@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,2241 @@
1
+ # Example: with-elevenlabs-scribe
2
+
3
+ ## app/api/chat/route.ts
4
+
5
+ ```typescript
6
+ import { openai } from "@ai-sdk/openai";
7
+ import {
8
+ streamText,
9
+ UIMessage,
10
+ convertToModelMessages,
11
+ tool,
12
+ stepCountIs,
13
+ zodSchema,
14
+ } from "ai";
15
+ import { z } from "zod";
16
+
17
+ // Allow streaming responses up to 30 seconds
18
+ export const maxDuration = 30;
19
+
20
+ export async function POST(req: Request) {
21
+ const { messages }: { messages: UIMessage[] } = await req.json();
22
+
23
+ const result = streamText({
24
+ model: openai("gpt-4o"),
25
+ messages: await convertToModelMessages(messages),
26
+ stopWhen: stepCountIs(10),
27
+ tools: {
28
+ get_current_weather: tool({
29
+ description: "Get the current weather",
30
+ inputSchema: zodSchema(
31
+ z.object({
32
+ city: z.string(),
33
+ }),
34
+ ),
35
+ execute: async ({ city }) => {
36
+ return `The weather in ${city} is sunny`;
37
+ },
38
+ }),
39
+ },
40
+ });
41
+
42
+ return result.toUIMessageStreamResponse();
43
+ }
44
+
45
+ ```
46
+
47
+ ## app/api/scribe-token/route.ts
48
+
49
+ ```typescript
50
+ /**
51
+ * Generate a single-use token for ElevenLabs Scribe v2 Realtime
52
+ * @see https://elevenlabs.io/docs/cookbooks/speech-to-text/streaming
53
+ */
54
+ export async function POST() {
55
+ const response = await fetch(
56
+ "https://api.elevenlabs.io/v1/single-use-token/realtime_scribe",
57
+ {
58
+ method: "POST",
59
+ headers: {
60
+ "xi-api-key": process.env["ELEVENLABS_API_KEY"]!,
61
+ },
62
+ },
63
+ );
64
+
65
+ if (!response.ok) {
66
+ const error = await response.text();
67
+ return new Response(error, { status: response.status });
68
+ }
69
+
70
+ const data = await response.json();
71
+ return Response.json({ token: data.token });
72
+ }
73
+
74
+ ```
75
+
76
+ ## app/globals.css
77
+
78
+ ```css
79
+ @import "tailwindcss";
80
+ @import "tw-animate-css";
81
+
82
+ @custom-variant dark (&:is(.dark *));
83
+
84
+ @theme inline {
85
+ --radius-sm: calc(var(--radius) - 4px);
86
+ --radius-md: calc(var(--radius) - 2px);
87
+ --radius-lg: var(--radius);
88
+ --radius-xl: calc(var(--radius) + 4px);
89
+ --color-background: var(--background);
90
+ --color-foreground: var(--foreground);
91
+ --color-card: var(--card);
92
+ --color-card-foreground: var(--card-foreground);
93
+ --color-popover: var(--popover);
94
+ --color-popover-foreground: var(--popover-foreground);
95
+ --color-primary: var(--primary);
96
+ --color-primary-foreground: var(--primary-foreground);
97
+ --color-secondary: var(--secondary);
98
+ --color-secondary-foreground: var(--secondary-foreground);
99
+ --color-muted: var(--muted);
100
+ --color-muted-foreground: var(--muted-foreground);
101
+ --color-accent: var(--accent);
102
+ --color-accent-foreground: var(--accent-foreground);
103
+ --color-destructive: var(--destructive);
104
+ --color-border: var(--border);
105
+ --color-input: var(--input);
106
+ --color-ring: var(--ring);
107
+ --color-chart-1: var(--chart-1);
108
+ --color-chart-2: var(--chart-2);
109
+ --color-chart-3: var(--chart-3);
110
+ --color-chart-4: var(--chart-4);
111
+ --color-chart-5: var(--chart-5);
112
+ --color-sidebar: var(--sidebar);
113
+ --color-sidebar-foreground: var(--sidebar-foreground);
114
+ --color-sidebar-primary: var(--sidebar-primary);
115
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
116
+ --color-sidebar-accent: var(--sidebar-accent);
117
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
118
+ --color-sidebar-border: var(--sidebar-border);
119
+ --color-sidebar-ring: var(--sidebar-ring);
120
+ }
121
+
122
+ :root {
123
+ --radius: 0.625rem;
124
+ --background: oklch(1 0 0);
125
+ --foreground: oklch(0.141 0.005 285.823);
126
+ --card: oklch(1 0 0);
127
+ --card-foreground: oklch(0.141 0.005 285.823);
128
+ --popover: oklch(1 0 0);
129
+ --popover-foreground: oklch(0.141 0.005 285.823);
130
+ --primary: oklch(0.21 0.006 285.885);
131
+ --primary-foreground: oklch(0.985 0 0);
132
+ --secondary: oklch(0.967 0.001 286.375);
133
+ --secondary-foreground: oklch(0.21 0.006 285.885);
134
+ --muted: oklch(0.967 0.001 286.375);
135
+ --muted-foreground: oklch(0.552 0.016 285.938);
136
+ --accent: oklch(0.967 0.001 286.375);
137
+ --accent-foreground: oklch(0.21 0.006 285.885);
138
+ --destructive: oklch(0.577 0.245 27.325);
139
+ --border: oklch(0.92 0.004 286.32);
140
+ --input: oklch(0.92 0.004 286.32);
141
+ --ring: oklch(0.705 0.015 286.067);
142
+ --chart-1: oklch(0.646 0.222 41.116);
143
+ --chart-2: oklch(0.6 0.118 184.704);
144
+ --chart-3: oklch(0.398 0.07 227.392);
145
+ --chart-4: oklch(0.828 0.189 84.429);
146
+ --chart-5: oklch(0.769 0.188 70.08);
147
+ --sidebar: oklch(0.985 0 0);
148
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
149
+ --sidebar-primary: oklch(0.21 0.006 285.885);
150
+ --sidebar-primary-foreground: oklch(0.985 0 0);
151
+ --sidebar-accent: oklch(0.967 0.001 286.375);
152
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
153
+ --sidebar-border: oklch(0.92 0.004 286.32);
154
+ --sidebar-ring: oklch(0.705 0.015 286.067);
155
+ }
156
+
157
+ .dark {
158
+ --background: oklch(0.141 0.005 285.823);
159
+ --foreground: oklch(0.985 0 0);
160
+ --card: oklch(0.21 0.006 285.885);
161
+ --card-foreground: oklch(0.985 0 0);
162
+ --popover: oklch(0.21 0.006 285.885);
163
+ --popover-foreground: oklch(0.985 0 0);
164
+ --primary: oklch(0.92 0.004 286.32);
165
+ --primary-foreground: oklch(0.21 0.006 285.885);
166
+ --secondary: oklch(0.274 0.006 286.033);
167
+ --secondary-foreground: oklch(0.985 0 0);
168
+ --muted: oklch(0.274 0.006 286.033);
169
+ --muted-foreground: oklch(0.705 0.015 286.067);
170
+ --accent: oklch(0.274 0.006 286.033);
171
+ --accent-foreground: oklch(0.985 0 0);
172
+ --destructive: oklch(0.704 0.191 22.216);
173
+ --border: oklch(1 0 0 / 10%);
174
+ --input: oklch(1 0 0 / 15%);
175
+ --ring: oklch(0.552 0.016 285.938);
176
+ --chart-1: oklch(0.488 0.243 264.376);
177
+ --chart-2: oklch(0.696 0.17 162.48);
178
+ --chart-3: oklch(0.769 0.188 70.08);
179
+ --chart-4: oklch(0.627 0.265 303.9);
180
+ --chart-5: oklch(0.645 0.246 16.439);
181
+ --sidebar: oklch(0.21 0.006 285.885);
182
+ --sidebar-foreground: oklch(0.985 0 0);
183
+ --sidebar-primary: oklch(0.488 0.243 264.376);
184
+ --sidebar-primary-foreground: oklch(0.985 0 0);
185
+ --sidebar-accent: oklch(0.274 0.006 286.033);
186
+ --sidebar-accent-foreground: oklch(0.985 0 0);
187
+ --sidebar-border: oklch(1 0 0 / 10%);
188
+ --sidebar-ring: oklch(0.552 0.016 285.938);
189
+ }
190
+
191
+ @layer base {
192
+ * {
193
+ @apply border-border outline-ring/50;
194
+ }
195
+ body {
196
+ @apply bg-background text-foreground;
197
+ }
198
+ }
199
+
200
+
201
+ ```
202
+
203
+ ## app/layout.tsx
204
+
205
+ ```tsx
206
+ import type { Metadata } from "next";
207
+ import "./globals.css";
208
+
209
+ export const metadata: Metadata = {
210
+ title: "ElevenLabs Scribe Dictation Example",
211
+ description:
212
+ "Example using @assistant-ui/react with ElevenLabs Scribe for real-time voice dictation",
213
+ };
214
+
215
+ export default function RootLayout({
216
+ children,
217
+ }: Readonly<{
218
+ children: React.ReactNode;
219
+ }>) {
220
+ return (
221
+ <html lang="en">
222
+ <body className="h-dvh">{children}</body>
223
+ </html>
224
+ );
225
+ }
226
+
227
+ ```
228
+
229
+ ## app/page.tsx
230
+
231
+ ```tsx
232
+ "use client";
233
+
234
+ import { Thread } from "@/components/assistant-ui/thread";
235
+ import { AssistantRuntimeProvider } from "@assistant-ui/react";
236
+ import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
237
+ import { ElevenLabsScribeAdapter } from "@/lib/elevenlabs-scribe-adapter";
238
+
239
+ export default function Home() {
240
+ const runtime = useChatRuntime({
241
+ adapters: {
242
+ dictation: new ElevenLabsScribeAdapter({
243
+ tokenEndpoint: "/api/scribe-token",
244
+ languageCode: "en", // Change to your preferred language
245
+ }),
246
+ },
247
+ });
248
+
249
+ return (
250
+ <AssistantRuntimeProvider runtime={runtime}>
251
+ <div className="h-full">
252
+ <Thread />
253
+ </div>
254
+ </AssistantRuntimeProvider>
255
+ );
256
+ }
257
+
258
+ ```
259
+
260
+ ## components.json
261
+
262
+ ```json
263
+ {
264
+ "$schema": "https://ui.shadcn.com/schema.json",
265
+ "style": "new-york",
266
+ "rsc": true,
267
+ "tsx": true,
268
+ "tailwind": {
269
+ "config": "",
270
+ "css": "app/globals.css",
271
+ "baseColor": "zinc",
272
+ "cssVariables": true,
273
+ "prefix": ""
274
+ },
275
+ "aliases": {
276
+ "components": "@/components",
277
+ "utils": "@/lib/utils",
278
+ "ui": "@/components/ui",
279
+ "lib": "@/lib",
280
+ "hooks": "@/hooks"
281
+ },
282
+ "iconLibrary": "lucide"
283
+ }
284
+
285
+ ```
286
+
287
+ ## components/assistant-ui/attachment.tsx
288
+
289
+ ```tsx
290
+ "use client";
291
+
292
+ import { PropsWithChildren, useEffect, useState, type FC } from "react";
293
+ import Image from "next/image";
294
+ import { XIcon, PlusIcon, FileText } from "lucide-react";
295
+ import {
296
+ AttachmentPrimitive,
297
+ ComposerPrimitive,
298
+ MessagePrimitive,
299
+ useAssistantState,
300
+ useAssistantApi,
301
+ } from "@assistant-ui/react";
302
+ import { useShallow } from "zustand/shallow";
303
+ import {
304
+ Tooltip,
305
+ TooltipContent,
306
+ TooltipTrigger,
307
+ } from "@/components/ui/tooltip";
308
+ import {
309
+ Dialog,
310
+ DialogTitle,
311
+ DialogContent,
312
+ DialogTrigger,
313
+ } from "@/components/ui/dialog";
314
+ import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
315
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
316
+ import { cn } from "@/lib/utils";
317
+
318
+ const useFileSrc = (file: File | undefined) => {
319
+ const [src, setSrc] = useState<string | undefined>(undefined);
320
+
321
+ useEffect(() => {
322
+ if (!file) {
323
+ setSrc(undefined);
324
+ return;
325
+ }
326
+
327
+ const objectUrl = URL.createObjectURL(file);
328
+ setSrc(objectUrl);
329
+
330
+ return () => {
331
+ URL.revokeObjectURL(objectUrl);
332
+ };
333
+ }, [file]);
334
+
335
+ return src;
336
+ };
337
+
338
+ const useAttachmentSrc = () => {
339
+ const { file, src } = useAssistantState(
340
+ useShallow(({ attachment }): { file?: File; src?: string } => {
341
+ if (attachment.type !== "image") return {};
342
+ if (attachment.file) return { file: attachment.file };
343
+ const src = attachment.content?.filter((c) => c.type === "image")[0]
344
+ ?.image;
345
+ if (!src) return {};
346
+ return { src };
347
+ }),
348
+ );
349
+
350
+ return useFileSrc(file) ?? src;
351
+ };
352
+
353
+ type AttachmentPreviewProps = {
354
+ src: string;
355
+ };
356
+
357
+ const AttachmentPreview: FC<AttachmentPreviewProps> = ({ src }) => {
358
+ const [isLoaded, setIsLoaded] = useState(false);
359
+ return (
360
+ <Image
361
+ src={src}
362
+ alt="Image Preview"
363
+ width={1}
364
+ height={1}
365
+ className={
366
+ isLoaded
367
+ ? "aui-attachment-preview-image-loaded block h-auto max-h-[80vh] w-auto max-w-full object-contain"
368
+ : "aui-attachment-preview-image-loading hidden"
369
+ }
370
+ onLoadingComplete={() => setIsLoaded(true)}
371
+ priority={false}
372
+ />
373
+ );
374
+ };
375
+
376
+ const AttachmentPreviewDialog: FC<PropsWithChildren> = ({ children }) => {
377
+ const src = useAttachmentSrc();
378
+
379
+ if (!src) return children;
380
+
381
+ return (
382
+ <Dialog>
383
+ <DialogTrigger
384
+ className="aui-attachment-preview-trigger cursor-pointer transition-colors hover:bg-accent/50"
385
+ asChild
386
+ >
387
+ {children}
388
+ </DialogTrigger>
389
+ <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">
390
+ <DialogTitle className="aui-sr-only sr-only">
391
+ Image Attachment Preview
392
+ </DialogTitle>
393
+ <div className="aui-attachment-preview relative mx-auto flex max-h-[80dvh] w-full items-center justify-center overflow-hidden bg-background">
394
+ <AttachmentPreview src={src} />
395
+ </div>
396
+ </DialogContent>
397
+ </Dialog>
398
+ );
399
+ };
400
+
401
+ const AttachmentThumb: FC = () => {
402
+ const isImage = useAssistantState(
403
+ ({ attachment }) => attachment.type === "image",
404
+ );
405
+ const src = useAttachmentSrc();
406
+
407
+ return (
408
+ <Avatar className="aui-attachment-tile-avatar h-full w-full rounded-none">
409
+ <AvatarImage
410
+ src={src}
411
+ alt="Attachment preview"
412
+ className="aui-attachment-tile-image object-cover"
413
+ />
414
+ <AvatarFallback delayMs={isImage ? 200 : 0}>
415
+ <FileText className="aui-attachment-tile-fallback-icon size-8 text-muted-foreground" />
416
+ </AvatarFallback>
417
+ </Avatar>
418
+ );
419
+ };
420
+
421
+ const AttachmentUI: FC = () => {
422
+ const api = useAssistantApi();
423
+ const isComposer = api.attachment.source === "composer";
424
+
425
+ const isImage = useAssistantState(
426
+ ({ attachment }) => attachment.type === "image",
427
+ );
428
+ const typeLabel = useAssistantState(({ attachment }) => {
429
+ const type = attachment.type;
430
+ switch (type) {
431
+ case "image":
432
+ return "Image";
433
+ case "document":
434
+ return "Document";
435
+ case "file":
436
+ return "File";
437
+ default:
438
+ const _exhaustiveCheck: never = type;
439
+ throw new Error(`Unknown attachment type: ${_exhaustiveCheck}`);
440
+ }
441
+ });
442
+
443
+ return (
444
+ <Tooltip>
445
+ <AttachmentPrimitive.Root
446
+ className={cn(
447
+ "aui-attachment-root relative",
448
+ isImage &&
449
+ "aui-attachment-root-composer only:[&>#attachment-tile]:size-24",
450
+ )}
451
+ >
452
+ <AttachmentPreviewDialog>
453
+ <TooltipTrigger asChild>
454
+ <div
455
+ className={cn(
456
+ "aui-attachment-tile size-14 cursor-pointer overflow-hidden rounded-[14px] border bg-muted transition-opacity hover:opacity-75",
457
+ isComposer &&
458
+ "aui-attachment-tile-composer border-foreground/20",
459
+ )}
460
+ role="button"
461
+ id="attachment-tile"
462
+ aria-label={`${typeLabel} attachment`}
463
+ >
464
+ <AttachmentThumb />
465
+ </div>
466
+ </TooltipTrigger>
467
+ </AttachmentPreviewDialog>
468
+ {isComposer && <AttachmentRemove />}
469
+ </AttachmentPrimitive.Root>
470
+ <TooltipContent side="top">
471
+ <AttachmentPrimitive.Name />
472
+ </TooltipContent>
473
+ </Tooltip>
474
+ );
475
+ };
476
+
477
+ const AttachmentRemove: FC = () => {
478
+ return (
479
+ <AttachmentPrimitive.Remove asChild>
480
+ <TooltipIconButton
481
+ tooltip="Remove file"
482
+ 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"
483
+ side="top"
484
+ >
485
+ <XIcon className="aui-attachment-remove-icon size-3 dark:stroke-[2.5px]" />
486
+ </TooltipIconButton>
487
+ </AttachmentPrimitive.Remove>
488
+ );
489
+ };
490
+
491
+ export const UserMessageAttachments: FC = () => {
492
+ return (
493
+ <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">
494
+ <MessagePrimitive.Attachments components={{ Attachment: AttachmentUI }} />
495
+ </div>
496
+ );
497
+ };
498
+
499
+ export const ComposerAttachments: FC = () => {
500
+ return (
501
+ <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">
502
+ <ComposerPrimitive.Attachments
503
+ components={{ Attachment: AttachmentUI }}
504
+ />
505
+ </div>
506
+ );
507
+ };
508
+
509
+ export const ComposerAddAttachment: FC = () => {
510
+ return (
511
+ <ComposerPrimitive.AddAttachment asChild>
512
+ <TooltipIconButton
513
+ tooltip="Add Attachment"
514
+ side="bottom"
515
+ variant="ghost"
516
+ size="icon"
517
+ className="aui-composer-add-attachment size-8.5 rounded-full p-1 font-semibold text-xs hover:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
518
+ aria-label="Add Attachment"
519
+ >
520
+ <PlusIcon className="aui-attachment-add-icon size-5 stroke-[1.5px]" />
521
+ </TooltipIconButton>
522
+ </ComposerPrimitive.AddAttachment>
523
+ );
524
+ };
525
+
526
+ ```
527
+
528
+ ## components/assistant-ui/markdown-text.tsx
529
+
530
+ ```tsx
531
+ "use client";
532
+
533
+ import "@assistant-ui/react-markdown/styles/dot.css";
534
+
535
+ import {
536
+ type CodeHeaderProps,
537
+ MarkdownTextPrimitive,
538
+ unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
539
+ useIsMarkdownCodeBlock,
540
+ } from "@assistant-ui/react-markdown";
541
+ import remarkGfm from "remark-gfm";
542
+ import { type FC, memo, useState } from "react";
543
+ import { CheckIcon, CopyIcon } from "lucide-react";
544
+
545
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
546
+ import { cn } from "@/lib/utils";
547
+
548
+ const MarkdownTextImpl = () => {
549
+ return (
550
+ <MarkdownTextPrimitive
551
+ remarkPlugins={[remarkGfm]}
552
+ className="aui-md"
553
+ components={defaultComponents}
554
+ />
555
+ );
556
+ };
557
+
558
+ export const MarkdownText = memo(MarkdownTextImpl);
559
+
560
+ const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
561
+ const { isCopied, copyToClipboard } = useCopyToClipboard();
562
+ const onCopy = () => {
563
+ if (!code || isCopied) return;
564
+ copyToClipboard(code);
565
+ };
566
+
567
+ return (
568
+ <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">
569
+ <span className="aui-code-header-language lowercase [&>span]:text-xs">
570
+ {language}
571
+ </span>
572
+ <TooltipIconButton tooltip="Copy" onClick={onCopy}>
573
+ {!isCopied && <CopyIcon />}
574
+ {isCopied && <CheckIcon />}
575
+ </TooltipIconButton>
576
+ </div>
577
+ );
578
+ };
579
+
580
+ const useCopyToClipboard = ({
581
+ copiedDuration = 3000,
582
+ }: {
583
+ copiedDuration?: number;
584
+ } = {}) => {
585
+ const [isCopied, setIsCopied] = useState<boolean>(false);
586
+
587
+ const copyToClipboard = (value: string) => {
588
+ if (!value) return;
589
+
590
+ navigator.clipboard.writeText(value).then(() => {
591
+ setIsCopied(true);
592
+ setTimeout(() => setIsCopied(false), copiedDuration);
593
+ });
594
+ };
595
+
596
+ return { isCopied, copyToClipboard };
597
+ };
598
+
599
+ const defaultComponents = memoizeMarkdownComponents({
600
+ h1: ({ className, ...props }) => (
601
+ <h1
602
+ className={cn(
603
+ "aui-md-h1 mb-8 scroll-m-20 font-extrabold text-4xl tracking-tight last:mb-0",
604
+ className,
605
+ )}
606
+ {...props}
607
+ />
608
+ ),
609
+ h2: ({ className, ...props }) => (
610
+ <h2
611
+ className={cn(
612
+ "aui-md-h2 mt-8 mb-4 scroll-m-20 font-semibold text-3xl tracking-tight first:mt-0 last:mb-0",
613
+ className,
614
+ )}
615
+ {...props}
616
+ />
617
+ ),
618
+ h3: ({ className, ...props }) => (
619
+ <h3
620
+ className={cn(
621
+ "aui-md-h3 mt-6 mb-4 scroll-m-20 font-semibold text-2xl tracking-tight first:mt-0 last:mb-0",
622
+ className,
623
+ )}
624
+ {...props}
625
+ />
626
+ ),
627
+ h4: ({ className, ...props }) => (
628
+ <h4
629
+ className={cn(
630
+ "aui-md-h4 mt-6 mb-4 scroll-m-20 font-semibold text-xl tracking-tight first:mt-0 last:mb-0",
631
+ className,
632
+ )}
633
+ {...props}
634
+ />
635
+ ),
636
+ h5: ({ className, ...props }) => (
637
+ <h5
638
+ className={cn(
639
+ "aui-md-h5 my-4 font-semibold text-lg first:mt-0 last:mb-0",
640
+ className,
641
+ )}
642
+ {...props}
643
+ />
644
+ ),
645
+ h6: ({ className, ...props }) => (
646
+ <h6
647
+ className={cn(
648
+ "aui-md-h6 my-4 font-semibold first:mt-0 last:mb-0",
649
+ className,
650
+ )}
651
+ {...props}
652
+ />
653
+ ),
654
+ p: ({ className, ...props }) => (
655
+ <p
656
+ className={cn(
657
+ "aui-md-p mt-5 mb-5 leading-7 first:mt-0 last:mb-0",
658
+ className,
659
+ )}
660
+ {...props}
661
+ />
662
+ ),
663
+ a: ({ className, ...props }) => (
664
+ <a
665
+ className={cn(
666
+ "aui-md-a font-medium text-primary underline underline-offset-4",
667
+ className,
668
+ )}
669
+ {...props}
670
+ />
671
+ ),
672
+ blockquote: ({ className, ...props }) => (
673
+ <blockquote
674
+ className={cn("aui-md-blockquote border-l-2 pl-6 italic", className)}
675
+ {...props}
676
+ />
677
+ ),
678
+ ul: ({ className, ...props }) => (
679
+ <ul
680
+ className={cn("aui-md-ul my-5 ml-6 list-disc [&>li]:mt-2", className)}
681
+ {...props}
682
+ />
683
+ ),
684
+ ol: ({ className, ...props }) => (
685
+ <ol
686
+ className={cn("aui-md-ol my-5 ml-6 list-decimal [&>li]:mt-2", className)}
687
+ {...props}
688
+ />
689
+ ),
690
+ hr: ({ className, ...props }) => (
691
+ <hr className={cn("aui-md-hr my-5 border-b", className)} {...props} />
692
+ ),
693
+ table: ({ className, ...props }) => (
694
+ <table
695
+ className={cn(
696
+ "aui-md-table my-5 w-full border-separate border-spacing-0 overflow-y-auto",
697
+ className,
698
+ )}
699
+ {...props}
700
+ />
701
+ ),
702
+ th: ({ className, ...props }) => (
703
+ <th
704
+ className={cn(
705
+ "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",
706
+ className,
707
+ )}
708
+ {...props}
709
+ />
710
+ ),
711
+ td: ({ className, ...props }) => (
712
+ <td
713
+ className={cn(
714
+ "aui-md-td border-b border-l px-4 py-2 text-left last:border-r [[align=center]]:text-center [[align=right]]:text-right",
715
+ className,
716
+ )}
717
+ {...props}
718
+ />
719
+ ),
720
+ tr: ({ className, ...props }) => (
721
+ <tr
722
+ className={cn(
723
+ "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",
724
+ className,
725
+ )}
726
+ {...props}
727
+ />
728
+ ),
729
+ sup: ({ className, ...props }) => (
730
+ <sup
731
+ className={cn("aui-md-sup [&>a]:text-xs [&>a]:no-underline", className)}
732
+ {...props}
733
+ />
734
+ ),
735
+ pre: ({ className, ...props }) => (
736
+ <pre
737
+ className={cn(
738
+ "aui-md-pre overflow-x-auto rounded-t-none! rounded-b-lg bg-black p-4 text-white",
739
+ className,
740
+ )}
741
+ {...props}
742
+ />
743
+ ),
744
+ code: function Code({ className, ...props }) {
745
+ const isCodeBlock = useIsMarkdownCodeBlock();
746
+ return (
747
+ <code
748
+ className={cn(
749
+ !isCodeBlock &&
750
+ "aui-md-inline-code rounded border bg-muted font-semibold",
751
+ className,
752
+ )}
753
+ {...props}
754
+ />
755
+ );
756
+ },
757
+ CodeHeader,
758
+ });
759
+
760
+ ```
761
+
762
+ ## components/assistant-ui/thread.tsx
763
+
764
+ ```tsx
765
+ import {
766
+ ComposerAddAttachment,
767
+ ComposerAttachments,
768
+ UserMessageAttachments,
769
+ } from "@/components/assistant-ui/attachment";
770
+ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
771
+ import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
772
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
773
+ import { Button } from "@/components/ui/button";
774
+ import { cn } from "@/lib/utils";
775
+ import {
776
+ ActionBarPrimitive,
777
+ AssistantIf,
778
+ BranchPickerPrimitive,
779
+ ComposerPrimitive,
780
+ ErrorPrimitive,
781
+ MessagePrimitive,
782
+ ThreadPrimitive,
783
+ } from "@assistant-ui/react";
784
+ import {
785
+ ArrowDownIcon,
786
+ ArrowUpIcon,
787
+ CheckIcon,
788
+ ChevronLeftIcon,
789
+ ChevronRightIcon,
790
+ CopyIcon,
791
+ DownloadIcon,
792
+ MicIcon,
793
+ PencilIcon,
794
+ RefreshCwIcon,
795
+ SquareIcon,
796
+ } from "lucide-react";
797
+ import type { FC } from "react";
798
+
799
+ export const Thread: FC = () => {
800
+ return (
801
+ <ThreadPrimitive.Root
802
+ className="aui-root aui-thread-root @container flex h-full flex-col bg-background"
803
+ style={{
804
+ ["--thread-max-width" as string]: "44rem",
805
+ }}
806
+ >
807
+ <ThreadPrimitive.Viewport
808
+ turnAnchor="top"
809
+ className="aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth px-4 pt-4"
810
+ >
811
+ <AssistantIf condition={({ thread }) => thread.isEmpty}>
812
+ <ThreadWelcome />
813
+ </AssistantIf>
814
+
815
+ <ThreadPrimitive.Messages
816
+ components={{
817
+ UserMessage,
818
+ EditComposer,
819
+ AssistantMessage,
820
+ }}
821
+ />
822
+
823
+ <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">
824
+ <ThreadScrollToBottom />
825
+ <Composer />
826
+ </ThreadPrimitive.ViewportFooter>
827
+ </ThreadPrimitive.Viewport>
828
+ </ThreadPrimitive.Root>
829
+ );
830
+ };
831
+
832
+ const ThreadScrollToBottom: FC = () => {
833
+ return (
834
+ <ThreadPrimitive.ScrollToBottom asChild>
835
+ <TooltipIconButton
836
+ tooltip="Scroll to bottom"
837
+ variant="outline"
838
+ 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"
839
+ >
840
+ <ArrowDownIcon />
841
+ </TooltipIconButton>
842
+ </ThreadPrimitive.ScrollToBottom>
843
+ );
844
+ };
845
+
846
+ const ThreadWelcome: FC = () => {
847
+ return (
848
+ <div className="aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col">
849
+ <div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center">
850
+ <div className="aui-thread-welcome-message flex size-full flex-col justify-center px-4">
851
+ <h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in font-semibold text-2xl duration-200">
852
+ Voice Input Demo
853
+ </h1>
854
+ <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">
855
+ Click the mic button to speak
856
+ </p>
857
+ </div>
858
+ </div>
859
+ <ThreadSuggestions />
860
+ </div>
861
+ );
862
+ };
863
+
864
+ const SUGGESTIONS = [
865
+ {
866
+ title: "What's the weather",
867
+ label: "in San Francisco?",
868
+ prompt: "What's the weather in San Francisco?",
869
+ },
870
+ {
871
+ title: "Explain React hooks",
872
+ label: "like useState and useEffect",
873
+ prompt: "Explain React hooks like useState and useEffect",
874
+ },
875
+ ] as const;
876
+
877
+ const ThreadSuggestions: FC = () => {
878
+ return (
879
+ <div className="aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4">
880
+ {SUGGESTIONS.map((suggestion, index) => (
881
+ <div
882
+ key={suggestion.prompt}
883
+ 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"
884
+ style={{ animationDelay: `${100 + index * 50}ms` }}
885
+ >
886
+ <ThreadPrimitive.Suggestion prompt={suggestion.prompt} send asChild>
887
+ <Button
888
+ variant="ghost"
889
+ 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"
890
+ aria-label={suggestion.prompt}
891
+ >
892
+ <span className="aui-thread-welcome-suggestion-text-1 font-medium">
893
+ {suggestion.title}
894
+ </span>
895
+ <span className="aui-thread-welcome-suggestion-text-2 text-muted-foreground">
896
+ {suggestion.label}
897
+ </span>
898
+ </Button>
899
+ </ThreadPrimitive.Suggestion>
900
+ </div>
901
+ ))}
902
+ </div>
903
+ );
904
+ };
905
+
906
+ const Composer: FC = () => {
907
+ return (
908
+ <ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
909
+ <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">
910
+ <ComposerAttachments />
911
+ <ComposerPrimitive.Input
912
+ placeholder="Send a message..."
913
+ 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"
914
+ rows={1}
915
+ autoFocus
916
+ aria-label="Message input"
917
+ />
918
+ <ComposerAction />
919
+ </ComposerPrimitive.AttachmentDropzone>
920
+ </ComposerPrimitive.Root>
921
+ );
922
+ };
923
+
924
+ const ComposerAction: FC = () => {
925
+ return (
926
+ <div className="aui-composer-action-wrapper relative mx-2 mb-2 flex items-center justify-between">
927
+ <div className="flex items-center gap-1">
928
+ <ComposerAddAttachment />
929
+
930
+ <ComposerPrimitive.If dictation={false}>
931
+ <ComposerPrimitive.Dictate asChild>
932
+ <TooltipIconButton
933
+ tooltip="Voice input"
934
+ side="top"
935
+ variant="ghost"
936
+ className="aui-composer-dictate size-8 rounded-full"
937
+ aria-label="Start voice input"
938
+ >
939
+ <MicIcon className="size-4" />
940
+ </TooltipIconButton>
941
+ </ComposerPrimitive.Dictate>
942
+ </ComposerPrimitive.If>
943
+
944
+ <ComposerPrimitive.If dictation>
945
+ <ComposerPrimitive.StopDictation asChild>
946
+ <TooltipIconButton
947
+ tooltip="Stop dictation"
948
+ side="top"
949
+ variant="default"
950
+ className="aui-composer-stop-dictation size-8 rounded-full"
951
+ aria-label="Stop voice input"
952
+ >
953
+ <SquareIcon className="size-3 animate-pulse fill-current" />
954
+ </TooltipIconButton>
955
+ </ComposerPrimitive.StopDictation>
956
+ </ComposerPrimitive.If>
957
+ </div>
958
+
959
+ <AssistantIf condition={({ thread }) => !thread.isRunning}>
960
+ <ComposerPrimitive.Send asChild>
961
+ <TooltipIconButton
962
+ tooltip="Send message"
963
+ side="bottom"
964
+ type="submit"
965
+ variant="default"
966
+ size="icon"
967
+ className="aui-composer-send size-8 rounded-full"
968
+ aria-label="Send message"
969
+ >
970
+ <ArrowUpIcon className="aui-composer-send-icon size-4" />
971
+ </TooltipIconButton>
972
+ </ComposerPrimitive.Send>
973
+ </AssistantIf>
974
+
975
+ <AssistantIf condition={({ thread }) => thread.isRunning}>
976
+ <ComposerPrimitive.Cancel asChild>
977
+ <Button
978
+ type="button"
979
+ variant="default"
980
+ size="icon"
981
+ className="aui-composer-cancel size-8 rounded-full"
982
+ aria-label="Stop generating"
983
+ >
984
+ <SquareIcon className="aui-composer-cancel-icon size-3 fill-current" />
985
+ </Button>
986
+ </ComposerPrimitive.Cancel>
987
+ </AssistantIf>
988
+ </div>
989
+ );
990
+ };
991
+
992
+ const MessageError: FC = () => {
993
+ return (
994
+ <MessagePrimitive.Error>
995
+ <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">
996
+ <ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" />
997
+ </ErrorPrimitive.Root>
998
+ </MessagePrimitive.Error>
999
+ );
1000
+ };
1001
+
1002
+ const AssistantMessage: FC = () => {
1003
+ return (
1004
+ <MessagePrimitive.Root
1005
+ 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"
1006
+ data-role="assistant"
1007
+ >
1008
+ <div className="aui-assistant-message-content wrap-break-word px-2 text-foreground leading-relaxed">
1009
+ <MessagePrimitive.Parts
1010
+ components={{
1011
+ Text: MarkdownText,
1012
+ tools: { Fallback: ToolFallback },
1013
+ }}
1014
+ />
1015
+ <MessageError />
1016
+ </div>
1017
+
1018
+ <div className="aui-assistant-message-footer mt-1 ml-2 flex">
1019
+ <BranchPicker />
1020
+ <AssistantActionBar />
1021
+ </div>
1022
+ </MessagePrimitive.Root>
1023
+ );
1024
+ };
1025
+
1026
+ const AssistantActionBar: FC = () => {
1027
+ return (
1028
+ <ActionBarPrimitive.Root
1029
+ hideWhenRunning
1030
+ autohide="not-last"
1031
+ autohideFloat="single-branch"
1032
+ 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"
1033
+ >
1034
+ <ActionBarPrimitive.Copy asChild>
1035
+ <TooltipIconButton tooltip="Copy">
1036
+ <AssistantIf condition={({ message }) => message.isCopied}>
1037
+ <CheckIcon />
1038
+ </AssistantIf>
1039
+ <AssistantIf condition={({ message }) => !message.isCopied}>
1040
+ <CopyIcon />
1041
+ </AssistantIf>
1042
+ </TooltipIconButton>
1043
+ </ActionBarPrimitive.Copy>
1044
+ <ActionBarPrimitive.ExportMarkdown asChild>
1045
+ <TooltipIconButton tooltip="Export as Markdown">
1046
+ <DownloadIcon />
1047
+ </TooltipIconButton>
1048
+ </ActionBarPrimitive.ExportMarkdown>
1049
+ <ActionBarPrimitive.Reload asChild>
1050
+ <TooltipIconButton tooltip="Refresh">
1051
+ <RefreshCwIcon />
1052
+ </TooltipIconButton>
1053
+ </ActionBarPrimitive.Reload>
1054
+ </ActionBarPrimitive.Root>
1055
+ );
1056
+ };
1057
+
1058
+ const UserMessage: FC = () => {
1059
+ return (
1060
+ <MessagePrimitive.Root
1061
+ 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"
1062
+ data-role="user"
1063
+ >
1064
+ <UserMessageAttachments />
1065
+
1066
+ <div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
1067
+ <div className="aui-user-message-content wrap-break-word rounded-2xl bg-muted px-4 py-2.5 text-foreground">
1068
+ <MessagePrimitive.Parts />
1069
+ </div>
1070
+ <div className="aui-user-action-bar-wrapper absolute top-1/2 left-0 -translate-x-full -translate-y-1/2 pr-2">
1071
+ <UserActionBar />
1072
+ </div>
1073
+ </div>
1074
+
1075
+ <BranchPicker className="aui-user-branch-picker col-span-full col-start-1 row-start-3 -mr-1 justify-end" />
1076
+ </MessagePrimitive.Root>
1077
+ );
1078
+ };
1079
+
1080
+ const UserActionBar: FC = () => {
1081
+ return (
1082
+ <ActionBarPrimitive.Root
1083
+ hideWhenRunning
1084
+ autohide="not-last"
1085
+ className="aui-user-action-bar-root flex flex-col items-end"
1086
+ >
1087
+ <ActionBarPrimitive.Edit asChild>
1088
+ <TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4">
1089
+ <PencilIcon />
1090
+ </TooltipIconButton>
1091
+ </ActionBarPrimitive.Edit>
1092
+ </ActionBarPrimitive.Root>
1093
+ );
1094
+ };
1095
+
1096
+ const EditComposer: FC = () => {
1097
+ return (
1098
+ <MessagePrimitive.Root className="aui-edit-composer-wrapper mx-auto flex w-full max-w-(--thread-max-width) flex-col px-2 py-3">
1099
+ <ComposerPrimitive.Root className="aui-edit-composer-root ml-auto flex w-full max-w-[85%] flex-col rounded-2xl bg-muted">
1100
+ <ComposerPrimitive.Input
1101
+ className="aui-edit-composer-input min-h-14 w-full resize-none bg-transparent p-4 text-foreground text-sm outline-none"
1102
+ autoFocus
1103
+ />
1104
+ <div className="aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end">
1105
+ <ComposerPrimitive.Cancel asChild>
1106
+ <Button variant="ghost" size="sm">
1107
+ Cancel
1108
+ </Button>
1109
+ </ComposerPrimitive.Cancel>
1110
+ <ComposerPrimitive.Send asChild>
1111
+ <Button size="sm">Update</Button>
1112
+ </ComposerPrimitive.Send>
1113
+ </div>
1114
+ </ComposerPrimitive.Root>
1115
+ </MessagePrimitive.Root>
1116
+ );
1117
+ };
1118
+
1119
+ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
1120
+ className,
1121
+ ...rest
1122
+ }) => {
1123
+ return (
1124
+ <BranchPickerPrimitive.Root
1125
+ hideWhenSingleBranch
1126
+ className={cn(
1127
+ "aui-branch-picker-root mr-2 -ml-2 inline-flex items-center text-muted-foreground text-xs",
1128
+ className,
1129
+ )}
1130
+ {...rest}
1131
+ >
1132
+ <BranchPickerPrimitive.Previous asChild>
1133
+ <TooltipIconButton tooltip="Previous">
1134
+ <ChevronLeftIcon />
1135
+ </TooltipIconButton>
1136
+ </BranchPickerPrimitive.Previous>
1137
+ <span className="aui-branch-picker-state font-medium">
1138
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
1139
+ </span>
1140
+ <BranchPickerPrimitive.Next asChild>
1141
+ <TooltipIconButton tooltip="Next">
1142
+ <ChevronRightIcon />
1143
+ </TooltipIconButton>
1144
+ </BranchPickerPrimitive.Next>
1145
+ </BranchPickerPrimitive.Root>
1146
+ );
1147
+ };
1148
+
1149
+ ```
1150
+
1151
+ ## components/assistant-ui/tool-fallback.tsx
1152
+
1153
+ ```tsx
1154
+ "use client";
1155
+
1156
+ import { memo, useCallback, useRef, useState } from "react";
1157
+ import {
1158
+ AlertCircleIcon,
1159
+ CheckIcon,
1160
+ ChevronDownIcon,
1161
+ LoaderIcon,
1162
+ XCircleIcon,
1163
+ } from "lucide-react";
1164
+ import {
1165
+ useScrollLock,
1166
+ type ToolCallMessagePartStatus,
1167
+ type ToolCallMessagePartComponent,
1168
+ } from "@assistant-ui/react";
1169
+ import {
1170
+ Collapsible,
1171
+ CollapsibleContent,
1172
+ CollapsibleTrigger,
1173
+ } from "@/components/ui/collapsible";
1174
+ import { cn } from "@/lib/utils";
1175
+
1176
+ const ANIMATION_DURATION = 200;
1177
+
1178
+ export type ToolFallbackRootProps = Omit<
1179
+ React.ComponentProps<typeof Collapsible>,
1180
+ "open" | "onOpenChange"
1181
+ > & {
1182
+ open?: boolean;
1183
+ onOpenChange?: (open: boolean) => void;
1184
+ defaultOpen?: boolean;
1185
+ };
1186
+
1187
+ function ToolFallbackRoot({
1188
+ className,
1189
+ open: controlledOpen,
1190
+ onOpenChange: controlledOnOpenChange,
1191
+ defaultOpen = false,
1192
+ children,
1193
+ ...props
1194
+ }: ToolFallbackRootProps) {
1195
+ const collapsibleRef = useRef<HTMLDivElement>(null);
1196
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
1197
+ const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION);
1198
+
1199
+ const isControlled = controlledOpen !== undefined;
1200
+ const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
1201
+
1202
+ const handleOpenChange = useCallback(
1203
+ (open: boolean) => {
1204
+ if (!open) {
1205
+ lockScroll();
1206
+ }
1207
+ if (!isControlled) {
1208
+ setUncontrolledOpen(open);
1209
+ }
1210
+ controlledOnOpenChange?.(open);
1211
+ },
1212
+ [lockScroll, isControlled, controlledOnOpenChange],
1213
+ );
1214
+
1215
+ return (
1216
+ <Collapsible
1217
+ ref={collapsibleRef}
1218
+ data-slot="tool-fallback-root"
1219
+ open={isOpen}
1220
+ onOpenChange={handleOpenChange}
1221
+ className={cn(
1222
+ "aui-tool-fallback-root group/tool-fallback-root w-full rounded-lg border py-3",
1223
+ className,
1224
+ )}
1225
+ style={
1226
+ {
1227
+ "--animation-duration": `${ANIMATION_DURATION}ms`,
1228
+ } as React.CSSProperties
1229
+ }
1230
+ {...props}
1231
+ >
1232
+ {children}
1233
+ </Collapsible>
1234
+ );
1235
+ }
1236
+
1237
+ type ToolStatus = ToolCallMessagePartStatus["type"];
1238
+
1239
+ const statusIconMap: Record<ToolStatus, React.ElementType> = {
1240
+ running: LoaderIcon,
1241
+ complete: CheckIcon,
1242
+ incomplete: XCircleIcon,
1243
+ "requires-action": AlertCircleIcon,
1244
+ };
1245
+
1246
+ function ToolFallbackTrigger({
1247
+ toolName,
1248
+ status,
1249
+ className,
1250
+ ...props
1251
+ }: React.ComponentProps<typeof CollapsibleTrigger> & {
1252
+ toolName: string;
1253
+ status?: ToolCallMessagePartStatus;
1254
+ }) {
1255
+ const statusType = status?.type ?? "complete";
1256
+ const isRunning = statusType === "running";
1257
+ const isCancelled =
1258
+ status?.type === "incomplete" && status.reason === "cancelled";
1259
+
1260
+ const Icon = statusIconMap[statusType];
1261
+ const label = isCancelled ? "Cancelled tool" : "Used tool";
1262
+
1263
+ return (
1264
+ <CollapsibleTrigger
1265
+ data-slot="tool-fallback-trigger"
1266
+ className={cn(
1267
+ "aui-tool-fallback-trigger group/trigger flex w-full items-center gap-2 px-4 text-sm transition-colors",
1268
+ className,
1269
+ )}
1270
+ {...props}
1271
+ >
1272
+ <Icon
1273
+ data-slot="tool-fallback-trigger-icon"
1274
+ className={cn(
1275
+ "aui-tool-fallback-trigger-icon size-4 shrink-0",
1276
+ isCancelled && "text-muted-foreground",
1277
+ isRunning && "animate-spin",
1278
+ )}
1279
+ />
1280
+ <span
1281
+ data-slot="tool-fallback-trigger-label"
1282
+ className={cn(
1283
+ "aui-tool-fallback-trigger-label-wrapper relative inline-block grow text-left leading-none",
1284
+ isCancelled && "text-muted-foreground line-through",
1285
+ )}
1286
+ >
1287
+ <span>
1288
+ {label}: <b>{toolName}</b>
1289
+ </span>
1290
+ {isRunning && (
1291
+ <span
1292
+ aria-hidden
1293
+ data-slot="tool-fallback-trigger-shimmer"
1294
+ className="aui-tool-fallback-trigger-shimmer shimmer pointer-events-none absolute inset-0 motion-reduce:animate-none"
1295
+ >
1296
+ {label}: <b>{toolName}</b>
1297
+ </span>
1298
+ )}
1299
+ </span>
1300
+ <ChevronDownIcon
1301
+ data-slot="tool-fallback-trigger-chevron"
1302
+ className={cn(
1303
+ "aui-tool-fallback-trigger-chevron size-4 shrink-0",
1304
+ "transition-transform duration-(--animation-duration) ease-out",
1305
+ "group-data-[state=closed]/trigger:-rotate-90",
1306
+ "group-data-[state=open]/trigger:rotate-0",
1307
+ )}
1308
+ />
1309
+ </CollapsibleTrigger>
1310
+ );
1311
+ }
1312
+
1313
+ function ToolFallbackContent({
1314
+ className,
1315
+ children,
1316
+ ...props
1317
+ }: React.ComponentProps<typeof CollapsibleContent>) {
1318
+ return (
1319
+ <CollapsibleContent
1320
+ data-slot="tool-fallback-content"
1321
+ className={cn(
1322
+ "aui-tool-fallback-content relative overflow-hidden text-sm outline-none",
1323
+ "group/collapsible-content ease-out",
1324
+ "data-[state=closed]:animate-collapsible-up",
1325
+ "data-[state=open]:animate-collapsible-down",
1326
+ "data-[state=closed]:fill-mode-forwards",
1327
+ "data-[state=closed]:pointer-events-none",
1328
+ "data-[state=open]:duration-(--animation-duration)",
1329
+ "data-[state=closed]:duration-(--animation-duration)",
1330
+ className,
1331
+ )}
1332
+ {...props}
1333
+ >
1334
+ <div className="mt-3 flex flex-col gap-2 border-t pt-2">{children}</div>
1335
+ </CollapsibleContent>
1336
+ );
1337
+ }
1338
+
1339
+ function ToolFallbackArgs({
1340
+ argsText,
1341
+ className,
1342
+ ...props
1343
+ }: React.ComponentProps<"div"> & {
1344
+ argsText?: string;
1345
+ }) {
1346
+ if (!argsText) return null;
1347
+
1348
+ return (
1349
+ <div
1350
+ data-slot="tool-fallback-args"
1351
+ className={cn("aui-tool-fallback-args px-4", className)}
1352
+ {...props}
1353
+ >
1354
+ <pre className="aui-tool-fallback-args-value whitespace-pre-wrap">
1355
+ {argsText}
1356
+ </pre>
1357
+ </div>
1358
+ );
1359
+ }
1360
+
1361
+ function ToolFallbackResult({
1362
+ result,
1363
+ className,
1364
+ ...props
1365
+ }: React.ComponentProps<"div"> & {
1366
+ result?: unknown;
1367
+ }) {
1368
+ if (result === undefined) return null;
1369
+
1370
+ return (
1371
+ <div
1372
+ data-slot="tool-fallback-result"
1373
+ className={cn(
1374
+ "aui-tool-fallback-result border-t border-dashed px-4 pt-2",
1375
+ className,
1376
+ )}
1377
+ {...props}
1378
+ >
1379
+ <p className="aui-tool-fallback-result-header font-semibold">Result:</p>
1380
+ <pre className="aui-tool-fallback-result-content whitespace-pre-wrap">
1381
+ {typeof result === "string" ? result : JSON.stringify(result, null, 2)}
1382
+ </pre>
1383
+ </div>
1384
+ );
1385
+ }
1386
+
1387
+ function ToolFallbackError({
1388
+ status,
1389
+ className,
1390
+ ...props
1391
+ }: React.ComponentProps<"div"> & {
1392
+ status?: ToolCallMessagePartStatus;
1393
+ }) {
1394
+ if (status?.type !== "incomplete") return null;
1395
+
1396
+ const error = status.error;
1397
+ const errorText = error
1398
+ ? typeof error === "string"
1399
+ ? error
1400
+ : JSON.stringify(error)
1401
+ : null;
1402
+
1403
+ if (!errorText) return null;
1404
+
1405
+ const isCancelled = status.reason === "cancelled";
1406
+ const headerText = isCancelled ? "Cancelled reason:" : "Error:";
1407
+
1408
+ return (
1409
+ <div
1410
+ data-slot="tool-fallback-error"
1411
+ className={cn("aui-tool-fallback-error px-4", className)}
1412
+ {...props}
1413
+ >
1414
+ <p className="aui-tool-fallback-error-header font-semibold text-muted-foreground">
1415
+ {headerText}
1416
+ </p>
1417
+ <p className="aui-tool-fallback-error-reason text-muted-foreground">
1418
+ {errorText}
1419
+ </p>
1420
+ </div>
1421
+ );
1422
+ }
1423
+
1424
+ const ToolFallbackImpl: ToolCallMessagePartComponent = ({
1425
+ toolName,
1426
+ argsText,
1427
+ result,
1428
+ status,
1429
+ }) => {
1430
+ const isCancelled =
1431
+ status?.type === "incomplete" && status.reason === "cancelled";
1432
+
1433
+ return (
1434
+ <ToolFallbackRoot
1435
+ className={cn(isCancelled && "border-muted-foreground/30 bg-muted/30")}
1436
+ >
1437
+ <ToolFallbackTrigger toolName={toolName} status={status} />
1438
+ <ToolFallbackContent>
1439
+ <ToolFallbackError status={status} />
1440
+ <ToolFallbackArgs
1441
+ argsText={argsText}
1442
+ className={cn(isCancelled && "opacity-60")}
1443
+ />
1444
+ {!isCancelled && <ToolFallbackResult result={result} />}
1445
+ </ToolFallbackContent>
1446
+ </ToolFallbackRoot>
1447
+ );
1448
+ };
1449
+
1450
+ const ToolFallback = memo(
1451
+ ToolFallbackImpl,
1452
+ ) as unknown as ToolCallMessagePartComponent & {
1453
+ Root: typeof ToolFallbackRoot;
1454
+ Trigger: typeof ToolFallbackTrigger;
1455
+ Content: typeof ToolFallbackContent;
1456
+ Args: typeof ToolFallbackArgs;
1457
+ Result: typeof ToolFallbackResult;
1458
+ Error: typeof ToolFallbackError;
1459
+ };
1460
+
1461
+ ToolFallback.displayName = "ToolFallback";
1462
+ ToolFallback.Root = ToolFallbackRoot;
1463
+ ToolFallback.Trigger = ToolFallbackTrigger;
1464
+ ToolFallback.Content = ToolFallbackContent;
1465
+ ToolFallback.Args = ToolFallbackArgs;
1466
+ ToolFallback.Result = ToolFallbackResult;
1467
+ ToolFallback.Error = ToolFallbackError;
1468
+
1469
+ export {
1470
+ ToolFallback,
1471
+ ToolFallbackRoot,
1472
+ ToolFallbackTrigger,
1473
+ ToolFallbackContent,
1474
+ ToolFallbackArgs,
1475
+ ToolFallbackResult,
1476
+ ToolFallbackError,
1477
+ };
1478
+
1479
+ ```
1480
+
1481
+ ## components/assistant-ui/tooltip-icon-button.tsx
1482
+
1483
+ ```tsx
1484
+ "use client";
1485
+
1486
+ import { ComponentPropsWithRef, forwardRef } from "react";
1487
+ import { Slottable } from "@radix-ui/react-slot";
1488
+
1489
+ import {
1490
+ Tooltip,
1491
+ TooltipContent,
1492
+ TooltipTrigger,
1493
+ } from "@/components/ui/tooltip";
1494
+ import { Button } from "@/components/ui/button";
1495
+ import { cn } from "@/lib/utils";
1496
+
1497
+ export type TooltipIconButtonProps = ComponentPropsWithRef<typeof Button> & {
1498
+ tooltip: string;
1499
+ side?: "top" | "bottom" | "left" | "right";
1500
+ };
1501
+
1502
+ export const TooltipIconButton = forwardRef<
1503
+ HTMLButtonElement,
1504
+ TooltipIconButtonProps
1505
+ >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
1506
+ return (
1507
+ <Tooltip>
1508
+ <TooltipTrigger asChild>
1509
+ <Button
1510
+ variant="ghost"
1511
+ size="icon"
1512
+ {...rest}
1513
+ className={cn("aui-button-icon size-6 p-1", className)}
1514
+ ref={ref}
1515
+ >
1516
+ <Slottable>{children}</Slottable>
1517
+ <span className="aui-sr-only sr-only">{tooltip}</span>
1518
+ </Button>
1519
+ </TooltipTrigger>
1520
+ <TooltipContent side={side}>{tooltip}</TooltipContent>
1521
+ </Tooltip>
1522
+ );
1523
+ });
1524
+
1525
+ TooltipIconButton.displayName = "TooltipIconButton";
1526
+
1527
+ ```
1528
+
1529
+ ## components/ui/avatar.tsx
1530
+
1531
+ ```tsx
1532
+ "use client";
1533
+
1534
+ import * as React from "react";
1535
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
1536
+
1537
+ import { cn } from "@/lib/utils";
1538
+
1539
+ function Avatar({
1540
+ className,
1541
+ ...props
1542
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
1543
+ return (
1544
+ <AvatarPrimitive.Root
1545
+ data-slot="avatar"
1546
+ className={cn(
1547
+ "relative flex size-8 shrink-0 overflow-hidden rounded-full",
1548
+ className,
1549
+ )}
1550
+ {...props}
1551
+ />
1552
+ );
1553
+ }
1554
+
1555
+ function AvatarImage({
1556
+ className,
1557
+ ...props
1558
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
1559
+ return (
1560
+ <AvatarPrimitive.Image
1561
+ data-slot="avatar-image"
1562
+ className={cn("aspect-square size-full", className)}
1563
+ {...props}
1564
+ />
1565
+ );
1566
+ }
1567
+
1568
+ function AvatarFallback({
1569
+ className,
1570
+ ...props
1571
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
1572
+ return (
1573
+ <AvatarPrimitive.Fallback
1574
+ data-slot="avatar-fallback"
1575
+ className={cn(
1576
+ "flex size-full items-center justify-center rounded-full bg-muted",
1577
+ className,
1578
+ )}
1579
+ {...props}
1580
+ />
1581
+ );
1582
+ }
1583
+
1584
+ export { Avatar, AvatarImage, AvatarFallback };
1585
+
1586
+ ```
1587
+
1588
+ ## components/ui/button.tsx
1589
+
1590
+ ```tsx
1591
+ import * as React from "react";
1592
+ import { Slot } from "@radix-ui/react-slot";
1593
+ import { cva, type VariantProps } from "class-variance-authority";
1594
+
1595
+ import { cn } from "@/lib/utils";
1596
+
1597
+ const buttonVariants = cva(
1598
+ "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
1599
+ {
1600
+ variants: {
1601
+ variant: {
1602
+ default:
1603
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
1604
+ destructive:
1605
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
1606
+ outline:
1607
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
1608
+ secondary:
1609
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
1610
+ ghost:
1611
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
1612
+ link: "text-primary underline-offset-4 hover:underline",
1613
+ },
1614
+ size: {
1615
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
1616
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
1617
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
1618
+ icon: "size-9",
1619
+ },
1620
+ },
1621
+ defaultVariants: {
1622
+ variant: "default",
1623
+ size: "default",
1624
+ },
1625
+ },
1626
+ );
1627
+
1628
+ function Button({
1629
+ className,
1630
+ variant,
1631
+ size,
1632
+ asChild = false,
1633
+ ...props
1634
+ }: React.ComponentProps<"button"> &
1635
+ VariantProps<typeof buttonVariants> & {
1636
+ asChild?: boolean;
1637
+ }) {
1638
+ const Comp = asChild ? Slot : "button";
1639
+
1640
+ return (
1641
+ <Comp
1642
+ data-slot="button"
1643
+ className={cn(buttonVariants({ variant, size, className }))}
1644
+ {...props}
1645
+ />
1646
+ );
1647
+ }
1648
+
1649
+ export { Button, buttonVariants };
1650
+
1651
+ ```
1652
+
1653
+ ## components/ui/collapsible.tsx
1654
+
1655
+ ```tsx
1656
+ "use client";
1657
+
1658
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
1659
+
1660
+ function Collapsible({
1661
+ ...props
1662
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
1663
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
1664
+ }
1665
+
1666
+ function CollapsibleTrigger({
1667
+ ...props
1668
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
1669
+ return (
1670
+ <CollapsiblePrimitive.CollapsibleTrigger
1671
+ data-slot="collapsible-trigger"
1672
+ {...props}
1673
+ />
1674
+ );
1675
+ }
1676
+
1677
+ function CollapsibleContent({
1678
+ ...props
1679
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
1680
+ return (
1681
+ <CollapsiblePrimitive.CollapsibleContent
1682
+ data-slot="collapsible-content"
1683
+ {...props}
1684
+ />
1685
+ );
1686
+ }
1687
+
1688
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
1689
+
1690
+ ```
1691
+
1692
+ ## components/ui/dialog.tsx
1693
+
1694
+ ```tsx
1695
+ "use client";
1696
+
1697
+ import * as React from "react";
1698
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
1699
+ import { XIcon } from "lucide-react";
1700
+
1701
+ import { cn } from "@/lib/utils";
1702
+
1703
+ function Dialog({
1704
+ ...props
1705
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
1706
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
1707
+ }
1708
+
1709
+ function DialogTrigger({
1710
+ ...props
1711
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
1712
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
1713
+ }
1714
+
1715
+ function DialogPortal({
1716
+ ...props
1717
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
1718
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
1719
+ }
1720
+
1721
+ function DialogClose({
1722
+ ...props
1723
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
1724
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
1725
+ }
1726
+
1727
+ function DialogOverlay({
1728
+ className,
1729
+ ...props
1730
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
1731
+ return (
1732
+ <DialogPrimitive.Overlay
1733
+ data-slot="dialog-overlay"
1734
+ className={cn(
1735
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=open]:animate-in",
1736
+ className,
1737
+ )}
1738
+ {...props}
1739
+ />
1740
+ );
1741
+ }
1742
+
1743
+ function DialogContent({
1744
+ className,
1745
+ children,
1746
+ showCloseButton = true,
1747
+ ...props
1748
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
1749
+ showCloseButton?: boolean;
1750
+ }) {
1751
+ return (
1752
+ <DialogPortal data-slot="dialog-portal">
1753
+ <DialogOverlay />
1754
+ <DialogPrimitive.Content
1755
+ data-slot="dialog-content"
1756
+ className={cn(
1757
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 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 outline-none duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in sm:max-w-lg",
1758
+ className,
1759
+ )}
1760
+ {...props}
1761
+ >
1762
+ {children}
1763
+ {showCloseButton && (
1764
+ <DialogPrimitive.Close
1765
+ data-slot="dialog-close"
1766
+ 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"
1767
+ >
1768
+ <XIcon />
1769
+ <span className="sr-only">Close</span>
1770
+ </DialogPrimitive.Close>
1771
+ )}
1772
+ </DialogPrimitive.Content>
1773
+ </DialogPortal>
1774
+ );
1775
+ }
1776
+
1777
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
1778
+ return (
1779
+ <div
1780
+ data-slot="dialog-header"
1781
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
1782
+ {...props}
1783
+ />
1784
+ );
1785
+ }
1786
+
1787
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
1788
+ return (
1789
+ <div
1790
+ data-slot="dialog-footer"
1791
+ className={cn(
1792
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
1793
+ className,
1794
+ )}
1795
+ {...props}
1796
+ />
1797
+ );
1798
+ }
1799
+
1800
+ function DialogTitle({
1801
+ className,
1802
+ ...props
1803
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
1804
+ return (
1805
+ <DialogPrimitive.Title
1806
+ data-slot="dialog-title"
1807
+ className={cn("font-semibold text-lg leading-none", className)}
1808
+ {...props}
1809
+ />
1810
+ );
1811
+ }
1812
+
1813
+ function DialogDescription({
1814
+ className,
1815
+ ...props
1816
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
1817
+ return (
1818
+ <DialogPrimitive.Description
1819
+ data-slot="dialog-description"
1820
+ className={cn("text-muted-foreground text-sm", className)}
1821
+ {...props}
1822
+ />
1823
+ );
1824
+ }
1825
+
1826
+ export {
1827
+ Dialog,
1828
+ DialogClose,
1829
+ DialogContent,
1830
+ DialogDescription,
1831
+ DialogFooter,
1832
+ DialogHeader,
1833
+ DialogOverlay,
1834
+ DialogPortal,
1835
+ DialogTitle,
1836
+ DialogTrigger,
1837
+ };
1838
+
1839
+ ```
1840
+
1841
+ ## components/ui/tooltip.tsx
1842
+
1843
+ ```tsx
1844
+ "use client";
1845
+
1846
+ import * as React from "react";
1847
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
1848
+
1849
+ import { cn } from "@/lib/utils";
1850
+
1851
+ function TooltipProvider({
1852
+ delayDuration = 0,
1853
+ ...props
1854
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
1855
+ return (
1856
+ <TooltipPrimitive.Provider
1857
+ data-slot="tooltip-provider"
1858
+ delayDuration={delayDuration}
1859
+ {...props}
1860
+ />
1861
+ );
1862
+ }
1863
+
1864
+ function Tooltip({
1865
+ ...props
1866
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
1867
+ return (
1868
+ <TooltipProvider>
1869
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
1870
+ </TooltipProvider>
1871
+ );
1872
+ }
1873
+
1874
+ function TooltipTrigger({
1875
+ ...props
1876
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
1877
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
1878
+ }
1879
+
1880
+ function TooltipContent({
1881
+ className,
1882
+ sideOffset = 0,
1883
+ children,
1884
+ ...props
1885
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
1886
+ return (
1887
+ <TooltipPrimitive.Portal>
1888
+ <TooltipPrimitive.Content
1889
+ data-slot="tooltip-content"
1890
+ sideOffset={sideOffset}
1891
+ className={cn(
1892
+ "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",
1893
+ className,
1894
+ )}
1895
+ {...props}
1896
+ >
1897
+ {children}
1898
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-primary fill-primary" />
1899
+ </TooltipPrimitive.Content>
1900
+ </TooltipPrimitive.Portal>
1901
+ );
1902
+ }
1903
+
1904
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
1905
+
1906
+ ```
1907
+
1908
+ ## lib/elevenlabs-scribe-adapter.ts
1909
+
1910
+ ```typescript
1911
+ import type { DictationAdapter } from "@assistant-ui/react";
1912
+ import { Scribe, RealtimeEvents } from "@elevenlabs/client";
1913
+
1914
+ /**
1915
+ * ElevenLabs Scribe v2 Realtime Adapter for Speech-to-Text
1916
+ *
1917
+ * Uses ElevenLabs Scribe v2 Realtime for real-time transcription via WebSocket.
1918
+ * Provides ultra-low latency (~150ms) transcription with support for 90+ languages.
1919
+ *
1920
+ * @see https://elevenlabs.io/docs/cookbooks/speech-to-text/streaming
1921
+ */
1922
+ export class ElevenLabsScribeAdapter implements DictationAdapter {
1923
+ private tokenEndpoint: string;
1924
+ private languageCode: string;
1925
+ public disableInputDuringDictation: boolean;
1926
+
1927
+ constructor(options: {
1928
+ tokenEndpoint: string;
1929
+ languageCode?: string;
1930
+ /**
1931
+ * Whether to disable text input while listening.
1932
+ * ElevenLabs Scribe returns cumulative transcripts that conflict
1933
+ * with simultaneous typing.
1934
+ * @default true
1935
+ */
1936
+ disableInputDuringDictation?: boolean;
1937
+ }) {
1938
+ this.tokenEndpoint = options.tokenEndpoint;
1939
+ this.languageCode = options.languageCode ?? "en";
1940
+ this.disableInputDuringDictation =
1941
+ options.disableInputDuringDictation ?? true;
1942
+ }
1943
+
1944
+ listen(): DictationAdapter.Session {
1945
+ const callbacks = {
1946
+ start: new Set<() => void>(),
1947
+ end: new Set<(result: DictationAdapter.Result) => void>(),
1948
+ speech: new Set<(result: DictationAdapter.Result) => void>(),
1949
+ };
1950
+
1951
+ let connection: ReturnType<typeof Scribe.connect> | null = null;
1952
+ let fullTranscript = "";
1953
+
1954
+ const session: DictationAdapter.Session = {
1955
+ status: { type: "starting" },
1956
+
1957
+ stop: async () => {
1958
+ if (connection) {
1959
+ connection.commit();
1960
+ await new Promise((resolve) => setTimeout(resolve, 500));
1961
+ connection.close();
1962
+ connection = null;
1963
+ }
1964
+ (session as { status: DictationAdapter.Status }).status = {
1965
+ type: "ended",
1966
+ reason: "stopped",
1967
+ };
1968
+ if (fullTranscript) {
1969
+ for (const cb of callbacks.end) cb({ transcript: fullTranscript });
1970
+ }
1971
+ },
1972
+
1973
+ cancel: () => {
1974
+ if (connection) {
1975
+ connection.close();
1976
+ connection = null;
1977
+ }
1978
+ (session as { status: DictationAdapter.Status }).status = {
1979
+ type: "ended",
1980
+ reason: "cancelled",
1981
+ };
1982
+ },
1983
+
1984
+ onSpeechStart: (callback: () => void) => {
1985
+ callbacks.start.add(callback);
1986
+ return () => {
1987
+ callbacks.start.delete(callback);
1988
+ };
1989
+ },
1990
+
1991
+ onSpeechEnd: (callback: (result: DictationAdapter.Result) => void) => {
1992
+ callbacks.end.add(callback);
1993
+ return () => {
1994
+ callbacks.end.delete(callback);
1995
+ };
1996
+ },
1997
+
1998
+ onSpeech: (callback: (result: DictationAdapter.Result) => void) => {
1999
+ callbacks.speech.add(callback);
2000
+ return () => {
2001
+ callbacks.speech.delete(callback);
2002
+ };
2003
+ },
2004
+ };
2005
+
2006
+ this.connect(session, callbacks, {
2007
+ setConnection: (conn) => {
2008
+ connection = conn;
2009
+ },
2010
+ getFullTranscript: () => fullTranscript,
2011
+ setFullTranscript: (t: string) => {
2012
+ fullTranscript = t;
2013
+ },
2014
+ });
2015
+
2016
+ return session;
2017
+ }
2018
+
2019
+ private async connect(
2020
+ session: DictationAdapter.Session,
2021
+ callbacks: {
2022
+ start: Set<() => void>;
2023
+ end: Set<(result: DictationAdapter.Result) => void>;
2024
+ speech: Set<(result: DictationAdapter.Result) => void>;
2025
+ },
2026
+ refs: {
2027
+ setConnection: (conn: ReturnType<typeof Scribe.connect>) => void;
2028
+ getFullTranscript: () => string;
2029
+ setFullTranscript: (t: string) => void;
2030
+ },
2031
+ ) {
2032
+ try {
2033
+ const tokenResponse = await fetch(this.tokenEndpoint, {
2034
+ method: "POST",
2035
+ });
2036
+
2037
+ if (!tokenResponse.ok) {
2038
+ throw new Error(`Failed to get token: ${tokenResponse.statusText}`);
2039
+ }
2040
+
2041
+ const { token } = await tokenResponse.json();
2042
+
2043
+ const currentStatus = (session as { status: DictationAdapter.Status })
2044
+ .status;
2045
+ if (currentStatus.type === "ended") {
2046
+ // Session was cancelled or stopped before the connection was created.
2047
+ // Avoid opening a new microphone session in this case.
2048
+ return;
2049
+ }
2050
+
2051
+ const connection = Scribe.connect({
2052
+ token,
2053
+ modelId: "scribe_v2_realtime",
2054
+ languageCode: this.languageCode,
2055
+ microphone: {
2056
+ echoCancellation: true,
2057
+ noiseSuppression: true,
2058
+ autoGainControl: true,
2059
+ },
2060
+ });
2061
+
2062
+ refs.setConnection(connection);
2063
+
2064
+ connection.on(RealtimeEvents.SESSION_STARTED, () => {
2065
+ (session as { status: DictationAdapter.Status }).status = {
2066
+ type: "running",
2067
+ };
2068
+ for (const cb of callbacks.start) cb();
2069
+ });
2070
+
2071
+ connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, (data) => {
2072
+ if (data.text) {
2073
+ for (const cb of callbacks.speech)
2074
+ cb({ transcript: data.text, isFinal: false });
2075
+ }
2076
+ });
2077
+
2078
+ connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, (data) => {
2079
+ if (data.text?.trim()) {
2080
+ refs.setFullTranscript(`${refs.getFullTranscript()}${data.text} `);
2081
+ for (const cb of callbacks.speech)
2082
+ cb({ transcript: data.text, isFinal: true });
2083
+ }
2084
+ });
2085
+
2086
+ connection.on(RealtimeEvents.CLOSE, () => {
2087
+ const currentStatus = (
2088
+ session as {
2089
+ status: DictationAdapter.Status;
2090
+ }
2091
+ ).status;
2092
+
2093
+ if (currentStatus.type !== "ended") {
2094
+ (session as { status: DictationAdapter.Status }).status = {
2095
+ type: "ended",
2096
+ reason: "stopped",
2097
+ };
2098
+ }
2099
+
2100
+ const transcript = refs.getFullTranscript().trim();
2101
+ if (transcript) {
2102
+ for (const cb of callbacks.end) cb({ transcript });
2103
+ }
2104
+ });
2105
+
2106
+ connection.on(RealtimeEvents.ERROR, (error) => {
2107
+ console.error("ElevenLabs Scribe error:", error);
2108
+ (session as { status: DictationAdapter.Status }).status = {
2109
+ type: "ended",
2110
+ reason: "error",
2111
+ };
2112
+ });
2113
+
2114
+ connection.on(RealtimeEvents.AUTH_ERROR, (data) => {
2115
+ console.error("ElevenLabs Scribe auth error:", data.error);
2116
+ (session as { status: DictationAdapter.Status }).status = {
2117
+ type: "ended",
2118
+ reason: "error",
2119
+ };
2120
+ });
2121
+ } catch (error) {
2122
+ console.error("ElevenLabs Scribe connection failed:", error);
2123
+ (session as { status: DictationAdapter.Status }).status = {
2124
+ type: "ended",
2125
+ reason: "error",
2126
+ };
2127
+ }
2128
+ }
2129
+ }
2130
+
2131
+ ```
2132
+
2133
+ ## lib/utils.ts
2134
+
2135
+ ```typescript
2136
+ import { clsx, type ClassValue } from "clsx";
2137
+ import { twMerge } from "tailwind-merge";
2138
+
2139
+ export function cn(...inputs: ClassValue[]) {
2140
+ return twMerge(clsx(inputs));
2141
+ }
2142
+
2143
+ ```
2144
+
2145
+ ## next.config.js
2146
+
2147
+ ```javascript
2148
+ /** @type {import('next').NextConfig} */
2149
+ const nextConfig = {
2150
+ transpilePackages: ["@assistant-ui/react", "@assistant-ui/react-ai-sdk"],
2151
+ };
2152
+
2153
+ export default nextConfig;
2154
+
2155
+ ```
2156
+
2157
+ ## package.json
2158
+
2159
+ ```json
2160
+ {
2161
+ "name": "example-with-elevenlabs-scribe",
2162
+ "private": true,
2163
+ "version": "0.0.0",
2164
+ "type": "module",
2165
+ "dependencies": {
2166
+ "@ai-sdk/openai": "^3.0.13",
2167
+ "@ai-sdk/react": "^3.0.44",
2168
+ "@assistant-ui/react": "workspace:^",
2169
+ "@assistant-ui/react-ai-sdk": "workspace:*",
2170
+ "@assistant-ui/react-markdown": "workspace:^",
2171
+ "@elevenlabs/client": "^0.13.0",
2172
+ "@radix-ui/react-avatar": "^1.1.11",
2173
+ "@radix-ui/react-collapsible": "^1.1.12",
2174
+ "@radix-ui/react-dialog": "^1.1.15",
2175
+ "@radix-ui/react-slot": "^1.2.4",
2176
+ "@radix-ui/react-tooltip": "^1.2.8",
2177
+ "@tailwindcss/postcss": "^4.1.18",
2178
+ "ai": "^6.0.42",
2179
+ "class-variance-authority": "^0.7.1",
2180
+ "clsx": "^2.1.1",
2181
+ "lucide-react": "^0.562.0",
2182
+ "motion": "^12.27.5",
2183
+ "next": "^16.1.4",
2184
+ "postcss": "^8.5.6",
2185
+ "react": "^19.2.3",
2186
+ "react-dom": "^19.2.3",
2187
+ "remark-gfm": "^4.0.1",
2188
+ "tailwind-merge": "^3.4.0",
2189
+ "tailwindcss": "^4.1.18",
2190
+ "zod": "^4.3.5",
2191
+ "zustand": "^5.0.10"
2192
+ },
2193
+ "devDependencies": {
2194
+ "@assistant-ui/x-buildutils": "workspace:*",
2195
+ "@types/node": "^25.0.9",
2196
+ "@types/react": "^19.2.9",
2197
+ "@types/react-dom": "^19.2.3",
2198
+ "tw-animate-css": "^1.4.0",
2199
+ "typescript": "^5.9.3"
2200
+ },
2201
+ "scripts": {
2202
+ "dev": "next dev",
2203
+ "build": "next build",
2204
+ "start": "next start"
2205
+ }
2206
+ }
2207
+
2208
+ ```
2209
+
2210
+ ## tsconfig.json
2211
+
2212
+ ```json
2213
+ {
2214
+ "extends": "@assistant-ui/x-buildutils/ts/base",
2215
+ "compilerOptions": {
2216
+ "target": "ES6",
2217
+ "module": "ESNext",
2218
+ "incremental": true,
2219
+ "plugins": [
2220
+ {
2221
+ "name": "next"
2222
+ }
2223
+ ],
2224
+ "allowJs": true,
2225
+ "strictNullChecks": true,
2226
+ "jsx": "preserve",
2227
+ "paths": {
2228
+ "@/*": ["./*"],
2229
+ "@assistant-ui/*": ["../../packages/*/src"],
2230
+ "@assistant-ui/react/*": ["../../packages/react/src/*"],
2231
+ "@assistant-ui/tap/*": ["../../packages/tap/src/*"],
2232
+ "assistant-stream": ["../../packages/assistant-stream/src"],
2233
+ "assistant-stream/*": ["../../packages/assistant-stream/src/*"]
2234
+ }
2235
+ },
2236
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
2237
+ "exclude": ["node_modules"]
2238
+ }
2239
+
2240
+ ```
2241
+