@assistant-ui/mcp-docs-server 0.1.32 → 0.1.34
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 +20 -22
- package/.docs/organized/code-examples/with-a2a.md +24 -24
- package/.docs/organized/code-examples/with-ag-ui.md +30 -25
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +11 -9
- package/.docs/organized/code-examples/with-artifacts.md +49 -37
- package/.docs/organized/code-examples/with-assistant-transport.md +63 -51
- package/.docs/organized/code-examples/with-browser-extension.md +22 -10
- package/.docs/organized/code-examples/with-chain-of-thought.md +406 -87
- package/.docs/organized/code-examples/with-cloud-standalone.md +25 -24
- package/.docs/organized/code-examples/with-cloud.md +11 -9
- package/.docs/organized/code-examples/with-custom-thread-list.md +14 -12
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +23 -21
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +14 -13
- package/.docs/organized/code-examples/with-expo.md +44 -29
- package/.docs/organized/code-examples/with-external-store.md +9 -7
- package/.docs/organized/code-examples/with-ffmpeg.md +322 -285
- package/.docs/organized/code-examples/with-generative-ui.md +1066 -256
- package/.docs/organized/code-examples/with-google-adk.md +10 -8
- package/.docs/organized/code-examples/with-heat-graph.md +13 -11
- package/.docs/organized/code-examples/with-image-generation.md +19 -17
- package/.docs/organized/code-examples/with-interactables.md +317 -239
- package/.docs/organized/code-examples/with-langchain.md +14 -12
- package/.docs/organized/code-examples/with-langgraph.md +91 -83
- package/.docs/organized/code-examples/with-livekit.md +24 -22
- package/.docs/organized/code-examples/with-mcp.md +18 -12
- package/.docs/organized/code-examples/with-opencode.md +66 -66
- package/.docs/organized/code-examples/with-react-hook-form.md +19 -17
- package/.docs/organized/code-examples/with-react-ink.md +297 -102
- package/.docs/organized/code-examples/with-react-router.md +17 -15
- package/.docs/organized/code-examples/with-resumable-stream.md +15 -14
- package/.docs/organized/code-examples/with-store.md +78 -76
- package/.docs/organized/code-examples/with-tanstack.md +13 -14
- package/.docs/organized/code-examples/with-tap-runtime.md +26 -24
- package/.docs/raw/docs/(docs)/architecture.mdx +94 -42
- package/.docs/raw/docs/(docs)/cli.mdx +1 -2
- package/.docs/raw/docs/(docs)/installation.mdx +1 -1
- package/.docs/raw/docs/(reference)/api-reference/adapters/persistence.mdx +5 -1
- package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +15 -15
- package/.docs/raw/docs/(reference)/api-reference/external-store/runtime.mdx +14 -1
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/index.mdx +18 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/rendering.mdx +33 -0
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/spec.mdx +45 -0
- package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +41 -41
- package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +19 -1
- package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +122 -122
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +43 -2
- package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +6 -0
- package/.docs/raw/docs/(reference)/api-reference/overview.mdx +4 -6
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +30 -0
- package/.docs/raw/docs/(reference)/api-reference/tools/component-tools.mdx +20 -3
- package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +8 -8
- package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +52 -4
- package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +100 -10
- package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +7 -59
- package/.docs/raw/docs/cloud/ai-sdk.mdx +0 -2
- package/.docs/raw/docs/{(docs)/copilots → copilots}/model-context.mdx +34 -26
- package/.docs/raw/docs/{(docs)/copilots → copilots}/motivation.mdx +32 -26
- package/.docs/raw/docs/guides/chain-of-thought.mdx +7 -9
- package/.docs/raw/docs/guides/context-api.mdx +2 -1
- package/.docs/raw/docs/guides/index.mdx +3 -12
- package/.docs/raw/docs/guides/mentions.mdx +4 -4
- package/.docs/raw/docs/guides/slash-commands.mdx +1 -1
- package/.docs/raw/docs/guides/suggestions.mdx +1 -1
- package/.docs/raw/docs/ink/adapters.mdx +23 -1
- package/.docs/raw/docs/ink/hooks.mdx +101 -85
- package/.docs/raw/docs/ink/migration.mdx +1 -1
- package/.docs/raw/docs/ink/primitives.mdx +2 -2
- package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +1 -1
- package/.docs/raw/docs/integrations/index.mdx +2 -2
- package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +17 -2
- package/.docs/raw/docs/migrations/toolkit-tools.mdx +232 -0
- package/.docs/raw/docs/primitives/chain-of-thought.mdx +10 -16
- package/.docs/raw/docs/primitives/message.mdx +9 -10
- package/.docs/raw/docs/react-native/hooks.mdx +62 -79
- package/.docs/raw/docs/react-native/migration.mdx +1 -1
- package/.docs/raw/docs/react-native/primitives.mdx +2 -2
- package/.docs/raw/docs/runtimes/a2a/client-and-hooks.mdx +3 -0
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +41 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +122 -1
- package/.docs/raw/docs/runtimes/concepts/architecture.mdx +46 -4
- package/.docs/raw/docs/runtimes/concepts/threads.mdx +7 -1
- package/.docs/raw/docs/runtimes/custom/assistant-transport.mdx +54 -12
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +62 -4
- package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +30 -7
- package/.docs/raw/docs/runtimes/google-adk/hooks.mdx +108 -38
- package/.docs/raw/docs/runtimes/langgraph/generative-ui.mdx +2 -2
- package/.docs/raw/docs/runtimes/langgraph/streaming.mdx +14 -1
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +64 -50
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +98 -86
- package/.docs/raw/docs/tools/backend.mdx +144 -0
- package/.docs/raw/docs/tools/defining-tools.mdx +538 -0
- package/.docs/raw/docs/tools/dynamic-tools.mdx +110 -0
- package/.docs/raw/docs/tools/generative-ui.mdx +214 -0
- package/.docs/raw/docs/tools/index.mdx +71 -0
- package/.docs/raw/docs/{guides → tools}/interactables.mdx +1 -1
- package/.docs/raw/docs/{integrations/tools → tools}/mcp.mdx +145 -50
- package/.docs/raw/docs/{guides → tools}/multi-agent.mdx +15 -17
- package/.docs/raw/docs/tools/tool-ui.mdx +967 -0
- package/.docs/raw/docs/{integrations/tools/react-mcp.mdx → tools/user-managed-mcp.mdx} +7 -7
- package/.docs/raw/docs/ui/directive-text.mdx +3 -3
- package/.docs/raw/docs/ui/mcp-config.mdx +4 -4
- package/.docs/raw/docs/ui/mermaid.mdx +16 -9
- package/.docs/raw/docs/ui/part-grouping.mdx +84 -50
- package/.docs/raw/docs/ui/reasoning.mdx +4 -5
- package/.docs/raw/docs/ui/tool-fallback.mdx +16 -0
- package/.docs/raw/docs/ui/tool-group.mdx +5 -6
- package/.docs/raw/docs/utilities/heat-graph.mdx +1 -1
- package/.docs/raw/docs/utilities/react-o11y.mdx +2 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/prepare-docs/code-examples.d.ts.map +1 -1
- package/dist/prepare-docs/code-examples.js.map +1 -1
- package/dist/prepare-docs/copy-raw.d.ts.map +1 -1
- package/dist/prepare-docs/copy-raw.js.map +1 -1
- package/dist/prepare-docs/prepare.js.map +1 -1
- package/dist/stdio.js.map +1 -1
- package/dist/tools/docs.js.map +1 -1
- package/dist/tools/examples.js.map +1 -1
- package/dist/tools/tests/test-setup.js.map +1 -1
- package/dist/utils/mdx.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/package.json +5 -5
- package/.docs/organized/code-examples/with-parent-id-grouping.md +0 -596
- package/.docs/raw/docs/(docs)/copilots/make-assistant-tool-ui.mdx +0 -151
- package/.docs/raw/docs/(docs)/copilots/make-assistant-tool.mdx +0 -230
- package/.docs/raw/docs/guides/generative-ui.mdx +0 -142
- package/.docs/raw/docs/guides/tool-ui.mdx +0 -858
- package/.docs/raw/docs/guides/tools.mdx +0 -736
- /package/.docs/raw/docs/{(docs)/copilots → copilots}/assistant-frame.mdx +0 -0
- /package/.docs/raw/docs/{(docs)/copilots → copilots}/make-assistant-visible.mdx +0 -0
- /package/.docs/raw/docs/{(docs)/copilots → copilots}/use-assistant-instructions.mdx +0 -0
- /package/.docs/raw/docs/{guides → tools}/mcp-apps.mdx +0 -0
|
@@ -3,46 +3,137 @@
|
|
|
3
3
|
## app/api/chat/route.ts
|
|
4
4
|
|
|
5
5
|
```typescript
|
|
6
|
-
import {
|
|
6
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
7
7
|
import {
|
|
8
8
|
streamText,
|
|
9
9
|
convertToModelMessages,
|
|
10
10
|
tool,
|
|
11
11
|
stepCountIs,
|
|
12
12
|
zodSchema,
|
|
13
|
+
createUIMessageStream,
|
|
14
|
+
JsonToSseTransformStream,
|
|
13
15
|
} from "ai";
|
|
14
|
-
import type { UIMessage } from "ai";
|
|
15
|
-
import {
|
|
16
|
+
import type { UIMessage, UIMessageStreamWriter } from "ai";
|
|
17
|
+
import { AISDKToolkit } from "@assistant-ui/react-ai-sdk";
|
|
16
18
|
import { z } from "zod";
|
|
19
|
+
import toolkit from "../../toolkit";
|
|
17
20
|
|
|
18
21
|
export const maxDuration = 30;
|
|
19
22
|
|
|
23
|
+
const aiToolkit = new AISDKToolkit({ toolkit });
|
|
24
|
+
|
|
25
|
+
type SearchResult = { id: string; url: string; title: string };
|
|
26
|
+
|
|
27
|
+
const searchSources = (query: string): SearchResult[] => {
|
|
28
|
+
const q = query.toLowerCase();
|
|
29
|
+
if (q.includes("fibonacci")) {
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
id: "src-fib-1",
|
|
33
|
+
url: "https://en.wikipedia.org/wiki/Fibonacci_number",
|
|
34
|
+
title: "Fibonacci number (Wikipedia)",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "src-fib-2",
|
|
38
|
+
url: "https://oeis.org/A000045",
|
|
39
|
+
title: "OEIS A000045: Fibonacci numbers",
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
if (
|
|
44
|
+
q.includes("renewable") ||
|
|
45
|
+
q.includes("energy") ||
|
|
46
|
+
q.includes("climate")
|
|
47
|
+
) {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
id: "src-iea",
|
|
51
|
+
url: "https://www.iea.org/reports/renewables-2024",
|
|
52
|
+
title: "IEA Renewables 2024",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "src-irena",
|
|
56
|
+
url: "https://www.irena.org/Publications",
|
|
57
|
+
title: "IRENA Publications",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
id: "src-wiki",
|
|
64
|
+
url: `https://en.wikipedia.org/wiki/Special:Search?search=${encodeURIComponent(query)}`,
|
|
65
|
+
title: "Wikipedia search results",
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "src-web",
|
|
69
|
+
url: `https://duckduckgo.com/?q=${encodeURIComponent(query)}`,
|
|
70
|
+
title: `Web results for: ${query}`,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
};
|
|
74
|
+
|
|
20
75
|
export async function POST(req: Request) {
|
|
21
76
|
const {
|
|
22
77
|
messages,
|
|
23
78
|
tools,
|
|
24
79
|
}: { messages: UIMessage[]; tools: Record<string, any> } = await req.json();
|
|
25
80
|
|
|
81
|
+
const uiStream = createUIMessageStream({
|
|
82
|
+
execute: async ({ writer }) => {
|
|
83
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
84
|
+
await streamFallback(writer, messages);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await streamModel(writer, messages, tools);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return new Response(
|
|
92
|
+
uiStream
|
|
93
|
+
.pipeThrough(new JsonToSseTransformStream())
|
|
94
|
+
.pipeThrough(new TextEncoderStream()),
|
|
95
|
+
{
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "text/event-stream",
|
|
98
|
+
"Cache-Control": "no-cache",
|
|
99
|
+
Connection: "keep-alive",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function streamModel(
|
|
106
|
+
writer: UIMessageStreamWriter,
|
|
107
|
+
messages: UIMessage[],
|
|
108
|
+
frontendToolDefs: Record<string, any>,
|
|
109
|
+
) {
|
|
110
|
+
const toolNameByCall = new Map<string, string>();
|
|
111
|
+
const openai = createOpenAI({
|
|
112
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
113
|
+
...(process.env.OPENAI_BASE_URL && {
|
|
114
|
+
baseURL: process.env.OPENAI_BASE_URL,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const toolkitTools = await aiToolkit.tools({ frontend: frontendToolDefs });
|
|
119
|
+
|
|
26
120
|
const result = streamText({
|
|
27
|
-
//
|
|
121
|
+
// Reasoning model so the chain-of-thought group has real content.
|
|
28
122
|
model: openai("gpt-5.4-mini"),
|
|
29
123
|
messages: await convertToModelMessages(messages),
|
|
30
124
|
stopWhen: stepCountIs(10),
|
|
31
125
|
tools: {
|
|
32
|
-
|
|
33
|
-
...frontendTools(tools),
|
|
34
|
-
|
|
35
|
-
// Backend-only tools
|
|
126
|
+
...toolkitTools,
|
|
36
127
|
get_current_weather: tool({
|
|
37
128
|
description: "Get the current weather for a city",
|
|
38
|
-
inputSchema: zodSchema(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
129
|
+
inputSchema: zodSchema(z.object({ city: z.string() })),
|
|
130
|
+
execute: async ({ city }) => `The weather in ${city} is sunny, 72°F`,
|
|
131
|
+
}),
|
|
132
|
+
search_web: tool({
|
|
133
|
+
description:
|
|
134
|
+
"Search the web for citations on a topic. Returns a list of source URLs and titles the assistant should consult.",
|
|
135
|
+
inputSchema: zodSchema(z.object({ query: z.string() })),
|
|
136
|
+
execute: async ({ query }) => ({ sources: searchSources(query) }),
|
|
46
137
|
}),
|
|
47
138
|
},
|
|
48
139
|
providerOptions: {
|
|
@@ -53,9 +144,106 @@ export async function POST(req: Request) {
|
|
|
53
144
|
},
|
|
54
145
|
});
|
|
55
146
|
|
|
56
|
-
|
|
147
|
+
// Forward model chunks, then synthesize `source-url` parts from each
|
|
148
|
+
// `search_web` tool output so the UI renders them as `source` parts. OpenAI
|
|
149
|
+
// does not natively emit `source-url` chunks without web search, so the route
|
|
150
|
+
// produces them deterministically from the tool result.
|
|
151
|
+
for await (const chunk of result.toUIMessageStream({ sendReasoning: true })) {
|
|
152
|
+
writer.write(chunk);
|
|
153
|
+
|
|
154
|
+
if (chunk.type === "tool-input-available") {
|
|
155
|
+
toolNameByCall.set(chunk.toolCallId, chunk.toolName);
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
chunk.type === "tool-output-available" &&
|
|
159
|
+
toolNameByCall.get(chunk.toolCallId) === "search_web"
|
|
160
|
+
) {
|
|
161
|
+
const output = chunk.output as { sources?: SearchResult[] };
|
|
162
|
+
for (const source of output.sources ?? []) {
|
|
163
|
+
writer.write({
|
|
164
|
+
type: "source-url",
|
|
165
|
+
sourceId: `${source.id}-${crypto.randomUUID()}`,
|
|
166
|
+
url: source.url,
|
|
167
|
+
title: source.title,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function streamFallback(
|
|
175
|
+
writer: UIMessageStreamWriter,
|
|
176
|
+
messages: UIMessage[],
|
|
177
|
+
) {
|
|
178
|
+
const lastUserText =
|
|
179
|
+
messages
|
|
180
|
+
.filter((m) => m.role === "user")
|
|
181
|
+
.at(-1)
|
|
182
|
+
?.parts.flatMap((p) => (p.type === "text" ? [p.text] : []))
|
|
183
|
+
.join(" ") ?? "your question";
|
|
184
|
+
|
|
185
|
+
const messageId = `msg-${crypto.randomUUID()}`;
|
|
186
|
+
const reasoningId = `r-${crypto.randomUUID()}`;
|
|
187
|
+
const textId = `t-${crypto.randomUUID()}`;
|
|
188
|
+
const toolCallId = `call-${crypto.randomUUID()}`;
|
|
189
|
+
const sources = searchSources(lastUserText);
|
|
190
|
+
|
|
191
|
+
writer.write({ type: "start", messageId });
|
|
192
|
+
writer.write({ type: "start-step" });
|
|
193
|
+
|
|
194
|
+
writer.write({ type: "reasoning-start", id: reasoningId });
|
|
195
|
+
const reasoning =
|
|
196
|
+
"OPENAI_API_KEY is not set, so this response is generated deterministically by the route. " +
|
|
197
|
+
"A real run emits the same chunk shapes from the model: a reasoning step, " +
|
|
198
|
+
"a search_web tool call, source-url parts derived from the tool output, " +
|
|
199
|
+
"and a final answer that summarises the sources.";
|
|
200
|
+
for (const word of reasoning.match(/\S+\s*|\s+/g) ?? [reasoning]) {
|
|
201
|
+
writer.write({ type: "reasoning-delta", id: reasoningId, delta: word });
|
|
202
|
+
await sleep(15);
|
|
203
|
+
}
|
|
204
|
+
writer.write({ type: "reasoning-end", id: reasoningId });
|
|
205
|
+
|
|
206
|
+
writer.write({
|
|
207
|
+
type: "tool-input-available",
|
|
208
|
+
toolCallId,
|
|
209
|
+
toolName: "search_web",
|
|
210
|
+
input: { query: lastUserText },
|
|
211
|
+
});
|
|
212
|
+
await sleep(150);
|
|
213
|
+
writer.write({
|
|
214
|
+
type: "tool-output-available",
|
|
215
|
+
toolCallId,
|
|
216
|
+
output: { sources },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
for (const source of sources) {
|
|
220
|
+
writer.write({
|
|
221
|
+
type: "source-url",
|
|
222
|
+
sourceId: `${source.id}-${crypto.randomUUID()}`,
|
|
223
|
+
url: source.url,
|
|
224
|
+
title: source.title,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
writer.write({ type: "text-start", id: textId });
|
|
229
|
+
const answer =
|
|
230
|
+
`\n\nMock answer for: "${lastUserText}". ` +
|
|
231
|
+
"The badges above are structured `source` message parts emitted as " +
|
|
232
|
+
"`source-url` chunks, not URLs scraped from text. Set `OPENAI_API_KEY` " +
|
|
233
|
+
"to switch to a real model run.";
|
|
234
|
+
for (const word of answer.match(/\S+\s*|\s+/g) ?? [answer]) {
|
|
235
|
+
writer.write({ type: "text-delta", id: textId, delta: word });
|
|
236
|
+
await sleep(15);
|
|
237
|
+
}
|
|
238
|
+
writer.write({ type: "text-end", id: textId });
|
|
239
|
+
|
|
240
|
+
writer.write({ type: "finish-step" });
|
|
241
|
+
writer.write({ type: "finish" });
|
|
57
242
|
}
|
|
58
243
|
|
|
244
|
+
const sleep = (ms: number) =>
|
|
245
|
+
new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
246
|
+
|
|
59
247
|
```
|
|
60
248
|
|
|
61
249
|
## app/globals.css
|
|
@@ -194,7 +382,8 @@ import "./globals.css";
|
|
|
194
382
|
|
|
195
383
|
export const metadata: Metadata = {
|
|
196
384
|
title: "Chain of Thought Example",
|
|
197
|
-
description:
|
|
385
|
+
description:
|
|
386
|
+
"Reasoning, tool calls, and source citations rendered as grouped message parts.",
|
|
198
387
|
};
|
|
199
388
|
|
|
200
389
|
export default function RootLayout({
|
|
@@ -218,10 +407,12 @@ export default function RootLayout({
|
|
|
218
407
|
|
|
219
408
|
import { type FC, type PropsWithChildren, useState } from "react";
|
|
220
409
|
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
|
410
|
+
import { Sources } from "@/components/assistant-ui/sources";
|
|
221
411
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
|
222
412
|
import { Button } from "@/components/ui/button";
|
|
223
413
|
import {
|
|
224
414
|
ComposerPrimitive,
|
|
415
|
+
groupPartByType,
|
|
225
416
|
MessagePrimitive,
|
|
226
417
|
SuggestionPrimitive,
|
|
227
418
|
ThreadPrimitive,
|
|
@@ -238,7 +429,7 @@ import {
|
|
|
238
429
|
export const MyThread: FC = () => {
|
|
239
430
|
return (
|
|
240
431
|
<ThreadPrimitive.Root
|
|
241
|
-
className="flex h-full flex-col
|
|
432
|
+
className="bg-background flex h-full flex-col"
|
|
242
433
|
style={{ ["--thread-max-width" as string]: "44rem" }}
|
|
243
434
|
>
|
|
244
435
|
<ThreadPrimitive.Viewport className="flex flex-1 flex-col overflow-y-scroll scroll-smooth px-4 pt-8">
|
|
@@ -278,7 +469,9 @@ const ThreadWelcome: FC = () => {
|
|
|
278
469
|
return (
|
|
279
470
|
<div className="mx-auto flex w-full max-w-(--thread-max-width) grow flex-col justify-center gap-6">
|
|
280
471
|
<div>
|
|
281
|
-
<h1 className="font-semibold
|
|
472
|
+
<h1 className="text-2xl font-semibold">
|
|
473
|
+
What should we think through?
|
|
474
|
+
</h1>
|
|
282
475
|
</div>
|
|
283
476
|
<div className="flex flex-wrap gap-2">
|
|
284
477
|
<ThreadPrimitive.Suggestions>
|
|
@@ -295,7 +488,7 @@ const ThreadSuggestionItem: FC = () => {
|
|
|
295
488
|
<Button
|
|
296
489
|
type="button"
|
|
297
490
|
variant="ghost"
|
|
298
|
-
className="h-auto w-full flex-col items-start justify-start gap-1 rounded-2xl border
|
|
491
|
+
className="bg-background hover:bg-muted h-auto w-full flex-col items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-start text-sm sm:w-[calc(50%-0.25rem)]"
|
|
299
492
|
>
|
|
300
493
|
<SuggestionPrimitive.Title className="font-medium" />
|
|
301
494
|
<SuggestionPrimitive.Description className="text-muted-foreground empty:hidden" />
|
|
@@ -308,7 +501,7 @@ const UserMessage: FC = () => {
|
|
|
308
501
|
return (
|
|
309
502
|
<MessagePrimitive.Root className="mx-auto w-full max-w-(--thread-max-width) py-3">
|
|
310
503
|
<div className="flex justify-end">
|
|
311
|
-
<div className="max-w-[80%] rounded-2xl
|
|
504
|
+
<div className="bg-primary text-primary-foreground max-w-[80%] rounded-2xl px-4 py-2">
|
|
312
505
|
<MessagePrimitive.Parts>
|
|
313
506
|
{({ part }) => {
|
|
314
507
|
if (part.type === "text") return <Text {...part} />;
|
|
@@ -326,13 +519,11 @@ const AssistantMessage: FC = () => {
|
|
|
326
519
|
<MessagePrimitive.Root className="mx-auto w-full max-w-(--thread-max-width) py-3">
|
|
327
520
|
<div className="flex flex-col gap-2 px-2 leading-relaxed">
|
|
328
521
|
<MessagePrimitive.GroupedParts
|
|
329
|
-
groupBy={(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return null;
|
|
335
|
-
}}
|
|
522
|
+
groupBy={groupPartByType({
|
|
523
|
+
reasoning: ["group-chainOfThought", "group-reasoning"],
|
|
524
|
+
"tool-call": ["group-chainOfThought", "group-tool"],
|
|
525
|
+
source: ["group-sources"],
|
|
526
|
+
})}
|
|
336
527
|
>
|
|
337
528
|
{({ part, children }) => {
|
|
338
529
|
switch (part.type) {
|
|
@@ -344,12 +535,16 @@ const AssistantMessage: FC = () => {
|
|
|
344
535
|
return (
|
|
345
536
|
<PartLayout label="Taking action">{children}</PartLayout>
|
|
346
537
|
);
|
|
538
|
+
case "group-sources":
|
|
539
|
+
return <SourcesLayout>{children}</SourcesLayout>;
|
|
347
540
|
case "text":
|
|
348
541
|
return <MarkdownText />;
|
|
349
542
|
case "reasoning":
|
|
350
543
|
return <Reasoning {...part} />;
|
|
351
544
|
case "tool-call":
|
|
352
545
|
return <ToolCall {...part} />;
|
|
546
|
+
case "source":
|
|
547
|
+
return <Sources {...part} />;
|
|
353
548
|
default:
|
|
354
549
|
return null;
|
|
355
550
|
}
|
|
@@ -367,7 +562,7 @@ const ChainOfThoughtGroup: FC<PropsWithChildren> = ({ children }) => {
|
|
|
367
562
|
<div className="my-2 rounded-lg border">
|
|
368
563
|
<button
|
|
369
564
|
type="button"
|
|
370
|
-
className="flex w-full cursor-pointer items-center gap-2 px-4 py-2
|
|
565
|
+
className="hover:bg-muted/50 flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm font-medium"
|
|
371
566
|
onClick={() => setOpen((o) => !o)}
|
|
372
567
|
>
|
|
373
568
|
{open ? (
|
|
@@ -382,6 +577,15 @@ const ChainOfThoughtGroup: FC<PropsWithChildren> = ({ children }) => {
|
|
|
382
577
|
);
|
|
383
578
|
};
|
|
384
579
|
|
|
580
|
+
const SourcesLayout: FC<PropsWithChildren> = ({ children }) => {
|
|
581
|
+
return (
|
|
582
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
583
|
+
<span className="text-muted-foreground mr-1 text-xs">Sources</span>
|
|
584
|
+
{children}
|
|
585
|
+
</div>
|
|
586
|
+
);
|
|
587
|
+
};
|
|
588
|
+
|
|
385
589
|
const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
|
|
386
590
|
children,
|
|
387
591
|
label,
|
|
@@ -392,7 +596,7 @@ const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
|
|
|
392
596
|
<div className="border-t">
|
|
393
597
|
<button
|
|
394
598
|
type="button"
|
|
395
|
-
className="flex w-full cursor-pointer items-center gap-2 px-4 py-1.5 text-
|
|
599
|
+
className="text-muted-foreground hover:bg-muted/50 flex w-full cursor-pointer items-center gap-2 px-4 py-1.5 text-xs"
|
|
396
600
|
onClick={() => setOpen((o) => !o)}
|
|
397
601
|
>
|
|
398
602
|
{open ? (
|
|
@@ -409,7 +613,7 @@ const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
|
|
|
409
613
|
|
|
410
614
|
const Reasoning: FC<{ text: string }> = ({ text }) => {
|
|
411
615
|
return (
|
|
412
|
-
<p className="
|
|
616
|
+
<p className="text-muted-foreground px-4 py-2 text-sm whitespace-pre-wrap italic">
|
|
413
617
|
{text}
|
|
414
618
|
</p>
|
|
415
619
|
);
|
|
@@ -436,10 +640,10 @@ const Composer: FC = () => {
|
|
|
436
640
|
const isRunning = useThread((state) => state.isRunning);
|
|
437
641
|
|
|
438
642
|
return (
|
|
439
|
-
<ComposerPrimitive.Root className="
|
|
643
|
+
<ComposerPrimitive.Root className="border-input bg-background has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-ring/20 flex w-full flex-col rounded-2xl border px-1 pt-2 transition-shadow outline-none has-[textarea:focus-visible]:ring-2">
|
|
440
644
|
<ComposerPrimitive.Input
|
|
441
645
|
placeholder="Send a message..."
|
|
442
|
-
className="mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none
|
|
646
|
+
className="placeholder:text-muted-foreground mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none"
|
|
443
647
|
rows={1}
|
|
444
648
|
autoFocus
|
|
445
649
|
/>
|
|
@@ -482,63 +686,30 @@ const Composer: FC = () => {
|
|
|
482
686
|
|
|
483
687
|
import {
|
|
484
688
|
AssistantRuntimeProvider,
|
|
485
|
-
|
|
689
|
+
Tools,
|
|
486
690
|
useAui,
|
|
487
691
|
AuiProvider,
|
|
488
692
|
Suggestions,
|
|
489
693
|
} from "@assistant-ui/react";
|
|
490
694
|
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
|
|
491
|
-
import { z } from "zod";
|
|
492
695
|
import { MyThread } from "./MyThread";
|
|
493
696
|
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
|
|
494
|
-
|
|
495
|
-
const ExecuteJsTool = makeAssistantTool({
|
|
496
|
-
toolName: "execute_js",
|
|
497
|
-
description: "Execute JavaScript code and return the result",
|
|
498
|
-
parameters: z.object({
|
|
499
|
-
code: z.string().describe("The JavaScript code to execute"),
|
|
500
|
-
}),
|
|
501
|
-
execute: async ({ code }) => {
|
|
502
|
-
try {
|
|
503
|
-
// biome-ignore lint/security/noGlobalEval: example code
|
|
504
|
-
const result = eval(code);
|
|
505
|
-
return { success: true, result: String(result) };
|
|
506
|
-
} catch (e) {
|
|
507
|
-
return { success: false, error: String(e) };
|
|
508
|
-
}
|
|
509
|
-
},
|
|
510
|
-
render: ({ args, result, status }) => (
|
|
511
|
-
<div className="my-2 rounded-lg border bg-muted/30 p-4 text-sm">
|
|
512
|
-
<p className="mb-1 font-semibold">execute_js</p>
|
|
513
|
-
<pre className="whitespace-pre-wrap rounded bg-background p-2 font-mono text-xs">
|
|
514
|
-
{args.code}
|
|
515
|
-
</pre>
|
|
516
|
-
{status.type !== "running" && result && (
|
|
517
|
-
<div className="mt-2 border-t pt-2">
|
|
518
|
-
<p className="font-semibold text-muted-foreground">
|
|
519
|
-
{result.success ? "Result:" : "Error:"}
|
|
520
|
-
</p>
|
|
521
|
-
<pre className="whitespace-pre-wrap font-mono text-xs">
|
|
522
|
-
{result.success ? result.result : result.error}
|
|
523
|
-
</pre>
|
|
524
|
-
</div>
|
|
525
|
-
)}
|
|
526
|
-
</div>
|
|
527
|
-
),
|
|
528
|
-
});
|
|
697
|
+
import toolkit from "./toolkit";
|
|
529
698
|
|
|
530
699
|
function MyThreadWithSuggestions() {
|
|
531
700
|
const aui = useAui({
|
|
532
701
|
suggestions: Suggestions([
|
|
533
702
|
{
|
|
534
703
|
title: "Calculate Fibonacci(20)",
|
|
535
|
-
label: "
|
|
536
|
-
prompt:
|
|
704
|
+
label: "with chain of thought",
|
|
705
|
+
prompt:
|
|
706
|
+
"Calculate the 20th Fibonacci number using JavaScript and show your reasoning.",
|
|
537
707
|
},
|
|
538
708
|
{
|
|
539
|
-
title: "
|
|
540
|
-
label: "show
|
|
541
|
-
prompt:
|
|
709
|
+
title: "Research with citations",
|
|
710
|
+
label: "show sources",
|
|
711
|
+
prompt:
|
|
712
|
+
"Research recent developments in renewable energy and cite your sources.",
|
|
542
713
|
},
|
|
543
714
|
]),
|
|
544
715
|
});
|
|
@@ -553,10 +724,12 @@ export default function Home() {
|
|
|
553
724
|
const runtime = useChatRuntime({
|
|
554
725
|
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
555
726
|
});
|
|
727
|
+
const aui = useAui({
|
|
728
|
+
tools: Tools({ toolkit }),
|
|
729
|
+
});
|
|
556
730
|
|
|
557
731
|
return (
|
|
558
|
-
<AssistantRuntimeProvider runtime={runtime}>
|
|
559
|
-
<ExecuteJsTool />
|
|
732
|
+
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
|
|
560
733
|
<div className="h-full">
|
|
561
734
|
<MyThreadWithSuggestions />
|
|
562
735
|
</div>
|
|
@@ -566,6 +739,59 @@ export default function Home() {
|
|
|
566
739
|
|
|
567
740
|
```
|
|
568
741
|
|
|
742
|
+
## app/toolkit.tsx
|
|
743
|
+
|
|
744
|
+
```tsx
|
|
745
|
+
"use generative";
|
|
746
|
+
|
|
747
|
+
import { defineToolkit } from "@assistant-ui/react";
|
|
748
|
+
import { z } from "zod";
|
|
749
|
+
|
|
750
|
+
type ExecuteJsResult =
|
|
751
|
+
| { success: true; result: string }
|
|
752
|
+
| { success: false; error: string };
|
|
753
|
+
|
|
754
|
+
export default defineToolkit({
|
|
755
|
+
execute_js: {
|
|
756
|
+
description: "Execute JavaScript code and return the result",
|
|
757
|
+
parameters: z.object({
|
|
758
|
+
code: z.string().describe("The JavaScript code to execute"),
|
|
759
|
+
}),
|
|
760
|
+
execute: async ({ code }) => {
|
|
761
|
+
"use client";
|
|
762
|
+
try {
|
|
763
|
+
const result = eval(code);
|
|
764
|
+
return { success: true, result: String(result) };
|
|
765
|
+
} catch (e) {
|
|
766
|
+
return { success: false, error: String(e) };
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
render: ({ args, result, status }) => {
|
|
770
|
+
const output = result as ExecuteJsResult | undefined;
|
|
771
|
+
return (
|
|
772
|
+
<div className="bg-muted/30 my-2 rounded-lg border p-4 text-sm">
|
|
773
|
+
<p className="mb-1 font-semibold">execute_js</p>
|
|
774
|
+
<pre className="bg-background rounded p-2 font-mono text-xs whitespace-pre-wrap">
|
|
775
|
+
{args.code}
|
|
776
|
+
</pre>
|
|
777
|
+
{status.type !== "running" && output && (
|
|
778
|
+
<div className="mt-2 border-t pt-2">
|
|
779
|
+
<p className="text-muted-foreground font-semibold">
|
|
780
|
+
{output.success ? "Result:" : "Error:"}
|
|
781
|
+
</p>
|
|
782
|
+
<pre className="font-mono text-xs whitespace-pre-wrap">
|
|
783
|
+
{output.success ? output.result : output.error}
|
|
784
|
+
</pre>
|
|
785
|
+
</div>
|
|
786
|
+
)}
|
|
787
|
+
</div>
|
|
788
|
+
);
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
```
|
|
794
|
+
|
|
569
795
|
## components.json
|
|
570
796
|
|
|
571
797
|
```json
|
|
@@ -599,12 +825,13 @@ export default function Home() {
|
|
|
599
825
|
## next.config.js
|
|
600
826
|
|
|
601
827
|
```javascript
|
|
828
|
+
import { withAui } from "@assistant-ui/next";
|
|
602
829
|
/** @type {import('next').NextConfig} */
|
|
603
830
|
const nextConfig = {
|
|
604
831
|
transpilePackages: ["@assistant-ui/react", "@assistant-ui/react-ai-sdk"],
|
|
605
832
|
};
|
|
606
833
|
|
|
607
|
-
export default nextConfig;
|
|
834
|
+
export default withAui(nextConfig);
|
|
608
835
|
|
|
609
836
|
```
|
|
610
837
|
|
|
@@ -622,26 +849,27 @@ export default nextConfig;
|
|
|
622
849
|
"start": "next start"
|
|
623
850
|
},
|
|
624
851
|
"dependencies": {
|
|
625
|
-
"@ai-sdk/openai": "^3.0.
|
|
852
|
+
"@ai-sdk/openai": "^3.0.68",
|
|
626
853
|
"@assistant-ui/react": "workspace:*",
|
|
627
854
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
628
855
|
"@assistant-ui/react-markdown": "workspace:*",
|
|
629
856
|
"@assistant-ui/ui": "workspace:*",
|
|
630
|
-
"ai": "^6.0.
|
|
857
|
+
"ai": "^6.0.197",
|
|
631
858
|
"class-variance-authority": "^0.7.1",
|
|
632
859
|
"clsx": "^2.1.1",
|
|
633
|
-
"lucide-react": "^1.
|
|
634
|
-
"next": "^16.2.
|
|
635
|
-
"react": "^19.2.
|
|
636
|
-
"react-dom": "^19.2.
|
|
860
|
+
"lucide-react": "^1.17.0",
|
|
861
|
+
"next": "^16.2.7",
|
|
862
|
+
"react": "^19.2.7",
|
|
863
|
+
"react-dom": "^19.2.7",
|
|
637
864
|
"tailwind-merge": "^3.6.0",
|
|
638
865
|
"zod": "^4.4.3"
|
|
639
866
|
},
|
|
640
867
|
"devDependencies": {
|
|
868
|
+
"@assistant-ui/next": "workspace:*",
|
|
641
869
|
"@assistant-ui/x-buildutils": "workspace:*",
|
|
642
870
|
"@tailwindcss/postcss": "^4.3.0",
|
|
643
|
-
"@types/node": "^25.9.
|
|
644
|
-
"@types/react": "^19.2.
|
|
871
|
+
"@types/node": "^25.9.2",
|
|
872
|
+
"@types/react": "^19.2.17",
|
|
645
873
|
"@types/react-dom": "^19.2.3",
|
|
646
874
|
"postcss": "^8.5.15",
|
|
647
875
|
"tailwindcss": "^4.3.0",
|
|
@@ -652,6 +880,97 @@ export default nextConfig;
|
|
|
652
880
|
|
|
653
881
|
```
|
|
654
882
|
|
|
883
|
+
## README.md
|
|
884
|
+
|
|
885
|
+
```markdown
|
|
886
|
+
# Chain of Thought Example
|
|
887
|
+
|
|
888
|
+
A real chat flow that renders reasoning, tool calls, and source citations as grouped message parts. Demonstrates `MessagePrimitive.GroupedParts` with a nested `groupBy` path, plus structured `source` parts emitted from a search tool.
|
|
889
|
+
|
|
890
|
+
## Quick start
|
|
891
|
+
|
|
892
|
+
### Using the CLI
|
|
893
|
+
|
|
894
|
+
```bash
|
|
895
|
+
npx assistant-ui@latest create my-app --example with-chain-of-thought
|
|
896
|
+
cd my-app
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
### Environment variables
|
|
900
|
+
|
|
901
|
+
`OPENAI_API_KEY` is optional. With the key set the route runs a real reasoning model and a `search_web` tool; without it the route streams a deterministic mock response with the same chunk shapes, so the UI is fully demonstrable offline.
|
|
902
|
+
|
|
903
|
+
Create `.env.local`:
|
|
904
|
+
|
|
905
|
+
```
|
|
906
|
+
OPENAI_API_KEY=sk-...
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
### Run
|
|
910
|
+
|
|
911
|
+
```bash
|
|
912
|
+
npm install
|
|
913
|
+
npm run dev
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
Open [http://localhost:3000](http://localhost:3000).
|
|
917
|
+
|
|
918
|
+
## What the example demonstrates
|
|
919
|
+
|
|
920
|
+
| Concern | Where it lives |
|
|
921
|
+
|---|---|
|
|
922
|
+
| Real chat flow with `useChatRuntime` | `app/page.tsx` |
|
|
923
|
+
| Reasoning, tool calls, source citations streamed as message parts | `app/api/chat/route.ts` |
|
|
924
|
+
| Nested adjacency-based grouping with `MessagePrimitive.GroupedParts` | `app/MyThread.tsx` |
|
|
925
|
+
| `Sources` component rendering `source` parts | `app/MyThread.tsx` |
|
|
926
|
+
| Fallback path that runs without an API key | `app/api/chat/route.ts` (`streamFallback`) |
|
|
927
|
+
|
|
928
|
+
## How parts are grouped
|
|
929
|
+
|
|
930
|
+
The thread groups three part types under one collapsible "Thinking" section, then renders source badges in a row below it. The grouping is configured on `MessagePrimitive.GroupedParts`:
|
|
931
|
+
|
|
932
|
+
```tsx
|
|
933
|
+
<MessagePrimitive.GroupedParts
|
|
934
|
+
groupBy={(part) => {
|
|
935
|
+
if (part.type === "reasoning") return ["group-chainOfThought", "group-reasoning"];
|
|
936
|
+
if (part.type === "tool-call") return ["group-chainOfThought", "group-tool"];
|
|
937
|
+
if (part.type === "source") return ["group-chainOfThought", "group-sources"];
|
|
938
|
+
return null;
|
|
939
|
+
}}
|
|
940
|
+
>
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
Each part is mapped to a nested group key path. Adjacent parts that share a path are coalesced and rendered through the matching `group-*` case in the render function. `null` leaves a part ungrouped.
|
|
944
|
+
|
|
945
|
+
This is the canonical pattern. For non-adjacent clustering (parts that share an identifier but appear at different positions in the message), see [`/docs/ui/part-grouping`](https://www.assistant-ui.com/docs/ui/part-grouping).
|
|
946
|
+
|
|
947
|
+
## How sources are emitted
|
|
948
|
+
|
|
949
|
+
The route emits structured `source-url` chunks; `@assistant-ui/react-ai-sdk` converts them to `source` message parts that the `Sources` component renders as badges. They are not URLs scraped from assistant text.
|
|
950
|
+
|
|
951
|
+
```ts
|
|
952
|
+
writer.write({
|
|
953
|
+
type: "source-url",
|
|
954
|
+
sourceId: "src-1",
|
|
955
|
+
url: "https://example.com/report",
|
|
956
|
+
title: "Example report",
|
|
957
|
+
});
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
In the real path the `search_web` tool returns sources in its output, and the route writes one `source-url` chunk per result immediately after the `tool-output-available` chunk. In the fallback path the same shape is written directly. The UI rendering is identical in both modes.
|
|
961
|
+
|
|
962
|
+
## Why these features live together
|
|
963
|
+
|
|
964
|
+
Chain-of-thought, tool calls, and citation sources are the parts of an assistant turn a user typically wants to inspect together. Grouping them under one collapsible keeps the message tidy by default and reveals the full reasoning trail on demand.
|
|
965
|
+
|
|
966
|
+
## Related documentation
|
|
967
|
+
|
|
968
|
+
- [Message Part Grouping](https://www.assistant-ui.com/docs/ui/part-grouping)
|
|
969
|
+
- [Sources component](https://www.assistant-ui.com/docs/ui/sources)
|
|
970
|
+
- [AI SDK v6 runtime](https://www.assistant-ui.com/docs/runtimes/ai-sdk/v6)
|
|
971
|
+
|
|
972
|
+
```
|
|
973
|
+
|
|
655
974
|
## tsconfig.json
|
|
656
975
|
|
|
657
976
|
```json
|