Gebruik de Vercel AI SDK UI en AI-elementen met de Prompt API.

Gepubliceerd: 16 juli 2026

In ' Gebruik de ingebouwde Prompt API met de Vercel AI SDK' zag je de vier belangrijkste generatieprimitieven, namelijk generateText() , streamText() , hybride code en gestructureerde uitvoer met Output.object() , allemaal mogelijk gemaakt door @browser-ai/core . Deze keer bouw je iets interactiever: een complete streaming chat-UI die volledig in de browser draait, met automatische terugval naar een cloudmodel wanneer de Prompt API niet beschikbaar is.

Wat je aan het bouwen bent

Een React-chatinterface die het volgende doet:

  • Maakt gebruik van de useChat hook van de Vercel AI SDK voor het streamen van gesprekken die meerdere beurten duren.
  • Voert de modelloop uit in de browser zonder dat een backend-server nodig is.
  • Schakelt automatisch terug naar Gemini 2.5 Flash wanneer de Prompt API niet beschikbaar is.
  • Geeft antwoorden van de assistent weer als GitHub-achtige Markdown, waarbij onvolledige tokens tijdens het streamen worden verwerkt.
  • Het apparaat toont een "Bezig met nadenken..."-flits terwijl het wacht op het eerste teken.
  • Scrollt automatisch naar nieuwe berichten, met een knop om terug naar beneden te scrollen wanneer je omhoog hebt gescrold.

Aanvullende afhankelijkheden

Naast ai , @browser-ai/core en @ai-sdk/google heeft de chat-UI React, de React-bindings van de AI SDK en een paar Markdown-pakketten nodig:

npm install react react-dom @ai-sdk/react
npm install react-markdown remark-gfm harden-react-markdown
npm install -D @types/react @types/react-dom

Voeg voor de gebruikersinterface ook Tailwind CSS, enkele componenthulpprogramma's en Lucide voor pictogrammen toe:

npm install -D tailwindcss postcss autoprefixer
npm install clsx tailwind-merge lucide-react

Stemconfiguratie: multi-entry build

Het project heeft al index.html . Voeg chat.html toe als tweede toegangspunt en configureer Vite om beide bestanden te compileren:

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: { alias: { '@': resolve(__dirname, './src') } },
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        chat: resolve(__dirname, 'chat.html'),
      },
    },
  },
});

chat.html is minimaal, slechts een <div id="root"> en een scripttag:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Built-in AI Chatbot</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/chat.tsx"></script>
  </body>
</html>

Automatische modelselectie

In de chatbot maakt de app die keuze automatisch: eerst probeert hij het ingebouwde model en als de Prompt API niet beschikbaar is, schakelt hij over op de cloud.

Dit gebeurt tijdens het laden van de module, voordat React wordt geladen, zodat de agent klaar is tegen de tijd dat de gebruiker zijn eerste bericht typt:

const agentPromise: Promise<ToolLoopAgent> = (async () => {
  const builtIn = browserAI();
  let model: any = builtIn;

  if (typeof builtIn.availability === 'function') {
    const availability = await builtIn.availability();
    if (availability === 'unavailable') {
      const { createGoogleGenerativeAI } = await import('@ai-sdk/google');
      model = createGoogleGenerativeAI({ apiKey })('gemini-2.5-flash');
    } else if (availability === 'downloadable') {
      await builtIn.createSessionWithProgress(() => {});
    }
  }

  return new ToolLoopAgent({ model, instructions: 'You are a helpful assistant.' });
})();

Het nieuwe onderdeel is ToolLoopAgent ; de abstractie van de AI SDK die een meerstaps conversatielus beheert bovenop elk model. Het neemt het model en een systeemprompt als invoer en regelt de heen-en-weercommunicatie intern.

Verbind de agent met useChat.

De useChat hook van het @ai-sdk/react pakket communiceert normaal gesproken met een HTTP-eindpunt. Voor browsergebaseerde inferentie kunt u in plaats daarvan DirectChatTransport gebruiken. Deze voert de ToolLoopAgent loop volledig in de browser uit, zonder tussenkomst van een server.

