@assistant-ui/mcp-docs-server 0.1.26 → 0.1.27
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 +1 -1
- package/.docs/organized/code-examples/with-a2a.md +1 -1
- package/.docs/organized/code-examples/with-ag-ui.md +2 -2
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +3 -3
- package/.docs/organized/code-examples/with-artifacts.md +3 -3
- package/.docs/organized/code-examples/with-assistant-transport.md +1 -1
- package/.docs/organized/code-examples/with-chain-of-thought.md +3 -3
- package/.docs/organized/code-examples/with-cloud-standalone.md +3 -3
- package/.docs/organized/code-examples/with-cloud.md +3 -3
- package/.docs/organized/code-examples/with-custom-thread-list.md +3 -3
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +511 -0
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +5 -5
- package/.docs/organized/code-examples/with-expo.md +17 -17
- package/.docs/organized/code-examples/with-external-store.md +1 -1
- package/.docs/organized/code-examples/with-ffmpeg.md +216 -62
- package/.docs/organized/code-examples/with-google-adk.md +2 -2
- package/.docs/organized/code-examples/with-heat-graph.md +1 -1
- package/.docs/organized/code-examples/with-interactables.md +66 -8
- package/.docs/organized/code-examples/with-langgraph.md +2 -2
- package/.docs/organized/code-examples/with-livekit.md +591 -0
- package/.docs/organized/code-examples/with-parent-id-grouping.md +2 -2
- package/.docs/organized/code-examples/with-react-hook-form.md +3 -3
- package/.docs/organized/code-examples/with-react-ink.md +1 -1
- package/.docs/organized/code-examples/with-react-router.md +6 -6
- package/.docs/organized/code-examples/with-store.md +7 -2
- package/.docs/organized/code-examples/with-tanstack.md +3 -3
- package/.docs/organized/code-examples/with-tap-runtime.md +1 -1
- package/.docs/raw/docs/(docs)/copilots/model-context.mdx +9 -1
- package/.docs/raw/docs/(docs)/guides/interactables.mdx +99 -37
- package/.docs/raw/docs/(docs)/guides/tool-ui.mdx +29 -0
- package/.docs/raw/docs/(docs)/guides/voice.mdx +333 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +23 -0
- package/.docs/raw/docs/runtimes/a2a/index.mdx +4 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +2 -2
- package/.docs/raw/docs/runtimes/assistant-transport.mdx +6 -2
- package/.docs/raw/docs/ui/context-display.mdx +2 -2
- package/.docs/raw/docs/ui/model-selector.mdx +1 -1
- package/.docs/raw/docs/ui/voice.mdx +172 -0
- package/package.json +3 -4
|
@@ -562,20 +562,20 @@ export default nextConfig;
|
|
|
562
562
|
"version": "0.0.0",
|
|
563
563
|
"type": "module",
|
|
564
564
|
"dependencies": {
|
|
565
|
-
"@ai-sdk/openai": "^3.0.
|
|
566
|
-
"@ai-sdk/react": "^3.0.
|
|
565
|
+
"@ai-sdk/openai": "^3.0.50",
|
|
566
|
+
"@ai-sdk/react": "^3.0.146",
|
|
567
567
|
"@assistant-ui/react": "workspace:^",
|
|
568
568
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
569
569
|
"@assistant-ui/react-markdown": "workspace:^",
|
|
570
570
|
"@assistant-ui/ui": "workspace:*",
|
|
571
|
-
"@elevenlabs/client": "^
|
|
571
|
+
"@elevenlabs/client": "^1.1.0",
|
|
572
572
|
"@tailwindcss/postcss": "^4.2.2",
|
|
573
|
-
"ai": "^6.0.
|
|
573
|
+
"ai": "^6.0.144",
|
|
574
574
|
"class-variance-authority": "^0.7.1",
|
|
575
575
|
"clsx": "^2.1.1",
|
|
576
576
|
"lucide-react": "^1.7.0",
|
|
577
577
|
"motion": "^12.38.0",
|
|
578
|
-
"next": "^16.2.
|
|
578
|
+
"next": "^16.2.2",
|
|
579
579
|
"postcss": "^8.5.8",
|
|
580
580
|
"react": "^19.2.4",
|
|
581
581
|
"react-dom": "^19.2.4",
|
|
@@ -1940,29 +1940,29 @@ module.exports = config;
|
|
|
1940
1940
|
"export:web": "expo export --platform web && node scripts/flatten-assets.mjs"
|
|
1941
1941
|
},
|
|
1942
1942
|
"dependencies": {
|
|
1943
|
-
"@ai-sdk/openai": "^3.0.
|
|
1944
|
-
"@ai-sdk/react": "^3.0.
|
|
1943
|
+
"@ai-sdk/openai": "^3.0.50",
|
|
1944
|
+
"@ai-sdk/react": "^3.0.146",
|
|
1945
1945
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
1946
1946
|
"@assistant-ui/react-native": "workspace:*",
|
|
1947
1947
|
"@expo/vector-icons": "^15.1.1",
|
|
1948
|
-
"@react-navigation/drawer": "^7.9.
|
|
1949
|
-
"@react-navigation/native": "^7.
|
|
1950
|
-
"ai": "^6.0.
|
|
1951
|
-
"expo": "~55.0.
|
|
1952
|
-
"expo-constants": "~55.0.
|
|
1953
|
-
"expo-font": "~55.0.
|
|
1954
|
-
"expo-image-picker": "~55.0.
|
|
1955
|
-
"expo-linking": "~55.0.
|
|
1956
|
-
"expo-router": "~55.0.
|
|
1957
|
-
"expo-server": "~55.0.
|
|
1958
|
-
"expo-splash-screen": "~55.0.
|
|
1959
|
-
"expo-status-bar": "~55.0.
|
|
1960
|
-
"expo-system-ui": "~55.0.
|
|
1948
|
+
"@react-navigation/drawer": "^7.9.8",
|
|
1949
|
+
"@react-navigation/native": "^7.2.2",
|
|
1950
|
+
"ai": "^6.0.144",
|
|
1951
|
+
"expo": "~55.0.11",
|
|
1952
|
+
"expo-constants": "~55.0.11",
|
|
1953
|
+
"expo-font": "~55.0.6",
|
|
1954
|
+
"expo-image-picker": "~55.0.16",
|
|
1955
|
+
"expo-linking": "~55.0.11",
|
|
1956
|
+
"expo-router": "~55.0.10",
|
|
1957
|
+
"expo-server": "~55.0.7",
|
|
1958
|
+
"expo-splash-screen": "~55.0.15",
|
|
1959
|
+
"expo-status-bar": "~55.0.5",
|
|
1960
|
+
"expo-system-ui": "~55.0.13",
|
|
1961
1961
|
"react": "19.2.0",
|
|
1962
1962
|
"react-dom": "19.2.0",
|
|
1963
1963
|
"react-native": "0.83.2",
|
|
1964
|
-
"react-native-gesture-handler": "~2.
|
|
1965
|
-
"react-native-reanimated": "~4.
|
|
1964
|
+
"react-native-gesture-handler": "~2.31.0",
|
|
1965
|
+
"react-native-reanimated": "~4.3.0",
|
|
1966
1966
|
"react-native-safe-area-context": "~5.7.0",
|
|
1967
1967
|
"react-native-screens": "~4.24.0",
|
|
1968
1968
|
"react-native-web": "~0.21.2",
|
|
@@ -276,9 +276,11 @@ import {
|
|
|
276
276
|
import { z } from "zod";
|
|
277
277
|
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
|
278
278
|
import { toBlobURL } from "@ffmpeg/util";
|
|
279
|
-
import { FC, useEffect, useRef, useState } from "react";
|
|
279
|
+
import { FC, useCallback, useEffect, useRef, useState } from "react";
|
|
280
280
|
import {
|
|
281
281
|
CircleCheckIcon,
|
|
282
|
+
DownloadIcon,
|
|
283
|
+
FileIcon,
|
|
282
284
|
RefreshCcwIcon,
|
|
283
285
|
TriangleAlertIcon,
|
|
284
286
|
} from "lucide-react";
|
|
@@ -315,7 +317,9 @@ const FfmpegTool: FC<{ file: File }> = ({ file }) => {
|
|
|
315
317
|
load();
|
|
316
318
|
}, []);
|
|
317
319
|
|
|
318
|
-
useAssistantInstructions(
|
|
320
|
+
useAssistantInstructions(
|
|
321
|
+
`The user has attached a file: ${file.name}. To add text overlays, use the render_overlay tool to render HTML to a PNG image, then use run_ffmpeg with the "overlay" filter to composite it onto the video. Do NOT use the drawtext filter.`,
|
|
322
|
+
);
|
|
319
323
|
|
|
320
324
|
useAssistantTool({
|
|
321
325
|
toolName: "run_ffmpeg",
|
|
@@ -324,14 +328,6 @@ const FfmpegTool: FC<{ file: File }> = ({ file }) => {
|
|
|
324
328
|
.string()
|
|
325
329
|
.array()
|
|
326
330
|
.describe("The ffmpeg command line arguments to provide"),
|
|
327
|
-
outputFileName: z
|
|
328
|
-
.string()
|
|
329
|
-
.describe(
|
|
330
|
-
"The name of the output file including extension, corresponding to the command provided",
|
|
331
|
-
),
|
|
332
|
-
outputMimeType: z
|
|
333
|
-
.string()
|
|
334
|
-
.describe("The mime type of the output file, e.g. image/png"),
|
|
335
331
|
}),
|
|
336
332
|
execute: async ({ command }) => {
|
|
337
333
|
const transcode = async () => {
|
|
@@ -359,57 +355,227 @@ const FfmpegTool: FC<{ file: File }> = ({ file }) => {
|
|
|
359
355
|
success: code === 0,
|
|
360
356
|
hint:
|
|
361
357
|
code === 0
|
|
362
|
-
? "
|
|
358
|
+
? "Success. Now call display_file to show the output to the user."
|
|
363
359
|
: `some error happened, logs: ${logs.join("\n")}`,
|
|
364
360
|
};
|
|
365
361
|
},
|
|
366
362
|
render: function RenderFfmpeg({
|
|
367
|
-
args: { command
|
|
363
|
+
args: { command },
|
|
368
364
|
result: { success } = {},
|
|
369
365
|
}) {
|
|
370
|
-
|
|
366
|
+
return (
|
|
367
|
+
<div className="mb-2 flex flex-col gap-2 rounded-lg border px-5 py-4">
|
|
368
|
+
<div className="flex items-center gap-2">
|
|
369
|
+
{success == null && (
|
|
370
|
+
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
|
|
371
|
+
)}
|
|
372
|
+
{success === false && (
|
|
373
|
+
<TriangleAlertIcon className="size-4 text-red-600" />
|
|
374
|
+
)}
|
|
375
|
+
{success === true && (
|
|
376
|
+
<CircleCheckIcon className="size-4 text-green-600" />
|
|
377
|
+
)}
|
|
378
|
+
<p>Running ffmpeg</p>
|
|
379
|
+
</div>
|
|
380
|
+
<pre className="overflow-y-scroll font-sm">
|
|
381
|
+
ffmpeg {command?.join(" ")}
|
|
382
|
+
</pre>
|
|
383
|
+
{success === false && (
|
|
384
|
+
<div className="mt-2 border-t border-dashed pt-3">
|
|
385
|
+
Encountered an error.
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
useAssistantTool({
|
|
394
|
+
toolName: "render_overlay",
|
|
395
|
+
parameters: z.object({
|
|
396
|
+
html: z
|
|
397
|
+
.string()
|
|
398
|
+
.describe(
|
|
399
|
+
"HTML content to render. Use inline styles for all styling. The background is transparent by default.",
|
|
400
|
+
),
|
|
401
|
+
width: z.number().describe("Width of the output image in pixels"),
|
|
402
|
+
height: z.number().describe("Height of the output image in pixels"),
|
|
403
|
+
fileName: z
|
|
404
|
+
.string()
|
|
405
|
+
.describe("Output filename in ffmpeg filesystem, e.g. overlay.png"),
|
|
406
|
+
}),
|
|
407
|
+
execute: async ({ html, width, height, fileName }) => {
|
|
408
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
409
|
+
<foreignObject width="100%" height="100%">
|
|
410
|
+
<div xmlns="http://www.w3.org/1999/xhtml">${html}</div>
|
|
411
|
+
</foreignObject>
|
|
412
|
+
</svg>`;
|
|
413
|
+
|
|
414
|
+
const img = new Image();
|
|
415
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
416
|
+
|
|
417
|
+
await new Promise<void>((resolve, reject) => {
|
|
418
|
+
img.onload = () => resolve();
|
|
419
|
+
img.onerror = () => reject(new Error("Failed to render HTML"));
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const canvas = document.createElement("canvas");
|
|
423
|
+
canvas.width = width;
|
|
424
|
+
canvas.height = height;
|
|
425
|
+
const ctx = canvas.getContext("2d")!;
|
|
426
|
+
ctx.drawImage(img, 0, 0);
|
|
427
|
+
|
|
428
|
+
const blob = await new Promise<Blob>((resolve) =>
|
|
429
|
+
canvas.toBlob((b) => resolve(b!), "image/png"),
|
|
430
|
+
);
|
|
431
|
+
const data = new Uint8Array(await blob.arrayBuffer());
|
|
432
|
+
|
|
433
|
+
const ffmpeg = ffmpegRef.current;
|
|
434
|
+
await ffmpeg.writeFile(fileName, data);
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
success: true,
|
|
438
|
+
hint: `Overlay image "${fileName}" (${width}x${height}) written to ffmpeg filesystem. Use it with the overlay filter in run_ffmpeg, e.g. -i ${fileName} -filter_complex "overlay=x=0:y=0"`,
|
|
439
|
+
};
|
|
440
|
+
},
|
|
441
|
+
render: function RenderOverlay({ args, result }) {
|
|
442
|
+
return (
|
|
443
|
+
<div className="mb-2 flex items-center gap-2 rounded-lg border px-5 py-4">
|
|
444
|
+
{!result && (
|
|
445
|
+
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
|
|
446
|
+
)}
|
|
447
|
+
{result?.success && (
|
|
448
|
+
<CircleCheckIcon className="size-4 text-green-600" />
|
|
449
|
+
)}
|
|
450
|
+
<p>
|
|
451
|
+
{result?.success
|
|
452
|
+
? `Rendered overlay → ${args.fileName}`
|
|
453
|
+
: "Rendering overlay..."}
|
|
454
|
+
</p>
|
|
455
|
+
</div>
|
|
456
|
+
);
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
useAssistantTool({
|
|
461
|
+
toolName: "display_file",
|
|
462
|
+
parameters: z.object({
|
|
463
|
+
fileName: z
|
|
464
|
+
.string()
|
|
465
|
+
.describe("The name of the file to display from the ffmpeg filesystem"),
|
|
466
|
+
mimeType: z
|
|
467
|
+
.string()
|
|
468
|
+
.describe("The mime type of the file, e.g. image/png, video/mp4"),
|
|
469
|
+
}),
|
|
470
|
+
execute: async ({ fileName }) => {
|
|
471
|
+
const ffmpeg = ffmpegRef.current;
|
|
472
|
+
try {
|
|
473
|
+
const data = (await ffmpeg.readFile(
|
|
474
|
+
fileName,
|
|
475
|
+
)) as Uint8Array<ArrayBuffer>;
|
|
476
|
+
return {
|
|
477
|
+
success: true,
|
|
478
|
+
size: data.byteLength,
|
|
479
|
+
hint: "A file preview and download button is now visible to the user. Do not describe the file or repeat its contents.",
|
|
480
|
+
};
|
|
481
|
+
} catch {
|
|
482
|
+
return {
|
|
483
|
+
success: false,
|
|
484
|
+
error: `File "${fileName}" not found in ffmpeg filesystem`,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
render: function RenderDisplayFile({
|
|
489
|
+
args: { fileName, mimeType },
|
|
490
|
+
result,
|
|
491
|
+
}) {
|
|
492
|
+
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
|
493
|
+
|
|
494
|
+
const readFile = useCallback(async () => {
|
|
371
495
|
const ffmpeg = ffmpegRef.current;
|
|
372
496
|
const data = (await ffmpeg.readFile(
|
|
373
|
-
|
|
497
|
+
fileName,
|
|
374
498
|
)) as Uint8Array<ArrayBuffer>;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
499
|
+
return URL.createObjectURL(new Blob([data.buffer], { type: mimeType }));
|
|
500
|
+
}, [fileName, mimeType]);
|
|
501
|
+
|
|
502
|
+
useEffect(() => {
|
|
503
|
+
if (!result?.success) return;
|
|
504
|
+
let revoked = false;
|
|
505
|
+
readFile().then((url) => {
|
|
506
|
+
if (revoked) {
|
|
507
|
+
URL.revokeObjectURL(url);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
setBlobUrl(url);
|
|
511
|
+
});
|
|
512
|
+
return () => {
|
|
513
|
+
revoked = true;
|
|
514
|
+
setBlobUrl((prev) => {
|
|
515
|
+
if (prev) URL.revokeObjectURL(prev);
|
|
516
|
+
return null;
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
}, [result?.success, readFile]);
|
|
520
|
+
|
|
521
|
+
const handleDownload = () => {
|
|
522
|
+
if (!blobUrl) return;
|
|
523
|
+
const a = document.createElement("a");
|
|
524
|
+
a.href = blobUrl;
|
|
525
|
+
a.download = fileName;
|
|
526
|
+
a.click();
|
|
381
527
|
};
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
)}
|
|
389
|
-
{success === false && (
|
|
390
|
-
<TriangleAlertIcon className="size-4 text-red-600" />
|
|
391
|
-
)}
|
|
392
|
-
{success === true && (
|
|
393
|
-
<CircleCheckIcon className="size-4 text-green-600" />
|
|
394
|
-
)}
|
|
395
|
-
<p>Running ffmpeg</p>
|
|
396
|
-
</div>
|
|
397
|
-
<pre className="overflow-y-scroll font-sm">
|
|
398
|
-
ffmpeg {command?.join(" ")}
|
|
399
|
-
</pre>
|
|
528
|
+
|
|
529
|
+
if (!result) {
|
|
530
|
+
return (
|
|
531
|
+
<div className="flex items-center gap-2 rounded-lg border px-5 py-4">
|
|
532
|
+
<RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
|
|
533
|
+
<p>Loading file...</p>
|
|
400
534
|
</div>
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!result.success) {
|
|
539
|
+
return (
|
|
540
|
+
<div className="flex items-center gap-2 rounded-lg border px-5 py-4">
|
|
541
|
+
<TriangleAlertIcon className="size-4 text-red-600" />
|
|
542
|
+
<p>File not found</p>
|
|
543
|
+
</div>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const isImage = mimeType?.startsWith("image/");
|
|
548
|
+
const isVideo = mimeType?.startsWith("video/");
|
|
549
|
+
const isAudio = mimeType?.startsWith("audio/");
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<div className="mb-2 flex flex-col gap-3 rounded-lg border px-5 py-4">
|
|
553
|
+
{blobUrl && isImage && (
|
|
554
|
+
<img
|
|
555
|
+
src={blobUrl}
|
|
556
|
+
alt={fileName}
|
|
557
|
+
className="max-h-64 w-fit rounded"
|
|
558
|
+
/>
|
|
407
559
|
)}
|
|
408
|
-
{
|
|
409
|
-
|
|
410
|
-
|
|
560
|
+
{blobUrl && isVideo && (
|
|
561
|
+
// biome-ignore lint/a11y/useMediaCaption: generated output
|
|
562
|
+
<video src={blobUrl} controls className="max-h-64 w-fit rounded" />
|
|
563
|
+
)}
|
|
564
|
+
{/* biome-ignore lint/a11y/useMediaCaption: generated output */}
|
|
565
|
+
{blobUrl && isAudio && <audio src={blobUrl} controls />}
|
|
566
|
+
{blobUrl && !isImage && !isVideo && !isAudio && (
|
|
567
|
+
<div className="flex items-center gap-2 text-muted-foreground">
|
|
568
|
+
<FileIcon className="size-5" />
|
|
569
|
+
<span>{fileName}</span>
|
|
411
570
|
</div>
|
|
412
571
|
)}
|
|
572
|
+
<button
|
|
573
|
+
onClick={handleDownload}
|
|
574
|
+
className="flex w-fit items-center gap-2 rounded-md border bg-background px-4 py-2 text-sm hover:bg-accent"
|
|
575
|
+
>
|
|
576
|
+
<DownloadIcon className="size-4" />
|
|
577
|
+
Download {fileName}
|
|
578
|
+
</button>
|
|
413
579
|
</div>
|
|
414
580
|
);
|
|
415
581
|
},
|
|
@@ -444,18 +610,6 @@ export default function Home() {
|
|
|
444
610
|
|
|
445
611
|
return (
|
|
446
612
|
<div className="flex h-full flex-col">
|
|
447
|
-
<div className="border-b">
|
|
448
|
-
<p className="my-4 ml-8 font-bold text-xl">
|
|
449
|
-
ConvertGPT (built with{" "}
|
|
450
|
-
<a
|
|
451
|
-
href="https://github.com/assistant-ui/assistant-ui"
|
|
452
|
-
className="underline"
|
|
453
|
-
>
|
|
454
|
-
assistant-ui
|
|
455
|
-
</a>
|
|
456
|
-
)
|
|
457
|
-
</p>
|
|
458
|
-
</div>
|
|
459
613
|
<AuiProvider value={aui}>
|
|
460
614
|
<Thread />
|
|
461
615
|
</AuiProvider>
|
|
@@ -523,18 +677,18 @@ export default nextConfig;
|
|
|
523
677
|
"start": "next start"
|
|
524
678
|
},
|
|
525
679
|
"dependencies": {
|
|
526
|
-
"@ai-sdk/openai": "^3.0.
|
|
680
|
+
"@ai-sdk/openai": "^3.0.50",
|
|
527
681
|
"@assistant-ui/react": "workspace:*",
|
|
528
682
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
529
683
|
"@assistant-ui/react-markdown": "workspace:*",
|
|
530
684
|
"@assistant-ui/ui": "workspace:*",
|
|
531
685
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
|
532
686
|
"@ffmpeg/util": "^0.12.2",
|
|
533
|
-
"ai": "^6.0.
|
|
687
|
+
"ai": "^6.0.144",
|
|
534
688
|
"class-variance-authority": "^0.7.1",
|
|
535
689
|
"clsx": "^2.1.1",
|
|
536
690
|
"lucide-react": "^1.7.0",
|
|
537
|
-
"next": "^16.2.
|
|
691
|
+
"next": "^16.2.2",
|
|
538
692
|
"react": "^19.2.4",
|
|
539
693
|
"react-dom": "^19.2.4",
|
|
540
694
|
"tailwind-merge": "^3.5.0",
|
|
@@ -304,11 +304,11 @@ export default nextConfig;
|
|
|
304
304
|
"@assistant-ui/react-google-adk": "workspace:*",
|
|
305
305
|
"@assistant-ui/react-markdown": "workspace:*",
|
|
306
306
|
"@assistant-ui/ui": "workspace:*",
|
|
307
|
-
"@google/adk": "^0.
|
|
307
|
+
"@google/adk": "^0.6.1",
|
|
308
308
|
"class-variance-authority": "^0.7.1",
|
|
309
309
|
"clsx": "^2.1.1",
|
|
310
310
|
"lucide-react": "^1.7.0",
|
|
311
|
-
"next": "^16.2.
|
|
311
|
+
"next": "^16.2.2",
|
|
312
312
|
"react": "^19.2.4",
|
|
313
313
|
"react-dom": "^19.2.4",
|
|
314
314
|
"tailwind-merge": "^3.5.0",
|
|
@@ -29,7 +29,7 @@ export async function POST(req: Request) {
|
|
|
29
29
|
|
|
30
30
|
// Convert client-defined tools (forwarded from model context) to AI SDK format.
|
|
31
31
|
// These have no `execute` — they are frontend tools executed on the client
|
|
32
|
-
// via useAssistantTool /
|
|
32
|
+
// via useAssistantTool / useAssistantInteractable.
|
|
33
33
|
const tools = clientTools
|
|
34
34
|
? Object.fromEntries(
|
|
35
35
|
Object.entries(clientTools).map(([name, def]) => [
|
|
@@ -214,14 +214,15 @@ export default function RootLayout({
|
|
|
214
214
|
```tsx
|
|
215
215
|
"use client";
|
|
216
216
|
|
|
217
|
-
import { useRef, useState, useCallback } from "react";
|
|
217
|
+
import { useRef, useState, useCallback, useEffect } from "react";
|
|
218
218
|
import { Thread } from "@/components/assistant-ui/thread";
|
|
219
219
|
import {
|
|
220
220
|
AssistantRuntimeProvider,
|
|
221
221
|
Interactables,
|
|
222
222
|
Suggestions,
|
|
223
223
|
useAui,
|
|
224
|
-
|
|
224
|
+
useAssistantInteractable,
|
|
225
|
+
useInteractableState,
|
|
225
226
|
useAssistantTool,
|
|
226
227
|
} from "@assistant-ui/react";
|
|
227
228
|
import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
|
|
@@ -231,6 +232,7 @@ import {
|
|
|
231
232
|
CheckCircle2Icon,
|
|
232
233
|
CircleIcon,
|
|
233
234
|
ListTodoIcon,
|
|
235
|
+
Loader2Icon,
|
|
234
236
|
StickyNoteIcon,
|
|
235
237
|
Trash2Icon,
|
|
236
238
|
PlusIcon,
|
|
@@ -258,12 +260,16 @@ const taskBoardInitialState: TaskBoardState = { tasks: [] };
|
|
|
258
260
|
let nextTaskId = 0;
|
|
259
261
|
|
|
260
262
|
function TaskBoard() {
|
|
261
|
-
const
|
|
263
|
+
const id = useAssistantInteractable("taskBoard", {
|
|
262
264
|
description:
|
|
263
265
|
"A task board showing the user's tasks. Use the manage_tasks tool (not update_taskBoard) to add/toggle/remove/clear tasks.",
|
|
264
266
|
stateSchema: taskBoardSchema,
|
|
265
267
|
initialState: taskBoardInitialState,
|
|
266
268
|
});
|
|
269
|
+
const [state, { setState, isPending }] = useInteractableState<TaskBoardState>(
|
|
270
|
+
id,
|
|
271
|
+
taskBoardInitialState,
|
|
272
|
+
);
|
|
267
273
|
|
|
268
274
|
const setStateRef = useRef(setState);
|
|
269
275
|
setStateRef.current = setState;
|
|
@@ -323,6 +329,9 @@ function TaskBoard() {
|
|
|
323
329
|
<div className="flex items-center gap-2 border-b px-4 py-3">
|
|
324
330
|
<ListTodoIcon className="size-4 text-muted-foreground" />
|
|
325
331
|
<span className="font-semibold text-sm">Task Board</span>
|
|
332
|
+
{isPending && (
|
|
333
|
+
<Loader2Icon className="size-3 animate-spin text-muted-foreground" />
|
|
334
|
+
)}
|
|
326
335
|
{state.tasks.length > 0 && (
|
|
327
336
|
<span className="ml-auto rounded-full bg-primary/10 px-2 py-0.5 font-medium text-primary text-xs">
|
|
328
337
|
{doneCount}/{state.tasks.length}
|
|
@@ -422,7 +431,7 @@ function NoteCard({
|
|
|
422
431
|
// Multi-instance: each NoteCard has a unique `id`, all share name "note"
|
|
423
432
|
// Partial updates: AI can send { color: "blue" } without resending title/content
|
|
424
433
|
// Selection: clicking a note calls setSelected(true)
|
|
425
|
-
|
|
434
|
+
useAssistantInteractable("note", {
|
|
426
435
|
id: noteId,
|
|
427
436
|
description:
|
|
428
437
|
"A sticky note. The AI can partially update any field (title, content, color) without resending the others.",
|
|
@@ -430,6 +439,10 @@ function NoteCard({
|
|
|
430
439
|
initialState: noteInitialState,
|
|
431
440
|
selected: selectedId === noteId,
|
|
432
441
|
});
|
|
442
|
+
const [state, { setSelected }] = useInteractableState<NoteState>(
|
|
443
|
+
noteId,
|
|
444
|
+
noteInitialState,
|
|
445
|
+
);
|
|
433
446
|
|
|
434
447
|
const isSelected = selectedId === noteId;
|
|
435
448
|
|
|
@@ -471,9 +484,31 @@ function NoteCard({
|
|
|
471
484
|
);
|
|
472
485
|
}
|
|
473
486
|
|
|
487
|
+
const NOTE_IDS_KEY = "interactables-example-note-ids";
|
|
488
|
+
|
|
489
|
+
function loadNoteIds(): string[] {
|
|
490
|
+
try {
|
|
491
|
+
const saved = localStorage.getItem(NOTE_IDS_KEY);
|
|
492
|
+
return saved ? JSON.parse(saved) : [];
|
|
493
|
+
} catch {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
474
498
|
function NotesPanel() {
|
|
475
499
|
const [noteIds, setNoteIds] = useState<string[]>([]);
|
|
476
500
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
501
|
+
const hydratedRef = useRef(false);
|
|
502
|
+
|
|
503
|
+
useEffect(() => {
|
|
504
|
+
if (!hydratedRef.current) {
|
|
505
|
+
hydratedRef.current = true;
|
|
506
|
+
const saved = loadNoteIds();
|
|
507
|
+
if (saved.length > 0) setNoteIds(saved);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
localStorage.setItem(NOTE_IDS_KEY, JSON.stringify(noteIds));
|
|
511
|
+
}, [noteIds]);
|
|
477
512
|
|
|
478
513
|
const noteIdsRef = useRef(noteIds);
|
|
479
514
|
noteIdsRef.current = noteIds;
|
|
@@ -572,6 +607,27 @@ function NotesPanel() {
|
|
|
572
607
|
// App
|
|
573
608
|
// ===========================================================================
|
|
574
609
|
|
|
610
|
+
const STORAGE_KEY = "interactables-example";
|
|
611
|
+
|
|
612
|
+
function useInteractablePersistence(aui: ReturnType<typeof useAui>) {
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
615
|
+
if (saved) {
|
|
616
|
+
try {
|
|
617
|
+
aui.interactables().importState(JSON.parse(saved));
|
|
618
|
+
} catch {
|
|
619
|
+
// ignore malformed data
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
aui.interactables().setPersistenceAdapter({
|
|
624
|
+
save: (state) => {
|
|
625
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}, [aui]);
|
|
629
|
+
}
|
|
630
|
+
|
|
575
631
|
export default function Home() {
|
|
576
632
|
const runtime = useChatRuntime({
|
|
577
633
|
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
|
|
@@ -599,6 +655,8 @@ export default function Home() {
|
|
|
599
655
|
]),
|
|
600
656
|
});
|
|
601
657
|
|
|
658
|
+
useInteractablePersistence(aui);
|
|
659
|
+
|
|
602
660
|
return (
|
|
603
661
|
<AssistantRuntimeProvider aui={aui} runtime={runtime}>
|
|
604
662
|
<main className="flex h-full">
|
|
@@ -677,15 +735,15 @@ export default nextConfig;
|
|
|
677
735
|
"start": "next start"
|
|
678
736
|
},
|
|
679
737
|
"dependencies": {
|
|
680
|
-
"@ai-sdk/openai": "^3.0.
|
|
738
|
+
"@ai-sdk/openai": "^3.0.50",
|
|
681
739
|
"@assistant-ui/react": "workspace:*",
|
|
682
740
|
"@assistant-ui/react-ai-sdk": "workspace:*",
|
|
683
741
|
"@assistant-ui/ui": "workspace:*",
|
|
684
|
-
"ai": "^6.0.
|
|
742
|
+
"ai": "^6.0.144",
|
|
685
743
|
"class-variance-authority": "^0.7.1",
|
|
686
744
|
"clsx": "^2.1.1",
|
|
687
745
|
"lucide-react": "^1.7.0",
|
|
688
|
-
"next": "^16.2.
|
|
746
|
+
"next": "^16.2.2",
|
|
689
747
|
"react": "^19.2.4",
|
|
690
748
|
"react-dom": "^19.2.4",
|
|
691
749
|
"tailwind-merge": "^3.5.0",
|
|
@@ -871,11 +871,11 @@ export default nextConfig;
|
|
|
871
871
|
"@assistant-ui/react-langgraph": "workspace:*",
|
|
872
872
|
"@assistant-ui/react-markdown": "workspace:*",
|
|
873
873
|
"@assistant-ui/ui": "workspace:*",
|
|
874
|
-
"@langchain/langgraph-sdk": "^1.8.
|
|
874
|
+
"@langchain/langgraph-sdk": "^1.8.4",
|
|
875
875
|
"class-variance-authority": "^0.7.1",
|
|
876
876
|
"clsx": "^2.1.1",
|
|
877
877
|
"lucide-react": "^1.7.0",
|
|
878
|
-
"next": "^16.2.
|
|
878
|
+
"next": "^16.2.2",
|
|
879
879
|
"react": "^19.2.4",
|
|
880
880
|
"react-dom": "^19.2.4",
|
|
881
881
|
"tailwind-merge": "^3.5.0"
|