Usa la IU del SDK de Vercel AI y los elementos de IA con la API de Prompt

Fecha de publicación: 16 de julio de 2026

En Usa la API de Prompt integrada con el SDK de IA de Vercel, viste las cuatro primitivas de generación principales, es decir, generateText(), streamText(), el código híbrido y el resultado estructurado con Output.object(), todo potenciado por @browser-ai/core. Esta vez, compilarás algo más interactivo: una IU de chat de transmisión completa que se ejecuta por completo en el navegador, con una conmutación por error automática a un modelo en la nube cuando la API de Prompt no está disponible.

Qué compilarás

Una interfaz de chat de React que hace lo siguiente:

  • Usa el hook useChat del SDK de IA de Vercel para transmitir conversaciones de varios turnos.
  • Ejecuta el bucle del modelo en el navegador sin necesidad de un servidor de backend.
  • Vuelve automáticamente a Gemini 2.5 Flash cuando la API de Prompt no está disponible.
  • Renderiza las respuestas del asistente como Markdown con formato de GitHub y controla los tokens incompletos durante la transmisión.
  • Muestra un brillo de "Pensando…" mientras espera el primer token.
  • Se desplaza automáticamente a los mensajes nuevos, con un botón de desplazamiento hacia abajo cuando te desplazaste hacia arriba.

Dependencias adicionales

Junto con ai, @browser-ai/core y @ai-sdk/google, la IU de chat necesita React, las vinculaciones de React del SDK de IA y algunos paquetes de 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

Para la IU, también agrega Tailwind CSS, algunas utilidades de componentes y Lucide para los íconos:

npm install -D tailwindcss postcss autoprefixer
npm install clsx tailwind-merge lucide-react

Configuración de la votación: compilación de varias entradas

El proyecto ya tiene index.html. Agrega chat.html como segundo punto de entrada y configura Vite para compilar ambos:

// 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 es mínimo, solo un <div id="root"> y una etiqueta de secuencia de comandos:

<!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>

Selección automática del modelo

En el chatbot, la app toma esa decisión automáticamente: primero prueba el modelo integrado y vuelve a la nube si la API de Prompt no está disponible.

Esto se hace en el tiempo de carga del módulo, antes de que se active React, por lo que el agente está listo para cuando el usuario escribe su primer mensaje:

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 nueva pieza es ToolLoopAgent; la abstracción del SDK de IA que administra un bucle de conversación de varios turnos sobre cualquier modelo. Toma el modelo y una instrucción del sistema, y controla el intercambio de información de forma interna.

Conecta el agente a useChat

El hook useChat del paquete @ai-sdk/react suele comunicarse con un extremo HTTP. Para la inferencia del lado del navegador, usa DirectChatTransport en su lugar. Ejecuta el bucle ToolLoopAgent por completo en el navegador sin que participe ningún servidor:

const transport = useMemo(() => new DirectChatTransport({ agent }), [agent]);
const { messages, sendMessage, status, stop } = useChat({ transport });

useMemo es importante, ya que DirectChatTransport contiene el estado de la conversación, por lo que debe ser una referencia estable. Si se vuelve a crear en cada renderización, se restablece la conversación.

useChat te brinda lo siguiente:

  • Messages: La conversación completa como UIMessage[], cada una con un role y un array parts
  • sendMessage({ text }): Envía un nuevo turno de usuario y comienza a transmitir la respuesta.
  • Status: 'idle' | 'submitted' | 'streaming' | 'error'
  • Stop: Cancela una generación en curso.

Renderiza mensajes

Cada mensaje tiene un array parts. Para este chatbot, solo nos interesan las partes type: 'text'. Los mensajes del usuario aparecen como una burbuja alineada a la derecha; los mensajes del asistente están alineados a la izquierda con un ícono:

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 y Response son elementos de IA. Son componentes de origen de estilo shadcn que copias en tu proyecto en lugar de instalarlos desde npm. Response incluye react-markdown con remark-gfm para Markdown con formato de GitHub (tablas, listas de tareas, texto tachado) y harden-react-markdown para desinfectar vínculos e imágenes en el resultado de la IA.

