Utilizzare l'interfaccia utente dell'SDK Vercel AI e gli elementi AI con l'API Prompt

Pubblicato il 16 luglio 2026

In Utilizzare l'API Prompt integrata con l'SDK AI di Vercel , hai visto le quattro primitive di generazione principali, ovvero generateText(), streamText(), codice ibrido e output strutturato utilizzando Output.object(), il tutto basato su @browser-ai/core. Questa volta creerai qualcosa di più interattivo: un'interfaccia utente di chat di streaming completa che viene eseguita interamente nel browser, con fallback automatico a un modello cloud quando l'API Prompt non è disponibile.

Cosa stai creando

Un'interfaccia di chat React che:

  • Utilizza l'hook useChat dell'SDK AI di Vercel per lo streaming di conversazioni multi-turn.
  • Esegue il loop del modello nel browser senza richiedere un server di backend.
  • Esegue automaticamente il fallback a Gemini 2.5 Flash quando l'API Prompt non è disponibile.
  • Esegue il rendering delle risposte dell'assistente come Markdown in stile GitHub, gestendo i token incompleti durante lo streaming.
  • Mostra un effetto di luce "Thinking…" durante l'attesa del primo token.
  • Esegue lo scorrimento automatico ai nuovi messaggi, con un pulsante di scorrimento verso il basso quando hai eseguito lo scorrimento verso l'alto.

Dipendenze aggiuntive

Oltre a ai, @browser-ai/core e @ai-sdk/google, l'interfaccia utente della chat richiede React, i binding React dell'SDK AI e alcuni pacchetti Markdown:

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

Per l'interfaccia utente, aggiungi anche Tailwind CSS, alcune utilità dei componenti e Lucide per le icone:

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

Configurazione di Vite: build multi-entry

Il progetto ha già index.html. Aggiungi chat.html come secondo punto di ingresso e configura Vite per creare entrambi:

// 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 è minimale, solo un <div id="root"> e un tag di script:

<!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>

Selezione automatica del modello

Nel chatbot, l'app effettua la scelta automaticamente: prova prima il modello integrato ed esegue il fallback al cloud se l'API Prompt non è disponibile.

Questa operazione viene eseguita al momento del caricamento del modulo, prima che React venga montato, in modo che l'agente sia pronto quando l'utente digita il primo messaggio:

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.' });
})();

Il nuovo elemento è ToolLoopAgent, l'astrazione dell'SDK AI che gestisce un loop di conversazione multi-turn su qualsiasi modello. Accetta il modello e un prompt di sistema e gestisce internamente l'andata e il ritorno.

Collega l'agente a useChat

L'hook useChat del pacchetto @ai-sdk/react comunica normalmente con un endpoint HTTP. Per l'inferenza lato browser, utilizza invece DirectChatTransport. Esegue il loop ToolLoopAgent interamente nel browser senza coinvolgere il server:

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

useMemo è importante perché DirectChatTransport contiene lo stato della conversazione, quindi deve essere un riferimento stabile. La ricreazione a ogni rendering reimposta la conversazione.

useChat ti offre:

  • Messages: la conversazione completa come UIMessage[], ognuna con un role e un array parts
  • sendMessage({ text }): invia un nuovo turno utente e inizia lo streaming della risposta
  • Status: 'idle' | 'submitted' | 'streaming' | 'error'
  • Stop: annulla una generazione in corso

Esegui il rendering dei messaggi

Ogni messaggio ha un array parts. Per questo chatbot, ci interessano solo le parti type: 'text'. I messaggi utente vengono visualizzati come una bolla allineata a destra, mentre i messaggi dell'assistente sono allineati a sinistra con un'icona:

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 e Response sono elementi AI. Sono componenti di origine in stile shadcn che copi nel tuo progetto anziché installarli da npm. Response racchiude react-markdown con remark-gfm per Markdown in stile GitHub (tabelle, elenchi di attività, testo barrato) e harden-react-markdown per sanificare link e immagini nell'output AI.

