@assistant-ui/mcp-docs-server 0.1.32 → 0.1.33
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 +15 -17
- package/.docs/organized/code-examples/with-a2a.md +19 -19
- package/.docs/organized/code-examples/with-ag-ui.md +25 -20
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +6 -4
- package/.docs/organized/code-examples/with-artifacts.md +34 -28
- package/.docs/organized/code-examples/with-assistant-transport.md +59 -47
- package/.docs/organized/code-examples/with-browser-extension.md +17 -5
- package/.docs/organized/code-examples/with-chain-of-thought.md +376 -78
- package/.docs/organized/code-examples/with-cloud-standalone.md +20 -19
- package/.docs/organized/code-examples/with-cloud.md +6 -4
- package/.docs/organized/code-examples/with-custom-thread-list.md +9 -7
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +18 -16
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +9 -8
- package/.docs/organized/code-examples/with-expo.md +23 -17
- package/.docs/organized/code-examples/with-external-store.md +4 -2
- package/.docs/organized/code-examples/with-ffmpeg.md +317 -280
- package/.docs/organized/code-examples/with-generative-ui.md +1018 -214
- package/.docs/organized/code-examples/with-google-adk.md +4 -2
- package/.docs/organized/code-examples/with-heat-graph.md +8 -6
- package/.docs/organized/code-examples/with-image-generation.md +19 -17
- package/.docs/organized/code-examples/with-interactables.md +312 -234
- package/.docs/organized/code-examples/with-langchain.md +9 -7
- package/.docs/organized/code-examples/with-langgraph.md +82 -78
- package/.docs/organized/code-examples/with-livekit.md +19 -17
- package/.docs/organized/code-examples/with-mcp.md +13 -7
- package/.docs/organized/code-examples/with-opencode.md +61 -61
- package/.docs/organized/code-examples/with-react-hook-form.md +14 -12
- package/.docs/organized/code-examples/with-react-ink.md +2 -2
- package/.docs/organized/code-examples/with-react-router.md +11 -9
- package/.docs/organized/code-examples/with-resumable-stream.md +10 -9
- package/.docs/organized/code-examples/with-store.md +14 -12
- package/.docs/organized/code-examples/with-tanstack.md +8 -9
- package/.docs/organized/code-examples/with-tap-runtime.md +21 -19
- package/.docs/raw/docs/(docs)/architecture.mdx +43 -2
- package/.docs/raw/docs/(docs)/cli.mdx +1 -2
- package/.docs/raw/docs/(docs)/copilots/model-context.mdx +34 -26
- package/.docs/raw/docs/(docs)/copilots/motivation.mdx +32 -26
- 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 +14 -14
- package/.docs/raw/docs/(reference)/api-reference/external-store/runtime.mdx +11 -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 +32 -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 +6 -7
- package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +58 -46
- package/.docs/raw/docs/cloud/ai-sdk.mdx +0 -2
- 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/hooks.mdx +98 -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 +226 -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 +57 -82
- 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/ai-sdk/v6.mdx +122 -1
- package/.docs/raw/docs/runtimes/concepts/threads.mdx +7 -1
- package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +18 -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 +1 -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 +136 -0
- package/.docs/raw/docs/tools/defining-tools.mdx +413 -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 +76 -0
- package/.docs/raw/docs/{guides → tools}/interactables.mdx +1 -1
- package/.docs/raw/docs/{integrations/tools → tools}/mcp.mdx +77 -50
- package/.docs/raw/docs/{guides → tools}/multi-agent.mdx +17 -19
- package/.docs/raw/docs/tools/tool-ui.mdx +967 -0
- package/.docs/raw/docs/{integrations/tools/react-mcp.mdx → tools/user-managed-mcp.mdx} +3 -3
- 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/part-grouping.mdx +84 -50
- package/.docs/raw/docs/ui/reasoning.mdx +4 -5
- package/.docs/raw/docs/ui/tool-group.mdx +5 -6
- package/.docs/raw/docs/utilities/heat-graph.mdx +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/prepare-docs/code-examples.d.ts.map +1 -1
- package/dist/prepare-docs/copy-raw.d.ts.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/package.json +3 -3
- 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/{guides → tools}/mcp-apps.mdx +0 -0
|
@@ -3,46 +3,132 @@
|
|
|
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";
|
|
16
|
+
import type { UIMessage, UIMessageStreamWriter } from "ai";
|
|
15
17
|
import { frontendTools } from "@assistant-ui/react-ai-sdk";
|
|
16
18
|
import { z } from "zod";
|
|
17
19
|
|
|
18
20
|
export const maxDuration = 30;
|
|
19
21
|
|
|
22
|
+
type SearchResult = { id: string; url: string; title: string };
|
|
23
|
+
|
|
24
|
+
const searchSources = (query: string): SearchResult[] => {
|
|
25
|
+
const q = query.toLowerCase();
|
|
26
|
+
if (q.includes("fibonacci")) {
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
id: "src-fib-1",
|
|
30
|
+
url: "https://en.wikipedia.org/wiki/Fibonacci_number",
|
|
31
|
+
title: "Fibonacci number (Wikipedia)",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "src-fib-2",
|
|
35
|
+
url: "https://oeis.org/A000045",
|
|
36
|
+
title: "OEIS A000045: Fibonacci numbers",
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
q.includes("renewable") ||
|
|
42
|
+
q.includes("energy") ||
|
|
43
|
+
q.includes("climate")
|
|
44
|
+
) {
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
id: "src-iea",
|
|
48
|
+
url: "https://www.iea.org/reports/renewables-2024",
|
|
49
|
+
title: "IEA Renewables 2024",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "src-irena",
|
|
53
|
+
url: "https://www.irena.org/Publications",
|
|
54
|
+
title: "IRENA Publications",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
id: "src-wiki",
|
|
61
|
+
url: `https://en.wikipedia.org/wiki/Special:Search?search=${encodeURIComponent(query)}`,
|
|
62
|
+
title: "Wikipedia search results",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "src-web",
|
|
66
|
+
url: `https://duckduckgo.com/?q=${encodeURIComponent(query)}`,
|
|
67
|
+
title: `Web results for: ${query}`,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
};
|
|
71
|
+
|
|
20
72
|
export async function POST(req: Request) {
|
|
21
73
|
const {
|
|
22
74
|
messages,
|
|
23
75
|
tools,
|
|
24
76
|
}: { messages: UIMessage[]; tools: Record<string, any> } = await req.json();
|
|
25
77
|
|
|
78
|
+
const uiStream = createUIMessageStream({
|
|
79
|
+
execute: async ({ writer }) => {
|
|
80
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
81
|
+
await streamFallback(writer, messages);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await streamModel(writer, messages, tools);
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return new Response(
|
|
89
|
+
uiStream
|
|
90
|
+
.pipeThrough(new JsonToSseTransformStream())
|
|
91
|
+
.pipeThrough(new TextEncoderStream()),
|
|
92
|
+
{
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "text/event-stream",
|
|
95
|
+
"Cache-Control": "no-cache",
|
|
96
|
+
Connection: "keep-alive",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function streamModel(
|
|
103
|
+
writer: UIMessageStreamWriter,
|
|
104
|
+
messages: UIMessage[],
|
|
105
|
+
frontendToolDefs: Record<string, any>,
|
|
106
|
+
) {
|
|
107
|
+
const toolNameByCall = new Map<string, string>();
|
|
108
|
+
const openai = createOpenAI({
|
|
109
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
110
|
+
...(process.env.OPENAI_BASE_URL && {
|
|
111
|
+
baseURL: process.env.OPENAI_BASE_URL,
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
|
|
26
115
|
const result = streamText({
|
|
27
|
-
//
|
|
116
|
+
// Reasoning model so the chain-of-thought group has real content.
|
|
28
117
|
model: openai("gpt-5.4-mini"),
|
|
29
118
|
messages: await convertToModelMessages(messages),
|
|
30
119
|
stopWhen: stepCountIs(10),
|
|
31
120
|
tools: {
|
|
32
|
-
|
|
33
|
-
...frontendTools(tools),
|
|
34
|
-
|
|
35
|
-
// Backend-only tools
|
|
121
|
+
...frontendTools(frontendToolDefs),
|
|
36
122
|
get_current_weather: tool({
|
|
37
123
|
description: "Get the current weather for a city",
|
|
38
|
-
inputSchema: zodSchema(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
124
|
+
inputSchema: zodSchema(z.object({ city: z.string() })),
|
|
125
|
+
execute: async ({ city }) => `The weather in ${city} is sunny, 72°F`,
|
|
126
|
+
}),
|
|
127
|
+
search_web: tool({
|
|
128
|
+
description:
|
|
129
|
+
"Search the web for citations on a topic. Returns a list of source URLs and titles the assistant should consult.",
|
|
130
|
+
inputSchema: zodSchema(z.object({ query: z.string() })),
|
|
131
|
+
execute: async ({ query }) => ({ sources: searchSources(query) }),
|
|
46
132
|
}),
|
|
47
133
|
},
|
|
48
134
|
providerOptions: {
|
|
@@ -53,9 +139,106 @@ export async function POST(req: Request) {
|
|
|
53
139
|
},
|
|
54
140
|
});
|
|
55
141
|
|
|
56
|
-
|
|
142
|
+
// Forward model chunks, then synthesize `source-url` parts from each
|
|
143
|
+
// `search_web` tool output so the UI renders them as `source` parts. OpenAI
|
|
144
|
+
// does not natively emit `source-url` chunks without web search, so the route
|
|
145
|
+
// produces them deterministically from the tool result.
|
|
146
|
+
for await (const chunk of result.toUIMessageStream({ sendReasoning: true })) {
|
|
147
|
+
writer.write(chunk);
|
|
148
|
+
|
|
149
|
+
if (chunk.type === "tool-input-available") {
|
|
150
|
+
toolNameByCall.set(chunk.toolCallId, chunk.toolName);
|
|
151
|
+
}
|
|
152
|
+
if (
|
|
153
|
+
chunk.type === "tool-output-available" &&
|
|
154
|
+
toolNameByCall.get(chunk.toolCallId) === "search_web"
|
|
155
|
+
) {
|
|
156
|
+
const output = chunk.output as { sources?: SearchResult[] };
|
|
157
|
+
for (const source of output.sources ?? []) {
|
|
158
|
+
writer.write({
|
|
159
|
+
type: "source-url",
|
|
160
|
+
sourceId: `${source.id}-${crypto.randomUUID()}`,
|
|
161
|
+
url: source.url,
|
|
162
|
+
title: source.title,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function streamFallback(
|
|
170
|
+
writer: UIMessageStreamWriter,
|
|
171
|
+
messages: UIMessage[],
|
|
172
|
+
) {
|
|
173
|
+
const lastUserText =
|
|
174
|
+
messages
|
|
175
|
+
.filter((m) => m.role === "user")
|
|
176
|
+
.at(-1)
|
|
177
|
+
?.parts.flatMap((p) => (p.type === "text" ? [p.text] : []))
|
|
178
|
+
.join(" ") ?? "your question";
|
|
179
|
+
|
|
180
|
+
const messageId = `msg-${crypto.randomUUID()}`;
|
|
181
|
+
const reasoningId = `r-${crypto.randomUUID()}`;
|
|
182
|
+
const textId = `t-${crypto.randomUUID()}`;
|
|
183
|
+
const toolCallId = `call-${crypto.randomUUID()}`;
|
|
184
|
+
const sources = searchSources(lastUserText);
|
|
185
|
+
|
|
186
|
+
writer.write({ type: "start", messageId });
|
|
187
|
+
writer.write({ type: "start-step" });
|
|
188
|
+
|
|
189
|
+
writer.write({ type: "reasoning-start", id: reasoningId });
|
|
190
|
+
const reasoning =
|
|
191
|
+
"OPENAI_API_KEY is not set, so this response is generated deterministically by the route. " +
|
|
192
|
+
"A real run emits the same chunk shapes from the model: a reasoning step, " +
|
|
193
|
+
"a search_web tool call, source-url parts derived from the tool output, " +
|
|
194
|
+
"and a final answer that summarises the sources.";
|
|
195
|
+
for (const word of reasoning.match(/\S+\s*|\s+/g) ?? [reasoning]) {
|
|
196
|
+
writer.write({ type: "reasoning-delta", id: reasoningId, delta: word });
|
|
197
|
+
await sleep(15);
|
|
198
|
+
}
|
|
199
|
+
writer.write({ type: "reasoning-end", id: reasoningId });
|
|
200
|
+
|
|
201
|
+
writer.write({
|
|
202
|
+
type: "tool-input-available",
|
|
203
|
+
toolCallId,
|
|
204
|
+
toolName: "search_web",
|
|
205
|
+
input: { query: lastUserText },
|
|
206
|
+
});
|
|
207
|
+
await sleep(150);
|
|
208
|
+
writer.write({
|
|
209
|
+
type: "tool-output-available",
|
|
210
|
+
toolCallId,
|
|
211
|
+
output: { sources },
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
for (const source of sources) {
|
|
215
|
+
writer.write({
|
|
216
|
+
type: "source-url",
|
|
217
|
+
sourceId: `${source.id}-${crypto.randomUUID()}`,
|
|
218
|
+
url: source.url,
|
|
219
|
+
title: source.title,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
writer.write({ type: "text-start", id: textId });
|
|
224
|
+
const answer =
|
|
225
|
+
`\n\nMock answer for: "${lastUserText}". ` +
|
|
226
|
+
"The badges above are structured `source` message parts emitted as " +
|
|
227
|
+
"`source-url` chunks, not URLs scraped from text. Set `OPENAI_API_KEY` " +
|
|
228
|
+
"to switch to a real model run.";
|
|
229
|
+
for (const word of answer.match(/\S+\s*|\s+/g) ?? [answer]) {
|
|
230
|
+
writer.write({ type: "text-delta", id: textId, delta: word });
|
|
231
|
+
await sleep(15);
|
|
232
|
+
}
|
|
233
|
+
writer.write({ type: "text-end", id: textId });
|
|
234
|
+
|
|
235
|
+
writer.write({ type: "finish-step" });
|
|
236
|
+
writer.write({ type: "finish" });
|
|
57
237
|
}
|
|
58
238
|
|
|
239
|
+
const sleep = (ms: number) =>
|
|
240
|
+
new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
241
|
+
|
|
59
242
|
```
|
|
60
243
|
|
|
61
244
|
## app/globals.css
|
|
@@ -194,7 +377,8 @@ import "./globals.css";
|
|
|
194
377
|
|
|
195
378
|
export const metadata: Metadata = {
|
|
196
379
|
title: "Chain of Thought Example",
|
|
197
|
-
description:
|
|
380
|
+
description:
|
|
381
|
+
"Reasoning, tool calls, and source citations rendered as grouped message parts.",
|
|
198
382
|
};
|
|
199
383
|
|
|
200
384
|
export default function RootLayout({
|
|
@@ -218,10 +402,12 @@ export default function RootLayout({
|
|
|
218
402
|
|
|
219
403
|
import { type FC, type PropsWithChildren, useState } from "react";
|
|
220
404
|
import { MarkdownText } from "@/components/assistant-ui/markdown-text";
|
|
405
|
+
import { Sources } from "@/components/assistant-ui/sources";
|
|
221
406
|
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
|
|
222
407
|
import { Button } from "@/components/ui/button";
|
|
223
408
|
import {
|
|
224
409
|
ComposerPrimitive,
|
|
410
|
+
groupPartByType,
|
|
225
411
|
MessagePrimitive,
|
|
226
412
|
SuggestionPrimitive,
|
|
227
413
|
ThreadPrimitive,
|
|
@@ -238,7 +424,7 @@ import {
|
|
|
238
424
|
export const MyThread: FC = () => {
|
|
239
425
|
return (
|
|
240
426
|
<ThreadPrimitive.Root
|
|
241
|
-
className="flex h-full flex-col
|
|
427
|
+
className="bg-background flex h-full flex-col"
|
|
242
428
|
style={{ ["--thread-max-width" as string]: "44rem" }}
|
|
243
429
|
>
|
|
244
430
|
<ThreadPrimitive.Viewport className="flex flex-1 flex-col overflow-y-scroll scroll-smooth px-4 pt-8">
|
|
@@ -278,7 +464,9 @@ const ThreadWelcome: FC = () => {
|
|
|
278
464
|
return (
|
|
279
465
|
<div className="mx-auto flex w-full max-w-(--thread-max-width) grow flex-col justify-center gap-6">
|
|
280
466
|
<div>
|
|
281
|
-
<h1 className="font-semibold
|
|
467
|
+
<h1 className="text-2xl font-semibold">
|
|
468
|
+
What should we think through?
|
|
469
|
+
</h1>
|
|
282
470
|
</div>
|
|
283
471
|
<div className="flex flex-wrap gap-2">
|
|
284
472
|
<ThreadPrimitive.Suggestions>
|
|
@@ -295,7 +483,7 @@ const ThreadSuggestionItem: FC = () => {
|
|
|
295
483
|
<Button
|
|
296
484
|
type="button"
|
|
297
485
|
variant="ghost"
|
|
298
|
-
className="h-auto w-full flex-col items-start justify-start gap-1 rounded-2xl border
|
|
486
|
+
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
487
|
>
|
|
300
488
|
<SuggestionPrimitive.Title className="font-medium" />
|
|
301
489
|
<SuggestionPrimitive.Description className="text-muted-foreground empty:hidden" />
|
|
@@ -308,7 +496,7 @@ const UserMessage: FC = () => {
|
|
|
308
496
|
return (
|
|
309
497
|
<MessagePrimitive.Root className="mx-auto w-full max-w-(--thread-max-width) py-3">
|
|
310
498
|
<div className="flex justify-end">
|
|
311
|
-
<div className="max-w-[80%] rounded-2xl
|
|
499
|
+
<div className="bg-primary text-primary-foreground max-w-[80%] rounded-2xl px-4 py-2">
|
|
312
500
|
<MessagePrimitive.Parts>
|
|
313
501
|
{({ part }) => {
|
|
314
502
|
if (part.type === "text") return <Text {...part} />;
|
|
@@ -326,13 +514,11 @@ const AssistantMessage: FC = () => {
|
|
|
326
514
|
<MessagePrimitive.Root className="mx-auto w-full max-w-(--thread-max-width) py-3">
|
|
327
515
|
<div className="flex flex-col gap-2 px-2 leading-relaxed">
|
|
328
516
|
<MessagePrimitive.GroupedParts
|
|
329
|
-
groupBy={(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return null;
|
|
335
|
-
}}
|
|
517
|
+
groupBy={groupPartByType({
|
|
518
|
+
reasoning: ["group-chainOfThought", "group-reasoning"],
|
|
519
|
+
"tool-call": ["group-chainOfThought", "group-tool"],
|
|
520
|
+
source: ["group-sources"],
|
|
521
|
+
})}
|
|
336
522
|
>
|
|
337
523
|
{({ part, children }) => {
|
|
338
524
|
switch (part.type) {
|
|
@@ -344,12 +530,16 @@ const AssistantMessage: FC = () => {
|
|
|
344
530
|
return (
|
|
345
531
|
<PartLayout label="Taking action">{children}</PartLayout>
|
|
346
532
|
);
|
|
533
|
+
case "group-sources":
|
|
534
|
+
return <SourcesLayout>{children}</SourcesLayout>;
|
|
347
535
|
case "text":
|
|
348
536
|
return <MarkdownText />;
|
|
349
537
|
case "reasoning":
|
|
350
538
|
return <Reasoning {...part} />;
|
|
351
539
|
case "tool-call":
|
|
352
540
|
return <ToolCall {...part} />;
|
|
541
|
+
case "source":
|
|
542
|
+
return <Sources {...part} />;
|
|
353
543
|
default:
|
|
354
544
|
return null;
|
|
355
545
|
}
|
|
@@ -367,7 +557,7 @@ const ChainOfThoughtGroup: FC<PropsWithChildren> = ({ children }) => {
|
|
|
367
557
|
<div className="my-2 rounded-lg border">
|
|
368
558
|
<button
|
|
369
559
|
type="button"
|
|
370
|
-
className="flex w-full cursor-pointer items-center gap-2 px-4 py-2
|
|
560
|
+
className="hover:bg-muted/50 flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm font-medium"
|
|
371
561
|
onClick={() => setOpen((o) => !o)}
|
|
372
562
|
>
|
|
373
563
|
{open ? (
|
|
@@ -382,6 +572,15 @@ const ChainOfThoughtGroup: FC<PropsWithChildren> = ({ children }) => {
|
|
|
382
572
|
);
|
|
383
573
|
};
|
|
384
574
|
|
|
575
|
+
const SourcesLayout: FC<PropsWithChildren> = ({ children }) => {
|
|
576
|
+
return (
|
|
577
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
578
|
+
<span className="text-muted-foreground mr-1 text-xs">Sources</span>
|
|
579
|
+
{children}
|
|
580
|
+
</div>
|
|
581
|
+
);
|
|
582
|
+
};
|
|
583
|
+
|
|
385
584
|
const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
|
|
386
585
|
children,
|
|
387
586
|
label,
|
|
@@ -392,7 +591,7 @@ const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
|
|
|
392
591
|
<div className="border-t">
|
|
393
592
|
<button
|
|
394
593
|
type="button"
|
|
395
|
-
className="flex w-full cursor-pointer items-center gap-2 px-4 py-1.5 text-
|
|
594
|
+
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
595
|
onClick={() => setOpen((o) => !o)}
|
|
397
596
|
>
|
|
398
597
|
{open ? (
|
|
@@ -409,7 +608,7 @@ const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
|
|
|
409
608
|
|
|
410
609
|
const Reasoning: FC<{ text: string }> = ({ text }) => {
|
|
411
610
|
return (
|
|
412
|
-
<p className="
|
|
611
|
+
<p className="text-muted-foreground px-4 py-2 text-sm whitespace-pre-wrap italic">
|
|
413
612
|
{text}
|
|
414
613
|
</p>
|
|
415
614
|
);
|
|
@@ -436,10 +635,10 @@ const Composer: FC = () => {
|
|
|
436
635
|
const isRunning = useThread((state) => state.isRunning);
|
|
437
636
|
|
|
438
637
|
return (
|
|
439
|
-
<ComposerPrimitive.Root className="
|
|
638
|
+
<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
639
|
<ComposerPrimitive.Input
|
|
441
640
|
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
|
|
641
|
+
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
642
|
rows={1}
|
|
444
643
|
autoFocus
|
|
445
644
|
/>
|
|
@@ -482,7 +681,8 @@ const Composer: FC = () => {
|
|
|
482
681
|
|
|
483
682
|
import {
|
|
484
683
|
AssistantRuntimeProvider,
|
|
485
|
-
|
|
684
|
+
Tools,
|
|
685
|
+
type Toolkit,
|
|
486
686
|
useAui,
|
|
487
687
|
AuiProvider,
|
|
488
688
|
Suggestions,
|
|
@@ -492,53 +692,56 @@ import { z } from "zod";
|
|
|
492
692
|
import { MyThread } from "./MyThread";
|
|
493
693
|
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
|
|
494
694
|
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
695
|
+
const toolkit = {
|
|
696
|
+
execute_js: {
|
|
697
|
+
type: "frontend",
|
|
698
|
+
description: "Execute JavaScript code and return the result",
|
|
699
|
+
parameters: z.object({
|
|
700
|
+
code: z.string().describe("The JavaScript code to execute"),
|
|
701
|
+
}),
|
|
702
|
+
execute: async ({ code }) => {
|
|
703
|
+
try {
|
|
704
|
+
const result = eval(code);
|
|
705
|
+
return { success: true, result: String(result) };
|
|
706
|
+
} catch (e) {
|
|
707
|
+
return { success: false, error: String(e) };
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
render: ({ args, result, status }) => (
|
|
711
|
+
<div className="bg-muted/30 my-2 rounded-lg border p-4 text-sm">
|
|
712
|
+
<p className="mb-1 font-semibold">execute_js</p>
|
|
713
|
+
<pre className="bg-background rounded p-2 font-mono text-xs whitespace-pre-wrap">
|
|
714
|
+
{args.code}
|
|
715
|
+
</pre>
|
|
716
|
+
{status.type !== "running" && result && (
|
|
717
|
+
<div className="mt-2 border-t pt-2">
|
|
718
|
+
<p className="text-muted-foreground font-semibold">
|
|
719
|
+
{result.success ? "Result:" : "Error:"}
|
|
720
|
+
</p>
|
|
721
|
+
<pre className="font-mono text-xs whitespace-pre-wrap">
|
|
722
|
+
{result.success ? result.result : result.error}
|
|
723
|
+
</pre>
|
|
724
|
+
</div>
|
|
725
|
+
)}
|
|
726
|
+
</div>
|
|
727
|
+
),
|
|
509
728
|
},
|
|
510
|
-
|
|
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
|
-
});
|
|
729
|
+
} satisfies Toolkit;
|
|
529
730
|
|
|
530
731
|
function MyThreadWithSuggestions() {
|
|
531
732
|
const aui = useAui({
|
|
532
733
|
suggestions: Suggestions([
|
|
533
734
|
{
|
|
534
735
|
title: "Calculate Fibonacci(20)",
|
|
535
|
-
label: "
|
|
536
|
-
prompt:
|
|
736
|
+
label: "with chain of thought",
|
|
737
|
+
prompt:
|
|
738
|
+
"Calculate the 20th Fibonacci number using JavaScript and show your reasoning.",
|
|
537
739
|
},
|
|
538
740
|
{
|
|
539
|
-
title: "
|
|
540
|
-
label: "show
|
|
541
|
-
prompt:
|
|
741
|
+
title: "Research with citations",
|
|
742
|
+
label: "show sources",
|
|
743
|
+
prompt:
|
|
744
|
+
"Research recent developments in renewable energy and cite your sources.",
|
|
542
745
|
},
|
|
543
746
|
]),
|
|
544
747
|
});
|
|
@@ -553,10 +756,12 @@ export default function Home() {
|
|
|
553
756
|
const runtime = useChatRuntime({
|
|
554
757
|
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
555
758
|
});
|
|
759
|
+
const aui = useAui({
|
|
760
|
+
tools: Tools({ toolkit }),
|
|
761
|
+
});
|
|
556
762
|
|
|
557
763
|
return (
|
|
558
|
-
<AssistantRuntimeProvider runtime={runtime}>
|
|
559
|
-
<ExecuteJsTool />
|
|
764
|
+
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
|
|
560
765
|
<div className="h-full">
|
|
561
766
|
<MyThreadWithSuggestions />
|
|
562
767
|
</div>
|
|
@@ -599,12 +804,13 @@ export default function Home() {
|
|
|
599
804
|
## next.config.js
|
|
600
805
|
|
|
601
806
|
```javascript
|
|
807
|
+
import { withAui } from "@assistant-ui/next";
|
|
602
808
|
/** @type {import('next').NextConfig} */
|
|
603
809
|
const nextConfig = {
|
|
604
810
|
transpilePackages: ["@assistant-ui/react", "@assistant-ui/react-ai-sdk"],
|
|
605
811
|
};
|
|
606
812
|
|
|
607
|
-
export default nextConfig;
|
|
813
|
+
export default withAui(nextConfig);
|
|
608
814
|
|
|
609
815
|
```
|
|
610
816
|
|
|
@@ -622,15 +828,15 @@ export default nextConfig;
|
|
|
622
828
|
"start": "next start"
|
|
623
829
|
},
|
|
624
830
|
"dependencies": {
|
|
625
|
-
"@ai-sdk/openai": "^3.0.
|
|
831
|
+
"@ai-sdk/openai": "^3.0.67",
|
|
626
832
|
"@assistant-ui/react": "workspace:*",
|
|
627
833
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
628
834
|
"@assistant-ui/react-markdown": "workspace:*",
|
|
629
835
|
"@assistant-ui/ui": "workspace:*",
|
|
630
|
-
"ai": "^6.0.
|
|
836
|
+
"ai": "^6.0.193",
|
|
631
837
|
"class-variance-authority": "^0.7.1",
|
|
632
838
|
"clsx": "^2.1.1",
|
|
633
|
-
"lucide-react": "^1.
|
|
839
|
+
"lucide-react": "^1.17.0",
|
|
634
840
|
"next": "^16.2.6",
|
|
635
841
|
"react": "^19.2.6",
|
|
636
842
|
"react-dom": "^19.2.6",
|
|
@@ -638,6 +844,7 @@ export default nextConfig;
|
|
|
638
844
|
"zod": "^4.4.3"
|
|
639
845
|
},
|
|
640
846
|
"devDependencies": {
|
|
847
|
+
"@assistant-ui/next": "workspace:*",
|
|
641
848
|
"@assistant-ui/x-buildutils": "workspace:*",
|
|
642
849
|
"@tailwindcss/postcss": "^4.3.0",
|
|
643
850
|
"@types/node": "^25.9.1",
|
|
@@ -652,6 +859,97 @@ export default nextConfig;
|
|
|
652
859
|
|
|
653
860
|
```
|
|
654
861
|
|
|
862
|
+
## README.md
|
|
863
|
+
|
|
864
|
+
```markdown
|
|
865
|
+
# Chain of Thought Example
|
|
866
|
+
|
|
867
|
+
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.
|
|
868
|
+
|
|
869
|
+
## Quick start
|
|
870
|
+
|
|
871
|
+
### Using the CLI
|
|
872
|
+
|
|
873
|
+
```bash
|
|
874
|
+
npx assistant-ui@latest create my-app --example with-chain-of-thought
|
|
875
|
+
cd my-app
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
### Environment variables
|
|
879
|
+
|
|
880
|
+
`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.
|
|
881
|
+
|
|
882
|
+
Create `.env.local`:
|
|
883
|
+
|
|
884
|
+
```
|
|
885
|
+
OPENAI_API_KEY=sk-...
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
### Run
|
|
889
|
+
|
|
890
|
+
```bash
|
|
891
|
+
npm install
|
|
892
|
+
npm run dev
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
Open [http://localhost:3000](http://localhost:3000).
|
|
896
|
+
|
|
897
|
+
## What the example demonstrates
|
|
898
|
+
|
|
899
|
+
| Concern | Where it lives |
|
|
900
|
+
|---|---|
|
|
901
|
+
| Real chat flow with `useChatRuntime` | `app/page.tsx` |
|
|
902
|
+
| Reasoning, tool calls, source citations streamed as message parts | `app/api/chat/route.ts` |
|
|
903
|
+
| Nested adjacency-based grouping with `MessagePrimitive.GroupedParts` | `app/MyThread.tsx` |
|
|
904
|
+
| `Sources` component rendering `source` parts | `app/MyThread.tsx` |
|
|
905
|
+
| Fallback path that runs without an API key | `app/api/chat/route.ts` (`streamFallback`) |
|
|
906
|
+
|
|
907
|
+
## How parts are grouped
|
|
908
|
+
|
|
909
|
+
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`:
|
|
910
|
+
|
|
911
|
+
```tsx
|
|
912
|
+
<MessagePrimitive.GroupedParts
|
|
913
|
+
groupBy={(part) => {
|
|
914
|
+
if (part.type === "reasoning") return ["group-chainOfThought", "group-reasoning"];
|
|
915
|
+
if (part.type === "tool-call") return ["group-chainOfThought", "group-tool"];
|
|
916
|
+
if (part.type === "source") return ["group-chainOfThought", "group-sources"];
|
|
917
|
+
return null;
|
|
918
|
+
}}
|
|
919
|
+
>
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
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.
|
|
923
|
+
|
|
924
|
+
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).
|
|
925
|
+
|
|
926
|
+
## How sources are emitted
|
|
927
|
+
|
|
928
|
+
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.
|
|
929
|
+
|
|
930
|
+
```ts
|
|
931
|
+
writer.write({
|
|
932
|
+
type: "source-url",
|
|
933
|
+
sourceId: "src-1",
|
|
934
|
+
url: "https://example.com/report",
|
|
935
|
+
title: "Example report",
|
|
936
|
+
});
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
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.
|
|
940
|
+
|
|
941
|
+
## Why these features live together
|
|
942
|
+
|
|
943
|
+
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.
|
|
944
|
+
|
|
945
|
+
## Related documentation
|
|
946
|
+
|
|
947
|
+
- [Message Part Grouping](https://www.assistant-ui.com/docs/ui/part-grouping)
|
|
948
|
+
- [Sources component](https://www.assistant-ui.com/docs/ui/sources)
|
|
949
|
+
- [AI SDK v6 runtime](https://www.assistant-ui.com/docs/runtimes/ai-sdk/v6)
|
|
950
|
+
|
|
951
|
+
```
|
|
952
|
+
|
|
655
953
|
## tsconfig.json
|
|
656
954
|
|
|
657
955
|
```json
|