@assistant-ui/mcp-docs-server 0.1.22 → 0.1.24

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 (75) hide show
  1. package/.docs/organized/code-examples/waterfall.md +801 -0
  2. package/.docs/organized/code-examples/with-ag-ui.md +39 -27
  3. package/.docs/organized/code-examples/with-ai-sdk-v6.md +39 -29
  4. package/.docs/organized/code-examples/with-artifacts.md +467 -0
  5. package/.docs/organized/code-examples/with-assistant-transport.md +32 -25
  6. package/.docs/organized/code-examples/with-chain-of-thought.md +42 -33
  7. package/.docs/organized/code-examples/with-cloud-standalone.md +674 -0
  8. package/.docs/organized/code-examples/with-cloud.md +35 -28
  9. package/.docs/organized/code-examples/with-custom-thread-list.md +35 -28
  10. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +42 -31
  11. package/.docs/organized/code-examples/with-expo.md +2012 -0
  12. package/.docs/organized/code-examples/with-external-store.md +32 -26
  13. package/.docs/organized/code-examples/with-ffmpeg.md +32 -28
  14. package/.docs/organized/code-examples/with-langgraph.md +97 -39
  15. package/.docs/organized/code-examples/with-parent-id-grouping.md +33 -26
  16. package/.docs/organized/code-examples/with-react-hook-form.md +63 -61
  17. package/.docs/organized/code-examples/with-react-router.md +38 -31
  18. package/.docs/organized/code-examples/with-store.md +17 -25
  19. package/.docs/organized/code-examples/with-tanstack.md +36 -26
  20. package/.docs/organized/code-examples/with-tap-runtime.md +11 -25
  21. package/.docs/raw/docs/(docs)/cli.mdx +13 -6
  22. package/.docs/raw/docs/(docs)/guides/attachments.mdx +26 -3
  23. package/.docs/raw/docs/(docs)/guides/chain-of-thought.mdx +5 -5
  24. package/.docs/raw/docs/(docs)/guides/context-api.mdx +53 -52
  25. package/.docs/raw/docs/(docs)/guides/dictation.mdx +0 -2
  26. package/.docs/raw/docs/(docs)/guides/message-timing.mdx +169 -0
  27. package/.docs/raw/docs/(docs)/guides/quoting.mdx +327 -0
  28. package/.docs/raw/docs/(docs)/guides/speech.mdx +0 -1
  29. package/.docs/raw/docs/(docs)/index.mdx +12 -2
  30. package/.docs/raw/docs/(docs)/installation.mdx +8 -2
  31. package/.docs/raw/docs/(docs)/llm.mdx +9 -7
  32. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar-more.mdx +1 -1
  33. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +2 -2
  34. package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +27 -27
  35. package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +60 -0
  36. package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +78 -4
  37. package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +32 -0
  38. package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +61 -0
  39. package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +1 -1
  40. package/.docs/raw/docs/(reference)/legacy/styled/assistant-modal.mdx +1 -6
  41. package/.docs/raw/docs/(reference)/legacy/styled/decomposition.mdx +2 -2
  42. package/.docs/raw/docs/(reference)/legacy/styled/markdown.mdx +1 -6
  43. package/.docs/raw/docs/(reference)/legacy/styled/thread.mdx +1 -5
  44. package/.docs/raw/docs/(reference)/migrations/v0-12.mdx +17 -17
  45. package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +209 -0
  46. package/.docs/raw/docs/cloud/ai-sdk.mdx +296 -0
  47. package/.docs/raw/docs/cloud/authorization.mdx +178 -79
  48. package/.docs/raw/docs/cloud/{persistence/langgraph.mdx → langgraph.mdx} +2 -2
  49. package/.docs/raw/docs/cloud/overview.mdx +29 -39
  50. package/.docs/raw/docs/react-native/adapters.mdx +118 -0
  51. package/.docs/raw/docs/react-native/custom-backend.mdx +210 -0
  52. package/.docs/raw/docs/react-native/hooks.mdx +364 -0
  53. package/.docs/raw/docs/react-native/index.mdx +332 -0
  54. package/.docs/raw/docs/react-native/primitives.mdx +653 -0
  55. package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +60 -15
  56. package/.docs/raw/docs/runtimes/assistant-transport.mdx +103 -0
  57. package/.docs/raw/docs/runtimes/custom/external-store.mdx +25 -2
  58. package/.docs/raw/docs/runtimes/data-stream.mdx +1 -3
  59. package/.docs/raw/docs/runtimes/langgraph/index.mdx +113 -9
  60. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +1 -4
  61. package/.docs/raw/docs/ui/attachment.mdx +4 -2
  62. package/.docs/raw/docs/ui/context-display.mdx +147 -0
  63. package/.docs/raw/docs/ui/message-timing.mdx +92 -0
  64. package/.docs/raw/docs/ui/part-grouping.mdx +1 -1
  65. package/.docs/raw/docs/ui/reasoning.mdx +4 -4
  66. package/.docs/raw/docs/ui/scrollbar.mdx +2 -2
  67. package/.docs/raw/docs/ui/syntax-highlighting.mdx +55 -50
  68. package/.docs/raw/docs/ui/thread.mdx +16 -9
  69. package/dist/index.d.ts +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/package.json +3 -3
  72. package/src/tools/tests/integration.test.ts +2 -2
  73. package/src/tools/tests/json-parsing.test.ts +1 -1
  74. package/src/tools/tests/mcp-protocol.test.ts +1 -3
  75. package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +0 -108