La proprietà parseIncompleteMarkdown è true mentre il messaggio è ancora in streaming. Durante lo streaming, il modello potrebbe scrivere **bold ma non chiudere ancora il **, lasciando un token sospeso che verrebbe visualizzato come asterischi letterali. parseIncompleteMarkdown chiude tutti i **, __, `, ~~ aperti e tronca gli inizi di link [ sospesi in modo che l'output di rendering rimanga pulito in ogni blocco incrementale.

Lo stato "Thinking…"

Tra l'invio di un messaggio e la ricezione del primo token, status è 'submitted'. Durante questa finestra, l'app mostra un effetto di luce animato:

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

La condizione messages.at(-1)?.role !== 'assistant' impedisce che l'effetto di luce venga visualizzato di nuovo una volta avviato lo streaming del messaggio dell'assistente.

ThinkingMessage utilizza un componente Shimmer: un <span> con una sfumatura in movimento che utilizza background-clip: text che conferisce al testo "Thinking…" un effetto di evidenziazione a scorrimento.

Scorrimento automatico

Quando arrivano nuovi contenuti, l'app scorre verso il basso, ma solo se l'utente è già in fondo. Scorrere i contenuti mentre l'utente sta leggendo qualcosa durante una conversazione sarebbe fastidioso.

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);
};

Quando isAtBottom è false, viene visualizzato un pulsante di scorrimento verso il basso mobile, che scompare quando l'utente torna in fondo.

L'area di input

La dimensione automatica dell'area di testo viene modificata durante la digitazione reimpostando l'altezza su auto a ogni evento di input e poi impostandola su scrollHeight. L'invio avviene con Invio (non Maiusc+Invio) e, durante lo streaming di una risposta, il pulsante Invia viene sostituito da un pulsante Interrompi che chiama stop():

<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>
  );
}

Montaggio con uno stato di caricamento

Poiché agentPromise è asincrono, attendi prima di eseguire il rendering di Chat. Un wrapper App risolve la promessa e mostra un indicatore di caricamento nel frattempo:

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} />;
}

Una volta risolto l'agente, sia che si tratti dell'avvio immediato del modello integrato sia dell'attesa del download di un modello, l'indicatore di caricamento scompare e l'interfaccia utente della chat viene montata.

Demo

La demo dal vivo è un chatbot completamente funzionale che viene eseguito interamente nel browser. Digita un messaggio e premi Invio. Se l'API Prompt è disponibile, la risposta viene trasmessa in streaming direttamente dal modello on-device senza richiesta di rete. Se il browser non supporta l'API Prompt, viene eseguito automaticamente il fallback a Gemini 2.5 Flash. Prova a chiedergli di spiegare qualcosa in un elenco, scrivere uno snippet di codice o utilizzare la formattazione Markdown. Le risposte vengono visualizzate con blocchi di codice formattati, tabelle e codice in linea.

Un'interfaccia di chat che mostra una conversazione con un assistente AI, con un campo di immissione di testo e un'area di risposta in streaming.

Conclusione

Nel corso di questi due articoli hai visto l'intera gamma di possibilità offerte dall'SDK AI di Vercel con l'API Prompt integrata del browser, dalle primitive di generazione non elaborate fino a un'interfaccia di chat di streaming raffinata.

In Utilizzare l'API Prompt integrata con l'SDK AI di Vercel, hai imparato a utilizzare generateText() e streamText() per la generazione di testo non in streaming e in streaming, a richiedere output JSON strutturato con Output.object(), e a scrivere codice ibrido che sceglie tra il modello integrato e un provider cloud in fase di runtime senza modificare la logica di generazione.

In questo documento, hai preso gli stessi blocchi di creazione e li hai racchiusi in un'interfaccia utente React completa: un ToolLoopAgent per gestire il loop di conversazione, useChat con DirectChatTransport per trasmettere le risposte in streaming direttamente nel browser e i componenti AI Elements per eseguire il rendering delle risposte Markdown in modo pulito man mano che arrivano; il tutto con fallback automatico al cloud quando l'API Prompt non è disponibile.

Il risultato sono due demo che funzionano interamente nel browser, senza richiedere un backend: