Publié le 16 juillet 2026
Dans Utiliser l'API Prompt intégrée avec le SDK Vercel AI, vous avez découvert les quatre primitives de génération de base, à savoir generateText(), streamText(), le code hybride et la sortie structurée à l'aide de
Output.object(), le tout alimenté par @browser-ai/core. Cette fois, vous allez créer quelque chose de plus interactif : une interface utilisateur de chat en streaming complète qui s'exécute entièrement dans le navigateur, avec un basculement automatique vers un modèle cloud lorsque l'API Prompt n'est pas disponible.
Ce que vous allez créer
Une interface de chat React qui effectue les opérations suivantes :
- Utilise le hook
useChatdu SDK Vercel AI pour le streaming de conversations multitours. - Exécute la boucle de modèle dans le navigateur sans serveur backend requis.
- Bascule automatiquement vers Gemini 2.5 Flash lorsque l'API Prompt n'est pas disponible.
- Affiche les réponses de l'assistant au format Markdown GitHub, en gérant les jetons incomplets lors du streaming.
- Affiche un effet de scintillement "Réflexion…" en attendant le premier jeton.
- Fait défiler automatiquement les nouveaux messages, avec un bouton de défilement vers le bas lorsque vous avez fait défiler la page vers le haut.
Autres dépendances
En plus de ai, @browser-ai/core et @ai-sdk/google, l'interface utilisateur de chat a besoin de
React, des liaisons React du SDK AI et de quelques packages 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
Pour l'interface utilisateur, ajoutez également Tailwind CSS, quelques utilitaires de composants et Lucide pour les icônes :
npm install -D tailwindcss postcss autoprefixer
npm install clsx tailwind-merge lucide-react
Configuration de vote : compilation multi-entrée
Le projet comporte déjà index.html. Ajoutez chat.html comme deuxième point d'entrée et configurez Vite pour compiler les deux :
// 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 est minimal, juste un <div id="root"> et une balise de 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>
Sélection automatique du modèle
Dans le chatbot, l'application fait ce choix automatiquement : elle essaie d'abord le modèle intégré et bascule vers le cloud si l'API Prompt n'est pas disponible.
Elle effectue cette opération au moment du chargement du module, avant le montage de React, de sorte que l'agent est prêt lorsque l'utilisateur saisit son premier message :
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.' });
})();
La nouvelle pièce est ToolLoopAgent, l'abstraction du SDK IA qui gère une boucle de conversation multitour au-dessus de n'importe quel modèle. Il prend le modèle et un prompt système, et gère les allers-retours en interne.
Connecter l'agent à useChat
Le hook useChat du package @ai-sdk/react communique normalement avec un point de terminaison HTTP.
Pour l'inférence côté navigateur, utilisez plutôt DirectChatTransport. Il exécute la boucle ToolLoopAgent entièrement dans le navigateur sans aucun serveur impliqué :
const transport = useMemo(() => new DirectChatTransport({ agent }), [agent]);
const { messages, sendMessage, status, stop } = useChat({ transport });
useMemo est important, car DirectChatTransport contient l'état de la conversation. Il doit donc s'agir d'une référence stable. Le recréer à chaque rendu réinitialise la conversation.
useChat vous donne :
Messages: la conversation complète sous forme deUIMessage[], chacune avec unroleet un tableaupartssendMessage({ text }): envoie un nouveau tour d'utilisateur et commence à diffuser la réponseStatus:'idle'|'submitted'|'streaming'|'error'Stop: annule une génération en cours
Afficher les messages
Chaque message comporte un tableau parts. Pour ce chatbot, nous ne nous intéressons qu'aux parties type:
'text'. Les messages utilisateur apparaissent sous forme de bulle alignée à droite. Les messages de l'assistant sont alignés à gauche avec une icône :
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 et Response sont des éléments d'IA.
Il s'agit de composants sources de style shadcn que vous copiez dans
votre projet plutôt que de les installer à partir de npm. Response encapsule react-markdown
avec remark-gfm pour le Markdown GitHub (tableaux, listes de tâches,
texte barré) et harden-react-markdown pour assainir les liens et les images dans la sortie de l'IA.
La propriété parseIncompleteMarkdown est true pendant que le message est toujours en streaming. Pendant le streaming, le modèle peut écrire **bold sans
encore fermer le **, ce qui laisse un jeton en suspens qui s'afficherait sous forme d'astérisques littéraux. parseIncompleteMarkdown ferme tous les **, __, `, ~~ ouverts et
tronque les débuts de liens [ en suspens afin que la sortie affichée reste propre sur chaque
bloc incrémental.
L'état "Réflexion…"
Entre l'envoi d'un message et la réception du premier jeton, status est
'submitted'. Pendant cette période, l'application affiche un scintillement animé :
{status === 'submitted' && messages.at(-1)?.role !== 'assistant' && (
<ThinkingMessage />
)}
La condition messages.at(-1)?.role !== 'assistant' empêche le scintillement de
réapparaître une fois que le message de l'assistant a commencé à être diffusé.
ThinkingMessage utilise un composant Shimmer : un <span> avec un dégradé en mouvement
utilisant background-clip: text qui donne au texte "Réflexion…" un effet de surbrillance
balayant.
Défilement automatique
Lorsque du nouveau contenu arrive, l'application fait défiler la page vers le bas, mais uniquement si l'utilisateur se trouve déjà en bas. Il serait ennuyeux de les faire défiler loin de ce qu'ils lisent au milieu d'une conversation.
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 bouton flottant de défilement vers le bas s'affiche lorsque isAtBottom est faux et disparaît lorsque l'utilisateur est de retour en bas.
La zone de saisie
La zone de texte est redimensionnée automatiquement lorsque vous tapez en réinitialisant sa hauteur sur auto à chaque événement d'entrée, puis en la définissant sur scrollHeight. Elle est envoyée sur Entrée (pas Maj+Entrée). Pendant la diffusion d'une réponse, le bouton "Envoyer" est remplacé par un bouton "Arrêter" qui appelle 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>
);
}
Montage avec un état de chargement
Étant donné que agentPromise est asynchrone, attendez-le avant d'afficher Chat. Un wrapper App résout la promesse et affiche un spinner en attendant :
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} />;
}
Une fois l'agent résolu, qu'il s'agisse du modèle intégré qui démarre instantanément ou de l'attente du téléchargement d'un modèle, le spinner disparaît et l'interface utilisateur de chat se monte.
Démo
La démo en direct est un chatbot entièrement fonctionnel qui s'exécute entièrement dans votre navigateur. Saisissez un message et appuyez sur Entrée. Si l'API Prompt est disponible, la réponse est diffusée directement à partir du modèle sur l'appareil sans aucune requête réseau. Si votre navigateur n'est pas compatible avec l'API Prompt, il bascule automatiquement vers Gemini 2.5 Flash. Essayez de lui demander d'expliquer quelque chose dans une liste, d'écrire un extrait de code ou d'utiliser la mise en forme Markdown. Les réponses sont affichées avec des blocs de code, des tableaux et du code en ligne mis en forme.

Conclusion
Au cours de ces deux articles, vous avez découvert toute la gamme de possibilités offertes par le SDK Vercel AI avec l'API Prompt intégrée du navigateur, depuis les primitives de génération brutes jusqu'à une interface de chat en streaming soignée.
Dans Utiliser l'API Prompt intégrée avec le SDK Vercel AI, vous avez appris à
utiliser generateText() et streamText() pour la génération de texte en streaming et non en streaming, à demander une sortie JSON structurée avec Output.object(), et
à écrire du code hybride qui choisit entre le modèle intégré et un fournisseur cloud
au moment de l'exécution sans modifier la logique de génération.
Dans ce document, vous avez utilisé ces mêmes blocs de construction et les avez encapsulés dans une interface utilisateur React complète : un ToolLoopAgent pour gérer la boucle de conversation, useChat avec DirectChatTransport pour diffuser les réponses directement dans le navigateur et des composants d'éléments d'IA pour afficher les réponses Markdown de manière propre à leur arrivée. Le tout avec un basculement automatique vers le cloud lorsque l'API Prompt n'est pas disponible.
Le résultat est deux démos qui fonctionnent entièrement dans le navigateur, sans aucun backend requis :
- Utiliser la démo de l'API Prompt intégrée avec le SDK Vercel AI : génération de texte, streaming et sortie structurée côte à côte
- Utiliser l'interface utilisateur du SDK Vercel AI et les éléments d'IA avec la démo de l'API Prompt : un chatbot en streaming complet avec rendu Markdown et mode sombre