Usar a interface do SDK de IA da Vercel e os elementos de IA com a API Prompt

Publicado em 16 de julho de 2026

Em Usar a API Prompt integrada com o SDK da Vercel AI, você viu as quatro primitivas de geração principais, ou seja, generateText(), streamText(), código híbrido e saída estruturada usando Output.object(), tudo com tecnologia @browser-ai/core. Desta vez, você cria algo mais interativo: uma interface de chat de streaming completa que é executada totalmente no navegador, com fallback automático para um modelo de nuvem quando a API Prompt não está disponível.

O que você vai criar

Uma interface de chat do React que faz o seguinte:

  • Usa o hook useChat do SDK da Vercel AI para streaming de conversas multiturno.
  • Executa o loop do modelo no navegador sem precisar de um servidor de back-end.
  • Faz o fallback para o Gemini 2.5 Flash automaticamente quando a API Prompt não está disponível.
  • Renderiza respostas do assistente como Markdown com estilo do GitHub, processando tokens incompletos durante o streaming.
  • Mostra um brilho "Pensando…" enquanto aguarda o primeiro token.
  • Faz a rolagem automática para novas mensagens, com um botão de rolagem para baixo quando você rola para cima.

Outras dependências

Além de ai, @browser-ai/core e @ai-sdk/google, a interface de chat precisa do React, das vinculações do React do SDK de IA e de alguns pacotes 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 a interface, adicione também o Tailwind CSS, alguns utilitários de componentes e o Lucide para ícones:

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

Configuração de votação: build de várias entradas

O projeto já tem index.html. Adicione chat.html como um segundo ponto de entrada e configure o Vite para criar os dois:

// 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 é mínimo, apenas um <div id="root"> e uma tag 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>

Seleção automática de modelos

No chatbot, o app faz essa escolha automaticamente: ele tenta o modelo integrado primeiro e faz o fallback para a nuvem se a API Prompt não estiver disponível.

Ele faz isso no tempo de carregamento do módulo, antes da montagem do React, para que o agente esteja pronto quando o usuário digitar a primeira mensagem:

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.' });
})();

A nova parte é ToolLoopAgent, a abstração do SDK de IA que gerencia um loop de conversa multiturno em qualquer modelo. Ele usa o modelo e um comando do sistema e processa a troca de mensagens internamente.

Conectar o agente ao useChat

O hook useChat do pacote @ai-sdk/react normalmente se comunica com um endpoint HTTP. Para inferência do lado do navegador, use DirectChatTransport. Ele executa o loop ToolLoopAgent totalmente no navegador, sem nenhum servidor envolvido:

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

useMemo é importante porque DirectChatTransport mantém o estado da conversa. Portanto, ele precisa ser uma referência estável. Recriá-lo em cada renderização redefine a conversa.

useChat oferece:

  • Messages: a conversa completa como UIMessage[], cada uma com uma matriz role e parts
  • sendMessage({ text }): envia uma nova rodada de usuário e começa a transmitir a resposta
  • Status: 'idle' | 'submitted' | 'streaming' | 'error'
  • Stop: cancela uma geração em andamento

Renderizar mensagens

Cada mensagem tem uma matriz parts. Para esse chatbot, só nos interessam as partes type: 'text'. As mensagens do usuário aparecem como uma bolha alinhada à direita. As mensagens do assistente são alinhadas à esquerda com um ícone:

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 são elementos de IA. Eles são componentes de origem no estilo shadcn que você copia para o projeto em vez de instalar do npm. Response envolve react-markdown com remark-gfm para Markdown com estilo do GitHub (tabelas, listas de tarefas, tachado) e harden-react-markdown para higienizar links e imagens na saída de IA.

A propriedade parseIncompleteMarkdown é true enquanto a mensagem ainda está sendo transmitida. Durante o streaming, o modelo pode gravar **bold sem ainda fechar **, deixando um token pendurado que seria renderizado como asteriscos literais. parseIncompleteMarkdown fecha qualquer **, __, `, ~~ aberto e trunca os inícios de links [ pendurados para que a saída renderizada permaneça limpa em cada bloco incremental.

O estado "Pensando…"

Entre o envio de uma mensagem e o recebimento do primeiro token, status é 'submitted'. Durante essa janela, o app mostra um brilho animado:

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

A condição messages.at(-1)?.role !== 'assistant' impede que o brilho reapareça depois que a mensagem do assistente começar a ser transmitida.

ThinkingMessage usa um componente Shimmer: um <span> com um gradiente móvel usando background-clip: text que dá ao texto "Pensando…" um efeito de destaque abrangente.

Rolagem automática

Quando um novo conteúdo chega, o app rola para baixo, mas apenas se o usuário já estiver na parte de baixo. Rolar a tela para longe de algo que eles estão lendo no meio da conversa seria irritante.

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

Um botão flutuante de rolagem para baixo aparece quando isAtBottom é falso e desaparece quando o usuário volta para a parte de baixo.

A área de entrada

A caixa de texto é redimensionada automaticamente à medida que você digita, redefinindo a altura para auto em cada evento de entrada e definindo-a como scrollHeight. Ela é enviada ao pressionar Enter (não Shift + Enter) e, enquanto uma resposta está sendo transmitida, o botão "Enviar" é substituído por um botão "Parar" que chama 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>
  );
}

Montar com um estado de carregamento

Como agentPromise é assíncrono, aguarde antes de renderizar Chat. Um wrapper App resolve a promessa e mostra um indicador de carregamento enquanto isso:

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

Quando o agente é resolvido, seja o modelo integrado inicializando instantaneamente ou aguardando o download de um modelo, o indicador de carregamento desaparece e a interface de chat é montada.

Demonstração

A demonstração ao vivo é um chatbot totalmente funcional executado no navegador. Digite uma mensagem e pressione Enter. Se a API Prompt estiver disponível, a resposta será transmitida diretamente do modelo no dispositivo, sem solicitação de rede. Se o navegador não oferecer suporte à API Prompt, ele fará o fallback para o Gemini 2.5 Flash automaticamente. Peça para explicar algo em uma lista, escrever um snippet de código ou usar a formatação Markdown. As respostas são renderizadas com blocos de código formatados, tabelas e código inline.

Uma interface de chat mostrando uma conversa com um assistente de IA, com um campo de entrada de texto e uma área de resposta de streaming.

Conclusão

Ao longo destes dois artigos, você viu toda a variedade do que o SDK da Vercel AI torna possível com a API Prompt integrada do navegador, desde primitivas de geração bruta até uma interface de chat de streaming refinada.

Em Usar a API Prompt integrada com o SDK da Vercel AI, você aprendeu a usar generateText() e streamText() para geração de texto não transmitido e transmitido, como solicitar saída JSON estruturada com Output.object(), e como escrever código híbrido que escolhe entre o modelo integrado e um provedor de nuvem no momento da execução, sem alterações na lógica de geração.

Neste documento, você usou esses mesmos blocos de construção e os envolveu em uma interface do React completa: um ToolLoopAgent para gerenciar o loop de conversa, useChat com DirectChatTransport para transmitir respostas diretamente no navegador e componentes de elementos de IA para renderizar respostas Markdown de maneira limpa à medida que chegam. Tudo com fallback automático da nuvem quando a API Prompt não está disponível.

O resultado são duas demonstrações que funcionam totalmente no navegador, sem necessidade de back-end: