Utiliser l'UI et les éléments d'IA du SDK Vercel AI avec l'API Prompt

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 useChat du 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 de UIMessage[], chacune avec un role et un tableau parts
  • sendMessage({ text }): envoie un nouveau tour d'utilisateur et commence à diffuser la réponse
  • Status: '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.

Interface de chat montrant une conversation avec un assistant IA, avec un champ de saisie de texte et une zone de réponse en streaming.

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 :