@assistant-ui/mcp-docs-server 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.docs/organized/code-examples/local-ollama.md +1135 -0
  2. package/.docs/organized/code-examples/search-agent-for-e-commerce.md +1721 -0
  3. package/.docs/organized/code-examples/with-ai-sdk.md +1081 -0
  4. package/.docs/organized/code-examples/with-cloud.md +1164 -0
  5. package/.docs/organized/code-examples/with-external-store.md +1064 -0
  6. package/.docs/organized/code-examples/with-ffmpeg.md +1305 -0
  7. package/.docs/organized/code-examples/with-langgraph.md +1819 -0
  8. package/.docs/organized/code-examples/with-openai-assistants.md +1175 -0
  9. package/.docs/organized/code-examples/with-react-hook-form.md +1727 -0
  10. package/.docs/organized/code-examples/with-vercel-ai-rsc.md +1157 -0
  11. package/.docs/raw/blog/2024-07-29-hello/index.mdx +65 -0
  12. package/.docs/raw/blog/2024-09-11/index.mdx +10 -0
  13. package/.docs/raw/blog/2024-12-15/index.mdx +10 -0
  14. package/.docs/raw/blog/2025-01-31-changelog/index.mdx +129 -0
  15. package/.docs/raw/docs/about-assistantui.mdx +44 -0
  16. package/.docs/raw/docs/api-reference/context-providers/AssistantRuntimeProvider.mdx +30 -0
  17. package/.docs/raw/docs/api-reference/context-providers/TextContentPartProvider.mdx +26 -0
  18. package/.docs/raw/docs/api-reference/integrations/react-hook-form.mdx +103 -0
  19. package/.docs/raw/docs/api-reference/integrations/vercel-ai-sdk.mdx +145 -0
  20. package/.docs/raw/docs/api-reference/overview.mdx +583 -0
  21. package/.docs/raw/docs/api-reference/primitives/ActionBar.mdx +264 -0
  22. package/.docs/raw/docs/api-reference/primitives/AssistantModal.mdx +129 -0
  23. package/.docs/raw/docs/api-reference/primitives/Attachment.mdx +96 -0
  24. package/.docs/raw/docs/api-reference/primitives/BranchPicker.mdx +87 -0
  25. package/.docs/raw/docs/api-reference/primitives/Composer.mdx +204 -0
  26. package/.docs/raw/docs/api-reference/primitives/ContentPart.mdx +173 -0
  27. package/.docs/raw/docs/api-reference/primitives/Error.mdx +70 -0
  28. package/.docs/raw/docs/api-reference/primitives/Message.mdx +181 -0
  29. package/.docs/raw/docs/api-reference/primitives/Thread.mdx +197 -0
  30. package/.docs/raw/docs/api-reference/primitives/composition.mdx +21 -0
  31. package/.docs/raw/docs/api-reference/runtimes/AssistantRuntime.mdx +33 -0
  32. package/.docs/raw/docs/api-reference/runtimes/AttachmentRuntime.mdx +46 -0
  33. package/.docs/raw/docs/api-reference/runtimes/ComposerRuntime.mdx +69 -0
  34. package/.docs/raw/docs/api-reference/runtimes/ContentPartRuntime.mdx +22 -0
  35. package/.docs/raw/docs/api-reference/runtimes/MessageRuntime.mdx +49 -0
  36. package/.docs/raw/docs/api-reference/runtimes/ThreadListItemRuntime.mdx +32 -0
  37. package/.docs/raw/docs/api-reference/runtimes/ThreadListRuntime.mdx +31 -0
  38. package/.docs/raw/docs/api-reference/runtimes/ThreadRuntime.mdx +48 -0
  39. package/.docs/raw/docs/architecture.mdx +92 -0
  40. package/.docs/raw/docs/cloud/authorization.mdx +152 -0
  41. package/.docs/raw/docs/cloud/overview.mdx +55 -0
  42. package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +54 -0
  43. package/.docs/raw/docs/cloud/persistence/langgraph.mdx +123 -0
  44. package/.docs/raw/docs/concepts/architecture.mdx +19 -0
  45. package/.docs/raw/docs/concepts/runtime-layer.mdx +163 -0
  46. package/.docs/raw/docs/concepts/why.mdx +9 -0
  47. package/.docs/raw/docs/copilots/make-assistant-readable.mdx +71 -0
  48. package/.docs/raw/docs/copilots/make-assistant-tool-ui.mdx +76 -0
  49. package/.docs/raw/docs/copilots/make-assistant-tool.mdx +117 -0
  50. package/.docs/raw/docs/copilots/model-context.mdx +135 -0
  51. package/.docs/raw/docs/copilots/motivation.mdx +191 -0
  52. package/.docs/raw/docs/copilots/use-assistant-instructions.mdx +62 -0
  53. package/.docs/raw/docs/getting-started.mdx +1133 -0
  54. package/.docs/raw/docs/guides/Attachments.mdx +640 -0
  55. package/.docs/raw/docs/guides/Branching.mdx +59 -0
  56. package/.docs/raw/docs/guides/Editing.mdx +56 -0
  57. package/.docs/raw/docs/guides/Speech.mdx +43 -0
  58. package/.docs/raw/docs/guides/ToolUI.mdx +663 -0
  59. package/.docs/raw/docs/guides/Tools.mdx +496 -0
  60. package/.docs/raw/docs/index.mdx +7 -0
  61. package/.docs/raw/docs/legacy/styled/AssistantModal.mdx +85 -0
  62. package/.docs/raw/docs/legacy/styled/Decomposition.mdx +633 -0
  63. package/.docs/raw/docs/legacy/styled/Markdown.mdx +86 -0
  64. package/.docs/raw/docs/legacy/styled/Scrollbar.mdx +71 -0
  65. package/.docs/raw/docs/legacy/styled/Thread.mdx +84 -0
  66. package/.docs/raw/docs/legacy/styled/ThreadWidth.mdx +21 -0
  67. package/.docs/raw/docs/mcp-docs-server.mdx +324 -0
  68. package/.docs/raw/docs/migrations/deprecation-policy.mdx +41 -0
  69. package/.docs/raw/docs/migrations/v0-7.mdx +188 -0
  70. package/.docs/raw/docs/migrations/v0-8.mdx +160 -0
  71. package/.docs/raw/docs/migrations/v0-9.mdx +75 -0
  72. package/.docs/raw/docs/react-compatibility.mdx +208 -0
  73. package/.docs/raw/docs/runtimes/ai-sdk/rsc.mdx +226 -0
  74. package/.docs/raw/docs/runtimes/ai-sdk/use-assistant-hook.mdx +195 -0
  75. package/.docs/raw/docs/runtimes/ai-sdk/use-chat-hook.mdx +138 -0
  76. package/.docs/raw/docs/runtimes/ai-sdk/use-chat.mdx +136 -0
  77. package/.docs/raw/docs/runtimes/custom/external-store.mdx +1624 -0
  78. package/.docs/raw/docs/runtimes/custom/local.mdx +1185 -0
  79. package/.docs/raw/docs/runtimes/helicone.mdx +60 -0
  80. package/.docs/raw/docs/runtimes/langgraph/index.mdx +320 -0
  81. package/.docs/raw/docs/runtimes/langgraph/tutorial/index.mdx +11 -0
  82. package/.docs/raw/docs/runtimes/langgraph/tutorial/introduction.mdx +28 -0
  83. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +120 -0
  84. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +336 -0
  85. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +385 -0
  86. package/.docs/raw/docs/runtimes/langserve.mdx +126 -0
  87. package/.docs/raw/docs/runtimes/mastra/full-stack-integration.mdx +218 -0
  88. package/.docs/raw/docs/runtimes/mastra/overview.mdx +17 -0
  89. package/.docs/raw/docs/runtimes/mastra/separate-server-integration.mdx +196 -0
  90. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +222 -0
  91. package/.docs/raw/docs/ui/AssistantModal.mdx +46 -0
  92. package/.docs/raw/docs/ui/AssistantSidebar.mdx +42 -0
  93. package/.docs/raw/docs/ui/Attachment.mdx +82 -0
  94. package/.docs/raw/docs/ui/Markdown.mdx +72 -0
  95. package/.docs/raw/docs/ui/Mermaid.mdx +79 -0
  96. package/.docs/raw/docs/ui/Scrollbar.mdx +59 -0
  97. package/.docs/raw/docs/ui/SyntaxHighlighting.mdx +253 -0
  98. package/.docs/raw/docs/ui/Thread.mdx +47 -0
  99. package/.docs/raw/docs/ui/ThreadList.mdx +49 -0
  100. package/.docs/raw/docs/ui/ToolFallback.mdx +64 -0
  101. package/.docs/raw/docs/ui/primitives/Thread.mdx +197 -0
  102. package/LICENSE +21 -0
  103. package/README.md +128 -0
  104. package/dist/chunk-C7O7EFKU.js +38 -0
  105. package/dist/chunk-CZCDQ3YH.js +420 -0
  106. package/dist/index.js +1 -0
  107. package/dist/prepare-docs/prepare.js +199 -0
  108. package/dist/stdio.js +8 -0
  109. package/package.json +43 -0
