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
useChatdo 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 comoUIMessage[], cada uma com uma matrizroleepartssendMessage({ text }): envia uma nova rodada de usuário e começa a transmitir a respostaStatus:'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.

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:
- Usar a API Prompt integrada com a demonstração do SDK da Vercel AI: geração de texto, streaming e saída estruturada lado a lado
- Usar a interface do SDK da Vercel AI e os elementos de IA com a demonstração da API Prompt: um chatbot de streaming completo com renderização Markdown e modo escuro