@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.
Files changed (134) hide show
  1. package/.docs/organized/code-examples/waterfall.md +20 -22
  2. package/.docs/organized/code-examples/with-a2a.md +24 -24
  3. package/.docs/organized/code-examples/with-ag-ui.md +30 -25
  4. package/.docs/organized/code-examples/with-ai-sdk-v6.md +11 -9
  5. package/.docs/organized/code-examples/with-artifacts.md +49 -37
  6. package/.docs/organized/code-examples/with-assistant-transport.md +63 -51
  7. package/.docs/organized/code-examples/with-browser-extension.md +22 -10
  8. package/.docs/organized/code-examples/with-chain-of-thought.md +406 -87
  9. package/.docs/organized/code-examples/with-cloud-standalone.md +25 -24
  10. package/.docs/organized/code-examples/with-cloud.md +11 -9
  11. package/.docs/organized/code-examples/with-custom-thread-list.md +14 -12
  12. package/.docs/organized/code-examples/with-elevenlabs-conversational.md +23 -21
  13. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +14 -13
  14. package/.docs/organized/code-examples/with-expo.md +44 -29
  15. package/.docs/organized/code-examples/with-external-store.md +9 -7
  16. package/.docs/organized/code-examples/with-ffmpeg.md +322 -285
  17. package/.docs/organized/code-examples/with-generative-ui.md +1066 -256
  18. package/.docs/organized/code-examples/with-google-adk.md +10 -8
  19. package/.docs/organized/code-examples/with-heat-graph.md +13 -11
  20. package/.docs/organized/code-examples/with-image-generation.md +19 -17
  21. package/.docs/organized/code-examples/with-interactables.md +317 -239
  22. package/.docs/organized/code-examples/with-langchain.md +14 -12
  23. package/.docs/organized/code-examples/with-langgraph.md +91 -83
  24. package/.docs/organized/code-examples/with-livekit.md +24 -22
  25. package/.docs/organized/code-examples/with-mcp.md +18 -12
  26. package/.docs/organized/code-examples/with-opencode.md +66 -66
  27. package/.docs/organized/code-examples/with-react-hook-form.md +19 -17
  28. package/.docs/organized/code-examples/with-react-ink.md +297 -102
  29. package/.docs/organized/code-examples/with-react-router.md +17 -15
  30. package/.docs/organized/code-examples/with-resumable-stream.md +15 -14
  31. package/.docs/organized/code-examples/with-store.md +78 -76
  32. package/.docs/organized/code-examples/with-tanstack.md +13 -14
  33. package/.docs/organized/code-examples/with-tap-runtime.md +26 -24
  34. package/.docs/raw/docs/(docs)/architecture.mdx +94 -42
  35. package/.docs/raw/docs/(docs)/cli.mdx +1 -2
  36. package/.docs/raw/docs/(docs)/installation.mdx +1 -1
  37. package/.docs/raw/docs/(reference)/api-reference/adapters/persistence.mdx +5 -1
  38. package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +15 -15
  39. package/.docs/raw/docs/(reference)/api-reference/external-store/runtime.mdx +14 -1
  40. package/.docs/raw/docs/(reference)/api-reference/generative-ui/index.mdx +18 -0
  41. package/.docs/raw/docs/(reference)/api-reference/generative-ui/rendering.mdx +33 -0
  42. package/.docs/raw/docs/(reference)/api-reference/generative-ui/spec.mdx +45 -0
  43. package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +41 -41
  44. package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +19 -1
  45. package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +122 -122
  46. package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +43 -2
  47. package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +6 -0
  48. package/.docs/raw/docs/(reference)/api-reference/overview.mdx +4 -6
  49. package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +30 -0
  50. package/.docs/raw/docs/(reference)/api-reference/tools/component-tools.mdx +20 -3
  51. package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +8 -8
  52. package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +52 -4
  53. package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +100 -10
  54. package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +7 -59
  55. package/.docs/raw/docs/cloud/ai-sdk.mdx +0 -2
  56. package/.docs/raw/docs/{(docs)/copilots → copilots}/model-context.mdx +34 -26
  57. package/.docs/raw/docs/{(docs)/copilots → copilots}/motivation.mdx +32 -26
  58. package/.docs/raw/docs/guides/chain-of-thought.mdx +7 -9
  59. package/.docs/raw/docs/guides/context-api.mdx +2 -1
  60. package/.docs/raw/docs/guides/index.mdx +3 -12
  61. package/.docs/raw/docs/guides/mentions.mdx +4 -4
  62. package/.docs/raw/docs/guides/slash-commands.mdx +1 -1
  63. package/.docs/raw/docs/guides/suggestions.mdx +1 -1
  64. package/.docs/raw/docs/ink/adapters.mdx +23 -1
  65. package/.docs/raw/docs/ink/hooks.mdx +101 -85
  66. package/.docs/raw/docs/ink/migration.mdx +1 -1
  67. package/.docs/raw/docs/ink/primitives.mdx +2 -2
  68. package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +1 -1
  69. package/.docs/raw/docs/integrations/index.mdx +2 -2
  70. package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +17 -2
  71. package/.docs/raw/docs/migrations/toolkit-tools.mdx +232 -0
  72. package/.docs/raw/docs/primitives/chain-of-thought.mdx +10 -16
  73. package/.docs/raw/docs/primitives/message.mdx +9 -10
  74. package/.docs/raw/docs/react-native/hooks.mdx +62 -79
  75. package/.docs/raw/docs/react-native/migration.mdx +1 -1
  76. package/.docs/raw/docs/react-native/primitives.mdx +2 -2
  77. package/.docs/raw/docs/runtimes/a2a/client-and-hooks.mdx +3 -0
  78. package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +41 -0
  79. package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +122 -1
  80. package/.docs/raw/docs/runtimes/concepts/architecture.mdx +46 -4
  81. package/.docs/raw/docs/runtimes/concepts/threads.mdx +7 -1
  82. package/.docs/raw/docs/runtimes/custom/assistant-transport.mdx +54 -12
  83. package/.docs/raw/docs/runtimes/custom/external-store.mdx +62 -4
  84. package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +30 -7
  85. package/.docs/raw/docs/runtimes/google-adk/hooks.mdx +108 -38
  86. package/.docs/raw/docs/runtimes/langgraph/generative-ui.mdx +2 -2
  87. package/.docs/raw/docs/runtimes/langgraph/streaming.mdx +14 -1
  88. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +64 -50
  89. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +98 -86
  90. package/.docs/raw/docs/tools/backend.mdx +144 -0
  91. package/.docs/raw/docs/tools/defining-tools.mdx +538 -0
  92. package/.docs/raw/docs/tools/dynamic-tools.mdx +110 -0
  93. package/.docs/raw/docs/tools/generative-ui.mdx +214 -0
  94. package/.docs/raw/docs/tools/index.mdx +71 -0
  95. package/.docs/raw/docs/{guides → tools}/interactables.mdx +1 -1
  96. package/.docs/raw/docs/{integrations/tools → tools}/mcp.mdx +145 -50
  97. package/.docs/raw/docs/{guides → tools}/multi-agent.mdx +15 -17
  98. package/.docs/raw/docs/tools/tool-ui.mdx +967 -0
  99. package/.docs/raw/docs/{integrations/tools/react-mcp.mdx → tools/user-managed-mcp.mdx} +7 -7
  100. package/.docs/raw/docs/ui/directive-text.mdx +3 -3
  101. package/.docs/raw/docs/ui/mcp-config.mdx +4 -4
  102. package/.docs/raw/docs/ui/mermaid.mdx +16 -9
  103. package/.docs/raw/docs/ui/part-grouping.mdx +84 -50
  104. package/.docs/raw/docs/ui/reasoning.mdx +4 -5
  105. package/.docs/raw/docs/ui/tool-fallback.mdx +16 -0
  106. package/.docs/raw/docs/ui/tool-group.mdx +5 -6
  107. package/.docs/raw/docs/utilities/heat-graph.mdx +1 -1
  108. package/.docs/raw/docs/utilities/react-o11y.mdx +2 -2
  109. package/dist/constants.js.map +1 -1
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js.map +1 -1
  112. package/dist/prepare-docs/code-examples.d.ts.map +1 -1
  113. package/dist/prepare-docs/code-examples.js.map +1 -1
  114. package/dist/prepare-docs/copy-raw.d.ts.map +1 -1
  115. package/dist/prepare-docs/copy-raw.js.map +1 -1
  116. package/dist/prepare-docs/prepare.js.map +1 -1
  117. package/dist/stdio.js.map +1 -1
  118. package/dist/tools/docs.js.map +1 -1
  119. package/dist/tools/examples.js.map +1 -1
  120. package/dist/tools/tests/test-setup.js.map +1 -1
  121. package/dist/utils/mdx.js.map +1 -1
  122. package/dist/utils/paths.d.ts.map +1 -1
  123. package/dist/utils/paths.js.map +1 -1
  124. package/package.json +5 -5
  125. package/.docs/organized/code-examples/with-parent-id-grouping.md +0 -596
  126. package/.docs/raw/docs/(docs)/copilots/make-assistant-tool-ui.mdx +0 -151
  127. package/.docs/raw/docs/(docs)/copilots/make-assistant-tool.mdx +0 -230
  128. package/.docs/raw/docs/guides/generative-ui.mdx +0 -142
  129. package/.docs/raw/docs/guides/tool-ui.mdx +0 -858
  130. package/.docs/raw/docs/guides/tools.mdx +0 -736
  131. /package/.docs/raw/docs/{(docs)/copilots → copilots}/assistant-frame.mdx +0 -0
  132. /package/.docs/raw/docs/{(docs)/copilots → copilots}/make-assistant-visible.mdx +0 -0
  133. /package/.docs/raw/docs/{(docs)/copilots → copilots}/use-assistant-instructions.mdx +0 -0
  134. /package/.docs/raw/docs/{guides → tools}/mcp-apps.mdx +0 -0