@@ -0,0 +1,1819 @@
1
+ # Example: with-langgraph
2
+
3
+ ## app/api/[..._path]/route.ts
4
+
5
+ ```typescript
6
+ import { NextRequest, NextResponse } from "next/server";
7
+
8
+ export const runtime = "edge";
9
+
10
+ function getCorsHeaders() {
11
+ return {
12
+ "Access-Control-Allow-Origin": "*",
13
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
14
+ "Access-Control-Allow-Headers": "*",
15
+ };
16
+ }
17
+
18
+ async function handleRequest(req: NextRequest, method: string) {
19
+ try {
20
+ const path = req.nextUrl.pathname.replace(/^\/?api\//, "");
21
+ const url = new URL(req.url);
22
+ const searchParams = new URLSearchParams(url.search);
23
+ searchParams.delete("_path");
24
+ searchParams.delete("nxtP_path");
25
+ const queryString = searchParams.toString()
26
+ ? `?${searchParams.toString()}`
27
+ : "";
28
+
29
+ const options: RequestInit = {
30
+ method,
31
+ headers: {
32
+ "x-api-key": process.env["LANGCHAIN_API_KEY"] || "",
33
+ },
34
+ };
35
+
36
+ if (["POST", "PUT", "PATCH"].includes(method)) {
37
+ options.body = await req.text();
38
+ }
39
+
40
+ const res = await fetch(
41
+ `${process.env["LANGGRAPH_API_URL"]}/${path}${queryString}`,
42
+ options,
43
+ );
44
+
45
+ return new NextResponse(res.body, {
46
+ status: res.status,
47
+ statusText: res.statusText,
48
+ headers: {
49
+ ...res.headers,
50
+ ...getCorsHeaders(),
51
+ },
52
+ });
53
+ } catch (e: unknown) {
54
+ if (e instanceof Error) {
55
+ return NextResponse.json(
56
+ { error: e.message },
57
+ { status: (e as { status?: number }).status ?? 500 },
58
+ );
59
+ }
60
+ return NextResponse.json({ error: "Unknown error" }, { status: 500 });
61
+ }
62
+ }
63
+
64
+ export const GET = (req: NextRequest) => handleRequest(req, "GET");
65
+ export const POST = (req: NextRequest) => handleRequest(req, "POST");
66
+ export const PUT = (req: NextRequest) => handleRequest(req, "PUT");
67
+ export const PATCH = (req: NextRequest) => handleRequest(req, "PATCH");
68
+ export const DELETE = (req: NextRequest) => handleRequest(req, "DELETE");
69
+
70
+ // Add a new OPTIONS handler
71
+ export const OPTIONS = () => {
72
+ return new NextResponse(null, {
73
+ status: 204,
74
+ headers: {
75
+ ...getCorsHeaders(),
76
+ },
77
+ });
78
+ };
79
+
80
+ ```
81
+
82
+ ## app/api/assistant-ui-token/route.ts
83
+
84
+ ```typescript
85
+ import { customAlphabet } from "nanoid";
86
+ import { cookies } from "next/headers";
87
+ import jwt, { JwtPayload } from "jsonwebtoken";
88
+ import { AssistantCloud } from "@assistant-ui/react";
89
+
90
+ const generateId = customAlphabet(
91
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
92
+ 32,
93
+ );
94
+ const randomUserId = () => {
95
+ const userId = "usr_anon_" + generateId();
96
+ return userId;
97
+ };
98
+
99
+ const getJwtForUser = (userId: string) => {
100
+ return jwt.sign(
101
+ {
102
+ sub: userId,
103
+ iat: Math.floor(Date.now() / 1000),
104
+ exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 1 week
105
+ },
106
+ process.env["JWT_SECRET"]!,
107
+ );
108
+ };
109
+
110
+ const getUserIdFromJwt = (token: string) => {
111
+ const decoded = jwt.verify(token, process.env["JWT_SECRET"]!) as JwtPayload;
112
+ return decoded.sub!;
113
+ };
114
+
115
+ export const POST = async () => {
116
+ const cookieStore = await cookies();
117
+ const jwtCookie = cookieStore.get("jwt");
118
+ let userId;
119
+ if (!jwtCookie) {
120
+ userId = randomUserId();
121
+ } else {
122
+ userId = getUserIdFromJwt(jwtCookie.value);
123
+ }
124
+
125
+ cookieStore.set("jwt", getJwtForUser(userId), {
126
+ path: "/",
127
+ httpOnly: true,
128
+ secure: process.env.NODE_ENV === "production",
129
+ sameSite: "strict",
130
+ maxAge: 60 * 60 * 24 * 7, // 1 week
131
+ });
132
+
133
+ const client = new AssistantCloud({
134
+ apiKey: process.env["ASSISTANT_API_KEY"]!,
135
+ userId,
136
+ workspaceId: userId,
137
+ });
138
+ const { token } = await client.auth.tokens.create();
139
+ return Response.json({ token });
140
+ };
141
+
142
+ ```
143
+
144
+ ## app/globals.css
145
+
146
+ ```css
147
+ @import "tailwindcss";
148
+ @import "tw-animate-css";
149
+
150
+ @custom-variant dark (&:is(.dark *));
151
+
152
+ @theme inline {
153
+ --color-background: var(--background);
154
+ --color-foreground: var(--foreground);
155
+ --font-sans: var(--font-geist-sans);
156
+ --font-mono: var(--font-geist-mono);
157
+ --color-sidebar-ring: var(--sidebar-ring);
158
+ --color-sidebar-border: var(--sidebar-border);
159
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
160
+ --color-sidebar-accent: var(--sidebar-accent);
161
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
162
+ --color-sidebar-primary: var(--sidebar-primary);
163
+ --color-sidebar-foreground: var(--sidebar-foreground);
164
+ --color-sidebar: var(--sidebar);
165
+ --color-chart-5: var(--chart-5);
166
+ --color-chart-4: var(--chart-4);
167
+ --color-chart-3: var(--chart-3);
168
+ --color-chart-2: var(--chart-2);
169
+ --color-chart-1: var(--chart-1);
170
+ --color-ring: var(--ring);
171
+ --color-input: var(--input);
172
+ --color-border: var(--border);
173
+ --color-destructive: var(--destructive);
174
+ --color-accent-foreground: var(--accent-foreground);
175
+ --color-accent: var(--accent);
176
+ --color-muted-foreground: var(--muted-foreground);
177
+ --color-muted: var(--muted);
178
+ --color-secondary-foreground: var(--secondary-foreground);
179
+ --color-secondary: var(--secondary);
180
+ --color-primary-foreground: var(--primary-foreground);
181
+ --color-primary: var(--primary);
182
+ --color-popover-foreground: var(--popover-foreground);
183
+ --color-popover: var(--popover);
184
+ --color-card-foreground: var(--card-foreground);
185
+ --color-card: var(--card);
186
+ --radius-sm: calc(var(--radius) - 4px);
187
+ --radius-md: calc(var(--radius) - 2px);
188
+ --radius-lg: var(--radius);
189
+ --radius-xl: calc(var(--radius) + 4px);
190
+ }
191
+
192
+ :root {
193
+ --radius: 0.625rem;
194
+ --background: oklch(1 0 0);
195
+ --foreground: oklch(0.141 0.005 285.823);
196
+ --card: oklch(1 0 0);
197
+ --card-foreground: oklch(0.141 0.005 285.823);
198
+ --popover: oklch(1 0 0);
199
+ --popover-foreground: oklch(0.141 0.005 285.823);
200
+ --primary: oklch(0.21 0.006 285.885);
201
+ --primary-foreground: oklch(0.985 0 0);
202
+ --secondary: oklch(0.967 0.001 286.375);
203
+ --secondary-foreground: oklch(0.21 0.006 285.885);
204
+ --muted: oklch(0.967 0.001 286.375);
205
+ --muted-foreground: oklch(0.552 0.016 285.938);
206
+ --accent: oklch(0.967 0.001 286.375);
207
+ --accent-foreground: oklch(0.21 0.006 285.885);
208
+ --destructive: oklch(0.577 0.245 27.325);
209
+ --border: oklch(0.92 0.004 286.32);
210
+ --input: oklch(0.92 0.004 286.32);
211
+ --ring: oklch(0.705 0.015 286.067);
212
+ --chart-1: oklch(0.646 0.222 41.116);
213
+ --chart-2: oklch(0.6 0.118 184.704);
214
+ --chart-3: oklch(0.398 0.07 227.392);
215
+ --chart-4: oklch(0.828 0.189 84.429);
216
+ --chart-5: oklch(0.769 0.188 70.08);
217
+ --sidebar: oklch(0.985 0 0);
218
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
219
+ --sidebar-primary: oklch(0.21 0.006 285.885);
220
+ --sidebar-primary-foreground: oklch(0.985 0 0);
221
+ --sidebar-accent: oklch(0.967 0.001 286.375);
222
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
223
+ --sidebar-border: oklch(0.92 0.004 286.32);
224
+ --sidebar-ring: oklch(0.705 0.015 286.067);
225
+ }
226
+
227
+ .dark {
228
+ --background: oklch(0.141 0.005 285.823);
229
+ --foreground: oklch(0.985 0 0);
230
+ --card: oklch(0.21 0.006 285.885);
231
+ --card-foreground: oklch(0.985 0 0);
232
+ --popover: oklch(0.21 0.006 285.885);
233
+ --popover-foreground: oklch(0.985 0 0);
234
+ --primary: oklch(0.92 0.004 286.32);
235
+ --primary-foreground: oklch(0.21 0.006 285.885);
236
+ --secondary: oklch(0.274 0.006 286.033);
237
+ --secondary-foreground: oklch(0.985 0 0);
238
+ --muted: oklch(0.274 0.006 286.033);
239
+ --muted-foreground: oklch(0.705 0.015 286.067);
240
+ --accent: oklch(0.274 0.006 286.033);
241
+ --accent-foreground: oklch(0.985 0 0);
242
+ --destructive: oklch(0.704 0.191 22.216);
243
+ --border: oklch(1 0 0 / 10%);
244
+ --input: oklch(1 0 0 / 15%);
245
+ --ring: oklch(0.552 0.016 285.938);
246
+ --chart-1: oklch(0.488 0.243 264.376);
247
+ --chart-2: oklch(0.696 0.17 162.48);
248
+ --chart-3: oklch(0.769 0.188 70.08);
249
+ --chart-4: oklch(0.627 0.265 303.9);
250
+ --chart-5: oklch(0.645 0.246 16.439);
251
+ --sidebar: oklch(0.21 0.006 285.885);
252
+ --sidebar-foreground: oklch(0.985 0 0);
253
+ --sidebar-primary: oklch(0.488 0.243 264.376);
254
+ --sidebar-primary-foreground: oklch(0.985 0 0);
255
+ --sidebar-accent: oklch(0.274 0.006 286.033);
256
+ --sidebar-accent-foreground: oklch(0.985 0 0);
257
+ --sidebar-border: oklch(1 0 0 / 10%);
258
+ --sidebar-ring: oklch(0.552 0.016 285.938);
259
+ }
260
+
261
+ @layer base {
262
+ * {
263
+ @apply border-border outline-ring/50;
264
+ }
265
+ body {
266
+ @apply bg-background text-foreground;
267
+ }
268
+ }
269
+
270
+ ```
271
+
272
+ ## app/layout.tsx
273
+
274
+ ```tsx
275
+ import "./globals.css";
276
+
277
+ import { cn } from "@/lib/utils";
278
+ import { Montserrat } from "next/font/google";
279
+ import { MyRuntimeProvider } from "./MyRuntimeProvider";
280
+ import { Suspense } from "react";
281
+
282
+ const montserrat = Montserrat({ subsets: ["latin"] });
283
+
284
+ export default function RootLayout({
285
+ children,
286
+ }: Readonly<{
287
+ children: React.ReactNode;
288
+ }>) {
289
+ return (
290
+ <MyRuntimeProvider>
291
+ <html lang="en">
292
+ <body className={cn(montserrat.className, "h-dvh")}>
293
+ <Suspense>{children}</Suspense>
294
+ </body>
295
+ </html>
296
+ </MyRuntimeProvider>
297
+ );
298
+ }
299
+
300
+ ```
301
+
302
+ ## app/MyRuntimeProvider.tsx
303
+
304
+ ```tsx
305
+ "use client";
306
+
307
+ import {
308
+ AssistantCloud,
309
+ AssistantRuntimeProvider,
310
+ useCloudThreadListRuntime,
311
+ useThreadListItemRuntime,
312
+ } from "@assistant-ui/react";
313
+ import { useLangGraphRuntime } from "@assistant-ui/react-langgraph";
314
+ import { createThread, getThreadState, sendMessage } from "@/lib/chatApi";
315
+ import { LangChainMessage } from "@assistant-ui/react-langgraph";
316
+
317
+ const useMyLangGraphRuntime = () => {
318
+ const threadListItemRuntime = useThreadListItemRuntime();
319
+ const runtime = useLangGraphRuntime({
320
+ stream: async function* (messages) {
321
+ const { externalId } = await threadListItemRuntime.initialize();
322
+ if (!externalId) throw new Error("Thread not found");
323
+
324
+ const generator = sendMessage({
325
+ threadId: externalId,
326
+ messages,
327
+ });
328
+
329
+ yield* generator;
330
+ },
331
+ onSwitchToThread: async (externalId) => {
332
+ const state = await getThreadState(externalId);
333
+ return {
334
+ messages:
335
+ (state.values as { messages?: LangChainMessage[] }).messages ?? [],
336
+ interrupts: state.tasks[0]?.interrupts ?? [],
337
+ };
338
+ },
339
+ });
340
+
341
+ return runtime;
342
+ };
343
+
344
+ const cloud = new AssistantCloud({
345
+ baseUrl: process.env["NEXT_PUBLIC_ASSISTANT_BASE_URL"]!,
346
+ authToken: () =>
347
+ fetch("/api/assistant-ui-token", { method: "POST" })
348
+ .then((r) => r.json())
349
+ .then((r) => r.token),
350
+ });
351
+
352
+ export function MyRuntimeProvider({
353
+ children,
354
+ }: Readonly<{
355
+ children: React.ReactNode;
356
+ }>) {
357
+ const runtime = useCloudThreadListRuntime({
358
+ cloud,
359
+ runtimeHook: useMyLangGraphRuntime,
360
+ create: async () => {
361
+ const { thread_id } = await createThread();
362
+ return { externalId: thread_id };
363
+ },
364
+ });
365
+
366
+ return (
367
+ <AssistantRuntimeProvider runtime={runtime}>
368
+ {children}
369
+ </AssistantRuntimeProvider>
370
+ );
371
+ }
372
+
373
+ ```
374
+
375
+ ## app/page.tsx
376
+
377
+ ```tsx
378
+ "use client";
379
+
380
+ import { Thread } from "@/components/assistant-ui/thread";
381
+ import { PriceSnapshotTool } from "@/components/tools/price-snapshot/PriceSnapshotTool";
382
+ import { PurchaseStockTool } from "@/components/tools/purchase-stock/PurchaseStockTool";
383
+ import { ThreadList } from "@/components/assistant-ui/thread-list";
384
+
385
+ export default function Home() {
386
+ return (
387
+ <div className="flex h-dvh">
388
+ <div className="max-w-md">
389
+ <ThreadList />
390
+ </div>
391
+ <div className="flex-grow">
392
+ <Thread />
393
+ <PriceSnapshotTool />
394
+ <PurchaseStockTool />
395
+ </div>
396
+ </div>
397
+ );
398
+ }
399
+
400
+ ```
401
+
402
+ ## components.json
403
+
404
+ ```json
405
+ {
406
+ "$schema": "https://ui.shadcn.com/schema.json",
407
+ "style": "new-york",
408
+ "rsc": true,
409
+ "tsx": true,
410
+ "tailwind": {
411
+ "config": "",
412
+ "css": "app/globals.css",
413
+ "baseColor": "zinc",
414
+ "cssVariables": true,
415
+ "prefix": ""
416
+ },
417
+ "aliases": {
418
+ "components": "@/components",
419
+ "utils": "@/lib/utils"
420
+ }
421
+ }
422
+
423
+ ```
424
+
425
+ ## components/assistant-ui/markdown-text.tsx
426
+
427
+ ```tsx
428
+ "use client";
429
+
430
+ import "@assistant-ui/react-markdown/styles/dot.css";
431
+
432
+ import {
433
+ CodeHeaderProps,
434
+ MarkdownTextPrimitive,
435
+ unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
436
+ useIsMarkdownCodeBlock,
437
+ } from "@assistant-ui/react-markdown";
438
+ import remarkGfm from "remark-gfm";
439
+ import { FC, memo, useState } from "react";
440
+ import { CheckIcon, CopyIcon } from "lucide-react";
441
+
442
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
443
+ import { cn } from "@/lib/utils";
444
+
445
+ const MarkdownTextImpl = () => {
446
+ return (
447
+ <MarkdownTextPrimitive
448
+ remarkPlugins={[remarkGfm]}
449
+ className="aui-md"
450
+ components={defaultComponents}
451
+ />
452
+ );
453
+ };
454
+
455
+ export const MarkdownText = memo(MarkdownTextImpl);
456
+
457
+ const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
458
+ const { isCopied, copyToClipboard } = useCopyToClipboard();
459
+ const onCopy = () => {
460
+ if (!code || isCopied) return;
461
+ copyToClipboard(code);
462
+ };
463
+
464
+ return (
465
+ <div className="flex items-center justify-between gap-4 rounded-t-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white">
466
+ <span className="lowercase [&>span]:text-xs">{language}</span>
467
+ <TooltipIconButton tooltip="Copy" onClick={onCopy}>
468
+ {!isCopied && <CopyIcon />}
469
+ {isCopied && <CheckIcon />}
470
+ </TooltipIconButton>
471
+ </div>
472
+ );
473
+ };
474
+
475
+ const useCopyToClipboard = ({
476
+ copiedDuration = 3000,
477
+ }: {
478
+ copiedDuration?: number;
479
+ } = {}) => {
480
+ const [isCopied, setIsCopied] = useState<boolean>(false);
481
+
482
+ const copyToClipboard = (value: string) => {
483
+ if (!value) return;
484
+
485
+ navigator.clipboard.writeText(value).then(() => {
486
+ setIsCopied(true);
487
+ setTimeout(() => setIsCopied(false), copiedDuration);
488
+ });
489
+ };
490
+
491
+ return { isCopied, copyToClipboard };
492
+ };
493
+
494
+ const defaultComponents = memoizeMarkdownComponents({
495
+ h1: ({ className, ...props }) => (
496
+ <h1
497
+ className={cn(
498
+ "mb-8 scroll-m-20 text-4xl font-extrabold tracking-tight last:mb-0",
499
+ className,
500
+ )}
501
+ {...props}
502
+ />
503
+ ),
504
+ h2: ({ className, ...props }) => (
505
+ <h2
506
+ className={cn(
507
+ "mb-4 mt-8 scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0 last:mb-0",
508
+ className,
509
+ )}
510
+ {...props}
511
+ />
512
+ ),
513
+ h3: ({ className, ...props }) => (
514
+ <h3
515
+ className={cn(
516
+ "mb-4 mt-6 scroll-m-20 text-2xl font-semibold tracking-tight first:mt-0 last:mb-0",
517
+ className,
518
+ )}
519
+ {...props}
520
+ />
521
+ ),
522
+ h4: ({ className, ...props }) => (
523
+ <h4
524
+ className={cn(
525
+ "mb-4 mt-6 scroll-m-20 text-xl font-semibold tracking-tight first:mt-0 last:mb-0",
526
+ className,
527
+ )}
528
+ {...props}
529
+ />
530
+ ),
531
+ h5: ({ className, ...props }) => (
532
+ <h5
533
+ className={cn(
534
+ "my-4 text-lg font-semibold first:mt-0 last:mb-0",
535
+ className,
536
+ )}
537
+ {...props}
538
+ />
539
+ ),
540
+ h6: ({ className, ...props }) => (
541
+ <h6
542
+ className={cn("my-4 font-semibold first:mt-0 last:mb-0", className)}
543
+ {...props}
544
+ />
545
+ ),
546
+ p: ({ className, ...props }) => (
547
+ <p
548
+ className={cn("mb-5 mt-5 leading-7 first:mt-0 last:mb-0", className)}
549
+ {...props}
550
+ />
551
+ ),
552
+ a: ({ className, ...props }) => (
553
+ <a
554
+ className={cn(
555
+ "text-primary font-medium underline underline-offset-4",
556
+ className,
557
+ )}
558
+ {...props}
559
+ />
560
+ ),
561
+ blockquote: ({ className, ...props }) => (
562
+ <blockquote
563
+ className={cn("border-l-2 pl-6 italic", className)}
564
+ {...props}
565
+ />
566
+ ),
567
+ ul: ({ className, ...props }) => (
568
+ <ul
569
+ className={cn("my-5 ml-6 list-disc [&>li]:mt-2", className)}
570
+ {...props}
571
+ />
572
+ ),
573
+ ol: ({ className, ...props }) => (
574
+ <ol
575
+ className={cn("my-5 ml-6 list-decimal [&>li]:mt-2", className)}
576
+ {...props}
577
+ />
578
+ ),
579
+ hr: ({ className, ...props }) => (
580
+ <hr className={cn("my-5 border-b", className)} {...props} />
581
+ ),
582
+ table: ({ className, ...props }) => (
583
+ <table
584
+ className={cn(
585
+ "my-5 w-full border-separate border-spacing-0 overflow-y-auto",
586
+ className,
587
+ )}
588
+ {...props}
589
+ />
590
+ ),
591
+ th: ({ className, ...props }) => (
592
+ <th
593
+ className={cn(
594
+ "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",
595
+ className,
596
+ )}
597
+ {...props}
598
+ />
599
+ ),
600
+ td: ({ className, ...props }) => (
601
+ <td
602
+ className={cn(
603
+ "border-b border-l px-4 py-2 text-left last:border-r [&[align=center]]:text-center [&[align=right]]:text-right",
604
+ className,
605
+ )}
606
+ {...props}
607
+ />
608
+ ),
609
+ tr: ({ className, ...props }) => (
610
+ <tr
611
+ className={cn(
612
+ "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",
613
+ className,
614
+ )}
615
+ {...props}
616
+ />
617
+ ),
618
+ sup: ({ className, ...props }) => (
619
+ <sup
620
+ className={cn("[&>a]:text-xs [&>a]:no-underline", className)}
621
+ {...props}
622
+ />
623
+ ),
624
+ pre: ({ className, ...props }) => (
625
+ <pre
626
+ className={cn(
627
+ "overflow-x-auto rounded-b-lg bg-black p-4 text-white",
628
+ className,
629
+ )}
630
+ {...props}
631
+ />
632
+ ),
633
+ code: function Code({ className, ...props }) {
634
+ const isCodeBlock = useIsMarkdownCodeBlock();
635
+ return (
636
+ <code
637
+ className={cn(
638
+ !isCodeBlock && "bg-muted rounded border font-semibold",
639
+ className,
640
+ )}
641
+ {...props}
642
+ />
643
+ );
644
+ },
645
+ CodeHeader,
646
+ });
647
+
648
+ ```
649
+
650
+ ## components/assistant-ui/thread-list.tsx
651
+
652
+ ```tsx
653
+ import type { FC } from "react";
654
+ import {
655
+ ThreadListItemPrimitive,
656
+ ThreadListPrimitive,
657
+ } from "@assistant-ui/react";
658
+ import { ArchiveIcon, PlusIcon } from "lucide-react";
659
+
660
+ import { Button } from "@/components/ui/button";
661
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
662
+
663
+ export const ThreadList: FC = () => {
664
+ return (
665
+ <ThreadListPrimitive.Root className="flex flex-col items-stretch gap-1.5">
666
+ <ThreadListNew />
667
+ <ThreadListItems />
668
+ </ThreadListPrimitive.Root>
669
+ );
670
+ };
671
+
672
+ const ThreadListNew: FC = () => {
673
+ return (
674
+ <ThreadListPrimitive.New asChild>
675
+ <Button
676
+ className="data-[active]:bg-muted hover:bg-muted flex items-center justify-start gap-1 rounded-lg px-2.5 py-2 text-start"
677
+ variant="ghost"
678
+ >
679
+ <PlusIcon />
680
+ New Thread
681
+ </Button>
682
+ </ThreadListPrimitive.New>
683
+ );
684
+ };
685
+
686
+ const ThreadListItems: FC = () => {
687
+ return <ThreadListPrimitive.Items components={{ ThreadListItem }} />;
688
+ };
689
+
690
+ const ThreadListItem: FC = () => {
691
+ return (
692
+ <ThreadListItemPrimitive.Root className="data-[active]:bg-muted hover:bg-muted focus-visible:bg-muted focus-visible:ring-ring flex items-center gap-2 rounded-lg transition-all focus-visible:outline-none focus-visible:ring-2">
693
+ <ThreadListItemPrimitive.Trigger className="flex-grow px-3 py-2 text-start">
694
+ <ThreadListItemTitle />
695
+ </ThreadListItemPrimitive.Trigger>
696
+ <ThreadListItemArchive />
697
+ </ThreadListItemPrimitive.Root>
698
+ );
699
+ };
700
+
701
+ const ThreadListItemTitle: FC = () => {
702
+ return (
703
+ <p className="text-sm">
704
+ <ThreadListItemPrimitive.Title fallback="New Chat" />
705
+ </p>
706
+ );
707
+ };
708
+
709
+ const ThreadListItemArchive: FC = () => {
710
+ return (
711
+ <ThreadListItemPrimitive.Archive asChild>
712
+ <TooltipIconButton
713
+ className="hover:text-primary text-foreground ml-auto mr-3 size-4 p-0"
714
+ variant="ghost"
715
+ tooltip="Archive thread"
716
+ >
717
+ <ArchiveIcon />
718
+ </TooltipIconButton>
719
+ </ThreadListItemPrimitive.Archive>
720
+ );
721
+ };
722
+
723
+ ```
724
+
725
+ ## components/assistant-ui/thread.tsx
726
+
727
+ ```tsx
728
+ import {
729
+ ActionBarPrimitive,
730
+ BranchPickerPrimitive,
731
+ ComposerPrimitive,
732
+ MessagePrimitive,
733
+ ThreadPrimitive,
734
+ } from "@assistant-ui/react";
735
+ import type { FC } from "react";
736
+ import {
737
+ ArrowDownIcon,
738
+ CheckIcon,
739
+ ChevronLeftIcon,
740
+ ChevronRightIcon,
741
+ CopyIcon,
742
+ PencilIcon,
743
+ RefreshCwIcon,
744
+ SendHorizontalIcon,
745
+ } from "lucide-react";
746
+ import { cn } from "@/lib/utils";
747
+
748
+ import { Button } from "@/components/ui/button";
749
+ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
750
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
751
+
752
+ export const Thread: FC = () => {
753
+ return (
754
+ <ThreadPrimitive.Root
755
+ className="bg-background box-border flex h-full flex-col overflow-hidden"
756
+ style={{
757
+ ["--thread-max-width" as string]: "42rem",
758
+ }}
759
+ >
760
+ <ThreadPrimitive.Viewport className="flex h-full flex-col items-center overflow-y-scroll scroll-smooth bg-inherit px-4 pt-8">
761
+ <ThreadWelcome />
762
+
763
+ <ThreadPrimitive.Messages
764
+ components={{
765
+ UserMessage: UserMessage,
766
+ EditComposer: EditComposer,
767
+ AssistantMessage: AssistantMessage,
768
+ }}
769
+ />
770
+
771
+ <ThreadPrimitive.If empty={false}>
772
+ <div className="min-h-8 flex-grow" />
773
+ </ThreadPrimitive.If>
774
+
775
+ <div className="sticky bottom-0 mt-3 flex w-full max-w-[var(--thread-max-width)] flex-col items-center justify-end rounded-t-lg bg-inherit pb-4">
776
+ <ThreadScrollToBottom />
777
+ <Composer />
778
+ </div>
779
+ </ThreadPrimitive.Viewport>
780
+ </ThreadPrimitive.Root>
781
+ );
782
+ };
783
+
784
+ const ThreadScrollToBottom: FC = () => {
785
+ return (
786
+ <ThreadPrimitive.ScrollToBottom asChild>
787
+ <TooltipIconButton
788
+ tooltip="Scroll to bottom"
789
+ variant="outline"
790
+ className="absolute -top-8 rounded-full disabled:invisible"
791
+ >
792
+ <ArrowDownIcon />
793
+ </TooltipIconButton>
794
+ </ThreadPrimitive.ScrollToBottom>
795
+ );
796
+ };
797
+
798
+ const ThreadWelcome: FC = () => {
799
+ return (
800
+ <ThreadPrimitive.Empty>
801
+ <div className="flex w-full max-w-[var(--thread-max-width)] flex-grow flex-col">
802
+ <div className="flex w-full flex-grow flex-col items-center justify-center">
803
+ <p className="mt-4 font-medium">How can I help you today?</p>
804
+ </div>
805
+ <ThreadWelcomeSuggestions />
806
+ </div>
807
+ </ThreadPrimitive.Empty>
808
+ );
809
+ };
810
+
811
+ const ThreadWelcomeSuggestions: FC = () => {
812
+ return (
813
+ <div className="mt-3 flex w-full items-stretch justify-center gap-4">
814
+ <ThreadPrimitive.Suggestion
815
+ className="hover:bg-muted/80 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-lg border p-3 transition-colors ease-in"
816
+ prompt="What is the weather in Tokyo?"
817
+ method="replace"
818
+ autoSend
819
+ >
820
+ <span className="line-clamp-2 text-ellipsis text-sm font-semibold">
821
+ What is the weather in Tokyo?
822
+ </span>
823
+ </ThreadPrimitive.Suggestion>
824
+ <ThreadPrimitive.Suggestion
825
+ className="hover:bg-muted/80 flex max-w-sm grow basis-0 flex-col items-center justify-center rounded-lg border p-3 transition-colors ease-in"
826
+ prompt="What is assistant-ui?"
827
+ method="replace"
828
+ autoSend
829
+ >
830
+ <span className="line-clamp-2 text-ellipsis text-sm font-semibold">
831
+ What is assistant-ui?
832
+ </span>
833
+ </ThreadPrimitive.Suggestion>
834
+ </div>
835
+ );
836
+ };
837
+
838
+ const Composer: FC = () => {
839
+ return (
840
+ <ComposerPrimitive.Root className="focus-within:border-ring/20 flex w-full flex-wrap items-end rounded-lg border bg-inherit px-2.5 shadow-sm transition-colors ease-in">
841
+ <ComposerPrimitive.Input
842
+ rows={1}
843
+ autoFocus
844
+ placeholder="Write a message..."
845
+ className="placeholder:text-muted-foreground max-h-40 flex-grow resize-none border-none bg-transparent px-2 py-4 text-sm outline-none focus:ring-0 disabled:cursor-not-allowed"
846
+ />
847
+ <ComposerAction />
848
+ </ComposerPrimitive.Root>
849
+ );
850
+ };
851
+
852
+ const ComposerAction: FC = () => {
853
+ return (
854
+ <>
855
+ <ThreadPrimitive.If running={false}>
856
+ <ComposerPrimitive.Send asChild>
857
+ <TooltipIconButton
858
+ tooltip="Send"
859
+ variant="default"
860
+ className="my-2.5 size-8 p-2 transition-opacity ease-in"
861
+ >
862
+ <SendHorizontalIcon />
863
+ </TooltipIconButton>
864
+ </ComposerPrimitive.Send>
865
+ </ThreadPrimitive.If>
866
+ <ThreadPrimitive.If running>
867
+ <ComposerPrimitive.Cancel asChild>
868
+ <TooltipIconButton
869
+ tooltip="Cancel"
870
+ variant="default"
871
+ className="my-2.5 size-8 p-2 transition-opacity ease-in"
872
+ >
873
+ <CircleStopIcon />
874
+ </TooltipIconButton>
875
+ </ComposerPrimitive.Cancel>
876
+ </ThreadPrimitive.If>
877
+ </>
878
+ );
879
+ };
880
+
881
+ const UserMessage: FC = () => {
882
+ return (
883
+ <MessagePrimitive.Root className="grid w-full max-w-[var(--thread-max-width)] auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] gap-y-2 py-4 [&:where(>*)]:col-start-2">
884
+ <UserActionBar />
885
+
886
+ <div className="bg-muted text-foreground col-start-2 row-start-2 max-w-[calc(var(--thread-max-width)*0.8)] break-words rounded-3xl px-5 py-2.5">
887
+ <MessagePrimitive.Content />
888
+ </div>
889
+
890
+ <BranchPicker className="col-span-full col-start-1 row-start-3 -mr-1 justify-end" />
891
+ </MessagePrimitive.Root>
892
+ );
893
+ };
894
+
895
+ const UserActionBar: FC = () => {
896
+ return (
897
+ <ActionBarPrimitive.Root
898
+ hideWhenRunning
899
+ autohide="not-last"
900
+ className="col-start-1 row-start-2 mr-3 mt-2.5 flex flex-col items-end"
901
+ >
902
+ <ActionBarPrimitive.Edit asChild>
903
+ <TooltipIconButton tooltip="Edit">
904
+ <PencilIcon />
905
+ </TooltipIconButton>
906
+ </ActionBarPrimitive.Edit>
907
+ </ActionBarPrimitive.Root>
908
+ );
909
+ };
910
+
911
+ const EditComposer: FC = () => {
912
+ return (
913
+ <ComposerPrimitive.Root className="bg-muted my-4 flex w-full max-w-[var(--thread-max-width)] flex-col gap-2 rounded-xl">
914
+ <ComposerPrimitive.Input className="text-foreground flex h-8 w-full resize-none bg-transparent p-4 pb-0 outline-none" />
915
+
916
+ <div className="mx-3 mb-3 flex items-center justify-center gap-2 self-end">
917
+ <ComposerPrimitive.Cancel asChild>
918
+ <Button variant="ghost">Cancel</Button>
919
+ </ComposerPrimitive.Cancel>
920
+ <ComposerPrimitive.Send asChild>
921
+ <Button>Send</Button>
922
+ </ComposerPrimitive.Send>
923
+ </div>
924
+ </ComposerPrimitive.Root>
925
+ );
926
+ };
927
+
928
+ const AssistantMessage: FC = () => {
929
+ return (
930
+ <MessagePrimitive.Root className="relative grid w-full max-w-[var(--thread-max-width)] grid-cols-[auto_auto_1fr] grid-rows-[auto_1fr] py-4">
931
+ <div className="text-foreground col-span-2 col-start-2 row-start-1 my-1.5 max-w-[calc(var(--thread-max-width)*0.8)] break-words leading-7">
932
+ <MessagePrimitive.Content components={{ Text: MarkdownText }} />
933
+ </div>
934
+
935
+ <AssistantActionBar />
936
+
937
+ <BranchPicker className="col-start-2 row-start-2 -ml-2 mr-2" />
938
+ </MessagePrimitive.Root>
939
+ );
940
+ };
941
+
942
+ const AssistantActionBar: FC = () => {
943
+ return (
944
+ <ActionBarPrimitive.Root
945
+ hideWhenRunning
946
+ autohide="not-last"
947
+ autohideFloat="single-branch"
948
+ className="text-muted-foreground data-[floating]:bg-background col-start-3 row-start-2 -ml-1 flex gap-1 data-[floating]:absolute data-[floating]:rounded-md data-[floating]:border data-[floating]:p-1 data-[floating]:shadow-sm"
949
+ >
950
+ <ActionBarPrimitive.Copy asChild>
951
+ <TooltipIconButton tooltip="Copy">
952
+ <MessagePrimitive.If copied>
953
+ <CheckIcon />
954
+ </MessagePrimitive.If>
955
+ <MessagePrimitive.If copied={false}>
956
+ <CopyIcon />
957
+ </MessagePrimitive.If>
958
+ </TooltipIconButton>
959
+ </ActionBarPrimitive.Copy>
960
+ <ActionBarPrimitive.Reload asChild>
961
+ <TooltipIconButton tooltip="Refresh">
962
+ <RefreshCwIcon />
963
+ </TooltipIconButton>
964
+ </ActionBarPrimitive.Reload>
965
+ </ActionBarPrimitive.Root>
966
+ );
967
+ };
968
+
969
+ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
970
+ className,
971
+ ...rest
972
+ }) => {
973
+ return (
974
+ <BranchPickerPrimitive.Root
975
+ hideWhenSingleBranch
976
+ className={cn(
977
+ "text-muted-foreground inline-flex items-center text-xs",
978
+ className,
979
+ )}
980
+ {...rest}
981
+ >
982
+ <BranchPickerPrimitive.Previous asChild>
983
+ <TooltipIconButton tooltip="Previous">
984
+ <ChevronLeftIcon />
985
+ </TooltipIconButton>
986
+ </BranchPickerPrimitive.Previous>
987
+ <span className="font-medium">
988
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
989
+ </span>
990
+ <BranchPickerPrimitive.Next asChild>
991
+ <TooltipIconButton tooltip="Next">
992
+ <ChevronRightIcon />
993
+ </TooltipIconButton>
994
+ </BranchPickerPrimitive.Next>
995
+ </BranchPickerPrimitive.Root>
996
+ );
997
+ };
998
+
999
+ const CircleStopIcon = () => {
1000
+ return (
1001
+ <svg
1002
+ xmlns="http://www.w3.org/2000/svg"
1003
+ viewBox="0 0 16 16"
1004
+ fill="currentColor"
1005
+ width="16"
1006
+ height="16"
1007
+ >
1008
+ <rect width="10" height="10" x="3" y="3" rx="2" />
1009
+ </svg>
1010
+ );
1011
+ };
1012
+
1013
+ ```
1014
+
1015
+ ## components/assistant-ui/tooltip-icon-button.tsx
1016
+
1017
+ ```tsx
1018
+ "use client";
1019
+
1020
+ import { ComponentPropsWithoutRef, forwardRef } from "react";
1021
+
1022
+ import {
1023
+ Tooltip,
1024
+ TooltipContent,
1025
+ TooltipProvider,
1026
+ TooltipTrigger,
1027
+ } from "@/components/ui/tooltip";
1028
+ import { Button } from "@/components/ui/button";
1029
+ import { cn } from "@/lib/utils";
1030
+
1031
+ export type TooltipIconButtonProps = ComponentPropsWithoutRef<typeof Button> & {
1032
+ tooltip: string;
1033
+ side?: "top" | "bottom" | "left" | "right";
1034
+ };
1035
+
1036
+ export const TooltipIconButton = forwardRef<
1037
+ HTMLButtonElement,
1038
+ TooltipIconButtonProps
1039
+ >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
1040
+ return (
1041
+ <TooltipProvider>
1042
+ <Tooltip>
1043
+ <TooltipTrigger asChild>
1044
+ <Button
1045
+ variant="ghost"
1046
+ size="icon"
1047
+ {...rest}
1048
+ className={cn("size-6 p-1", className)}
1049
+ ref={ref}
1050
+ >
1051
+ {children}
1052
+ <span className="sr-only">{tooltip}</span>
1053
+ </Button>
1054
+ </TooltipTrigger>
1055
+ <TooltipContent side={side}>{tooltip}</TooltipContent>
1056
+ </Tooltip>
1057
+ </TooltipProvider>
1058
+ );
1059
+ });
1060
+
1061
+ TooltipIconButton.displayName = "TooltipIconButton";
1062
+
1063
+ ```
1064
+
1065
+ ## components/tools/price-snapshot/price-snapshot.tsx
1066
+
1067
+ ```tsx
1068
+ "use client";
1069
+
1070
+ import { ArrowDownIcon, ArrowUpIcon } from "lucide-react";
1071
+
1072
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
1073
+
1074
+ type PriceSnapshotToolArgs = {
1075
+ ticker: string;
1076
+ };
1077
+
1078
+ type PriceSnapshotToolResult = {
1079
+ price: number;
1080
+ day_change: number;
1081
+ day_change_percent: number;
1082
+ time: string;
1083
+ };
1084
+
1085
+ export function PriceSnapshot({
1086
+ ticker,
1087
+ price,
1088
+ day_change,
1089
+ day_change_percent,
1090
+ time,
1091
+ }: PriceSnapshotToolArgs & PriceSnapshotToolResult) {
1092
+ const isPositiveChange = day_change >= 0;
1093
+ const changeColor = isPositiveChange ? "text-green-600" : "text-red-600";
1094
+ const ArrowIcon = isPositiveChange ? ArrowUpIcon : ArrowDownIcon;
1095
+
1096
+ return (
1097
+ <Card className="mx-auto w-full max-w-md">
1098
+ <CardHeader>
1099
+ <CardTitle className="text-2xl font-bold">{ticker}</CardTitle>
1100
+ </CardHeader>
1101
+ <CardContent>
1102
+ <div className="grid grid-cols-2 gap-4">
1103
+ <div className="col-span-2">
1104
+ <p className="text-3xl font-semibold">${price?.toFixed(2)}</p>
1105
+ </div>
1106
+ <div>
1107
+ <p className="text-muted-foreground text-sm">Day Change</p>
1108
+ <p
1109
+ className={`flex items-center text-lg font-medium ${changeColor}`}
1110
+ >
1111
+ <ArrowIcon className="mr-1 h-4 w-4" />$
1112
+ {Math.abs(day_change)?.toFixed(2)} (
1113
+ {Math.abs(day_change_percent)?.toFixed(2)}%)
1114
+ </p>
1115
+ </div>
1116
+ <div>
1117
+ <p className="text-muted-foreground text-sm">Last Updated</p>
1118
+ <p className="text-lg font-medium">
1119
+ {new Date(time).toLocaleTimeString()}
1120
+ </p>
1121
+ </div>
1122
+ </div>
1123
+ </CardContent>
1124
+ </Card>
1125
+ );
1126
+ }
1127
+
1128
+ ```
1129
+
1130
+ ## components/tools/price-snapshot/PriceSnapshotTool.tsx
1131
+
1132
+ ```tsx
1133
+ "use client";
1134
+
1135
+ import { PriceSnapshot } from "./price-snapshot";
1136
+ import { makeAssistantToolUI } from "@assistant-ui/react";
1137
+
1138
+ type PriceSnapshotToolArgs = {
1139
+ ticker: string;
1140
+ };
1141
+ type PriceSnapshotToolResult = {
1142
+ snapshot: {
1143
+ price: number;
1144
+ day_change: number;
1145
+ day_change_percent: number;
1146
+ time: string;
1147
+ };
1148
+ };
1149
+ export const PriceSnapshotTool = makeAssistantToolUI<
1150
+ PriceSnapshotToolArgs,
1151
+ string
1152
+ >({
1153
+ toolName: "price_snapshot",
1154
+ render: function PriceSnapshotUI({ args, argsText, result }) {
1155
+ const resultObj = result
1156
+ ? (JSON.parse(result) as PriceSnapshotToolResult)
1157
+ : undefined;
1158
+
1159
+ return (
1160
+ <div className="mb-4 flex flex-col items-center gap-2">
1161
+ <pre className="whitespace-pre-wrap">price_snapshot({argsText})</pre>
1162
+ {resultObj && (
1163
+ <PriceSnapshot ticker={args.ticker} {...resultObj.snapshot} />
1164
+ )}
1165
+ </div>
1166
+ );
1167
+ },
1168
+ });
1169
+
1170
+ ```
1171
+
1172
+ ## components/tools/purchase-stock/PurchaseStockTool.tsx
1173
+
1174
+ ```tsx
1175
+ "use client";
1176
+
1177
+ import { TransactionConfirmationPending } from "./transaction-confirmation-pending";
1178
+ import { TransactionConfirmationFinal } from "./transaction-confirmation-final";
1179
+ import { makeAssistantToolUI } from "@assistant-ui/react";
1180
+
1181
+ type PurchaseStockArgs = {
1182
+ ticker: string;
1183
+ companyName: string;
1184
+ quantity: number;
1185
+ maxPurchasePrice: number;
1186
+ };
1187
+
1188
+ export const PurchaseStockTool = makeAssistantToolUI<PurchaseStockArgs, string>(
1189
+ {
1190
+ toolName: "purchase_stock",
1191
+ render: function PurchaseStockUI({
1192
+ args,
1193
+ argsText,
1194
+ result,
1195
+ status,
1196
+ addResult,
1197
+ }) {
1198
+ const resultObj = result
1199
+ ? (JSON.parse(result) as { transactionId: string })
1200
+ : undefined;
1201
+
1202
+ const handleConfirm = async () => {
1203
+ addResult(JSON.stringify({ confirmed: true }));
1204
+ };
1205
+
1206
+ return (
1207
+ <div className="mb-4 flex flex-col items-center gap-2">
1208
+ <pre className="whitespace-pre-wrap">purchase_stock({argsText})</pre>
1209
+ {!resultObj && status.type !== "running" && (
1210
+ <TransactionConfirmationPending
1211
+ {...args}
1212
+ onConfirm={handleConfirm}
1213
+ />
1214
+ )}
1215
+ {resultObj && <TransactionConfirmationFinal {...args} />}
1216
+ </div>
1217
+ );
1218
+ },
1219
+ },
1220
+ );
1221
+
1222
+ ```
1223
+
1224
+ ## components/tools/purchase-stock/transaction-confirmation-final.tsx
1225
+
1226
+ ```tsx
1227
+ "use client";
1228
+
1229
+ import { CheckCircle } from "lucide-react";
1230
+
1231
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
1232
+
1233
+ type TransactionConfirmation = {
1234
+ ticker: string;
1235
+ companyName: string;
1236
+ quantity: number;
1237
+ maxPurchasePrice: number;
1238
+ };
1239
+
1240
+ export function TransactionConfirmationFinal(props: TransactionConfirmation) {
1241
+ const { ticker, companyName, quantity, maxPurchasePrice } = props;
1242
+
1243
+ return (
1244
+ <Card className="mx-auto w-full max-w-md">
1245
+ <CardHeader className="text-center">
1246
+ <CheckCircle className="mx-auto mb-4 h-16 w-16 text-green-500" />
1247
+ <CardTitle className="text-2xl font-bold text-green-700">
1248
+ Transaction Confirmed
1249
+ </CardTitle>
1250
+ </CardHeader>
1251
+ <CardContent className="space-y-4">
1252
+ <div className="rounded-md border border-green-200 bg-green-50 p-4">
1253
+ <h3 className="mb-2 text-lg font-semibold text-green-800">
1254
+ Purchase Summary
1255
+ </h3>
1256
+ <div className="grid grid-cols-2 gap-2 text-sm">
1257
+ <p className="font-medium text-green-700">Ticker:</p>
1258
+ <p className="font-bold text-green-900">{ticker}</p>
1259
+ <p className="font-medium text-green-700">Company:</p>
1260
+ <p className="text-green-900">{companyName}</p>
1261
+ <p className="font-medium text-green-700">Quantity:</p>
1262
+ <p className="text-green-900">{quantity} shares</p>
1263
+ <p className="font-medium text-green-700">Price per Share:</p>
1264
+ <p className="text-green-900">${maxPurchasePrice?.toFixed(2)}</p>
1265
+ </div>
1266
+ </div>
1267
+ <div className="rounded-md border border-green-300 bg-green-100 p-4">
1268
+ <p className="text-lg font-semibold text-green-800">Total Cost:</p>
1269
+ <p className="text-2xl font-bold text-green-900">
1270
+ ${(quantity * maxPurchasePrice)?.toFixed(2)}
1271
+ </p>
1272
+ </div>
1273
+ <p className="text-center text-sm text-green-600">
1274
+ Your purchase of {quantity} shares of {companyName} ({ticker}) has
1275
+ been successfully processed.
1276
+ </p>
1277
+ </CardContent>
1278
+ </Card>
1279
+ );
1280
+ }
1281
+
1282
+ ```
1283
+
1284
+ ## components/tools/purchase-stock/transaction-confirmation-pending.tsx
1285
+
1286
+ ```tsx
1287
+ "use client";
1288
+
1289
+ import { CheckIcon } from "lucide-react";
1290
+
1291
+ import { Button } from "@/components/ui/button";
1292
+ import {
1293
+ Card,
1294
+ CardContent,
1295
+ CardFooter,
1296
+ CardHeader,
1297
+ CardTitle,
1298
+ } from "@/components/ui/card";
1299
+
1300
+ type TransactionConfirmation = {
1301
+ ticker: string;
1302
+ companyName: string;
1303
+ quantity: number;
1304
+ maxPurchasePrice: number;
1305
+ onConfirm: () => void;
1306
+ };
1307
+
1308
+ export function TransactionConfirmationPending(props: TransactionConfirmation) {
1309
+ const { ticker, companyName, quantity, maxPurchasePrice, onConfirm } = props;
1310
+
1311
+ return (
1312
+ <Card className="mx-auto w-full max-w-md">
1313
+ <CardHeader>
1314
+ <CardTitle className="text-2xl font-bold">
1315
+ Confirm Transaction
1316
+ </CardTitle>
1317
+ </CardHeader>
1318
+ <CardContent className="space-y-4">
1319
+ <div className="grid grid-cols-2 gap-2">
1320
+ <p className="text-muted-foreground text-sm font-medium">Ticker:</p>
1321
+ <p className="text-sm font-bold">{ticker}</p>
1322
+ <p className="text-muted-foreground text-sm font-medium">Company:</p>
1323
+ <p className="text-sm">{companyName}</p>
1324
+ <p className="text-muted-foreground text-sm font-medium">Quantity:</p>
1325
+ <p className="text-sm">{quantity} shares</p>
1326
+ <p className="text-muted-foreground text-sm font-medium">
1327
+ Max Purchase Price:
1328
+ </p>
1329
+ <p className="text-sm">${maxPurchasePrice?.toFixed(2)}</p>
1330
+ </div>
1331
+ <div className="bg-muted rounded-md p-3">
1332
+ <p className="text-sm font-medium">Total Maximum Cost:</p>
1333
+ <p className="text-lg font-bold">
1334
+ ${(quantity * maxPurchasePrice)?.toFixed(2)}
1335
+ </p>
1336
+ </div>
1337
+ </CardContent>
1338
+ <CardFooter className="flex justify-end">
1339
+ {/* <Button variant="outline" onClick={onReject}>
1340
+ <X className="mr-2 h-4 w-4" />
1341
+ Reject
1342
+ </Button> */}
1343
+ <Button onClick={onConfirm}>
1344
+ <CheckIcon className="mr-2 h-4 w-4" />
1345
+ Confirm
1346
+ </Button>
1347
+ </CardFooter>
1348
+ </Card>
1349
+ );
1350
+ }
1351
+
1352
+ ```
1353
+
1354
+ ## components/tools/ToolFallback.tsx
1355
+
1356
+ ```tsx
1357
+ import { ToolCallContentPartComponent } from "@assistant-ui/react";
1358
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
1359
+ import { useState } from "react";
1360
+ import { Button } from "../ui/button";
1361
+
1362
+ export const ToolFallback: ToolCallContentPartComponent = ({
1363
+ toolName,
1364
+ argsText,
1365
+ result,
1366
+ }) => {
1367
+ const [isCollapsed, setIsCollapsed] = useState(true);
1368
+ return (
1369
+ <div className="mb-4 flex w-full flex-col gap-3 rounded-lg border py-3">
1370
+ <div className="flex items-center gap-2 px-4">
1371
+ <CheckIcon className="size-4" />
1372
+ <p className="">
1373
+ Used tool: <b>{toolName}</b>
1374
+ </p>
1375
+ <div className="flex-grow" />
1376
+ <Button onClick={() => setIsCollapsed(!isCollapsed)}>
1377
+ {isCollapsed ? <ChevronUpIcon /> : <ChevronDownIcon />}
1378
+ </Button>
1379
+ </div>
1380
+ {!isCollapsed && (
1381
+ <div className="flex flex-col gap-2 border-t pt-2">
1382
+ <div className="px-4">
1383
+ <pre className="whitespace-pre-wrap">{argsText}</pre>
1384
+ </div>
1385
+ {result !== undefined && (
1386
+ <div className="border-t border-dashed px-4 pt-2">
1387
+ <p className="font-semibold">Result:</p>
1388
+ <pre className="whitespace-pre-wrap">
1389
+ {typeof result === "string"
1390
+ ? result
1391
+ : JSON.stringify(result, null, 2)}
1392
+ </pre>
1393
+ </div>
1394
+ )}
1395
+ </div>
1396
+ )}
1397
+ </div>
1398
+ );
1399
+ };
1400
+
1401
+ ```
1402
+
1403
+ ## components/ui/button.tsx
1404
+
1405
+ ```tsx
1406
+ import * as React from "react";
1407
+ import { Slot } from "@radix-ui/react-slot";
1408
+ import { cva, type VariantProps } from "class-variance-authority";
1409
+
1410
+ import { cn } from "@/lib/utils";
1411
+
1412
+ const buttonVariants = cva(
1413
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium 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",
1414
+ {
1415
+ variants: {
1416
+ variant: {
1417
+ default:
1418
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
1419
+ destructive:
1420
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
1421
+ outline:
1422
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
1423
+ secondary:
1424
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
1425
+ ghost: "hover:bg-accent hover:text-accent-foreground",
1426
+ link: "text-primary underline-offset-4 hover:underline",
1427
+ },
1428
+ size: {
1429
+ default: "h-9 px-4 py-2",
1430
+ sm: "h-8 rounded-md px-3 text-xs",
1431
+ lg: "h-10 rounded-md px-8",
1432
+ icon: "h-9 w-9",
1433
+ },
1434
+ },
1435
+ defaultVariants: {
1436
+ variant: "default",
1437
+ size: "default",
1438
+ },
1439
+ },
1440
+ );
1441
+
1442
+ export interface ButtonProps
1443
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
1444
+ VariantProps<typeof buttonVariants> {
1445
+ asChild?: boolean;
1446
+ }
1447
+
1448
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
1449
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
1450
+ const Comp = asChild ? Slot : "button";
1451
+ return (
1452
+ <Comp
1453
+ className={cn(buttonVariants({ variant, size, className }))}
1454
+ ref={ref}
1455
+ {...props}
1456
+ />
1457
+ );
1458
+ },
1459
+ );
1460
+ Button.displayName = "Button";
1461
+
1462
+ export { Button, buttonVariants };
1463
+
1464
+ ```
1465
+
1466
+ ## components/ui/card.tsx
1467
+
1468
+ ```tsx
1469
+ import * as React from "react";
1470
+
1471
+ import { cn } from "@/lib/utils";
1472
+
1473
+ const Card = React.forwardRef<
1474
+ HTMLDivElement,
1475
+ React.HTMLAttributes<HTMLDivElement>
1476
+ >(({ className, ...props }, ref) => (
1477
+ <div
1478
+ ref={ref}
1479
+ className={cn(
1480
+ "bg-card text-card-foreground rounded-xl border shadow",
1481
+ className,
1482
+ )}
1483
+ {...props}
1484
+ />
1485
+ ));
1486
+ Card.displayName = "Card";
1487
+
1488
+ const CardHeader = React.forwardRef<
1489
+ HTMLDivElement,
1490
+ React.HTMLAttributes<HTMLDivElement>
1491
+ >(({ className, ...props }, ref) => (
1492
+ <div
1493
+ ref={ref}
1494
+ className={cn("flex flex-col space-y-1.5 p-6", className)}
1495
+ {...props}
1496
+ />
1497
+ ));
1498
+ CardHeader.displayName = "CardHeader";
1499
+
1500
+ const CardTitle = React.forwardRef<
1501
+ HTMLDivElement,
1502
+ React.HTMLAttributes<HTMLDivElement>
1503
+ >(({ className, ...props }, ref) => (
1504
+ <div
1505
+ ref={ref}
1506
+ className={cn("font-semibold leading-none tracking-tight", className)}
1507
+ {...props}
1508
+ />
1509
+ ));
1510
+ CardTitle.displayName = "CardTitle";
1511
+
1512
+ const CardDescription = React.forwardRef<
1513
+ HTMLDivElement,
1514
+ React.HTMLAttributes<HTMLDivElement>
1515
+ >(({ className, ...props }, ref) => (
1516
+ <div
1517
+ ref={ref}
1518
+ className={cn("text-muted-foreground text-sm", className)}
1519
+ {...props}
1520
+ />
1521
+ ));
1522
+ CardDescription.displayName = "CardDescription";
1523
+
1524
+ const CardContent = React.forwardRef<
1525
+ HTMLDivElement,
1526
+ React.HTMLAttributes<HTMLDivElement>
1527
+ >(({ className, ...props }, ref) => (
1528
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
1529
+ ));
1530
+ CardContent.displayName = "CardContent";
1531
+
1532
+ const CardFooter = React.forwardRef<
1533
+ HTMLDivElement,
1534
+ React.HTMLAttributes<HTMLDivElement>
1535
+ >(({ className, ...props }, ref) => (
1536
+ <div
1537
+ ref={ref}
1538
+ className={cn("flex items-center p-6 pt-0", className)}
1539
+ {...props}
1540
+ />
1541
+ ));
1542
+ CardFooter.displayName = "CardFooter";
1543
+
1544
+ export {
1545
+ Card,
1546
+ CardHeader,
1547
+ CardFooter,
1548
+ CardTitle,
1549
+ CardDescription,
1550
+ CardContent,
1551
+ };
1552
+
1553
+ ```
1554
+
1555
+ ## components/ui/tooltip.tsx
1556
+
1557
+ ```tsx
1558
+ "use client";
1559
+
1560
+ import * as React from "react";
1561
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
1562
+
1563
+ import { cn } from "@/lib/utils";
1564
+
1565
+ const TooltipProvider = TooltipPrimitive.Provider;
1566
+
1567
+ const Tooltip = TooltipPrimitive.Root;
1568
+
1569
+ const TooltipTrigger = TooltipPrimitive.Trigger;
1570
+
1571
+ const TooltipContent = React.forwardRef<
1572
+ React.ElementRef<typeof TooltipPrimitive.Content>,
1573
+ React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
1574
+ >(({ className, sideOffset = 4, ...props }, ref) => (
1575
+ <TooltipPrimitive.Portal>
1576
+ <TooltipPrimitive.Content
1577
+ ref={ref}
1578
+ sideOffset={sideOffset}
1579
+ className={cn(
1580
+ "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out 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 origin-[--radix-tooltip-content-transform-origin] overflow-hidden rounded-md px-3 py-1.5 text-xs",
1581
+ className,
1582
+ )}
1583
+ {...props}
1584
+ />
1585
+ </TooltipPrimitive.Portal>
1586
+ ));
1587
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
1588
+
1589
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
1590
+
1591
+ ```
1592
+
1593
+ ## eslint.config.ts
1594
+
1595
+ ```typescript
1596
+ export { default } from "@assistant-ui/x-buildutils/eslint";
1597
+
1598
+ ```
1599
+
1600
+ ## lib/chatApi.ts
1601
+
1602
+ ```typescript
1603
+ import { ThreadState, Client } from "@langchain/langgraph-sdk";
1604
+ import {
1605
+ LangChainMessage,
1606
+ LangGraphMessagesEvent,
1607
+ } from "@assistant-ui/react-langgraph";
1608
+
1609
+ const createClient = () => {
1610
+ const apiUrl =
1611
+ process.env["NEXT_PUBLIC_LANGGRAPH_API_URL"] ||
1612
+ new URL("/api", window.location.href).href;
1613
+ return new Client({
1614
+ apiUrl,
1615
+ });
1616
+ };
1617
+
1618
+ export const createAssistant = async (graphId: string) => {
1619
+ const client = createClient();
1620
+ return client.assistants.create({ graphId });
1621
+ };
1622
+
1623
+ export const createThread = async () => {
1624
+ const client = createClient();
1625
+ return client.threads.create();
1626
+ };
1627
+
1628
+ export const getThreadState = async (
1629
+ threadId: string,
1630
+ ): Promise<ThreadState<Record<string, unknown>>> => {
1631
+ const client = createClient();
1632
+ return client.threads.getState(threadId);
1633
+ };
1634
+
1635
+ export const updateState = async (
1636
+ threadId: string,
1637
+ fields: {
1638
+ newState: Record<string, unknown>;
1639
+ asNode?: string;
1640
+ },
1641
+ ) => {
1642
+ const client = createClient();
1643
+ return client.threads.updateState(threadId, {
1644
+ values: fields.newState,
1645
+ asNode: fields.asNode!,
1646
+ });
1647
+ };
1648
+
1649
+ export const sendMessage = (params: {
1650
+ threadId: string;
1651
+ messages: LangChainMessage[];
1652
+ }): AsyncGenerator<LangGraphMessagesEvent<LangChainMessage>> => {
1653
+ const client = createClient();
1654
+
1655
+ const input: Record<string, unknown> | null = {
1656
+ messages: params.messages,
1657
+ };
1658
+ const config = {
1659
+ configurable: {
1660
+ model_name: "openai",
1661
+ },
1662
+ };
1663
+
1664
+ return client.runs.stream(
1665
+ params.threadId,
1666
+ process.env["NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID"]!,
1667
+ {
1668
+ input,
1669
+ config,
1670
+ streamMode: "messages",
1671
+ },
1672
+ ) as AsyncGenerator<LangGraphMessagesEvent<LangChainMessage>>;
1673
+ };
1674
+
1675
+ ```
1676
+
1677
+ ## lib/utils.ts
1678
+
1679
+ ```typescript
1680
+ import { type ClassValue, clsx } from "clsx";
1681
+ import { twMerge } from "tailwind-merge";
1682
+
1683
+ export function cn(...inputs: ClassValue[]) {
1684
+ return twMerge(clsx(inputs));
1685
+ }
1686
+
1687
+ ```
1688
+
1689
+ ## next-env.d.ts
1690
+
1691
+ ```typescript
1692
+ /// <reference types="next" />
1693
+ /// <reference types="next/image-types/global" />
1694
+
1695
+ // NOTE: This file should not be edited
1696
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
1697
+
1698
+ ```
1699
+
1700
+ ## next.config.ts
1701
+
1702
+ ```typescript
1703
+ import type { NextConfig } from "next";
1704
+
1705
+ const nextConfig: NextConfig = {
1706
+ /* config options here */
1707
+ };
1708
+
1709
+ export default nextConfig;
1710
+
1711
+ ```
1712
+
1713
+ ## package.json
1714
+
1715
+ ```json
1716
+ {
1717
+ "name": "with-langgraph",
1718
+ "version": "0.1.0",
1719
+ "private": true,
1720
+ "scripts": {
1721
+ "dev": "next dev --turbo",
1722
+ "build": "next build",
1723
+ "start": "next start",
1724
+ "lint": "next lint"
1725
+ },
1726
+ "dependencies": {
1727
+ "@assistant-ui/react": "workspace:*",
1728
+ "@assistant-ui/react-langgraph": "workspace:*",
1729
+ "@assistant-ui/react-markdown": "workspace:*",
1730
+ "@langchain/langgraph-sdk": "^0.0.81",
1731
+ "@radix-ui/react-slot": "^1.2.3",
1732
+ "@radix-ui/react-tooltip": "^1.2.7",
1733
+ "class-variance-authority": "^0.7.1",
1734
+ "clsx": "^2.1.1",
1735
+ "js-cookie": "^3.0.5",
1736
+ "jsonwebtoken": "^9.0.2",
1737
+ "lucide-react": "^0.511.0",
1738
+ "nanoid": "5.1.5",
1739
+ "next": "15.3.3",
1740
+ "react": "19.1.0",
1741
+ "react-dom": "19.1.0",
1742
+ "remark-gfm": "^4.0.1",
1743
+ "tailwind-merge": "^3.3.0",
1744
+ "tw-animate-css": "^1.3.3"
1745
+ },
1746
+ "devDependencies": {
1747
+ "@assistant-ui/x-buildutils": "workspace:*",
1748
+ "@types/js-cookie": "^3.0.6",
1749
+ "@types/jsonwebtoken": "^9.0.9",
1750
+ "@types/node": "^22",
1751
+ "@types/react": "^19",
1752
+ "@types/react-dom": "^19",
1753
+ "eslint": "^9",
1754
+ "eslint-config-next": "15.3.3",
1755
+ "postcss": "^8",
1756
+ "tailwindcss": "^4.1.8",
1757
+ "typescript": "^5.8.3"
1758
+ }
1759
+ }
1760
+
1761
+ ```
1762
+
1763
+ ## README.md
1764
+
1765
+ ```markdown
1766
+ # LangGraph Example
1767
+
1768
+ [Hosted Demo](https://assistant-ui-langgraph.vercel.app/)
1769
+
1770
+ This example demonstrates how to use LangChain LangGraph with assistant-ui.
1771
+
1772
+ It is meant to be used with the backend found at LangGraph's Stockbroker example: https://github.com/bracesproul/langgraphjs-examples/tree/main/stockbroker
1773
+
1774
+ You need to set the following environment variables:
1775
+
1776
+ ```env
1777
+ NEXT_PUBLIC_API_URL=https://stockbrokeragent-bracesprouls-projects.vercel.app/api
1778
+ NEXT_PUBLIC_LANGGRAPH_ASSISTANT_ID=stockbroker
1779
+ ```
1780
+
1781
+ To run the example, run the following commands:
1782
+
1783
+ ```sh
1784
+ npm install
1785
+ npm run dev
1786
+ ```
1787
+ ```
1788
+
1789
+ ## tsconfig.json
1790
+
1791
+ ```json
1792
+ {
1793
+ "extends": "@assistant-ui/x-buildutils/ts/base",
1794
+ "compilerOptions": {
1795
+ "target": "ES6",
1796
+ "module": "ESNext",
1797
+ "incremental": true,
1798
+ "plugins": [
1799
+ {
1800
+ "name": "next"
1801
+ }
1802
+ ],
1803
+ "allowJs": true,
1804
+ "strictNullChecks": true,
1805
+ "jsx": "preserve",
1806
+ "paths": {
1807
+ "@/*": ["./*"],
1808
+ "@assistant-ui/*": ["../../packages/*/src"],
1809
+ "@assistant-ui/react/*": ["../../packages/react/src/*"],
1810
+ "assistant-stream": ["../../packages/assistant-stream/src"],
1811
+ "assistant-stream/*": ["../../packages/assistant-stream/src/*"]
1812
+ }
1813
+ },
1814
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
1815
+ "exclude": ["node_modules"]
1816
+ }
1817
+
1818
+ ```
1819
+