const transport = useMemo(() => new DirectChatTransport({ agent }), [agent]);
const { messages, sendMessage, status, stop } = useChat({ transport });

useMemo is belangrijk omdat DirectChatTransport de gespreksstatus bewaart, dus het moet een stabiele referentie zijn. Het opnieuw aanmaken ervan bij elke rendering reset het gesprek.

useChat biedt je:

  • Messages : het volledige gesprek als UIMessage[] , elk met een role en een array parts .
  • sendMessage({ text }) : verstuurt een nieuwe gebruikersbeurt en begint met het streamen van de reactie.
  • Status : 'idle' | 'submitted' | 'streaming' | 'error'
  • Stop : annuleert een lopende generatie

Berichten weergeven

Elk bericht bestaat uit een array parts . Voor deze chatbot zijn we alleen geïnteresseerd in onderdelen type: 'text' . Gebruikersberichten verschijnen als een rechts uitgelijnde tekstballon; berichten van de assistent worden links uitgelijnd met een pictogram.

const ChatMessage = ({ message, isStreaming }: { message: UIMessage; isStreaming: boolean }) => {
  const isUser = message.role === 'user';

  const textParts = message.parts.map((part, i) => {
    if (part.type !== 'text') return null;
    if (isUser) return <span key={i}>{part.text}</span>;
    return <Response key={i} parseIncompleteMarkdown={isStreaming}>{part.text}</Response>;
  });

  if (isUser) {
    return (
      <div className="flex flex-col items-end gap-2 animate-fade-up">
        <MessageContent className="w-fit max-w-[min(80%,56ch)] ...">
          {textParts}
        </MessageContent>
      </div>
    );
  }

  return (
    <div className="flex items-start gap-3">
      <AIIcon />
      <MessageContent className="text-[13px] leading-[1.65]">{textParts}</MessageContent>
    </div>
  );
};

MessageContent en Response zijn AI-elementen . Het zijn shadcn -achtige broncomponenten die je naar je project kopieert in plaats van ze via npm te installeren. Response combineert react-markdown met remark-gfm voor GitHub-achtige Markdown (tabellen, takenlijsten, doorhalen) en harden-react-markdown om links en afbeeldingen in de AI-output te saneren.