@@ -267,30 +267,26 @@ export default dynamic(() => Promise.resolve(NoSSRWrapper), {
267
267
 
268
268
  import {
269
269
  useAssistantInstructions,
270
- useAssistantTool,
271
270
  useAui,
272
271
  useAuiState,
273
272
  AuiProvider,
274
273
  Suggestions,
274
+ Tools,
275
275
  } from "@assistant-ui/react";
276
- import { z } from "zod";
277
276
  import { FFmpeg } from "@ffmpeg/ffmpeg";
278
277
  import { toBlobURL } from "@ffmpeg/util";
279
- import { type FC, useCallback, useEffect, useRef, useState } from "react";
280
- import {
281
- CircleCheckIcon,
282
- DownloadIcon,
283
- FileIcon,
284
- RefreshCcwIcon,
285
- TriangleAlertIcon,
286
- } from "lucide-react";
278
+ import { type FC, type ReactNode, useEffect, useRef, useState } from "react";
287
279
  import { Thread } from "@/components/assistant-ui/thread";
280
+ import { useFfmpegToolkit } from "./toolkit";
288
281
 
289
282
  // MVP: upload file, enter command
290
283
  // MVP: convert command to tool call
291
284
  // MVP: tool call: ffmpeg
292
285
 
293
- const FfmpegTool: FC<{ file: File }> = ({ file }) => {
286
+ const FfmpegToolsProvider: FC<{ file: File; children: ReactNode }> = ({
287
+ file,
288
+ children,
289
+ }) => {
294
290
  const loadingRef = useRef(false);
295
291
  const ffmpegRef = useRef(new FFmpeg());
296
292
 
@@ -321,272 +317,13 @@ const FfmpegTool: FC<{ file: File }> = ({ file }) => {
321
317
  `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
318
  );
323
319
 
324
- useAssistantTool({
325
- toolName: "run_ffmpeg",
326
- parameters: z.object({
327
- command: z
328
- .string()
329
- .array()
330
- .describe("The ffmpeg command line arguments to provide"),
331
- }),
332
- execute: async ({ command }) => {
333
- const transcode = async () => {
334
- const ffmpeg = ffmpegRef.current;
335
-
336
- const logs: string[] = [];
337
- const logger = ({ message }: { message: string }) => {
338
- logs.push(message);
339
- };
340
- ffmpeg.on("log", logger);
341
-
342
- await ffmpeg.writeFile(
343
- file.name,
344
- new Uint8Array(await file.arrayBuffer()),
345
- );
346
-
347
- const code = await ffmpeg.exec(command);
348
- ffmpeg.off("log", logger);
349
-
350
- return { code, logs };
351
- };
352
- const { code, logs } = await transcode();
353
-
354
- return {
355
- success: code === 0,
356
- hint:
357
- code === 0
358
- ? "Success. Now call display_file to show the output to the user."
359
- : `some error happened, logs: ${logs.join("\n")}`,
360
- };
361
- },
362
- render: function RenderFfmpeg({
363
- args: { command },
364
- result: { success } = {},
365
- }) {
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());
320
+ const toolkit = useFfmpegToolkit(file, ffmpegRef);
432
321
 
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
- // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
493
- const [blobUrl, setBlobUrl] = useState<string | null>(null);
494
-
495
- // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
496
- const readFile = useCallback(async () => {
497
- const ffmpeg = ffmpegRef.current;
498
- const data = (await ffmpeg.readFile(
499
- fileName,
500
- )) as Uint8Array<ArrayBuffer>;
501
- return URL.createObjectURL(new Blob([data.buffer], { type: mimeType }));
502
- }, [fileName, mimeType]);
503
-
504
- // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
505
- useEffect(() => {
506
- if (!result?.success) return;
507
- let revoked = false;
508
- readFile().then((url) => {
509
- if (revoked) {
510
- URL.revokeObjectURL(url);
511
- return;
512
- }
513
- setBlobUrl(url);
514
- });
515
- return () => {
516
- revoked = true;
517
- setBlobUrl((prev) => {
518
- if (prev) URL.revokeObjectURL(prev);
519
- return null;
520
- });
521
- };
522
- }, [result?.success, readFile]);
523
-
524
- const handleDownload = () => {
525
- if (!blobUrl) return;
526
- const a = document.createElement("a");
527
- a.href = blobUrl;
528
- a.download = fileName;
529
- a.click();
530
- };
531
-
532
- if (!result) {
533
- return (
534
- <div className="flex items-center gap-2 rounded-lg border px-5 py-4">
535
- <RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
536
- <p>Loading file...</p>
537
- </div>
538
- );
539
- }
540
-
541
- if (!result.success) {
542
- return (
543
- <div className="flex items-center gap-2 rounded-lg border px-5 py-4">
544
- <TriangleAlertIcon className="size-4 text-red-600" />
545
- <p>File not found</p>
546
- </div>
547
- );
548
- }
549
-
550
- const isImage = mimeType?.startsWith("image/");
551
- const isVideo = mimeType?.startsWith("video/");
552
- const isAudio = mimeType?.startsWith("audio/");
553
-
554
- return (
555
- <div className="mb-2 flex flex-col gap-3 rounded-lg border px-5 py-4">
556
- {blobUrl && isImage && (
557
- // biome-ignore lint/performance/noImgElement: blob URL display
558
- <img
559
- src={blobUrl}
560
- alt={fileName}
561
- className="max-h-64 w-fit rounded"
562
- />
563
- )}
564
- {blobUrl && isVideo && (
565
- // biome-ignore lint/a11y/useMediaCaption: generated output
566
- <video src={blobUrl} controls className="max-h-64 w-fit rounded" />
567
- )}
568
- {/* biome-ignore lint/a11y/useMediaCaption: generated output */}
569
- {blobUrl && isAudio && <audio src={blobUrl} controls />}
570
- {blobUrl && !isImage && !isVideo && !isAudio && (
571
- <div className="flex items-center gap-2 text-muted-foreground">
572
- <FileIcon className="size-5" />
573
- <span>{fileName}</span>
574
- </div>
575
- )}
576
- <button
577
- type="button"
578
- onClick={handleDownload}
579
- className="flex w-fit items-center gap-2 rounded-md border bg-background px-4 py-2 text-sm hover:bg-accent"
580
- >
581
- <DownloadIcon className="size-4" />
582
- Download {fileName}
583
- </button>
584
- </div>
585
- );
586
- },
322
+ const aui = useAui({
323
+ tools: Tools({ toolkit }),
587
324
  });
588
325
 
589
- return null;
326
+ return <AuiProvider value={aui}>{children}</AuiProvider>;
590
327
  };
591
328
 
592
329
  export default function Home() {
@@ -616,15 +353,313 @@ export default function Home() {
616
353
  return (
617
354
  <div className="flex h-full flex-col">
618
355
  <AuiProvider value={aui}>
619
- <Thread />
356
+ {lastFile ? (
357
+ <FfmpegToolsProvider file={lastFile}>
358
+ <Thread />
359
+ </FfmpegToolsProvider>
360
+ ) : (
361
+ <Thread />
362
+ )}
620
363
  </AuiProvider>
621
- {lastFile && <FfmpegTool file={lastFile} />}
622
364
  </div>
623
365
  );
624
366
  }
625
367
 
626
368
  ```
627
369
 
370
+ ## app/toolkit.tsx
371
+
372
+ ```tsx
373
+ "use client";
374
+
375
+ import type { MutableRefObject } from "react";
376
+ import { useCallback, useEffect, useMemo, useState } from "react";
377
+ import { defineToolkit } from "@assistant-ui/react";
378
+ import type { FFmpeg } from "@ffmpeg/ffmpeg";
379
+ import {
380
+ CircleCheckIcon,
381
+ DownloadIcon,
382
+ FileIcon,
383
+ RefreshCcwIcon,
384
+ TriangleAlertIcon,
385
+ } from "lucide-react";
386
+ import { z } from "zod";
387
+
388
+ export function useFfmpegToolkit(
389
+ file: File,
390
+ ffmpegRef: MutableRefObject<FFmpeg>,
391
+ ) {
392
+ return useMemo(
393
+ () =>
394
+ defineToolkit({
395
+ run_ffmpeg: {
396
+ type: "frontend",
397
+ parameters: z.object({
398
+ command: z
399
+ .string()
400
+ .array()
401
+ .describe("The ffmpeg command line arguments to provide"),
402
+ }),
403
+ execute: async ({ command }) => {
404
+ const ffmpeg = ffmpegRef.current;
405
+
406
+ const logs: string[] = [];
407
+ const logger = ({ message }: { message: string }) => {
408
+ logs.push(message);
409
+ };
410
+ ffmpeg.on("log", logger);
411
+
412
+ await ffmpeg.writeFile(
413
+ file.name,
414
+ new Uint8Array(await file.arrayBuffer()),
415
+ );
416
+
417
+ const code = await ffmpeg.exec(command);
418
+ ffmpeg.off("log", logger);
419
+
420
+ return {
421
+ success: code === 0,
422
+ hint:
423
+ code === 0
424
+ ? "Success. Now call display_file to show the output to the user."
425
+ : `some error happened, logs: ${logs.join("\n")}`,
426
+ };
427
+ },
428
+ render: function RenderFfmpeg({
429
+ args: { command },
430
+ result: { success } = {},
431
+ }) {
432
+ return (
433
+ <div className="mb-2 flex flex-col gap-2 rounded-lg border px-5 py-4">
434
+ <div className="flex items-center gap-2">
435
+ {success == null && (
436
+ <RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
437
+ )}
438
+ {success === false && (
439
+ <TriangleAlertIcon className="size-4 text-red-600" />
440
+ )}
441
+ {success === true && (
442
+ <CircleCheckIcon className="size-4 text-green-600" />
443
+ )}
444
+ <p>Running ffmpeg</p>
445
+ </div>
446
+ <pre className="font-sm overflow-y-scroll">
447
+ ffmpeg {command?.join(" ")}
448
+ </pre>
449
+ {success === false && (
450
+ <div className="mt-2 border-t border-dashed pt-3">
451
+ Encountered an error.
452
+ </div>
453
+ )}
454
+ </div>
455
+ );
456
+ },
457
+ },
458
+ render_overlay: {
459
+ type: "frontend",
460
+ parameters: z.object({
461
+ html: z
462
+ .string()
463
+ .describe(
464
+ "HTML content to render. Use inline styles for all styling. The background is transparent by default.",
465
+ ),
466
+ width: z.number().describe("Width of the output image in pixels"),
467
+ height: z.number().describe("Height of the output image in pixels"),
468
+ fileName: z
469
+ .string()
470
+ .describe(
471
+ "Output filename in ffmpeg filesystem, e.g. overlay.png",
472
+ ),
473
+ }),
474
+ execute: async ({ html, width, height, fileName }) => {
475
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
476
+ <foreignObject width="100%" height="100%">
477
+ <div xmlns="http://www.w3.org/1999/xhtml">${html}</div>
478
+ </foreignObject>
479
+ </svg>`;
480
+
481
+ const img = new Image();
482
+ img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
483
+
484
+ await new Promise<void>((resolve, reject) => {
485
+ img.onload = () => resolve();
486
+ img.onerror = () => reject(new Error("Failed to render HTML"));
487
+ });
488
+
489
+ const canvas = document.createElement("canvas");
490
+ canvas.width = width;
491
+ canvas.height = height;
492
+ const ctx = canvas.getContext("2d")!;
493
+ ctx.drawImage(img, 0, 0);
494
+
495
+ const blob = await new Promise<Blob>((resolve) =>
496
+ canvas.toBlob((b) => resolve(b!), "image/png"),
497
+ );
498
+ const data = new Uint8Array(await blob.arrayBuffer());
499
+
500
+ const ffmpeg = ffmpegRef.current;
501
+ await ffmpeg.writeFile(fileName, data);
502
+
503
+ return {
504
+ success: true,
505
+ 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"`,
506
+ };
507
+ },
508
+ render: function RenderOverlay({ args, result }) {
509
+ return (
510
+ <div className="mb-2 flex items-center gap-2 rounded-lg border px-5 py-4">
511
+ {!result && (
512
+ <RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
513
+ )}
514
+ {result?.success && (
515
+ <CircleCheckIcon className="size-4 text-green-600" />
516
+ )}
517
+ <p>
518
+ {result?.success
519
+ ? `Rendered overlay -> ${args.fileName}`
520
+ : "Rendering overlay..."}
521
+ </p>
522
+ </div>
523
+ );
524
+ },
525
+ },
526
+ display_file: {
527
+ type: "frontend",
528
+ parameters: z.object({
529
+ fileName: z
530
+ .string()
531
+ .describe(
532
+ "The name of the file to display from the ffmpeg filesystem",
533
+ ),
534
+ mimeType: z
535
+ .string()
536
+ .describe("The mime type of the file, e.g. image/png, video/mp4"),
537
+ }),
538
+ execute: async ({ fileName }) => {
539
+ const ffmpeg = ffmpegRef.current;
540
+ try {
541
+ const data = (await ffmpeg.readFile(
542
+ fileName,
543
+ )) as Uint8Array<ArrayBuffer>;
544
+ return {
545
+ success: true,
546
+ size: data.byteLength,
547
+ hint: "A file preview and download button is now visible to the user. Do not describe the file or repeat its contents.",
548
+ };
549
+ } catch {
550
+ return {
551
+ success: false,
552
+ error: `File "${fileName}" not found in ffmpeg filesystem`,
553
+ };
554
+ }
555
+ },
556
+ render: function RenderDisplayFile({
557
+ args: { fileName, mimeType },
558
+ result,
559
+ }) {
560
+ const [blobUrl, setBlobUrl] = useState<string | null>(null);
561
+
562
+ const readFile = useCallback(async () => {
563
+ const ffmpeg = ffmpegRef.current;
564
+ const data = (await ffmpeg.readFile(
565
+ fileName,
566
+ )) as Uint8Array<ArrayBuffer>;
567
+ return URL.createObjectURL(
568
+ new Blob([data.buffer], { type: mimeType }),
569
+ );
570
+ }, [fileName, mimeType]);
571
+
572
+ useEffect(() => {
573
+ if (!result?.success) return;
574
+ let revoked = false;
575
+ readFile().then((url) => {
576
+ if (revoked) {
577
+ URL.revokeObjectURL(url);
578
+ return;
579
+ }
580
+ setBlobUrl(url);
581
+ });
582
+ return () => {
583
+ revoked = true;
584
+ setBlobUrl((prev) => {
585
+ if (prev) URL.revokeObjectURL(prev);
586
+ return null;
587
+ });
588
+ };
589
+ }, [result?.success, readFile]);
590
+
591
+ const handleDownload = () => {
592
+ if (!blobUrl) return;
593
+ const a = document.createElement("a");
594
+ a.href = blobUrl;
595
+ a.download = fileName;
596
+ a.click();
597
+ };
598
+
599
+ if (!result) {
600
+ return (
601
+ <div className="flex items-center gap-2 rounded-lg border px-5 py-4">
602
+ <RefreshCcwIcon className="size-4 animate-spin text-blue-600" />
603
+ <p>Loading file...</p>
604
+ </div>
605
+ );
606
+ }
607
+
608
+ if (!result.success) {
609
+ return (
610
+ <div className="flex items-center gap-2 rounded-lg border px-5 py-4">
611
+ <TriangleAlertIcon className="size-4 text-red-600" />
612
+ <p>File not found</p>
613
+ </div>
614
+ );
615
+ }
616
+
617
+ const isImage = mimeType?.startsWith("image/");
618
+ const isVideo = mimeType?.startsWith("video/");
619
+ const isAudio = mimeType?.startsWith("audio/");
620
+
621
+ return (
622
+ <div className="mb-2 flex flex-col gap-3 rounded-lg border px-5 py-4">
623
+ {blobUrl && isImage && (
624
+ <img
625
+ src={blobUrl}
626
+ alt={fileName}
627
+ className="max-h-64 w-fit rounded"
628
+ />
629
+ )}
630
+ {blobUrl && isVideo && (
631
+ <video
632
+ src={blobUrl}
633
+ controls
634
+ className="max-h-64 w-fit rounded"
635
+ />
636
+ )}
637
+ {blobUrl && isAudio && <audio src={blobUrl} controls />}
638
+ {blobUrl && !isImage && !isVideo && !isAudio && (
639
+ <div className="text-muted-foreground flex items-center gap-2">
640
+ <FileIcon className="size-5" />
641
+ <span>{fileName}</span>
642
+ </div>
643
+ )}
644
+ <button
645
+ type="button"
646
+ onClick={handleDownload}
647
+ className="bg-background hover:bg-accent flex w-fit items-center gap-2 rounded-md border px-4 py-2 text-sm"
648
+ >
649
+ <DownloadIcon className="size-4" />
650
+ Download {fileName}
651
+ </button>
652
+ </div>
653
+ );
654
+ },
655
+ },
656
+ }),
657
+ [ffmpegRef, file],
658
+ );
659
+ }
660
+
661
+ ```
662
+
628
663
  ## components.json
629
664
 
630
665
  ```json
