@assistant-ui/mcp-docs-server 0.1.17 → 0.1.19

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.
Files changed (202) hide show
  1. package/.docs/organized/code-examples/with-ag-ui.md +518 -234
  2. package/.docs/organized/code-examples/{with-ai-sdk-v5.md → with-ai-sdk-v6.md} +476 -189
  3. package/.docs/organized/code-examples/with-assistant-transport.md +503 -301
  4. package/.docs/organized/code-examples/with-cloud.md +524 -226
  5. package/.docs/organized/code-examples/with-custom-thread-list.md +433 -146
  6. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +2241 -0
  7. package/.docs/organized/code-examples/with-external-store.md +517 -231
  8. package/.docs/organized/code-examples/with-ffmpeg.md +500 -220
  9. package/.docs/organized/code-examples/with-langgraph.md +630 -319
  10. package/.docs/organized/code-examples/with-parent-id-grouping.md +517 -231
  11. package/.docs/organized/code-examples/with-react-hook-form.md +517 -233
  12. package/.docs/organized/code-examples/with-react-router.md +2167 -0
  13. package/.docs/organized/code-examples/{store-example.md → with-store.md} +18 -22
  14. package/.docs/organized/code-examples/with-tanstack.md +23 -41
  15. package/.docs/raw/blog/2025-01-31-changelog/index.mdx +0 -2
  16. package/.docs/raw/docs/{about-assistantui.mdx → (docs)/about-assistantui.mdx} +2 -1
  17. package/.docs/raw/docs/{architecture.mdx → (docs)/architecture.mdx} +3 -2
  18. package/.docs/raw/docs/{cli.mdx → (docs)/cli.mdx} +1 -19
  19. package/.docs/raw/docs/{copilots → (docs)/copilots}/make-assistant-readable.mdx +1 -0
  20. package/.docs/raw/docs/{copilots → (docs)/copilots}/make-assistant-tool-ui.mdx +2 -1
  21. package/.docs/raw/docs/{copilots → (docs)/copilots}/make-assistant-tool.mdx +2 -1
  22. package/.docs/raw/docs/{copilots → (docs)/copilots}/model-context.mdx +1 -0
  23. package/.docs/raw/docs/{copilots → (docs)/copilots}/motivation.mdx +1 -0
  24. package/.docs/raw/docs/{copilots → (docs)/copilots}/use-assistant-instructions.mdx +1 -0
  25. package/.docs/raw/docs/{devtools.mdx → (docs)/devtools.mdx} +4 -4
  26. package/.docs/raw/docs/{guides/Attachments.mdx → (docs)/guides/attachments.mdx} +4 -5
  27. package/.docs/raw/docs/{guides/Branching.mdx → (docs)/guides/branching.mdx} +2 -1
  28. package/.docs/raw/docs/{guides → (docs)/guides}/context-api.mdx +1 -0
  29. package/.docs/raw/docs/(docs)/guides/dictation.mdx +370 -0
  30. package/.docs/raw/docs/{guides/Editing.mdx → (docs)/guides/editing.mdx} +1 -0
  31. package/.docs/raw/docs/{guides/Latex.mdx → (docs)/guides/latex.mdx} +1 -2
  32. package/.docs/raw/docs/{guides/Speech.mdx → (docs)/guides/speech.mdx} +9 -10
  33. package/.docs/raw/docs/{guides/ToolUI.mdx → (docs)/guides/tool-ui.mdx} +15 -14
  34. package/.docs/raw/docs/{guides/Tools.mdx → (docs)/guides/tools.mdx} +10 -7
  35. package/.docs/raw/docs/{getting-started.mdx → (docs)/index.mdx} +17 -22
  36. package/.docs/raw/docs/{mcp-docs-server.mdx → (docs)/mcp-docs-server.mdx} +1 -2
  37. package/.docs/raw/docs/{api-reference/context-providers/AssistantRuntimeProvider.mdx → (reference)/api-reference/context-providers/assistant-runtime-provider.mdx} +2 -1
  38. package/.docs/raw/docs/{api-reference/context-providers/TextMessagePartProvider.mdx → (reference)/api-reference/context-providers/text-message-part-provider.mdx} +2 -1
  39. package/.docs/raw/docs/{api-reference → (reference)/api-reference}/integrations/react-data-stream.mdx +2 -1
  40. package/.docs/raw/docs/{api-reference → (reference)/api-reference}/integrations/react-hook-form.mdx +2 -1
  41. package/.docs/raw/docs/{api-reference → (reference)/api-reference}/integrations/vercel-ai-sdk.mdx +2 -2
  42. package/.docs/raw/docs/{api-reference → (reference)/api-reference}/overview.mdx +1 -1
  43. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar-more.mdx +327 -0
  44. package/.docs/raw/docs/{api-reference/primitives/ActionBar.mdx → (reference)/api-reference/primitives/action-bar.mdx} +3 -1
  45. package/.docs/raw/docs/{api-reference/primitives/AssistantIf.mdx → (reference)/api-reference/primitives/assistant-if.mdx} +2 -2
  46. package/.docs/raw/docs/{api-reference/primitives/AssistantModal.mdx → (reference)/api-reference/primitives/assistant-modal.mdx} +3 -1
  47. package/.docs/raw/docs/{api-reference/primitives/Attachment.mdx → (reference)/api-reference/primitives/attachment.mdx} +3 -2
  48. package/.docs/raw/docs/{api-reference/primitives/BranchPicker.mdx → (reference)/api-reference/primitives/branch-picker.mdx} +2 -1
  49. package/.docs/raw/docs/{api-reference/primitives/Composer.mdx → (reference)/api-reference/primitives/composer.mdx} +101 -2
  50. package/.docs/raw/docs/{api-reference → (reference)/api-reference}/primitives/composition.mdx +1 -0
  51. package/.docs/raw/docs/{api-reference/primitives/Error.mdx → (reference)/api-reference/primitives/error.mdx} +2 -1
  52. package/.docs/raw/docs/{api-reference/primitives/MessagePart.mdx → (reference)/api-reference/primitives/message-part.mdx} +2 -2
  53. package/.docs/raw/docs/{api-reference/primitives/Message.mdx → (reference)/api-reference/primitives/message.mdx} +2 -1
  54. package/.docs/raw/docs/(reference)/api-reference/primitives/thread-list-item-more.mdx +221 -0
  55. package/.docs/raw/docs/{api-reference/primitives/ThreadListItem.mdx → (reference)/api-reference/primitives/thread-list-item.mdx} +2 -1
  56. package/.docs/raw/docs/{api-reference/primitives/ThreadList.mdx → (reference)/api-reference/primitives/thread-list.mdx} +2 -1
  57. package/.docs/raw/docs/{api-reference/primitives/Thread.mdx → (reference)/api-reference/primitives/thread.mdx} +2 -1
  58. package/.docs/raw/docs/{api-reference/runtimes/AssistantRuntime.mdx → (reference)/api-reference/runtimes/assistant-runtime.mdx} +2 -1
  59. package/.docs/raw/docs/{api-reference/runtimes/AttachmentRuntime.mdx → (reference)/api-reference/runtimes/attachment-runtime.mdx} +3 -2
  60. package/.docs/raw/docs/{api-reference/runtimes/ComposerRuntime.mdx → (reference)/api-reference/runtimes/composer-runtime.mdx} +2 -1
  61. package/.docs/raw/docs/{api-reference/runtimes/MessagePartRuntime.mdx → (reference)/api-reference/runtimes/message-part-runtime.mdx} +3 -2
  62. package/.docs/raw/docs/{api-reference/runtimes/MessageRuntime.mdx → (reference)/api-reference/runtimes/message-runtime.mdx} +3 -2
  63. package/.docs/raw/docs/{api-reference/runtimes/ThreadListItemRuntime.mdx → (reference)/api-reference/runtimes/thread-list-item-runtime.mdx} +2 -1
  64. package/.docs/raw/docs/{api-reference/runtimes/ThreadListRuntime.mdx → (reference)/api-reference/runtimes/thread-list-runtime.mdx} +2 -1
  65. package/.docs/raw/docs/{api-reference/runtimes/ThreadRuntime.mdx → (reference)/api-reference/runtimes/thread-runtime.mdx} +3 -5
  66. package/.docs/raw/docs/{legacy/styled/AssistantModal.mdx → (reference)/legacy/styled/assistant-modal.mdx} +2 -3
  67. package/.docs/raw/docs/{legacy/styled/Decomposition.mdx → (reference)/legacy/styled/decomposition.mdx} +1 -0
  68. package/.docs/raw/docs/{legacy/styled/Markdown.mdx → (reference)/legacy/styled/markdown.mdx} +2 -4
  69. package/.docs/raw/docs/{legacy/styled/Scrollbar.mdx → (reference)/legacy/styled/scrollbar.mdx} +2 -1
  70. package/.docs/raw/docs/{legacy/styled/ThreadWidth.mdx → (reference)/legacy/styled/thread-width.mdx} +1 -0
  71. package/.docs/raw/docs/{legacy/styled/Thread.mdx → (reference)/legacy/styled/thread.mdx} +2 -3
  72. package/.docs/raw/docs/{migrations → (reference)/migrations}/deprecation-policy.mdx +1 -0
  73. package/.docs/raw/docs/{migrations → (reference)/migrations}/react-langgraph-v0-7.mdx +1 -2
  74. package/.docs/raw/docs/{migrations → (reference)/migrations}/v0-11.mdx +1 -0
  75. package/.docs/raw/docs/{migrations → (reference)/migrations}/v0-12.mdx +1 -0
  76. package/.docs/raw/docs/{react-compatibility.mdx → (reference)/react-compatibility.mdx} +2 -3
  77. package/.docs/raw/docs/cloud/authorization.mdx +1 -0
  78. package/.docs/raw/docs/cloud/overview.mdx +1 -0
  79. package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +2 -3
  80. package/.docs/raw/docs/cloud/persistence/langgraph.mdx +5 -7
  81. package/.docs/raw/docs/runtimes/ai-sdk/use-chat.mdx +9 -8
  82. package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +2 -3
  83. package/.docs/raw/docs/runtimes/assistant-transport.mdx +7 -6
  84. package/.docs/raw/docs/runtimes/custom/custom-thread-list.mdx +38 -3
  85. package/.docs/raw/docs/runtimes/custom/external-store.mdx +6 -8
  86. package/.docs/raw/docs/runtimes/custom/local.mdx +43 -16
  87. package/.docs/raw/docs/runtimes/data-stream.mdx +32 -4
  88. package/.docs/raw/docs/runtimes/helicone.mdx +1 -0
  89. package/.docs/raw/docs/runtimes/langgraph/index.mdx +3 -3
  90. package/.docs/raw/docs/runtimes/langgraph/tutorial/index.mdx +1 -0
  91. package/.docs/raw/docs/runtimes/langgraph/tutorial/introduction.mdx +1 -0
  92. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +1 -0
  93. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +1 -0
  94. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +2 -1
  95. package/.docs/raw/docs/runtimes/langserve.mdx +2 -2
  96. package/.docs/raw/docs/runtimes/mastra/full-stack-integration.mdx +4 -5
  97. package/.docs/raw/docs/runtimes/mastra/overview.mdx +1 -0
  98. package/.docs/raw/docs/runtimes/mastra/separate-server-integration.mdx +3 -4
  99. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +2 -4
  100. package/.docs/raw/docs/ui/assistant-modal.mdx +163 -0
  101. package/.docs/raw/docs/ui/assistant-sidebar.mdx +90 -0
  102. package/.docs/raw/docs/ui/attachment.mdx +227 -0
  103. package/.docs/raw/docs/ui/{Markdown.mdx → markdown.mdx} +11 -6
  104. package/.docs/raw/docs/ui/{Mermaid.mdx → mermaid.mdx} +12 -5
  105. package/.docs/raw/docs/ui/{PartGrouping.mdx → part-grouping.mdx} +4 -6
  106. package/.docs/raw/docs/ui/reasoning.mdx +148 -0
  107. package/.docs/raw/docs/ui/{Scrollbar.mdx → scrollbar.mdx} +9 -7
  108. package/.docs/raw/docs/ui/sources.mdx +87 -0
  109. package/.docs/raw/docs/ui/{SyntaxHighlighting.mdx → syntax-highlighting.mdx} +9 -5
  110. package/.docs/raw/docs/ui/thread-list.mdx +275 -0
  111. package/.docs/raw/docs/ui/{Thread.mdx → thread.mdx} +5 -6
  112. package/.docs/raw/docs/ui/tool-fallback.mdx +112 -0
  113. package/.docs/raw/docs/ui/tool-group.mdx +214 -0
  114. package/dist/constants.d.ts +10 -0
  115. package/dist/constants.d.ts.map +1 -0
  116. package/dist/constants.js +14 -0
  117. package/dist/constants.js.map +1 -0
  118. package/dist/index.d.ts +4 -0
  119. package/dist/index.d.ts.map +1 -0
  120. package/dist/index.js +33 -1
  121. package/dist/index.js.map +1 -0
  122. package/dist/prepare-docs/code-examples.d.ts +2 -0
  123. package/dist/prepare-docs/code-examples.d.ts.map +1 -0
  124. package/dist/prepare-docs/code-examples.js +129 -0
  125. package/dist/prepare-docs/code-examples.js.map +1 -0
  126. package/dist/prepare-docs/copy-raw.d.ts +2 -0
  127. package/dist/prepare-docs/copy-raw.d.ts.map +1 -0
  128. package/dist/prepare-docs/copy-raw.js +50 -0
  129. package/dist/prepare-docs/copy-raw.js.map +1 -0
  130. package/dist/prepare-docs/prepare.d.ts +2 -0
  131. package/dist/prepare-docs/prepare.d.ts.map +1 -0
  132. package/dist/prepare-docs/prepare.js +18 -195
  133. package/dist/prepare-docs/prepare.js.map +1 -0
  134. package/dist/stdio.d.ts +3 -0
  135. package/dist/stdio.d.ts.map +1 -0
  136. package/dist/stdio.js +4 -5
  137. package/dist/stdio.js.map +1 -0
  138. package/dist/tools/docs.d.ts +23 -0
  139. package/dist/tools/docs.d.ts.map +1 -0
  140. package/dist/tools/docs.js +168 -0
  141. package/dist/tools/docs.js.map +1 -0
  142. package/dist/tools/examples.d.ts +23 -0
  143. package/dist/tools/examples.d.ts.map +1 -0
  144. package/dist/tools/examples.js +95 -0
  145. package/dist/tools/examples.js.map +1 -0
  146. package/dist/tools/tests/test-setup.d.ts +4 -0
  147. package/dist/tools/tests/test-setup.d.ts.map +1 -0
  148. package/dist/tools/tests/test-setup.js +36 -0
  149. package/dist/tools/tests/test-setup.js.map +1 -0
  150. package/dist/utils/logger.d.ts +7 -0
  151. package/dist/utils/logger.d.ts.map +1 -0
  152. package/dist/utils/logger.js +20 -0
  153. package/dist/utils/logger.js.map +1 -0
  154. package/dist/utils/mcp-format.d.ts +7 -0
  155. package/dist/utils/mcp-format.d.ts.map +1 -0
  156. package/dist/utils/mcp-format.js +11 -0
  157. package/dist/utils/mcp-format.js.map +1 -0
  158. package/dist/utils/mdx.d.ts +9 -0
  159. package/dist/utils/mdx.d.ts.map +1 -0
  160. package/dist/utils/mdx.js +27 -0
  161. package/dist/utils/mdx.js.map +1 -0
  162. package/dist/utils/paths.d.ts +8 -0
  163. package/dist/utils/paths.d.ts.map +1 -0
  164. package/dist/utils/paths.js +84 -0
  165. package/dist/utils/paths.js.map +1 -0
  166. package/dist/utils/security.d.ts +2 -0
  167. package/dist/utils/security.d.ts.map +1 -0
  168. package/dist/utils/security.js +43 -0
  169. package/dist/utils/security.js.map +1 -0
  170. package/package.json +37 -19
  171. package/src/constants.ts +22 -0
  172. package/src/index.ts +51 -0
  173. package/src/prepare-docs/code-examples.ts +158 -0
  174. package/src/prepare-docs/copy-raw.ts +55 -0
  175. package/src/prepare-docs/prepare.ts +24 -0
  176. package/src/stdio.ts +7 -0
  177. package/src/tools/docs.ts +207 -0
  178. package/src/tools/examples.ts +107 -0
  179. package/src/tools/tests/docs.test.ts +124 -0
  180. package/src/tools/tests/examples.test.ts +94 -0
  181. package/src/tools/tests/integration.test.ts +46 -0
  182. package/src/tools/tests/json-parsing.test.ts +23 -0
  183. package/src/tools/tests/mcp-protocol.test.ts +133 -0
  184. package/src/tools/tests/path-traversal.test.ts +81 -0
  185. package/src/tools/tests/test-setup.ts +40 -0
  186. package/src/utils/logger.ts +20 -0
  187. package/src/utils/mcp-format.ts +12 -0
  188. package/src/utils/mdx.ts +39 -0
  189. package/src/utils/paths.ts +114 -0
  190. package/src/utils/security.ts +52 -0
  191. package/src/utils/tests/security.test.ts +119 -0
  192. package/.docs/raw/docs/index.mdx +0 -7
  193. package/.docs/raw/docs/ui/AssistantModal.mdx +0 -45
  194. package/.docs/raw/docs/ui/AssistantSidebar.mdx +0 -41
  195. package/.docs/raw/docs/ui/Attachment.mdx +0 -84
  196. package/.docs/raw/docs/ui/Reasoning.mdx +0 -152
  197. package/.docs/raw/docs/ui/ThreadList.mdx +0 -90
  198. package/.docs/raw/docs/ui/ToolFallback.mdx +0 -63
  199. package/.docs/raw/docs/ui/ToolGroup.mdx +0 -96
  200. package/dist/chunk-M2RKUM66.js +0 -38
  201. package/dist/chunk-NVNFQ5ZO.js +0 -423
  202. /package/.docs/raw/docs/{copilots → (docs)/copilots}/assistant-frame.mdx +0 -0
