@assistant-ui/mcp-docs-server 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.docs/organized/code-examples/local-ollama.md +1135 -0
- package/.docs/organized/code-examples/search-agent-for-e-commerce.md +1721 -0
- package/.docs/organized/code-examples/with-ai-sdk.md +1081 -0
- package/.docs/organized/code-examples/with-cloud.md +1164 -0
- package/.docs/organized/code-examples/with-external-store.md +1064 -0
- package/.docs/organized/code-examples/with-ffmpeg.md +1305 -0
- package/.docs/organized/code-examples/with-langgraph.md +1819 -0
- package/.docs/organized/code-examples/with-openai-assistants.md +1175 -0
- package/.docs/organized/code-examples/with-react-hook-form.md +1727 -0
- package/.docs/organized/code-examples/with-vercel-ai-rsc.md +1157 -0
- package/.docs/raw/blog/2024-07-29-hello/index.mdx +65 -0
- package/.docs/raw/blog/2024-09-11/index.mdx +10 -0
- package/.docs/raw/blog/2024-12-15/index.mdx +10 -0
- package/.docs/raw/blog/2025-01-31-changelog/index.mdx +129 -0
- package/.docs/raw/docs/about-assistantui.mdx +44 -0
- package/.docs/raw/docs/api-reference/context-providers/AssistantRuntimeProvider.mdx +30 -0
- package/.docs/raw/docs/api-reference/context-providers/TextContentPartProvider.mdx +26 -0
- package/.docs/raw/docs/api-reference/integrations/react-hook-form.mdx +103 -0
- package/.docs/raw/docs/api-reference/integrations/vercel-ai-sdk.mdx +145 -0
- package/.docs/raw/docs/api-reference/overview.mdx +583 -0
- package/.docs/raw/docs/api-reference/primitives/ActionBar.mdx +264 -0
- package/.docs/raw/docs/api-reference/primitives/AssistantModal.mdx +129 -0
- package/.docs/raw/docs/api-reference/primitives/Attachment.mdx +96 -0
- package/.docs/raw/docs/api-reference/primitives/BranchPicker.mdx +87 -0
- package/.docs/raw/docs/api-reference/primitives/Composer.mdx +204 -0
- package/.docs/raw/docs/api-reference/primitives/ContentPart.mdx +173 -0
- package/.docs/raw/docs/api-reference/primitives/Error.mdx +70 -0
- package/.docs/raw/docs/api-reference/primitives/Message.mdx +181 -0
- package/.docs/raw/docs/api-reference/primitives/Thread.mdx +197 -0
- package/.docs/raw/docs/api-reference/primitives/composition.mdx +21 -0
- package/.docs/raw/docs/api-reference/runtimes/AssistantRuntime.mdx +33 -0
- package/.docs/raw/docs/api-reference/runtimes/AttachmentRuntime.mdx +46 -0
- package/.docs/raw/docs/api-reference/runtimes/ComposerRuntime.mdx +69 -0
- package/.docs/raw/docs/api-reference/runtimes/ContentPartRuntime.mdx +22 -0
- package/.docs/raw/docs/api-reference/runtimes/MessageRuntime.mdx +49 -0
- package/.docs/raw/docs/api-reference/runtimes/ThreadListItemRuntime.mdx +32 -0
- package/.docs/raw/docs/api-reference/runtimes/ThreadListRuntime.mdx +31 -0
- package/.docs/raw/docs/api-reference/runtimes/ThreadRuntime.mdx +48 -0
- package/.docs/raw/docs/architecture.mdx +92 -0
- package/.docs/raw/docs/cloud/authorization.mdx +152 -0
- package/.docs/raw/docs/cloud/overview.mdx +55 -0
- package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +54 -0
- package/.docs/raw/docs/cloud/persistence/langgraph.mdx +123 -0
- package/.docs/raw/docs/concepts/architecture.mdx +19 -0
- package/.docs/raw/docs/concepts/runtime-layer.mdx +163 -0
- package/.docs/raw/docs/concepts/why.mdx +9 -0
- package/.docs/raw/docs/copilots/make-assistant-readable.mdx +71 -0
- package/.docs/raw/docs/copilots/make-assistant-tool-ui.mdx +76 -0
- package/.docs/raw/docs/copilots/make-assistant-tool.mdx +117 -0
- package/.docs/raw/docs/copilots/model-context.mdx +135 -0
- package/.docs/raw/docs/copilots/motivation.mdx +191 -0
- package/.docs/raw/docs/copilots/use-assistant-instructions.mdx +62 -0
- package/.docs/raw/docs/getting-started.mdx +1133 -0
- package/.docs/raw/docs/guides/Attachments.mdx +640 -0
- package/.docs/raw/docs/guides/Branching.mdx +59 -0
- package/.docs/raw/docs/guides/Editing.mdx +56 -0
- package/.docs/raw/docs/guides/Speech.mdx +43 -0
- package/.docs/raw/docs/guides/ToolUI.mdx +663 -0
- package/.docs/raw/docs/guides/Tools.mdx +496 -0
- package/.docs/raw/docs/index.mdx +7 -0
- package/.docs/raw/docs/legacy/styled/AssistantModal.mdx +85 -0
- package/.docs/raw/docs/legacy/styled/Decomposition.mdx +633 -0
- package/.docs/raw/docs/legacy/styled/Markdown.mdx +86 -0
- package/.docs/raw/docs/legacy/styled/Scrollbar.mdx +71 -0
- package/.docs/raw/docs/legacy/styled/Thread.mdx +84 -0
- package/.docs/raw/docs/legacy/styled/ThreadWidth.mdx +21 -0
- package/.docs/raw/docs/mcp-docs-server.mdx +324 -0
- package/.docs/raw/docs/migrations/deprecation-policy.mdx +41 -0
- package/.docs/raw/docs/migrations/v0-7.mdx +188 -0
- package/.docs/raw/docs/migrations/v0-8.mdx +160 -0
- package/.docs/raw/docs/migrations/v0-9.mdx +75 -0
- package/.docs/raw/docs/react-compatibility.mdx +208 -0
- package/.docs/raw/docs/runtimes/ai-sdk/rsc.mdx +226 -0
- package/.docs/raw/docs/runtimes/ai-sdk/use-assistant-hook.mdx +195 -0
- package/.docs/raw/docs/runtimes/ai-sdk/use-chat-hook.mdx +138 -0
- package/.docs/raw/docs/runtimes/ai-sdk/use-chat.mdx +136 -0
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +1624 -0
- package/.docs/raw/docs/runtimes/custom/local.mdx +1185 -0
- package/.docs/raw/docs/runtimes/helicone.mdx +60 -0
- package/.docs/raw/docs/runtimes/langgraph/index.mdx +320 -0
- package/.docs/raw/docs/runtimes/langgraph/tutorial/index.mdx +11 -0
- package/.docs/raw/docs/runtimes/langgraph/tutorial/introduction.mdx +28 -0
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +120 -0
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +336 -0
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +385 -0
- package/.docs/raw/docs/runtimes/langserve.mdx +126 -0
- package/.docs/raw/docs/runtimes/mastra/full-stack-integration.mdx +218 -0
- package/.docs/raw/docs/runtimes/mastra/overview.mdx +17 -0
- package/.docs/raw/docs/runtimes/mastra/separate-server-integration.mdx +196 -0
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +222 -0
- package/.docs/raw/docs/ui/AssistantModal.mdx +46 -0
- package/.docs/raw/docs/ui/AssistantSidebar.mdx +42 -0
- package/.docs/raw/docs/ui/Attachment.mdx +82 -0
- package/.docs/raw/docs/ui/Markdown.mdx +72 -0
- package/.docs/raw/docs/ui/Mermaid.mdx +79 -0
- package/.docs/raw/docs/ui/Scrollbar.mdx +59 -0
- package/.docs/raw/docs/ui/SyntaxHighlighting.mdx +253 -0
- package/.docs/raw/docs/ui/Thread.mdx +47 -0
- package/.docs/raw/docs/ui/ThreadList.mdx +49 -0
- package/.docs/raw/docs/ui/ToolFallback.mdx +64 -0
- package/.docs/raw/docs/ui/primitives/Thread.mdx +197 -0
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/chunk-C7O7EFKU.js +38 -0
- package/dist/chunk-CZCDQ3YH.js +420 -0
- package/dist/index.js +1 -0
- package/dist/prepare-docs/prepare.js +199 -0
- package/dist/stdio.js +8 -0
- package/package.json +43 -0
|
@@ -0,0 +1,1624 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ExternalStoreRuntime
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
import { Callout } from "fumadocs-ui/components/callout";
|
|
6
|
+
import { Steps, Step } from "fumadocs-ui/components/steps";
|
|
7
|
+
import { Card, Cards } from "fumadocs-ui/components/card";
|
|
8
|
+
import { ParametersTable } from "@/components/docs";
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
`ExternalStoreRuntime` bridges your existing state management with assistant-ui components. It requires an `ExternalStoreAdapter<TMessage>` that handles communication between your state and the UI.
|
|
13
|
+
|
|
14
|
+
**Key differences from `LocalRuntime`:**
|
|
15
|
+
|
|
16
|
+
- **You own the state** - Full control over message state, thread management, and persistence logic
|
|
17
|
+
- **Bring your own state management** - Works with Redux, Zustand, TanStack Query, or any React state library
|
|
18
|
+
- **Custom message formats** - Use your backend's message structure with automatic conversion
|
|
19
|
+
|
|
20
|
+
<Callout type="warn">
|
|
21
|
+
`ExternalStoreRuntime` gives you total control over state (persist, sync,
|
|
22
|
+
share), but you must wire up every callback.
|
|
23
|
+
</Callout>
|
|
24
|
+
|
|
25
|
+
## Example Implementation
|
|
26
|
+
|
|
27
|
+
```tsx twoslash title="app/MyRuntimeProvider.tsx"
|
|
28
|
+
type MyMessage = {
|
|
29
|
+
role: "user" | "assistant";
|
|
30
|
+
content: string;
|
|
31
|
+
};
|
|
32
|
+
const backendApi = async (input: string): Promise<MyMessage> => {
|
|
33
|
+
return { role: "assistant", content: "Hello, world!" };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ---cut---
|
|
37
|
+
import { useState, ReactNode } from "react";
|
|
38
|
+
import {
|
|
39
|
+
useExternalStoreRuntime,
|
|
40
|
+
ThreadMessageLike,
|
|
41
|
+
AppendMessage,
|
|
42
|
+
AssistantRuntimeProvider,
|
|
43
|
+
} from "@assistant-ui/react";
|
|
44
|
+
|
|
45
|
+
const convertMessage = (message: MyMessage): ThreadMessageLike => {
|
|
46
|
+
return {
|
|
47
|
+
role: message.role,
|
|
48
|
+
content: [{ type: "text", text: message.content }],
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function MyRuntimeProvider({
|
|
53
|
+
children,
|
|
54
|
+
}: Readonly<{
|
|
55
|
+
children: ReactNode;
|
|
56
|
+
}>) {
|
|
57
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
58
|
+
const [messages, setMessages] = useState<MyMessage[]>([]);
|
|
59
|
+
|
|
60
|
+
const onNew = async (message: AppendMessage) => {
|
|
61
|
+
if (message.content[0]?.type !== "text")
|
|
62
|
+
throw new Error("Only text messages are supported");
|
|
63
|
+
|
|
64
|
+
const input = message.content[0].text;
|
|
65
|
+
setMessages((currentConversation) => [
|
|
66
|
+
...currentConversation,
|
|
67
|
+
{ role: "user", content: input },
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
setIsRunning(true);
|
|
71
|
+
const assistantMessage = await backendApi(input);
|
|
72
|
+
setMessages((currentConversation) => [
|
|
73
|
+
...currentConversation,
|
|
74
|
+
assistantMessage,
|
|
75
|
+
]);
|
|
76
|
+
setIsRunning(false);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const runtime = useExternalStoreRuntime({
|
|
80
|
+
isRunning,
|
|
81
|
+
messages,
|
|
82
|
+
convertMessage,
|
|
83
|
+
onNew,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
88
|
+
{children}
|
|
89
|
+
</AssistantRuntimeProvider>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## When to Use
|
|
95
|
+
|
|
96
|
+
Use `ExternalStoreRuntime` if you need:
|
|
97
|
+
|
|
98
|
+
- **Full control over message state** - Manage messages with Redux, Zustand, TanStack Query, or any React state management library
|
|
99
|
+
- **Custom multi-thread implementation** - Build your own thread management system with custom storage
|
|
100
|
+
- **Integration with existing state** - Keep chat state in your existing state management solution
|
|
101
|
+
- **Custom message formats** - Use your backend's message structure with automatic conversion
|
|
102
|
+
- **Complex synchronization** - Sync messages with external data sources, databases, or multiple clients
|
|
103
|
+
- **Custom persistence logic** - Implement your own storage patterns and caching strategies
|
|
104
|
+
|
|
105
|
+
## Key Features
|
|
106
|
+
|
|
107
|
+
<Cards>
|
|
108
|
+
<Card
|
|
109
|
+
title="State Management Integration"
|
|
110
|
+
description="Works seamlessly with Redux, Zustand, TanStack Query, and more"
|
|
111
|
+
/>
|
|
112
|
+
<Card
|
|
113
|
+
title="Message Conversion"
|
|
114
|
+
description="Automatic conversion between your message format and assistant-ui's format"
|
|
115
|
+
/>
|
|
116
|
+
<Card
|
|
117
|
+
title="Real-time Streaming"
|
|
118
|
+
description="Built-in support for streaming responses and progressive updates"
|
|
119
|
+
/>
|
|
120
|
+
<Card
|
|
121
|
+
title="Thread Management"
|
|
122
|
+
description="Multi-conversation support with archiving and thread switching"
|
|
123
|
+
/>
|
|
124
|
+
</Cards>
|
|
125
|
+
|
|
126
|
+
## Architecture
|
|
127
|
+
|
|
128
|
+
### How It Works
|
|
129
|
+
|
|
130
|
+
`ExternalStoreRuntime` acts as a bridge between your state management and assistant-ui:
|
|
131
|
+
|
|
132
|
+
```mermaid
|
|
133
|
+
graph TD
|
|
134
|
+
A[Your State Management] -->|messages| B[ExternalStoreAdapter]
|
|
135
|
+
B --> C[ExternalStoreRuntime]
|
|
136
|
+
C --> D[assistant-ui Components]
|
|
137
|
+
D -->|user actions| B
|
|
138
|
+
B -->|state updates| A
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Key Concepts
|
|
142
|
+
|
|
143
|
+
1. **State Ownership** - You own and control all message state
|
|
144
|
+
2. **Adapter Pattern** - The adapter translates between your state and assistant-ui
|
|
145
|
+
3. **Capability-Based Features** - UI features are enabled based on which handlers you provide
|
|
146
|
+
4. **Message Conversion** - Automatic conversion between your message format and assistant-ui's format
|
|
147
|
+
5. **Optimistic Updates** - Built-in handling for streaming and loading states
|
|
148
|
+
|
|
149
|
+
## Getting Started
|
|
150
|
+
|
|
151
|
+
<Steps>
|
|
152
|
+
<Step>
|
|
153
|
+
### Install Dependencies
|
|
154
|
+
|
|
155
|
+
```sh npm2yarn
|
|
156
|
+
npm install @assistant-ui/react
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
</Step>
|
|
160
|
+
|
|
161
|
+
<Step>
|
|
162
|
+
### Create Runtime Provider
|
|
163
|
+
|
|
164
|
+
```tsx title="app/MyRuntimeProvider.tsx"
|
|
165
|
+
"use client";
|
|
166
|
+
|
|
167
|
+
import { useState } from "react";
|
|
168
|
+
import {
|
|
169
|
+
useExternalStoreRuntime,
|
|
170
|
+
ThreadMessageLike,
|
|
171
|
+
AppendMessage,
|
|
172
|
+
AssistantRuntimeProvider,
|
|
173
|
+
} from "@assistant-ui/react";
|
|
174
|
+
|
|
175
|
+
export function MyRuntimeProvider({ children }) {
|
|
176
|
+
const [messages, setMessages] = useState<ThreadMessageLike[]>([]);
|
|
177
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
178
|
+
|
|
179
|
+
const onNew = async (message: AppendMessage) => {
|
|
180
|
+
// Add user message
|
|
181
|
+
const userMessage: ThreadMessageLike = {
|
|
182
|
+
role: "user",
|
|
183
|
+
content: message.content,
|
|
184
|
+
};
|
|
185
|
+
setMessages(prev => [...prev, userMessage]);
|
|
186
|
+
|
|
187
|
+
// Generate response
|
|
188
|
+
setIsRunning(true);
|
|
189
|
+
const response = await callYourAPI(message);
|
|
190
|
+
|
|
191
|
+
const assistantMessage: ThreadMessageLike = {
|
|
192
|
+
role: "assistant",
|
|
193
|
+
content: response.content,
|
|
194
|
+
};
|
|
195
|
+
setMessages(prev => [...prev, assistantMessage]);
|
|
196
|
+
setIsRunning(false);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const runtime = useExternalStoreRuntime({
|
|
200
|
+
messages,
|
|
201
|
+
setMessages,
|
|
202
|
+
isRunning,
|
|
203
|
+
onNew,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
208
|
+
{children}
|
|
209
|
+
</AssistantRuntimeProvider>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
</Step>
|
|
215
|
+
|
|
216
|
+
<Step>
|
|
217
|
+
### Use in Your App
|
|
218
|
+
|
|
219
|
+
```tsx title="app/page.tsx"
|
|
220
|
+
import { Thread } from "@assistant-ui/react";
|
|
221
|
+
import { MyRuntimeProvider } from "./MyRuntimeProvider";
|
|
222
|
+
|
|
223
|
+
export default function Page() {
|
|
224
|
+
return (
|
|
225
|
+
<MyRuntimeProvider>
|
|
226
|
+
<Thread />
|
|
227
|
+
</MyRuntimeProvider>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
</Step>
|
|
233
|
+
</Steps>
|
|
234
|
+
|
|
235
|
+
## Implementation Patterns
|
|
236
|
+
|
|
237
|
+
### Message Conversion
|
|
238
|
+
|
|
239
|
+
Two approaches for converting your message format:
|
|
240
|
+
|
|
241
|
+
#### 1. Simple Conversion (Recommended)
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
const convertMessage = (message: MyMessage): ThreadMessageLike => ({
|
|
245
|
+
role: message.role,
|
|
246
|
+
content: [{ type: "text", text: message.text }],
|
|
247
|
+
id: message.id,
|
|
248
|
+
createdAt: new Date(message.timestamp),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const runtime = useExternalStoreRuntime({
|
|
252
|
+
messages: myMessages,
|
|
253
|
+
convertMessage,
|
|
254
|
+
onNew,
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### 2. Advanced Conversion with `useExternalMessageConverter`
|
|
259
|
+
|
|
260
|
+
For complex scenarios with performance optimization:
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
import { useExternalMessageConverter } from "@assistant-ui/react";
|
|
264
|
+
|
|
265
|
+
const convertedMessages = useExternalMessageConverter({
|
|
266
|
+
messages,
|
|
267
|
+
convertMessage: (message: MyMessage): ThreadMessageLike => ({
|
|
268
|
+
role: message.role,
|
|
269
|
+
content: [{ type: "text", text: message.text }],
|
|
270
|
+
id: message.id,
|
|
271
|
+
createdAt: new Date(message.timestamp),
|
|
272
|
+
}),
|
|
273
|
+
joinStrategy: "concat-content", // Merge adjacent assistant messages
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const runtime = useExternalStoreRuntime({
|
|
277
|
+
messages: convertedMessages,
|
|
278
|
+
onNew,
|
|
279
|
+
// No convertMessage needed - already converted
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Join Strategy
|
|
284
|
+
|
|
285
|
+
Controls how adjacent assistant messages are combined:
|
|
286
|
+
|
|
287
|
+
- **`concat-content`** (default): Merges adjacent assistant messages into one
|
|
288
|
+
- **`none`**: Keeps all messages separate
|
|
289
|
+
|
|
290
|
+
This is useful when your backend sends multiple message chunks that should appear as a single message in the UI.
|
|
291
|
+
|
|
292
|
+
<Callout type="info">
|
|
293
|
+
`useExternalMessageConverter` provides performance optimization for complex
|
|
294
|
+
message conversion scenarios. For simpler cases, consider using the basic
|
|
295
|
+
`convertMessage` approach shown above.
|
|
296
|
+
</Callout>
|
|
297
|
+
|
|
298
|
+
### Essential Handlers
|
|
299
|
+
|
|
300
|
+
#### Basic Chat (onNew only)
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
const runtime = useExternalStoreRuntime({
|
|
304
|
+
messages,
|
|
305
|
+
onNew: async (message) => {
|
|
306
|
+
// Add user message to state
|
|
307
|
+
const userMsg = { role: "user", content: message.content };
|
|
308
|
+
setMessages([...messages, userMsg]);
|
|
309
|
+
|
|
310
|
+
// Get AI response
|
|
311
|
+
const response = await callAI(message);
|
|
312
|
+
setMessages([...messages, userMsg, response]);
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### Full-Featured Chat
|
|
318
|
+
|
|
319
|
+
```tsx
|
|
320
|
+
const runtime = useExternalStoreRuntime({
|
|
321
|
+
messages,
|
|
322
|
+
setMessages, // Enables branch switching
|
|
323
|
+
onNew, // Required
|
|
324
|
+
onEdit, // Enables message editing
|
|
325
|
+
onReload, // Enables regeneration
|
|
326
|
+
onCancel, // Enables cancellation
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
<Callout type="info">
|
|
331
|
+
Each handler you provide enables specific UI features: - `setMessages` →
|
|
332
|
+
Branch switching - `onEdit` → Message editing - `onReload` → Regenerate button
|
|
333
|
+
- `onCancel` → Cancel button during generation
|
|
334
|
+
</Callout>
|
|
335
|
+
|
|
336
|
+
### Streaming Responses
|
|
337
|
+
|
|
338
|
+
Implement real-time streaming with progressive updates:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
const onNew = async (message: AppendMessage) => {
|
|
342
|
+
// Add user message
|
|
343
|
+
const userMessage: ThreadMessageLike = {
|
|
344
|
+
role: "user",
|
|
345
|
+
content: message.content,
|
|
346
|
+
id: generateId(),
|
|
347
|
+
};
|
|
348
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
349
|
+
|
|
350
|
+
// Create placeholder for assistant message
|
|
351
|
+
setIsRunning(true);
|
|
352
|
+
const assistantId = generateId();
|
|
353
|
+
const assistantMessage: ThreadMessageLike = {
|
|
354
|
+
role: "assistant",
|
|
355
|
+
content: [{ type: "text", text: "" }],
|
|
356
|
+
id: assistantId,
|
|
357
|
+
};
|
|
358
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
359
|
+
|
|
360
|
+
// Stream response
|
|
361
|
+
const stream = await api.streamChat(message);
|
|
362
|
+
for await (const chunk of stream) {
|
|
363
|
+
setMessages((prev) =>
|
|
364
|
+
prev.map((m) =>
|
|
365
|
+
m.id === assistantId
|
|
366
|
+
? {
|
|
367
|
+
...m,
|
|
368
|
+
content: [
|
|
369
|
+
{
|
|
370
|
+
type: "text",
|
|
371
|
+
text: (m.content[0] as any).text + chunk,
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
}
|
|
375
|
+
: m,
|
|
376
|
+
),
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
setIsRunning(false);
|
|
380
|
+
};
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Message Editing
|
|
384
|
+
|
|
385
|
+
Enable message editing by implementing the `onEdit` handler:
|
|
386
|
+
|
|
387
|
+
<Callout type="info">
|
|
388
|
+
You can also implement `onEdit(editedMessage)` and `onRemove(messageId)`
|
|
389
|
+
callbacks to handle user-initiated edits or deletions in your external store.
|
|
390
|
+
This enables features like "edit and re-run" on your backend.
|
|
391
|
+
</Callout>
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
const onEdit = async (message: AppendMessage) => {
|
|
395
|
+
// Find the index where to insert the edited message
|
|
396
|
+
const index = messages.findIndex((m) => m.id === message.parentId) + 1;
|
|
397
|
+
|
|
398
|
+
// Keep messages up to the parent
|
|
399
|
+
const newMessages = [...messages.slice(0, index)];
|
|
400
|
+
|
|
401
|
+
// Add the edited message
|
|
402
|
+
const editedMessage: ThreadMessageLike = {
|
|
403
|
+
role: "user",
|
|
404
|
+
content: message.content,
|
|
405
|
+
id: message.id || generateId(),
|
|
406
|
+
};
|
|
407
|
+
newMessages.push(editedMessage);
|
|
408
|
+
|
|
409
|
+
setMessages(newMessages);
|
|
410
|
+
|
|
411
|
+
// Generate new response
|
|
412
|
+
setIsRunning(true);
|
|
413
|
+
const response = await api.chat(message);
|
|
414
|
+
newMessages.push({
|
|
415
|
+
role: "assistant",
|
|
416
|
+
content: response.content,
|
|
417
|
+
id: generateId(),
|
|
418
|
+
});
|
|
419
|
+
setMessages(newMessages);
|
|
420
|
+
setIsRunning(false);
|
|
421
|
+
};
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Tool Calling
|
|
425
|
+
|
|
426
|
+
Support tool calls with proper result handling:
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
const onAddToolResult = (options: AddToolResultOptions) => {
|
|
430
|
+
setMessages((prev) =>
|
|
431
|
+
prev.map((message) => {
|
|
432
|
+
if (message.id === options.messageId) {
|
|
433
|
+
// Update the specific tool call with its result
|
|
434
|
+
return {
|
|
435
|
+
...message,
|
|
436
|
+
content: message.content.map((part) => {
|
|
437
|
+
if (
|
|
438
|
+
part.type === "tool-call" &&
|
|
439
|
+
part.toolCallId === options.toolCallId
|
|
440
|
+
) {
|
|
441
|
+
return {
|
|
442
|
+
...part,
|
|
443
|
+
result: options.result,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return part;
|
|
447
|
+
}),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return message;
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const runtime = useExternalStoreRuntime({
|
|
456
|
+
messages,
|
|
457
|
+
onNew,
|
|
458
|
+
onAddToolResult,
|
|
459
|
+
// ... other props
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
#### Automatic Tool Result Matching
|
|
464
|
+
|
|
465
|
+
The runtime automatically matches tool results with their corresponding tool calls. When messages are converted and joined:
|
|
466
|
+
|
|
467
|
+
1. **Tool Call Tracking** - The runtime tracks tool calls by their `toolCallId`
|
|
468
|
+
2. **Result Association** - Tool results are automatically associated with their corresponding calls
|
|
469
|
+
3. **Message Grouping** - Related tool messages are intelligently grouped together
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
// Example: Tool call and result in separate messages
|
|
473
|
+
const messages = [
|
|
474
|
+
{
|
|
475
|
+
role: "assistant",
|
|
476
|
+
content: [
|
|
477
|
+
{
|
|
478
|
+
type: "tool-call",
|
|
479
|
+
toolCallId: "call_123",
|
|
480
|
+
toolName: "get_weather",
|
|
481
|
+
args: { location: "San Francisco" },
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
role: "tool",
|
|
487
|
+
content: [
|
|
488
|
+
{
|
|
489
|
+
type: "tool-result",
|
|
490
|
+
toolCallId: "call_123",
|
|
491
|
+
result: { temperature: 72, condition: "sunny" },
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
},
|
|
495
|
+
];
|
|
496
|
+
|
|
497
|
+
// These are automatically matched and grouped by the runtime
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### File Attachments
|
|
501
|
+
|
|
502
|
+
Enable file uploads with the attachment adapter:
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
const attachmentAdapter: AttachmentAdapter = {
|
|
506
|
+
accept: "image/*,application/pdf,.txt,.md",
|
|
507
|
+
async add(file) {
|
|
508
|
+
// Upload file to your server
|
|
509
|
+
const formData = new FormData();
|
|
510
|
+
formData.append("file", file);
|
|
511
|
+
|
|
512
|
+
const response = await fetch("/api/upload", {
|
|
513
|
+
method: "POST",
|
|
514
|
+
body: formData,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
const { id, url } = await response.json();
|
|
518
|
+
return {
|
|
519
|
+
id,
|
|
520
|
+
type: "document",
|
|
521
|
+
name: file.name,
|
|
522
|
+
file,
|
|
523
|
+
url,
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
async remove(attachment) {
|
|
527
|
+
// Remove file from server
|
|
528
|
+
await fetch(`/api/upload/${attachment.id}`, {
|
|
529
|
+
method: "DELETE",
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const runtime = useExternalStoreRuntime({
|
|
535
|
+
messages,
|
|
536
|
+
onNew,
|
|
537
|
+
adapters: {
|
|
538
|
+
attachments: attachmentAdapter,
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Thread Management
|
|
544
|
+
|
|
545
|
+
#### Managing Thread Context
|
|
546
|
+
|
|
547
|
+
When implementing multi-thread support with `ExternalStoreRuntime`, you need to carefully manage thread context across your application. Here's a comprehensive approach:
|
|
548
|
+
|
|
549
|
+
```tsx
|
|
550
|
+
// Create a context for thread management
|
|
551
|
+
const ThreadContext = createContext<{
|
|
552
|
+
currentThreadId: string;
|
|
553
|
+
setCurrentThreadId: (id: string) => void;
|
|
554
|
+
threads: Map<string, ThreadMessageLike[]>;
|
|
555
|
+
setThreads: React.Dispatch<
|
|
556
|
+
React.SetStateAction<Map<string, ThreadMessageLike[]>>
|
|
557
|
+
>;
|
|
558
|
+
}>({
|
|
559
|
+
currentThreadId: "default",
|
|
560
|
+
setCurrentThreadId: () => {},
|
|
561
|
+
threads: new Map(),
|
|
562
|
+
setThreads: () => {},
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Thread provider component
|
|
566
|
+
export function ThreadProvider({ children }: { children: ReactNode }) {
|
|
567
|
+
const [threads, setThreads] = useState<Map<string, ThreadMessageLike[]>>(
|
|
568
|
+
new Map([["default", []]]),
|
|
569
|
+
);
|
|
570
|
+
const [currentThreadId, setCurrentThreadId] = useState("default");
|
|
571
|
+
|
|
572
|
+
return (
|
|
573
|
+
<ThreadContext.Provider
|
|
574
|
+
value={{ currentThreadId, setCurrentThreadId, threads, setThreads }}
|
|
575
|
+
>
|
|
576
|
+
{children}
|
|
577
|
+
</ThreadContext.Provider>
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Hook for accessing thread context
|
|
582
|
+
export function useThreadContext() {
|
|
583
|
+
const context = useContext(ThreadContext);
|
|
584
|
+
if (!context) {
|
|
585
|
+
throw new Error("useThreadContext must be used within ThreadProvider");
|
|
586
|
+
}
|
|
587
|
+
return context;
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
#### Complete Thread Implementation
|
|
592
|
+
|
|
593
|
+
Here's a full implementation with proper context management:
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
function ChatWithThreads() {
|
|
597
|
+
const { currentThreadId, setCurrentThreadId, threads, setThreads } =
|
|
598
|
+
useThreadContext();
|
|
599
|
+
const [threadList, setThreadList] = useState<ExternalStoreThreadData[]>([
|
|
600
|
+
{ threadId: "default", status: "regular", title: "New Chat" },
|
|
601
|
+
]);
|
|
602
|
+
|
|
603
|
+
// Get messages for current thread
|
|
604
|
+
const currentMessages = threads.get(currentThreadId) || [];
|
|
605
|
+
|
|
606
|
+
const threadListAdapter: ExternalStoreThreadListAdapter = {
|
|
607
|
+
threadId: currentThreadId,
|
|
608
|
+
threads: threadList.filter((t) => t.status === "regular"),
|
|
609
|
+
archivedThreads: threadList.filter((t) => t.status === "archived"),
|
|
610
|
+
|
|
611
|
+
onSwitchToNewThread: () => {
|
|
612
|
+
const newId = `thread-${Date.now()}`;
|
|
613
|
+
setThreadList((prev) => [
|
|
614
|
+
...prev,
|
|
615
|
+
{
|
|
616
|
+
threadId: newId,
|
|
617
|
+
status: "regular",
|
|
618
|
+
title: "New Chat",
|
|
619
|
+
},
|
|
620
|
+
]);
|
|
621
|
+
setThreads((prev) => new Map(prev).set(newId, []));
|
|
622
|
+
setCurrentThreadId(newId);
|
|
623
|
+
},
|
|
624
|
+
|
|
625
|
+
onSwitchToThread: (threadId) => {
|
|
626
|
+
setCurrentThreadId(threadId);
|
|
627
|
+
},
|
|
628
|
+
|
|
629
|
+
onRename: (threadId, newTitle) => {
|
|
630
|
+
setThreadList((prev) =>
|
|
631
|
+
prev.map((t) =>
|
|
632
|
+
t.threadId === threadId ? { ...t, title: newTitle } : t,
|
|
633
|
+
),
|
|
634
|
+
);
|
|
635
|
+
},
|
|
636
|
+
|
|
637
|
+
onArchive: (threadId) => {
|
|
638
|
+
setThreadList((prev) =>
|
|
639
|
+
prev.map((t) =>
|
|
640
|
+
t.threadId === threadId ? { ...t, status: "archived" } : t,
|
|
641
|
+
),
|
|
642
|
+
);
|
|
643
|
+
},
|
|
644
|
+
|
|
645
|
+
onDelete: (threadId) => {
|
|
646
|
+
setThreadList((prev) => prev.filter((t) => t.threadId !== threadId));
|
|
647
|
+
setThreads((prev) => {
|
|
648
|
+
const next = new Map(prev);
|
|
649
|
+
next.delete(threadId);
|
|
650
|
+
return next;
|
|
651
|
+
});
|
|
652
|
+
if (currentThreadId === threadId) {
|
|
653
|
+
setCurrentThreadId("default");
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const runtime = useExternalStoreRuntime({
|
|
659
|
+
messages: currentMessages,
|
|
660
|
+
setMessages: (messages) => {
|
|
661
|
+
setThreads((prev) => new Map(prev).set(currentThreadId, messages));
|
|
662
|
+
},
|
|
663
|
+
onNew: async (message) => {
|
|
664
|
+
// Handle new message for current thread
|
|
665
|
+
// Your implementation here
|
|
666
|
+
},
|
|
667
|
+
adapters: {
|
|
668
|
+
threadList: threadListAdapter,
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
return (
|
|
673
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
674
|
+
<ThreadList />
|
|
675
|
+
<Thread />
|
|
676
|
+
</AssistantRuntimeProvider>
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// App component with proper context wrapping
|
|
681
|
+
export function App() {
|
|
682
|
+
return (
|
|
683
|
+
<ThreadProvider>
|
|
684
|
+
<ChatWithThreads />
|
|
685
|
+
</ThreadProvider>
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
#### Thread Context Best Practices
|
|
691
|
+
|
|
692
|
+
<Callout type="info">
|
|
693
|
+
**Critical**: When using `ExternalStoreRuntime` with threads, the
|
|
694
|
+
`currentThreadId` must be consistent across all components and handlers.
|
|
695
|
+
Mismatched thread IDs will cause messages to appear in wrong threads or
|
|
696
|
+
disappear entirely.
|
|
697
|
+
</Callout>
|
|
698
|
+
|
|
699
|
+
1. **Centralize Thread State**: Always use a context or global state management solution to ensure thread ID consistency:
|
|
700
|
+
|
|
701
|
+
```tsx
|
|
702
|
+
// ❌ Bad: Local state in multiple components
|
|
703
|
+
function ThreadList() {
|
|
704
|
+
const [currentThreadId, setCurrentThreadId] = useState("default");
|
|
705
|
+
// This won't sync with the runtime!
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ✅ Good: Shared context
|
|
709
|
+
function ThreadList() {
|
|
710
|
+
const { currentThreadId, setCurrentThreadId } = useThreadContext();
|
|
711
|
+
// Thread ID is synchronized everywhere
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
2. **Sync Thread Changes**: Ensure all thread-related operations update both the thread ID and messages:
|
|
716
|
+
|
|
717
|
+
```tsx
|
|
718
|
+
// ❌ Bad: Only updating thread ID
|
|
719
|
+
onSwitchToThread: (threadId) => {
|
|
720
|
+
setCurrentThreadId(threadId);
|
|
721
|
+
// Messages won't update!
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// ✅ Good: Complete state update
|
|
725
|
+
onSwitchToThread: (threadId) => {
|
|
726
|
+
setCurrentThreadId(threadId);
|
|
727
|
+
// Messages automatically update via currentMessages = threads.get(currentThreadId)
|
|
728
|
+
};
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
3. **Handle Edge Cases**: Always provide fallbacks for missing threads:
|
|
732
|
+
|
|
733
|
+
```tsx
|
|
734
|
+
// Ensure thread always exists
|
|
735
|
+
const currentMessages = threads.get(currentThreadId) || [];
|
|
736
|
+
|
|
737
|
+
// Initialize new threads properly
|
|
738
|
+
const initializeThread = (threadId: string) => {
|
|
739
|
+
if (!threads.has(threadId)) {
|
|
740
|
+
setThreads((prev) => new Map(prev).set(threadId, []));
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
4. **Persist Thread State**: For production apps, sync thread state with your backend:
|
|
746
|
+
|
|
747
|
+
```tsx
|
|
748
|
+
// Save thread state to backend
|
|
749
|
+
useEffect(() => {
|
|
750
|
+
const saveThread = async () => {
|
|
751
|
+
await api.saveThread(currentThreadId, threads.get(currentThreadId) || []);
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
const debounced = debounce(saveThread, 1000);
|
|
755
|
+
debounced();
|
|
756
|
+
|
|
757
|
+
return () => debounced.cancel();
|
|
758
|
+
}, [currentThreadId, threads]);
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
## Integration Examples
|
|
762
|
+
|
|
763
|
+
### Redux Integration
|
|
764
|
+
|
|
765
|
+
```tsx title="app/chatSlice.ts"
|
|
766
|
+
// Using Redux Toolkit (recommended)
|
|
767
|
+
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
|
768
|
+
import { ThreadMessageLike } from "@assistant-ui/react";
|
|
769
|
+
|
|
770
|
+
interface ChatState {
|
|
771
|
+
messages: ThreadMessageLike[];
|
|
772
|
+
isRunning: boolean;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const chatSlice = createSlice({
|
|
776
|
+
name: "chat",
|
|
777
|
+
initialState: {
|
|
778
|
+
messages: [] as ThreadMessageLike[],
|
|
779
|
+
isRunning: false,
|
|
780
|
+
},
|
|
781
|
+
reducers: {
|
|
782
|
+
setMessages: (state, action: PayloadAction<ThreadMessageLike[]>) => {
|
|
783
|
+
state.messages = action.payload;
|
|
784
|
+
},
|
|
785
|
+
addMessage: (state, action: PayloadAction<ThreadMessageLike>) => {
|
|
786
|
+
state.messages.push(action.payload);
|
|
787
|
+
},
|
|
788
|
+
setIsRunning: (state, action: PayloadAction<boolean>) => {
|
|
789
|
+
state.isRunning = action.payload;
|
|
790
|
+
},
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
export const { setMessages, addMessage, setIsRunning } = chatSlice.actions;
|
|
795
|
+
export const selectMessages = (state: RootState) => state.chat.messages;
|
|
796
|
+
export const selectIsRunning = (state: RootState) => state.chat.isRunning;
|
|
797
|
+
export default chatSlice.reducer;
|
|
798
|
+
|
|
799
|
+
// ReduxRuntimeProvider.tsx
|
|
800
|
+
import { useSelector, useDispatch } from "react-redux";
|
|
801
|
+
import {
|
|
802
|
+
selectMessages,
|
|
803
|
+
selectIsRunning,
|
|
804
|
+
addMessage,
|
|
805
|
+
setMessages,
|
|
806
|
+
setIsRunning,
|
|
807
|
+
} from "./chatSlice";
|
|
808
|
+
|
|
809
|
+
export function ReduxRuntimeProvider({ children }) {
|
|
810
|
+
const messages = useSelector(selectMessages);
|
|
811
|
+
const isRunning = useSelector(selectIsRunning);
|
|
812
|
+
const dispatch = useDispatch();
|
|
813
|
+
|
|
814
|
+
const runtime = useExternalStoreRuntime({
|
|
815
|
+
messages,
|
|
816
|
+
isRunning,
|
|
817
|
+
setMessages: (messages) => dispatch(setMessages(messages)),
|
|
818
|
+
onNew: async (message) => {
|
|
819
|
+
// Add user message
|
|
820
|
+
dispatch(
|
|
821
|
+
addMessage({
|
|
822
|
+
role: "user",
|
|
823
|
+
content: message.content,
|
|
824
|
+
id: `msg-${Date.now()}`,
|
|
825
|
+
createdAt: new Date(),
|
|
826
|
+
}),
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
// Generate response
|
|
830
|
+
dispatch(setIsRunning(true));
|
|
831
|
+
const response = await api.chat(message);
|
|
832
|
+
dispatch(
|
|
833
|
+
addMessage({
|
|
834
|
+
role: "assistant",
|
|
835
|
+
content: response.content,
|
|
836
|
+
id: `msg-${Date.now()}`,
|
|
837
|
+
createdAt: new Date(),
|
|
838
|
+
}),
|
|
839
|
+
);
|
|
840
|
+
dispatch(setIsRunning(false));
|
|
841
|
+
},
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
return (
|
|
845
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
846
|
+
{children}
|
|
847
|
+
</AssistantRuntimeProvider>
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### Zustand Integration (v5)
|
|
853
|
+
|
|
854
|
+
```tsx title="app/chatStore.ts"
|
|
855
|
+
// Using Zustand v5 with TypeScript
|
|
856
|
+
import { create } from "zustand";
|
|
857
|
+
import { immer } from "zustand/middleware/immer";
|
|
858
|
+
import { ThreadMessageLike } from "@assistant-ui/react";
|
|
859
|
+
|
|
860
|
+
interface ChatState {
|
|
861
|
+
messages: ThreadMessageLike[];
|
|
862
|
+
isRunning: boolean;
|
|
863
|
+
addMessage: (message: ThreadMessageLike) => void;
|
|
864
|
+
setMessages: (messages: ThreadMessageLike[]) => void;
|
|
865
|
+
setIsRunning: (isRunning: boolean) => void;
|
|
866
|
+
updateMessage: (id: string, updates: Partial<ThreadMessageLike>) => void;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Zustand v5 requires the extra parentheses for TypeScript
|
|
870
|
+
const useChatStore = create<ChatState>()(
|
|
871
|
+
immer((set) => ({
|
|
872
|
+
messages: [],
|
|
873
|
+
isRunning: false,
|
|
874
|
+
|
|
875
|
+
addMessage: (message) =>
|
|
876
|
+
set((state) => {
|
|
877
|
+
state.messages.push(message);
|
|
878
|
+
}),
|
|
879
|
+
|
|
880
|
+
setMessages: (messages) =>
|
|
881
|
+
set((state) => {
|
|
882
|
+
state.messages = messages;
|
|
883
|
+
}),
|
|
884
|
+
|
|
885
|
+
setIsRunning: (isRunning) =>
|
|
886
|
+
set((state) => {
|
|
887
|
+
state.isRunning = isRunning;
|
|
888
|
+
}),
|
|
889
|
+
|
|
890
|
+
updateMessage: (id, updates) =>
|
|
891
|
+
set((state) => {
|
|
892
|
+
const index = state.messages.findIndex((m) => m.id === id);
|
|
893
|
+
if (index !== -1) {
|
|
894
|
+
Object.assign(state.messages[index], updates);
|
|
895
|
+
}
|
|
896
|
+
}),
|
|
897
|
+
})),
|
|
898
|
+
);
|
|
899
|
+
|
|
900
|
+
// ZustandRuntimeProvider.tsx
|
|
901
|
+
import { useShallow } from "zustand/shallow";
|
|
902
|
+
|
|
903
|
+
export function ZustandRuntimeProvider({ children }) {
|
|
904
|
+
// Use useShallow to prevent unnecessary re-renders
|
|
905
|
+
const { messages, isRunning, addMessage, setMessages, setIsRunning } =
|
|
906
|
+
useChatStore(
|
|
907
|
+
useShallow((state) => ({
|
|
908
|
+
messages: state.messages,
|
|
909
|
+
isRunning: state.isRunning,
|
|
910
|
+
addMessage: state.addMessage,
|
|
911
|
+
setMessages: state.setMessages,
|
|
912
|
+
setIsRunning: state.setIsRunning,
|
|
913
|
+
})),
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
const runtime = useExternalStoreRuntime({
|
|
917
|
+
messages,
|
|
918
|
+
isRunning,
|
|
919
|
+
setMessages,
|
|
920
|
+
onNew: async (message) => {
|
|
921
|
+
// Add user message
|
|
922
|
+
addMessage({
|
|
923
|
+
role: "user",
|
|
924
|
+
content: message.content,
|
|
925
|
+
id: `msg-${Date.now()}`,
|
|
926
|
+
createdAt: new Date(),
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Generate response
|
|
930
|
+
setIsRunning(true);
|
|
931
|
+
const response = await api.chat(message);
|
|
932
|
+
addMessage({
|
|
933
|
+
role: "assistant",
|
|
934
|
+
content: response.content,
|
|
935
|
+
id: `msg-${Date.now()}-assistant`,
|
|
936
|
+
createdAt: new Date(),
|
|
937
|
+
});
|
|
938
|
+
setIsRunning(false);
|
|
939
|
+
},
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
return (
|
|
943
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
944
|
+
{children}
|
|
945
|
+
</AssistantRuntimeProvider>
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
### TanStack Query Integration
|
|
951
|
+
|
|
952
|
+
```tsx title="app/chatQueries.ts"
|
|
953
|
+
// Using TanStack Query v5 with TypeScript
|
|
954
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
955
|
+
import { ThreadMessageLike, AppendMessage } from "@assistant-ui/react";
|
|
956
|
+
|
|
957
|
+
// Query key factory pattern
|
|
958
|
+
export const messageKeys = {
|
|
959
|
+
all: ["messages"] as const,
|
|
960
|
+
thread: (threadId: string) => [...messageKeys.all, threadId] as const,
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
// TanStackQueryRuntimeProvider.tsx
|
|
964
|
+
export function TanStackQueryRuntimeProvider({ children }) {
|
|
965
|
+
const queryClient = useQueryClient();
|
|
966
|
+
const threadId = "main"; // Or from context/props
|
|
967
|
+
|
|
968
|
+
const { data: messages = [] } = useQuery({
|
|
969
|
+
queryKey: messageKeys.thread(threadId),
|
|
970
|
+
queryFn: () => fetchMessages(threadId),
|
|
971
|
+
staleTime: 1000 * 60 * 5, // Consider data fresh for 5 minutes
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
const sendMessage = useMutation({
|
|
975
|
+
mutationFn: api.chat,
|
|
976
|
+
|
|
977
|
+
// Optimistic updates with proper TypeScript types
|
|
978
|
+
onMutate: async (message: AppendMessage) => {
|
|
979
|
+
// Cancel any outgoing refetches
|
|
980
|
+
await queryClient.cancelQueries({
|
|
981
|
+
queryKey: messageKeys.thread(threadId),
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
// Snapshot the previous value
|
|
985
|
+
const previousMessages = queryClient.getQueryData<ThreadMessageLike[]>(
|
|
986
|
+
messageKeys.thread(threadId),
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
// Optimistically update with typed data
|
|
990
|
+
const optimisticMessage: ThreadMessageLike = {
|
|
991
|
+
role: "user",
|
|
992
|
+
content: message.content,
|
|
993
|
+
id: `temp-${Date.now()}`,
|
|
994
|
+
createdAt: new Date(),
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
queryClient.setQueryData<ThreadMessageLike[]>(
|
|
998
|
+
messageKeys.thread(threadId),
|
|
999
|
+
(old = []) => [...old, optimisticMessage],
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
return { previousMessages, tempId: optimisticMessage.id };
|
|
1003
|
+
},
|
|
1004
|
+
|
|
1005
|
+
onSuccess: (response, variables, context) => {
|
|
1006
|
+
// Replace optimistic message with real data
|
|
1007
|
+
queryClient.setQueryData<ThreadMessageLike[]>(
|
|
1008
|
+
messageKeys.thread(threadId),
|
|
1009
|
+
(old = []) => {
|
|
1010
|
+
// Remove temp message and add real ones
|
|
1011
|
+
return old
|
|
1012
|
+
.filter((m) => m.id !== context?.tempId)
|
|
1013
|
+
.concat([
|
|
1014
|
+
{
|
|
1015
|
+
role: "user",
|
|
1016
|
+
content: variables.content,
|
|
1017
|
+
id: `user-${Date.now()}`,
|
|
1018
|
+
createdAt: new Date(),
|
|
1019
|
+
},
|
|
1020
|
+
response,
|
|
1021
|
+
]);
|
|
1022
|
+
},
|
|
1023
|
+
);
|
|
1024
|
+
},
|
|
1025
|
+
|
|
1026
|
+
onError: (error, variables, context) => {
|
|
1027
|
+
// Rollback to previous messages on error
|
|
1028
|
+
if (context?.previousMessages) {
|
|
1029
|
+
queryClient.setQueryData(
|
|
1030
|
+
messageKeys.thread(threadId),
|
|
1031
|
+
context.previousMessages,
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
|
|
1036
|
+
onSettled: () => {
|
|
1037
|
+
// Always refetch after error or success
|
|
1038
|
+
queryClient.invalidateQueries({
|
|
1039
|
+
queryKey: messageKeys.thread(threadId),
|
|
1040
|
+
});
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const runtime = useExternalStoreRuntime({
|
|
1045
|
+
messages,
|
|
1046
|
+
isRunning: sendMessage.isPending,
|
|
1047
|
+
onNew: async (message) => {
|
|
1048
|
+
await sendMessage.mutateAsync(message);
|
|
1049
|
+
},
|
|
1050
|
+
// Enable message editing
|
|
1051
|
+
setMessages: (newMessages) => {
|
|
1052
|
+
queryClient.setQueryData(messageKeys.thread(threadId), newMessages);
|
|
1053
|
+
},
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
return (
|
|
1057
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
1058
|
+
{children}
|
|
1059
|
+
</AssistantRuntimeProvider>
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
## Key Features
|
|
1065
|
+
|
|
1066
|
+
### Automatic Optimistic Updates
|
|
1067
|
+
|
|
1068
|
+
When `isRunning` becomes true, the runtime automatically shows an optimistic assistant message:
|
|
1069
|
+
|
|
1070
|
+
```tsx
|
|
1071
|
+
// Your code
|
|
1072
|
+
setIsRunning(true);
|
|
1073
|
+
|
|
1074
|
+
// Runtime automatically:
|
|
1075
|
+
// 1. Shows empty assistant message with "in_progress" status
|
|
1076
|
+
// 2. Displays typing indicator
|
|
1077
|
+
// 3. Updates status to "complete" when isRunning becomes false
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
### Message Status Management
|
|
1081
|
+
|
|
1082
|
+
Assistant messages get automatic status updates:
|
|
1083
|
+
|
|
1084
|
+
- `"in_progress"` - When `isRunning` is true
|
|
1085
|
+
- `"complete"` - When `isRunning` becomes false
|
|
1086
|
+
- `"cancelled"` - When cancelled via `onCancel`
|
|
1087
|
+
|
|
1088
|
+
### Tool Result Matching
|
|
1089
|
+
|
|
1090
|
+
The runtime automatically matches tool results with their calls:
|
|
1091
|
+
|
|
1092
|
+
```tsx
|
|
1093
|
+
// Tool call and result can be in separate messages
|
|
1094
|
+
const messages = [
|
|
1095
|
+
{
|
|
1096
|
+
role: "assistant",
|
|
1097
|
+
content: [
|
|
1098
|
+
{
|
|
1099
|
+
type: "tool-call",
|
|
1100
|
+
toolCallId: "call_123",
|
|
1101
|
+
toolName: "get_weather",
|
|
1102
|
+
args: { location: "SF" },
|
|
1103
|
+
},
|
|
1104
|
+
],
|
|
1105
|
+
},
|
|
1106
|
+
{
|
|
1107
|
+
role: "tool",
|
|
1108
|
+
content: [
|
|
1109
|
+
{
|
|
1110
|
+
type: "tool-result",
|
|
1111
|
+
toolCallId: "call_123",
|
|
1112
|
+
result: { temp: 72 },
|
|
1113
|
+
},
|
|
1114
|
+
],
|
|
1115
|
+
},
|
|
1116
|
+
];
|
|
1117
|
+
// Runtime automatically associates these
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
## Working with External Messages
|
|
1121
|
+
|
|
1122
|
+
### Converting Back to Your Format
|
|
1123
|
+
|
|
1124
|
+
Use `getExternalStoreMessages` to access your original messages:
|
|
1125
|
+
|
|
1126
|
+
```tsx
|
|
1127
|
+
import { getExternalStoreMessages } from "@assistant-ui/react";
|
|
1128
|
+
|
|
1129
|
+
const MyComponent = () => {
|
|
1130
|
+
const originalMessages = useMessage((m) => getExternalStoreMessages(m));
|
|
1131
|
+
// originalMessages is MyMessage[] (your original type)
|
|
1132
|
+
};
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
<Callout type="info">
|
|
1136
|
+
After the chat finishes, use `getExternalStoreMessages(runtime)` to convert
|
|
1137
|
+
back to your domain model. Refer to the API reference for return structures
|
|
1138
|
+
and edge-case behaviors.
|
|
1139
|
+
</Callout>
|
|
1140
|
+
|
|
1141
|
+
<Callout type="warning">
|
|
1142
|
+
`getExternalStoreMessages` may return multiple messages for a single UI
|
|
1143
|
+
message. This happens because assistant-ui merges adjacent assistant and tool
|
|
1144
|
+
messages for display.
|
|
1145
|
+
</Callout>
|
|
1146
|
+
|
|
1147
|
+
### Content Part Access
|
|
1148
|
+
|
|
1149
|
+
```tsx
|
|
1150
|
+
const ToolUI = makeAssistantToolUI({
|
|
1151
|
+
render: () => {
|
|
1152
|
+
const originalMessages = useContentPart((p) => getExternalStoreMessages(p));
|
|
1153
|
+
// Access original message data for this content part
|
|
1154
|
+
},
|
|
1155
|
+
});
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
## Debugging
|
|
1159
|
+
|
|
1160
|
+
### Common Debugging Scenarios
|
|
1161
|
+
|
|
1162
|
+
```tsx
|
|
1163
|
+
// Debug message conversion
|
|
1164
|
+
const convertMessage = (message: MyMessage): ThreadMessageLike => {
|
|
1165
|
+
console.log("Converting message:", message);
|
|
1166
|
+
const converted = {
|
|
1167
|
+
role: message.role,
|
|
1168
|
+
content: [{ type: "text", text: message.content }],
|
|
1169
|
+
};
|
|
1170
|
+
console.log("Converted to:", converted);
|
|
1171
|
+
return converted;
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// Debug adapter calls
|
|
1175
|
+
const onNew = async (message: AppendMessage) => {
|
|
1176
|
+
console.log("onNew called with:", message);
|
|
1177
|
+
// ... implementation
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
// Enable verbose logging
|
|
1181
|
+
const runtime = useExternalStoreRuntime({
|
|
1182
|
+
messages,
|
|
1183
|
+
onNew: (...args) => {
|
|
1184
|
+
console.log("Runtime onNew:", args);
|
|
1185
|
+
return onNew(...args);
|
|
1186
|
+
},
|
|
1187
|
+
// ... other props
|
|
1188
|
+
});
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
## Best Practices
|
|
1192
|
+
|
|
1193
|
+
### 1. Immutable Updates
|
|
1194
|
+
|
|
1195
|
+
Always create new arrays when updating messages:
|
|
1196
|
+
|
|
1197
|
+
```tsx
|
|
1198
|
+
// ❌ Wrong - mutating array
|
|
1199
|
+
messages.push(newMessage);
|
|
1200
|
+
setMessages(messages);
|
|
1201
|
+
|
|
1202
|
+
// ✅ Correct - new array
|
|
1203
|
+
setMessages([...messages, newMessage]);
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
### 2. Stable Handler References
|
|
1207
|
+
|
|
1208
|
+
Memoize handlers to prevent runtime recreation:
|
|
1209
|
+
|
|
1210
|
+
```tsx
|
|
1211
|
+
const onNew = useCallback(
|
|
1212
|
+
async (message: AppendMessage) => {
|
|
1213
|
+
// Handle new message
|
|
1214
|
+
},
|
|
1215
|
+
[
|
|
1216
|
+
/* dependencies */
|
|
1217
|
+
],
|
|
1218
|
+
);
|
|
1219
|
+
|
|
1220
|
+
const runtime = useExternalStoreRuntime({
|
|
1221
|
+
messages,
|
|
1222
|
+
onNew, // Stable reference
|
|
1223
|
+
});
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### 3. Performance Optimization
|
|
1227
|
+
|
|
1228
|
+
```tsx
|
|
1229
|
+
// For large message lists
|
|
1230
|
+
const recentMessages = useMemo(
|
|
1231
|
+
() => messages.slice(-50), // Show last 50 messages
|
|
1232
|
+
[messages],
|
|
1233
|
+
);
|
|
1234
|
+
|
|
1235
|
+
// For expensive conversions
|
|
1236
|
+
const convertMessage = useCallback((msg) => {
|
|
1237
|
+
// Conversion logic
|
|
1238
|
+
}, []);
|
|
1239
|
+
```
|
|
1240
|
+
|
|
1241
|
+
## `LocalRuntime` vs `ExternalStoreRuntime`
|
|
1242
|
+
|
|
1243
|
+
### When to Choose Which
|
|
1244
|
+
|
|
1245
|
+
| Scenario | Recommendation |
|
|
1246
|
+
| -------------------------------- | ------------------------------------------------------------ |
|
|
1247
|
+
| Quick prototype | `LocalRuntime` |
|
|
1248
|
+
| Using Redux/Zustand | `ExternalStoreRuntime` |
|
|
1249
|
+
| Need Assistant Cloud integration | `LocalRuntime` |
|
|
1250
|
+
| Custom thread storage | Both (`LocalRuntime` with adapter or `ExternalStoreRuntime`) |
|
|
1251
|
+
| Simple single thread | `LocalRuntime` |
|
|
1252
|
+
| Complex state logic | `ExternalStoreRuntime` |
|
|
1253
|
+
|
|
1254
|
+
### Feature Comparison
|
|
1255
|
+
|
|
1256
|
+
| Feature | `LocalRuntime` | `ExternalStoreRuntime` |
|
|
1257
|
+
| ---------------- | --------------------------- | ---------------------- |
|
|
1258
|
+
| State Management | Built-in | You provide |
|
|
1259
|
+
| Multi-thread | Via Cloud or custom adapter | Via adapter |
|
|
1260
|
+
| Message Format | ThreadMessage | Any (with conversion) |
|
|
1261
|
+
| Setup Complexity | Low | Medium |
|
|
1262
|
+
| Flexibility | Medium | High |
|
|
1263
|
+
|
|
1264
|
+
## Common Pitfalls
|
|
1265
|
+
|
|
1266
|
+
<Callout type="error">
|
|
1267
|
+
**Features not appearing**: Each UI feature requires its corresponding handler:
|
|
1268
|
+
|
|
1269
|
+
```tsx
|
|
1270
|
+
// ❌ No edit button
|
|
1271
|
+
const runtime = useExternalStoreRuntime({ messages, onNew });
|
|
1272
|
+
|
|
1273
|
+
// ✅ Edit button appears
|
|
1274
|
+
const runtime = useExternalStoreRuntime({ messages, onNew, onEdit });
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
</Callout>
|
|
1278
|
+
|
|
1279
|
+
<Callout type="warning">
|
|
1280
|
+
|
|
1281
|
+
**State not updating**: Common causes:
|
|
1282
|
+
|
|
1283
|
+
1. Mutating arrays instead of creating new ones
|
|
1284
|
+
2. Missing `setMessages` for branch switching
|
|
1285
|
+
3. Not handling async operations properly
|
|
1286
|
+
4. Incorrect message format conversion
|
|
1287
|
+
|
|
1288
|
+
</Callout>
|
|
1289
|
+
|
|
1290
|
+
### Debugging Checklist
|
|
1291
|
+
|
|
1292
|
+
- Are you creating new arrays when updating messages?
|
|
1293
|
+
- Did you provide all required handlers for desired features?
|
|
1294
|
+
- Is your `convertMessage` returning valid `ThreadMessageLike`?
|
|
1295
|
+
- Are you properly handling `isRunning` state?
|
|
1296
|
+
- For threads: Is your thread list adapter complete?
|
|
1297
|
+
|
|
1298
|
+
### Thread-Specific Debugging
|
|
1299
|
+
|
|
1300
|
+
Common thread context issues and solutions:
|
|
1301
|
+
|
|
1302
|
+
**Messages disappearing when switching threads:**
|
|
1303
|
+
|
|
1304
|
+
```tsx
|
|
1305
|
+
// Check 1: Ensure currentThreadId is consistent
|
|
1306
|
+
console.log("Runtime threadId:", threadListAdapter.threadId);
|
|
1307
|
+
console.log("Current threadId:", currentThreadId);
|
|
1308
|
+
console.log("Messages for thread:", threads.get(currentThreadId));
|
|
1309
|
+
|
|
1310
|
+
// Check 2: Verify setMessages uses correct thread
|
|
1311
|
+
setMessages: (messages) => {
|
|
1312
|
+
console.log("Setting messages for thread:", currentThreadId);
|
|
1313
|
+
setThreads((prev) => new Map(prev).set(currentThreadId, messages));
|
|
1314
|
+
};
|
|
1315
|
+
```
|
|
1316
|
+
|
|
1317
|
+
**Thread list not updating:**
|
|
1318
|
+
|
|
1319
|
+
```tsx
|
|
1320
|
+
// Ensure threadList state is properly managed
|
|
1321
|
+
onSwitchToNewThread: () => {
|
|
1322
|
+
const newId = `thread-${Date.now()}`;
|
|
1323
|
+
console.log("Creating new thread:", newId);
|
|
1324
|
+
|
|
1325
|
+
// All three updates must happen together
|
|
1326
|
+
setThreadList((prev) => [...prev, newThreadData]);
|
|
1327
|
+
setThreads((prev) => new Map(prev).set(newId, []));
|
|
1328
|
+
setCurrentThreadId(newId);
|
|
1329
|
+
};
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
**Messages going to wrong thread:**
|
|
1333
|
+
|
|
1334
|
+
```tsx
|
|
1335
|
+
// Add validation to prevent race conditions
|
|
1336
|
+
const validateThreadContext = () => {
|
|
1337
|
+
const runtimeThread = threadListAdapter.threadId;
|
|
1338
|
+
const contextThread = currentThreadId;
|
|
1339
|
+
|
|
1340
|
+
if (runtimeThread !== contextThread) {
|
|
1341
|
+
console.error("Thread mismatch!", { runtimeThread, contextThread });
|
|
1342
|
+
throw new Error("Thread context mismatch");
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
|
|
1346
|
+
// Call before any message operation
|
|
1347
|
+
onNew: async (message) => {
|
|
1348
|
+
validateThreadContext();
|
|
1349
|
+
// ... handle message
|
|
1350
|
+
};
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
## API Reference
|
|
1354
|
+
|
|
1355
|
+
### `ExternalStoreAdapter`
|
|
1356
|
+
|
|
1357
|
+
The main interface for connecting your state to assistant-ui.
|
|
1358
|
+
|
|
1359
|
+
<ParametersTable
|
|
1360
|
+
type="ExternalStoreAdapter<T>"
|
|
1361
|
+
parameters={[
|
|
1362
|
+
{
|
|
1363
|
+
name: "messages",
|
|
1364
|
+
type: "readonly T[]",
|
|
1365
|
+
description: "Array of messages from your state",
|
|
1366
|
+
required: true,
|
|
1367
|
+
},
|
|
1368
|
+
{
|
|
1369
|
+
name: "onNew",
|
|
1370
|
+
type: "(message: AppendMessage) => Promise<void>",
|
|
1371
|
+
description: "Handler for new messages from the user",
|
|
1372
|
+
required: true,
|
|
1373
|
+
},
|
|
1374
|
+
{
|
|
1375
|
+
name: "isRunning",
|
|
1376
|
+
type: "boolean",
|
|
1377
|
+
description:
|
|
1378
|
+
"Whether the assistant is currently generating a response. When true, shows optimistic assistant message",
|
|
1379
|
+
default: "false",
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
name: "isDisabled",
|
|
1383
|
+
type: "boolean",
|
|
1384
|
+
description: "Whether the chat input should be disabled",
|
|
1385
|
+
default: "false",
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
name: "suggestions",
|
|
1389
|
+
type: "readonly ThreadSuggestion[]",
|
|
1390
|
+
description: "Suggested prompts to display",
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
name: "extras",
|
|
1394
|
+
type: "unknown",
|
|
1395
|
+
description: "Additional data accessible via runtime.extras",
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
name: "setMessages",
|
|
1399
|
+
type: "(messages: T[]) => void",
|
|
1400
|
+
description: "Update messages (required for branch switching)",
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
name: "onEdit",
|
|
1404
|
+
type: "(message: AppendMessage) => Promise<void>",
|
|
1405
|
+
description: "Handler for message edits (required for edit feature)",
|
|
1406
|
+
},
|
|
1407
|
+
{
|
|
1408
|
+
name: "onReload",
|
|
1409
|
+
type: "(parentId: string | null, config: StartRunConfig) => Promise<void>",
|
|
1410
|
+
description:
|
|
1411
|
+
"Handler for regenerating messages (required for reload feature)",
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
name: "onCancel",
|
|
1415
|
+
type: "() => Promise<void>",
|
|
1416
|
+
description: "Handler for cancelling the current generation",
|
|
1417
|
+
},
|
|
1418
|
+
{
|
|
1419
|
+
name: "onAddToolResult",
|
|
1420
|
+
type: "(options: AddToolResultOptions) => Promise<void> | void",
|
|
1421
|
+
description: "Handler for adding tool call results",
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
name: "convertMessage",
|
|
1425
|
+
type: "(message: T, index: number) => ThreadMessageLike",
|
|
1426
|
+
description:
|
|
1427
|
+
"Convert your message format to assistant-ui format. Not needed if using ThreadMessage type",
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
name: "joinStrategy",
|
|
1431
|
+
type: '"concat-content" | "none"',
|
|
1432
|
+
description: "How to join adjacent assistant messages when converting",
|
|
1433
|
+
default: '"concat-content"',
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
name: "adapters",
|
|
1437
|
+
type: "object",
|
|
1438
|
+
description: "Feature adapters (same as LocalRuntime)",
|
|
1439
|
+
children: [
|
|
1440
|
+
{
|
|
1441
|
+
type: "adapters",
|
|
1442
|
+
parameters: [
|
|
1443
|
+
{
|
|
1444
|
+
name: "attachments",
|
|
1445
|
+
type: "AttachmentAdapter",
|
|
1446
|
+
description: "Enable file attachments",
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
name: "speech",
|
|
1450
|
+
type: "SpeechSynthesisAdapter",
|
|
1451
|
+
description: "Enable text-to-speech",
|
|
1452
|
+
},
|
|
1453
|
+
{
|
|
1454
|
+
name: "feedback",
|
|
1455
|
+
type: "FeedbackAdapter",
|
|
1456
|
+
description: "Enable message feedback",
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
name: "threadList",
|
|
1460
|
+
type: "ExternalStoreThreadListAdapter",
|
|
1461
|
+
description: "Enable multi-thread management",
|
|
1462
|
+
},
|
|
1463
|
+
],
|
|
1464
|
+
},
|
|
1465
|
+
],
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
name: "unstable_capabilities",
|
|
1469
|
+
type: "object",
|
|
1470
|
+
description: "Configure runtime capabilities",
|
|
1471
|
+
children: [
|
|
1472
|
+
{
|
|
1473
|
+
type: "unstable_capabilities",
|
|
1474
|
+
parameters: [
|
|
1475
|
+
{
|
|
1476
|
+
name: "copy",
|
|
1477
|
+
type: "boolean",
|
|
1478
|
+
description: "Enable message copy feature",
|
|
1479
|
+
default: "true",
|
|
1480
|
+
},
|
|
1481
|
+
],
|
|
1482
|
+
},
|
|
1483
|
+
],
|
|
1484
|
+
},
|
|
1485
|
+
]}
|
|
1486
|
+
/>
|
|
1487
|
+
|
|
1488
|
+
### `ThreadMessageLike`
|
|
1489
|
+
|
|
1490
|
+
A flexible message format that can be converted to assistant-ui's internal format.
|
|
1491
|
+
|
|
1492
|
+
<ParametersTable
|
|
1493
|
+
type="ThreadMessageLike"
|
|
1494
|
+
parameters={[
|
|
1495
|
+
{
|
|
1496
|
+
name: "role",
|
|
1497
|
+
type: '"assistant" | "user" | "system"',
|
|
1498
|
+
description: "The role of the message sender",
|
|
1499
|
+
required: true,
|
|
1500
|
+
},
|
|
1501
|
+
{
|
|
1502
|
+
name: "content",
|
|
1503
|
+
type: "string | readonly ContentPart[]",
|
|
1504
|
+
description: "Message content as string or structured content parts",
|
|
1505
|
+
required: true,
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
name: "id",
|
|
1509
|
+
type: "string",
|
|
1510
|
+
description: "Unique identifier for the message",
|
|
1511
|
+
},
|
|
1512
|
+
{
|
|
1513
|
+
name: "createdAt",
|
|
1514
|
+
type: "Date",
|
|
1515
|
+
description: "Timestamp when the message was created",
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
name: "status",
|
|
1519
|
+
type: "MessageStatus",
|
|
1520
|
+
description:
|
|
1521
|
+
"Status of assistant messages (in_progress, complete, cancelled)",
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
name: "attachments",
|
|
1525
|
+
type: "readonly CompleteAttachment[]",
|
|
1526
|
+
description: "File attachments (user messages only)",
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
name: "metadata",
|
|
1530
|
+
type: "object",
|
|
1531
|
+
description: "Additional message metadata",
|
|
1532
|
+
children: [
|
|
1533
|
+
{
|
|
1534
|
+
type: "metadata",
|
|
1535
|
+
parameters: [
|
|
1536
|
+
{
|
|
1537
|
+
name: "steps",
|
|
1538
|
+
type: "readonly ThreadStep[]",
|
|
1539
|
+
description: "Tool call steps for assistant messages",
|
|
1540
|
+
},
|
|
1541
|
+
{
|
|
1542
|
+
name: "custom",
|
|
1543
|
+
type: "Record<string, unknown>",
|
|
1544
|
+
description: "Custom metadata for your application",
|
|
1545
|
+
},
|
|
1546
|
+
],
|
|
1547
|
+
},
|
|
1548
|
+
],
|
|
1549
|
+
},
|
|
1550
|
+
]}
|
|
1551
|
+
/>
|
|
1552
|
+
|
|
1553
|
+
### `ExternalStoreThreadListAdapter`
|
|
1554
|
+
|
|
1555
|
+
Enable multi-thread support with custom thread management.
|
|
1556
|
+
|
|
1557
|
+
<ParametersTable
|
|
1558
|
+
type="ExternalStoreThreadListAdapter"
|
|
1559
|
+
parameters={[
|
|
1560
|
+
{
|
|
1561
|
+
name: "threadId",
|
|
1562
|
+
type: "string",
|
|
1563
|
+
description: "ID of the current active thread",
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
name: "threads",
|
|
1567
|
+
type: "readonly ExternalStoreThreadData[]",
|
|
1568
|
+
description: "Array of regular threads with { threadId, title }",
|
|
1569
|
+
},
|
|
1570
|
+
{
|
|
1571
|
+
name: "archivedThreads",
|
|
1572
|
+
type: "readonly ExternalStoreThreadData[]",
|
|
1573
|
+
description: "Array of archived threads",
|
|
1574
|
+
},
|
|
1575
|
+
{
|
|
1576
|
+
name: "onSwitchToNewThread",
|
|
1577
|
+
type: "() => Promise<void> | void",
|
|
1578
|
+
description: "Handler for creating a new thread",
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
name: "onSwitchToThread",
|
|
1582
|
+
type: "(threadId: string) => Promise<void> | void",
|
|
1583
|
+
description: "Handler for switching to an existing thread",
|
|
1584
|
+
},
|
|
1585
|
+
{
|
|
1586
|
+
name: "onRename",
|
|
1587
|
+
type: "(threadId: string, newTitle: string) => Promise<void> | void",
|
|
1588
|
+
description: "Handler for renaming a thread",
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
name: "onArchive",
|
|
1592
|
+
type: "(threadId: string) => Promise<void> | void",
|
|
1593
|
+
description: "Handler for archiving a thread",
|
|
1594
|
+
},
|
|
1595
|
+
{
|
|
1596
|
+
name: "onUnarchive",
|
|
1597
|
+
type: "(threadId: string) => Promise<void> | void",
|
|
1598
|
+
description: "Handler for unarchiving a thread",
|
|
1599
|
+
},
|
|
1600
|
+
{
|
|
1601
|
+
name: "onDelete",
|
|
1602
|
+
type: "(threadId: string) => Promise<void> | void",
|
|
1603
|
+
description: "Handler for deleting a thread",
|
|
1604
|
+
},
|
|
1605
|
+
]}
|
|
1606
|
+
/>
|
|
1607
|
+
|
|
1608
|
+
<Callout type="info">
|
|
1609
|
+
The thread list adapter enables multi-thread support. Without it, the runtime
|
|
1610
|
+
only manages the current conversation.
|
|
1611
|
+
</Callout>
|
|
1612
|
+
|
|
1613
|
+
### Related Runtime APIs
|
|
1614
|
+
|
|
1615
|
+
- [AssistantRuntime API](/docs/api-reference/runtimes/AssistantRuntime) - Core runtime interface and methods
|
|
1616
|
+
- [ThreadRuntime API](/docs/api-reference/runtimes/ThreadRuntime) - Thread-specific operations and state management
|
|
1617
|
+
- [Runtime Providers](/docs/api-reference/context-providers/AssistantRuntimeProvider) - Context providers for runtime integration
|
|
1618
|
+
|
|
1619
|
+
## Related Resources
|
|
1620
|
+
|
|
1621
|
+
- [Runtime Layer Concepts](/docs/concepts/runtime-layer)
|
|
1622
|
+
- [Pick a Runtime Guide](/docs/runtimes/pick-a-runtime)
|
|
1623
|
+
- [`LocalRuntime` Documentation](/docs/runtimes/custom/local)
|
|
1624
|
+
- [Examples Repository](https://github.com/assistant-ui/assistant-ui/tree/main/examples/with-external-store)
|