La propiedad parseIncompleteMarkdown es true mientras el mensaje aún se está transmitiendo. Durante la transmisión, el modelo puede escribir **bold pero aún no cerrar el **, lo que deja un token colgante que se renderizaría como asteriscos literales. parseIncompleteMarkdown cierra cualquier **, __, `, ~~ abierto y trunca los inicios de vínculos [ colgantes para que el resultado renderizado se mantenga limpio en cada fragmento incremental.

El estado "Pensando…"

Entre el envío de un mensaje y la recepción del primer token, status es 'submitted'. Durante este período, la app muestra un brillo animado:

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

La condición messages.at(-1)?.role !== 'assistant' evita que el brillo vuelva a aparecer una vez que el mensaje del asistente comienza a transmitirse.

ThinkingMessage usa un componente Shimmer: un <span> con un gradiente en movimiento que usa background-clip: text que le da al texto "Pensando…" un efecto de resaltado amplio.

Desplazamiento automático

Cuando llega contenido nuevo, la app se desplaza hacia abajo, pero solo si el usuario ya está en la parte inferior. Desplazarlos lejos de algo que están leyendo en medio de la conversación sería molesto.

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);
};

Aparece un botón flotante de desplazamiento hacia abajo cuando isAtBottom es falso y se desvanece cuando el usuario vuelve a la parte inferior.

El área de entrada

El área de texto cambia de tamaño automáticamente a medida que escribes restableciendo su altura a auto en cada evento de entrada y, luego, configurándola en scrollHeight. Se envía con Intro (no Mayúsculas + Intro) y, mientras se transmite una respuesta, el botón Enviar se reemplaza por un botón Detener que llama a 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>
  );
}

Activa con un estado de carga

Como agentPromise es asíncrono, espera a que se renderice Chat. Un wrapper App resuelve la promesa y muestra un indicador mientras tanto:

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 vez que se resuelve el agente, ya sea que el modelo integrado se inicie de inmediato o espere a que se descargue un modelo, el indicador desaparece y se activa la IU de chat.

Demostración

La demostración en vivo es un chatbot completamente funcional que se ejecuta por completo en tu navegador. Escribe un mensaje y presiona Intro. Si la API de Prompt está disponible, la respuesta se transmite directamente desde el modelo integrado en el dispositivo sin solicitud de red. Si tu navegador no admite la API de Prompt, vuelve automáticamente a Gemini 2.5 Flash. Intenta pedirle que explique algo en una lista, escriba un fragmento de código o use el formato Markdown. Las respuestas se renderizan con bloques de código, tablas y código intercalado con formato listos para usar.

Una interfaz de chat que muestra una conversación con un asistente de IA, con un campo de entrada de texto y un área de respuesta de transmisión.

Conclusión

A lo largo de estos dos artículos, viste el rango completo de lo que el SDK de IA de Vercel hace posible con la API de Prompt integrada del navegador, desde primitivas de generación sin procesar hasta una interfaz de chat de transmisión pulida.

En Usa la API de Prompt integrada con el SDK de IA de Vercel, aprendiste a usar generateText() y streamText() para la generación de texto sin transmisión y con transmisión, a solicitar un resultado JSON estructurado con Output.object(), y a escribir código híbrido que elige entre el modelo integrado y un proveedor de la nube en el tiempo de ejecución sin cambios en la lógica de generación.

En este documento, tomaste esos mismos componentes básicos y los uniste en una IU de React completa: un ToolLoopAgent para administrar el bucle de conversación, useChat con DirectChatTransport para mostrar respuestas en tiempo real directamente en el navegador y componentes de elementos de IA para renderizar respuestas de Markdown de forma limpia a medida que llegan; todo con resguardo automático a la nube cuando la API de Prompt no está disponible.

El resultado son dos demostraciones que funcionan por completo en el navegador, sin necesidad de backend: