@assistant-ui/mcp-docs-server 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -43
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -91
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +9 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +91 -0
- package/dist/proxy.js.map +1 -0
- package/package.json +8 -12
- package/src/index.ts +3 -115
- package/src/proxy.test.ts +385 -0
- package/src/proxy.ts +126 -0
- package/.docs/organized/code-examples/waterfall.md +0 -807
- package/.docs/organized/code-examples/with-a2a.md +0 -675
- package/.docs/organized/code-examples/with-ag-ui.md +0 -553
- package/.docs/organized/code-examples/with-ai-sdk-v7.md +0 -466
- package/.docs/organized/code-examples/with-artifacts.md +0 -815
- package/.docs/organized/code-examples/with-assistant-transport.md +0 -576
- package/.docs/organized/code-examples/with-browser-extension.md +0 -371
- package/.docs/organized/code-examples/with-chain-of-thought.md +0 -962
- package/.docs/organized/code-examples/with-cloud-standalone.md +0 -681
- package/.docs/organized/code-examples/with-cloud.md +0 -439
- package/.docs/organized/code-examples/with-custom-thread-list.md +0 -569
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +0 -527
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +0 -697
- package/.docs/organized/code-examples/with-eve.md +0 -405
- package/.docs/organized/code-examples/with-expo.md +0 -2246
- package/.docs/organized/code-examples/with-external-store.md +0 -421
- package/.docs/organized/code-examples/with-ffmpeg.md +0 -829
- package/.docs/organized/code-examples/with-generative-ui.md +0 -1810
- package/.docs/organized/code-examples/with-google-adk.md +0 -368
- package/.docs/organized/code-examples/with-heat-graph.md +0 -304
- package/.docs/organized/code-examples/with-image-generation.md +0 -463
- package/.docs/organized/code-examples/with-interactables.md +0 -733
- package/.docs/organized/code-examples/with-langchain.md +0 -446
- package/.docs/organized/code-examples/with-langgraph.md +0 -855
- package/.docs/organized/code-examples/with-livekit.md +0 -643
- package/.docs/organized/code-examples/with-mcp.md +0 -782
- package/.docs/organized/code-examples/with-nuxt.md +0 -2428
- package/.docs/organized/code-examples/with-opencode.md +0 -1974
- package/.docs/organized/code-examples/with-openui.md +0 -450
- package/.docs/organized/code-examples/with-pi.md +0 -2084
- package/.docs/organized/code-examples/with-react-hook-form.md +0 -727
- package/.docs/organized/code-examples/with-react-ink-web.md +0 -740
- package/.docs/organized/code-examples/with-react-ink.md +0 -473
- package/.docs/organized/code-examples/with-react-router.md +0 -936
- package/.docs/organized/code-examples/with-resumable-stream.md +0 -668
- package/.docs/organized/code-examples/with-store.md +0 -679
- package/.docs/organized/code-examples/with-svelte.md +0 -415
- package/.docs/organized/code-examples/with-sveltekit.md +0 -1061
- package/.docs/organized/code-examples/with-tanstack.md +0 -812
- package/.docs/organized/code-examples/with-tap-runtime.md +0 -813
- package/.docs/organized/code-examples/with-virtualized-thread.md +0 -681
- package/.docs/organized/code-examples/with-vue.md +0 -408
- package/.docs/raw/docs/(getting-started)/architecture.mdx +0 -146
- package/.docs/raw/docs/(getting-started)/base-ui.mdx +0 -39
- package/.docs/raw/docs/(getting-started)/cli.mdx +0 -542
- package/.docs/raw/docs/(getting-started)/devtools.mdx +0 -79
- package/.docs/raw/docs/(getting-started)/index.mdx +0 -23
- package/.docs/raw/docs/(getting-started)/installation.mdx +0 -483
- package/.docs/raw/docs/(getting-started)/llm.mdx +0 -211
- package/.docs/raw/docs/(getting-started)/rtl.mdx +0 -78
- package/.docs/raw/docs/(reference)/api-reference/adapters/attachments.mdx +0 -36
- package/.docs/raw/docs/(reference)/api-reference/adapters/feedback.mdx +0 -20
- package/.docs/raw/docs/(reference)/api-reference/adapters/index.mdx +0 -34
- package/.docs/raw/docs/(reference)/api-reference/adapters/model.mdx +0 -44
- package/.docs/raw/docs/(reference)/api-reference/adapters/persistence.mdx +0 -59
- package/.docs/raw/docs/(reference)/api-reference/adapters/runtime.mdx +0 -20
- package/.docs/raw/docs/(reference)/api-reference/adapters/suggestions.mdx +0 -26
- package/.docs/raw/docs/(reference)/api-reference/context-providers/assistant-runtime-provider.mdx +0 -84
- package/.docs/raw/docs/(reference)/api-reference/context-providers/index.mdx +0 -22
- package/.docs/raw/docs/(reference)/api-reference/context-providers/scoped-providers.mdx +0 -64
- package/.docs/raw/docs/(reference)/api-reference/external-store/index.mdx +0 -22
- package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +0 -57
- package/.docs/raw/docs/(reference)/api-reference/external-store/runtime.mdx +0 -49
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/a2ui.mdx +0 -40
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/actions.mdx +0 -56
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/components.mdx +0 -86
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/index.mdx +0 -39
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/json-generative-ui.mdx +0 -42
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/rendering.mdx +0 -84
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/slack.mdx +0 -85
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/spec.mdx +0 -45
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/teams.mdx +0 -93
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/tokens.mdx +0 -62
- package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +0 -129
- package/.docs/raw/docs/(reference)/api-reference/hooks/index.mdx +0 -31
- package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +0 -34
- package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +0 -350
- package/.docs/raw/docs/(reference)/api-reference/hooks/runtimes.mdx +0 -28
- package/.docs/raw/docs/(reference)/api-reference/hooks/state.mdx +0 -97
- package/.docs/raw/docs/(reference)/api-reference/integrations/ai-sdk.mdx +0 -200
- package/.docs/raw/docs/(reference)/api-reference/integrations/cloud-ai-sdk.mdx +0 -24
- package/.docs/raw/docs/(reference)/api-reference/integrations/eve.mdx +0 -118
- package/.docs/raw/docs/(reference)/api-reference/integrations/index.mdx +0 -28
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-data-stream.mdx +0 -37
- package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +0 -50
- package/.docs/raw/docs/(reference)/api-reference/model-context/index.mdx +0 -22
- package/.docs/raw/docs/(reference)/api-reference/model-context/registry.mdx +0 -20
- package/.docs/raw/docs/(reference)/api-reference/overview.mdx +0 -566
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar-more.mdx +0 -152
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +0 -239
- package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +0 -208
- package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-modal.mdx +0 -108
- package/.docs/raw/docs/(reference)/api-reference/primitives/attachment.mdx +0 -99
- package/.docs/raw/docs/(reference)/api-reference/primitives/branch-picker.mdx +0 -145
- package/.docs/raw/docs/(reference)/api-reference/primitives/chain-of-thought.mdx +0 -82
- package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +0 -637
- package/.docs/raw/docs/(reference)/api-reference/primitives/composition.mdx +0 -22
- package/.docs/raw/docs/(reference)/api-reference/primitives/error.mdx +0 -65
- package/.docs/raw/docs/(reference)/api-reference/primitives/index.mdx +0 -73
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +0 -117
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +0 -339
- package/.docs/raw/docs/(reference)/api-reference/primitives/queue-item.mdx +0 -77
- package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +0 -88
- package/.docs/raw/docs/(reference)/api-reference/primitives/suggestion.mdx +0 -81
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread-list-item-more.mdx +0 -129
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread-list-item.mdx +0 -138
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread-list.mdx +0 -122
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +0 -289
- package/.docs/raw/docs/(reference)/api-reference/runtimes/assistant-runtime.mdx +0 -20
- package/.docs/raw/docs/(reference)/api-reference/runtimes/attachment-runtime.mdx +0 -24
- package/.docs/raw/docs/(reference)/api-reference/runtimes/composer-runtime.mdx +0 -32
- package/.docs/raw/docs/(reference)/api-reference/runtimes/index.mdx +0 -43
- package/.docs/raw/docs/(reference)/api-reference/runtimes/message-part-runtime.mdx +0 -43
- package/.docs/raw/docs/(reference)/api-reference/runtimes/message-runtime.mdx +0 -24
- package/.docs/raw/docs/(reference)/api-reference/runtimes/queue-state.mdx +0 -20
- package/.docs/raw/docs/(reference)/api-reference/runtimes/thread-list-item-runtime.mdx +0 -24
- package/.docs/raw/docs/(reference)/api-reference/runtimes/thread-list-runtime.mdx +0 -24
- package/.docs/raw/docs/(reference)/api-reference/runtimes/thread-runtime.mdx +0 -36
- package/.docs/raw/docs/(reference)/api-reference/tools/component-tools.mdx +0 -85
- package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +0 -45
- package/.docs/raw/docs/(reference)/api-reference/tools/interactables-legacy.mdx +0 -55
- package/.docs/raw/docs/(reference)/api-reference/tools/interactables.mdx +0 -151
- package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +0 -106
- package/.docs/raw/docs/(reference)/api-reference/tools/status.mdx +0 -62
- package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +0 -178
- package/.docs/raw/docs/(reference)/api-reference/transport/assistant-transport.mdx +0 -48
- package/.docs/raw/docs/(reference)/api-reference/transport/frame.mdx +0 -62
- package/.docs/raw/docs/(reference)/api-reference/transport/index.mdx +0 -22
- package/.docs/raw/docs/(reference)/api-reference/utilities/index.mdx +0 -19
- package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +0 -152
- package/.docs/raw/docs/(reference)/api-reference/voice/index.mdx +0 -22
- package/.docs/raw/docs/(reference)/api-reference/voice/session.mdx +0 -54
- package/.docs/raw/docs/(reference)/api-reference/voice/speech-dictation.mdx +0 -36
- package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +0 -527
- package/.docs/raw/docs/cloud/ai-sdk.mdx +0 -604
- package/.docs/raw/docs/cloud/authorization.mdx +0 -252
- package/.docs/raw/docs/cloud/index.mdx +0 -46
- package/.docs/raw/docs/cloud/langgraph.mdx +0 -554
- package/.docs/raw/docs/copilots/assistant-frame.mdx +0 -411
- package/.docs/raw/docs/copilots/make-assistant-visible.mdx +0 -82
- package/.docs/raw/docs/copilots/model-context.mdx +0 -156
- package/.docs/raw/docs/copilots/motivation.mdx +0 -202
- package/.docs/raw/docs/copilots/use-assistant-instructions.mdx +0 -64
- package/.docs/raw/docs/guides/attachments.mdx +0 -619
- package/.docs/raw/docs/guides/branching.mdx +0 -76
- package/.docs/raw/docs/guides/chain-of-thought.mdx +0 -164
- package/.docs/raw/docs/guides/chatgpt-subscription.mdx +0 -108
- package/.docs/raw/docs/guides/context-api.mdx +0 -619
- package/.docs/raw/docs/guides/dictation.mdx +0 -298
- package/.docs/raw/docs/guides/editing.mdx +0 -102
- package/.docs/raw/docs/guides/electron.mdx +0 -369
- package/.docs/raw/docs/guides/headless-composer-input.mdx +0 -113
- package/.docs/raw/docs/guides/image-generation.mdx +0 -74
- package/.docs/raw/docs/guides/index.mdx +0 -117
- package/.docs/raw/docs/guides/input-history.mdx +0 -55
- package/.docs/raw/docs/guides/latex.mdx +0 -158
- package/.docs/raw/docs/guides/mentions.mdx +0 -575
- package/.docs/raw/docs/guides/message-timing.mdx +0 -215
- package/.docs/raw/docs/guides/quoting.mdx +0 -169
- package/.docs/raw/docs/guides/resumable-stream-deployment.mdx +0 -212
- package/.docs/raw/docs/guides/resumable-stream-stores.mdx +0 -152
- package/.docs/raw/docs/guides/resumable-streams.mdx +0 -292
- package/.docs/raw/docs/guides/slash-commands.mdx +0 -358
- package/.docs/raw/docs/guides/speech.mdx +0 -172
- package/.docs/raw/docs/guides/suggestions.mdx +0 -384
- package/.docs/raw/docs/guides/virtualization.mdx +0 -133
- package/.docs/raw/docs/guides/voice.mdx +0 -310
- package/.docs/raw/docs/ink/adapters.mdx +0 -99
- package/.docs/raw/docs/ink/custom-backend.mdx +0 -254
- package/.docs/raw/docs/ink/hooks.mdx +0 -469
- package/.docs/raw/docs/ink/index.mdx +0 -237
- package/.docs/raw/docs/ink/migration.mdx +0 -138
- package/.docs/raw/docs/ink/primitives.mdx +0 -1259
- package/.docs/raw/docs/integrations/attachments/custom-adapter.mdx +0 -528
- package/.docs/raw/docs/integrations/auth/better-auth.mdx +0 -188
- package/.docs/raw/docs/integrations/auth/clerk.mdx +0 -166
- package/.docs/raw/docs/integrations/auth/next-auth.mdx +0 -191
- package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +0 -88
- package/.docs/raw/docs/integrations/frameworks/cloudflare-agents.mdx +0 -283
- package/.docs/raw/docs/integrations/frameworks/mastra/full-stack.mdx +0 -188
- package/.docs/raw/docs/integrations/frameworks/mastra/overview.mdx +0 -57
- package/.docs/raw/docs/integrations/frameworks/mastra/separate-server.mdx +0 -201
- package/.docs/raw/docs/integrations/gateways/index.mdx +0 -162
- package/.docs/raw/docs/integrations/index.mdx +0 -178
- package/.docs/raw/docs/integrations/observability/helicone.mdx +0 -129
- package/.docs/raw/docs/integrations/observability/langfuse.mdx +0 -161
- package/.docs/raw/docs/integrations/observability/langsmith.mdx +0 -151
- package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +0 -773
- package/.docs/raw/docs/migrations/deprecation-policy.mdx +0 -42
- package/.docs/raw/docs/migrations/index.mdx +0 -50
- package/.docs/raw/docs/migrations/react-compatibility.mdx +0 -74
- package/.docs/raw/docs/migrations/react-langgraph-v0-7.mdx +0 -328
- package/.docs/raw/docs/migrations/toolkit-tools.mdx +0 -236
- package/.docs/raw/docs/migrations/v0-11.mdx +0 -172
- package/.docs/raw/docs/migrations/v0-12.mdx +0 -302
- package/.docs/raw/docs/migrations/v0-14.mdx +0 -297
- package/.docs/raw/docs/migrations/v0-15.mdx +0 -270
- package/.docs/raw/docs/primitives/action-bar.mdx +0 -352
- package/.docs/raw/docs/primitives/assistant-modal.mdx +0 -216
- package/.docs/raw/docs/primitives/attachment.mdx +0 -217
- package/.docs/raw/docs/primitives/branch-picker.mdx +0 -222
- package/.docs/raw/docs/primitives/chain-of-thought.mdx +0 -315
- package/.docs/raw/docs/primitives/composer.mdx +0 -659
- package/.docs/raw/docs/primitives/error.mdx +0 -142
- package/.docs/raw/docs/primitives/index.mdx +0 -99
- package/.docs/raw/docs/primitives/message.mdx +0 -618
- package/.docs/raw/docs/primitives/selection-toolbar.mdx +0 -191
- package/.docs/raw/docs/primitives/suggestion.mdx +0 -254
- package/.docs/raw/docs/primitives/thread-list.mdx +0 -467
- package/.docs/raw/docs/primitives/thread.mdx +0 -509
- package/.docs/raw/docs/react-native/adapters.mdx +0 -94
- package/.docs/raw/docs/react-native/custom-backend.mdx +0 -207
- package/.docs/raw/docs/react-native/hooks.mdx +0 -339
- package/.docs/raw/docs/react-native/index.mdx +0 -289
- package/.docs/raw/docs/react-native/migration.mdx +0 -142
- package/.docs/raw/docs/react-native/primitives.mdx +0 -976
- package/.docs/raw/docs/runtimes/a2a/client-and-hooks.mdx +0 -399
- package/.docs/raw/docs/runtimes/a2a/overview.mdx +0 -60
- package/.docs/raw/docs/runtimes/a2a/quickstart.mdx +0 -216
- package/.docs/raw/docs/runtimes/ag-ui/agent-state.mdx +0 -124
- package/.docs/raw/docs/runtimes/ag-ui/overview.mdx +0 -70
- package/.docs/raw/docs/runtimes/ag-ui/quickstart.mdx +0 -243
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +0 -267
- package/.docs/raw/docs/runtimes/ai-sdk/overview.mdx +0 -61
- package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +0 -122
- package/.docs/raw/docs/runtimes/ai-sdk/v5-legacy.mdx +0 -135
- package/.docs/raw/docs/runtimes/ai-sdk/v6-legacy.mdx +0 -636
- package/.docs/raw/docs/runtimes/ai-sdk/v7.mdx +0 -731
- package/.docs/raw/docs/runtimes/claude-managed-agents.mdx +0 -118
- package/.docs/raw/docs/runtimes/concepts/adapters.mdx +0 -265
- package/.docs/raw/docs/runtimes/concepts/architecture.mdx +0 -167
- package/.docs/raw/docs/runtimes/concepts/stability.mdx +0 -68
- package/.docs/raw/docs/runtimes/concepts/threads.mdx +0 -513
- package/.docs/raw/docs/runtimes/custom/assistant-transport.mdx +0 -763
- package/.docs/raw/docs/runtimes/custom/data-stream.mdx +0 -339
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +0 -957
- package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +0 -902
- package/.docs/raw/docs/runtimes/custom/overview.mdx +0 -71
- package/.docs/raw/docs/runtimes/eve/overview.mdx +0 -151
- package/.docs/raw/docs/runtimes/eve/quickstart.mdx +0 -192
- package/.docs/raw/docs/runtimes/google-adk/api.mdx +0 -256
- package/.docs/raw/docs/runtimes/google-adk/hooks.mdx +0 -784
- package/.docs/raw/docs/runtimes/google-adk/overview.mdx +0 -69
- package/.docs/raw/docs/runtimes/google-adk/quickstart.mdx +0 -229
- package/.docs/raw/docs/runtimes/langchain.mdx +0 -901
- package/.docs/raw/docs/runtimes/langgraph/agent-state.mdx +0 -181
- package/.docs/raw/docs/runtimes/langgraph/generative-ui.mdx +0 -305
- package/.docs/raw/docs/runtimes/langgraph/interrupts.mdx +0 -104
- package/.docs/raw/docs/runtimes/langgraph/overview.mdx +0 -84
- package/.docs/raw/docs/runtimes/langgraph/quickstart.mdx +0 -496
- package/.docs/raw/docs/runtimes/langgraph/streaming.mdx +0 -144
- package/.docs/raw/docs/runtimes/langgraph/threads.mdx +0 -113
- package/.docs/raw/docs/runtimes/langgraph/tutorial/introduction.mdx +0 -29
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +0 -93
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +0 -347
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +0 -393
- package/.docs/raw/docs/runtimes/opencode/hooks.mdx +0 -194
- package/.docs/raw/docs/runtimes/opencode/overview.mdx +0 -58
- package/.docs/raw/docs/runtimes/opencode/quickstart.mdx +0 -119
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +0 -133
- package/.docs/raw/docs/tools/a2ui.mdx +0 -107
- package/.docs/raw/docs/tools/backend.mdx +0 -147
- package/.docs/raw/docs/tools/defining-tools.mdx +0 -574
- package/.docs/raw/docs/tools/dynamic-tools.mdx +0 -112
- package/.docs/raw/docs/tools/generative-ui-primitive.mdx +0 -180
- package/.docs/raw/docs/tools/generative-ui-slack.mdx +0 -167
- package/.docs/raw/docs/tools/generative-ui-teams.mdx +0 -160
- package/.docs/raw/docs/tools/generative-ui.mdx +0 -322
- package/.docs/raw/docs/tools/index.mdx +0 -72
- package/.docs/raw/docs/tools/interactables.mdx +0 -1084
- package/.docs/raw/docs/tools/mcp-apps.mdx +0 -363
- package/.docs/raw/docs/tools/mcp.mdx +0 -466
- package/.docs/raw/docs/tools/multi-agent.mdx +0 -237
- package/.docs/raw/docs/tools/openui.mdx +0 -175
- package/.docs/raw/docs/tools/tool-ui.mdx +0 -1020
- package/.docs/raw/docs/tools/user-managed-mcp.mdx +0 -428
- package/.docs/raw/docs/ui/assistant-modal.mdx +0 -166
- package/.docs/raw/docs/ui/assistant-sidebar.mdx +0 -89
- package/.docs/raw/docs/ui/attachment.mdx +0 -279
- package/.docs/raw/docs/ui/composer-trigger-popover.mdx +0 -247
- package/.docs/raw/docs/ui/context-display.mdx +0 -148
- package/.docs/raw/docs/ui/directive-text.mdx +0 -114
- package/.docs/raw/docs/ui/file.mdx +0 -159
- package/.docs/raw/docs/ui/follow-up-suggestions.mdx +0 -82
- package/.docs/raw/docs/ui/image.mdx +0 -102
- package/.docs/raw/docs/ui/markdown.mdx +0 -103
- package/.docs/raw/docs/ui/mcp-config.mdx +0 -107
- package/.docs/raw/docs/ui/mermaid.mdx +0 -92
- package/.docs/raw/docs/ui/message-timing.mdx +0 -93
- package/.docs/raw/docs/ui/model-selector.mdx +0 -455
- package/.docs/raw/docs/ui/part-grouping.mdx +0 -614
- package/.docs/raw/docs/ui/quote.mdx +0 -211
- package/.docs/raw/docs/ui/reasoning.mdx +0 -214
- package/.docs/raw/docs/ui/scrollbar.mdx +0 -77
- package/.docs/raw/docs/ui/sources.mdx +0 -186
- package/.docs/raw/docs/ui/streamdown.mdx +0 -392
- package/.docs/raw/docs/ui/syntax-highlighting.mdx +0 -205
- package/.docs/raw/docs/ui/thread-list.mdx +0 -411
- package/.docs/raw/docs/ui/thread.mdx +0 -559
- package/.docs/raw/docs/ui/tool-fallback.mdx +0 -141
- package/.docs/raw/docs/ui/tool-group.mdx +0 -240
- package/.docs/raw/docs/ui/voice.mdx +0 -173
- package/.docs/raw/docs/utilities/heat-graph.mdx +0 -237
- package/.docs/raw/docs/utilities/react-o11y.mdx +0 -362
- package/.docs/raw/docs/utilities/tw-shimmer.mdx +0 -212
- package/dist/constants.d.ts +0 -14
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -19
- package/dist/constants.js.map +0 -1
- package/dist/prepare-docs/code-examples.d.ts +0 -5
- package/dist/prepare-docs/code-examples.d.ts.map +0 -1
- package/dist/prepare-docs/code-examples.js +0 -117
- package/dist/prepare-docs/code-examples.js.map +0 -1
- package/dist/prepare-docs/copy-raw.d.ts +0 -5
- package/dist/prepare-docs/copy-raw.d.ts.map +0 -1
- package/dist/prepare-docs/copy-raw.js +0 -49
- package/dist/prepare-docs/copy-raw.js.map +0 -1
- package/dist/prepare-docs/prepare.d.ts +0 -1
- package/dist/prepare-docs/prepare.js +0 -22
- package/dist/prepare-docs/prepare.js.map +0 -1
- package/dist/prompts/xulux-playground.d.ts +0 -12
- package/dist/prompts/xulux-playground.d.ts.map +0 -1
- package/dist/prompts/xulux-playground.js +0 -33
- package/dist/prompts/xulux-playground.js.map +0 -1
- package/dist/tools/docs.d.ts +0 -22
- package/dist/tools/docs.d.ts.map +0 -1
- package/dist/tools/docs.js +0 -177
- package/dist/tools/docs.js.map +0 -1
- package/dist/tools/examples.d.ts +0 -24
- package/dist/tools/examples.d.ts.map +0 -1
- package/dist/tools/examples.js +0 -87
- package/dist/tools/examples.js.map +0 -1
- package/dist/tools/resources.d.ts +0 -6
- package/dist/tools/resources.d.ts.map +0 -1
- package/dist/tools/resources.js +0 -75
- package/dist/tools/resources.js.map +0 -1
- package/dist/tools/search.d.ts +0 -24
- package/dist/tools/search.d.ts.map +0 -1
- package/dist/tools/search.js +0 -39
- package/dist/tools/search.js.map +0 -1
- package/dist/tools/tests/mcp-test-client.d.ts +0 -15
- package/dist/tools/tests/mcp-test-client.d.ts.map +0 -1
- package/dist/tools/tests/mcp-test-client.js +0 -68
- package/dist/tools/tests/mcp-test-client.js.map +0 -1
- package/dist/tools/tests/test-setup.d.ts +0 -7
- package/dist/tools/tests/test-setup.d.ts.map +0 -1
- package/dist/tools/tests/test-setup.js +0 -36
- package/dist/tools/tests/test-setup.js.map +0 -1
- package/dist/tools/xulux-templates.d.ts +0 -58
- package/dist/tools/xulux-templates.d.ts.map +0 -1
- package/dist/tools/xulux-templates.js +0 -84
- package/dist/tools/xulux-templates.js.map +0 -1
- package/dist/utils/cache.d.ts +0 -5
- package/dist/utils/cache.d.ts.map +0 -1
- package/dist/utils/cache.js +0 -18
- package/dist/utils/cache.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -10
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -20
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/mcp-format.d.ts +0 -11
- package/dist/utils/mcp-format.d.ts.map +0 -1
- package/dist/utils/mcp-format.js +0 -14
- package/dist/utils/mcp-format.js.map +0 -1
- package/dist/utils/mdx.d.ts +0 -12
- package/dist/utils/mdx.d.ts.map +0 -1
- package/dist/utils/mdx.js +0 -45
- package/dist/utils/mdx.js.map +0 -1
- package/dist/utils/paths.d.ts +0 -12
- package/dist/utils/paths.d.ts.map +0 -1
- package/dist/utils/paths.js +0 -93
- package/dist/utils/paths.js.map +0 -1
- package/dist/utils/search.d.ts +0 -10
- package/dist/utils/search.d.ts.map +0 -1
- package/dist/utils/search.js +0 -97
- package/dist/utils/search.js.map +0 -1
- package/dist/utils/security.d.ts +0 -5
- package/dist/utils/security.d.ts.map +0 -1
- package/dist/utils/security.js +0 -22
- package/dist/utils/security.js.map +0 -1
- package/dist/xulux/catalog-client.d.ts +0 -14
- package/dist/xulux/catalog-client.d.ts.map +0 -1
- package/dist/xulux/catalog-client.js +0 -112
- package/dist/xulux/catalog-client.js.map +0 -1
- package/dist/xulux/fallback-catalog.d.ts +0 -7
- package/dist/xulux/fallback-catalog.d.ts.map +0 -1
- package/dist/xulux/fallback-catalog.js +0 -47
- package/dist/xulux/fallback-catalog.js.map +0 -1
- package/dist/xulux/fetch-sandbox.d.ts +0 -5
- package/dist/xulux/fetch-sandbox.d.ts.map +0 -1
- package/dist/xulux/fetch-sandbox.js +0 -40
- package/dist/xulux/fetch-sandbox.js.map +0 -1
- package/dist/xulux/template-service.d.ts +0 -84
- package/dist/xulux/template-service.d.ts.map +0 -1
- package/dist/xulux/template-service.js +0 -223
- package/dist/xulux/template-service.js.map +0 -1
- package/dist/xulux/types.d.ts +0 -55
- package/dist/xulux/types.d.ts.map +0 -1
- package/dist/xulux/types.js +0 -6
- package/dist/xulux/types.js.map +0 -1
- package/src/constants.ts +0 -24
- package/src/prepare-docs/code-examples.ts +0 -158
- package/src/prepare-docs/copy-raw.ts +0 -50
- package/src/prepare-docs/prepare.ts +0 -24
- package/src/prompts/xulux-playground.ts +0 -36
- package/src/tools/docs.ts +0 -255
- package/src/tools/examples.ts +0 -114
- package/src/tools/resources.ts +0 -111
- package/src/tools/search.ts +0 -46
- package/src/tools/tests/completions.test.ts +0 -60
- package/src/tools/tests/directory-size-cap.test.ts +0 -50
- package/src/tools/tests/docs.test.ts +0 -147
- package/src/tools/tests/examples.test.ts +0 -94
- package/src/tools/tests/integration.test.ts +0 -45
- package/src/tools/tests/json-parsing.test.ts +0 -23
- package/src/tools/tests/listings-cache.test.ts +0 -19
- package/src/tools/tests/mcp-protocol.test.ts +0 -216
- package/src/tools/tests/mcp-test-client.ts +0 -111
- package/src/tools/tests/path-traversal.test.ts +0 -84
- package/src/tools/tests/resources.test.ts +0 -133
- package/src/tools/tests/search.test.ts +0 -37
- package/src/tools/tests/test-setup.ts +0 -50
- package/src/tools/tests/xulux-templates.test.ts +0 -325
- package/src/tools/xulux-templates.ts +0 -141
- package/src/utils/cache.ts +0 -20
- package/src/utils/logger.ts +0 -20
- package/src/utils/mcp-format.ts +0 -14
- package/src/utils/mdx.ts +0 -59
- package/src/utils/paths.ts +0 -139
- package/src/utils/search.ts +0 -131
- package/src/utils/security.ts +0 -52
- package/src/utils/tests/cache.test.ts +0 -51
- package/src/utils/tests/mcp-format.test.ts +0 -22
- package/src/utils/tests/security.test.ts +0 -119
- package/src/xulux/catalog-client.ts +0 -150
- package/src/xulux/fallback-catalog.ts +0 -63
- package/src/xulux/fetch-sandbox.ts +0 -56
- package/src/xulux/template-service.ts +0 -406
- package/src/xulux/types.ts +0 -64
|
@@ -1,2246 +0,0 @@
|
|
|
1
|
-
# Example: with-expo
|
|
2
|
-
|
|
3
|
-
## .vscode/extensions.json
|
|
4
|
-
|
|
5
|
-
```json
|
|
6
|
-
{ "recommendations": ["expo.vscode-expo-tools"] }
|
|
7
|
-
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
## .vscode/settings.json
|
|
11
|
-
|
|
12
|
-
```json
|
|
13
|
-
{
|
|
14
|
-
"editor.codeActionsOnSave": {
|
|
15
|
-
"source.fixAll": "explicit",
|
|
16
|
-
"source.organizeImports": "explicit",
|
|
17
|
-
"source.sortMembers": "explicit"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## app.json
|
|
24
|
-
|
|
25
|
-
```json
|
|
26
|
-
{
|
|
27
|
-
"expo": {
|
|
28
|
-
"name": "with-expo",
|
|
29
|
-
"slug": "with-expo",
|
|
30
|
-
"version": "0.0.0",
|
|
31
|
-
"orientation": "portrait",
|
|
32
|
-
"icon": "./assets/images/icon.png",
|
|
33
|
-
"scheme": "withexpo",
|
|
34
|
-
"userInterfaceStyle": "automatic",
|
|
35
|
-
"newArchEnabled": true,
|
|
36
|
-
"ios": {
|
|
37
|
-
"supportsTablet": true,
|
|
38
|
-
"bundleIdentifier": "com.assistant-ui.with-expo"
|
|
39
|
-
},
|
|
40
|
-
"android": {
|
|
41
|
-
"adaptiveIcon": {
|
|
42
|
-
"backgroundColor": "#E6F4FE",
|
|
43
|
-
"foregroundImage": "./assets/images/android-icon-foreground.png",
|
|
44
|
-
"backgroundImage": "./assets/images/android-icon-background.png",
|
|
45
|
-
"monochromeImage": "./assets/images/android-icon-monochrome.png"
|
|
46
|
-
},
|
|
47
|
-
"edgeToEdgeEnabled": true,
|
|
48
|
-
"predictiveBackGestureEnabled": false
|
|
49
|
-
},
|
|
50
|
-
"web": {
|
|
51
|
-
"output": "static",
|
|
52
|
-
"favicon": "./assets/images/favicon.png"
|
|
53
|
-
},
|
|
54
|
-
"plugins": [
|
|
55
|
-
"expo-router",
|
|
56
|
-
[
|
|
57
|
-
"expo-splash-screen",
|
|
58
|
-
{
|
|
59
|
-
"image": "./assets/images/splash-icon.png",
|
|
60
|
-
"imageWidth": 200,
|
|
61
|
-
"resizeMode": "contain",
|
|
62
|
-
"backgroundColor": "#ffffff",
|
|
63
|
-
"dark": {
|
|
64
|
-
"backgroundColor": "#18181b"
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
],
|
|
68
|
-
"expo-font",
|
|
69
|
-
"expo-image",
|
|
70
|
-
"expo-status-bar"
|
|
71
|
-
],
|
|
72
|
-
"experiments": {
|
|
73
|
-
"typedRoutes": true,
|
|
74
|
-
"reactCompiler": true
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## app/_layout.tsx
|
|
82
|
-
|
|
83
|
-
```tsx
|
|
84
|
-
import {
|
|
85
|
-
DarkTheme,
|
|
86
|
-
DefaultTheme,
|
|
87
|
-
ThemeProvider,
|
|
88
|
-
type Theme,
|
|
89
|
-
} from "@react-navigation/native";
|
|
90
|
-
import { Drawer } from "expo-router/drawer";
|
|
91
|
-
import { StatusBar } from "expo-status-bar";
|
|
92
|
-
import "react-native-reanimated";
|
|
93
|
-
import { Platform, Pressable } from "react-native";
|
|
94
|
-
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
|
95
|
-
import { useFonts } from "expo-font";
|
|
96
|
-
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
97
|
-
|
|
98
|
-
import {
|
|
99
|
-
AssistantRuntimeProvider,
|
|
100
|
-
AuiConfig,
|
|
101
|
-
useAui,
|
|
102
|
-
Tools,
|
|
103
|
-
} from "@assistant-ui/react-native";
|
|
104
|
-
import { useAppRuntime } from "@/hooks/use-app-runtime";
|
|
105
|
-
import { useTheme } from "@/hooks/use-theme";
|
|
106
|
-
import { Icon } from "@/components/ui/icon";
|
|
107
|
-
import { ThreadListDrawer } from "@/components/thread-list/ThreadListDrawer";
|
|
108
|
-
import { haptics } from "@/lib/haptics";
|
|
109
|
-
import toolkit from "@/components/assistant-ui/tools";
|
|
110
|
-
|
|
111
|
-
function NewChatButton() {
|
|
112
|
-
const aui = useAui();
|
|
113
|
-
const { colors } = useTheme();
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<Pressable
|
|
117
|
-
accessibilityLabel="New chat"
|
|
118
|
-
accessibilityRole="button"
|
|
119
|
-
hitSlop={8}
|
|
120
|
-
onPress={() => {
|
|
121
|
-
haptics.selection();
|
|
122
|
-
aui.threads.switchToNewThread();
|
|
123
|
-
}}
|
|
124
|
-
style={{ marginRight: 16 }}
|
|
125
|
-
>
|
|
126
|
-
<Icon name="compose" size={22} color={colors.foreground} />
|
|
127
|
-
</Pressable>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function DrawerLayout() {
|
|
132
|
-
const { isDark, colors } = useTheme();
|
|
133
|
-
|
|
134
|
-
const base = isDark ? DarkTheme : DefaultTheme;
|
|
135
|
-
const navTheme: Theme = {
|
|
136
|
-
...base,
|
|
137
|
-
colors: {
|
|
138
|
-
...base.colors,
|
|
139
|
-
background: colors.background,
|
|
140
|
-
card: colors.background,
|
|
141
|
-
text: colors.foreground,
|
|
142
|
-
border: colors.border,
|
|
143
|
-
primary: colors.foreground,
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
return (
|
|
148
|
-
<ThemeProvider value={navTheme}>
|
|
149
|
-
<Drawer
|
|
150
|
-
drawerContent={(props) => <ThreadListDrawer {...props} />}
|
|
151
|
-
screenOptions={{
|
|
152
|
-
headerRight: () => <NewChatButton />,
|
|
153
|
-
headerShadowVisible: false,
|
|
154
|
-
headerTintColor: colors.foreground,
|
|
155
|
-
headerStyle: { backgroundColor: colors.background },
|
|
156
|
-
headerTitleStyle: { fontWeight: "600" },
|
|
157
|
-
drawerType: "front",
|
|
158
|
-
swipeEnabled: true,
|
|
159
|
-
drawerStyle: { backgroundColor: colors.background },
|
|
160
|
-
}}
|
|
161
|
-
>
|
|
162
|
-
<Drawer.Screen name="index" options={{ title: "Chat" }} />
|
|
163
|
-
</Drawer>
|
|
164
|
-
<StatusBar style="auto" />
|
|
165
|
-
</ThemeProvider>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export default function RootLayout() {
|
|
170
|
-
// iOS renders SF Symbols, so it never needs the MaterialIcons glyph font.
|
|
171
|
-
const [fontsLoaded] = useFonts(
|
|
172
|
-
Platform.OS === "ios" ? {} : MaterialIcons.font,
|
|
173
|
-
);
|
|
174
|
-
const runtime = useAppRuntime();
|
|
175
|
-
const config = AuiConfig({
|
|
176
|
-
tools: Tools({ toolkit }),
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
if (!fontsLoaded) return null;
|
|
180
|
-
|
|
181
|
-
return (
|
|
182
|
-
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
183
|
-
<AssistantRuntimeProvider runtime={runtime} config={config}>
|
|
184
|
-
<DrawerLayout />
|
|
185
|
-
</AssistantRuntimeProvider>
|
|
186
|
-
</GestureHandlerRootView>
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
## app/api/chat+api.ts
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
import { frontendTools } from "@assistant-ui/ai-sdk";
|
|
196
|
-
import { openai } from "@ai-sdk/openai";
|
|
197
|
-
import {
|
|
198
|
-
convertToModelMessages,
|
|
199
|
-
pruneMessages,
|
|
200
|
-
stepCountIs,
|
|
201
|
-
streamText,
|
|
202
|
-
} from "ai";
|
|
203
|
-
|
|
204
|
-
export async function POST(req: Request) {
|
|
205
|
-
const body = await req.json();
|
|
206
|
-
const { messages, tools } = body;
|
|
207
|
-
|
|
208
|
-
const model = openai("gpt-5.6-luna");
|
|
209
|
-
|
|
210
|
-
const prunedMessages = pruneMessages({
|
|
211
|
-
messages: await convertToModelMessages(messages),
|
|
212
|
-
reasoning: "none",
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const result = streamText({
|
|
216
|
-
model,
|
|
217
|
-
messages: prunedMessages,
|
|
218
|
-
maxOutputTokens: 15000,
|
|
219
|
-
stopWhen: stepCountIs(10),
|
|
220
|
-
tools: frontendTools(tools),
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
return result.toUIMessageStreamResponse();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
## app/index.tsx
|
|
229
|
-
|
|
230
|
-
```tsx
|
|
231
|
-
import { Thread } from "@/components/assistant-ui/thread";
|
|
232
|
-
|
|
233
|
-
export default function ChatPage() {
|
|
234
|
-
return <Thread />;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
## babel.config.js
|
|
240
|
-
|
|
241
|
-
```javascript
|
|
242
|
-
const path = require("node:path");
|
|
243
|
-
|
|
244
|
-
module.exports = (api) => {
|
|
245
|
-
api.cache(true);
|
|
246
|
-
return {
|
|
247
|
-
presets: [
|
|
248
|
-
[
|
|
249
|
-
"babel-preset-expo",
|
|
250
|
-
{
|
|
251
|
-
"react-compiler": {
|
|
252
|
-
// Workspace packages resolve to paths outside node_modules, so the
|
|
253
|
-
// preset's node_modules exclusion does not cover them. Restrict the
|
|
254
|
-
// compiler to this app's own sources.
|
|
255
|
-
sources: (filename) => filename.startsWith(__dirname + path.sep),
|
|
256
|
-
},
|
|
257
|
-
},
|
|
258
|
-
],
|
|
259
|
-
],
|
|
260
|
-
};
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
## components/assistant-ui/composer.tsx
|
|
266
|
-
|
|
267
|
-
```tsx
|
|
268
|
-
import { View, Pressable, Platform, StyleSheet } from "react-native";
|
|
269
|
-
import { Image } from "expo-image";
|
|
270
|
-
import * as ImagePicker from "expo-image-picker";
|
|
271
|
-
import {
|
|
272
|
-
useAui,
|
|
273
|
-
useAuiState,
|
|
274
|
-
AuiIf,
|
|
275
|
-
ComposerPrimitive,
|
|
276
|
-
AttachmentPrimitive,
|
|
277
|
-
} from "@assistant-ui/react-native";
|
|
278
|
-
import { Icon } from "@/components/ui/icon";
|
|
279
|
-
import { useTheme } from "@/hooks/use-theme";
|
|
280
|
-
import { Radius, Spacing } from "@/constants/theme";
|
|
281
|
-
import { haptics } from "@/lib/haptics";
|
|
282
|
-
|
|
283
|
-
function AttachmentPreview() {
|
|
284
|
-
const { colors } = useTheme();
|
|
285
|
-
const attachment = useAuiState((s) => s.attachment);
|
|
286
|
-
if (!attachment) return null;
|
|
287
|
-
|
|
288
|
-
const imageContent = attachment.content?.find((c: any) => c.type === "image");
|
|
289
|
-
const uri = (imageContent as any)?.image;
|
|
290
|
-
|
|
291
|
-
return (
|
|
292
|
-
<AttachmentPrimitive.Root style={styles.attachmentItem}>
|
|
293
|
-
{uri ? (
|
|
294
|
-
<Image
|
|
295
|
-
source={{ uri }}
|
|
296
|
-
style={[styles.attachmentImage, { backgroundColor: colors.muted }]}
|
|
297
|
-
contentFit="cover"
|
|
298
|
-
transition={120}
|
|
299
|
-
/>
|
|
300
|
-
) : null}
|
|
301
|
-
<AttachmentPrimitive.Remove
|
|
302
|
-
onPressIn={haptics.light}
|
|
303
|
-
style={styles.attachmentRemove}
|
|
304
|
-
>
|
|
305
|
-
<View
|
|
306
|
-
style={[styles.removeBadge, { backgroundColor: colors.foreground }]}
|
|
307
|
-
>
|
|
308
|
-
<Icon name="remove" size={16} color={colors.background} />
|
|
309
|
-
</View>
|
|
310
|
-
</AttachmentPrimitive.Remove>
|
|
311
|
-
</AttachmentPrimitive.Root>
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function SendButton() {
|
|
316
|
-
const { colors } = useTheme();
|
|
317
|
-
const canSend = useAuiState((s) => s.composer.canSend);
|
|
318
|
-
|
|
319
|
-
return (
|
|
320
|
-
<ComposerPrimitive.Send
|
|
321
|
-
accessibilityLabel="Send message"
|
|
322
|
-
onPressIn={() => canSend && haptics.success()}
|
|
323
|
-
style={[
|
|
324
|
-
styles.actionButton,
|
|
325
|
-
{ backgroundColor: canSend ? colors.primary : colors.muted },
|
|
326
|
-
]}
|
|
327
|
-
>
|
|
328
|
-
<Icon
|
|
329
|
-
name="send"
|
|
330
|
-
size={18}
|
|
331
|
-
color={canSend ? colors.primaryForeground : colors.mutedForeground}
|
|
332
|
-
/>
|
|
333
|
-
</ComposerPrimitive.Send>
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function CancelButton() {
|
|
338
|
-
const { colors } = useTheme();
|
|
339
|
-
return (
|
|
340
|
-
<ComposerPrimitive.Cancel
|
|
341
|
-
accessibilityLabel="Stop generating"
|
|
342
|
-
onPressIn={haptics.light}
|
|
343
|
-
style={[styles.actionButton, { backgroundColor: colors.foreground }]}
|
|
344
|
-
>
|
|
345
|
-
<Icon name="stop" size={15} color={colors.background} />
|
|
346
|
-
</ComposerPrimitive.Cancel>
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function AttachButton() {
|
|
351
|
-
const { colors } = useTheme();
|
|
352
|
-
const aui = useAui();
|
|
353
|
-
|
|
354
|
-
const pickImage = async () => {
|
|
355
|
-
haptics.selection();
|
|
356
|
-
const result = await ImagePicker.launchImageLibraryAsync({
|
|
357
|
-
mediaTypes: ["images"],
|
|
358
|
-
allowsMultipleSelection: true,
|
|
359
|
-
quality: 0.8,
|
|
360
|
-
base64: true,
|
|
361
|
-
});
|
|
362
|
-
if (result.canceled) return;
|
|
363
|
-
|
|
364
|
-
for (const asset of result.assets) {
|
|
365
|
-
// iOS may report HEIC, which OpenAI rejects; normalize to JPEG.
|
|
366
|
-
const dataUrl = `data:image/jpeg;base64,${asset.base64}`;
|
|
367
|
-
await aui.composer.addAttachment({
|
|
368
|
-
name: asset.fileName ?? "image.jpg",
|
|
369
|
-
contentType: "image/jpeg",
|
|
370
|
-
type: "image",
|
|
371
|
-
content: [{ type: "image", image: dataUrl }],
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
return (
|
|
377
|
-
<Pressable
|
|
378
|
-
accessibilityLabel="Add image"
|
|
379
|
-
accessibilityRole="button"
|
|
380
|
-
onPress={pickImage}
|
|
381
|
-
hitSlop={6}
|
|
382
|
-
style={({ pressed }) => [
|
|
383
|
-
styles.attachButton,
|
|
384
|
-
{ backgroundColor: pressed ? colors.muted : "transparent" },
|
|
385
|
-
]}
|
|
386
|
-
>
|
|
387
|
-
<Icon name="add" size={20} color={colors.mutedForeground} />
|
|
388
|
-
</Pressable>
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export function Composer() {
|
|
393
|
-
const { colors } = useTheme();
|
|
394
|
-
const hasAttachments = useAuiState((s) => s.composer.attachments.length > 0);
|
|
395
|
-
|
|
396
|
-
return (
|
|
397
|
-
<View style={styles.container}>
|
|
398
|
-
<View
|
|
399
|
-
style={[
|
|
400
|
-
styles.shell,
|
|
401
|
-
{ backgroundColor: colors.composer, borderColor: colors.border },
|
|
402
|
-
]}
|
|
403
|
-
>
|
|
404
|
-
{hasAttachments && (
|
|
405
|
-
<View style={styles.attachmentsRow}>
|
|
406
|
-
<ComposerPrimitive.Attachments>
|
|
407
|
-
{() => <AttachmentPreview />}
|
|
408
|
-
</ComposerPrimitive.Attachments>
|
|
409
|
-
</View>
|
|
410
|
-
)}
|
|
411
|
-
|
|
412
|
-
<ComposerPrimitive.Input
|
|
413
|
-
style={[styles.input, { color: colors.foreground }]}
|
|
414
|
-
placeholder="Message…"
|
|
415
|
-
placeholderTextColor={colors.mutedForeground}
|
|
416
|
-
multiline
|
|
417
|
-
maxLength={4000}
|
|
418
|
-
/>
|
|
419
|
-
|
|
420
|
-
<View style={styles.actionRow}>
|
|
421
|
-
<AttachButton />
|
|
422
|
-
<AuiIf condition={(s) => !s.thread.isRunning}>
|
|
423
|
-
<SendButton />
|
|
424
|
-
</AuiIf>
|
|
425
|
-
<AuiIf condition={(s) => s.thread.isRunning}>
|
|
426
|
-
<CancelButton />
|
|
427
|
-
</AuiIf>
|
|
428
|
-
</View>
|
|
429
|
-
</View>
|
|
430
|
-
</View>
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const styles = StyleSheet.create({
|
|
435
|
-
container: {
|
|
436
|
-
width: "100%",
|
|
437
|
-
maxWidth: Spacing.threadMaxWidth + 24,
|
|
438
|
-
alignSelf: "center",
|
|
439
|
-
paddingHorizontal: 12,
|
|
440
|
-
paddingTop: 8,
|
|
441
|
-
},
|
|
442
|
-
shell: {
|
|
443
|
-
flexDirection: "column",
|
|
444
|
-
gap: 6,
|
|
445
|
-
borderRadius: Radius.composer,
|
|
446
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
447
|
-
padding: 8,
|
|
448
|
-
},
|
|
449
|
-
attachmentsRow: {
|
|
450
|
-
flexDirection: "row",
|
|
451
|
-
flexWrap: "wrap",
|
|
452
|
-
gap: 8,
|
|
453
|
-
paddingHorizontal: 4,
|
|
454
|
-
paddingTop: 4,
|
|
455
|
-
},
|
|
456
|
-
attachmentItem: {
|
|
457
|
-
position: "relative",
|
|
458
|
-
},
|
|
459
|
-
attachmentImage: {
|
|
460
|
-
width: 56,
|
|
461
|
-
height: 56,
|
|
462
|
-
borderRadius: Radius.attachment,
|
|
463
|
-
},
|
|
464
|
-
attachmentRemove: {
|
|
465
|
-
position: "absolute",
|
|
466
|
-
top: -6,
|
|
467
|
-
right: -6,
|
|
468
|
-
},
|
|
469
|
-
removeBadge: {
|
|
470
|
-
width: 20,
|
|
471
|
-
height: 20,
|
|
472
|
-
borderRadius: 10,
|
|
473
|
-
alignItems: "center",
|
|
474
|
-
justifyContent: "center",
|
|
475
|
-
},
|
|
476
|
-
input: {
|
|
477
|
-
fontSize: 16,
|
|
478
|
-
lineHeight: 22,
|
|
479
|
-
minHeight: 28,
|
|
480
|
-
maxHeight: 132,
|
|
481
|
-
paddingHorizontal: 10,
|
|
482
|
-
paddingTop: 6,
|
|
483
|
-
paddingBottom: 2,
|
|
484
|
-
...Platform.select({
|
|
485
|
-
web: { outlineStyle: "none" } as any,
|
|
486
|
-
default: {},
|
|
487
|
-
}),
|
|
488
|
-
},
|
|
489
|
-
actionRow: {
|
|
490
|
-
flexDirection: "row",
|
|
491
|
-
alignItems: "center",
|
|
492
|
-
justifyContent: "space-between",
|
|
493
|
-
},
|
|
494
|
-
attachButton: {
|
|
495
|
-
width: 32,
|
|
496
|
-
height: 32,
|
|
497
|
-
borderRadius: Radius.pill,
|
|
498
|
-
alignItems: "center",
|
|
499
|
-
justifyContent: "center",
|
|
500
|
-
},
|
|
501
|
-
actionButton: {
|
|
502
|
-
width: 32,
|
|
503
|
-
height: 32,
|
|
504
|
-
borderRadius: Radius.pill,
|
|
505
|
-
alignItems: "center",
|
|
506
|
-
justifyContent: "center",
|
|
507
|
-
},
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
## components/assistant-ui/message-action-bar.tsx
|
|
513
|
-
|
|
514
|
-
```tsx
|
|
515
|
-
import { View, StyleSheet } from "react-native";
|
|
516
|
-
import * as Clipboard from "expo-clipboard";
|
|
517
|
-
import { Icon } from "@/components/ui/icon";
|
|
518
|
-
import { ActionBarPrimitive } from "@assistant-ui/react-native";
|
|
519
|
-
import { useTheme } from "@/hooks/use-theme";
|
|
520
|
-
import { haptics } from "@/lib/haptics";
|
|
521
|
-
import { Radius } from "@/constants/theme";
|
|
522
|
-
|
|
523
|
-
const copyToClipboard = async (text: string) => {
|
|
524
|
-
await Clipboard.setStringAsync(text);
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
export function MessageActionBar() {
|
|
528
|
-
const { colors } = useTheme();
|
|
529
|
-
|
|
530
|
-
const buttonStyle = ({ pressed }: { pressed: boolean }) => [
|
|
531
|
-
styles.button,
|
|
532
|
-
pressed && { backgroundColor: colors.muted },
|
|
533
|
-
];
|
|
534
|
-
|
|
535
|
-
return (
|
|
536
|
-
<View style={styles.container}>
|
|
537
|
-
<ActionBarPrimitive.Copy
|
|
538
|
-
copyToClipboard={copyToClipboard}
|
|
539
|
-
onPressIn={haptics.selection}
|
|
540
|
-
style={buttonStyle}
|
|
541
|
-
>
|
|
542
|
-
{({ isCopied }) => (
|
|
543
|
-
<Icon
|
|
544
|
-
name={isCopied ? "check" : "copy"}
|
|
545
|
-
size={16}
|
|
546
|
-
color={isCopied ? colors.foreground : colors.mutedForeground}
|
|
547
|
-
/>
|
|
548
|
-
)}
|
|
549
|
-
</ActionBarPrimitive.Copy>
|
|
550
|
-
<ActionBarPrimitive.Reload
|
|
551
|
-
onPressIn={haptics.selection}
|
|
552
|
-
style={buttonStyle}
|
|
553
|
-
>
|
|
554
|
-
<Icon name="reload" size={16} color={colors.mutedForeground} />
|
|
555
|
-
</ActionBarPrimitive.Reload>
|
|
556
|
-
</View>
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const styles = StyleSheet.create({
|
|
561
|
-
container: {
|
|
562
|
-
flexDirection: "row",
|
|
563
|
-
alignItems: "center",
|
|
564
|
-
gap: 2,
|
|
565
|
-
},
|
|
566
|
-
button: {
|
|
567
|
-
padding: 6,
|
|
568
|
-
borderRadius: Radius.sm,
|
|
569
|
-
},
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
## components/assistant-ui/message-branch-picker.tsx
|
|
575
|
-
|
|
576
|
-
```tsx
|
|
577
|
-
import { View, Text, StyleSheet } from "react-native";
|
|
578
|
-
import { Icon } from "@/components/ui/icon";
|
|
579
|
-
import { BranchPickerPrimitive, useAuiState } from "@assistant-ui/react-native";
|
|
580
|
-
import { useTheme } from "@/hooks/use-theme";
|
|
581
|
-
|
|
582
|
-
export function MessageBranchPicker({
|
|
583
|
-
align = "flex-start",
|
|
584
|
-
}: {
|
|
585
|
-
align?: "flex-start" | "flex-end";
|
|
586
|
-
}) {
|
|
587
|
-
const { colors } = useTheme();
|
|
588
|
-
const branchNumber = useAuiState((s) => s.message.branchNumber);
|
|
589
|
-
const branchCount = useAuiState((s) => s.message.branchCount);
|
|
590
|
-
|
|
591
|
-
if (branchCount <= 1) return null;
|
|
592
|
-
|
|
593
|
-
return (
|
|
594
|
-
<View style={[styles.container, { justifyContent: align }]}>
|
|
595
|
-
<BranchPickerPrimitive.Previous
|
|
596
|
-
style={[styles.button, { opacity: branchNumber <= 1 ? 0.35 : 1 }]}
|
|
597
|
-
hitSlop={4}
|
|
598
|
-
>
|
|
599
|
-
<Icon name="chevron-left" size={16} color={colors.mutedForeground} />
|
|
600
|
-
</BranchPickerPrimitive.Previous>
|
|
601
|
-
<Text style={[styles.label, { color: colors.mutedForeground }]}>
|
|
602
|
-
<BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
|
|
603
|
-
</Text>
|
|
604
|
-
<BranchPickerPrimitive.Next
|
|
605
|
-
style={[
|
|
606
|
-
styles.button,
|
|
607
|
-
{ opacity: branchNumber >= branchCount ? 0.35 : 1 },
|
|
608
|
-
]}
|
|
609
|
-
hitSlop={4}
|
|
610
|
-
>
|
|
611
|
-
<Icon name="chevron-right" size={16} color={colors.mutedForeground} />
|
|
612
|
-
</BranchPickerPrimitive.Next>
|
|
613
|
-
</View>
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const styles = StyleSheet.create({
|
|
618
|
-
container: {
|
|
619
|
-
flexDirection: "row",
|
|
620
|
-
alignItems: "center",
|
|
621
|
-
gap: 2,
|
|
622
|
-
},
|
|
623
|
-
button: {
|
|
624
|
-
padding: 4,
|
|
625
|
-
},
|
|
626
|
-
label: {
|
|
627
|
-
fontSize: 12,
|
|
628
|
-
fontVariant: ["tabular-nums"],
|
|
629
|
-
},
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
## components/assistant-ui/message.tsx
|
|
635
|
-
|
|
636
|
-
```tsx
|
|
637
|
-
import { useEffect, useRef } from "react";
|
|
638
|
-
import { View, Text, Animated, Platform, StyleSheet } from "react-native";
|
|
639
|
-
import { Image } from "expo-image";
|
|
640
|
-
import {
|
|
641
|
-
AuiIf,
|
|
642
|
-
useAuiState,
|
|
643
|
-
MessagePrimitive,
|
|
644
|
-
ErrorPrimitive,
|
|
645
|
-
type TextMessagePartComponent,
|
|
646
|
-
} from "@assistant-ui/react-native";
|
|
647
|
-
import { useTheme } from "@/hooks/use-theme";
|
|
648
|
-
import { Radius } from "@/constants/theme";
|
|
649
|
-
import { MessageActionBar } from "./message-action-bar";
|
|
650
|
-
import { MessageBranchPicker } from "./message-branch-picker";
|
|
651
|
-
|
|
652
|
-
const UserText: TextMessagePartComponent = ({ text }) => {
|
|
653
|
-
const { colors } = useTheme();
|
|
654
|
-
return (
|
|
655
|
-
<Text style={[styles.userText, { color: colors.foreground }]}>{text}</Text>
|
|
656
|
-
);
|
|
657
|
-
};
|
|
658
|
-
|
|
659
|
-
const AssistantText: TextMessagePartComponent = ({ text }) => {
|
|
660
|
-
const { colors } = useTheme();
|
|
661
|
-
return (
|
|
662
|
-
<Text style={[styles.assistantText, { color: colors.foreground }]}>
|
|
663
|
-
{text}
|
|
664
|
-
</Text>
|
|
665
|
-
);
|
|
666
|
-
};
|
|
667
|
-
|
|
668
|
-
function TypingDot({ delay }: { delay: number }) {
|
|
669
|
-
const { colors } = useTheme();
|
|
670
|
-
const opacity = useRef(new Animated.Value(0.3)).current;
|
|
671
|
-
|
|
672
|
-
useEffect(() => {
|
|
673
|
-
const anim = Animated.loop(
|
|
674
|
-
Animated.sequence([
|
|
675
|
-
Animated.timing(opacity, {
|
|
676
|
-
toValue: 1,
|
|
677
|
-
duration: 400,
|
|
678
|
-
delay,
|
|
679
|
-
useNativeDriver: Platform.OS !== "web",
|
|
680
|
-
}),
|
|
681
|
-
Animated.timing(opacity, {
|
|
682
|
-
toValue: 0.3,
|
|
683
|
-
duration: 400,
|
|
684
|
-
useNativeDriver: Platform.OS !== "web",
|
|
685
|
-
}),
|
|
686
|
-
]),
|
|
687
|
-
);
|
|
688
|
-
anim.start();
|
|
689
|
-
return () => anim.stop();
|
|
690
|
-
}, [opacity, delay]);
|
|
691
|
-
|
|
692
|
-
return (
|
|
693
|
-
<Animated.View
|
|
694
|
-
style={[styles.dot, { opacity, backgroundColor: colors.mutedForeground }]}
|
|
695
|
-
/>
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function TypingIndicator() {
|
|
700
|
-
const isRunning = useAuiState((s) => s.message.status?.type === "running");
|
|
701
|
-
if (!isRunning) return null;
|
|
702
|
-
|
|
703
|
-
return (
|
|
704
|
-
<View style={styles.typing}>
|
|
705
|
-
<TypingDot delay={0} />
|
|
706
|
-
<TypingDot delay={160} />
|
|
707
|
-
<TypingDot delay={320} />
|
|
708
|
-
</View>
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
function MessageImageAttachment() {
|
|
713
|
-
const { colors } = useTheme();
|
|
714
|
-
const attachment = useAuiState((s) => s.attachment);
|
|
715
|
-
if (!attachment) return null;
|
|
716
|
-
|
|
717
|
-
const imageContent = attachment.content?.find((c: any) => c.type === "image");
|
|
718
|
-
const uri = (imageContent as any)?.image;
|
|
719
|
-
if (!uri) return null;
|
|
720
|
-
|
|
721
|
-
return (
|
|
722
|
-
<Image
|
|
723
|
-
source={{ uri }}
|
|
724
|
-
style={[styles.messageImage, { backgroundColor: colors.muted }]}
|
|
725
|
-
contentFit="cover"
|
|
726
|
-
transition={120}
|
|
727
|
-
/>
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function UserMessage() {
|
|
732
|
-
const { colors } = useTheme();
|
|
733
|
-
return (
|
|
734
|
-
<MessagePrimitive.Root style={styles.userContainer}>
|
|
735
|
-
<View style={styles.userAttachments}>
|
|
736
|
-
<MessagePrimitive.Attachments>
|
|
737
|
-
{() => <MessageImageAttachment />}
|
|
738
|
-
</MessagePrimitive.Attachments>
|
|
739
|
-
</View>
|
|
740
|
-
<View style={[styles.userBubble, { backgroundColor: colors.muted }]}>
|
|
741
|
-
<MessagePrimitive.Parts components={{ Text: UserText }} />
|
|
742
|
-
</View>
|
|
743
|
-
<MessageBranchPicker align="flex-end" />
|
|
744
|
-
</MessagePrimitive.Root>
|
|
745
|
-
);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function AssistantMessage() {
|
|
749
|
-
const { colors } = useTheme();
|
|
750
|
-
return (
|
|
751
|
-
<MessagePrimitive.Root style={styles.assistantContainer}>
|
|
752
|
-
<View style={styles.assistantContent}>
|
|
753
|
-
<MessagePrimitive.Parts
|
|
754
|
-
components={{ Text: AssistantText, Empty: TypingIndicator }}
|
|
755
|
-
/>
|
|
756
|
-
<ErrorPrimitive.Root
|
|
757
|
-
style={[
|
|
758
|
-
styles.error,
|
|
759
|
-
{
|
|
760
|
-
backgroundColor: colors.destructiveSurface,
|
|
761
|
-
borderColor: colors.destructive,
|
|
762
|
-
},
|
|
763
|
-
]}
|
|
764
|
-
>
|
|
765
|
-
<ErrorPrimitive.Message
|
|
766
|
-
style={[styles.errorText, { color: colors.destructive }]}
|
|
767
|
-
/>
|
|
768
|
-
</ErrorPrimitive.Root>
|
|
769
|
-
</View>
|
|
770
|
-
<AuiIf
|
|
771
|
-
condition={(s) =>
|
|
772
|
-
!(
|
|
773
|
-
s.message.role === "assistant" &&
|
|
774
|
-
s.message.status.type === "running"
|
|
775
|
-
)
|
|
776
|
-
}
|
|
777
|
-
>
|
|
778
|
-
<View style={styles.actionsRow}>
|
|
779
|
-
<MessageBranchPicker align="flex-start" />
|
|
780
|
-
<MessageActionBar />
|
|
781
|
-
</View>
|
|
782
|
-
</AuiIf>
|
|
783
|
-
</MessagePrimitive.Root>
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
export function MessageBubble() {
|
|
788
|
-
const role = useAuiState((s) => s.message.role);
|
|
789
|
-
if (role === "user") return <UserMessage />;
|
|
790
|
-
return <AssistantMessage />;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
const styles = StyleSheet.create({
|
|
794
|
-
userContainer: {
|
|
795
|
-
alignItems: "flex-end",
|
|
796
|
-
},
|
|
797
|
-
userAttachments: {
|
|
798
|
-
flexDirection: "row",
|
|
799
|
-
flexWrap: "wrap",
|
|
800
|
-
justifyContent: "flex-end",
|
|
801
|
-
gap: 6,
|
|
802
|
-
},
|
|
803
|
-
userBubble: {
|
|
804
|
-
maxWidth: "85%",
|
|
805
|
-
paddingHorizontal: 16,
|
|
806
|
-
paddingVertical: 10,
|
|
807
|
-
borderRadius: Radius.bubble,
|
|
808
|
-
},
|
|
809
|
-
assistantContainer: {
|
|
810
|
-
alignItems: "flex-start",
|
|
811
|
-
},
|
|
812
|
-
assistantContent: {
|
|
813
|
-
paddingHorizontal: 2,
|
|
814
|
-
},
|
|
815
|
-
userText: {
|
|
816
|
-
fontSize: 16,
|
|
817
|
-
lineHeight: 22,
|
|
818
|
-
letterSpacing: -0.2,
|
|
819
|
-
},
|
|
820
|
-
assistantText: {
|
|
821
|
-
fontSize: 16,
|
|
822
|
-
lineHeight: 25,
|
|
823
|
-
letterSpacing: -0.2,
|
|
824
|
-
},
|
|
825
|
-
typing: {
|
|
826
|
-
flexDirection: "row",
|
|
827
|
-
alignItems: "center",
|
|
828
|
-
gap: 5,
|
|
829
|
-
paddingVertical: 8,
|
|
830
|
-
},
|
|
831
|
-
dot: {
|
|
832
|
-
width: 7,
|
|
833
|
-
height: 7,
|
|
834
|
-
borderRadius: 3.5,
|
|
835
|
-
},
|
|
836
|
-
messageImage: {
|
|
837
|
-
width: 200,
|
|
838
|
-
height: 200,
|
|
839
|
-
borderRadius: Radius.card,
|
|
840
|
-
marginBottom: 6,
|
|
841
|
-
},
|
|
842
|
-
actionsRow: {
|
|
843
|
-
flexDirection: "row",
|
|
844
|
-
alignItems: "center",
|
|
845
|
-
gap: 4,
|
|
846
|
-
marginTop: 6,
|
|
847
|
-
marginLeft: -4,
|
|
848
|
-
},
|
|
849
|
-
error: {
|
|
850
|
-
marginTop: 8,
|
|
851
|
-
borderRadius: Radius.md,
|
|
852
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
853
|
-
paddingHorizontal: 12,
|
|
854
|
-
paddingVertical: 10,
|
|
855
|
-
},
|
|
856
|
-
errorText: {
|
|
857
|
-
fontSize: 14,
|
|
858
|
-
lineHeight: 20,
|
|
859
|
-
},
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
```
|
|
863
|
-
|
|
864
|
-
## components/assistant-ui/thread.tsx
|
|
865
|
-
|
|
866
|
-
```tsx
|
|
867
|
-
import {
|
|
868
|
-
View,
|
|
869
|
-
Text,
|
|
870
|
-
StyleSheet,
|
|
871
|
-
KeyboardAvoidingView,
|
|
872
|
-
Platform,
|
|
873
|
-
} from "react-native";
|
|
874
|
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
875
|
-
import { MessageBubble } from "./message";
|
|
876
|
-
import { Composer } from "./composer";
|
|
877
|
-
import { AuiIf, ThreadPrimitive } from "@assistant-ui/react-native";
|
|
878
|
-
import { useTheme } from "@/hooks/use-theme";
|
|
879
|
-
import { Radius, Spacing } from "@/constants/theme";
|
|
880
|
-
import { haptics } from "@/lib/haptics";
|
|
881
|
-
|
|
882
|
-
const suggestions = [
|
|
883
|
-
"What's the weather in Tokyo?",
|
|
884
|
-
"Tell me a joke",
|
|
885
|
-
"Help me write an email",
|
|
886
|
-
];
|
|
887
|
-
|
|
888
|
-
function SuggestionChip({ prompt }: { prompt: string }) {
|
|
889
|
-
const { colors } = useTheme();
|
|
890
|
-
return (
|
|
891
|
-
<ThreadPrimitive.Suggestion
|
|
892
|
-
prompt={prompt}
|
|
893
|
-
send
|
|
894
|
-
onPressIn={haptics.selection}
|
|
895
|
-
style={({ pressed }: { pressed: boolean }) => [
|
|
896
|
-
styles.chip,
|
|
897
|
-
{
|
|
898
|
-
borderColor: colors.border,
|
|
899
|
-
backgroundColor: pressed ? colors.muted : colors.background,
|
|
900
|
-
},
|
|
901
|
-
]}
|
|
902
|
-
>
|
|
903
|
-
<Text style={[styles.chipText, { color: colors.foreground }]}>
|
|
904
|
-
{prompt}
|
|
905
|
-
</Text>
|
|
906
|
-
</ThreadPrimitive.Suggestion>
|
|
907
|
-
);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
function EmptyState() {
|
|
911
|
-
const { colors } = useTheme();
|
|
912
|
-
return (
|
|
913
|
-
<View style={styles.empty}>
|
|
914
|
-
<Text style={[styles.welcome, { color: colors.foreground }]}>
|
|
915
|
-
How can I help you today?
|
|
916
|
-
</Text>
|
|
917
|
-
<View style={styles.chips}>
|
|
918
|
-
{suggestions.map((prompt) => (
|
|
919
|
-
<SuggestionChip key={prompt} prompt={prompt} />
|
|
920
|
-
))}
|
|
921
|
-
</View>
|
|
922
|
-
</View>
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
function ChatMessages() {
|
|
927
|
-
return (
|
|
928
|
-
<>
|
|
929
|
-
<AuiIf condition={(s) => s.thread.isEmpty}>
|
|
930
|
-
<EmptyState />
|
|
931
|
-
</AuiIf>
|
|
932
|
-
<AuiIf condition={(s) => !s.thread.isEmpty}>
|
|
933
|
-
<ThreadPrimitive.MessagesFlatList
|
|
934
|
-
style={styles.flex}
|
|
935
|
-
contentContainerStyle={styles.messageList}
|
|
936
|
-
showsVerticalScrollIndicator={false}
|
|
937
|
-
>
|
|
938
|
-
{() => <MessageBubble />}
|
|
939
|
-
</ThreadPrimitive.MessagesFlatList>
|
|
940
|
-
</AuiIf>
|
|
941
|
-
</>
|
|
942
|
-
);
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
export function Thread() {
|
|
946
|
-
const insets = useSafeAreaInsets();
|
|
947
|
-
const { colors } = useTheme();
|
|
948
|
-
|
|
949
|
-
return (
|
|
950
|
-
<View style={[styles.container, { backgroundColor: colors.background }]}>
|
|
951
|
-
<KeyboardAvoidingView
|
|
952
|
-
style={styles.flex}
|
|
953
|
-
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
|
954
|
-
>
|
|
955
|
-
<View style={styles.flex}>
|
|
956
|
-
<ChatMessages />
|
|
957
|
-
</View>
|
|
958
|
-
<View style={{ paddingBottom: insets.bottom + 8 }}>
|
|
959
|
-
<Composer />
|
|
960
|
-
</View>
|
|
961
|
-
</KeyboardAvoidingView>
|
|
962
|
-
</View>
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
const styles = StyleSheet.create({
|
|
967
|
-
container: {
|
|
968
|
-
flex: 1,
|
|
969
|
-
},
|
|
970
|
-
flex: {
|
|
971
|
-
flex: 1,
|
|
972
|
-
},
|
|
973
|
-
messageList: {
|
|
974
|
-
width: "100%",
|
|
975
|
-
maxWidth: Spacing.threadMaxWidth,
|
|
976
|
-
marginHorizontal: "auto",
|
|
977
|
-
paddingVertical: 20,
|
|
978
|
-
paddingHorizontal: 12,
|
|
979
|
-
gap: 18,
|
|
980
|
-
},
|
|
981
|
-
empty: {
|
|
982
|
-
flex: 1,
|
|
983
|
-
justifyContent: "center",
|
|
984
|
-
alignItems: "center",
|
|
985
|
-
paddingHorizontal: 24,
|
|
986
|
-
},
|
|
987
|
-
welcome: {
|
|
988
|
-
fontSize: 24,
|
|
989
|
-
fontWeight: "600",
|
|
990
|
-
letterSpacing: -0.4,
|
|
991
|
-
textAlign: "center",
|
|
992
|
-
marginBottom: 24,
|
|
993
|
-
},
|
|
994
|
-
chips: {
|
|
995
|
-
flexDirection: "row",
|
|
996
|
-
flexWrap: "wrap",
|
|
997
|
-
justifyContent: "center",
|
|
998
|
-
gap: 8,
|
|
999
|
-
},
|
|
1000
|
-
chip: {
|
|
1001
|
-
paddingHorizontal: 14,
|
|
1002
|
-
paddingVertical: 8,
|
|
1003
|
-
borderRadius: Radius.pill,
|
|
1004
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
1005
|
-
},
|
|
1006
|
-
chipText: {
|
|
1007
|
-
fontSize: 14,
|
|
1008
|
-
letterSpacing: -0.2,
|
|
1009
|
-
},
|
|
1010
|
-
});
|
|
1011
|
-
|
|
1012
|
-
```
|
|
1013
|
-
|
|
1014
|
-
## components/assistant-ui/tools.tsx
|
|
1015
|
-
|
|
1016
|
-
```tsx
|
|
1017
|
-
"use generative";
|
|
1018
|
-
|
|
1019
|
-
import type { ReactNode } from "react";
|
|
1020
|
-
import { View, Text, StyleSheet } from "react-native";
|
|
1021
|
-
import {
|
|
1022
|
-
defineToolkit,
|
|
1023
|
-
type ToolCallMessagePartProps,
|
|
1024
|
-
} from "@assistant-ui/react-native";
|
|
1025
|
-
import { z } from "zod";
|
|
1026
|
-
import { useTheme } from "@/hooks/use-theme";
|
|
1027
|
-
import { Radius } from "@/constants/theme";
|
|
1028
|
-
|
|
1029
|
-
// Open-Meteo API adapters (free, no API key needed)
|
|
1030
|
-
|
|
1031
|
-
const geocodeLocationWithOpenMeteo = async (query: string) => {
|
|
1032
|
-
try {
|
|
1033
|
-
const response = await fetch(
|
|
1034
|
-
`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(query)}&count=1`,
|
|
1035
|
-
);
|
|
1036
|
-
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
1037
|
-
const data = await response.json();
|
|
1038
|
-
if (!data.results || data.results.length === 0)
|
|
1039
|
-
throw new Error("No results found");
|
|
1040
|
-
return { success: true as const, result: data.results[0] };
|
|
1041
|
-
} catch (error) {
|
|
1042
|
-
return {
|
|
1043
|
-
success: false as const,
|
|
1044
|
-
error:
|
|
1045
|
-
error instanceof Error ? error.message : "Failed to geocode location",
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
};
|
|
1049
|
-
|
|
1050
|
-
const mapWeatherCode = (code: number): string => {
|
|
1051
|
-
if (code === 0) return "Clear";
|
|
1052
|
-
if (code <= 3) return "Partly Cloudy";
|
|
1053
|
-
if (code <= 48) return "Foggy";
|
|
1054
|
-
if (code <= 57) return "Drizzle";
|
|
1055
|
-
if (code <= 67) return "Rain";
|
|
1056
|
-
if (code <= 77) return "Snow";
|
|
1057
|
-
if (code <= 82) return "Showers";
|
|
1058
|
-
if (code <= 86) return "Snow Showers";
|
|
1059
|
-
if (code === 95) return "Thunderstorm";
|
|
1060
|
-
return "Stormy";
|
|
1061
|
-
};
|
|
1062
|
-
|
|
1063
|
-
const mapWeatherEmoji = (code: number): string => {
|
|
1064
|
-
if (code === 0) return "☀️";
|
|
1065
|
-
if (code <= 3) return "⛅";
|
|
1066
|
-
if (code <= 48) return "🌫️";
|
|
1067
|
-
if (code <= 57) return "🌦️";
|
|
1068
|
-
if (code <= 67) return "🌧️";
|
|
1069
|
-
if (code <= 77) return "❄️";
|
|
1070
|
-
if (code <= 82) return "🌦️";
|
|
1071
|
-
if (code <= 86) return "🌨️";
|
|
1072
|
-
if (code === 95) return "⛈️";
|
|
1073
|
-
return "🌩️";
|
|
1074
|
-
};
|
|
1075
|
-
|
|
1076
|
-
const fetchWeatherFromOpenMeteo = async ({
|
|
1077
|
-
query,
|
|
1078
|
-
longitude,
|
|
1079
|
-
latitude,
|
|
1080
|
-
}: {
|
|
1081
|
-
query: string;
|
|
1082
|
-
longitude: number;
|
|
1083
|
-
latitude: number;
|
|
1084
|
-
}) => {
|
|
1085
|
-
try {
|
|
1086
|
-
const response = await fetch(
|
|
1087
|
-
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&timezone=auto&temperature_unit=fahrenheit¤t=temperature_2m,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min&forecast_days=5`,
|
|
1088
|
-
);
|
|
1089
|
-
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
1090
|
-
const data = await response.json();
|
|
1091
|
-
const current = data.current;
|
|
1092
|
-
const daily = data.daily;
|
|
1093
|
-
if (!current || !daily?.time) throw new Error("Invalid API response");
|
|
1094
|
-
|
|
1095
|
-
const forecast = daily.time.slice(0, 5).map((date: string, i: number) => {
|
|
1096
|
-
const d = new Date(`${date}T12:00:00Z`);
|
|
1097
|
-
const label =
|
|
1098
|
-
i === 0
|
|
1099
|
-
? "Today"
|
|
1100
|
-
: new Intl.DateTimeFormat("en-US", {
|
|
1101
|
-
weekday: "short",
|
|
1102
|
-
timeZone: "UTC",
|
|
1103
|
-
}).format(d);
|
|
1104
|
-
return {
|
|
1105
|
-
label,
|
|
1106
|
-
code: daily.weather_code[i],
|
|
1107
|
-
min: Math.round(daily.temperature_2m_min[i]),
|
|
1108
|
-
max: Math.round(daily.temperature_2m_max[i]),
|
|
1109
|
-
};
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
return {
|
|
1113
|
-
success: true as const,
|
|
1114
|
-
location: query,
|
|
1115
|
-
temperature: Math.round(current.temperature_2m),
|
|
1116
|
-
weatherCode: current.weather_code,
|
|
1117
|
-
windSpeed: Math.round(current.wind_speed_10m),
|
|
1118
|
-
forecast,
|
|
1119
|
-
};
|
|
1120
|
-
} catch (error) {
|
|
1121
|
-
return {
|
|
1122
|
-
success: false as const,
|
|
1123
|
-
error: error instanceof Error ? error.message : "Failed to fetch weather",
|
|
1124
|
-
};
|
|
1125
|
-
}
|
|
1126
|
-
};
|
|
1127
|
-
|
|
1128
|
-
// Tool UI Components
|
|
1129
|
-
|
|
1130
|
-
function ToolCard({ children }: { children: ReactNode }) {
|
|
1131
|
-
const { colors } = useTheme();
|
|
1132
|
-
return (
|
|
1133
|
-
<View
|
|
1134
|
-
style={[
|
|
1135
|
-
styles.card,
|
|
1136
|
-
{ backgroundColor: colors.muted, borderColor: colors.border },
|
|
1137
|
-
]}
|
|
1138
|
-
>
|
|
1139
|
-
{children}
|
|
1140
|
-
</View>
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
function ToolStatus({ label }: { label: string }) {
|
|
1145
|
-
const { colors } = useTheme();
|
|
1146
|
-
return (
|
|
1147
|
-
<ToolCard>
|
|
1148
|
-
<Text style={[styles.statusText, { color: colors.mutedForeground }]}>
|
|
1149
|
-
{label}
|
|
1150
|
-
</Text>
|
|
1151
|
-
</ToolCard>
|
|
1152
|
-
);
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
function ToolError({ label }: { label: string }) {
|
|
1156
|
-
const { colors } = useTheme();
|
|
1157
|
-
return (
|
|
1158
|
-
<View
|
|
1159
|
-
style={[
|
|
1160
|
-
styles.card,
|
|
1161
|
-
{
|
|
1162
|
-
backgroundColor: colors.destructiveSurface,
|
|
1163
|
-
borderColor: colors.destructive,
|
|
1164
|
-
},
|
|
1165
|
-
]}
|
|
1166
|
-
>
|
|
1167
|
-
<Text style={[styles.statusText, { color: colors.destructive }]}>
|
|
1168
|
-
{label}
|
|
1169
|
-
</Text>
|
|
1170
|
-
</View>
|
|
1171
|
-
);
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function GeocodeToolUI(
|
|
1175
|
-
props: ToolCallMessagePartProps<
|
|
1176
|
-
{ query: string },
|
|
1177
|
-
{
|
|
1178
|
-
success: boolean;
|
|
1179
|
-
result?: { name: string; latitude: number; longitude: number };
|
|
1180
|
-
error?: string;
|
|
1181
|
-
}
|
|
1182
|
-
>,
|
|
1183
|
-
) {
|
|
1184
|
-
const { colors } = useTheme();
|
|
1185
|
-
|
|
1186
|
-
if (props.status?.type === "running") {
|
|
1187
|
-
return <ToolStatus label="Finding location…" />;
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
if (props.result?.error) {
|
|
1191
|
-
return <ToolError label={`Geocoding failed: ${props.result.error}`} />;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
const result = props.result?.result;
|
|
1195
|
-
if (!result) return null;
|
|
1196
|
-
|
|
1197
|
-
return (
|
|
1198
|
-
<ToolCard>
|
|
1199
|
-
<View style={styles.row}>
|
|
1200
|
-
<Text style={styles.pin}>{"📍"}</Text>
|
|
1201
|
-
<View>
|
|
1202
|
-
<Text style={[styles.locationName, { color: colors.foreground }]}>
|
|
1203
|
-
{result.name}
|
|
1204
|
-
</Text>
|
|
1205
|
-
<Text style={[styles.coords, { color: colors.mutedForeground }]}>
|
|
1206
|
-
{Math.abs(result.latitude).toFixed(2)}
|
|
1207
|
-
{"°"}
|
|
1208
|
-
{result.latitude >= 0 ? "N" : "S"},{" "}
|
|
1209
|
-
{Math.abs(result.longitude).toFixed(2)}
|
|
1210
|
-
{"°"}
|
|
1211
|
-
{result.longitude >= 0 ? "E" : "W"}
|
|
1212
|
-
</Text>
|
|
1213
|
-
</View>
|
|
1214
|
-
</View>
|
|
1215
|
-
</ToolCard>
|
|
1216
|
-
);
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
function WeatherToolUI(
|
|
1220
|
-
props: ToolCallMessagePartProps<
|
|
1221
|
-
{ query: string; longitude: number; latitude: number },
|
|
1222
|
-
{
|
|
1223
|
-
success: boolean;
|
|
1224
|
-
location?: string;
|
|
1225
|
-
temperature?: number;
|
|
1226
|
-
weatherCode?: number;
|
|
1227
|
-
windSpeed?: number;
|
|
1228
|
-
forecast?: Array<{
|
|
1229
|
-
label: string;
|
|
1230
|
-
code: number;
|
|
1231
|
-
min: number;
|
|
1232
|
-
max: number;
|
|
1233
|
-
}>;
|
|
1234
|
-
error?: string;
|
|
1235
|
-
}
|
|
1236
|
-
>,
|
|
1237
|
-
) {
|
|
1238
|
-
const { colors } = useTheme();
|
|
1239
|
-
|
|
1240
|
-
if (props.status?.type === "running") {
|
|
1241
|
-
return <ToolStatus label={`Fetching weather for ${props.args.query}…`} />;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
if (!props.result?.success) {
|
|
1245
|
-
return (
|
|
1246
|
-
<ToolError
|
|
1247
|
-
label={`Weather unavailable: ${props.result?.error ?? "Unknown error"}`}
|
|
1248
|
-
/>
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
const { location, temperature, weatherCode, windSpeed, forecast } =
|
|
1253
|
-
props.result;
|
|
1254
|
-
|
|
1255
|
-
return (
|
|
1256
|
-
<ToolCard>
|
|
1257
|
-
<View style={styles.weatherHeader}>
|
|
1258
|
-
<Text style={styles.weatherEmoji}>
|
|
1259
|
-
{mapWeatherEmoji(weatherCode ?? 0)}
|
|
1260
|
-
</Text>
|
|
1261
|
-
<View>
|
|
1262
|
-
<Text style={[styles.locationName, { color: colors.foreground }]}>
|
|
1263
|
-
{location}
|
|
1264
|
-
</Text>
|
|
1265
|
-
<Text style={[styles.condition, { color: colors.mutedForeground }]}>
|
|
1266
|
-
{mapWeatherCode(weatherCode ?? 0)}
|
|
1267
|
-
</Text>
|
|
1268
|
-
</View>
|
|
1269
|
-
</View>
|
|
1270
|
-
|
|
1271
|
-
<Text style={[styles.tempLarge, { color: colors.foreground }]}>
|
|
1272
|
-
{temperature ?? "--"}
|
|
1273
|
-
{"°"}F
|
|
1274
|
-
</Text>
|
|
1275
|
-
|
|
1276
|
-
{windSpeed != null && (
|
|
1277
|
-
<Text style={[styles.wind, { color: colors.mutedForeground }]}>
|
|
1278
|
-
Wind: {windSpeed} mph
|
|
1279
|
-
</Text>
|
|
1280
|
-
)}
|
|
1281
|
-
|
|
1282
|
-
{forecast && forecast.length > 0 && (
|
|
1283
|
-
<View style={[styles.forecastRow, { borderTopColor: colors.border }]}>
|
|
1284
|
-
{forecast.map((day, i) => (
|
|
1285
|
-
<View key={i} style={styles.forecastDay}>
|
|
1286
|
-
<Text
|
|
1287
|
-
style={[
|
|
1288
|
-
styles.forecastLabel,
|
|
1289
|
-
{ color: colors.mutedForeground },
|
|
1290
|
-
]}
|
|
1291
|
-
>
|
|
1292
|
-
{day.label}
|
|
1293
|
-
</Text>
|
|
1294
|
-
<Text style={styles.forecastEmoji}>
|
|
1295
|
-
{mapWeatherEmoji(day.code)}
|
|
1296
|
-
</Text>
|
|
1297
|
-
<Text style={[styles.forecastTemp, { color: colors.foreground }]}>
|
|
1298
|
-
{day.max}
|
|
1299
|
-
{"°"}
|
|
1300
|
-
</Text>
|
|
1301
|
-
<Text
|
|
1302
|
-
style={[
|
|
1303
|
-
styles.forecastTempLow,
|
|
1304
|
-
{ color: colors.mutedForeground },
|
|
1305
|
-
]}
|
|
1306
|
-
>
|
|
1307
|
-
{day.min}
|
|
1308
|
-
{"°"}
|
|
1309
|
-
</Text>
|
|
1310
|
-
</View>
|
|
1311
|
-
))}
|
|
1312
|
-
</View>
|
|
1313
|
-
)}
|
|
1314
|
-
</ToolCard>
|
|
1315
|
-
);
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// Toolkit definition
|
|
1319
|
-
|
|
1320
|
-
export default defineToolkit({
|
|
1321
|
-
geocode_location: {
|
|
1322
|
-
description: "Geocode a location using Open-Meteo's geocoding API",
|
|
1323
|
-
parameters: z.object({
|
|
1324
|
-
query: z.string(),
|
|
1325
|
-
}),
|
|
1326
|
-
execute: async (args: { query: string }) => {
|
|
1327
|
-
"use client";
|
|
1328
|
-
return geocodeLocationWithOpenMeteo(args.query);
|
|
1329
|
-
},
|
|
1330
|
-
render: GeocodeToolUI,
|
|
1331
|
-
},
|
|
1332
|
-
weather_search: {
|
|
1333
|
-
description:
|
|
1334
|
-
"Find the weather in a location given a longitude and latitude",
|
|
1335
|
-
parameters: z.object({
|
|
1336
|
-
query: z.string(),
|
|
1337
|
-
longitude: z.number(),
|
|
1338
|
-
latitude: z.number(),
|
|
1339
|
-
}),
|
|
1340
|
-
execute: async (args: {
|
|
1341
|
-
query: string;
|
|
1342
|
-
longitude: number;
|
|
1343
|
-
latitude: number;
|
|
1344
|
-
}) => {
|
|
1345
|
-
"use client";
|
|
1346
|
-
return fetchWeatherFromOpenMeteo(args);
|
|
1347
|
-
},
|
|
1348
|
-
render: WeatherToolUI,
|
|
1349
|
-
},
|
|
1350
|
-
});
|
|
1351
|
-
|
|
1352
|
-
const styles = StyleSheet.create({
|
|
1353
|
-
card: {
|
|
1354
|
-
padding: 14,
|
|
1355
|
-
borderRadius: Radius.card,
|
|
1356
|
-
borderWidth: StyleSheet.hairlineWidth,
|
|
1357
|
-
marginVertical: 4,
|
|
1358
|
-
gap: 4,
|
|
1359
|
-
},
|
|
1360
|
-
statusText: {
|
|
1361
|
-
fontSize: 13,
|
|
1362
|
-
},
|
|
1363
|
-
row: {
|
|
1364
|
-
flexDirection: "row",
|
|
1365
|
-
alignItems: "center",
|
|
1366
|
-
gap: 10,
|
|
1367
|
-
},
|
|
1368
|
-
pin: {
|
|
1369
|
-
fontSize: 20,
|
|
1370
|
-
},
|
|
1371
|
-
locationName: {
|
|
1372
|
-
fontSize: 15,
|
|
1373
|
-
fontWeight: "600",
|
|
1374
|
-
},
|
|
1375
|
-
coords: {
|
|
1376
|
-
fontSize: 13,
|
|
1377
|
-
marginTop: 2,
|
|
1378
|
-
},
|
|
1379
|
-
weatherHeader: {
|
|
1380
|
-
flexDirection: "row",
|
|
1381
|
-
alignItems: "center",
|
|
1382
|
-
gap: 10,
|
|
1383
|
-
marginBottom: 4,
|
|
1384
|
-
},
|
|
1385
|
-
weatherEmoji: {
|
|
1386
|
-
fontSize: 32,
|
|
1387
|
-
},
|
|
1388
|
-
condition: {
|
|
1389
|
-
fontSize: 13,
|
|
1390
|
-
marginTop: 2,
|
|
1391
|
-
},
|
|
1392
|
-
tempLarge: {
|
|
1393
|
-
fontSize: 40,
|
|
1394
|
-
fontWeight: "700",
|
|
1395
|
-
letterSpacing: -1,
|
|
1396
|
-
},
|
|
1397
|
-
wind: {
|
|
1398
|
-
fontSize: 13,
|
|
1399
|
-
marginTop: 2,
|
|
1400
|
-
},
|
|
1401
|
-
forecastRow: {
|
|
1402
|
-
flexDirection: "row",
|
|
1403
|
-
justifyContent: "space-between",
|
|
1404
|
-
marginTop: 12,
|
|
1405
|
-
paddingTop: 12,
|
|
1406
|
-
borderTopWidth: StyleSheet.hairlineWidth,
|
|
1407
|
-
},
|
|
1408
|
-
forecastDay: {
|
|
1409
|
-
alignItems: "center",
|
|
1410
|
-
flex: 1,
|
|
1411
|
-
gap: 4,
|
|
1412
|
-
},
|
|
1413
|
-
forecastLabel: {
|
|
1414
|
-
fontSize: 11,
|
|
1415
|
-
fontWeight: "500",
|
|
1416
|
-
},
|
|
1417
|
-
forecastEmoji: {
|
|
1418
|
-
fontSize: 18,
|
|
1419
|
-
},
|
|
1420
|
-
forecastTemp: {
|
|
1421
|
-
fontSize: 13,
|
|
1422
|
-
fontWeight: "600",
|
|
1423
|
-
},
|
|
1424
|
-
forecastTempLow: {
|
|
1425
|
-
fontSize: 12,
|
|
1426
|
-
},
|
|
1427
|
-
});
|
|
1428
|
-
|
|
1429
|
-
```
|
|
1430
|
-
|
|
1431
|
-
## components/thread-list/ThreadListDrawer.tsx
|
|
1432
|
-
|
|
1433
|
-
```tsx
|
|
1434
|
-
import { View, Text, Pressable, StyleSheet } from "react-native";
|
|
1435
|
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
1436
|
-
import {
|
|
1437
|
-
ThreadListPrimitive,
|
|
1438
|
-
useAui,
|
|
1439
|
-
useAuiState,
|
|
1440
|
-
} from "@assistant-ui/react-native";
|
|
1441
|
-
import type { DrawerContentComponentProps } from "expo-router/build/react-navigation/drawer/types";
|
|
1442
|
-
import { ThreadListItem } from "./ThreadListItem";
|
|
1443
|
-
import { Icon } from "@/components/ui/icon";
|
|
1444
|
-
import { useTheme } from "@/hooks/use-theme";
|
|
1445
|
-
import { Radius } from "@/constants/theme";
|
|
1446
|
-
import { haptics } from "@/lib/haptics";
|
|
1447
|
-
|
|
1448
|
-
export function ThreadListDrawer({ navigation }: DrawerContentComponentProps) {
|
|
1449
|
-
const { colors } = useTheme();
|
|
1450
|
-
const aui = useAui();
|
|
1451
|
-
const isNewThreadActive = useAuiState(
|
|
1452
|
-
(s) => s.threads.newThreadId === s.threads.mainThreadId,
|
|
1453
|
-
);
|
|
1454
|
-
const insets = useSafeAreaInsets();
|
|
1455
|
-
|
|
1456
|
-
return (
|
|
1457
|
-
<View
|
|
1458
|
-
style={[
|
|
1459
|
-
styles.container,
|
|
1460
|
-
{ backgroundColor: colors.background, paddingTop: insets.top + 12 },
|
|
1461
|
-
]}
|
|
1462
|
-
>
|
|
1463
|
-
<ThreadListPrimitive.Root style={styles.root}>
|
|
1464
|
-
<Pressable
|
|
1465
|
-
accessibilityRole="button"
|
|
1466
|
-
accessibilityState={{ selected: isNewThreadActive }}
|
|
1467
|
-
onPressIn={haptics.selection}
|
|
1468
|
-
onPress={() => {
|
|
1469
|
-
aui.threads.switchToNewThread();
|
|
1470
|
-
navigation.closeDrawer();
|
|
1471
|
-
}}
|
|
1472
|
-
style={({ pressed }) => [
|
|
1473
|
-
styles.newButton,
|
|
1474
|
-
{ backgroundColor: pressed ? colors.muted : "transparent" },
|
|
1475
|
-
]}
|
|
1476
|
-
>
|
|
1477
|
-
<Icon name="compose" size={18} color={colors.foreground} />
|
|
1478
|
-
<Text style={[styles.newLabel, { color: colors.foreground }]}>
|
|
1479
|
-
New chat
|
|
1480
|
-
</Text>
|
|
1481
|
-
</Pressable>
|
|
1482
|
-
|
|
1483
|
-
<Text style={[styles.sectionLabel, { color: colors.mutedForeground }]}>
|
|
1484
|
-
Recent
|
|
1485
|
-
</Text>
|
|
1486
|
-
|
|
1487
|
-
<ThreadListPrimitive.Items
|
|
1488
|
-
renderItem={() => (
|
|
1489
|
-
<ThreadListItem onSelect={() => navigation.closeDrawer()} />
|
|
1490
|
-
)}
|
|
1491
|
-
style={styles.list}
|
|
1492
|
-
contentContainerStyle={styles.listContent}
|
|
1493
|
-
showsVerticalScrollIndicator={false}
|
|
1494
|
-
/>
|
|
1495
|
-
</ThreadListPrimitive.Root>
|
|
1496
|
-
</View>
|
|
1497
|
-
);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
const styles = StyleSheet.create({
|
|
1501
|
-
container: {
|
|
1502
|
-
flex: 1,
|
|
1503
|
-
},
|
|
1504
|
-
root: {
|
|
1505
|
-
flex: 1,
|
|
1506
|
-
},
|
|
1507
|
-
newButton: {
|
|
1508
|
-
flexDirection: "row",
|
|
1509
|
-
alignItems: "center",
|
|
1510
|
-
gap: 10,
|
|
1511
|
-
height: 40,
|
|
1512
|
-
paddingHorizontal: 12,
|
|
1513
|
-
marginHorizontal: 8,
|
|
1514
|
-
marginBottom: 4,
|
|
1515
|
-
borderRadius: Radius.md,
|
|
1516
|
-
},
|
|
1517
|
-
newLabel: {
|
|
1518
|
-
fontSize: 15,
|
|
1519
|
-
fontWeight: "500",
|
|
1520
|
-
letterSpacing: -0.2,
|
|
1521
|
-
},
|
|
1522
|
-
sectionLabel: {
|
|
1523
|
-
fontSize: 12,
|
|
1524
|
-
fontWeight: "500",
|
|
1525
|
-
paddingHorizontal: 20,
|
|
1526
|
-
paddingTop: 12,
|
|
1527
|
-
paddingBottom: 6,
|
|
1528
|
-
},
|
|
1529
|
-
list: {
|
|
1530
|
-
flex: 1,
|
|
1531
|
-
},
|
|
1532
|
-
listContent: {
|
|
1533
|
-
paddingBottom: 8,
|
|
1534
|
-
},
|
|
1535
|
-
});
|
|
1536
|
-
|
|
1537
|
-
```
|
|
1538
|
-
|
|
1539
|
-
## components/thread-list/ThreadListItem.tsx
|
|
1540
|
-
|
|
1541
|
-
```tsx
|
|
1542
|
-
import { Text, Pressable, StyleSheet } from "react-native";
|
|
1543
|
-
import {
|
|
1544
|
-
ThreadListItemPrimitive,
|
|
1545
|
-
useAui,
|
|
1546
|
-
useAuiState,
|
|
1547
|
-
} from "@assistant-ui/react-native";
|
|
1548
|
-
import { useTheme } from "@/hooks/use-theme";
|
|
1549
|
-
import { Radius } from "@/constants/theme";
|
|
1550
|
-
import { haptics } from "@/lib/haptics";
|
|
1551
|
-
|
|
1552
|
-
export function ThreadListItem({ onSelect }: { onSelect: () => void }) {
|
|
1553
|
-
const { colors } = useTheme();
|
|
1554
|
-
const aui = useAui();
|
|
1555
|
-
const isActive = useAuiState(
|
|
1556
|
-
(s) => s.threads.mainThreadId === s.threadListItem.id,
|
|
1557
|
-
);
|
|
1558
|
-
|
|
1559
|
-
return (
|
|
1560
|
-
<ThreadListItemPrimitive.Root>
|
|
1561
|
-
<Pressable
|
|
1562
|
-
accessibilityRole="button"
|
|
1563
|
-
accessibilityState={{ selected: isActive }}
|
|
1564
|
-
onPressIn={haptics.selection}
|
|
1565
|
-
onPress={() => {
|
|
1566
|
-
aui.threadListItem.switchTo();
|
|
1567
|
-
onSelect();
|
|
1568
|
-
}}
|
|
1569
|
-
style={({ pressed }) => [
|
|
1570
|
-
styles.item,
|
|
1571
|
-
(isActive || pressed) && { backgroundColor: colors.muted },
|
|
1572
|
-
]}
|
|
1573
|
-
>
|
|
1574
|
-
<Text
|
|
1575
|
-
numberOfLines={1}
|
|
1576
|
-
style={[
|
|
1577
|
-
styles.title,
|
|
1578
|
-
{
|
|
1579
|
-
color: colors.foreground,
|
|
1580
|
-
fontWeight: isActive ? "600" : "400",
|
|
1581
|
-
},
|
|
1582
|
-
]}
|
|
1583
|
-
>
|
|
1584
|
-
<ThreadListItemPrimitive.Title fallback="New chat" />
|
|
1585
|
-
</Text>
|
|
1586
|
-
</Pressable>
|
|
1587
|
-
</ThreadListItemPrimitive.Root>
|
|
1588
|
-
);
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
const styles = StyleSheet.create({
|
|
1592
|
-
item: {
|
|
1593
|
-
height: 38,
|
|
1594
|
-
justifyContent: "center",
|
|
1595
|
-
paddingHorizontal: 12,
|
|
1596
|
-
marginHorizontal: 8,
|
|
1597
|
-
borderRadius: Radius.md,
|
|
1598
|
-
},
|
|
1599
|
-
title: {
|
|
1600
|
-
fontSize: 15,
|
|
1601
|
-
letterSpacing: -0.2,
|
|
1602
|
-
},
|
|
1603
|
-
});
|
|
1604
|
-
|
|
1605
|
-
```
|
|
1606
|
-
|
|
1607
|
-
## components/ui/icon-mappings.ts
|
|
1608
|
-
|
|
1609
|
-
```typescript
|
|
1610
|
-
import type { ComponentProps } from "react";
|
|
1611
|
-
import type MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
|
1612
|
-
import type { SymbolViewProps, SymbolWeight } from "expo-symbols";
|
|
1613
|
-
|
|
1614
|
-
export type IconName =
|
|
1615
|
-
| "compose"
|
|
1616
|
-
| "send"
|
|
1617
|
-
| "stop"
|
|
1618
|
-
| "add"
|
|
1619
|
-
| "remove"
|
|
1620
|
-
| "copy"
|
|
1621
|
-
| "check"
|
|
1622
|
-
| "reload"
|
|
1623
|
-
| "chevron-left"
|
|
1624
|
-
| "chevron-right"
|
|
1625
|
-
| "menu"
|
|
1626
|
-
| "location";
|
|
1627
|
-
|
|
1628
|
-
export type IconProps = {
|
|
1629
|
-
name: IconName;
|
|
1630
|
-
size?: number;
|
|
1631
|
-
color: string;
|
|
1632
|
-
weight?: SymbolWeight;
|
|
1633
|
-
};
|
|
1634
|
-
|
|
1635
|
-
/** SF Symbols rendered natively on iOS via `expo-symbols`. */
|
|
1636
|
-
export const SF_SYMBOLS: Record<IconName, SymbolViewProps["name"]> = {
|
|
1637
|
-
compose: "square.and.pencil",
|
|
1638
|
-
send: "arrow.up",
|
|
1639
|
-
stop: "stop.fill",
|
|
1640
|
-
add: "plus",
|
|
1641
|
-
remove: "xmark",
|
|
1642
|
-
copy: "doc.on.doc",
|
|
1643
|
-
check: "checkmark",
|
|
1644
|
-
reload: "arrow.clockwise",
|
|
1645
|
-
"chevron-left": "chevron.left",
|
|
1646
|
-
"chevron-right": "chevron.right",
|
|
1647
|
-
menu: "line.3.horizontal",
|
|
1648
|
-
location: "location.fill",
|
|
1649
|
-
};
|
|
1650
|
-
|
|
1651
|
-
/** Material Icons fallback for Android and web. */
|
|
1652
|
-
export const MATERIAL_ICONS: Record<
|
|
1653
|
-
IconName,
|
|
1654
|
-
ComponentProps<typeof MaterialIcons>["name"]
|
|
1655
|
-
> = {
|
|
1656
|
-
compose: "edit",
|
|
1657
|
-
send: "arrow-upward",
|
|
1658
|
-
stop: "stop",
|
|
1659
|
-
add: "add",
|
|
1660
|
-
remove: "close",
|
|
1661
|
-
copy: "content-copy",
|
|
1662
|
-
check: "check",
|
|
1663
|
-
reload: "refresh",
|
|
1664
|
-
"chevron-left": "chevron-left",
|
|
1665
|
-
"chevron-right": "chevron-right",
|
|
1666
|
-
menu: "menu",
|
|
1667
|
-
location: "place",
|
|
1668
|
-
};
|
|
1669
|
-
|
|
1670
|
-
```
|
|
1671
|
-
|
|
1672
|
-
## components/ui/icon.ios.tsx
|
|
1673
|
-
|
|
1674
|
-
```tsx
|
|
1675
|
-
import { SymbolView } from "expo-symbols";
|
|
1676
|
-
import { SF_SYMBOLS, type IconProps } from "./icon-mappings";
|
|
1677
|
-
|
|
1678
|
-
export function Icon({
|
|
1679
|
-
name,
|
|
1680
|
-
size = 24,
|
|
1681
|
-
color,
|
|
1682
|
-
weight = "regular",
|
|
1683
|
-
}: IconProps) {
|
|
1684
|
-
return (
|
|
1685
|
-
<SymbolView
|
|
1686
|
-
name={SF_SYMBOLS[name]}
|
|
1687
|
-
tintColor={color}
|
|
1688
|
-
weight={weight}
|
|
1689
|
-
resizeMode="scaleAspectFit"
|
|
1690
|
-
style={{ width: size, height: size }}
|
|
1691
|
-
/>
|
|
1692
|
-
);
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
```
|
|
1696
|
-
|
|
1697
|
-
## components/ui/icon.tsx
|
|
1698
|
-
|
|
1699
|
-
```tsx
|
|
1700
|
-
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
|
1701
|
-
import { MATERIAL_ICONS, type IconProps } from "./icon-mappings";
|
|
1702
|
-
|
|
1703
|
-
export function Icon({ name, size = 24, color }: IconProps) {
|
|
1704
|
-
return (
|
|
1705
|
-
<MaterialIcons name={MATERIAL_ICONS[name]} size={size} color={color} />
|
|
1706
|
-
);
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
```
|
|
1710
|
-
|
|
1711
|
-
## constants/theme.ts
|
|
1712
|
-
|
|
1713
|
-
```typescript
|
|
1714
|
-
/**
|
|
1715
|
-
* Design tokens for the assistant-ui Expo example.
|
|
1716
|
-
*
|
|
1717
|
-
* These mirror the canonical web kit (`packages/ui`): a neutral zinc palette,
|
|
1718
|
-
* a clean ChatGPT-grade look with subtle hairline borders and no heavy glass or
|
|
1719
|
-
* shadow. Values are the sRGB equivalents of the web `oklch` tokens.
|
|
1720
|
-
*/
|
|
1721
|
-
|
|
1722
|
-
export type Palette = {
|
|
1723
|
-
background: string;
|
|
1724
|
-
foreground: string;
|
|
1725
|
-
card: string;
|
|
1726
|
-
cardForeground: string;
|
|
1727
|
-
muted: string;
|
|
1728
|
-
mutedForeground: string;
|
|
1729
|
-
accent: string;
|
|
1730
|
-
border: string;
|
|
1731
|
-
primary: string;
|
|
1732
|
-
primaryForeground: string;
|
|
1733
|
-
composer: string;
|
|
1734
|
-
destructive: string;
|
|
1735
|
-
destructiveForeground: string;
|
|
1736
|
-
destructiveSurface: string;
|
|
1737
|
-
ring: string;
|
|
1738
|
-
};
|
|
1739
|
-
|
|
1740
|
-
export const Colors: { light: Palette; dark: Palette } = {
|
|
1741
|
-
light: {
|
|
1742
|
-
background: "#ffffff",
|
|
1743
|
-
foreground: "#18181b",
|
|
1744
|
-
card: "#ffffff",
|
|
1745
|
-
cardForeground: "#18181b",
|
|
1746
|
-
muted: "#f4f4f5",
|
|
1747
|
-
mutedForeground: "#71717a",
|
|
1748
|
-
accent: "#f4f4f5",
|
|
1749
|
-
border: "#e4e4e7",
|
|
1750
|
-
primary: "#18181b",
|
|
1751
|
-
primaryForeground: "#fafafa",
|
|
1752
|
-
composer: "#fafafa",
|
|
1753
|
-
destructive: "#dc2626",
|
|
1754
|
-
destructiveForeground: "#ffffff",
|
|
1755
|
-
destructiveSurface: "rgba(220, 38, 38, 0.08)",
|
|
1756
|
-
ring: "#a1a1aa",
|
|
1757
|
-
},
|
|
1758
|
-
dark: {
|
|
1759
|
-
background: "#18181b",
|
|
1760
|
-
foreground: "#fafafa",
|
|
1761
|
-
card: "#27272a",
|
|
1762
|
-
cardForeground: "#fafafa",
|
|
1763
|
-
muted: "#3f3f46",
|
|
1764
|
-
mutedForeground: "#a1a1aa",
|
|
1765
|
-
accent: "#3f3f46",
|
|
1766
|
-
border: "rgba(255, 255, 255, 0.1)",
|
|
1767
|
-
primary: "#e4e4e7",
|
|
1768
|
-
primaryForeground: "#18181b",
|
|
1769
|
-
composer: "#232326",
|
|
1770
|
-
destructive: "#f87171",
|
|
1771
|
-
destructiveForeground: "#18181b",
|
|
1772
|
-
destructiveSurface: "rgba(248, 113, 113, 0.12)",
|
|
1773
|
-
ring: "#52525b",
|
|
1774
|
-
},
|
|
1775
|
-
};
|
|
1776
|
-
|
|
1777
|
-
export const Radius = {
|
|
1778
|
-
sm: 6,
|
|
1779
|
-
md: 8,
|
|
1780
|
-
lg: 10,
|
|
1781
|
-
card: 16,
|
|
1782
|
-
bubble: 18,
|
|
1783
|
-
composer: 24,
|
|
1784
|
-
attachment: 14,
|
|
1785
|
-
pill: 999,
|
|
1786
|
-
} as const;
|
|
1787
|
-
|
|
1788
|
-
export const Spacing = {
|
|
1789
|
-
threadMaxWidth: 768,
|
|
1790
|
-
gutter: 16,
|
|
1791
|
-
} as const;
|
|
1792
|
-
|
|
1793
|
-
```
|
|
1794
|
-
|
|
1795
|
-
## hooks/anonymous-session-fetch.test.ts
|
|
1796
|
-
|
|
1797
|
-
```typescript
|
|
1798
|
-
import { describe, expect, it, vi } from "vitest";
|
|
1799
|
-
import {
|
|
1800
|
-
createAnonymousSessionFetch,
|
|
1801
|
-
shouldUseAnonymousSessionFetch,
|
|
1802
|
-
} from "./anonymous-session-fetch";
|
|
1803
|
-
|
|
1804
|
-
describe("Expo anonymous session fetch", () => {
|
|
1805
|
-
it("only bootstraps the protected browser flow on web", () => {
|
|
1806
|
-
expect(
|
|
1807
|
-
shouldUseAnonymousSessionFetch(
|
|
1808
|
-
"https://www.assistant-ui.com/api/chat",
|
|
1809
|
-
"web",
|
|
1810
|
-
),
|
|
1811
|
-
).toBe(true);
|
|
1812
|
-
expect(
|
|
1813
|
-
shouldUseAnonymousSessionFetch(
|
|
1814
|
-
"https://www.assistant-ui.com/api/chat",
|
|
1815
|
-
"ios",
|
|
1816
|
-
),
|
|
1817
|
-
).toBe(false);
|
|
1818
|
-
expect(shouldUseAnonymousSessionFetch("/api/chat", "web")).toBe(false);
|
|
1819
|
-
});
|
|
1820
|
-
|
|
1821
|
-
it("retries bootstrap on the next request after issuance fails", async () => {
|
|
1822
|
-
const fetchMock = vi
|
|
1823
|
-
.fn<typeof fetch>()
|
|
1824
|
-
.mockRejectedValueOnce(new Error("temporary network failure"))
|
|
1825
|
-
.mockResolvedValueOnce(new Response(null, { status: 403 }))
|
|
1826
|
-
.mockResolvedValueOnce(Response.json({ token: "signed-session" }))
|
|
1827
|
-
.mockResolvedValueOnce(new Response("ok"));
|
|
1828
|
-
const sessionFetch = createAnonymousSessionFetch(
|
|
1829
|
-
"https://www.assistant-ui.com/api/chat",
|
|
1830
|
-
fetchMock,
|
|
1831
|
-
);
|
|
1832
|
-
|
|
1833
|
-
const failedResponse = await sessionFetch(
|
|
1834
|
-
"https://www.assistant-ui.com/api/chat",
|
|
1835
|
-
{ method: "POST" },
|
|
1836
|
-
);
|
|
1837
|
-
const recoveredResponse = await sessionFetch(
|
|
1838
|
-
"https://www.assistant-ui.com/api/chat",
|
|
1839
|
-
{ method: "POST" },
|
|
1840
|
-
);
|
|
1841
|
-
|
|
1842
|
-
expect(failedResponse.status).toBe(403);
|
|
1843
|
-
expect(recoveredResponse.status).toBe(200);
|
|
1844
|
-
expect(fetchMock).toHaveBeenCalledTimes(4);
|
|
1845
|
-
expect(
|
|
1846
|
-
new Headers(fetchMock.mock.calls[1]?.[1]?.headers).get(
|
|
1847
|
-
"x-assistant-ui-anonymous-session",
|
|
1848
|
-
),
|
|
1849
|
-
).toBeNull();
|
|
1850
|
-
expect(
|
|
1851
|
-
new Headers(fetchMock.mock.calls[3]?.[1]?.headers).get(
|
|
1852
|
-
"x-assistant-ui-anonymous-session",
|
|
1853
|
-
),
|
|
1854
|
-
).toBe("signed-session");
|
|
1855
|
-
});
|
|
1856
|
-
|
|
1857
|
-
it("accepts a same-origin 204 cookie bootstrap", async () => {
|
|
1858
|
-
const fetchMock = vi
|
|
1859
|
-
.fn<typeof fetch>()
|
|
1860
|
-
.mockResolvedValueOnce(new Response(null, { status: 204 }))
|
|
1861
|
-
.mockResolvedValueOnce(new Response("ok"));
|
|
1862
|
-
const sessionFetch = createAnonymousSessionFetch(
|
|
1863
|
-
"https://example.com/api/chat",
|
|
1864
|
-
fetchMock,
|
|
1865
|
-
);
|
|
1866
|
-
|
|
1867
|
-
const response = await sessionFetch("https://example.com/api/chat", {
|
|
1868
|
-
method: "POST",
|
|
1869
|
-
});
|
|
1870
|
-
|
|
1871
|
-
expect(response.status).toBe(200);
|
|
1872
|
-
expect(fetchMock.mock.calls[0]?.[1]?.credentials).toBe("same-origin");
|
|
1873
|
-
expect(
|
|
1874
|
-
new Headers(fetchMock.mock.calls[1]?.[1]?.headers).get(
|
|
1875
|
-
"x-assistant-ui-anonymous-session",
|
|
1876
|
-
),
|
|
1877
|
-
).toBeNull();
|
|
1878
|
-
});
|
|
1879
|
-
});
|
|
1880
|
-
|
|
1881
|
-
```
|
|
1882
|
-
|
|
1883
|
-
## hooks/anonymous-session-fetch.ts
|
|
1884
|
-
|
|
1885
|
-
```typescript
|
|
1886
|
-
// Mirrored inline in examples/with-react-ink-web/app/ink-app.tsx; keep in sync.
|
|
1887
|
-
const ANONYMOUS_SESSION_HEADER = "x-assistant-ui-anonymous-session";
|
|
1888
|
-
|
|
1889
|
-
export function shouldUseAnonymousSessionFetch(
|
|
1890
|
-
chatApi: string,
|
|
1891
|
-
platform: string,
|
|
1892
|
-
): boolean {
|
|
1893
|
-
return platform === "web" && chatApi.startsWith("http");
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
export function createAnonymousSessionFetch(
|
|
1897
|
-
chatApi: string,
|
|
1898
|
-
fetchImplementation: typeof fetch = fetch,
|
|
1899
|
-
): typeof fetch {
|
|
1900
|
-
let tokenPromise: Promise<string | null> | null = null;
|
|
1901
|
-
|
|
1902
|
-
const getToken = () => {
|
|
1903
|
-
tokenPromise ??= (async () => {
|
|
1904
|
-
const endpoint = new URL("/api/anonymous-session", chatApi);
|
|
1905
|
-
const response = await fetchImplementation(endpoint, {
|
|
1906
|
-
credentials: "same-origin",
|
|
1907
|
-
});
|
|
1908
|
-
if (!response.ok) {
|
|
1909
|
-
throw new Error("Unable to start an anonymous session");
|
|
1910
|
-
}
|
|
1911
|
-
if (response.status === 204) return null;
|
|
1912
|
-
const payload = (await response.json()) as { token?: unknown };
|
|
1913
|
-
if (typeof payload.token !== "string" || !payload.token) {
|
|
1914
|
-
throw new Error("Anonymous session token is missing");
|
|
1915
|
-
}
|
|
1916
|
-
return payload.token;
|
|
1917
|
-
})().catch(() => {
|
|
1918
|
-
tokenPromise = null;
|
|
1919
|
-
return null;
|
|
1920
|
-
});
|
|
1921
|
-
return tokenPromise;
|
|
1922
|
-
};
|
|
1923
|
-
|
|
1924
|
-
return async (input, init) => {
|
|
1925
|
-
const requestTemplate =
|
|
1926
|
-
input instanceof Request ? new Request(input, init) : null;
|
|
1927
|
-
|
|
1928
|
-
const send = async () => {
|
|
1929
|
-
const headers = new Headers(requestTemplate?.headers ?? init?.headers);
|
|
1930
|
-
const token = await getToken();
|
|
1931
|
-
if (token) headers.set(ANONYMOUS_SESSION_HEADER, token);
|
|
1932
|
-
if (requestTemplate) {
|
|
1933
|
-
return fetchImplementation(requestTemplate.clone(), { headers });
|
|
1934
|
-
}
|
|
1935
|
-
return fetchImplementation(input, { ...init, headers });
|
|
1936
|
-
};
|
|
1937
|
-
|
|
1938
|
-
const response = await send();
|
|
1939
|
-
if (response.status !== 401) return response;
|
|
1940
|
-
tokenPromise = null;
|
|
1941
|
-
return send();
|
|
1942
|
-
};
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
```
|
|
1946
|
-
|
|
1947
|
-
## hooks/use-app-runtime.ts
|
|
1948
|
-
|
|
1949
|
-
```typescript
|
|
1950
|
-
import { useMemo } from "react";
|
|
1951
|
-
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";
|
|
1952
|
-
import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
|
|
1953
|
-
import { Platform } from "react-native";
|
|
1954
|
-
import {
|
|
1955
|
-
createAnonymousSessionFetch,
|
|
1956
|
-
shouldUseAnonymousSessionFetch,
|
|
1957
|
-
} from "./anonymous-session-fetch";
|
|
1958
|
-
|
|
1959
|
-
const CHAT_API = process.env.EXPO_PUBLIC_CHAT_ENDPOINT_URL ?? "/api/chat";
|
|
1960
|
-
|
|
1961
|
-
export function useAppRuntime() {
|
|
1962
|
-
const transport = useMemo(
|
|
1963
|
-
() =>
|
|
1964
|
-
new AssistantChatTransport({
|
|
1965
|
-
api: CHAT_API,
|
|
1966
|
-
...(shouldUseAnonymousSessionFetch(CHAT_API, Platform.OS)
|
|
1967
|
-
? { fetch: createAnonymousSessionFetch(CHAT_API) }
|
|
1968
|
-
: {}),
|
|
1969
|
-
}),
|
|
1970
|
-
[],
|
|
1971
|
-
);
|
|
1972
|
-
return useChatRuntime({
|
|
1973
|
-
transport,
|
|
1974
|
-
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
1975
|
-
});
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
```
|
|
1979
|
-
|
|
1980
|
-
## hooks/use-color-scheme.ts
|
|
1981
|
-
|
|
1982
|
-
```typescript
|
|
1983
|
-
export { useColorScheme } from "react-native";
|
|
1984
|
-
|
|
1985
|
-
```
|
|
1986
|
-
|
|
1987
|
-
## hooks/use-color-scheme.web.ts
|
|
1988
|
-
|
|
1989
|
-
```typescript
|
|
1990
|
-
import { useEffect, useState } from "react";
|
|
1991
|
-
import { useColorScheme as useRNColorScheme } from "react-native";
|
|
1992
|
-
|
|
1993
|
-
/**
|
|
1994
|
-
* To support static rendering, this value needs to be re-calculated on the client side for web
|
|
1995
|
-
*/
|
|
1996
|
-
export function useColorScheme() {
|
|
1997
|
-
const [hasHydrated, setHasHydrated] = useState(false);
|
|
1998
|
-
|
|
1999
|
-
useEffect(() => {
|
|
2000
|
-
setHasHydrated(true);
|
|
2001
|
-
}, []);
|
|
2002
|
-
|
|
2003
|
-
const colorScheme = useRNColorScheme();
|
|
2004
|
-
|
|
2005
|
-
if (hasHydrated) {
|
|
2006
|
-
return colorScheme;
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
return "light";
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
```
|
|
2013
|
-
|
|
2014
|
-
## hooks/use-theme.ts
|
|
2015
|
-
|
|
2016
|
-
```typescript
|
|
2017
|
-
import { Colors, type Palette } from "@/constants/theme";
|
|
2018
|
-
import { useColorScheme } from "@/hooks/use-color-scheme";
|
|
2019
|
-
|
|
2020
|
-
export function useTheme(): { isDark: boolean; colors: Palette } {
|
|
2021
|
-
const isDark = useColorScheme() === "dark";
|
|
2022
|
-
return { isDark, colors: isDark ? Colors.dark : Colors.light };
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
```
|
|
2026
|
-
|
|
2027
|
-
## lib/haptics.ts
|
|
2028
|
-
|
|
2029
|
-
```typescript
|
|
2030
|
-
import { Platform } from "react-native";
|
|
2031
|
-
import * as Haptics from "expo-haptics";
|
|
2032
|
-
|
|
2033
|
-
const enabled = Platform.OS === "ios" || Platform.OS === "android";
|
|
2034
|
-
|
|
2035
|
-
export const haptics = {
|
|
2036
|
-
selection() {
|
|
2037
|
-
if (enabled) void Haptics.selectionAsync();
|
|
2038
|
-
},
|
|
2039
|
-
light() {
|
|
2040
|
-
if (enabled) void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
2041
|
-
},
|
|
2042
|
-
success() {
|
|
2043
|
-
if (enabled)
|
|
2044
|
-
void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
|
|
2045
|
-
},
|
|
2046
|
-
};
|
|
2047
|
-
|
|
2048
|
-
```
|
|
2049
|
-
|
|
2050
|
-
## metro.config.js
|
|
2051
|
-
|
|
2052
|
-
```javascript
|
|
2053
|
-
const { getDefaultConfig } = require("expo/metro-config");
|
|
2054
|
-
const { withAui } = require("@assistant-ui/metro");
|
|
2055
|
-
const path = require("node:path");
|
|
2056
|
-
|
|
2057
|
-
const projectRoot = __dirname;
|
|
2058
|
-
const monorepoRoot = path.resolve(projectRoot, "../..");
|
|
2059
|
-
|
|
2060
|
-
const config = getDefaultConfig(projectRoot);
|
|
2061
|
-
|
|
2062
|
-
// Watch all files within the monorepo
|
|
2063
|
-
config.watchFolders = [monorepoRoot];
|
|
2064
|
-
|
|
2065
|
-
// Enable symlinks support for pnpm
|
|
2066
|
-
config.resolver.unstable_enableSymlinks = true;
|
|
2067
|
-
|
|
2068
|
-
// Let Metro know where to resolve packages
|
|
2069
|
-
config.resolver.nodeModulesPaths = [
|
|
2070
|
-
path.resolve(projectRoot, "node_modules"),
|
|
2071
|
-
path.resolve(monorepoRoot, "node_modules"),
|
|
2072
|
-
];
|
|
2073
|
-
|
|
2074
|
-
// Force resolving shared dependencies from the app's node_modules
|
|
2075
|
-
config.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
2076
|
-
if (
|
|
2077
|
-
moduleName === "react" ||
|
|
2078
|
-
moduleName === "react-native" ||
|
|
2079
|
-
moduleName.startsWith("react/") ||
|
|
2080
|
-
moduleName.startsWith("react-native/")
|
|
2081
|
-
) {
|
|
2082
|
-
return context.resolveRequest(
|
|
2083
|
-
{
|
|
2084
|
-
...context,
|
|
2085
|
-
originModulePath: path.resolve(projectRoot, "package.json"),
|
|
2086
|
-
},
|
|
2087
|
-
moduleName,
|
|
2088
|
-
platform,
|
|
2089
|
-
);
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
return context.resolveRequest(context, moduleName, platform);
|
|
2093
|
-
};
|
|
2094
|
-
|
|
2095
|
-
module.exports = withAui(config);
|
|
2096
|
-
|
|
2097
|
-
```
|
|
2098
|
-
|
|
2099
|
-
## package.json
|
|
2100
|
-
|
|
2101
|
-
```json
|
|
2102
|
-
{
|
|
2103
|
-
"name": "with-expo",
|
|
2104
|
-
"main": "expo-router/entry",
|
|
2105
|
-
"version": "0.0.0",
|
|
2106
|
-
"scripts": {
|
|
2107
|
-
"start": "expo start",
|
|
2108
|
-
"android": "expo run:android",
|
|
2109
|
-
"ios": "expo run:ios",
|
|
2110
|
-
"web": "expo start --web",
|
|
2111
|
-
"export:web": "expo export --platform web && node scripts/flatten-assets.mjs",
|
|
2112
|
-
"test": "vitest run"
|
|
2113
|
-
},
|
|
2114
|
-
"dependencies": {
|
|
2115
|
-
"@ai-sdk/openai": "^4.0.42",
|
|
2116
|
-
"@ai-sdk/react": "^4.0.69",
|
|
2117
|
-
"@assistant-ui/ai-sdk": "workspace:*",
|
|
2118
|
-
"@assistant-ui/react-native": "workspace:*",
|
|
2119
|
-
"@expo/vector-icons": "^15.1.1",
|
|
2120
|
-
"@react-navigation/drawer": "7.9.4",
|
|
2121
|
-
"@react-navigation/native": "7.1.33",
|
|
2122
|
-
"ai": "^7.0.66",
|
|
2123
|
-
"expo": "~57.0.16",
|
|
2124
|
-
"expo-clipboard": "~57.0.1",
|
|
2125
|
-
"expo-constants": "~57.0.14",
|
|
2126
|
-
"expo-font": "~57.0.1",
|
|
2127
|
-
"expo-haptics": "~57.0.1",
|
|
2128
|
-
"expo-image": "~57.0.3",
|
|
2129
|
-
"expo-image-picker": "~57.0.13",
|
|
2130
|
-
"expo-linking": "~57.0.6",
|
|
2131
|
-
"expo-router": "~57.0.16",
|
|
2132
|
-
"expo-server": "~57.0.3",
|
|
2133
|
-
"expo-splash-screen": "~57.0.8",
|
|
2134
|
-
"expo-status-bar": "~57.0.1",
|
|
2135
|
-
"expo-symbols": "^57.0.2",
|
|
2136
|
-
"expo-system-ui": "~57.0.2",
|
|
2137
|
-
"react": "19.2.3",
|
|
2138
|
-
"react-dom": "19.2.3",
|
|
2139
|
-
"react-native": "0.86.2",
|
|
2140
|
-
"react-native-gesture-handler": "~2.32.0",
|
|
2141
|
-
"react-native-reanimated": "4.5.1",
|
|
2142
|
-
"react-native-safe-area-context": "~5.7.0",
|
|
2143
|
-
"react-native-screens": "4.26.2",
|
|
2144
|
-
"react-native-web": "~0.21.2",
|
|
2145
|
-
"react-native-worklets": "0.10.1",
|
|
2146
|
-
"zod": "^4.4.3"
|
|
2147
|
-
},
|
|
2148
|
-
"devDependencies": {
|
|
2149
|
-
"@assistant-ui/metro": "workspace:*",
|
|
2150
|
-
"@babel/core": "^7.29.7",
|
|
2151
|
-
"@types/react": "~19.2.18",
|
|
2152
|
-
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
2153
|
-
"typescript-7": "npm:typescript@~7.0.2",
|
|
2154
|
-
"vitest": "^4.1.10"
|
|
2155
|
-
},
|
|
2156
|
-
"private": true
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
```
|
|
2160
|
-
|
|
2161
|
-
## README.md
|
|
2162
|
-
|
|
2163
|
-
```markdown
|
|
2164
|
-
# assistant-ui + Expo
|
|
2165
|
-
|
|
2166
|
-
A native chat app built with [assistant-ui](https://www.assistant-ui.com) and [Expo](https://expo.dev). It runs on iOS, Android, and the web from a single codebase, and is styled to match the assistant-ui web kit: a clean, neutral, ChatGPT-grade look with subtle hairline borders.
|
|
2167
|
-
|
|
2168
|
-
The chat is powered by `@assistant-ui/react-native` with the AI SDK runtime (`@assistant-ui/ai-sdk`). The example leans on native Expo APIs throughout:
|
|
2169
|
-
|
|
2170
|
-
- **SF Symbols** (`expo-symbols`) for native iconography on iOS, with a Material Icons fallback on Android and the web.
|
|
2171
|
-
- **Haptics** (`expo-haptics`) for tactile feedback on send, stop, and selection.
|
|
2172
|
-
- **Image picker** (`expo-image-picker`) and **expo-image** for attachments.
|
|
2173
|
-
- **Clipboard** (`expo-clipboard`) so the copy action works natively.
|
|
2174
|
-
- A native **drawer** (`@react-navigation/drawer`) for the thread list, with a swipe gesture to switch conversations.
|
|
2175
|
-
|
|
2176
|
-
It follows the assistant-ui component conventions: `MessagePrimitive.Parts` for message rendering, `AuiIf` for declarative state-driven UI, the `ThreadList` / `ThreadListItem` primitives for the drawer, and a `"use generative"` toolkit (`components/assistant-ui/tools.tsx`) that renders weather cards inline.
|
|
2177
|
-
|
|
2178
|
-
## Get started
|
|
2179
|
-
|
|
2180
|
-
1. Install dependencies from the repository root:
|
|
2181
|
-
|
|
2182
|
-
```bash
|
|
2183
|
-
pnpm install
|
|
2184
|
-
```
|
|
2185
|
-
|
|
2186
|
-
2. Configure the chat backend. Copy `.env.example` to `.env` and set your key:
|
|
2187
|
-
|
|
2188
|
-
```bash
|
|
2189
|
-
cp .env.example .env
|
|
2190
|
-
```
|
|
2191
|
-
|
|
2192
|
-
The bundled API route (`app/api/chat+api.ts`) needs `OPENAI_API_KEY`. To point the app at a separately hosted backend instead, set `EXPO_PUBLIC_CHAT_ENDPOINT_URL`. Browser deployments use `/api/anonymous-session` when the hosted backend supports it and otherwise send the request normally. Native deployments should use a backend intended for their app.
|
|
2193
|
-
|
|
2194
|
-
3. Start the app:
|
|
2195
|
-
|
|
2196
|
-
```bash
|
|
2197
|
-
pnpm --filter with-expo start
|
|
2198
|
-
```
|
|
2199
|
-
|
|
2200
|
-
From there you can open it in the iOS simulator, an Android emulator, or the browser.
|
|
2201
|
-
|
|
2202
|
-
## Project structure
|
|
2203
|
-
|
|
2204
|
-
- `app/_layout.tsx` wires the runtime, the toolkit, and the drawer navigation.
|
|
2205
|
-
- `app/index.tsx` renders the `Thread`.
|
|
2206
|
-
- `components/assistant-ui/` holds the composer, message, action bar, branch picker, and tool UIs.
|
|
2207
|
-
- `components/thread-list/` holds the drawer thread list.
|
|
2208
|
-
- `constants/theme.ts` and `hooks/use-theme.ts` define the shared design tokens.
|
|
2209
|
-
- `components/ui/icon.tsx` is the cross-platform icon (SF Symbols on iOS, Material Icons elsewhere).
|
|
2210
|
-
|
|
2211
|
-
## Learn more
|
|
2212
|
-
|
|
2213
|
-
- [assistant-ui documentation](https://www.assistant-ui.com/docs)
|
|
2214
|
-
- [Expo documentation](https://docs.expo.dev)
|
|
2215
|
-
|
|
2216
|
-
```
|
|
2217
|
-
|
|
2218
|
-
## tsconfig.json
|
|
2219
|
-
|
|
2220
|
-
```json
|
|
2221
|
-
{
|
|
2222
|
-
"extends": "expo/tsconfig.base",
|
|
2223
|
-
"compilerOptions": {
|
|
2224
|
-
"strict": true,
|
|
2225
|
-
"paths": {
|
|
2226
|
-
"@/*": ["./*"]
|
|
2227
|
-
}
|
|
2228
|
-
},
|
|
2229
|
-
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
|
2230
|
-
}
|
|
2231
|
-
|
|
2232
|
-
```
|
|
2233
|
-
|
|
2234
|
-
## vercel.json
|
|
2235
|
-
|
|
2236
|
-
```json
|
|
2237
|
-
{
|
|
2238
|
-
"$schema": "https://openapi.vercel.sh/vercel.json",
|
|
2239
|
-
"ignoreCommand": "bash ../../scripts/vercel-ignore-changeset-release.sh",
|
|
2240
|
-
"buildCommand": "cd ../.. && pnpm turbo build --filter=@assistant-ui/react-native --filter=@assistant-ui/ai-sdk --filter=@assistant-ui/metro && cd examples/with-expo && pnpm run export:web",
|
|
2241
|
-
"outputDirectory": "dist",
|
|
2242
|
-
"framework": null
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
```
|
|
2246
|
-
|