@@ -658,13 +693,14 @@ export default function Home() {
658
693
  ## next.config.ts
659
694
 
660
695
  ```typescript
696
+ import { withAui } from "@assistant-ui/next";
661
697
  import type { NextConfig } from "next";
662
698
 
663
699
  const nextConfig: NextConfig = {
664
700
  /* config options here */
665
701
  };
666
702
 
667
- export default nextConfig;
703
+ export default withAui(nextConfig);
668
704
 
669
705
  ```
670
706
 
@@ -682,28 +718,29 @@ export default nextConfig;
682
718
  "start": "next start"
683
719
  },
684
720
  "dependencies": {
685
- "@ai-sdk/openai": "^3.0.64",
721
+ "@ai-sdk/openai": "^3.0.68",
686
722
  "@assistant-ui/react": "workspace:*",
687
723
  "@assistant-ui/react-ai-sdk": "workspace:*",
688
724
  "@assistant-ui/react-markdown": "workspace:*",
689
725
  "@assistant-ui/ui": "workspace:*",
690
726
  "@ffmpeg/ffmpeg": "^0.12.15",
691
727
  "@ffmpeg/util": "^0.12.2",
692
- "ai": "^6.0.188",
728
+ "ai": "^6.0.197",
693
729
  "class-variance-authority": "^0.7.1",
694
730
  "clsx": "^2.1.1",
695
- "lucide-react": "^1.16.0",
696
- "next": "^16.2.6",
697
- "react": "^19.2.6",
698
- "react-dom": "^19.2.6",
731
+ "lucide-react": "^1.17.0",
732
+ "next": "^16.2.7",
733
+ "react": "^19.2.7",
734
+ "react-dom": "^19.2.7",
699
735
  "tailwind-merge": "^3.6.0",
700
736
  "zod": "^4.4.3"
701
737
  },
702
738
  "devDependencies": {
739
+ "@assistant-ui/next": "workspace:*",
703
740
  "@assistant-ui/x-buildutils": "workspace:*",
704
741
  "@tailwindcss/postcss": "^4.3.0",
705
- "@types/node": "^25.9.1",
706
- "@types/react": "^19.2.15",
742
+ "@types/node": "^25.9.2",
743
+ "@types/react": "^19.2.17",
707
744
  "@types/react-dom": "^19.2.3",
708
745
  "postcss": "^8.5.15",
709
746
  "tailwindcss": "^4.3.0",