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

Data di pubblicazione: 16 luglio 2026

In Utilizzare l'API Prompt integrata con l'SDK Vercel AI, hai visto i quattro primitivi di generazione principali, ovvero generateText(), streamText(), codice ibrido e output strutturato utilizzando Output.object(), tutti basati su @browser-ai/core. Questa volta creerai qualcosa di più interattivo: un'interfaccia utente di chat in streaming completa che viene eseguita interamente nel browser, con il 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 Vercel AI per lo streaming di conversazioni multi-turno.
  • Esegue il ciclo 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 con sapore di GitHub, gestendo i token incompleti durante lo streaming.
  • Mostra un effetto luccicante "Elaborazione in corso…" durante l'attesa del primo token.
  • Scorre automaticamente fino ai nuovi messaggi, con un pulsante per scorrere fino in fondo quando hai scorso verso l'alto.

Dipendenze aggiuntive

Oltre a ai, @browser-ai/core e @ai-sdk/google, la UI 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 la UI, 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 del voto: creazione di più voci

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 esegue automaticamente questa scelta: prova prima il modello integrato e torna al cloud se l'API Prompt non è disponibile.

Lo fa al momento del caricamento del modulo, prima che React venga montato, quindi l'agente è 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 componente è ToolLoopAgent, l'astrazione dell'SDK AI che gestisce un ciclo di conversazione multi-turno su qualsiasi modello. Prende il modello e un prompt di sistema e gestisce internamente lo scambio di informazioni.

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 ciclo ToolLoopAgent interamente nel browser senza coinvolgere alcun 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: l'intera conversazione come UIMessage[], ognuna con un role e un array parts
  • sendMessage({ text }): invia il turno di un nuovo utente e inizia lo streaming della risposta
  • Status: 'idle' | 'submitted' | 'streaming' | 'error'
  • Stop: annulla una generazione in corso

Rendering dei messaggi

Ogni messaggio ha un array parts. Per questo chatbot, ci interessano solo le parti type: 'text'. I messaggi dell'utente vengono visualizzati come fumetto allineato a destra; 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 con sapore di GitHub (tabelle, elenchi di attività, barrato) e harden-react-markdown per sanificare link e immagini nell'output dell'AI.

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

Stato "Elaborazione in corso…"

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

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

La condizione messages.at(-1)?.role !== 'assistant' impedisce la ricomparsa del tremolio una volta iniziato lo streaming del messaggio dell'assistente.

ThinkingMessage utilizza un componente Shimmer: un <span> con una sfumatura in movimento che utilizza background-clip: text per dare al testo "Elaborazione in corso…" un effetto di evidenziazione dinamico.

Scorrimento automatico

Quando arrivano nuovi contenuti, l'app scorre fino in fondo, ma solo se l'utente si trova già in fondo. Far scorrere via qualcosa che stanno leggendo nel bel mezzo di 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);
};

Un pulsante mobile per scorrere verso il basso viene visualizzato quando isAtBottom è false e scompare quando l'utente torna in fondo alla pagina.

L'area di input

L'area di testo viene ridimensionata automaticamente durante la digitazione reimpostando l'altezza su auto a ogni evento di input e poi impostandola su scrollHeight. Viene inviato premendo 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 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 il problema dell'agente, che si tratti dell'avvio immediato del modello integrato o dell'attesa del download di un modello, lo spinner scompare e viene visualizzata la UI della chat.

Demo

La demo dal vivo è un chatbot completamente funzionante 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, tabelle e codice in linea formattati.

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 Vercel AI con l'API Prompt integrata del browser, dai primitivi di generazione non elaborati fino a un'interfaccia di chat in streaming raffinata.

In Utilizzare l'API Prompt integrata con l'SDK Vercel AI, hai imparato a utilizzare generateText() e streamText() per la generazione di testo non in streaming e in streaming, a richiedere l'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 modifiche alla logica di generazione.

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

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