@assistant-ui/mcp-docs-server 0.1.3 → 0.1.5

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 (48) hide show
  1. package/.docs/organized/code-examples/local-ollama.md +13 -13
  2. package/.docs/organized/code-examples/search-agent-for-e-commerce.md +18 -18
  3. package/.docs/organized/code-examples/{with-vercel-ai-rsc.md → with-ai-sdk-v5.md} +225 -230
  4. package/.docs/organized/code-examples/with-ai-sdk.md +13 -13
  5. package/.docs/organized/code-examples/with-cloud.md +12 -12
  6. package/.docs/organized/code-examples/with-external-store.md +9 -9
  7. package/.docs/organized/code-examples/with-ffmpeg.md +19 -19
  8. package/.docs/organized/code-examples/with-langgraph.md +14 -14
  9. package/.docs/organized/code-examples/with-openai-assistants.md +12 -12
  10. package/.docs/organized/code-examples/with-parent-id-grouping.md +1374 -0
  11. package/.docs/organized/code-examples/with-react-hook-form.md +18 -18
  12. package/.docs/raw/docs/about-assistantui.mdx +9 -0
  13. package/.docs/raw/docs/api-reference/context-providers/{TextContentPartProvider.mdx → TextMessagePartProvider.mdx} +3 -3
  14. package/.docs/raw/docs/api-reference/integrations/react-hook-form.mdx +2 -2
  15. package/.docs/raw/docs/api-reference/overview.mdx +23 -23
  16. package/.docs/raw/docs/api-reference/primitives/Error.mdx +5 -3
  17. package/.docs/raw/docs/api-reference/primitives/Message.mdx +32 -0
  18. package/.docs/raw/docs/api-reference/primitives/{ContentPart.mdx → MessagePart.mdx} +41 -41
  19. package/.docs/raw/docs/api-reference/runtimes/MessagePartRuntime.mdx +22 -0
  20. package/.docs/raw/docs/api-reference/runtimes/ThreadListRuntime.mdx +1 -0
  21. package/.docs/raw/docs/api-reference/runtimes/ThreadRuntime.mdx +1 -0
  22. package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +89 -32
  23. package/.docs/raw/docs/cloud/persistence/langgraph.mdx +187 -32
  24. package/.docs/raw/docs/concepts/runtime-layer.mdx +7 -7
  25. package/.docs/raw/docs/copilots/make-assistant-tool-ui.mdx +22 -13
  26. package/.docs/raw/docs/copilots/make-assistant-tool.mdx +20 -14
  27. package/.docs/raw/docs/getting-started.mdx +11 -10
  28. package/.docs/raw/docs/guides/Attachments.mdx +24 -21
  29. package/.docs/raw/docs/guides/Latex.mdx +81 -0
  30. package/.docs/raw/docs/guides/ToolUI.mdx +13 -8
  31. package/.docs/raw/docs/migrations/v0-11.mdx +169 -0
  32. package/.docs/raw/docs/migrations/v0-7.mdx +8 -8
  33. package/.docs/raw/docs/migrations/v0-8.mdx +14 -14
  34. package/.docs/raw/docs/migrations/v0-9.mdx +3 -3
  35. package/.docs/raw/docs/runtimes/ai-sdk/rsc.mdx +2 -2
  36. package/.docs/raw/docs/runtimes/ai-sdk/use-assistant-hook.mdx +1 -1
  37. package/.docs/raw/docs/runtimes/ai-sdk/use-chat-hook.mdx +2 -2
  38. package/.docs/raw/docs/runtimes/ai-sdk/use-chat-v5.mdx +129 -0
  39. package/.docs/raw/docs/runtimes/ai-sdk/use-chat.mdx +3 -3
  40. package/.docs/raw/docs/runtimes/custom/external-store.mdx +5 -5
  41. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +2 -2
  42. package/.docs/raw/docs/ui/Attachment.mdx +5 -2
  43. package/.docs/raw/docs/ui/Markdown.mdx +2 -3
  44. package/.docs/raw/docs/ui/PartGrouping.mdx +540 -0
  45. package/.docs/raw/docs/ui/ToolFallback.mdx +2 -2
  46. package/.docs/raw/docs/ui/ToolGroup.mdx +96 -0
  47. package/package.json +8 -8
  48. package/.docs/raw/docs/api-reference/runtimes/ContentPartRuntime.mdx +0 -22
@@ -0,0 +1,1374 @@
1
+ # Example: with-parent-id-grouping
2
+
3
+ ## app/globals.css
4
+
5
+ ```css
6
+ @import "tailwindcss";
7
+ @import "tw-animate-css";
8
+
9
+ @custom-variant dark (&:is(.dark *));
10
+
11
+ @theme inline {
12
+ --color-background: var(--background);
13
+ --color-foreground: var(--foreground);
14
+ --font-sans: var(--font-geist-sans);
15
+ --font-mono: var(--font-geist-mono);
16
+ --color-sidebar-ring: var(--sidebar-ring);
17
+ --color-sidebar-border: var(--sidebar-border);
18
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
19
+ --color-sidebar-accent: var(--sidebar-accent);
20
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
21
+ --color-sidebar-primary: var(--sidebar-primary);
22
+ --color-sidebar-foreground: var(--sidebar-foreground);
23
+ --color-sidebar: var(--sidebar);
24
+ --color-chart-5: var(--chart-5);
25
+ --color-chart-4: var(--chart-4);
26
+ --color-chart-3: var(--chart-3);
27
+ --color-chart-2: var(--chart-2);
28
+ --color-chart-1: var(--chart-1);
29
+ --color-ring: var(--ring);
30
+ --color-input: var(--input);
31
+ --color-border: var(--border);
32
+ --color-destructive: var(--destructive);
33
+ --color-accent-foreground: var(--accent-foreground);
34
+ --color-accent: var(--accent);
35
+ --color-muted-foreground: var(--muted-foreground);
36
+ --color-muted: var(--muted);
37
+ --color-secondary-foreground: var(--secondary-foreground);
38
+ --color-secondary: var(--secondary);
39
+ --color-primary-foreground: var(--primary-foreground);
40
+ --color-primary: var(--primary);
41
+ --color-popover-foreground: var(--popover-foreground);
42
+ --color-popover: var(--popover);
43
+ --color-card-foreground: var(--card-foreground);
44
+ --color-card: var(--card);
45
+ --radius-sm: calc(var(--radius) - 4px);
46
+ --radius-md: calc(var(--radius) - 2px);
47
+ --radius-lg: var(--radius);
48
+ --radius-xl: calc(var(--radius) + 4px);
49
+ }
50
+
51
+ :root {
52
+ --radius: 0.625rem;
53
+ --background: oklch(1 0 0);
54
+ --foreground: oklch(0.141 0.005 285.823);
55
+ --card: oklch(1 0 0);
56
+ --card-foreground: oklch(0.141 0.005 285.823);
57
+ --popover: oklch(1 0 0);
58
+ --popover-foreground: oklch(0.141 0.005 285.823);
59
+ --primary: oklch(0.21 0.006 285.885);
60
+ --primary-foreground: oklch(0.985 0 0);
61
+ --secondary: oklch(0.967 0.001 286.375);
62
+ --secondary-foreground: oklch(0.21 0.006 285.885);
63
+ --muted: oklch(0.967 0.001 286.375);
64
+ --muted-foreground: oklch(0.552 0.016 285.938);
65
+ --accent: oklch(0.967 0.001 286.375);
66
+ --accent-foreground: oklch(0.21 0.006 285.885);
67
+ --destructive: oklch(0.577 0.245 27.325);
68
+ --border: oklch(0.92 0.004 286.32);
69
+ --input: oklch(0.92 0.004 286.32);
70
+ --ring: oklch(0.705 0.015 286.067);
71
+ --chart-1: oklch(0.646 0.222 41.116);
72
+ --chart-2: oklch(0.6 0.118 184.704);
73
+ --chart-3: oklch(0.398 0.07 227.392);
74
+ --chart-4: oklch(0.828 0.189 84.429);
75
+ --chart-5: oklch(0.769 0.188 70.08);
76
+ --sidebar: oklch(0.985 0 0);
77
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
78
+ --sidebar-primary: oklch(0.21 0.006 285.885);
79
+ --sidebar-primary-foreground: oklch(0.985 0 0);
80
+ --sidebar-accent: oklch(0.967 0.001 286.375);
81
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
82
+ --sidebar-border: oklch(0.92 0.004 286.32);
83
+ --sidebar-ring: oklch(0.705 0.015 286.067);
84
+ }
85
+
86
+ .dark {
87
+ --background: oklch(0.141 0.005 285.823);
88
+ --foreground: oklch(0.985 0 0);
89
+ --card: oklch(0.21 0.006 285.885);
90
+ --card-foreground: oklch(0.985 0 0);
91
+ --popover: oklch(0.21 0.006 285.885);
92
+ --popover-foreground: oklch(0.985 0 0);
93
+ --primary: oklch(0.92 0.004 286.32);
94
+ --primary-foreground: oklch(0.21 0.006 285.885);
95
+ --secondary: oklch(0.274 0.006 286.033);
96
+ --secondary-foreground: oklch(0.985 0 0);
97
+ --muted: oklch(0.274 0.006 286.033);
98
+ --muted-foreground: oklch(0.705 0.015 286.067);
99
+ --accent: oklch(0.274 0.006 286.033);
100
+ --accent-foreground: oklch(0.985 0 0);
101
+ --destructive: oklch(0.704 0.191 22.216);
102
+ --border: oklch(1 0 0 / 10%);
103
+ --input: oklch(1 0 0 / 15%);
104
+ --ring: oklch(0.552 0.016 285.938);
105
+ --chart-1: oklch(0.488 0.243 264.376);
106
+ --chart-2: oklch(0.696 0.17 162.48);
107
+ --chart-3: oklch(0.769 0.188 70.08);
108
+ --chart-4: oklch(0.627 0.265 303.9);
109
+ --chart-5: oklch(0.645 0.246 16.439);
110
+ --sidebar: oklch(0.21 0.006 285.885);
111
+ --sidebar-foreground: oklch(0.985 0 0);
112
+ --sidebar-primary: oklch(0.488 0.243 264.376);
113
+ --sidebar-primary-foreground: oklch(0.985 0 0);
114
+ --sidebar-accent: oklch(0.274 0.006 286.033);
115
+ --sidebar-accent-foreground: oklch(0.985 0 0);
116
+ --sidebar-border: oklch(1 0 0 / 10%);
117
+ --sidebar-ring: oklch(0.552 0.016 285.938);
118
+ }
119
+
120
+ @layer base {
121
+ * {
122
+ @apply border-border outline-ring/50;
123
+ }
124
+ body {
125
+ @apply bg-background text-foreground;
126
+ }
127
+ }
128
+
129
+ ```
130
+
131
+ ## app/layout.tsx
132
+
133
+ ```tsx
134
+ import type { Metadata } from "next";
135
+ import { MyRuntimeProvider } from "@/app/MyRuntimeProvider";
136
+
137
+ import "./globals.css";
138
+
139
+ export const metadata: Metadata = {
140
+ title: "Create Next App",
141
+ description: "Generated by create next app",
142
+ };
143
+
144
+ export default function RootLayout({
145
+ children,
146
+ }: Readonly<{
147
+ children: React.ReactNode;
148
+ }>) {
149
+ return (
150
+ <MyRuntimeProvider>
151
+ <html lang="en" className="h-dvh">
152
+ <body className="h-dvh font-sans">{children}</body>
153
+ </html>
154
+ </MyRuntimeProvider>
155
+ );
156
+ }
157
+
158
+ ```
159
+
160
+ ## app/MyRuntimeProvider.tsx
161
+
162
+ ```tsx
163
+ "use client";
164
+
165
+ import {
166
+ ThreadMessageLike,
167
+ AppendMessage,
168
+ AssistantRuntimeProvider,
169
+ useExternalStoreRuntime,
170
+ } from "@assistant-ui/react";
171
+ import { useState } from "react";
172
+
173
+ const convertMessage = (message: ThreadMessageLike) => {
174
+ return message;
175
+ };
176
+
177
+ export function MyRuntimeProvider({
178
+ children,
179
+ }: Readonly<{
180
+ children: React.ReactNode;
181
+ }>) {
182
+ const [messages, setMessages] = useState<readonly ThreadMessageLike[]>([
183
+ {
184
+ id: "msg-1",
185
+ createdAt: new Date("2024-01-01T10:00:00"),
186
+ role: "user",
187
+ content: [
188
+ {
189
+ type: "text",
190
+ text: "Tell me about climate change and its effects",
191
+ },
192
+ ],
193
+ },
194
+ {
195
+ id: "msg-2",
196
+ createdAt: new Date("2024-01-01T10:00:05"),
197
+ role: "assistant",
198
+ content: [
199
+ {
200
+ type: "text",
201
+ text: "Climate change refers to long-term shifts in global temperatures and weather patterns. Let me provide you with information about its causes and effects.",
202
+ },
203
+ {
204
+ type: "tool-call",
205
+ toolCallId: "call-1",
206
+ toolName: "search_research",
207
+ args: { query: "climate change causes" },
208
+ argsText: '{"query": "climate change causes"}',
209
+ result: {
210
+ sources: [
211
+ "https://climate.nasa.gov/causes/",
212
+ "https://www.ipcc.ch/report/ar6/wg1/",
213
+ ],
214
+ },
215
+ parentId: "research-climate-causes",
216
+ },
217
+ {
218
+ type: "source",
219
+ sourceType: "url",
220
+ id: "source-1",
221
+ url: "https://climate.nasa.gov/causes/",
222
+ title: "NASA: Climate Change Causes",
223
+ parentId: "research-climate-causes",
224
+ },
225
+ {
226
+ type: "source",
227
+ sourceType: "url",
228
+ id: "source-2",
229
+ url: "https://www.ipcc.ch/report/ar6/wg1/",
230
+ title: "IPCC Sixth Assessment Report",
231
+ parentId: "research-climate-causes",
232
+ },
233
+ {
234
+ type: "text",
235
+ text: "The main causes of climate change include:",
236
+ parentId: "research-climate-causes",
237
+ },
238
+ {
239
+ type: "text",
240
+ text: "1. **Greenhouse Gas Emissions**: The burning of fossil fuels releases CO2 and other greenhouse gases that trap heat in the atmosphere.",
241
+ parentId: "research-climate-causes",
242
+ },
243
+ {
244
+ type: "text",
245
+ text: "2. **Deforestation**: Removing forests reduces the Earth's capacity to absorb CO2.",
246
+ parentId: "research-climate-causes",
247
+ },
248
+ {
249
+ type: "text",
250
+ text: "Now, let me search for information about the effects of climate change:",
251
+ },
252
+ {
253
+ type: "tool-call",
254
+ toolCallId: "call-2",
255
+ toolName: "search_research",
256
+ args: { query: "climate change effects impacts" },
257
+ argsText: '{"query": "climate change effects impacts"}',
258
+ result: {
259
+ sources: [
260
+ "https://www.who.int/health-topics/climate-change",
261
+ "https://www.unep.org/facts-about-climate-emergency",
262
+ ],
263
+ },
264
+ parentId: "research-climate-effects",
265
+ },
266
+ {
267
+ type: "source",
268
+ sourceType: "url",
269
+ id: "source-3",
270
+ url: "https://www.who.int/health-topics/climate-change",
271
+ title: "WHO: Climate Change and Health",
272
+ parentId: "research-climate-effects",
273
+ },
274
+ {
275
+ type: "source",
276
+ sourceType: "url",
277
+ id: "source-4",
278
+ url: "https://www.unep.org/facts-about-climate-emergency",
279
+ title: "UNEP: Climate Emergency Facts",
280
+ parentId: "research-climate-effects",
281
+ },
282
+ {
283
+ type: "text",
284
+ text: "The major effects of climate change include:",
285
+ parentId: "research-climate-effects",
286
+ },
287
+ {
288
+ type: "text",
289
+ text: "• **Rising Sea Levels**: Melting ice caps and thermal expansion of oceans threaten coastal communities.",
290
+ parentId: "research-climate-effects",
291
+ },
292
+ {
293
+ type: "text",
294
+ text: "• **Extreme Weather Events**: More frequent and intense hurricanes, droughts, and heatwaves.",
295
+ parentId: "research-climate-effects",
296
+ },
297
+ {
298
+ type: "text",
299
+ text: "• **Ecosystem Disruption**: Changes in temperature and precipitation patterns affect biodiversity and agriculture.",
300
+ parentId: "research-climate-effects",
301
+ },
302
+ {
303
+ type: "text",
304
+ text: "In conclusion, climate change is one of the most pressing challenges facing humanity, requiring immediate action on both mitigation and adaptation strategies.",
305
+ },
306
+ ] as any, // Using any to bypass strict typing for demo purposes
307
+ status: { type: "complete", reason: "stop" },
308
+ },
309
+ ]);
310
+
311
+ const onNew = async (message: AppendMessage) => {
312
+ if (message.content.length !== 1 || message.content[0]?.type !== "text")
313
+ throw new Error("Only text content is supported");
314
+
315
+ const userMessage: ThreadMessageLike = {
316
+ id: `msg-user-${Date.now()}`,
317
+ createdAt: new Date(),
318
+ role: "user",
319
+ content: [{ type: "text", text: message.content[0].text }],
320
+ };
321
+ setMessages((currentMessages) => [...currentMessages, userMessage]);
322
+
323
+ // Simulate assistant response with parent IDs
324
+ await new Promise((resolve) => setTimeout(resolve, 1000));
325
+
326
+ const assistantMessage: ThreadMessageLike = {
327
+ id: `msg-assistant-${Date.now()}`,
328
+ createdAt: new Date(),
329
+ role: "assistant",
330
+ content: [
331
+ {
332
+ type: "text",
333
+ text: "I understand you want to know more. Let me research that for you.",
334
+ },
335
+ {
336
+ type: "tool-call",
337
+ toolCallId: `call-${Date.now()}`,
338
+ toolName: "search",
339
+ args: { query: "recent developments" },
340
+ argsText: '{"query": "recent developments"}',
341
+ result: { data: "Latest information found" },
342
+ parentId: "new-research",
343
+ },
344
+ {
345
+ type: "text",
346
+ text: "Based on my research:",
347
+ parentId: "new-research",
348
+ },
349
+ {
350
+ type: "text",
351
+ text: "Here's what I found about your query...",
352
+ parentId: "new-research",
353
+ },
354
+ {
355
+ type: "text",
356
+ text: "I hope this information helps!",
357
+ },
358
+ ] as any, // Using any to bypass strict typing for demo purposes
359
+ status: { type: "complete", reason: "stop" },
360
+ };
361
+ setMessages((currentMessages) => [...currentMessages, assistantMessage]);
362
+ };
363
+
364
+ const runtime = useExternalStoreRuntime<ThreadMessageLike>({
365
+ messages,
366
+ setMessages,
367
+ onNew,
368
+ convertMessage,
369
+ });
370
+
371
+ return (
372
+ <AssistantRuntimeProvider runtime={runtime}>
373
+ {children}
374
+ </AssistantRuntimeProvider>
375
+ );
376
+ }
377
+
378
+ ```
379
+
380
+ ## app/page.tsx
381
+
382
+ ```tsx
383
+ "use client";
384
+
385
+ import { Thread } from "@/components/assistant-ui/thread";
386
+
387
+ export default function Home() {
388
+ return (
389
+ <main className="h-dvh">
390
+ <Thread />
391
+ </main>
392
+ );
393
+ }
394
+
395
+ ```
396
+
397
+ ## components.json
398
+
399
+ ```json
400
+ {
401
+ "$schema": "https://ui.shadcn.com/schema.json",
402
+ "style": "new-york",
403
+ "rsc": true,
404
+ "tsx": true,
405
+ "tailwind": {
406
+ "config": "",
407
+ "css": "app/globals.css",
408
+ "baseColor": "zinc",
409
+ "cssVariables": true,
410
+ "prefix": ""
411
+ },
412
+ "aliases": {
413
+ "components": "@/components",
414
+ "utils": "@/lib/utils",
415
+ "ui": "@/components/ui",
416
+ "lib": "@/lib",
417
+ "hooks": "@/hooks"
418
+ },
419
+ "iconLibrary": "lucide"
420
+ }
421
+
422
+ ```
423
+
424
+ ## components/assistant-ui/markdown-text.tsx
425
+
426
+ ```tsx
427
+ "use client";
428
+
429
+ import "@assistant-ui/react-markdown/styles/dot.css";
430
+
431
+ import {
432
+ CodeHeaderProps,
433
+ MarkdownTextPrimitive,
434
+ unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
435
+ useIsMarkdownCodeBlock,
436
+ } from "@assistant-ui/react-markdown";
437
+ import remarkGfm from "remark-gfm";
438
+ import { FC, memo, useState } from "react";
439
+ import { CheckIcon, CopyIcon } from "lucide-react";
440
+
441
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
442
+ import { cn } from "@/lib/utils";
443
+
444
+ const MarkdownTextImpl = () => {
445
+ return (
446
+ <MarkdownTextPrimitive
447
+ remarkPlugins={[remarkGfm]}
448
+ className="aui-md"
449
+ components={defaultComponents}
450
+ />
451
+ );
452
+ };
453
+
454
+ export const MarkdownText = memo(MarkdownTextImpl);
455
+
456
+ const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
457
+ const { isCopied, copyToClipboard } = useCopyToClipboard();
458
+ const onCopy = () => {
459
+ if (!code || isCopied) return;
460
+ copyToClipboard(code);
461
+ };
462
+
463
+ return (
464
+ <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">
465
+ <span className="lowercase [&>span]:text-xs">{language}</span>
466
+ <TooltipIconButton tooltip="Copy" onClick={onCopy}>
467
+ {!isCopied && <CopyIcon />}
468
+ {isCopied && <CheckIcon />}
469
+ </TooltipIconButton>
470
+ </div>
471
+ );
472
+ };
473
+
474
+ const useCopyToClipboard = ({
475
+ copiedDuration = 3000,
476
+ }: {
477
+ copiedDuration?: number;
478
+ } = {}) => {
479
+ const [isCopied, setIsCopied] = useState<boolean>(false);
480
+
481
+ const copyToClipboard = (value: string) => {
482
+ if (!value) return;
483
+
484
+ navigator.clipboard.writeText(value).then(() => {
485
+ setIsCopied(true);
486
+ setTimeout(() => setIsCopied(false), copiedDuration);
487
+ });
488
+ };
489
+
490
+ return { isCopied, copyToClipboard };
491
+ };
492
+
493
+ const defaultComponents = memoizeMarkdownComponents({
494
+ h1: ({ className, ...props }) => (
495
+ <h1
496
+ className={cn(
497
+ "mb-8 scroll-m-20 text-4xl font-extrabold tracking-tight last:mb-0",
498
+ className,
499
+ )}
500
+ {...props}
501
+ />
502
+ ),
503
+ h2: ({ className, ...props }) => (
504
+ <h2
505
+ className={cn(
506
+ "mb-4 mt-8 scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0 last:mb-0",
507
+ className,
508
+ )}
509
+ {...props}
510
+ />
511
+ ),
512
+ h3: ({ className, ...props }) => (
513
+ <h3
514
+ className={cn(
515
+ "mb-4 mt-6 scroll-m-20 text-2xl font-semibold tracking-tight first:mt-0 last:mb-0",
516
+ className,
517
+ )}
518
+ {...props}
519
+ />
520
+ ),
521
+ h4: ({ className, ...props }) => (
522
+ <h4
523
+ className={cn(
524
+ "mb-4 mt-6 scroll-m-20 text-xl font-semibold tracking-tight first:mt-0 last:mb-0",
525
+ className,
526
+ )}
527
+ {...props}
528
+ />
529
+ ),
530
+ h5: ({ className, ...props }) => (
531
+ <h5
532
+ className={cn(
533
+ "my-4 text-lg font-semibold first:mt-0 last:mb-0",
534
+ className,
535
+ )}
536
+ {...props}
537
+ />
538
+ ),
539
+ h6: ({ className, ...props }) => (
540
+ <h6
541
+ className={cn("my-4 font-semibold first:mt-0 last:mb-0", className)}
542
+ {...props}
543
+ />
544
+ ),
545
+ p: ({ className, ...props }) => (
546
+ <p
547
+ className={cn("mb-5 mt-5 leading-7 first:mt-0 last:mb-0", className)}
548
+ {...props}
549
+ />
550
+ ),
551
+ a: ({ className, ...props }) => (
552
+ <a
553
+ className={cn(
554
+ "text-primary font-medium underline underline-offset-4",
555
+ className,
556
+ )}
557
+ {...props}
558
+ />
559
+ ),
560
+ blockquote: ({ className, ...props }) => (
561
+ <blockquote
562
+ className={cn("border-l-2 pl-6 italic", className)}
563
+ {...props}
564
+ />
565
+ ),
566
+ ul: ({ className, ...props }) => (
567
+ <ul
568
+ className={cn("my-5 ml-6 list-disc [&>li]:mt-2", className)}
569
+ {...props}
570
+ />
571
+ ),
572
+ ol: ({ className, ...props }) => (
573
+ <ol
574
+ className={cn("my-5 ml-6 list-decimal [&>li]:mt-2", className)}
575
+ {...props}
576
+ />
577
+ ),
578
+ hr: ({ className, ...props }) => (
579
+ <hr className={cn("my-5 border-b", className)} {...props} />
580
+ ),
581
+ table: ({ className, ...props }) => (
582
+ <table
583
+ className={cn(
584
+ "my-5 w-full border-separate border-spacing-0 overflow-y-auto",
585
+ className,
586
+ )}
587
+ {...props}
588
+ />
589
+ ),
590
+ th: ({ className, ...props }) => (
591
+ <th
592
+ className={cn(
593
+ "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",
594
+ className,
595
+ )}
596
+ {...props}
597
+ />
598
+ ),
599
+ td: ({ className, ...props }) => (
600
+ <td
601
+ className={cn(
602
+ "border-b border-l px-4 py-2 text-left last:border-r [&[align=center]]:text-center [&[align=right]]:text-right",
603
+ className,
604
+ )}
605
+ {...props}
606
+ />
607
+ ),
608
+ tr: ({ className, ...props }) => (
609
+ <tr
610
+ className={cn(
611
+ "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",
612
+ className,
613
+ )}
614
+ {...props}
615
+ />
616
+ ),
617
+ sup: ({ className, ...props }) => (
618
+ <sup
619
+ className={cn("[&>a]:text-xs [&>a]:no-underline", className)}
620
+ {...props}
621
+ />
622
+ ),
623
+ pre: ({ className, ...props }) => (
624
+ <pre
625
+ className={cn(
626
+ "overflow-x-auto rounded-b-lg bg-black p-4 text-white",
627
+ className,
628
+ )}
629
+ {...props}
630
+ />
631
+ ),
632
+ code: function Code({ className, ...props }) {
633
+ const isCodeBlock = useIsMarkdownCodeBlock();
634
+ return (
635
+ <code
636
+ className={cn(
637
+ !isCodeBlock && "bg-muted rounded border font-semibold",
638
+ className,
639
+ )}
640
+ {...props}
641
+ />
642
+ );
643
+ },
644
+ CodeHeader,
645
+ });
646
+
647
+ ```
648
+
649
+ ## components/assistant-ui/thread.tsx
650
+
651
+ ```tsx
652
+ import {
653
+ ActionBarPrimitive,
654
+ BranchPickerPrimitive,
655
+ ComposerPrimitive,
656
+ MessagePrimitive,
657
+ ThreadPrimitive,
658
+ } from "@assistant-ui/react";
659
+ import type { FC, PropsWithChildren } from "react";
660
+ import {
661
+ ArrowDownIcon,
662
+ CheckIcon,
663
+ ChevronLeftIcon,
664
+ ChevronRightIcon,
665
+ CopyIcon,
666
+ PencilIcon,
667
+ RefreshCwIcon,
668
+ SendHorizontalIcon,
669
+ ChevronDownIcon,
670
+ ChevronUpIcon,
671
+ } from "lucide-react";
672
+ import { cn } from "@/lib/utils";
673
+
674
+ import { Button } from "@/components/ui/button";
675
+ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
676
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
677
+ import { useState } from "react";
678
+
679
+ export const Thread: FC = () => {
680
+ return (
681
+ <ThreadPrimitive.Root
682
+ className="bg-background box-border flex h-full flex-col overflow-hidden"
683
+ style={{
684
+ ["--thread-max-width" as string]: "42rem",
685
+ }}
686
+ >
687
+ <ThreadPrimitive.Viewport className="flex h-full flex-col items-center overflow-y-scroll scroll-smooth bg-inherit px-4 pt-8">
688
+ <ThreadWelcome />
689
+
690
+ <ThreadPrimitive.Messages
691
+ components={{
692
+ UserMessage: UserMessage,
693
+ EditComposer: EditComposer,
694
+ AssistantMessage: AssistantMessage,
695
+ }}
696
+ />
697
+
698
+ <ThreadPrimitive.If empty={false}>
699
+ <div className="min-h-8 flex-grow" />
700
+ </ThreadPrimitive.If>
701
+
702
+ <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">
703
+ <ThreadScrollToBottom />
704
+ <Composer />
705
+ </div>
706
+ </ThreadPrimitive.Viewport>
707
+ </ThreadPrimitive.Root>
708
+ );
709
+ };
710
+
711
+ const ThreadScrollToBottom: FC = () => {
712
+ return (
713
+ <ThreadPrimitive.ScrollToBottom asChild>
714
+ <TooltipIconButton
715
+ tooltip="Scroll to bottom"
716
+ variant="outline"
717
+ className="absolute -top-8 rounded-full disabled:invisible"
718
+ >
719
+ <ArrowDownIcon />
720
+ </TooltipIconButton>
721
+ </ThreadPrimitive.ScrollToBottom>
722
+ );
723
+ };
724
+
725
+ const ThreadWelcome: FC = () => {
726
+ return (
727
+ <ThreadPrimitive.Empty>
728
+ <div className="flex w-full max-w-[var(--thread-max-width)] flex-grow flex-col">
729
+ <div className="flex w-full flex-grow flex-col items-center justify-center">
730
+ <p className="mt-4 font-medium">Parent ID Grouping Demo</p>
731
+ <p className="text-muted-foreground mt-2 text-sm">
732
+ This example demonstrates how message parts can be grouped by parent
733
+ ID
734
+ </p>
735
+ </div>
736
+ <ThreadWelcomeSuggestions />
737
+ </div>
738
+ </ThreadPrimitive.Empty>
739
+ );
740
+ };
741
+
742
+ const ThreadWelcomeSuggestions: FC = () => {
743
+ return (
744
+ <div className="mt-3 flex w-full items-stretch justify-center gap-4">
745
+ <ThreadPrimitive.Suggestion
746
+ 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"
747
+ prompt="Tell me more about this"
748
+ method="replace"
749
+ autoSend
750
+ >
751
+ <span className="line-clamp-2 text-ellipsis text-sm font-semibold">
752
+ Tell me more about this
753
+ </span>
754
+ </ThreadPrimitive.Suggestion>
755
+ <ThreadPrimitive.Suggestion
756
+ 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"
757
+ prompt="What are the latest updates?"
758
+ method="replace"
759
+ autoSend
760
+ >
761
+ <span className="line-clamp-2 text-ellipsis text-sm font-semibold">
762
+ What are the latest updates?
763
+ </span>
764
+ </ThreadPrimitive.Suggestion>
765
+ </div>
766
+ );
767
+ };
768
+
769
+ const Composer: FC = () => {
770
+ return (
771
+ <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">
772
+ <ComposerPrimitive.Input
773
+ rows={1}
774
+ autoFocus
775
+ placeholder="Write a message..."
776
+ 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"
777
+ />
778
+ <ComposerAction />
779
+ </ComposerPrimitive.Root>
780
+ );
781
+ };
782
+
783
+ const ComposerAction: FC = () => {
784
+ return (
785
+ <>
786
+ <ThreadPrimitive.If running={false}>
787
+ <ComposerPrimitive.Send asChild>
788
+ <TooltipIconButton
789
+ tooltip="Send"
790
+ variant="default"
791
+ className="my-2.5 size-8 p-2 transition-opacity ease-in"
792
+ >
793
+ <SendHorizontalIcon />
794
+ </TooltipIconButton>
795
+ </ComposerPrimitive.Send>
796
+ </ThreadPrimitive.If>
797
+ <ThreadPrimitive.If running>
798
+ <ComposerPrimitive.Cancel asChild>
799
+ <TooltipIconButton
800
+ tooltip="Cancel"
801
+ variant="default"
802
+ className="my-2.5 size-8 p-2 transition-opacity ease-in"
803
+ >
804
+ <CircleStopIcon />
805
+ </TooltipIconButton>
806
+ </ComposerPrimitive.Cancel>
807
+ </ThreadPrimitive.If>
808
+ </>
809
+ );
810
+ };
811
+
812
+ const UserMessage: FC = () => {
813
+ return (
814
+ <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">
815
+ <UserActionBar />
816
+
817
+ <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">
818
+ <MessagePrimitive.Parts />
819
+ </div>
820
+
821
+ <BranchPicker className="col-span-full col-start-1 row-start-3 -mr-1 justify-end" />
822
+ </MessagePrimitive.Root>
823
+ );
824
+ };
825
+
826
+ const UserActionBar: FC = () => {
827
+ return (
828
+ <ActionBarPrimitive.Root
829
+ hideWhenRunning
830
+ autohide="not-last"
831
+ className="col-start-1 row-start-2 mr-3 mt-2.5 flex flex-col items-end"
832
+ >
833
+ <ActionBarPrimitive.Edit asChild>
834
+ <TooltipIconButton tooltip="Edit">
835
+ <PencilIcon />
836
+ </TooltipIconButton>
837
+ </ActionBarPrimitive.Edit>
838
+ </ActionBarPrimitive.Root>
839
+ );
840
+ };
841
+
842
+ const EditComposer: FC = () => {
843
+ return (
844
+ <ComposerPrimitive.Root className="bg-muted my-4 flex w-full max-w-[var(--thread-max-width)] flex-col gap-2 rounded-xl">
845
+ <ComposerPrimitive.Input className="text-foreground flex h-8 w-full resize-none bg-transparent p-4 pb-0 outline-none" />
846
+
847
+ <div className="mx-3 mb-3 flex items-center justify-center gap-2 self-end">
848
+ <ComposerPrimitive.Cancel asChild>
849
+ <Button variant="ghost">Cancel</Button>
850
+ </ComposerPrimitive.Cancel>
851
+ <ComposerPrimitive.Send asChild>
852
+ <Button>Send</Button>
853
+ </ComposerPrimitive.Send>
854
+ </div>
855
+ </ComposerPrimitive.Root>
856
+ );
857
+ };
858
+
859
+ // Custom Group component for parent ID grouping
860
+ const ParentIdGroup: FC<
861
+ PropsWithChildren<{ groupKey: string | undefined; indices: number[] }>
862
+ > = ({ groupKey, indices, children }) => {
863
+ const [isCollapsed, setIsCollapsed] = useState(false);
864
+
865
+ if (!groupKey) {
866
+ // Ungrouped parts - just render them directly
867
+ return <>{children}</>;
868
+ }
869
+
870
+ return (
871
+ <div className="border-border/50 bg-muted/20 my-2 overflow-hidden rounded-lg border">
872
+ <button
873
+ onClick={() => setIsCollapsed(!isCollapsed)}
874
+ className="hover:bg-muted/40 flex w-full items-center justify-between px-4 py-2 text-sm font-medium transition-colors"
875
+ >
876
+ <span className="flex items-center gap-2">
877
+ <span className="text-muted-foreground">Research Group:</span>
878
+ <span className="text-foreground">
879
+ {groupKey === "research-climate-causes" && "Climate Change Causes"}
880
+ {groupKey === "research-climate-effects" &&
881
+ "Climate Change Effects"}
882
+ {groupKey === "new-research" && "Recent Research"}
883
+ {![
884
+ "research-climate-causes",
885
+ "research-climate-effects",
886
+ "new-research",
887
+ ].includes(groupKey) && groupKey}
888
+ </span>
889
+ <span className="text-muted-foreground text-xs">
890
+ ({indices.length} parts)
891
+ </span>
892
+ </span>
893
+ {isCollapsed ? (
894
+ <ChevronDownIcon className="h-4 w-4" />
895
+ ) : (
896
+ <ChevronUpIcon className="h-4 w-4" />
897
+ )}
898
+ </button>
899
+ {!isCollapsed && <div className="space-y-2 px-4 py-2">{children}</div>}
900
+ </div>
901
+ );
902
+ };
903
+
904
+ const AssistantMessage: FC = () => {
905
+ return (
906
+ <MessagePrimitive.Root className="relative grid w-full max-w-[var(--thread-max-width)] grid-cols-[auto_auto_1fr] grid-rows-[auto_1fr] py-4">
907
+ <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">
908
+ <MessagePrimitive.Unstable_PartsGroupedByParentId
909
+ components={{
910
+ Text: MarkdownText,
911
+ Group: ParentIdGroup,
912
+ Source: ({ url, title }) => (
913
+ <div className="text-muted-foreground text-sm">
914
+ <a
915
+ href={url}
916
+ target="_blank"
917
+ rel="noopener noreferrer"
918
+ className="hover:underline"
919
+ >
920
+ 📄 {title || url}
921
+ </a>
922
+ </div>
923
+ ),
924
+ tools: {
925
+ Fallback: ({ toolName, args, result }) => (
926
+ <div className="bg-muted/40 my-1 rounded-md p-2 text-sm">
927
+ <div className="text-muted-foreground font-medium">
928
+ 🔧 {toolName}
929
+ </div>
930
+ <div className="text-muted-foreground mt-1 text-xs">
931
+ <details>
932
+ <summary className="cursor-pointer">View details</summary>
933
+ <pre className="mt-2 overflow-x-auto">
934
+ {JSON.stringify({ args, result }, null, 2)}
935
+ </pre>
936
+ </details>
937
+ </div>
938
+ </div>
939
+ ),
940
+ },
941
+ }}
942
+ />
943
+ </div>
944
+
945
+ <AssistantActionBar />
946
+
947
+ <BranchPicker className="col-start-2 row-start-2 -ml-2 mr-2" />
948
+ </MessagePrimitive.Root>
949
+ );
950
+ };
951
+
952
+ const AssistantActionBar: FC = () => {
953
+ return (
954
+ <ActionBarPrimitive.Root
955
+ hideWhenRunning
956
+ autohide="not-last"
957
+ autohideFloat="single-branch"
958
+ 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"
959
+ >
960
+ <ActionBarPrimitive.Copy asChild>
961
+ <TooltipIconButton tooltip="Copy">
962
+ <MessagePrimitive.If copied>
963
+ <CheckIcon />
964
+ </MessagePrimitive.If>
965
+ <MessagePrimitive.If copied={false}>
966
+ <CopyIcon />
967
+ </MessagePrimitive.If>
968
+ </TooltipIconButton>
969
+ </ActionBarPrimitive.Copy>
970
+ <ActionBarPrimitive.Reload asChild>
971
+ <TooltipIconButton tooltip="Refresh">
972
+ <RefreshCwIcon />
973
+ </TooltipIconButton>
974
+ </ActionBarPrimitive.Reload>
975
+ </ActionBarPrimitive.Root>
976
+ );
977
+ };
978
+
979
+ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
980
+ className,
981
+ ...rest
982
+ }) => {
983
+ return (
984
+ <BranchPickerPrimitive.Root
985
+ hideWhenSingleBranch
986
+ className={cn(
987
+ "text-muted-foreground inline-flex items-center text-xs",
988
+ className,
989
+ )}
990
+ {...rest}
991
+ >
992
+ <BranchPickerPrimitive.Previous asChild>
993
+ <TooltipIconButton tooltip="Previous">
994
+ <ChevronLeftIcon />
995
+ </TooltipIconButton>
996
+ </BranchPickerPrimitive.Previous>
997
+ <span className="font-medium">
998
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
999
+ </span>
1000
+ <BranchPickerPrimitive.Next asChild>
1001
+ <TooltipIconButton tooltip="Next">
1002
+ <ChevronRightIcon />
1003
+ </TooltipIconButton>
1004
+ </BranchPickerPrimitive.Next>
1005
+ </BranchPickerPrimitive.Root>
1006
+ );
1007
+ };
1008
+
1009
+ const CircleStopIcon = () => {
1010
+ return (
1011
+ <svg
1012
+ xmlns="http://www.w3.org/2000/svg"
1013
+ viewBox="0 0 16 16"
1014
+ fill="currentColor"
1015
+ width="16"
1016
+ height="16"
1017
+ >
1018
+ <rect width="10" height="10" x="3" y="3" rx="2" />
1019
+ </svg>
1020
+ );
1021
+ };
1022
+
1023
+ ```
1024
+
1025
+ ## components/assistant-ui/tooltip-icon-button.tsx
1026
+
1027
+ ```tsx
1028
+ "use client";
1029
+
1030
+ import { ComponentPropsWithoutRef, forwardRef } from "react";
1031
+
1032
+ import {
1033
+ Tooltip,
1034
+ TooltipContent,
1035
+ TooltipTrigger,
1036
+ } from "@/components/ui/tooltip";
1037
+ import { Button } from "@/components/ui/button";
1038
+ import { cn } from "@/lib/utils";
1039
+
1040
+ export type TooltipIconButtonProps = ComponentPropsWithoutRef<typeof Button> & {
1041
+ tooltip: string;
1042
+ side?: "top" | "bottom" | "left" | "right";
1043
+ };
1044
+
1045
+ export const TooltipIconButton = forwardRef<
1046
+ HTMLButtonElement,
1047
+ TooltipIconButtonProps
1048
+ >(({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
1049
+ return (
1050
+ <Tooltip>
1051
+ <TooltipTrigger asChild>
1052
+ <Button
1053
+ variant="ghost"
1054
+ size="icon"
1055
+ {...rest}
1056
+ className={cn("size-6 p-1", className)}
1057
+ ref={ref}
1058
+ >
1059
+ {children}
1060
+ <span className="sr-only">{tooltip}</span>
1061
+ </Button>
1062
+ </TooltipTrigger>
1063
+ <TooltipContent side={side}>{tooltip}</TooltipContent>
1064
+ </Tooltip>
1065
+ );
1066
+ });
1067
+
1068
+ TooltipIconButton.displayName = "TooltipIconButton";
1069
+
1070
+ ```
1071
+
1072
+ ## components/ui/button.tsx
1073
+
1074
+ ```tsx
1075
+ "use client";
1076
+
1077
+ import * as React from "react";
1078
+ import { Slot } from "@radix-ui/react-slot";
1079
+ import { cva, type VariantProps } from "class-variance-authority";
1080
+
1081
+ import { cn } from "@/lib/utils";
1082
+
1083
+ const buttonVariants = cva(
1084
+ "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",
1085
+ {
1086
+ variants: {
1087
+ variant: {
1088
+ default:
1089
+ "bg-primary text-primary-foreground shadow hover:bg-primary/90",
1090
+ destructive:
1091
+ "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
1092
+ outline:
1093
+ "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
1094
+ secondary:
1095
+ "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
1096
+ ghost: "hover:bg-accent hover:text-accent-foreground",
1097
+ link: "text-primary underline-offset-4 hover:underline",
1098
+ },
1099
+ size: {
1100
+ default: "h-9 px-4 py-2",
1101
+ sm: "h-8 rounded-md px-3 text-xs",
1102
+ lg: "h-10 rounded-md px-8",
1103
+ icon: "h-9 w-9",
1104
+ },
1105
+ },
1106
+ defaultVariants: {
1107
+ variant: "default",
1108
+ size: "default",
1109
+ },
1110
+ },
1111
+ );
1112
+
1113
+ export interface ButtonProps
1114
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
1115
+ VariantProps<typeof buttonVariants> {
1116
+ asChild?: boolean;
1117
+ }
1118
+
1119
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
1120
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
1121
+ const Comp = asChild ? Slot : "button";
1122
+ return (
1123
+ <Comp
1124
+ className={cn(buttonVariants({ variant, size, className }))}
1125
+ ref={ref}
1126
+ {...props}
1127
+ />
1128
+ );
1129
+ },
1130
+ );
1131
+ Button.displayName = "Button";
1132
+
1133
+ export { Button, buttonVariants };
1134
+
1135
+ ```
1136
+
1137
+ ## components/ui/tooltip.tsx
1138
+
1139
+ ```tsx
1140
+ "use client";
1141
+
1142
+ import * as React from "react";
1143
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
1144
+
1145
+ import { cn } from "@/lib/utils";
1146
+
1147
+ function TooltipProvider({
1148
+ delayDuration = 0,
1149
+ ...props
1150
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
1151
+ return (
1152
+ <TooltipPrimitive.Provider
1153
+ data-slot="tooltip-provider"
1154
+ delayDuration={delayDuration}
1155
+ {...props}
1156
+ />
1157
+ );
1158
+ }
1159
+
1160
+ function Tooltip({
1161
+ ...props
1162
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
1163
+ return (
1164
+ <TooltipProvider>
1165
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
1166
+ </TooltipProvider>
1167
+ );
1168
+ }
1169
+
1170
+ function TooltipTrigger({
1171
+ ...props
1172
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
1173
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
1174
+ }
1175
+
1176
+ function TooltipContent({
1177
+ className,
1178
+ sideOffset = 0,
1179
+ children,
1180
+ ...props
1181
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
1182
+ return (
1183
+ <TooltipPrimitive.Portal>
1184
+ <TooltipPrimitive.Content
1185
+ data-slot="tooltip-content"
1186
+ sideOffset={sideOffset}
1187
+ className={cn(
1188
+ "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 w-fit origin-[--radix-tooltip-content-transform-origin] text-balance rounded-md px-3 py-1.5 text-xs",
1189
+ className,
1190
+ )}
1191
+ {...props}
1192
+ >
1193
+ {children}
1194
+ <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
1195
+ </TooltipPrimitive.Content>
1196
+ </TooltipPrimitive.Portal>
1197
+ );
1198
+ }
1199
+
1200
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
1201
+
1202
+ ```
1203
+
1204
+ ## eslint.config.ts
1205
+
1206
+ ```typescript
1207
+ export { default } from "@assistant-ui/x-buildutils/eslint";
1208
+
1209
+ ```
1210
+
1211
+ ## lib/utils.ts
1212
+
1213
+ ```typescript
1214
+ import { clsx, type ClassValue } from "clsx";
1215
+ import { twMerge } from "tailwind-merge";
1216
+
1217
+ export function cn(...inputs: ClassValue[]) {
1218
+ return twMerge(clsx(inputs));
1219
+ }
1220
+
1221
+ ```
1222
+
1223
+ ## next.config.ts
1224
+
1225
+ ```typescript
1226
+ import type { NextConfig } from "next";
1227
+
1228
+ const nextConfig: NextConfig = {
1229
+ /* config options here */
1230
+ };
1231
+
1232
+ export default nextConfig;
1233
+
1234
+ ```
1235
+
1236
+ ## package.json
1237
+
1238
+ ```json
1239
+ {
1240
+ "name": "with-parent-id-grouping",
1241
+ "version": "0.1.0",
1242
+ "private": true,
1243
+ "scripts": {
1244
+ "dev": "next dev --turbo",
1245
+ "build": "next build",
1246
+ "start": "next start",
1247
+ "lint": "next lint"
1248
+ },
1249
+ "dependencies": {
1250
+ "@ai-sdk/openai": "^1.3.22",
1251
+ "@assistant-ui/react": "workspace:*",
1252
+ "@assistant-ui/react-markdown": "workspace:*",
1253
+ "@radix-ui/react-slot": "^1.2.3",
1254
+ "@radix-ui/react-tooltip": "^1.2.7",
1255
+ "class-variance-authority": "^0.7.1",
1256
+ "clsx": "^2.1.1",
1257
+ "lucide-react": "^0.535.0",
1258
+ "next": "15.4.5",
1259
+ "react": "19.1.1",
1260
+ "react-dom": "19.1.1",
1261
+ "remark-gfm": "^4.0.1",
1262
+ "tailwind-merge": "^3.3.1",
1263
+ "tw-animate-css": "^1.3.6"
1264
+ },
1265
+ "devDependencies": {
1266
+ "@assistant-ui/x-buildutils": "workspace:*",
1267
+ "@types/node": "^24",
1268
+ "@types/react": "^19",
1269
+ "@types/react-dom": "^19",
1270
+ "eslint": "^9",
1271
+ "eslint-config-next": "15.4.5",
1272
+ "postcss": "^8",
1273
+ "tailwindcss": "^4.1.11",
1274
+ "typescript": "^5"
1275
+ }
1276
+ }
1277
+
1278
+ ```
1279
+
1280
+ ## README.md
1281
+
1282
+ ```markdown
1283
+ # Parent ID Grouping Example
1284
+
1285
+ This example demonstrates how to use the parent ID feature in assistant-ui to group related message parts together.
1286
+
1287
+ ## Features
1288
+
1289
+ - **Parent ID Support**: Message parts can have a `parentId` field that groups them together
1290
+ - **Visual Grouping**: Related parts are displayed in collapsible groups
1291
+ - **Custom Group Component**: The example shows how to create a custom Group component that:
1292
+ - Shows grouped parts in a bordered container
1293
+ - Provides expand/collapse functionality
1294
+ - Displays meaningful labels for each group
1295
+ - Leaves ungrouped parts (without parentId) as-is
1296
+
1297
+ ## How it works
1298
+
1299
+ 1. **Message Structure**: The example uses the external store runtime with predefined messages that include parts with `parentId` fields:
1300
+ ```typescript
1301
+ {
1302
+ type: "text",
1303
+ text: "Some related text",
1304
+ parentId: "research-climate-causes"
1305
+ }
1306
+ ```
1307
+
1308
+ 2. **Grouping Component**: Uses `MessagePrimitive.Unstable_PartsGroupedByParentId` which automatically:
1309
+ - Groups parts by their `parentId`
1310
+ - Maintains order based on first occurrence
1311
+ - Places ungrouped parts after grouped ones
1312
+
1313
+ 3. **Custom Rendering**: The `ParentIdGroup` component provides:
1314
+ - Collapsible sections for each group
1315
+ - Custom styling with borders and backgrounds
1316
+ - Meaningful labels based on the parent ID
1317
+
1318
+ ## Running the Example
1319
+
1320
+ ```bash
1321
+ # Install dependencies
1322
+ npm install
1323
+
1324
+ # Run the development server
1325
+ npm run dev
1326
+ ```
1327
+
1328
+ Open [http://localhost:3000](http://localhost:3000) to see the example.
1329
+
1330
+ ## Key Components
1331
+
1332
+ - `MyRuntimeProvider.tsx`: Sets up the external store with dummy messages containing parent IDs
1333
+ - `thread.tsx`: Contains the custom `ParentIdGroup` component and uses `Unstable_PartsGroupedByParentId`
1334
+
1335
+ ## Use Cases
1336
+
1337
+ This pattern is useful for:
1338
+ - Grouping research sources with their related findings
1339
+ - Organizing multi-step tool executions
1340
+ - Creating hierarchical content structures
1341
+ - Showing related content in collapsible sections
1342
+ ```
1343
+
1344
+ ## tsconfig.json
1345
+
1346
+ ```json
1347
+ {
1348
+ "extends": "@assistant-ui/x-buildutils/ts/base",
1349
+ "compilerOptions": {
1350
+ "target": "ES6",
1351
+ "module": "ESNext",
1352
+ "incremental": true,
1353
+ "plugins": [
1354
+ {
1355
+ "name": "next"
1356
+ }
1357
+ ],
1358
+ "allowJs": true,
1359
+ "strictNullChecks": true,
1360
+ "jsx": "preserve",
1361
+ "paths": {
1362
+ "@/*": ["./*"],
1363
+ "@assistant-ui/*": ["../../packages/*/src"],
1364
+ "@assistant-ui/react/*": ["../../packages/react/src/*"],
1365
+ "assistant-stream": ["../../packages/assistant-stream/src"],
1366
+ "assistant-stream/*": ["../../packages/assistant-stream/src/*"]
1367
+ }
1368
+ },
1369
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
1370
+ "exclude": ["node_modules"]
1371
+ }
1372
+
1373
+ ```
1374
+