@@ -0,0 +1,2167 @@
1
+ # Example: with-react-router
2
+
3
+ ## app/app.css
4
+
5
+ ```css
6
+ @import "tailwindcss";
7
+ @import "tw-animate-css";
8
+
9
+ @custom-variant dark (&:is(.dark *));
10
+
11
+ @theme {
12
+ --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
13
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
14
+ }
15
+
16
+ html,
17
+ body {
18
+ @apply bg-white dark:bg-gray-950;
19
+
20
+ @media (prefers-color-scheme: dark) {
21
+ color-scheme: dark;
22
+ }
23
+ }
24
+
25
+ @theme inline {
26
+ --radius-sm: calc(var(--radius) - 4px);
27
+ --radius-md: calc(var(--radius) - 2px);
28
+ --radius-lg: var(--radius);
29
+ --radius-xl: calc(var(--radius) + 4px);
30
+ --color-background: var(--background);
31
+ --color-foreground: var(--foreground);
32
+ --color-card: var(--card);
33
+ --color-card-foreground: var(--card-foreground);
34
+ --color-popover: var(--popover);
35
+ --color-popover-foreground: var(--popover-foreground);
36
+ --color-primary: var(--primary);
37
+ --color-primary-foreground: var(--primary-foreground);
38
+ --color-secondary: var(--secondary);
39
+ --color-secondary-foreground: var(--secondary-foreground);
40
+ --color-muted: var(--muted);
41
+ --color-muted-foreground: var(--muted-foreground);
42
+ --color-accent: var(--accent);
43
+ --color-accent-foreground: var(--accent-foreground);
44
+ --color-destructive: var(--destructive);
45
+ --color-border: var(--border);
46
+ --color-input: var(--input);
47
+ --color-ring: var(--ring);
48
+ --color-chart-1: var(--chart-1);
49
+ --color-chart-2: var(--chart-2);
50
+ --color-chart-3: var(--chart-3);
51
+ --color-chart-4: var(--chart-4);
52
+ --color-chart-5: var(--chart-5);
53
+ --color-sidebar: var(--sidebar);
54
+ --color-sidebar-foreground: var(--sidebar-foreground);
55
+ --color-sidebar-primary: var(--sidebar-primary);
56
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
57
+ --color-sidebar-accent: var(--sidebar-accent);
58
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
59
+ --color-sidebar-border: var(--sidebar-border);
60
+ --color-sidebar-ring: var(--sidebar-ring);
61
+ }
62
+
63
+ :root {
64
+ --radius: 0.625rem;
65
+ --background: oklch(1 0 0);
66
+ --foreground: oklch(0.145 0 0);
67
+ --card: oklch(1 0 0);
68
+ --card-foreground: oklch(0.145 0 0);
69
+ --popover: oklch(1 0 0);
70
+ --popover-foreground: oklch(0.145 0 0);
71
+ --primary: oklch(0.205 0 0);
72
+ --primary-foreground: oklch(0.985 0 0);
73
+ --secondary: oklch(0.97 0 0);
74
+ --secondary-foreground: oklch(0.205 0 0);
75
+ --muted: oklch(0.97 0 0);
76
+ --muted-foreground: oklch(0.556 0 0);
77
+ --accent: oklch(0.97 0 0);
78
+ --accent-foreground: oklch(0.205 0 0);
79
+ --destructive: oklch(0.577 0.245 27.325);
80
+ --border: oklch(0.922 0 0);
81
+ --input: oklch(0.922 0 0);
82
+ --ring: oklch(0.708 0 0);
83
+ --chart-1: oklch(0.646 0.222 41.116);
84
+ --chart-2: oklch(0.6 0.118 184.704);
85
+ --chart-3: oklch(0.398 0.07 227.392);
86
+ --chart-4: oklch(0.828 0.189 84.429);
87
+ --chart-5: oklch(0.769 0.188 70.08);
88
+ --sidebar: oklch(0.985 0 0);
89
+ --sidebar-foreground: oklch(0.145 0 0);
90
+ --sidebar-primary: oklch(0.205 0 0);
91
+ --sidebar-primary-foreground: oklch(0.985 0 0);
92
+ --sidebar-accent: oklch(0.97 0 0);
93
+ --sidebar-accent-foreground: oklch(0.205 0 0);
94
+ --sidebar-border: oklch(0.922 0 0);
95
+ --sidebar-ring: oklch(0.708 0 0);
96
+ }
97
+
98
+ .dark {
99
+ --background: oklch(0.145 0 0);
100
+ --foreground: oklch(0.985 0 0);
101
+ --card: oklch(0.205 0 0);
102
+ --card-foreground: oklch(0.985 0 0);
103
+ --popover: oklch(0.205 0 0);
104
+ --popover-foreground: oklch(0.985 0 0);
105
+ --primary: oklch(0.922 0 0);
106
+ --primary-foreground: oklch(0.205 0 0);
107
+ --secondary: oklch(0.269 0 0);
108
+ --secondary-foreground: oklch(0.985 0 0);
109
+ --muted: oklch(0.269 0 0);
110
+ --muted-foreground: oklch(0.708 0 0);
111
+ --accent: oklch(0.269 0 0);
112
+ --accent-foreground: oklch(0.985 0 0);
113
+ --destructive: oklch(0.704 0.191 22.216);
114
+ --border: oklch(1 0 0 / 10%);
115
+ --input: oklch(1 0 0 / 15%);
116
+ --ring: oklch(0.556 0 0);
117
+ --chart-1: oklch(0.488 0.243 264.376);
118
+ --chart-2: oklch(0.696 0.17 162.48);
119
+ --chart-3: oklch(0.769 0.188 70.08);
120
+ --chart-4: oklch(0.627 0.265 303.9);
121
+ --chart-5: oklch(0.645 0.246 16.439);
122
+ --sidebar: oklch(0.205 0 0);
123
+ --sidebar-foreground: oklch(0.985 0 0);
124
+ --sidebar-primary: oklch(0.488 0.243 264.376);
125
+ --sidebar-primary-foreground: oklch(0.985 0 0);
126
+ --sidebar-accent: oklch(0.269 0 0);
127
+ --sidebar-accent-foreground: oklch(0.985 0 0);
128
+ --sidebar-border: oklch(1 0 0 / 10%);
129
+ --sidebar-ring: oklch(0.556 0 0);
130
+ }
131
+
132
+ @layer base {
133
+ * {
134
+ @apply border-border outline-ring/50;
135
+ }
136
+ body {
137
+ @apply bg-background text-foreground;
138
+ }
139
+ }
140
+
141
+ ```
142
+
143
+ ## app/components/assistant-ui/attachment.tsx
144
+
145
+ ```tsx
146
+ "use client";
147
+
148
+ import { type PropsWithChildren, useEffect, useState, type FC } from "react";
149
+ import { XIcon, PlusIcon, FileText } from "lucide-react";
150
+ import {
151
+ AttachmentPrimitive,
152
+ ComposerPrimitive,
153
+ MessagePrimitive,
154
+ useAssistantState,
155
+ useAssistantApi,
156
+ } from "@assistant-ui/react";
157
+ import { useShallow } from "zustand/shallow";
158
+ import {
159
+ Tooltip,
160
+ TooltipContent,
161
+ TooltipTrigger,
162
+ } from "@/components/ui/tooltip";
163
+ import {
164
+ Dialog,
165
+ DialogTitle,
166
+ DialogContent,
167
+ DialogTrigger,
168
+ } from "@/components/ui/dialog";
169
+ import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
170
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
171
+ import { cn } from "@/lib/utils";
172
+
173
+ const useFileSrc = (file: File | undefined) => {
174
+ const [src, setSrc] = useState<string | undefined>(undefined);
175
+
176
+ useEffect(() => {
177
+ if (!file) {
178
+ setSrc(undefined);
179
+ return;
180
+ }
181
+
182
+ const objectUrl = URL.createObjectURL(file);
183
+ setSrc(objectUrl);
184
+
185
+ return () => {
186
+ URL.revokeObjectURL(objectUrl);
187
+ };
188
+ }, [file]);
189
+
190
+ return src;
191
+ };
192
+
193
+ const useAttachmentSrc = () => {
194
+ const { file, src } = useAssistantState(
195
+ useShallow(({ attachment }): { file?: File; src?: string } => {
196
+ if (attachment.type !== "image") return {};
197
+ if (attachment.file) return { file: attachment.file };
198
+ const src = attachment.content?.filter((c) => c.type === "image")[0]
199
+ ?.image;
200
+ if (!src) return {};
201
+ return { src };
202
+ }),
203
+ );
204
+
205
+ return useFileSrc(file) ?? src;
206
+ };
207
+
208
+ type AttachmentPreviewProps = {
209
+ src: string;
210
+ };
211
+
212
+ const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {
213
+ const [isLoaded, setIsLoaded] = useState(false);
214
+ return (
215
+ <img
216
+ src={src}
217
+ alt="Attachment preview"
218
+ className={
219
+ isLoaded
220
+ ? "aui-attachment-preview-image-loaded block h-auto max-h-[80vh] w-auto max-w-full object-contain"
221
+ : "aui-attachment-preview-image-loading hidden"
222
+ }
223
+ onLoad={() => setIsLoaded(true)}
224
+ />
225
+ );
226
+ };
227
+
228
+ const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {
229
+ const src = useAttachmentSrc();
230
+
231
+ if (!src) return children;
232
+
233
+ return (
234
+ <Dialog>
235
+ <DialogTrigger
236
+ className="aui-attachment-preview-trigger cursor-pointer transition-colors hover:bg-accent/50"
237
+ asChild
238
+ >
239
+ {children}
240
+ </DialogTrigger>
241
+ <DialogContent className="aui-attachment-preview-dialog-content p-2 sm:max-w-3xl [&>button]:rounded-full [&>button]:bg-foreground/60 [&>button]:p-1 [&>button]:opacity-100 [&>button]:ring-0! [&_svg]:text-background [&>button]:hover:[&_svg]:text-destructive">
242
+ <DialogTitle className="aui-sr-only sr-only">
243
+ Image Attachment Preview
244
+ </DialogTitle>
245
+ <div className="aui-attachment-preview relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden bg-background">
246
+ <AttachmentPreview src={src} />
247
+ </div>
248
+ </DialogContent>
249
+ </Dialog>
250
+ );
251
+ };
252
+
253
+ const AttachmentThumb: FC = () => {
254
+ const isImage = useAssistantState(
255
+ ({ attachment }) => attachment.type === "image",
256
+ );
257
+ const src = useAttachmentSrc();
258
+
259
+ return (
260
+ <Avatar className="aui-attachment-tile-avatar h-full w-full rounded-none">
261
+ <AvatarImage
262
+ src={src}
263
+ alt="Attachment preview"
264
+ className="aui-attachment-tile-image object-cover"
265
+ />
266
+ <AvatarFallback delayMs={isImage ? 200 : 0}>
267
+ <FileText className="aui-attachment-tile-fallback-icon size-8 text-muted-foreground" />
268
+ </AvatarFallback>
269
+ </Avatar>
270
+ );
271
+ };
272
+
273
+ const AttachmentUI: FC = () => {
274
+ const api = useAssistantApi();
275
+ const isComposer = api.attachment.source === "composer";
276
+
277
+ const isImage = useAssistantState(
278
+ ({ attachment }) => attachment.type === "image",
279
+ );
280
+ const typeLabel = useAssistantState(({ attachment }) => {
281
+ const type = attachment.type;
282
+ switch (type) {
283
+ case "image":
284
+ return "Image";
285
+ case "document":
286
+ return "Document";
287
+ case "file":
288
+ return "File";
289
+ default:
290
+ const _exhaustiveCheck: never = type;
291
+ throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`);
292
+ }
293
+ });
294
+
295
+ return (
296
+ <Tooltip>
297
+ <AttachmentPrimitive.Root
298
+ className={cn(
299
+ "aui-attachment-root relative",
300
+ isImage &&
301
+ "aui-attachment-root-composer only:[&>#attachment-tile]:size-24",
302
+ )}
303
+ >
304
+ <AttachmentPreviewDialog>
305
+ <TooltipTrigger asChild>
306
+ <div
307
+ className={cn(
308
+ "aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[14px] border bg-muted transition-opacity hover:opacity-75",
309
+ isComposer &&
310
+ "aui-attachment-tile-composer border-foreground/20",
311
+ )}
312
+ role="button"
313
+ id="attachment-tile"
314
+ aria-label={`${typeLabel} attachment`}
315
+ >
316
+ <AttachmentThumb />
317
+ </div>
318
+ </TooltipTrigger>
319
+ </AttachmentPreviewDialog>
320
+ {isComposer && <AttachmentRemove />}
321
+ </AttachmentPrimitive.Root>
322
+ <TooltipContent side="top">
323
+ <AttachmentPrimitive.Name />
324
+ </TooltipContent>
325
+ </Tooltip>
326
+ );
327
+ };
328
+
329
+ const AttachmentRemove: FC = () => {
330
+ return (
331
+ <AttachmentPrimitive.Remove asChild>
332
+ <TooltipIconButton
333
+ tooltip="Remove file"
334
+ className="aui-attachment-tile-remove absolute top-1.5 right-1.5 size-3.5 rounded-full bg-white text-muted-foreground opacity-100 shadow-sm hover:bg-white! [&_svg]:text-black hover:[&_svg]:text-destructive"
335
+ side="top"
336
+ >
337
+ <XIcon className="aui-attachment-remove-icon size-3 dark:stroke-[2.5px]" />
338
+ </TooltipIconButton>
339
+ </AttachmentPrimitive.Remove>
340
+ );
341
+ };
342
+
343
+ export const UserMessageAttachments: FC = () => {
344
+ return (
345
+ <div className="aui-user-message-attachments-end col-span-full col-start-1 row-start-1 flex w-full flex-row justify-end gap-2">
346
+ <MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} />
347
+ </div>
348
+ );
349
+ };
350
+
351
+ export const ComposerAttachments: FC = () => {
352
+ return (
353
+ <div className="aui-composer-attachments mb-2 flex w-full flex-row items-center gap-2 overflow-x-auto px-1.5 pt-0.5 pb-1 empty:hidden">
354
+ <ComposerPrimitive.Attachments
355
+ components={{ Attachment: AttachmentUI }}
356
+ />
357
+ </div>
358
+ );
359
+ };
360
+
361
+ export const ComposerAddAttachment: FC = () => {
362
+ return (
363
+ <ComposerPrimitive.AddAttachment asChild>
364
+ <TooltipIconButton
365
+ tooltip="Add Attachment"
366
+ side="bottom"
367
+ variant="ghost"
368
+ size="icon"
369
+ className="aui-composer-add-attachment size-[34px] rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
370
+ aria-label="Add Attachment"
371
+ >
372
+ <PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
373
+ </TooltipIconButton>
374
+ </ComposerPrimitive.AddAttachment>
375
+ );
376
+ };
377
+
378
+ ```
379
+
380
+ ## app/components/assistant-ui/markdown-text.tsx
381
+
382
+ ```tsx
383
+ "use client";
384
+
385
+ import "@assistant-ui/react-markdown/styles/dot.css";
386
+
387
+ import {
388
+ type CodeHeaderProps,
389
+ MarkdownTextPrimitive,
390
+ unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
391
+ useIsMarkdownCodeBlock,
392
+ } from "@assistant-ui/react-markdown";
393
+ import remarkGfm from "remark-gfm";
394
+ import { type FC, memo, useState } from "react";
395
+ import { CheckIcon, CopyIcon } from "lucide-react";
396
+
397
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
398
+ import { cn } from "@/lib/utils";
399
+
400
+ const MarkdownTextImpl = () => {
401
+ return (
402
+ <MarkdownTextPrimitive
403
+ remarkPlugins={[remarkGfm]}
404
+ className="aui-md"
405
+ components={defaultComponents}
406
+ />
407
+ );
408
+ };
409
+
410
+ export const MarkdownText = memo(MarkdownTextImpl);
411
+
412
+ const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
413
+ const { isCopied, copyToClipboard } = useCopyToClipboard();
414
+ const onCopy = () => {
415
+ if (!code || isCopied) return;
416
+ copyToClipboard(code);
417
+ };
418
+
419
+ return (
420
+ <div className="aui-code-header-root mt-4 flex items-center justify-between gap-4 rounded-t-lg bg-muted-foreground/15 px-4 py-2 font-semibold text-foreground text-sm dark:bg-muted-foreground/20">
421
+ <span className="aui-code-header-language lowercase [&>span]:text-xs">
422
+ {language}
423
+ </span>
424
+ <TooltipIconButton tooltip="Copy" onClick={onCopy}>
425
+ {!isCopied && <CopyIcon />}
426
+ {isCopied && <CheckIcon />}
427
+ </TooltipIconButton>
428
+ </div>
429
+ );
430
+ };
431
+
432
+ const useCopyToClipboard = ({
433
+ copiedDuration = 3000,
434
+ }: {
435
+ copiedDuration?: number;
436
+ } = {}) => {
437
+ const [isCopied, setIsCopied] = useState<boolean>(false);
438
+
439
+ const copyToClipboard = (value: string) => {
440
+ if (!value) return;
441
+
442
+ navigator.clipboard.writeText(value).then(() => {
443
+ setIsCopied(true);
444
+ setTimeout(() => setIsCopied(false), copiedDuration);
445
+ });
446
+ };
447
+
448
+ return { isCopied, copyToClipboard };
449
+ };
450
+
451
+ const defaultComponents = memoizeMarkdownComponents({
452
+ h1: ({ className, ...props }) => (
453
+ <h1
454
+ className={cn(
455
+ "aui-md-h1 mb-8 scroll-m-20 font-extrabold text-4xl tracking-tight last:mb-0",
456
+ className,
457
+ )}
458
+ {...props}
459
+ />
460
+ ),
461
+ h2: ({ className, ...props }) => (
462
+ <h2
463
+ className={cn(
464
+ "aui-md-h2 mt-8 mb-4 scroll-m-20 font-semibold text-3xl tracking-tight first:mt-0 last:mb-0",
465
+ className,
466
+ )}
467
+ {...props}
468
+ />
469
+ ),
470
+ h3: ({ className, ...props }) => (
471
+ <h3
472
+ className={cn(
473
+ "aui-md-h3 mt-6 mb-4 scroll-m-20 font-semibold text-2xl tracking-tight first:mt-0 last:mb-0",
474
+ className,
475
+ )}
476
+ {...props}
477
+ />
478
+ ),
479
+ h4: ({ className, ...props }) => (
480
+ <h4
481
+ className={cn(
482
+ "aui-md-h4 mt-6 mb-4 scroll-m-20 font-semibold text-xl tracking-tight first:mt-0 last:mb-0",
483
+ className,
484
+ )}
485
+ {...props}
486
+ />
487
+ ),
488
+ h5: ({ className, ...props }) => (
489
+ <h5
490
+ className={cn(
491
+ "aui-md-h5 my-4 font-semibold text-lg first:mt-0 last:mb-0",
492
+ className,
493
+ )}
494
+ {...props}
495
+ />
496
+ ),
497
+ h6: ({ className, ...props }) => (
498
+ <h6
499
+ className={cn(
500
+ "aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0",
501
+ className,
502
+ )}
503
+ {...props}
504
+ />
505
+ ),
506
+ p: ({ className, ...props }) => (
507
+ <p
508
+ className={cn(
509
+ "aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0",
510
+ className,
511
+ )}
512
+ {...props}
513
+ />
514
+ ),
515
+ a: ({ className, ...props }) => (
516
+ <a
517
+ className={cn(
518
+ "aui-md-a font-medium text-primary underline underline-offset-4",
519
+ className,
520
+ )}
521
+ {...props}
522
+ />
523
+ ),
524
+ blockquote: ({ className, ...props }) => (
525
+ <blockquote
526
+ className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)}
527
+ {...props}
528
+ />
529
+ ),
530
+ ul: ({ className, ...props }) => (
531
+ <ul
532
+ className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)}
533
+ {...props}
534
+ />
535
+ ),
536
+ ol: ({ className, ...props }) => (
537
+ <ol
538
+ className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)}
539
+ {...props}
540
+ />
541
+ ),
542
+ hr: ({ className, ...props }) => (
543
+ <hr className={cn("aui-md-hr my-5 border-b", className)} {...props} />
544
+ ),
545
+ table: ({ className, ...props }) => (
546
+ <table
547
+ className={cn(
548
+ "aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto",
549
+ className,
550
+ )}
551
+ {...props}
552
+ />
553
+ ),
554
+ th: ({ className, ...props }) => (
555
+ <th
556
+ className={cn(
557
+ "aui-md-th bg-muted px-4 py-2 text-left font-bold first:rounded-tl-lg last:rounded-tr-lg [[align=center]]:text-center [[align=right]]:text-right",
558
+ className,
559
+ )}
560
+ {...props}
561
+ />
562
+ ),
563
+ td: ({ className, ...props }) => (
564
+ <td
565
+ className={cn(
566
+ "aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right",
567
+ className,
568
+ )}
569
+ {...props}
570
+ />
571
+ ),
572
+ tr: ({ className, ...props }) => (
573
+ <tr
574
+ className={cn(
575
+ "aui-md-tr m-0 border-b p-0 first:border-t [&:last-child>td:first-child]:rounded-bl-lg [&:last-child>td:last-child]:rounded-br-lg",
576
+ className,
577
+ )}
578
+ {...props}
579
+ />
580
+ ),
581
+ sup: ({ className, ...props }) => (
582
+ <sup
583
+ className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)}
584
+ {...props}
585
+ />
586
+ ),
587
+ pre: ({ className, ...props }) => (
588
+ <pre
589
+ className={cn(
590
+ "aui-md-pre overflow-x-auto rounded-t-none! rounded-b-lg bg-black p-4 text-white",
591
+ className,
592
+ )}
593
+ {...props}
594
+ />
595
+ ),
596
+ code: function Code({ className, ...props }) {
597
+ const isCodeBlock = useIsMarkdownCodeBlock();
598
+ return (
599
+ <code
600
+ className={cn(
601
+ !isCodeBlock &&
602
+ "aui-md-inline-code rounded border bg-muted font-semibold",
603
+ className,
604
+ )}
605
+ {...props}
606
+ />
607
+ );
608
+ },
609
+ CodeHeader,
610
+ });
611
+
612
+ ```
613
+
614
+ ## app/components/assistant-ui/thread.tsx
615
+
616
+ ```tsx
617
+ import {
618
+ ComposerAddAttachment,
619
+ ComposerAttachments,
620
+ UserMessageAttachments,
621
+ } from "@/components/assistant-ui/attachment";
622
+ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
623
+ import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
624
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
625
+ import { Button } from "@/components/ui/button";
626
+ import { cn } from "@/lib/utils";
627
+ import {
628
+ ActionBarPrimitive,
629
+ AssistantIf,
630
+ BranchPickerPrimitive,
631
+ ComposerPrimitive,
632
+ ErrorPrimitive,
633
+ MessagePrimitive,
634
+ ThreadPrimitive,
635
+ } from "@assistant-ui/react";
636
+ import {
637
+ ArrowDownIcon,
638
+ ArrowUpIcon,
639
+ CheckIcon,
640
+ ChevronLeftIcon,
641
+ ChevronRightIcon,
642
+ CopyIcon,
643
+ DownloadIcon,
644
+ PencilIcon,
645
+ RefreshCwIcon,
646
+ SquareIcon,
647
+ } from "lucide-react";
648
+ import type { FC } from "react";
649
+
650
+ export const Thread: FC = () => {
651
+ return (
652
+ <ThreadPrimitive.Root
653
+ className="aui-root aui-thread-root @container flex h-full flex-col bg-background"
654
+ style={{
655
+ ["--thread-max-width" as string]: "44rem",
656
+ }}
657
+ >
658
+ <ThreadPrimitive.Viewport
659
+ turnAnchor="top"
660
+ className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
661
+ >
662
+ <AssistantIf condition={({ thread }) => thread.isEmpty}>
663
+ <ThreadWelcome />
664
+ </AssistantIf>
665
+
666
+ <ThreadPrimitive.Messages
667
+ components={{
668
+ UserMessage,
669
+ EditComposer,
670
+ AssistantMessage,
671
+ }}
672
+ />
673
+
674
+ <ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6">
675
+ <ThreadScrollToBottom />
676
+ <Composer />
677
+ </ThreadPrimitive.ViewportFooter>
678
+ </ThreadPrimitive.Viewport>
679
+ </ThreadPrimitive.Root>
680
+ );
681
+ };
682
+
683
+ const ThreadScrollToBottom: FC = () => {
684
+ return (
685
+ <ThreadPrimitive.ScrollToBottom asChild>
686
+ <TooltipIconButton
687
+ tooltip="Scroll to bottom"
688
+ variant="outline"
689
+ className="aui-thread-scroll-to-bottom absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible dark:bg-background dark:hover:bg-accent"
690
+ >
691
+ <ArrowDownIcon />
692
+ </TooltipIconButton>
693
+ </ThreadPrimitive.ScrollToBottom>
694
+ );
695
+ };
696
+
697
+ const ThreadWelcome: FC = () => {
698
+ return (
699
+ <div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col">
700
+ <div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center">
701
+ <div className="aui-thread-welcome-message flex size-full flex-col justify-center px-4">
702
+ <h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in font-semibold text-2xl duration-200">
703
+ Hello there!
704
+ </h1>
705
+ <p className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in text-muted-foreground text-xl delay-75 duration-200">
706
+ How can I help you today?
707
+ </p>
708
+ </div>
709
+ </div>
710
+ <ThreadSuggestions />
711
+ </div>
712
+ );
713
+ };
714
+
715
+ const SUGGESTIONS = [
716
+ {
717
+ title: "What's the weather",
718
+ label: "in San Francisco?",
719
+ prompt: "What's the weather in San Francisco?",
720
+ },
721
+ {
722
+ title: "Explain React hooks",
723
+ label: "like useState and useEffect",
724
+ prompt: "Explain React hooks like useState and useEffect",
725
+ },
726
+ ] as const;
727
+
728
+ const ThreadSuggestions: FC = () => {
729
+ return (
730
+ <div className="aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4">
731
+ {SUGGESTIONS.map((suggestion, index) => (
732
+ <div
733
+ key={suggestion.prompt}
734
+ className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 @md:nth-[n+3]:block nth-[n+3]:hidden animate-in fill-mode-both duration-200"
735
+ style={{ animationDelay: `${100 + index * 50}ms` }}
736
+ >
737
+ <ThreadPrimitive.Suggestion prompt={suggestion.prompt} send asChild>
738
+ <Button
739
+ variant="ghost"
740
+ className="aui-thread-welcome-suggestion h-auto w-full @md:flex-col flex-wrap items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-left text-sm transition-colors hover:bg-muted"
741
+ aria-label={suggestion.prompt}
742
+ >
743
+ <span className="aui-thread-welcome-suggestion-text-1 font-medium">
744
+ {suggestion.title}
745
+ </span>
746
+ <span className="aui-thread-welcome-suggestion-text-2 text-muted-foreground">
747
+ {suggestion.label}
748
+ </span>
749
+ </Button>
750
+ </ThreadPrimitive.Suggestion>
751
+ </div>
752
+ ))}
753
+ </div>
754
+ );
755
+ };
756
+
757
+ const Composer: FC = () => {
758
+ return (
759
+ <ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
760
+ <ComposerPrimitive.AttachmentDropzone className="aui-composer-attachment-dropzone flex w-full flex-col rounded-2xl border border-input bg-background px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:border-dashed data-[dragging=true]:bg-accent/50">
761
+ <ComposerAttachments />
762
+ <ComposerPrimitive.Input
763
+ placeholder="Send a message..."
764
+ className="aui-composer-input mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-0"
765
+ rows={1}
766
+ autoFocus
767
+ aria-label="Message input"
768
+ />
769
+ <ComposerAction />
770
+ </ComposerPrimitive.AttachmentDropzone>
771
+ </ComposerPrimitive.Root>
772
+ );
773
+ };
774
+
775
+ const ComposerAction: FC = () => {
776
+ return (
777
+ <div className="aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between">
778
+ <ComposerAddAttachment />
779
+
780
+ <AssistantIf condition={({ thread }) => !thread.isRunning}>
781
+ <ComposerPrimitive.Send asChild>
782
+ <TooltipIconButton
783
+ tooltip="Send message"
784
+ side="bottom"
785
+ type="submit"
786
+ variant="default"
787
+ size="icon"
788
+ className="aui-composer-send size-8 rounded-full"
789
+ aria-label="Send message"
790
+ >
791
+ <ArrowUpIcon className="aui-composer-send-icon size-4" />
792
+ </TooltipIconButton>
793
+ </ComposerPrimitive.Send>
794
+ </AssistantIf>
795
+
796
+ <AssistantIf condition={({ thread }) => thread.isRunning}>
797
+ <ComposerPrimitive.Cancel asChild>
798
+ <Button
799
+ type="button"
800
+ variant="default"
801
+ size="icon"
802
+ className="aui-composer-cancel size-8 rounded-full"
803
+ aria-label="Stop generating"
804
+ >
805
+ <SquareIcon className="aui-composer-cancel-icon size-3 fill-current" />
806
+ </Button>
807
+ </ComposerPrimitive.Cancel>
808
+ </AssistantIf>
809
+ </div>
810
+ );
811
+ };
812
+
813
+ const MessageError: FC = () => {
814
+ return (
815
+ <MessagePrimitive.Error>
816
+ <ErrorPrimitive.Root className="aui-message-error-root mt-2 rounded-md border border-destructive bg-destructive/10 p-3 text-destructive text-sm dark:bg-destructive/5 dark:text-red-200">
817
+ <ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" />
818
+ </ErrorPrimitive.Root>
819
+ </MessagePrimitive.Error>
820
+ );
821
+ };
822
+
823
+ const AssistantMessage: FC = () => {
824
+ return (
825
+ <MessagePrimitive.Root
826
+ className="aui-assistant-message-root fade-in slide-in-from-bottom-1 relative mx-auto w-full max-w-(--thread-max-width) animate-in py-3 duration-150"
827
+ data-role="assistant"
828
+ >
829
+ <div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed">
830
+ <MessagePrimitive.Parts
831
+ components={{
832
+ Text: MarkdownText,
833
+ tools: { Fallback: ToolFallback },
834
+ }}
835
+ />
836
+ <MessageError />
837
+ </div>
838
+
839
+ <div className="aui-assistant-message-footer mt-1 ml-2 flex">
840
+ <BranchPicker />
841
+ <AssistantActionBar />
842
+ </div>
843
+ </MessagePrimitive.Root>
844
+ );
845
+ };
846
+
847
+ const AssistantActionBar: FC = () => {
848
+ return (
849
+ <ActionBarPrimitive.Root
850
+ hideWhenRunning
851
+ autohide="not-last"
852
+ autohideFloat="single-branch"
853
+ className="aui-assistant-action-bar-root col-start-3 row-start-2 -ml-1 flex gap-1 text-muted-foreground data-floating:absolute data-floating:rounded-md data-floating:border data-floating:bg-background data-floating:p-1 data-floating:shadow-sm"
854
+ >
855
+ <ActionBarPrimitive.Copy asChild>
856
+ <TooltipIconButton tooltip="Copy">
857
+ <AssistantIf condition={({ message }) => message.isCopied}>
858
+ <CheckIcon />
859
+ </AssistantIf>
860
+ <AssistantIf condition={({ message }) => !message.isCopied}>
861
+ <CopyIcon />
862
+ </AssistantIf>
863
+ </TooltipIconButton>
864
+ </ActionBarPrimitive.Copy>
865
+ <ActionBarPrimitive.ExportMarkdown asChild>
866
+ <TooltipIconButton tooltip="Export as Markdown">
867
+ <DownloadIcon />
868
+ </TooltipIconButton>
869
+ </ActionBarPrimitive.ExportMarkdown>
870
+ <ActionBarPrimitive.Reload asChild>
871
+ <TooltipIconButton tooltip="Refresh">
872
+ <RefreshCwIcon />
873
+ </TooltipIconButton>
874
+ </ActionBarPrimitive.Reload>
875
+ </ActionBarPrimitive.Root>
876
+ );
877
+ };
878
+
879
+ const UserMessage: FC = () => {
880
+ return (
881
+ <MessagePrimitive.Root
882
+ className="aui-user-message-root fade-in slide-in-from-bottom-1 mx-auto grid w-full max-w-(--thread-max-width) animate-in auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 py-3 duration-150 [&:where(>*)]:col-start-2"
883
+ data-role="user"
884
+ >
885
+ <UserMessageAttachments />
886
+
887
+ <div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
888
+ <div className="aui-user-message-content wrap-break-word rounded-2xl bg-muted px-4 py-2.5 text-foreground">
889
+ <MessagePrimitive.Parts />
890
+ </div>
891
+ <div className="aui-user-action-bar-wrapper absolute top-1/2 left-0 -translate-x-full -translate-y-1/2 pr-2">
892
+ <UserActionBar />
893
+ </div>
894
+ </div>
895
+
896
+ <BranchPicker className="aui-user-branch-picker col-span-full col-start-1 row-start-3 -mr-1 justify-end" />
897
+ </MessagePrimitive.Root>
898
+ );
899
+ };
900
+
901
+ const UserActionBar: FC = () => {
902
+ return (
903
+ <ActionBarPrimitive.Root
904
+ hideWhenRunning
905
+ autohide="not-last"
906
+ className="aui-user-action-bar-root flex flex-col items-end"
907
+ >
908
+ <ActionBarPrimitive.Edit asChild>
909
+ <TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4">
910
+ <PencilIcon />
911
+ </TooltipIconButton>
912
+ </ActionBarPrimitive.Edit>
913
+ </ActionBarPrimitive.Root>
914
+ );
915
+ };
916
+
917
+ const EditComposer: FC = () => {
918
+ return (
919
+ <MessagePrimitive.Root className="aui-edit-composer-wrapper mx-auto flex w-full max-w-(--thread-max-width) flex-col px-2 py-3">
920
+ <ComposerPrimitive.Root className="aui-edit-composer-root ml-auto flex w-full max-w-[85%] flex-col rounded-2xl bg-muted">
921
+ <ComposerPrimitive.Input
922
+ className="aui-edit-composer-input min-h-14 w-full resize-none bg-transparent p-4 text-foreground text-sm outline-none"
923
+ autoFocus
924
+ />
925
+ <div className="aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end">
926
+ <ComposerPrimitive.Cancel asChild>
927
+ <Button variant="ghost" size="sm">
928
+ Cancel
929
+ </Button>
930
+ </ComposerPrimitive.Cancel>
931
+ <ComposerPrimitive.Send asChild>
932
+ <Button size="sm">Update</Button>
933
+ </ComposerPrimitive.Send>
934
+ </div>
935
+ </ComposerPrimitive.Root>
936
+ </MessagePrimitive.Root>
937
+ );
938
+ };
939
+
940
+ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
941
+ className,
942
+ ...rest
943
+ }) => {
944
+ return (
945
+ <BranchPickerPrimitive.Root
946
+ hideWhenSingleBranch
947
+ className={cn(
948
+ "aui-branch-picker-root mr-2 -ml-2 inline-flex items-center text-muted-foreground text-xs",
949
+ className,
950
+ )}
951
+ {...rest}
952
+ >
953
+ <BranchPickerPrimitive.Previous asChild>
954
+ <TooltipIconButton tooltip="Previous">
955
+ <ChevronLeftIcon />
956
+ </TooltipIconButton>
957
+ </BranchPickerPrimitive.Previous>
958
+ <span className="aui-branch-picker-state font-medium">
959
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
960
+ </span>
961
+ <BranchPickerPrimitive.Next asChild>
962
+ <TooltipIconButton tooltip="Next">
963
+ <ChevronRightIcon />
964
+ </TooltipIconButton>
965
+ </BranchPickerPrimitive.Next>
966
+ </BranchPickerPrimitive.Root>
967
+ );
968
+ };
969
+
970
+ ```
971
+
972
+ ## app/components/assistant-ui/tool-fallback.tsx
973
+
974
+ ```tsx
975
+ import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
976
+ import {
977
+ CheckIcon,
978
+ ChevronDownIcon,
979
+ ChevronUpIcon,
980
+ XCircleIcon,
981
+ } from "lucide-react";
982
+ import { useState } from "react";
983
+ import { Button } from "@/components/ui/button";
984
+ import { cn } from "@/lib/utils";
985
+
986
+ export const ToolFallback: ToolCallMessagePartComponent = ({
987
+ toolName,
988
+ argsText,
989
+ result,
990
+ status,
991
+ }) => {
992
+ const [isCollapsed, setIsCollapsed] = useState(true);
993
+
994
+ const isCancelled =
995
+ status?.type === "incomplete" && status.reason === "cancelled";
996
+ const cancelledReason =
997
+ isCancelled && status.error
998
+ ? typeof status.error === "string"
999
+ ? status.error
1000
+ : JSON.stringify(status.error)
1001
+ : null;
1002
+
1003
+ return (
1004
+ <div
1005
+ className={cn(
1006
+ "aui-tool-fallback-root mb-4 flex w-full flex-col gap-3 rounded-lg border py-3",
1007
+ isCancelled && "border-muted-foreground/30 bg-muted/30",
1008
+ )}
1009
+ >
1010
+ <div className="aui-tool-fallback-header flex items-center gap-2 px-4">
1011
+ {isCancelled ? (
1012
+ <XCircleIcon className="aui-tool-fallback-icon size-4 text-muted-foreground" />
1013
+ ) : (
1014
+ <CheckIcon className="aui-tool-fallback-icon size-4" />
1015
+ )}
1016
+ <p
1017
+ className={cn(
1018
+ "aui-tool-fallback-title grow",
1019
+ isCancelled && "text-muted-foreground line-through",
1020
+ )}
1021
+ >
1022
+ {isCancelled ? "Cancelled tool: " : "Used tool: "}
1023
+ <b>{toolName}</b>
1024
+ </p>
1025
+ <Button onClick={() => setIsCollapsed(!isCollapsed)}>
1026
+ {isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
1027
+ </Button>
1028
+ </div>
1029
+ {!isCollapsed && (
1030
+ <div className="aui-tool-fallback-content flex flex-col gap-2 border-t pt-2">
1031
+ {cancelledReason && (
1032
+ <div className="aui-tool-fallback-cancelled-root px-4">
1033
+ <p className="aui-tool-fallback-cancelled-header font-semibold text-muted-foreground">
1034
+ Cancelled reason:
1035
+ </p>
1036
+ <p className="aui-tool-fallback-cancelled-reason text-muted-foreground">
1037
+ {cancelledReason}
1038
+ </p>
1039
+ </div>
1040
+ )}
1041
+ <div
1042
+ className={cn(
1043
+ "aui-tool-fallback-args-root px-4",
1044
+ isCancelled && "opacity-60",
1045
+ )}
1046
+ >
1047
+ <pre className="aui-tool-fallback-args-value whitespace-pre-wrap">
1048
+ {argsText}
1049
+ </pre>
1050
+ </div>
1051
+ {!isCancelled && result !== undefined && (
1052
+ <div className="aui-tool-fallback-result-root border-t border-dashed px-4 pt-2">
1053
+ <p className="aui-tool-fallback-result-header font-semibold">
1054
+ Result:
1055
+ </p>
1056
+ <pre className="aui-tool-fallback-result-content whitespace-pre-wrap">
1057
+ {typeof result === "string"
1058
+ ? result
1059
+ : JSON.stringify(result, null, 2)}
1060
+ </pre>
1061
+ </div>
1062
+ )}
1063
+ </div>
1064
+ )}
1065
+ </div>
1066
+ );
1067
+ };
1068
+
1069
+ ```
1070
+
1071
+ ## app/components/assistant-ui/tooltip-icon-button.tsx
1072
+
1073
+ ```tsx
1074
+ "use client";
1075
+
1076
+ import { type ComponentPropsWithRef, forwardRef } from "react";
1077
+ import { Slottable } from "@radix-ui/react-slot";
1078
+
1079
+ import {
1080
+ Tooltip,
1081
+ TooltipContent,
1082
+ TooltipTrigger,
1083
+ } from "@/components/ui/tooltip";
1084
+ import { Button } from "@/components/ui/button";
1085
+ import { cn } from "@/lib/utils";
1086
+
1087
+ export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & {
1088
+ tooltip: string;
1089
+ side?: "top" | "bottom" | "left" | "right";
1090
+ };
1091
+
1092
+ export const TooltipIconButton = forwardRef<
1093
+ HTMLButtonElement,
1094
+ TooltipIconButtonProps
1095
+ >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
1096
+ return (
1097
+ <Tooltip>
1098
+ <TooltipTrigger asChild>
1099
+ <Button
1100
+ variant="ghost"
1101
+ size="icon"
1102
+ {...rest}
1103
+ className={cn("aui-button-icon size-6 p-1", className)}
1104
+ ref={ref}
1105
+ >
1106
+ <Slottable>{children}</Slottable>
1107
+ <span className="aui-sr-only sr-only">{tooltip}</span>
1108
+ </Button>
1109
+ </TooltipTrigger>
1110
+ <TooltipContent side={side}>{tooltip}</TooltipContent>
1111
+ </Tooltip>
1112
+ );
1113
+ });
1114
+
1115
+ TooltipIconButton.displayName = "TooltipIconButton";
1116
+
1117
+ ```
1118
+
1119
+ ## app/components/ui/avatar.tsx
1120
+
1121
+ ```tsx
1122
+ "use client";
1123
+
1124
+ import * as React from "react";
1125
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
1126
+
1127
+ import { cn } from "@/lib/utils";
1128
+
1129
+ const Avatar = React.forwardRef<
1130
+ React.ElementRef<typeof AvatarPrimitive.Root>,
1131
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
1132
+ >(({ className, ...props }, ref) => (
1133
+ <AvatarPrimitive.Root
1134
+ ref={ref}
1135
+ className={cn(
1136
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
1137
+ className,
1138
+ )}
1139
+ {...props}
1140
+ />
1141
+ ));
1142
+ Avatar.displayName = AvatarPrimitive.Root.displayName;
1143
+
1144
+ const AvatarImage = React.forwardRef<
1145
+ React.ElementRef<typeof AvatarPrimitive.Image>,
1146
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
1147
+ >(({ className, ...props }, ref) => (
1148
+ <AvatarPrimitive.Image
1149
+ ref={ref}
1150
+ className={cn("aspect-square h-full w-full", className)}
1151
+ {...props}
1152
+ />
1153
+ ));
1154
+ AvatarImage.displayName = AvatarPrimitive.Image.displayName;
1155
+
1156
+ const AvatarFallback = React.forwardRef<
1157
+ React.ElementRef<typeof AvatarPrimitive.Fallback>,
1158
+ React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
1159
+ >(({ className, ...props }, ref) => (
1160
+ <AvatarPrimitive.Fallback
1161
+ ref={ref}
1162
+ className={cn(
1163
+ "flex h-full w-full items-center justify-center rounded-full bg-muted",
1164
+ className,
1165
+ )}
1166
+ {...props}
1167
+ />
1168
+ ));
1169
+ AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
1170
+
1171
+ export { Avatar, AvatarImage, AvatarFallback };
1172
+
1173
+ ```
1174
+
1175
+ ## app/components/ui/button.tsx
1176
+
1177
+ ```tsx
1178
+ import * as React from "react";
1179
+ import { Slot } from "@radix-ui/react-slot";
1180
+ import { cva, type VariantProps } from "class-variance-authority";
1181
+
1182
+ import { cn } from "@/lib/utils";
1183
+
1184
+ const buttonVariants = cva(
1185
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
1186
+ {
1187
+ variants: {
1188
+ variant: {
1189
+ default:
1190
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
1191
+ destructive:
1192
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
1193
+ outline:
1194
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
1195
+ secondary:
1196
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
1197
+ ghost: "hover:bg-accent hover:text-accent-foreground",
1198
+ link: "text-primary underline-offset-4 hover:underline",
1199
+ },
1200
+ size: {
1201
+ default: "h-9 px-4 py-2",
1202
+ sm: "h-8 rounded-md px-3 text-xs",
1203
+ lg: "h-10 rounded-md px-8",
1204
+ icon: "h-9 w-9",
1205
+ },
1206
+ },
1207
+ defaultVariants: {
1208
+ variant: "default",
1209
+ size: "default",
1210
+ },
1211
+ },
1212
+ );
1213
+
1214
+ export interface ButtonProps
1215
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
1216
+ VariantProps<typeof buttonVariants> {
1217
+ asChild?: boolean;
1218
+ }
1219
+
1220
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
1221
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
1222
+ const Comp = asChild ? Slot : "button";
1223
+ return (
1224
+ <Comp
1225
+ className={cn(buttonVariants({ variant, size, className }))}
1226
+ ref={ref}
1227
+ {...props}
1228
+ />
1229
+ );
1230
+ },
1231
+ );
1232
+ Button.displayName = "Button";
1233
+
1234
+ export { Button, buttonVariants };
1235
+
1236
+ ```
1237
+
1238
+ ## app/components/ui/dialog.tsx
1239
+
1240
+ ```tsx
1241
+ "use client";
1242
+
1243
+ import * as React from "react";
1244
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
1245
+ import { XIcon } from "lucide-react";
1246
+
1247
+ import { cn } from "@/lib/utils";
1248
+
1249
+ function Dialog({
1250
+ ...props
1251
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
1252
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
1253
+ }
1254
+
1255
+ function DialogTrigger({
1256
+ ...props
1257
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
1258
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
1259
+ }
1260
+
1261
+ function DialogPortal({
1262
+ ...props
1263
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
1264
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
1265
+ }
1266
+
1267
+ function DialogClose({
1268
+ ...props
1269
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
1270
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
1271
+ }
1272
+
1273
+ function DialogOverlay({
1274
+ className,
1275
+ ...props
1276
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
1277
+ return (
1278
+ <DialogPrimitive.Overlay
1279
+ data-slot="dialog-overlay"
1280
+ className={cn(
1281
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
1282
+ className,
1283
+ )}
1284
+ {...props}
1285
+ />
1286
+ );
1287
+ }
1288
+
1289
+ function DialogContent({
1290
+ className,
1291
+ children,
1292
+ ...props
1293
+ }: React.ComponentProps<typeof DialogPrimitive.Content>) {
1294
+ return (
1295
+ <DialogPortal data-slot="dialog-portal">
1296
+ <DialogOverlay />
1297
+ <DialogPrimitive.Content
1298
+ data-slot="dialog-content"
1299
+ className={cn(
1300
+ "data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg",
1301
+ className,
1302
+ )}
1303
+ {...props}
1304
+ >
1305
+ {children}
1306
+ <DialogPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0">
1307
+ <XIcon />
1308
+ <span className="sr-only">Close</span>
1309
+ </DialogPrimitive.Close>
1310
+ </DialogPrimitive.Content>
1311
+ </DialogPortal>
1312
+ );
1313
+ }
1314
+
1315
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
1316
+ return (
1317
+ <div
1318
+ data-slot="dialog-header"
1319
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
1320
+ {...props}
1321
+ />
1322
+ );
1323
+ }
1324
+
1325
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
1326
+ return (
1327
+ <div
1328
+ data-slot="dialog-footer"
1329
+ className={cn(
1330
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
1331
+ className,
1332
+ )}
1333
+ {...props}
1334
+ />
1335
+ );
1336
+ }
1337
+
1338
+ function DialogTitle({
1339
+ className,
1340
+ ...props
1341
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
1342
+ return (
1343
+ <DialogPrimitive.Title
1344
+ data-slot="dialog-title"
1345
+ className={cn("font-semibold text-lg leading-none", className)}
1346
+ {...props}
1347
+ />
1348
+ );
1349
+ }
1350
+
1351
+ function DialogDescription({
1352
+ className,
1353
+ ...props
1354
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
1355
+ return (
1356
+ <DialogPrimitive.Description
1357
+ data-slot="dialog-description"
1358
+ className={cn("text-muted-foreground text-sm", className)}
1359
+ {...props}
1360
+ />
1361
+ );
1362
+ }
1363
+
1364
+ export {
1365
+ Dialog,
1366
+ DialogClose,
1367
+ DialogContent,
1368
+ DialogDescription,
1369
+ DialogFooter,
1370
+ DialogHeader,
1371
+ DialogOverlay,
1372
+ DialogPortal,
1373
+ DialogTitle,
1374
+ DialogTrigger,
1375
+ };
1376
+
1377
+ ```
1378
+
1379
+ ## app/components/ui/tooltip.tsx
1380
+
1381
+ ```tsx
1382
+ import * as React from "react";
1383
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
1384
+
1385
+ import { cn } from "@/lib/utils";
1386
+
1387
+ function TooltipProvider({
1388
+ delayDuration = 0,
1389
+ ...props
1390
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
1391
+ return (
1392
+ <TooltipPrimitive.Provider
1393
+ data-slot="tooltip-provider"
1394
+ delayDuration={delayDuration}
1395
+ {...props}
1396
+ />
1397
+ );
1398
+ }
1399
+
1400
+ function Tooltip({
1401
+ ...props
1402
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
1403
+ return (
1404
+ <TooltipProvider>
1405
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
1406
+ </TooltipProvider>
1407
+ );
1408
+ }
1409
+
1410
+ function TooltipTrigger({
1411
+ ...props
1412
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
1413
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
1414
+ }
1415
+
1416
+ function TooltipContent({
1417
+ className,
1418
+ sideOffset = 0,
1419
+ children,
1420
+ ...props
1421
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
1422
+ return (
1423
+ <TooltipPrimitive.Portal>
1424
+ <TooltipPrimitive.Content
1425
+ data-slot="tooltip-content"
1426
+ sideOffset={sideOffset}
1427
+ className={cn(
1428
+ "fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-[--radix-tooltip-content-transform-origin] animate-in text-balance rounded-md bg-primary px-3 py-1.5 text-primary-foreground text-xs data-[state=closed]:animate-out",
1429
+ className,
1430
+ )}
1431
+ {...props}
1432
+ >
1433
+ {children}
1434
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-primary fill-primary" />
1435
+ </TooltipPrimitive.Content>
1436
+ </TooltipPrimitive.Portal>
1437
+ );
1438
+ }
1439
+
1440
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
1441
+
1442
+ ```
1443
+
1444
+ ## app/lib/utils.ts
1445
+
1446
+ ```typescript
1447
+ import { clsx, type ClassValue } from "clsx";
1448
+ import { twMerge } from "tailwind-merge";
1449
+
1450
+ export function cn(...inputs: ClassValue[]) {
1451
+ return twMerge(clsx(inputs));
1452
+ }
1453
+
1454
+ ```
1455
+
1456
+ ## app/MyRuntimeProvider.tsx
1457
+
1458
+ ```tsx
1459
+ import type { ThreadMessageLike, AppendMessage } from "@assistant-ui/react";
1460
+ import {
1461
+ AssistantRuntimeProvider,
1462
+ useExternalStoreRuntime,
1463
+ } from "@assistant-ui/react";
1464
+ import { createParser, type EventSourceMessage } from "eventsource-parser";
1465
+ import { useState, useCallback } from "react";
1466
+
1467
+ // SSE event types from server
1468
+ type SSEEvent =
1469
+ | { type: "text"; content: string }
1470
+ | { type: "tool_call"; id: string; name: string; arguments: string }
1471
+ | { type: "tool_result"; id: string; result: string };
1472
+
1473
+ // Parse SSE stream using eventsource-parser
1474
+ async function* parseSSEStream(
1475
+ response: Response,
1476
+ ): AsyncGenerator<SSEEvent, void, unknown> {
1477
+ const reader = response.body?.getReader();
1478
+ if (!reader) throw new Error("No reader available");
1479
+
1480
+ const decoder = new TextDecoder();
1481
+ const events: SSEEvent[] = [];
1482
+ let done = false;
1483
+
1484
+ const parser = createParser({
1485
+ onEvent: (event: EventSourceMessage) => {
1486
+ if (event.data === "[DONE]") {
1487
+ done = true;
1488
+ return;
1489
+ }
1490
+ try {
1491
+ events.push(JSON.parse(event.data) as SSEEvent);
1492
+ } catch {
1493
+ // Skip invalid JSON
1494
+ }
1495
+ },
1496
+ });
1497
+
1498
+ while (!done) {
1499
+ const result = await reader.read();
1500
+ if (result.done) break;
1501
+
1502
+ parser.feed(decoder.decode(result.value, { stream: true }));
1503
+
1504
+ while (events.length > 0) {
1505
+ yield events.shift()!;
1506
+ }
1507
+ }
1508
+
1509
+ // Yield any remaining events
1510
+ while (events.length > 0) {
1511
+ yield events.shift()!;
1512
+ }
1513
+ }
1514
+
1515
+ // Extract content part type from ThreadMessageLike
1516
+ type ContentPart = Exclude<ThreadMessageLike["content"], string>[number];
1517
+
1518
+ // Helper to update assistant message content
1519
+ const updateAssistantContent = (
1520
+ messages: readonly ThreadMessageLike[],
1521
+ updater: (content: ContentPart[]) => ContentPart[],
1522
+ ): ThreadMessageLike[] => {
1523
+ const newMessages = [...messages];
1524
+ const lastMsg = newMessages[newMessages.length - 1];
1525
+
1526
+ if (lastMsg?.role === "assistant") {
1527
+ const content = Array.isArray(lastMsg.content)
1528
+ ? [...lastMsg.content]
1529
+ : [{ type: "text" as const, text: lastMsg.content as string }];
1530
+ newMessages[newMessages.length - 1] = {
1531
+ ...lastMsg,
1532
+ content: updater(content),
1533
+ };
1534
+ } else {
1535
+ newMessages.push({ role: "assistant", content: updater([]) });
1536
+ }
1537
+
1538
+ return newMessages;
1539
+ };
1540
+
1541
+ export function MyRuntimeProvider({
1542
+ children,
1543
+ }: Readonly<{
1544
+ children: React.ReactNode;
1545
+ }>) {
1546
+ const [messages, setMessages] = useState<readonly ThreadMessageLike[]>([]);
1547
+ const [isRunning, setIsRunning] = useState(false);
1548
+
1549
+ const onNew = useCallback(
1550
+ async (message: AppendMessage) => {
1551
+ if (message.content[0]?.type !== "text")
1552
+ throw new Error("Only text content is supported");
1553
+
1554
+ const userMessage: ThreadMessageLike = {
1555
+ role: "user",
1556
+ content: [{ type: "text", text: message.content[0].text }],
1557
+ };
1558
+
1559
+ const newMessages = [...messages, userMessage];
1560
+ setMessages(newMessages);
1561
+ setIsRunning(true);
1562
+
1563
+ try {
1564
+ const response = await fetch("/api/chat", {
1565
+ method: "POST",
1566
+ headers: { "Content-Type": "application/json" },
1567
+ body: JSON.stringify({
1568
+ messages: newMessages.map((m) => ({
1569
+ role: m.role,
1570
+ content:
1571
+ typeof m.content === "string"
1572
+ ? m.content
1573
+ : ((m.content[0] as { text?: string })?.text ?? ""),
1574
+ })),
1575
+ }),
1576
+ });
1577
+
1578
+ if (!response.ok) throw new Error("Failed to fetch response");
1579
+
1580
+ let textContent = "";
1581
+
1582
+ for await (const event of parseSSEStream(response)) {
1583
+ switch (event.type) {
1584
+ case "text":
1585
+ textContent += event.content;
1586
+ setMessages((prev) =>
1587
+ updateAssistantContent(prev, (content) => {
1588
+ const idx = content.findIndex((p) => p.type === "text");
1589
+ const textPart = { type: "text" as const, text: textContent };
1590
+ if (idx >= 0) {
1591
+ content[idx] = textPart;
1592
+ } else {
1593
+ content.push(textPart);
1594
+ }
1595
+ return content;
1596
+ }),
1597
+ );
1598
+ break;
1599
+
1600
+ case "tool_call":
1601
+ setMessages((prev) =>
1602
+ updateAssistantContent(prev, (content) => [
1603
+ ...content,
1604
+ {
1605
+ type: "tool-call" as const,
1606
+ toolCallId: event.id,
1607
+ toolName: event.name,
1608
+ args: JSON.parse(event.arguments),
1609
+ argsText: event.arguments,
1610
+ },
1611
+ ]),
1612
+ );
1613
+ break;
1614
+
1615
+ case "tool_result":
1616
+ setMessages((prev) =>
1617
+ updateAssistantContent(prev, (content) =>
1618
+ content.map((part) =>
1619
+ part.type === "tool-call" && part.toolCallId === event.id
1620
+ ? { ...part, result: JSON.parse(event.result) }
1621
+ : part,
1622
+ ),
1623
+ ),
1624
+ );
1625
+ break;
1626
+ }
1627
+ }
1628
+ } catch (error) {
1629
+ console.error("Error:", error);
1630
+ setMessages((prev) => [
1631
+ ...prev,
1632
+ {
1633
+ role: "assistant",
1634
+ content: [
1635
+ {
1636
+ type: "text",
1637
+ text: "Sorry, an error occurred. Please try again.",
1638
+ },
1639
+ ],
1640
+ },
1641
+ ]);
1642
+ } finally {
1643
+ setIsRunning(false);
1644
+ }
1645
+ },
1646
+ [messages],
1647
+ );
1648
+
1649
+ const runtime = useExternalStoreRuntime<ThreadMessageLike>({
1650
+ messages,
1651
+ setMessages,
1652
+ onNew,
1653
+ convertMessage: (m) => m,
1654
+ isRunning,
1655
+ });
1656
+
1657
+ return (
1658
+ <AssistantRuntimeProvider runtime={runtime}>
1659
+ {children}
1660
+ </AssistantRuntimeProvider>
1661
+ );
1662
+ }
1663
+
1664
+ ```
1665
+
1666
+ ## app/root.tsx
1667
+
1668
+ ```tsx
1669
+ import {
1670
+ isRouteErrorResponse,
1671
+ Links,
1672
+ Meta,
1673
+ Outlet,
1674
+ Scripts,
1675
+ ScrollRestoration,
1676
+ } from "react-router";
1677
+
1678
+ import type { Route } from "./+types/root";
1679
+ import { MyRuntimeProvider } from "./MyRuntimeProvider";
1680
+ import "./app.css";
1681
+
1682
+ export const links: Route.LinksFunction = () => [
1683
+ { rel: "preconnect", href: "https://fonts.googleapis.com" },
1684
+ {
1685
+ rel: "preconnect",
1686
+ href: "https://fonts.gstatic.com",
1687
+ crossOrigin: "anonymous",
1688
+ },
1689
+ {
1690
+ rel: "stylesheet",
1691
+ href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
1692
+ },
1693
+ ];
1694
+
1695
+ export function Layout({ children }: { children: React.ReactNode }) {
1696
+ return (
1697
+ <html lang="en" className="h-dvh">
1698
+ <head>
1699
+ <meta charSet="utf-8" />
1700
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1701
+ <Meta />
1702
+ <Links />
1703
+ </head>
1704
+ <body className="h-dvh font-sans">
1705
+ <MyRuntimeProvider>{children}</MyRuntimeProvider>
1706
+ <ScrollRestoration />
1707
+ <Scripts />
1708
+ </body>
1709
+ </html>
1710
+ );
1711
+ }
1712
+
1713
+ export default function App() {
1714
+ return <Outlet />;
1715
+ }
1716
+
1717
+ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
1718
+ let message = "Oops!";
1719
+ let details = "An unexpected error occurred.";
1720
+ let stack: string | undefined;
1721
+
1722
+ if (isRouteErrorResponse(error)) {
1723
+ message = error.status === 404 ? "404" : "Error";
1724
+ details =
1725
+ error.status === 404
1726
+ ? "The requested page could not be found."
1727
+ : error.statusText || details;
1728
+ } else if (import.meta.env.DEV && error && error instanceof Error) {
1729
+ details = error.message;
1730
+ stack = error.stack;
1731
+ }
1732
+
1733
+ return (
1734
+ <main className="container mx-auto p-4 pt-16">
1735
+ <h1>{message}</h1>
1736
+ <p>{details}</p>
1737
+ {stack && (
1738
+ <pre className="w-full overflow-x-auto p-4">
1739
+ <code>{stack}</code>
1740
+ </pre>
1741
+ )}
1742
+ </main>
1743
+ );
1744
+ }
1745
+
1746
+ ```
1747
+
1748
+ ## app/routes.ts
1749
+
1750
+ ```typescript
1751
+ import { type RouteConfig, index, route } from "@react-router/dev/routes";
1752
+
1753
+ export default [
1754
+ index("routes/home.tsx"),
1755
+ route("api/chat", "routes/api.chat.ts"),
1756
+ ] satisfies RouteConfig;
1757
+
1758
+ ```
1759
+
1760
+ ## app/routes/api.chat.ts
1761
+
1762
+ ```typescript
1763
+ import type { Route } from "./+types/api.chat";
1764
+ import OpenAI from "openai";
1765
+
1766
+ type ChatCompletionMessageParam =
1767
+ OpenAI.Chat.Completions.ChatCompletionMessageParam;
1768
+
1769
+ const openai = new OpenAI({
1770
+ apiKey: process.env.OPENAI_API_KEY,
1771
+ });
1772
+
1773
+ // Define available tools
1774
+ const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
1775
+ {
1776
+ type: "function",
1777
+ function: {
1778
+ name: "get_weather",
1779
+ description: "Get the current weather for a location",
1780
+ parameters: {
1781
+ type: "object",
1782
+ properties: {
1783
+ location: {
1784
+ type: "string",
1785
+ description: "The city name, e.g. San Francisco",
1786
+ },
1787
+ },
1788
+ required: ["location"],
1789
+ },
1790
+ },
1791
+ },
1792
+ ];
1793
+
1794
+ // Simple tool execution
1795
+ async function executeTool(
1796
+ name: string,
1797
+ args: Record<string, unknown>,
1798
+ ): Promise<string> {
1799
+ if (name === "get_weather") {
1800
+ const location = args.location as string;
1801
+ // Simulate weather API call
1802
+ const temp = Math.floor(Math.random() * 30) + 10;
1803
+ const conditions = ["sunny", "cloudy", "rainy", "partly cloudy"];
1804
+ const condition = conditions[Math.floor(Math.random() * conditions.length)];
1805
+ return JSON.stringify({
1806
+ location,
1807
+ temperature: temp,
1808
+ unit: "celsius",
1809
+ condition,
1810
+ });
1811
+ }
1812
+ return JSON.stringify({ error: "Unknown tool" });
1813
+ }
1814
+
1815
+ export async function action({ request }: Route.ActionArgs) {
1816
+ const { messages } = await request.json();
1817
+
1818
+ const openaiMessages: ChatCompletionMessageParam[] = messages.map(
1819
+ (m: { role: string; content: string }) => ({
1820
+ role: m.role as "user" | "assistant",
1821
+ content: m.content,
1822
+ }),
1823
+ );
1824
+
1825
+ const encoder = new TextEncoder();
1826
+
1827
+ const stream = new ReadableStream({
1828
+ async start(controller) {
1829
+ let continueLoop = true;
1830
+
1831
+ while (continueLoop) {
1832
+ const response = await openai.chat.completions.create({
1833
+ model: "gpt-4o-mini",
1834
+ messages: openaiMessages,
1835
+ tools,
1836
+ stream: true,
1837
+ });
1838
+
1839
+ let currentToolCalls: {
1840
+ id: string;
1841
+ name: string;
1842
+ arguments: string;
1843
+ }[] = [];
1844
+ let assistantContent = "";
1845
+
1846
+ for await (const chunk of response) {
1847
+ const delta = chunk.choices[0]?.delta;
1848
+
1849
+ // Handle text content
1850
+ if (delta?.content) {
1851
+ assistantContent += delta.content;
1852
+ controller.enqueue(
1853
+ encoder.encode(
1854
+ `data: ${JSON.stringify({ type: "text", content: delta.content })}\n\n`,
1855
+ ),
1856
+ );
1857
+ }
1858
+
1859
+ // Handle tool calls
1860
+ if (delta?.tool_calls) {
1861
+ for (const toolCall of delta.tool_calls) {
1862
+ const index = toolCall.index;
1863
+
1864
+ if (!currentToolCalls[index]) {
1865
+ currentToolCalls[index] = {
1866
+ id: toolCall.id || "",
1867
+ name: toolCall.function?.name || "",
1868
+ arguments: "",
1869
+ };
1870
+ }
1871
+
1872
+ if (toolCall.id) {
1873
+ currentToolCalls[index].id = toolCall.id;
1874
+ }
1875
+ if (toolCall.function?.name) {
1876
+ currentToolCalls[index].name = toolCall.function.name;
1877
+ }
1878
+ if (toolCall.function?.arguments) {
1879
+ currentToolCalls[index].arguments +=
1880
+ toolCall.function.arguments;
1881
+ }
1882
+ }
1883
+ }
1884
+ }
1885
+
1886
+ // If there are tool calls, execute them and continue the loop
1887
+ if (currentToolCalls.length > 0) {
1888
+ // Add assistant message with tool calls
1889
+ openaiMessages.push({
1890
+ role: "assistant",
1891
+ content: assistantContent || null,
1892
+ tool_calls: currentToolCalls.map((tc) => ({
1893
+ id: tc.id,
1894
+ type: "function" as const,
1895
+ function: {
1896
+ name: tc.name,
1897
+ arguments: tc.arguments,
1898
+ },
1899
+ })),
1900
+ });
1901
+
1902
+ // Execute each tool and add results
1903
+ for (const toolCall of currentToolCalls) {
1904
+ // Send tool call event to client
1905
+ controller.enqueue(
1906
+ encoder.encode(
1907
+ `data: ${JSON.stringify({
1908
+ type: "tool_call",
1909
+ id: toolCall.id,
1910
+ name: toolCall.name,
1911
+ arguments: toolCall.arguments,
1912
+ })}\n\n`,
1913
+ ),
1914
+ );
1915
+
1916
+ let parsedArgs: unknown;
1917
+ try {
1918
+ parsedArgs = JSON.parse(toolCall.arguments);
1919
+ } catch (parseError) {
1920
+ const message =
1921
+ parseError instanceof Error
1922
+ ? parseError.message
1923
+ : "Invalid tool arguments";
1924
+
1925
+ // Surface parsing failure to the client and continue gracefully
1926
+ const errorResult = JSON.stringify({
1927
+ error: message,
1928
+ });
1929
+
1930
+ controller.enqueue(
1931
+ encoder.encode(
1932
+ `data: ${JSON.stringify({
1933
+ type: "tool_result",
1934
+ id: toolCall.id,
1935
+ result: errorResult,
1936
+ })}\n\n`,
1937
+ ),
1938
+ );
1939
+
1940
+ openaiMessages.push({
1941
+ role: "tool",
1942
+ tool_call_id: toolCall.id,
1943
+ content: errorResult,
1944
+ });
1945
+
1946
+ continue;
1947
+ }
1948
+
1949
+ const args = parsedArgs as Record<string, unknown>;
1950
+ const result = await executeTool(toolCall.name, args);
1951
+
1952
+ // Send tool result event to client
1953
+ controller.enqueue(
1954
+ encoder.encode(
1955
+ `data: ${JSON.stringify({
1956
+ type: "tool_result",
1957
+ id: toolCall.id,
1958
+ result,
1959
+ })}\n\n`,
1960
+ ),
1961
+ );
1962
+
1963
+ // Add tool result to messages
1964
+ openaiMessages.push({
1965
+ role: "tool",
1966
+ tool_call_id: toolCall.id,
1967
+ content: result,
1968
+ });
1969
+ }
1970
+
1971
+ // Reset for next iteration
1972
+ currentToolCalls = [];
1973
+ } else {
1974
+ // No tool calls, we're done
1975
+ continueLoop = false;
1976
+ }
1977
+ }
1978
+
1979
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
1980
+ controller.close();
1981
+ },
1982
+ });
1983
+
1984
+ return new Response(stream, {
1985
+ headers: {
1986
+ "Content-Type": "text/event-stream",
1987
+ "Cache-Control": "no-cache",
1988
+ Connection: "keep-alive",
1989
+ },
1990
+ });
1991
+ }
1992
+
1993
+ ```
1994
+
1995
+ ## app/routes/home.tsx
1996
+
1997
+ ```tsx
1998
+ import { Thread } from "@/components/assistant-ui/thread";
1999
+
2000
+ export function meta() {
2001
+ return [
2002
+ { title: "assistant-ui with React Router" },
2003
+ { name: "description", content: "assistant-ui example with React Router" },
2004
+ ];
2005
+ }
2006
+
2007
+ export default function Home() {
2008
+ return (
2009
+ <main className="h-dvh">
2010
+ <Thread />
2011
+ </main>
2012
+ );
2013
+ }
2014
+
2015
+ ```
2016
+
2017
+ ## components.json
2018
+
2019
+ ```json
2020
+ {
2021
+ "$schema": "https://ui.shadcn.com/schema.json",
2022
+ "style": "new-york",
2023
+ "rsc": false,
2024
+ "tsx": true,
2025
+ "tailwind": {
2026
+ "config": "",
2027
+ "css": "app/app.css",
2028
+ "baseColor": "neutral",
2029
+ "cssVariables": true,
2030
+ "prefix": ""
2031
+ },
2032
+ "iconLibrary": "lucide",
2033
+ "aliases": {
2034
+ "components": "@/components",
2035
+ "utils": "@/lib/utils",
2036
+ "ui": "@/components/ui",
2037
+ "lib": "@/lib",
2038
+ "hooks": "@/hooks"
2039
+ },
2040
+ "registries": {}
2041
+ }
2042
+
2043
+ ```
2044
+
2045
+ ## package.json
2046
+
2047
+ ```json
2048
+ {
2049
+ "name": "assistant-ui-with-react-router",
2050
+ "private": true,
2051
+ "type": "module",
2052
+ "scripts": {
2053
+ "build": "react-router build",
2054
+ "dev": "react-router dev",
2055
+ "start": "react-router-serve ./build/server/index.js",
2056
+ "typecheck": "react-router typegen && tsc"
2057
+ },
2058
+ "dependencies": {
2059
+ "@assistant-ui/react": "workspace:*",
2060
+ "@assistant-ui/react-markdown": "workspace:*",
2061
+ "@radix-ui/react-avatar": "^1.1.11",
2062
+ "@radix-ui/react-dialog": "^1.1.15",
2063
+ "@radix-ui/react-slot": "^1.2.4",
2064
+ "@radix-ui/react-tooltip": "^1.2.8",
2065
+ "@react-router/node": "7.12.0",
2066
+ "@react-router/serve": "7.12.0",
2067
+ "class-variance-authority": "^0.7.1",
2068
+ "clsx": "^2.1.1",
2069
+ "eventsource-parser": "^3.0.6",
2070
+ "isbot": "^5.1.33",
2071
+ "lucide-react": "^0.562.0",
2072
+ "openai": "^6.16.0",
2073
+ "react": "^19.2.3",
2074
+ "react-dom": "^19.2.3",
2075
+ "react-router": "7.12.0",
2076
+ "remark-gfm": "^4.0.1",
2077
+ "tailwind-merge": "^3.4.0",
2078
+ "zustand": "^5.0.10"
2079
+ },
2080
+ "devDependencies": {
2081
+ "@react-router/dev": "7.12.0",
2082
+ "@tailwindcss/vite": "^4.1.18",
2083
+ "@types/node": "^25.0.9",
2084
+ "@types/react": "^19.2.9",
2085
+ "@types/react-dom": "^19.2.3",
2086
+ "tailwindcss": "^4.1.18",
2087
+ "tw-animate-css": "^1.4.0",
2088
+ "typescript": "^5.9.3",
2089
+ "vite": "^7.3.1",
2090
+ "vite-tsconfig-paths": "^6.0.4"
2091
+ }
2092
+ }
2093
+
2094
+ ```
2095
+
2096
+ ## react-router.config.ts
2097
+
2098
+ ```typescript
2099
+ import type { Config } from "@react-router/dev/config";
2100
+
2101
+ export default {
2102
+ // Config options...
2103
+ // Server-side render by default, to enable SPA mode set this to `false`
2104
+ ssr: true,
2105
+ } satisfies Config;
2106
+
2107
+ ```
2108
+
2109
+ ## README.md
2110
+
2111
+ ```markdown
2112
+ # Assistant UI with React Router
2113
+ ```
2114
+
2115
+ ## tsconfig.json
2116
+
2117
+ ```json
2118
+ {
2119
+ "include": [
2120
+ "**/*",
2121
+ "**/.server/**/*",
2122
+ "**/.client/**/*",
2123
+ ".react-router/types/**/*"
2124
+ ],
2125
+ "compilerOptions": {
2126
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
2127
+ "types": ["node", "vite/client"],
2128
+ "target": "ES2022",
2129
+ "module": "ES2022",
2130
+ "moduleResolution": "bundler",
2131
+ "jsx": "react-jsx",
2132
+ "rootDirs": [".", "./.react-router/types"],
2133
+ "baseUrl": ".",
2134
+ "paths": {
2135
+ "@/*": ["./app/*"]
2136
+ },
2137
+ "esModuleInterop": true,
2138
+ "verbatimModuleSyntax": true,
2139
+ "noEmit": true,
2140
+ "resolveJsonModule": true,
2141
+ "skipLibCheck": true,
2142
+ "strict": true
2143
+ }
2144
+ }
2145
+
2146
+ ```
2147
+
2148
+ ## vite.config.ts
2149
+
2150
+ ```typescript
2151
+ import { reactRouter } from "@react-router/dev/vite";
2152
+ import tailwindcss from "@tailwindcss/vite";
2153
+ import { defineConfig } from "vite";
2154
+ import tsconfigPaths from "vite-tsconfig-paths";
2155
+
2156
+ export default defineConfig({
2157
+ plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
2158
+ resolve: {
2159
+ dedupe: ["react", "react-dom", "@assistant-ui/react"],
2160
+ },
2161
+ optimizeDeps: {
2162
+ include: ["remark-gfm"],
2163
+ },
2164
+ });
2165
+
2166
+ ```
2167
+