Buckets:
| <meta charset="utf-8" /><meta name="hf:doc:metadata" content="{"title":"Building a Next.js AI Chatbot with Vercel AI SDK","local":"building-a-nextjs-ai-chatbot-with-vercel-ai-sdk","sections":[{"title":"Prerequisites","local":"prerequisites","sections":[],"depth":2},{"title":"Step 1: Create the project","local":"step-1-create-the-project","sections":[],"depth":2},{"title":"Step 2: Configure Next.js for browser inference","local":"step-2-configure-nextjs-for-browser-inference","sections":[],"depth":2},{"title":"Step 3: Create the Web Worker","local":"step-3-create-the-web-worker","sections":[],"depth":2},{"title":"Step 4: Define the model configuration","local":"step-4-define-the-model-configuration","sections":[],"depth":2},{"title":"Step 5: Define tools","local":"step-5-define-tools","sections":[],"depth":2},{"title":"Step 6: Create the chat transport","local":"step-6-create-the-chat-transport","sections":[],"depth":2},{"title":"Step 7: Build the chat UI","local":"step-7-build-the-chat-ui","sections":[],"depth":2},{"title":"Step 8: Run the application","local":"step-8-run-the-application","sections":[],"depth":2},{"title":"Next steps","local":"next-steps","sections":[],"depth":2}],"depth":1}"> | |
| <link href="/docs/transformers.js/pr_1639/en/_app/immutable/assets/0.e3b0c442.css" rel="modulepreload"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/entry/start.7959d94a.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/chunks/scheduler.6efaaf90.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/chunks/singletons.a0579e01.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/chunks/paths.298d48dd.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/entry/app.ff61b44c.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/chunks/preload-helper.f4df480f.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/chunks/index.eb3e1f0f.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/nodes/0.42219c4a.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/chunks/each.e59479a4.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/nodes/38.09de7c85.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/chunks/Tip.292c2c3d.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/chunks/MermaidChart.svelte_svelte_type_style_lang.110c92a2.js"> | |
| <link rel="modulepreload" href="/docs/transformers.js/pr_1639/en/_app/immutable/chunks/CodeBlock.b7f804a3.js"><!-- HEAD_svelte-u9bgzb_START --><meta name="hf:doc:metadata" content="{"title":"Building a Next.js AI Chatbot with Vercel AI SDK","local":"building-a-nextjs-ai-chatbot-with-vercel-ai-sdk","sections":[{"title":"Prerequisites","local":"prerequisites","sections":[],"depth":2},{"title":"Step 1: Create the project","local":"step-1-create-the-project","sections":[],"depth":2},{"title":"Step 2: Configure Next.js for browser inference","local":"step-2-configure-nextjs-for-browser-inference","sections":[],"depth":2},{"title":"Step 3: Create the Web Worker","local":"step-3-create-the-web-worker","sections":[],"depth":2},{"title":"Step 4: Define the model configuration","local":"step-4-define-the-model-configuration","sections":[],"depth":2},{"title":"Step 5: Define tools","local":"step-5-define-tools","sections":[],"depth":2},{"title":"Step 6: Create the chat transport","local":"step-6-create-the-chat-transport","sections":[],"depth":2},{"title":"Step 7: Build the chat UI","local":"step-7-build-the-chat-ui","sections":[],"depth":2},{"title":"Step 8: Run the application","local":"step-8-run-the-application","sections":[],"depth":2},{"title":"Next steps","local":"next-steps","sections":[],"depth":2}],"depth":1}"><!-- HEAD_svelte-u9bgzb_END --> <p></p> <div class="items-center shrink-0 min-w-[100px] max-sm:min-w-[50px] justify-end ml-auto flex" style="float: right; margin-left: 10px; display: inline-flex; position: relative; z-index: 10;"><div class="inline-flex rounded-md max-sm:rounded-sm"><button class="inline-flex items-center gap-1 h-7 max-sm:h-7 px-2 max-sm:px-1.5 text-sm font-medium text-gray-800 border border-r-0 rounded-l-md max-sm:rounded-l-sm border-gray-200 bg-white hover:shadow-inner dark:border-gray-850 dark:bg-gray-950 dark:text-gray-200 dark:hover:bg-gray-800" aria-live="polite"><span class="inline-flex items-center justify-center rounded-md p-0.5 max-sm:p-0 hover:text-gray-800 dark:hover:text-gray-200"><svg class="sm:size-3.5 size-3" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg></span> <span>Copy page</span></button> <button class="inline-flex items-center justify-center w-6 max-sm:w-5 h-7 max-sm:h-7 disabled:pointer-events-none text-sm text-gray-500 hover:text-gray-700 dark:hover:text-white rounded-r-md max-sm:rounded-r-sm border border-l transition border-gray-200 bg-white hover:shadow-inner dark:border-gray-850 dark:bg-gray-950 dark:text-gray-200 dark:hover:bg-gray-800" aria-haspopup="menu" aria-expanded="false" aria-label="Open copy menu"><svg class="transition-transform text-gray-400 overflow-visible sm:size-3.5 size-3 rotate-0" width="1em" height="1em" viewBox="0 0 12 7" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 1L6 6L11 1" stroke="currentColor"></path></svg></button></div> </div> <h1 class="relative group"><a id="building-a-nextjs-ai-chatbot-with-vercel-ai-sdk" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#building-a-nextjs-ai-chatbot-with-vercel-ai-sdk"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Building a Next.js AI Chatbot with Vercel AI SDK</span></h1> <p data-svelte-h="svelte-cdwkfs">In this tutorial, we’ll build an in-browser AI chatbot using Next.js, Transformers.js, and the Vercel AI SDK v6. The chatbot runs entirely client-side with WebGPU acceleration — and supports tool calling with human approval.</p> <p data-svelte-h="svelte-1tlxmzn">Useful links:</p> <ul data-svelte-h="svelte-yqurg4"><li><a href="https://github.com/huggingface/transformers.js-examples/tree/main/next-vercel-ai-sdk-v6-tool-calling" rel="nofollow">Source code</a></li> <li><a href="https://www.browser-ai.dev/docs/ai-sdk-v6/transformers-js" rel="nofollow"><code>@browser-ai/transformers-js</code> docs</a></li> <li><a href="https://ai-sdk.dev/" rel="nofollow">Vercel AI SDK docs</a></li></ul> <h2 class="relative group"><a id="prerequisites" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#prerequisites"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Prerequisites</span></h2> <ul data-svelte-h="svelte-oeluux"><li><a href="https://nodejs.org/en/" rel="nofollow">Node.js</a> version 18+</li> <li><a href="https://www.npmjs.com/" rel="nofollow">npm</a> version 9+</li> <li>A browser with WebGPU support (Chrome 113+, Edge 113+, or Firefox/Safari with flags enabled)</li></ul> <h2 class="relative group"><a id="step-1-create-the-project" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#step-1-create-the-project"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Step 1: Create the project</span></h2> <p data-svelte-h="svelte-1w40azh">Create a new Next.js application:</p> <div class="code-block relative "><div class="absolute top-2.5 right-4"><button class="inline-flex items-center relative text-sm focus:text-green-500 cursor-pointer focus:outline-none transition duration-200 ease-in-out opacity-0 mx-0.5 text-gray-600 " title="code excerpt" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg> <div class="absolute pointer-events-none transition-opacity bg-black text-white py-1 px-2 leading-tight rounded font-normal shadow left-1/2 top-full transform -translate-x-1/2 translate-y-2 opacity-0"><div class="absolute bottom-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-black border-4 border-t-0" style="border-left-color: transparent; border-right-color: transparent; "></div> Copied</div></button></div> <pre class=""><!-- HTML_TAG_START -->npx create-next-app@latest next-ai-chatbot | |
| <span class="hljs-built_in">cd</span> next-ai-chatbot<!-- HTML_TAG_END --></pre></div> <p data-svelte-h="svelte-15hefzp">Install the AI and Transformers.js dependencies:</p> <div class="code-block relative "><div class="absolute top-2.5 right-4"><button class="inline-flex items-center relative text-sm focus:text-green-500 cursor-pointer focus:outline-none transition duration-200 ease-in-out opacity-0 mx-0.5 text-gray-600 " title="code excerpt" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg> <div class="absolute pointer-events-none transition-opacity bg-black text-white py-1 px-2 leading-tight rounded font-normal shadow left-1/2 top-full transform -translate-x-1/2 translate-y-2 opacity-0"><div class="absolute bottom-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-black border-4 border-t-0" style="border-left-color: transparent; border-right-color: transparent; "></div> Copied</div></button></div> <pre class=""><!-- HTML_TAG_START -->npm install ai @ai-sdk/react @browser-ai/transformers-js @huggingface/transformers zod<!-- HTML_TAG_END --></pre></div> <h2 class="relative group"><a id="step-2-configure-nextjs-for-browser-inference" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#step-2-configure-nextjs-for-browser-inference"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Step 2: Configure Next.js for browser inference</span></h2> <p data-svelte-h="svelte-1kpoltd">Transformers.js uses ONNX Runtime under the hood for both browser and server-side (Node.js) inference. In our case we only need the browser runtime so we can tell Next.js to exclude the Node.js-specific packages when bundling for the browser. Update <code>next.config.ts</code></p> <div class="code-block relative "><div class="absolute top-2.5 right-4"><button class="inline-flex items-center relative text-sm focus:text-green-500 cursor-pointer focus:outline-none transition duration-200 ease-in-out opacity-0 mx-0.5 text-gray-600 " title="code excerpt" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg> <div class="absolute pointer-events-none transition-opacity bg-black text-white py-1 px-2 leading-tight rounded font-normal shadow left-1/2 top-full transform -translate-x-1/2 translate-y-2 opacity-0"><div class="absolute bottom-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-black border-4 border-t-0" style="border-left-color: transparent; border-right-color: transparent; "></div> Copied</div></button></div> <pre class=""><!-- HTML_TAG_START --><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { <span class="hljs-title class_">NextConfig</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>; | |
| <span class="hljs-keyword">const</span> <span class="hljs-attr">nextConfig</span>: <span class="hljs-title class_">NextConfig</span> = { | |
| <span class="hljs-attr">output</span>: <span class="hljs-string">"export"</span>, <span class="hljs-comment">// optional: export as a static site</span> | |
| <span class="hljs-attr">turbopack</span>: {}, | |
| <span class="hljs-attr">webpack</span>: <span class="hljs-function">(<span class="hljs-params">config</span>) =></span> { | |
| config.<span class="hljs-property">resolve</span>.<span class="hljs-property">alias</span> = { | |
| ...config.<span class="hljs-property">resolve</span>.<span class="hljs-property">alias</span>, | |
| <span class="hljs-attr">sharp$</span>: <span class="hljs-literal">false</span>, | |
| <span class="hljs-string">"onnxruntime-node$"</span>: <span class="hljs-literal">false</span>, | |
| }; | |
| <span class="hljs-keyword">return</span> config; | |
| }, | |
| }; | |
| <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> nextConfig;<!-- HTML_TAG_END --></pre></div> <h2 class="relative group"><a id="step-3-create-the-web-worker" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#step-3-create-the-web-worker"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Step 3: Create the Web Worker</span></h2> <p data-svelte-h="svelte-1ueuvz9">Running model inference on the main thread would block the UI. The <code>@browser-ai/transformers-js</code> package provides a ready-made worker handler that handles all the complexity for you.</p> <p data-svelte-h="svelte-1rngpb1">Create <code>src/app/worker.ts</code>:</p> <div class="code-block relative "><div class="absolute top-2.5 right-4"><button class="inline-flex items-center relative text-sm focus:text-green-500 cursor-pointer focus:outline-none transition duration-200 ease-in-out opacity-0 mx-0.5 text-gray-600 " title="code excerpt" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg> <div class="absolute pointer-events-none transition-opacity bg-black text-white py-1 px-2 leading-tight rounded font-normal shadow left-1/2 top-full transform -translate-x-1/2 translate-y-2 opacity-0"><div class="absolute bottom-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-black border-4 border-t-0" style="border-left-color: transparent; border-right-color: transparent; "></div> Copied</div></button></div> <pre class=""><!-- HTML_TAG_START --><span class="hljs-keyword">import</span> { <span class="hljs-title class_">TransformersJSWorkerHandler</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"@browser-ai/transformers-js"</span>; | |
| <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">new</span> <span class="hljs-title class_">TransformersJSWorkerHandler</span>(); | |
| self.<span class="hljs-property">onmessage</span> = <span class="hljs-function">(<span class="hljs-params">msg: MessageEvent</span>) =></span> { | |
| handler.<span class="hljs-title function_">onmessage</span>(msg); | |
| };<!-- HTML_TAG_END --></pre></div> <p data-svelte-h="svelte-1tmmpn2">That’s it — the handler takes care of model loading, inference, streaming, and communication with the main thread.</p> <h2 class="relative group"><a id="step-4-define-the-model-configuration" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#step-4-define-the-model-configuration"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Step 4: Define the model configuration</span></h2> <p data-svelte-h="svelte-5qh26d">Create <code>src/app/models.ts</code> to define which models are available. These are ONNX-format models from Hugging Face:</p> <div class="code-block relative "><div class="absolute top-2.5 right-4"><button class="inline-flex items-center relative text-sm focus:text-green-500 cursor-pointer focus:outline-none transition duration-200 ease-in-out opacity-0 mx-0.5 text-gray-600 " title="code excerpt" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg> <div class="absolute pointer-events-none transition-opacity bg-black text-white py-1 px-2 leading-tight rounded font-normal shadow left-1/2 top-full transform -translate-x-1/2 translate-y-2 opacity-0"><div class="absolute bottom-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-black border-4 border-t-0" style="border-left-color: transparent; border-right-color: transparent; "></div> Copied</div></button></div> <pre class=""><!-- HTML_TAG_START --><span class="hljs-keyword">import</span> { <span class="hljs-title class_">WorkerLoadOptions</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"@browser-ai/transformers-js"</span>; | |
| <span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> <span class="hljs-title class_">ModelConfig</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_">Omit</span><<span class="hljs-title class_">WorkerLoadOptions</span>, "modelId"> { | |
| <span class="hljs-attr">id</span>: <span class="hljs-built_in">string</span>; | |
| <span class="hljs-attr">name</span>: <span class="hljs-built_in">string</span>; | |
| supportsWorker?: <span class="hljs-built_in">boolean</span>; | |
| } | |
| <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-attr">MODELS</span>: <span class="hljs-title class_">ModelConfig</span>[] = [ | |
| { | |
| <span class="hljs-attr">id</span>: <span class="hljs-string">"onnx-community/Qwen3-0.6B-ONNX"</span>, | |
| <span class="hljs-attr">name</span>: <span class="hljs-string">"Qwen3 0.6B"</span>, | |
| <span class="hljs-attr">device</span>: <span class="hljs-string">"webgpu"</span>, | |
| <span class="hljs-attr">dtype</span>: <span class="hljs-string">"q4f16"</span>, | |
| <span class="hljs-attr">supportsWorker</span>: <span class="hljs-literal">true</span>, | |
| }, | |
| { | |
| <span class="hljs-attr">id</span>: <span class="hljs-string">"onnx-community/granite-4.0-350m-ONNX-web"</span>, | |
| <span class="hljs-attr">name</span>: <span class="hljs-string">"Granite 4.0 350M"</span>, | |
| <span class="hljs-attr">device</span>: <span class="hljs-string">"webgpu"</span>, | |
| <span class="hljs-attr">dtype</span>: <span class="hljs-string">"fp16"</span>, | |
| <span class="hljs-attr">supportsWorker</span>: <span class="hljs-literal">true</span>, | |
| }, | |
| ];<!-- HTML_TAG_END --></pre></div> <blockquote class="tip"><p data-svelte-h="svelte-1pqpmuc">For tool calling, use reasoning models like Qwen3 which handle multi-step reasoning well, or fine-tuned model specifically for tool-calling capabilities. The <code>supportsWorker</code> flag controls whether the model is loaded in a Web Worker for better performance.</p></blockquote> <h2 class="relative group"><a id="step-5-define-tools" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#step-5-define-tools"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Step 5: Define tools</span></h2> <p data-svelte-h="svelte-oobfug">Create <code>src/app/tools.ts</code> with tools the model can call. Each tool uses <a href="https://zod.dev/" rel="nofollow">Zod</a> for input validation:</p> <div class="code-block relative "><div class="absolute top-2.5 right-4"><button class="inline-flex items-center relative text-sm focus:text-green-500 cursor-pointer focus:outline-none transition duration-200 ease-in-out opacity-0 mx-0.5 text-gray-600 " title="code excerpt" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg> <div class="absolute pointer-events-none transition-opacity bg-black text-white py-1 px-2 leading-tight rounded font-normal shadow left-1/2 top-full transform -translate-x-1/2 translate-y-2 opacity-0"><div class="absolute bottom-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-black border-4 border-t-0" style="border-left-color: transparent; border-right-color: transparent; "></div> Copied</div></button></div> <pre class=""><!-- HTML_TAG_START --><span class="hljs-keyword">import</span> { tool } <span class="hljs-keyword">from</span> <span class="hljs-string">"ai"</span>; | |
| <span class="hljs-keyword">import</span> z <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>; | |
| <span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-title function_">createTools</span> = (<span class="hljs-params"></span>) => ({ | |
| <span class="hljs-attr">getCurrentTime</span>: <span class="hljs-title function_">tool</span>({ | |
| <span class="hljs-attr">description</span>: <span class="hljs-string">"Get the current date and time."</span>, | |
| <span class="hljs-attr">inputSchema</span>: z.<span class="hljs-title function_">object</span>({}), | |
| <span class="hljs-attr">execute</span>: <span class="hljs-keyword">async</span> () => { | |
| <span class="hljs-keyword">const</span> now = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(); | |
| <span class="hljs-keyword">return</span> { | |
| <span class="hljs-attr">timestamp</span>: now.<span class="hljs-title function_">toISOString</span>(), | |
| <span class="hljs-attr">date</span>: now.<span class="hljs-title function_">toLocaleDateString</span>(<span class="hljs-string">"en-US"</span>, { | |
| <span class="hljs-attr">weekday</span>: <span class="hljs-string">"long"</span>, <span class="hljs-attr">year</span>: <span class="hljs-string">"numeric"</span>, <span class="hljs-attr">month</span>: <span class="hljs-string">"long"</span>, <span class="hljs-attr">day</span>: <span class="hljs-string">"numeric"</span>, | |
| }), | |
| <span class="hljs-attr">time</span>: now.<span class="hljs-title function_">toLocaleTimeString</span>(<span class="hljs-string">"en-US"</span>, { | |
| <span class="hljs-attr">hour</span>: <span class="hljs-string">"2-digit"</span>, <span class="hljs-attr">minute</span>: <span class="hljs-string">"2-digit"</span>, <span class="hljs-attr">second</span>: <span class="hljs-string">"2-digit"</span>, <span class="hljs-attr">hour12</span>: <span class="hljs-literal">true</span>, | |
| }), | |
| <span class="hljs-attr">timezone</span>: <span class="hljs-title class_">Intl</span>.<span class="hljs-title class_">DateTimeFormat</span>().<span class="hljs-title function_">resolvedOptions</span>().<span class="hljs-property">timeZone</span>, | |
| }; | |
| }, | |
| }), | |
| <span class="hljs-attr">randomNumber</span>: <span class="hljs-title function_">tool</span>({ | |
| <span class="hljs-attr">description</span>: <span class="hljs-string">"Generate a random integer between min and max (inclusive)."</span>, | |
| <span class="hljs-attr">inputSchema</span>: z.<span class="hljs-title function_">object</span>({ | |
| <span class="hljs-attr">min</span>: z.<span class="hljs-title function_">number</span>().<span class="hljs-title function_">describe</span>(<span class="hljs-string">"The minimum value (inclusive)"</span>), | |
| <span class="hljs-attr">max</span>: z.<span class="hljs-title function_">number</span>().<span class="hljs-title function_">describe</span>(<span class="hljs-string">"The maximum value (inclusive)"</span>), | |
| }), | |
| <span class="hljs-attr">execute</span>: <span class="hljs-keyword">async</span> ({ min, max }) => { | |
| <span class="hljs-keyword">return</span> <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">floor</span>(<span class="hljs-title class_">Math</span>.<span class="hljs-title function_">random</span>() * (<span class="hljs-title class_">Math</span>.<span class="hljs-title function_">floor</span>(max) - <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">ceil</span>(min) + <span class="hljs-number">1</span>)) + <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">ceil</span>(min); | |
| }, | |
| }), | |
| <span class="hljs-attr">getLocation</span>: <span class="hljs-title function_">tool</span>({ | |
| <span class="hljs-attr">description</span>: <span class="hljs-string">"Get the user's current geographic location."</span>, | |
| <span class="hljs-attr">inputSchema</span>: z.<span class="hljs-title function_">object</span>({}), | |
| <span class="hljs-attr">needsApproval</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// requires user confirmation before executing</span> | |
| <span class="hljs-attr">execute</span>: <span class="hljs-keyword">async</span> () => { | |
| <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =></span> { | |
| navigator.<span class="hljs-property">geolocation</span>.<span class="hljs-title function_">getCurrentPosition</span>( | |
| <span class="hljs-function">(<span class="hljs-params">pos</span>) =></span> <span class="hljs-title function_">resolve</span>({ | |
| <span class="hljs-attr">latitude</span>: pos.<span class="hljs-property">coords</span>.<span class="hljs-property">latitude</span>, | |
| <span class="hljs-attr">longitude</span>: pos.<span class="hljs-property">coords</span>.<span class="hljs-property">longitude</span>, | |
| }), | |
| <span class="hljs-function">(<span class="hljs-params">err</span>) =></span> <span class="hljs-title function_">reject</span>(err.<span class="hljs-property">message</span>), | |
| ); | |
| }); | |
| }, | |
| }), | |
| });<!-- HTML_TAG_END --></pre></div> <p data-svelte-h="svelte-fcq603">The <code>getLocation</code> tool uses <code>needsApproval: true</code>, which means the AI SDK will pause execution and wait for the user to approve or reject the tool call before running it.</p> <h2 class="relative group"><a id="step-6-create-the-chat-transport" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#step-6-create-the-chat-transport"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Step 6: Create the chat transport</span></h2> <p data-svelte-h="svelte-yuv7mn">The Vercel AI SDK’s <code>useChat</code> hook needs a <a href="https://ai-sdk.dev/docs/ai-sdk-ui/transport" rel="nofollow">transport</a> that handles communication with the model. For client-side inference, we implement a custom <code>ChatTransport</code>.</p> <p data-svelte-h="svelte-56csg1">Create <code>src/app/chat-transport.ts</code>:</p> <div class="code-block relative "><div class="absolute top-2.5 right-4"><button class="inline-flex items-center relative text-sm focus:text-green-500 cursor-pointer focus:outline-none transition duration-200 ease-in-out opacity-0 mx-0.5 text-gray-600 " title="code excerpt" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg> <div class="absolute pointer-events-none transition-opacity bg-black text-white py-1 px-2 leading-tight rounded font-normal shadow left-1/2 top-full transform -translate-x-1/2 translate-y-2 opacity-0"><div class="absolute bottom-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-black border-4 border-t-0" style="border-left-color: transparent; border-right-color: transparent; "></div> Copied</div></button></div> <pre class=""><!-- HTML_TAG_START --><span class="hljs-keyword">import</span> { | |
| <span class="hljs-title class_">ChatTransport</span>, <span class="hljs-title class_">UIMessageChunk</span>, streamText, | |
| convertToModelMessages, <span class="hljs-title class_">ChatRequestOptions</span>, | |
| createUIMessageStream, stepCountIs, | |
| } <span class="hljs-keyword">from</span> <span class="hljs-string">"ai"</span>; | |
| <span class="hljs-keyword">import</span> { | |
| <span class="hljs-title class_">TransformersJSLanguageModel</span>, | |
| <span class="hljs-title class_">TransformersUIMessage</span>, | |
| transformersJS, | |
| } <span class="hljs-keyword">from</span> <span class="hljs-string">"@browser-ai/transformers-js"</span>; | |
| <span class="hljs-keyword">import</span> { <span class="hljs-variable constant_">MODELS</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"./models"</span>; | |
| <span class="hljs-keyword">import</span> { createTools } <span class="hljs-keyword">from</span> <span class="hljs-string">"./tools"</span>; | |
| <span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">TransformersChatTransport</span> | |
| <span class="hljs-keyword">implements</span> <span class="hljs-title class_">ChatTransport</span><<span class="hljs-title class_">TransformersUIMessage</span>> | |
| { | |
| <span class="hljs-keyword">private</span> <span class="hljs-attr">model</span>: <span class="hljs-title class_">TransformersJSLanguageModel</span>; | |
| <span class="hljs-keyword">private</span> <span class="hljs-attr">tools</span>: <span class="hljs-title class_">ReturnType</span><<span class="hljs-keyword">typeof</span> createTools>; | |
| <span class="hljs-title function_">constructor</span>(<span class="hljs-params"></span>) { | |
| <span class="hljs-keyword">const</span> config = <span class="hljs-variable constant_">MODELS</span>[<span class="hljs-number">0</span>]; | |
| <span class="hljs-variable language_">this</span>.<span class="hljs-property">model</span> = <span class="hljs-title function_">transformersJS</span>(config.<span class="hljs-property">id</span>, { | |
| <span class="hljs-attr">device</span>: config.<span class="hljs-property">device</span>, | |
| <span class="hljs-attr">dtype</span>: config.<span class="hljs-property">dtype</span>, | |
| ...(config.<span class="hljs-property">supportsWorker</span> | |
| ? { | |
| <span class="hljs-attr">worker</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">Worker</span>(<span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(<span class="hljs-string">"./worker.ts"</span>, <span class="hljs-keyword">import</span>.<span class="hljs-property">meta</span>.<span class="hljs-property">url</span>), { | |
| <span class="hljs-attr">type</span>: <span class="hljs-string">"module"</span>, | |
| }), | |
| } | |
| : {}), | |
| }); | |
| <span class="hljs-variable language_">this</span>.<span class="hljs-property">tools</span> = <span class="hljs-title function_">createTools</span>(); | |
| } | |
| <span class="hljs-keyword">async</span> <span class="hljs-title function_">sendMessages</span>( | |
| <span class="hljs-attr">options</span>: { | |
| <span class="hljs-attr">chatId</span>: <span class="hljs-built_in">string</span>; | |
| <span class="hljs-attr">messages</span>: <span class="hljs-title class_">TransformersUIMessage</span>[]; | |
| <span class="hljs-attr">abortSignal</span>: <span class="hljs-title class_">AbortSignal</span> | <span class="hljs-literal">undefined</span>; | |
| } & { | |
| <span class="hljs-attr">trigger</span>: <span class="hljs-string">"submit-message"</span> | <span class="hljs-string">"submit-tool-result"</span> | <span class="hljs-string">"regenerate-message"</span>; | |
| <span class="hljs-attr">messageId</span>: <span class="hljs-built_in">string</span> | <span class="hljs-literal">undefined</span>; | |
| } & <span class="hljs-title class_">ChatRequestOptions</span>, | |
| ): <span class="hljs-title class_">Promise</span><<span class="hljs-title class_">ReadableStream</span><<span class="hljs-title class_">UIMessageChunk</span>>> { | |
| <span class="hljs-keyword">const</span> { messages, abortSignal } = options; | |
| <span class="hljs-keyword">const</span> prompt = <span class="hljs-keyword">await</span> <span class="hljs-title function_">convertToModelMessages</span>(messages); | |
| <span class="hljs-keyword">return</span> createUIMessageStream<<span class="hljs-title class_">TransformersUIMessage</span>>({ | |
| <span class="hljs-attr">execute</span>: <span class="hljs-keyword">async</span> ({ writer }) => { | |
| <span class="hljs-comment">// Track download progress if the model hasn't been downloaded yet</span> | |
| <span class="hljs-keyword">let</span> <span class="hljs-attr">downloadProgressId</span>: <span class="hljs-built_in">string</span> | <span class="hljs-literal">undefined</span>; | |
| <span class="hljs-keyword">const</span> availability = <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">model</span>.<span class="hljs-title function_">availability</span>(); | |
| <span class="hljs-keyword">if</span> (availability !== <span class="hljs-string">"available"</span>) { | |
| <span class="hljs-keyword">await</span> <span class="hljs-variable language_">this</span>.<span class="hljs-property">model</span>.<span class="hljs-title function_">createSessionWithProgress</span>( | |
| <span class="hljs-function">(<span class="hljs-params">progress: <span class="hljs-built_in">number</span></span>) =></span> { | |
| <span class="hljs-keyword">const</span> percent = <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">round</span>(progress * <span class="hljs-number">100</span>); | |
| <span class="hljs-keyword">if</span> (progress >= <span class="hljs-number">1</span>) { | |
| <span class="hljs-keyword">if</span> (downloadProgressId) { | |
| writer.<span class="hljs-title function_">write</span>({ | |
| <span class="hljs-attr">type</span>: <span class="hljs-string">"data-modelDownloadProgress"</span>, | |
| <span class="hljs-attr">id</span>: downloadProgressId, | |
| <span class="hljs-attr">data</span>: { | |
| <span class="hljs-attr">status</span>: <span class="hljs-string">"complete"</span>, <span class="hljs-attr">progress</span>: <span class="hljs-number">100</span>, | |
| <span class="hljs-attr">message</span>: <span class="hljs-string">"Model ready!"</span>, | |
| }, | |
| }); | |
| } | |
| <span class="hljs-keyword">return</span>; | |
| } | |
| <span class="hljs-keyword">if</span> (!downloadProgressId) { | |
| downloadProgressId = <span class="hljs-string">`download-<span class="hljs-subst">${<span class="hljs-built_in">Date</span>.now()}</span>`</span>; | |
| } | |
| writer.<span class="hljs-title function_">write</span>({ | |
| <span class="hljs-attr">type</span>: <span class="hljs-string">"data-modelDownloadProgress"</span>, | |
| <span class="hljs-attr">id</span>: downloadProgressId, | |
| <span class="hljs-attr">data</span>: { | |
| <span class="hljs-attr">status</span>: <span class="hljs-string">"downloading"</span>, <span class="hljs-attr">progress</span>: percent, | |
| <span class="hljs-attr">message</span>: <span class="hljs-string">`Downloading model... <span class="hljs-subst">${percent}</span>%`</span>, | |
| }, | |
| }); | |
| }, | |
| ); | |
| } | |
| <span class="hljs-keyword">const</span> result = <span class="hljs-title function_">streamText</span>({ | |
| <span class="hljs-attr">model</span>: <span class="hljs-variable language_">this</span>.<span class="hljs-property">model</span>, | |
| <span class="hljs-attr">tools</span>: <span class="hljs-variable language_">this</span>.<span class="hljs-property">tools</span>, | |
| <span class="hljs-attr">stopWhen</span>: <span class="hljs-title function_">stepCountIs</span>(<span class="hljs-number">5</span>), | |
| <span class="hljs-attr">messages</span>: prompt, | |
| abortSignal, | |
| }); | |
| writer.<span class="hljs-title function_">merge</span>(result.<span class="hljs-title function_">toUIMessageStream</span>({ <span class="hljs-attr">sendStart</span>: <span class="hljs-literal">false</span> })); | |
| }, | |
| }); | |
| } | |
| <span class="hljs-keyword">async</span> <span class="hljs-title function_">reconnectToStream</span>(): <span class="hljs-title class_">Promise</span><<span class="hljs-title class_">ReadableStream</span><<span class="hljs-title class_">UIMessageChunk</span>> | <span class="hljs-literal">null</span>> { | |
| <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>; | |
| } | |
| }<!-- HTML_TAG_END --></pre></div> <p data-svelte-h="svelte-1ew9lxu">Key parts of the transport:</p> <ul data-svelte-h="svelte-19ftkyp"><li><strong>Availability check</strong>: Determines if the model needs downloading before inference.</li> <li><strong>Progress streaming</strong>: Sends download progress as custom data parts (<code>data-modelDownloadProgress</code>) that the UI can render as a progress bar.</li> <li><strong>Tool support</strong>: Passes the tools to <code>streamText()</code> so the model can call them.</li> <li><strong>Step limiting</strong>: <code>stopWhen: stepCountIs(5)</code> prevents infinite tool-calling loops.</li></ul> <h2 class="relative group"><a id="step-7-build-the-chat-ui" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#step-7-build-the-chat-ui"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Step 7: Build the chat UI</span></h2> <p data-svelte-h="svelte-5i8jhk">Now wire everything together in your page component. Create <code>src/app/page.tsx</code>:</p> <div class="code-block relative "><div class="absolute top-2.5 right-4"><button class="inline-flex items-center relative text-sm focus:text-green-500 cursor-pointer focus:outline-none transition duration-200 ease-in-out opacity-0 mx-0.5 text-gray-600 " title="code excerpt" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg> <div class="absolute pointer-events-none transition-opacity bg-black text-white py-1 px-2 leading-tight rounded font-normal shadow left-1/2 top-full transform -translate-x-1/2 translate-y-2 opacity-0"><div class="absolute bottom-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-black border-4 border-t-0" style="border-left-color: transparent; border-right-color: transparent; "></div> Copied</div></button></div> <pre class=""><!-- HTML_TAG_START --><span class="hljs-string">"use client"</span>; | |
| <span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>; | |
| <span class="hljs-keyword">import</span> { useChat } <span class="hljs-keyword">from</span> <span class="hljs-string">"@ai-sdk/react"</span>; | |
| <span class="hljs-keyword">import</span> { <span class="hljs-title class_">TransformersUIMessage</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"@browser-ai/transformers-js"</span>; | |
| <span class="hljs-keyword">import</span> { lastAssistantMessageIsCompleteWithApprovalResponses } <span class="hljs-keyword">from</span> <span class="hljs-string">"ai"</span>; | |
| <span class="hljs-keyword">import</span> { <span class="hljs-title class_">TransformersChatTransport</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">"./chat-transport"</span>; | |
| <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">ChatPage</span>(<span class="hljs-params"></span>) { | |
| <span class="hljs-keyword">const</span> [input, setInput] = <span class="hljs-title function_">useState</span>(<span class="hljs-string">""</span>); | |
| <span class="hljs-keyword">const</span> { | |
| messages, | |
| sendMessage, | |
| status, | |
| stop, | |
| addToolApprovalResponse, | |
| } = useChat<<span class="hljs-title class_">TransformersUIMessage</span>>({ | |
| <span class="hljs-attr">transport</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">TransformersChatTransport</span>(), | |
| <span class="hljs-attr">experimental_throttle</span>: <span class="hljs-number">75</span>, | |
| <span class="hljs-comment">// Automatically resumes after tool approval responses are submitted</span> | |
| <span class="hljs-attr">sendAutomaticallyWhen</span>: lastAssistantMessageIsCompleteWithApprovalResponses, | |
| }); | |
| <span class="hljs-keyword">const</span> <span class="hljs-title function_">handleSubmit</span> = (<span class="hljs-params">e: React.FormEvent</span>) => { | |
| e.<span class="hljs-title function_">preventDefault</span>(); | |
| <span class="hljs-keyword">if</span> (input.<span class="hljs-title function_">trim</span>() && status === <span class="hljs-string">"ready"</span>) { | |
| <span class="hljs-title function_">sendMessage</span>({ <span class="hljs-attr">text</span>: input }); | |
| <span class="hljs-title function_">setInput</span>(<span class="hljs-string">""</span>); | |
| } | |
| }; | |
| <span class="hljs-keyword">return</span> ( | |
| <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">maxWidth:</span> <span class="hljs-attr">600</span>, <span class="hljs-attr">margin:</span> "<span class="hljs-attr">0</span> <span class="hljs-attr">auto</span>", <span class="hljs-attr">padding:</span> <span class="hljs-attr">24</span> }}></span> | |
| <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>AI Chatbot<span class="hljs-tag"></<span class="hljs-name">h1</span>></span> | |
| <span class="hljs-tag"><<span class="hljs-name">div</span>></span> | |
| {messages.map((message) => ( | |
| <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{message.id}</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">marginBottom:</span> <span class="hljs-attr">16</span> }}></span> | |
| <span class="hljs-tag"><<span class="hljs-name">strong</span>></span>{message.role === "user" ? "You" : "Assistant"}:<span class="hljs-tag"></<span class="hljs-name">strong</span>></span> | |
| {message.parts.map((part, i) => { | |
| switch (part.type) { | |
| case "text": | |
| return <span class="hljs-tag"><<span class="hljs-name">p</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{i}</span>></span>{part.text}<span class="hljs-tag"></<span class="hljs-name">p</span>></span>; | |
| case "data-modelDownloadProgress": | |
| if (!part.data.message) return null; | |
| return ( | |
| <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{i}</span>></span> | |
| <span class="hljs-tag"><<span class="hljs-name">p</span>></span>{part.data.message}<span class="hljs-tag"></<span class="hljs-name">p</span>></span> | |
| {part.data.status === "downloading" && ( | |
| <span class="hljs-tag"><<span class="hljs-name">progress</span> <span class="hljs-attr">value</span>=<span class="hljs-string">{part.data.progress}</span> <span class="hljs-attr">max</span>=<span class="hljs-string">{100}</span> /></span> | |
| )} | |
| <span class="hljs-tag"></<span class="hljs-name">div</span>></span> | |
| ); | |
| default: | |
| // Handle tool parts | |
| if (part.type.startsWith("tool-") && "state" in part) { | |
| if ( | |
| part.state === "approval-requested" && | |
| "approval" in part | |
| ) { | |
| return ( | |
| <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{i}</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">border:</span> "<span class="hljs-attr">1px</span> <span class="hljs-attr">solid</span> #<span class="hljs-attr">ccc</span>", <span class="hljs-attr">padding:</span> <span class="hljs-attr">8</span> }}></span> | |
| <span class="hljs-tag"><<span class="hljs-name">p</span>></span>Tool <span class="hljs-tag"><<span class="hljs-name">strong</span>></span>{part.type.replace("tool-", "")}<span class="hljs-tag"></<span class="hljs-name">strong</span>></span> wants to run.<span class="hljs-tag"></<span class="hljs-name">p</span>></span> | |
| <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =></span> | |
| addToolApprovalResponse({ id: part.approval!.id, approved: true }) | |
| }> | |
| Approve | |
| <span class="hljs-tag"></<span class="hljs-name">button</span>></span> | |
| <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =></span> | |
| addToolApprovalResponse({ | |
| id: part.approval!.id, approved: false, | |
| reason: "User denied", | |
| }) | |
| }> | |
| Deny | |
| <span class="hljs-tag"></<span class="hljs-name">button</span>></span> | |
| <span class="hljs-tag"></<span class="hljs-name">div</span>></span> | |
| ); | |
| } | |
| if ("output" in part && part.output) { | |
| return ( | |
| <span class="hljs-tag"><<span class="hljs-name">pre</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{i}</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">background:</span> "#<span class="hljs-attr">f5f5f5</span>", <span class="hljs-attr">padding:</span> <span class="hljs-attr">8</span> }}></span> | |
| {JSON.stringify(part.output, null, 2)} | |
| <span class="hljs-tag"></<span class="hljs-name">pre</span>></span> | |
| ); | |
| } | |
| } | |
| return null; | |
| } | |
| })} | |
| <span class="hljs-tag"></<span class="hljs-name">div</span>></span> | |
| ))} | |
| <span class="hljs-tag"></<span class="hljs-name">div</span>></span> | |
| {status === "submitted" && <span class="hljs-tag"><<span class="hljs-name">p</span>></span><span class="hljs-tag"><<span class="hljs-name">em</span>></span>Thinking...<span class="hljs-tag"></<span class="hljs-name">em</span>></span><span class="hljs-tag"></<span class="hljs-name">p</span>></span>} | |
| <span class="hljs-tag"><<span class="hljs-name">form</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{handleSubmit}</span>></span> | |
| <span class="hljs-tag"><<span class="hljs-name">input</span> | |
| <span class="hljs-attr">value</span>=<span class="hljs-string">{input}</span> | |
| <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =></span> setInput(e.target.value)} | |
| placeholder="Ask something..." | |
| style={{ width: "100%", padding: 8 }} | |
| /> | |
| <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">marginTop:</span> <span class="hljs-attr">8</span> }}></span> | |
| {status === "streaming" ? ( | |
| <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{stop}</span>></span>Stop<span class="hljs-tag"></<span class="hljs-name">button</span>></span> | |
| ) : ( | |
| <span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">disabled</span>=<span class="hljs-string">{!input.trim()}</span>></span>Send<span class="hljs-tag"></<span class="hljs-name">button</span>></span> | |
| )} | |
| <span class="hljs-tag"></<span class="hljs-name">div</span>></span> | |
| <span class="hljs-tag"></<span class="hljs-name">form</span>></span> | |
| <span class="hljs-tag"></<span class="hljs-name">div</span>></span></span> | |
| ); | |
| }<!-- HTML_TAG_END --></pre></div> <p data-svelte-h="svelte-1fmepox">The component renders message parts based on their <code>type</code>:</p> <ul data-svelte-h="svelte-1xtdjbi"><li><code>text</code> — standard text output from the model.</li> <li><code>data-modelDownloadProgress</code> — custom data parts sent by the transport during model download.</li> <li><code>tool-*</code> — tool call parts with states like <code>approval-requested</code>, <code>output-available</code>, etc.</li></ul> <p data-svelte-h="svelte-f6xors">The <code>sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses</code> option tells <code>useChat</code> to automatically resume generation after the user responds to a tool approval request.</p> <h2 class="relative group"><a id="step-8-run-the-application" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#step-8-run-the-application"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Step 8: Run the application</span></h2> <p data-svelte-h="svelte-1htrph">Start the development server:</p> <div class="code-block relative "><div class="absolute top-2.5 right-4"><button class="inline-flex items-center relative text-sm focus:text-green-500 cursor-pointer focus:outline-none transition duration-200 ease-in-out opacity-0 mx-0.5 text-gray-600 " title="code excerpt" type="button"><svg class="" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M28,10V28H10V10H28m0-2H10a2,2,0,0,0-2,2V28a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2Z" transform="translate(0)"></path><path d="M4,18H2V4A2,2,0,0,1,4,2H18V4H4Z" transform="translate(0)"></path><rect fill="none" width="32" height="32"></rect></svg> <div class="absolute pointer-events-none transition-opacity bg-black text-white py-1 px-2 leading-tight rounded font-normal shadow left-1/2 top-full transform -translate-x-1/2 translate-y-2 opacity-0"><div class="absolute bottom-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-black border-4 border-t-0" style="border-left-color: transparent; border-right-color: transparent; "></div> Copied</div></button></div> <pre class=""><!-- HTML_TAG_START -->npm run dev<!-- HTML_TAG_END --></pre></div> <p data-svelte-h="svelte-1ouaoyk">Open your browser and navigate to the URL shown in the terminal. The first time you send a message, the model will be downloaded and cached in the browser. Subsequent visits will load the cached model.</p> <p data-svelte-h="svelte-tp2g41">Try prompts like:</p> <ul data-svelte-h="svelte-knpt5y"><li>“What time is it?”</li> <li>“Generate a random number between 1 and 100”</li> <li>“Where am I located?” (this will trigger a tool approval prompt)</li></ul> <h2 class="relative group"><a id="next-steps" class="header-link block pr-1.5 text-lg no-hover:hidden with-hover:absolute with-hover:p-1.5 with-hover:opacity-0 with-hover:group-hover:opacity-100 with-hover:right-full" href="#next-steps"><span><svg class="" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span></a> <span>Next steps</span></h2> <ul data-svelte-h="svelte-1uwz74p"><li>Add more models and a model selector — see the <a href="https://github.com/huggingface/transformers.js-examples/tree/main/next-vercel-ai-sdk-v6-tool-calling" rel="nofollow">full example source</a> for a multi-model implementation with Zustand state management.</li> <li>Add a browser compatibility check with <code>doesBrowserSupportTransformersJS()</code> and fall back to a server-side route if WebGPU is unavailable.</li> <li>Explore the <a href="https://ai-sdk.dev/docs/agents/overview" rel="nofollow">Vercel AI SDK agents documentation</a> for more complex agent patterns.</li> <li>See the <a href="../integrations/vercel-ai-sdk">Vercel AI SDK guide</a> for a reference of all supported features (embeddings, vision, transcription, etc.).</li></ul> <a class="!text-gray-400 !no-underline text-sm flex items-center not-prose mt-4" href="https://github.com/huggingface/transformers.js/blob/main/packages/transformers/docs/source/tutorials/next-ai-sdk.md" target="_blank"><svg class="mr-1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" focusable="false" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 32 32"><path d="M31,16l-7,7l-1.41-1.41L28.17,16l-5.58-5.59L24,9l7,7z"></path><path d="M1,16l7-7l1.41,1.41L3.83,16l5.58,5.59L8,23l-7-7z"></path><path d="M12.419,25.484L17.639,6.552l1.932,0.518L14.351,26.002z"></path></svg> <span data-svelte-h="svelte-zjs2n5"><span class="underline">Update</span> on GitHub</span></a> <p></p> | |
| <script> | |
| { | |
| __sveltekit_14rd6kz = { | |
| assets: "/docs/transformers.js/pr_1639/en", | |
| base: "/docs/transformers.js/pr_1639/en", | |
| env: {} | |
| }; | |
| const element = document.currentScript.parentElement; | |
| const data = [null,null]; | |
| Promise.all([ | |
| import("/docs/transformers.js/pr_1639/en/_app/immutable/entry/start.7959d94a.js"), | |
| import("/docs/transformers.js/pr_1639/en/_app/immutable/entry/app.ff61b44c.js") | |
| ]).then(([kit, app]) => { | |
| kit.start(app, element, { | |
| node_ids: [0, 38], | |
| data, | |
| form: null, | |
| error: null | |
| }); | |
| }); | |
| } | |
| </script> | |
Xet Storage Details
- Size:
- 67.7 kB
- Xet hash:
- 0532cd567bfdb24bec10c195322fcbae46e662d2d18cd319bd9c4a30302a6de3
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.