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
useChatdell'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 comeUIMessage[], ognuna con unrolee un arraypartssendMessage({ text }): invia il turno di un nuovo utente e inizia lo streaming della rispostaStatus:'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.

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:
- Utilizza l'API Prompt integrata con la demo dell'SDK Vercel AI: generazione di testo, streaming e output strutturato affiancati
- Utilizza la UI e gli elementi AI dell'SDK Vercel AI con la demo dell'API Prompt: un chatbot di streaming completo con rendering Markdown e modalità Buio