@@ -0,0 +1,674 @@
1
+ # Example: with-cloud-standalone
2
+
3
+ ## app/api/chat/route.ts
4
+
5
+ ```typescript
6
+ import { openai } from "@ai-sdk/openai";
7
+ import { streamText, convertToModelMessages } from "ai";
8
+ import type { UIMessage } from "ai";
9
+
10
+ export const maxDuration = 30;
11
+
12
+ export async function POST(req: Request) {
13
+ const { messages }: { messages: UIMessage[] } = await req.json();
14
+
15
+ const result = streamText({
16
+ model: openai("gpt-4o-mini"),
17
+ messages: await convertToModelMessages(messages),
18
+ });
19
+
20
+ return result.toUIMessageStreamResponse({
21
+ messageMetadata: ({ part }) => {
22
+ if (part.type === "finish-step") {
23
+ return {
24
+ modelId: part.response.modelId,
25
+ usage: part.usage,
26
+ };
27
+ }
28
+ return undefined;
29
+ },
30
+ });
31
+ }
32
+
33
+ ```
34
+
35
+ ## app/globals.css
36
+
37
+ ```css
38
+ @import "tailwindcss";
39
+ @import "tw-animate-css";
40
+
41
+ @custom-variant dark (&:is(.dark *));
42
+
43
+ @theme inline {
44
+ --font-sans: var(--font-geist-sans);
45
+ --font-mono: var(--font-geist-mono);
46
+ --radius-sm: calc(var(--radius) - 4px);
47
+ --radius-md: calc(var(--radius) - 2px);
48
+ --radius-lg: var(--radius);
49
+ --radius-xl: calc(var(--radius) + 4px);
50
+ --color-background: var(--background);
51
+ --color-foreground: var(--foreground);
52
+ --color-card: var(--card);
53
+ --color-card-foreground: var(--card-foreground);
54
+ --color-popover: var(--popover);
55
+ --color-popover-foreground: var(--popover-foreground);
56
+ --color-primary: var(--primary);
57
+ --color-primary-foreground: var(--primary-foreground);
58
+ --color-secondary: var(--secondary);
59
+ --color-secondary-foreground: var(--secondary-foreground);
60
+ --color-muted: var(--muted);
61
+ --color-muted-foreground: var(--muted-foreground);
62
+ --color-accent: var(--accent);
63
+ --color-accent-foreground: var(--accent-foreground);
64
+ --color-destructive: var(--destructive);
65
+ --color-border: var(--border);
66
+ --color-input: var(--input);
67
+ --color-ring: var(--ring);
68
+ --color-sidebar: var(--sidebar);
69
+ --color-sidebar-foreground: var(--sidebar-foreground);
70
+ --color-sidebar-primary: var(--sidebar-primary);
71
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
72
+ --color-sidebar-accent: var(--sidebar-accent);
73
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
74
+ --color-sidebar-border: var(--sidebar-border);
75
+ --color-sidebar-ring: var(--sidebar-ring);
76
+ }
77
+
78
+ :root {
79
+ --radius: 0.625rem;
80
+ --background: oklch(1 0 0);
81
+ --foreground: oklch(0.141 0.005 285.823);
82
+ --card: oklch(1 0 0);
83
+ --card-foreground: oklch(0.141 0.005 285.823);
84
+ --popover: oklch(1 0 0);
85
+ --popover-foreground: oklch(0.141 0.005 285.823);
86
+ --primary: oklch(0.21 0.006 285.885);
87
+ --primary-foreground: oklch(0.985 0 0);
88
+ --secondary: oklch(0.967 0.001 286.375);
89
+ --secondary-foreground: oklch(0.21 0.006 285.885);
90
+ --muted: oklch(0.967 0.001 286.375);
91
+ --muted-foreground: oklch(0.552 0.016 285.938);
92
+ --accent: oklch(0.967 0.001 286.375);
93
+ --accent-foreground: oklch(0.21 0.006 285.885);
94
+ --destructive: oklch(0.577 0.245 27.325);
95
+ --border: oklch(0.92 0.004 286.32);
96
+ --input: oklch(0.92 0.004 286.32);
97
+ --ring: oklch(0.705 0.015 286.067);
98
+ --sidebar: oklch(0.985 0 0);
99
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
100
+ --sidebar-primary: oklch(0.21 0.006 285.885);
101
+ --sidebar-primary-foreground: oklch(0.985 0 0);
102
+ --sidebar-accent: oklch(0.967 0.001 286.375);
103
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
104
+ --sidebar-border: oklch(0.92 0.004 286.32);
105
+ --sidebar-ring: oklch(0.705 0.015 286.067);
106
+ }
107
+
108
+ .dark {
109
+ --background: oklch(0.141 0.005 285.823);
110
+ --foreground: oklch(0.985 0 0);
111
+ --card: oklch(0.21 0.006 285.885);
112
+ --card-foreground: oklch(0.985 0 0);
113
+ --popover: oklch(0.21 0.006 285.885);
114
+ --popover-foreground: oklch(0.985 0 0);
115
+ --primary: oklch(0.92 0.004 286.32);
116
+ --primary-foreground: oklch(0.21 0.006 285.885);
117
+ --secondary: oklch(0.274 0.006 286.033);
118
+ --secondary-foreground: oklch(0.985 0 0);
119
+ --muted: oklch(0.274 0.006 286.033);
120
+ --muted-foreground: oklch(0.705 0.015 286.067);
121
+ --accent: oklch(0.274 0.006 286.033);
122
+ --accent-foreground: oklch(0.985 0 0);
123
+ --destructive: oklch(0.704 0.191 22.216);
124
+ --border: oklch(1 0 0 / 10%);
125
+ --input: oklch(1 0 0 / 15%);
126
+ --ring: oklch(0.552 0.016 285.938);
127
+ --sidebar: oklch(0.21 0.006 285.885);
128
+ --sidebar-foreground: oklch(0.985 0 0);
129
+ --sidebar-primary: oklch(0.488 0.243 264.376);
130
+ --sidebar-primary-foreground: oklch(0.985 0 0);
131
+ --sidebar-accent: oklch(0.274 0.006 286.033);
132
+ --sidebar-accent-foreground: oklch(0.985 0 0);
133
+ --sidebar-border: oklch(1 0 0 / 10%);
134
+ --sidebar-ring: oklch(0.552 0.016 285.938);
135
+ }
136
+
137
+ @layer base {
138
+ * {
139
+ @apply border-border outline-ring/50;
140
+ }
141
+ body {
142
+ @apply bg-background text-foreground;
143
+ }
144
+ }
145
+
146
+ ```
147
+
148
+ ## app/layout.tsx
149
+
150
+ ```tsx
151
+ import type { Metadata } from "next";
152
+ import { Geist, Geist_Mono } from "next/font/google";
153
+ import "./globals.css";
154
+
155
+ const geistSans = Geist({
156
+ variable: "--font-geist-sans",
157
+ subsets: ["latin"],
158
+ });
159
+
160
+ const geistMono = Geist_Mono({
161
+ variable: "--font-geist-mono",
162
+ subsets: ["latin"],
163
+ });
164
+
165
+ export const metadata: Metadata = {
166
+ title: "Cloud AI SDK Example",
167
+ description: "Example using assistant-cloud with AI SDK v6",
168
+ };
169
+
170
+ export default function RootLayout({
171
+ children,
172
+ }: Readonly<{
173
+ children: React.ReactNode;
174
+ }>) {
175
+ return (
176
+ <html lang="en" className="dark">
177
+ <body
178
+ className={`${geistSans.variable} ${geistMono.variable} h-dvh antialiased`}
179
+ >
180
+ {children}
181
+ </body>
182
+ </html>
183
+ );
184
+ }
185
+
186
+ ```
187
+
188
+ ## app/page.client.tsx
189
+
190
+ ```tsx
191
+ "use client";
192
+
193
+ import { useState } from "react";
194
+ import { useCloudChat } from "@assistant-ui/cloud-ai-sdk";
195
+ import { Thread } from "@/components/chat/Thread";
196
+ import { Composer } from "@/components/chat/Composer";
197
+ import { ThreadList } from "@/components/chat/ThreadList";
198
+
199
+ export function ChatPageClient() {
200
+ // Zero-config mode: auto-initializes anonymous cloud from NEXT_PUBLIC_ASSISTANT_BASE_URL.
201
+ // For custom configuration, pass options:
202
+ // - { cloud: myCloud } for authenticated users
203
+ // - { threads: useThreads(...) } for external thread management
204
+ // - { onSyncError: (err) => ... } for error handling
205
+ const { messages, sendMessage, stop, status, threads } = useCloudChat();
206
+
207
+ const [input, setInput] = useState("");
208
+
209
+ const handleSubmit = () => {
210
+ if (!input.trim()) return;
211
+ sendMessage({ text: input });
212
+ setInput("");
213
+ };
214
+
215
+ const isRunning = status === "streaming" || status === "submitted";
216
+ const isLoading = status === "submitted";
217
+
218
+ const handleDelete = async (id: string) => {
219
+ if (threads.threadId === id) threads.selectThread(null);
220
+ await threads.delete(id);
221
+ };
222
+
223
+ return (
224
+ <div className="flex h-full">
225
+ <ThreadList
226
+ threads={threads.threads}
227
+ selectedId={threads.threadId}
228
+ onSelect={threads.selectThread}
229
+ onDelete={handleDelete}
230
+ isLoading={threads.isLoading}
231
+ />
232
+
233
+ <div className="flex min-w-0 flex-1 flex-col">
234
+ <Thread messages={messages} isLoading={isLoading}>
235
+ <Composer
236
+ value={input}
237
+ onChange={setInput}
238
+ onSubmit={handleSubmit}
239
+ isRunning={isRunning}
240
+ onCancel={stop}
241
+ />
242
+ </Thread>
243
+ </div>
244
+ </div>
245
+ );
246
+ }
247
+
248
+ ```
249
+
250
+ ## app/page.tsx
251
+
252
+ ```tsx
253
+ import { ChatPageClient } from "./page.client";
254
+
255
+ // Only needed if NEXT_PUBLIC_ASSISTANT_BASE_URL is unset and no cloud instance is passed to useCloudChat({ cloud }).
256
+ export const dynamic = "force-dynamic";
257
+
258
+ export default function Home() {
259
+ return <ChatPageClient />;
260
+ }
261
+
262
+ ```
263
+
264
+ ## components/chat/Composer.tsx
265
+
266
+ ```tsx
267
+ "use client";
268
+
269
+ import type { FormEvent, KeyboardEvent } from "react";
270
+ import { cn } from "@/lib/utils";
271
+ import { ArrowUp, Square } from "lucide-react";
272
+
273
+ type ComposerProps = {
274
+ value: string;
275
+ onChange: (value: string) => void;
276
+ onSubmit: () => void;
277
+ isRunning: boolean;
278
+ onCancel?: () => void;
279
+ };
280
+
281
+ export function Composer({
282
+ value,
283
+ onChange,
284
+ onSubmit,
285
+ isRunning,
286
+ onCancel,
287
+ }: ComposerProps) {
288
+ const handleSubmit = (e: FormEvent) => {
289
+ e.preventDefault();
290
+ if (value.trim() && !isRunning) {
291
+ onSubmit();
292
+ }
293
+ };
294
+
295
+ const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
296
+ if (e.key === "Enter" && !e.shiftKey) {
297
+ e.preventDefault();
298
+ if (value.trim() && !isRunning) {
299
+ onSubmit();
300
+ }
301
+ }
302
+ };
303
+
304
+ return (
305
+ <form onSubmit={handleSubmit} className="p-4 pt-2">
306
+ <div className="flex items-end gap-2 rounded-2xl border bg-background px-3 py-2 shadow-sm">
307
+ <textarea
308
+ value={value}
309
+ onChange={(e) => onChange(e.target.value)}
310
+ onKeyDown={handleKeyDown}
311
+ placeholder="Send a message..."
312
+ className="max-h-32 min-h-8 flex-1 resize-none bg-transparent py-1 text-sm leading-normal outline-none placeholder:text-muted-foreground"
313
+ rows={1}
314
+ // biome-ignore lint/a11y/noAutofocus: chat input should autofocus
315
+ autoFocus
316
+ />
317
+ {isRunning ? (
318
+ <button
319
+ type="button"
320
+ onClick={onCancel}
321
+ className="flex size-8 shrink-0 items-center justify-center rounded-full bg-destructive text-destructive-foreground transition-colors hover:bg-destructive/90"
322
+ aria-label="Stop generating"
323
+ >
324
+ <Square className="size-3 fill-current" />
325
+ </button>
326
+ ) : (
327
+ <button
328
+ type="submit"
329
+ disabled={!value.trim()}
330
+ className={cn(
331
+ "flex size-8 shrink-0 items-center justify-center rounded-full transition-colors",
332
+ value.trim()
333
+ ? "bg-primary text-primary-foreground hover:bg-primary/90"
334
+ : "bg-muted text-muted-foreground",
335
+ )}
336
+ aria-label="Send message"
337
+ >
338
+ <ArrowUp className="size-4" />
339
+ </button>
340
+ )}
341
+ </div>
342
+ </form>
343
+ );
344
+ }
345
+
346
+ ```
347
+
348
+ ## components/chat/Message.tsx
349
+
350
+ ```tsx
351
+ "use client";
352
+
353
+ import type { UIMessage } from "ai";
354
+ import { cn } from "@/lib/utils";
355
+
356
+ type MessageProps = {
357
+ message: UIMessage;
358
+ };
359
+
360
+ export function Message({ message }: MessageProps) {
361
+ const isUser = message.role === "user";
362
+
363
+ return (
364
+ <div className={cn("flex", isUser ? "justify-end" : "justify-start")}>
365
+ <div
366
+ className={cn(
367
+ "max-w-[80%] whitespace-pre-wrap break-words rounded-2xl px-4 py-2.5 text-sm",
368
+ isUser
369
+ ? "bg-primary text-primary-foreground"
370
+ : "bg-muted text-foreground",
371
+ )}
372
+ >
373
+ {message.parts.map((part, i) => {
374
+ if (part.type === "text") {
375
+ return <p key={i}>{part.text}</p>;
376
+ }
377
+ return null;
378
+ })}
379
+ </div>
380
+ </div>
381
+ );
382
+ }
383
+
384
+ ```
385
+
386
+ ## components/chat/Thread.tsx
387
+
388
+ ```tsx
389
+ "use client";
390
+
391
+ import type { UIMessage } from "ai";
392
+ import { Message } from "./Message";
393
+
394
+ type ThreadProps = {
395
+ messages: UIMessage[];
396
+ isLoading: boolean;
397
+ children?: React.ReactNode;
398
+ };
399
+
400
+ function ThreadWelcome() {
401
+ return (
402
+ <div className="flex flex-1 flex-col items-center justify-center text-center">
403
+ <h2 className="font-semibold text-lg">How can I help you today?</h2>
404
+ <p className="mt-1 text-muted-foreground text-sm">
405
+ Send a message to start a conversation.
406
+ </p>
407
+ </div>
408
+ );
409
+ }
410
+
411
+ function LoadingIndicator() {
412
+ return (
413
+ <div className="flex items-center gap-1.5 py-2">
414
+ <span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:-0.3s]" />
415
+ <span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:-0.15s]" />
416
+ <span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/60" />
417
+ </div>
418
+ );
419
+ }
420
+
421
+ export function Thread({ messages, isLoading, children }: ThreadProps) {
422
+ return (
423
+ <div className="flex h-full flex-col">
424
+ <div className="flex flex-1 flex-col overflow-y-auto px-4 py-6">
425
+ {messages.length === 0 ? (
426
+ <ThreadWelcome />
427
+ ) : (
428
+ <div className="mx-auto w-full max-w-3xl space-y-4">
429
+ {messages.map((msg) => (
430
+ <Message key={msg.id} message={msg} />
431
+ ))}
432
+ {isLoading && <LoadingIndicator />}
433
+ </div>
434
+ )}
435
+ </div>
436
+ <div className="mx-auto w-full max-w-3xl">{children}</div>
437
+ </div>
438
+ );
439
+ }
440
+
441
+ ```
442
+
443
+ ## components/chat/ThreadList.tsx
444
+
445
+ ```tsx
446
+ "use client";
447
+
448
+ import type { CloudThread } from "@assistant-ui/cloud-ai-sdk";
449
+ import { cn } from "@/lib/utils";
450
+ import { Plus, Trash2, MessageSquare } from "lucide-react";
451
+
452
+ type ThreadListProps = {
453
+ threads: CloudThread[];
454
+ selectedId: string | null;
455
+ onSelect: (id: string | null) => void;
456
+ onDelete: (id: string) => void;
457
+ isLoading: boolean;
458
+ };
459
+
460
+ export function ThreadList({
461
+ threads,
462
+ selectedId,
463
+ onSelect,
464
+ onDelete,
465
+ isLoading,
466
+ }: ThreadListProps) {
467
+ return (
468
+ <div className="flex h-full w-64 shrink-0 flex-col border-r bg-sidebar">
469
+ <div className="p-3">
470
+ <button
471
+ onClick={() => onSelect(null)}
472
+ className="flex w-full items-center justify-center gap-2 rounded-lg border border-sidebar-border bg-sidebar px-4 py-2 font-medium text-sidebar-foreground text-sm transition-colors hover:bg-sidebar-accent"
473
+ >
474
+ <Plus className="size-4" />
475
+ New Chat
476
+ </button>
477
+ </div>
478
+
479
+ <div className="flex-1 overflow-y-auto px-2 pb-2">
480
+ {isLoading && threads.length === 0 ? (
481
+ <div className="flex items-center justify-center py-8 text-muted-foreground text-sm">
482
+ Loading...
483
+ </div>
484
+ ) : threads.length === 0 ? (
485
+ <div className="flex flex-col items-center justify-center gap-2 py-8 text-center text-muted-foreground text-sm">
486
+ <MessageSquare className="size-6 opacity-40" />
487
+ <p>No conversations yet</p>
488
+ </div>
489
+ ) : (
490
+ <div className="space-y-0.5">
491
+ {threads.map((thread) => (
492
+ <div
493
+ key={thread.id}
494
+ onClick={() => onSelect(thread.id)}
495
+ className={cn(
496
+ "group flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm transition-colors",
497
+ selectedId === thread.id
498
+ ? "bg-sidebar-accent text-sidebar-accent-foreground"
499
+ : "text-sidebar-foreground hover:bg-sidebar-accent/50",
500
+ )}
501
+ >
502
+ <span className="flex-1 truncate">
503
+ {thread.title || "New conversation"}
504
+ </span>
505
+ <button
506
+ onClick={(e) => {
507
+ e.stopPropagation();
508
+ onDelete(thread.id);
509
+ }}
510
+ className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
511
+ aria-label="Delete thread"
512
+ >
513
+ <Trash2 className="size-3.5 text-muted-foreground hover:text-destructive" />
514
+ </button>
515
+ </div>
516
+ ))}
517
+ </div>
518
+ )}
519
+ </div>
520
+ </div>
521
+ );
522
+ }
523
+
524
+ ```
525
+
526
+ ## lib/utils.ts
527
+
528
+ ```typescript
529
+ import { clsx, type ClassValue } from "clsx";
530
+ import { twMerge } from "tailwind-merge";
531
+
532
+ export function cn(...inputs: ClassValue[]) {
533
+ return twMerge(clsx(inputs));
534
+ }
535
+
536
+ ```
537
+
538
+ ## next.config.js
539
+
540
+ ```javascript
541
+ /** @type {import('next').NextConfig} */
542
+ const nextConfig = {
543
+ transpilePackages: ["assistant-cloud"],
544
+ };
545
+
546
+ export default nextConfig;
547
+
548
+ ```
549
+
550
+ ## package.json
551
+
552
+ ```json
553
+ {
554
+ "name": "with-cloud-standalone",
555
+ "version": "0.0.0",
556
+ "private": true,
557
+ "type": "module",
558
+ "scripts": {
559
+ "dev": "next dev",
560
+ "build": "next build",
561
+ "start": "next start"
562
+ },
563
+ "dependencies": {
564
+ "@ai-sdk/openai": "^3.0.39",
565
+ "@assistant-ui/cloud-ai-sdk": "workspace:*",
566
+ "ai": "^6.0.111",
567
+ "assistant-cloud": "workspace:*",
568
+ "class-variance-authority": "^0.7.1",
569
+ "clsx": "^2.1.1",
570
+ "lucide-react": "^0.576.0",
571
+ "next": "^16.1.6",
572
+ "react": "^19.2.4",
573
+ "react-dom": "^19.2.4",
574
+ "tailwind-merge": "^3.5.0"
575
+ },
576
+ "devDependencies": {
577
+ "@assistant-ui/x-buildutils": "workspace:*",
578
+ "@tailwindcss/postcss": "^4.2.1",
579
+ "@types/node": "^25.3.3",
580
+ "@types/react": "^19.2.14",
581
+ "@types/react-dom": "^19.2.3",
582
+ "postcss": "^8.5.8",
583
+ "tailwindcss": "^4.2.1",
584
+ "tw-animate-css": "^1.4.0",
585
+ "typescript": "^5.9.3"
586
+ }
587
+ }
588
+
589
+ ```
590
+
591
+ ## README.md
592
+
593
+ ```markdown
594
+ # Cloud Persistence for AI SDK (Standalone)
595
+
596
+ Lightweight cloud persistence for AI SDK apps without assistant-ui components.
597
+
598
+ > **Want the full assistant-ui experience?** See [with-cloud](../with-cloud) instead, which uses `useChatRuntime` with `<Thread />` and other primitives.
599
+
600
+ ## Setup
601
+
602
+ 1. Get your project URL from [cloud.assistant-ui.com](https://cloud.assistant-ui.com)
603
+ 2. Add to `.env`:
604
+ ```
605
+ NEXT_PUBLIC_ASSISTANT_BASE_URL=https://your-project.assistant-api.com
606
+ OPENAI_API_KEY=sk-...
607
+ ```
608
+
609
+ ## Usage
610
+
611
+ ### Zero-config (anonymous users)
612
+
613
+ ```tsx
614
+ import { useCloudChat } from "@assistant-ui/cloud-ai-sdk";
615
+
616
+ function Chat() {
617
+ // Auto-initializes anonymous cloud from NEXT_PUBLIC_ASSISTANT_BASE_URL
618
+ const { messages, sendMessage, threads } = useCloudChat();
619
+ // ...
620
+ }
621
+ ```
622
+
623
+ ### With custom cloud instance (authenticated users)
624
+
625
+ ```tsx
626
+ import { AssistantCloud } from "assistant-cloud";
627
+ import { useCloudChat } from "@assistant-ui/cloud-ai-sdk";
628
+
629
+ function Chat() {
630
+ const cloud = useMemo(() => new AssistantCloud({
631
+ baseUrl: process.env.NEXT_PUBLIC_ASSISTANT_BASE_URL!,
632
+ authToken: async () => getToken(),
633
+ }), [getToken]);
634
+
635
+ const { messages, sendMessage, threads } = useCloudChat({ cloud });
636
+ // ...
637
+ }
638
+ ```
639
+
640
+ ### With external thread management
641
+
642
+ When you need thread operations in a separate component (e.g., a sidebar) or custom options like `includeArchived`:
643
+
644
+ ```tsx
645
+ // In parent or context
646
+ const myThreads = useThreads({ cloud, includeArchived: true });
647
+
648
+ // In chat component - uses your thread state
649
+ const { messages, sendMessage } = useCloudChat({ threads: myThreads });
650
+
651
+ // In sidebar component - same thread state
652
+ <ThreadList threads={myThreads.threads} onSelect={myThreads.selectThread} />
653
+ ```
654
+
655
+ ## How it works
656
+
657
+ Messages persist automatically as they complete. Thread creation is handled automatically when you send the first message — the thread is created, selected, and the list is refreshed. Call `threads.selectThread(id)` to switch threads, `threads.selectThread(null)` for a new chat.
658
+
659
+ ```
660
+
661
+ ## tsconfig.json
662
+
663
+ ```json
664
+ {
665
+ "extends": "@assistant-ui/x-buildutils/ts/next",
666
+ "compilerOptions": {
667
+ "paths": { "@/*": ["./*"] }
668
+ },
669
+ "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
670
+ "exclude": ["node_modules"]
671
+ }
672
+
673
+ ```
674
+