@assistant-ui/mcp-docs-server 0.1.21 → 0.1.23
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.
- package/.docs/organized/code-examples/waterfall.md +801 -0
- package/.docs/organized/code-examples/with-ag-ui.md +38 -26
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +38 -28
- package/.docs/organized/code-examples/with-artifacts.md +467 -0
- package/.docs/organized/code-examples/with-assistant-transport.md +31 -24
- package/.docs/organized/code-examples/with-chain-of-thought.md +607 -0
- package/.docs/organized/code-examples/with-cloud-standalone.md +675 -0
- package/.docs/organized/code-examples/with-cloud.md +34 -27
- package/.docs/organized/code-examples/with-custom-thread-list.md +34 -27
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +41 -30
- package/.docs/organized/code-examples/with-expo.md +2031 -0
- package/.docs/organized/code-examples/with-external-store.md +32 -25
- package/.docs/organized/code-examples/with-ffmpeg.md +31 -27
- package/.docs/organized/code-examples/with-langgraph.md +96 -38
- package/.docs/organized/code-examples/with-parent-id-grouping.md +32 -25
- package/.docs/organized/code-examples/with-react-hook-form.md +63 -58
- package/.docs/organized/code-examples/with-react-router.md +38 -30
- package/.docs/organized/code-examples/with-store.md +16 -24
- package/.docs/organized/code-examples/with-tanstack.md +36 -26
- package/.docs/organized/code-examples/with-tap-runtime.md +10 -24
- package/.docs/raw/docs/(docs)/cli.mdx +13 -6
- package/.docs/raw/docs/(docs)/guides/attachments.mdx +26 -3
- package/.docs/raw/docs/(docs)/guides/chain-of-thought.mdx +162 -0
- package/.docs/raw/docs/(docs)/guides/context-api.mdx +53 -52
- package/.docs/raw/docs/(docs)/guides/dictation.mdx +0 -2
- package/.docs/raw/docs/(docs)/guides/message-timing.mdx +169 -0
- package/.docs/raw/docs/(docs)/guides/quoting.mdx +327 -0
- package/.docs/raw/docs/(docs)/guides/speech.mdx +0 -1
- package/.docs/raw/docs/(docs)/index.mdx +13 -3
- package/.docs/raw/docs/(docs)/installation.mdx +8 -2
- package/.docs/raw/docs/(docs)/llm.mdx +10 -8
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar-more.mdx +1 -1
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +2 -2
- package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +27 -27
- package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +60 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +78 -4
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +32 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +61 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +1 -1
- package/.docs/raw/docs/(reference)/legacy/styled/assistant-modal.mdx +1 -6
- package/.docs/raw/docs/(reference)/legacy/styled/decomposition.mdx +2 -2
- package/.docs/raw/docs/(reference)/legacy/styled/markdown.mdx +1 -6
- package/.docs/raw/docs/(reference)/legacy/styled/thread.mdx +1 -5
- package/.docs/raw/docs/(reference)/migrations/v0-12.mdx +17 -17
- package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +205 -0
- package/.docs/raw/docs/cloud/ai-sdk.mdx +292 -0
- package/.docs/raw/docs/cloud/authorization.mdx +178 -79
- package/.docs/raw/docs/cloud/{persistence/langgraph.mdx → langgraph.mdx} +2 -2
- package/.docs/raw/docs/cloud/overview.mdx +29 -39
- package/.docs/raw/docs/react-native/adapters.mdx +118 -0
- package/.docs/raw/docs/react-native/custom-backend.mdx +210 -0
- package/.docs/raw/docs/react-native/hooks.mdx +364 -0
- package/.docs/raw/docs/react-native/index.mdx +332 -0
- package/.docs/raw/docs/react-native/primitives.mdx +653 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +7 -15
- package/.docs/raw/docs/runtimes/assistant-transport.mdx +103 -0
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +25 -2
- package/.docs/raw/docs/runtimes/data-stream.mdx +1 -3
- package/.docs/raw/docs/runtimes/langgraph/index.mdx +113 -9
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +1 -4
- package/.docs/raw/docs/ui/attachment.mdx +4 -2
- package/.docs/raw/docs/ui/message-timing.mdx +92 -0
- package/.docs/raw/docs/ui/part-grouping.mdx +1 -1
- package/.docs/raw/docs/ui/reasoning.mdx +4 -4
- package/.docs/raw/docs/ui/scrollbar.mdx +2 -2
- package/.docs/raw/docs/ui/syntax-highlighting.mdx +55 -50
- package/.docs/raw/docs/ui/thread.mdx +16 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/tools/tests/integration.test.ts +2 -2
- package/src/tools/tests/json-parsing.test.ts +1 -1
- package/src/tools/tests/mcp-protocol.test.ts +1 -3
- package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +0 -108
|
@@ -0,0 +1,675 @@
|
|
|
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.33",
|
|
565
|
+
"@ai-sdk/react": "^3.0.100",
|
|
566
|
+
"ai": "^6.0.98",
|
|
567
|
+
"@assistant-ui/cloud-ai-sdk": "workspace:*",
|
|
568
|
+
"assistant-cloud": "workspace:*",
|
|
569
|
+
"class-variance-authority": "^0.7.1",
|
|
570
|
+
"clsx": "^2.1.1",
|
|
571
|
+
"lucide-react": "^0.575.0",
|
|
572
|
+
"next": "^16.1.6",
|
|
573
|
+
"react": "^19.2.4",
|
|
574
|
+
"react-dom": "^19.2.4",
|
|
575
|
+
"tailwind-merge": "^3.5.0"
|
|
576
|
+
},
|
|
577
|
+
"devDependencies": {
|
|
578
|
+
"@assistant-ui/x-buildutils": "workspace:*",
|
|
579
|
+
"@tailwindcss/postcss": "^4.2.1",
|
|
580
|
+
"@types/node": "^25.3.0",
|
|
581
|
+
"@types/react": "^19.2.14",
|
|
582
|
+
"@types/react-dom": "^19.2.3",
|
|
583
|
+
"postcss": "^8.5.6",
|
|
584
|
+
"tailwindcss": "^4.2.1",
|
|
585
|
+
"tw-animate-css": "^1.4.0",
|
|
586
|
+
"typescript": "^5.9.3"
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
## README.md
|
|
593
|
+
|
|
594
|
+
```markdown
|
|
595
|
+
# Cloud Persistence for AI SDK (Standalone)
|
|
596
|
+
|
|
597
|
+
Lightweight cloud persistence for AI SDK apps without assistant-ui components.
|
|
598
|
+
|
|
599
|
+
> **Want the full assistant-ui experience?** See [with-cloud](../with-cloud) instead, which uses `useChatRuntime` with `<Thread />` and other primitives.
|
|
600
|
+
|
|
601
|
+
## Setup
|
|
602
|
+
|
|
603
|
+
1. Get your project URL from [cloud.assistant-ui.com](https://cloud.assistant-ui.com)
|
|
604
|
+
2. Add to `.env`:
|
|
605
|
+
```
|
|
606
|
+
NEXT_PUBLIC_ASSISTANT_BASE_URL=https://your-project.assistant-api.com
|
|
607
|
+
OPENAI_API_KEY=sk-...
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
## Usage
|
|
611
|
+
|
|
612
|
+
### Zero-config (anonymous users)
|
|
613
|
+
|
|
614
|
+
```tsx
|
|
615
|
+
import { useCloudChat } from "@assistant-ui/cloud-ai-sdk";
|
|
616
|
+
|
|
617
|
+
function Chat() {
|
|
618
|
+
// Auto-initializes anonymous cloud from NEXT_PUBLIC_ASSISTANT_BASE_URL
|
|
619
|
+
const { messages, sendMessage, threads } = useCloudChat();
|
|
620
|
+
// ...
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### With custom cloud instance (authenticated users)
|
|
625
|
+
|
|
626
|
+
```tsx
|
|
627
|
+
import { AssistantCloud } from "assistant-cloud";
|
|
628
|
+
import { useCloudChat } from "@assistant-ui/cloud-ai-sdk";
|
|
629
|
+
|
|
630
|
+
function Chat() {
|
|
631
|
+
const cloud = useMemo(() => new AssistantCloud({
|
|
632
|
+
baseUrl: process.env.NEXT_PUBLIC_ASSISTANT_BASE_URL!,
|
|
633
|
+
authToken: async () => getToken(),
|
|
634
|
+
}), [getToken]);
|
|
635
|
+
|
|
636
|
+
const { messages, sendMessage, threads } = useCloudChat({ cloud });
|
|
637
|
+
// ...
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### With external thread management
|
|
642
|
+
|
|
643
|
+
When you need thread operations in a separate component (e.g., a sidebar) or custom options like `includeArchived`:
|
|
644
|
+
|
|
645
|
+
```tsx
|
|
646
|
+
// In parent or context
|
|
647
|
+
const myThreads = useThreads({ cloud, includeArchived: true });
|
|
648
|
+
|
|
649
|
+
// In chat component - uses your thread state
|
|
650
|
+
const { messages, sendMessage } = useCloudChat({ threads: myThreads });
|
|
651
|
+
|
|
652
|
+
// In sidebar component - same thread state
|
|
653
|
+
<ThreadList threads={myThreads.threads} onSelect={myThreads.selectThread} />
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
## How it works
|
|
657
|
+
|
|
658
|
+
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.
|
|
659
|
+
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
## tsconfig.json
|
|
663
|
+
|
|
664
|
+
```json
|
|
665
|
+
{
|
|
666
|
+
"extends": "@assistant-ui/x-buildutils/ts/next",
|
|
667
|
+
"compilerOptions": {
|
|
668
|
+
"paths": { "@/*": ["./*"] }
|
|
669
|
+
},
|
|
670
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
671
|
+
"exclude": ["node_modules"]
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
```
|
|
675
|
+
|