De eigenschap parseIncompleteMarkdown is true zolang het bericht nog wordt gestreamd. Tijdens het streamen kan het model **bold schrijven, maar de ** nog niet sluiten, waardoor er een losstaand token overblijft dat als letterlijke sterretjes wordt weergegeven. parseIncompleteMarkdown sluit alle openstaande ** , __ , ` , ~~ en verwijdert losse [ link starts, zodat de weergegeven uitvoer bij elke incrementele chunk schoon blijft.

De "Denk..."-toestand

Tussen het versturen van een bericht en het ontvangen van het eerste token is status 'submitted' . Gedurende deze periode toont de app een geanimeerde flikkering:

{status === 'submitted' && messages.at(-1)?.role !== 'assistant' && (
  <ThinkingMessage />
)}

De voorwaarde messages.at(-1)?.role !== 'assistant' voorkomt dat de glinstering opnieuw verschijnt zodra het assistentbericht is begonnen binnen te stromen.

ThinkingMessage gebruikt een Shimmer component: een <span> met een bewegend verloop met behulp van background-clip: text , waardoor de tekst "Thinking…" een vloeiend highlight-effect krijgt.

Automatisch scrollen

Wanneer er nieuwe content binnenkomt, scrollt de app naar beneden, maar alleen als de gebruiker zich al onderaan bevindt. Het zou immers irritant zijn als de gebruiker midden in een gesprek wordt weggescrold van iets wat hij of zij aan het lezen is.

const [isAtBottom, setIsAtBottom] = useState(true);

useEffect(() => {
  if (isAtBottom) endRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, status, isAtBottom]);

const handleScroll = () => {
  const el = containerRef.current;
  if (!el) return;
  setIsAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 50);
};

Er verschijnt een zwevende knop om naar beneden te scrollen wanneer isAtBottom onwaar is, en deze verdwijnt weer wanneer de gebruiker terug is op de onderkant van de pagina.

Het invoergebied

Het tekstveld past zich automatisch aan tijdens het typen door de hoogte bij elke invoergebeurtenis terug te zetten naar auto en deze vervolgens in te stellen op scrollHeight . Het verzendt het bericht met Enter (niet met Shift+Enter) en terwijl een reactie wordt verwerkt, wordt de knop 'Verzenden' vervangen door een knop 'Stop' die de functie ' stop() aanroept.

<textarea
  onInput={(e) => {
    const el = e.currentTarget;
    el.style.height = 'auto';
    el.style.height = `${el.scrollHeight}px`;
  }}
  onKeyDown={(e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      if (input.trim() && !isStreaming) {
        sendMessage({ text: input });
        setInput('');
      }
    }
  }}
/>;
{
  isStreaming ? (
    <Button variant="outline" onClick={stop}>
      Stop
    </Button>
  ) : (
    <Button type="submit" disabled={!input.trim()}>
      Send
    </Button>
  );
}

Monteer in een laadstand

Omdat agentPromise asynchroon is, moet je wachten tot de promise is opgelost voordat je Chat weergeeft. Een App wrapper lost de promise op en toont ondertussen een laadindicator:

function App() {
  const [agent, setAgent] = (useState < ToolLoopAgent) | (null > null);

  useEffect(() => {
    agentPromise.then(setAgent);
  }, []);

  if (!agent) {
    return (
      <div className="flex h-dvh items-center justify-center">
        <Loader size={20} />
      </div>
    );
  }

  return <Chat agent={agent} />;
}

Zodra de agent het probleem heeft opgelost, of dat nu komt doordat het ingebouwde model direct opstart of doordat er gewacht wordt op het downloaden van een model, verdwijnt de laadindicator en wordt de chatinterface geladen.

Demo

De live demo is een volledig functionele chatbot die volledig in uw browser draait. Typ een bericht en druk op Enter . Als de Prompt API beschikbaar is, wordt het antwoord rechtstreeks vanuit het lokale model verzonden zonder netwerkverzoek. Als uw browser de Prompt API niet ondersteunt, wordt automatisch teruggevallen op Gemini 2.5 Flash. Probeer de chatbot eens te vragen iets in een lijst uit te leggen, een codefragment te schrijven of Markdown-opmaak te gebruiken. De antwoorden worden direct weergegeven met opgemaakte codeblokken, tabellen en inline code.

Een chatinterface die een gesprek met een AI-assistent weergeeft, met een tekstinvoerveld en een gebied voor het weergeven van een reactie.

Conclusie

In deze twee artikelen hebt u het volledige scala aan mogelijkheden gezien dat de Vercel AI SDK biedt in combinatie met de ingebouwde Prompt API van de browser, van eenvoudige generatieprimitieven tot een verfijnde streaming chatinterface.

In ' De ingebouwde Prompt API gebruiken met de Vercel AI SDK' leerde u hoe u generateText() en streamText() kunt gebruiken voor het genereren van tekst, zowel niet-streaming als streaming, hoe u gestructureerde JSON-uitvoer kunt opvragen met Output.object() , en hoe u hybride code kunt schrijven die tijdens de uitvoering kiest tussen het ingebouwde model en een cloudprovider zonder de generatielogica te wijzigen.

In dit document heb je diezelfde bouwstenen gebruikt en ze verpakt in een complete React-gebruikersinterface: een ToolLoopAgent om de conversatieloop te beheren, useChat met DirectChatTransport om reacties rechtstreeks in de browser te streamen, en AI Elements-componenten om Markdown-reacties netjes weer te geven zodra ze binnenkomen; dit alles met automatische cloud-fallback wanneer de Prompt API niet beschikbaar is.

Het resultaat zijn twee demo's die volledig in de browser werken, zonder dat er een backend nodig is: