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

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:
- Utilizzare l'API Prompt integrata con l'SDK AI di Vercel: generazione di testo, streaming e output strutturato affiancati
- Demo Utilizzare l'interfaccia utente dell'SDK AI di Vercel e gli elementi AI con l'API Prompt: un chatbot di streaming completo con rendering Markdown e modalità scura