Construindo um Chatbot
Frontend + IA
Interface mínima de chat — campo, envio, loading e exibição de respostas.
Nesta aula você vai
- Montar UI de chat sem biblioteca pesada
- Enviar mensagem via fetch ao backend
- Tratar estados loading, erro e empty
Frontend + IA
Objetivos
- Criar interface mínima que stakeholders entendem
- Nunca chamar API do LLM direto do browser
O que o usuário enxerga
Até agora você trabalhou no terminal e em scripts. O cliente não compra JSON no console — compra uma caixa de conversa no site.
A boa notícia: a UI de chat pode ser simples. Lista de mensagens, campo de texto, botão enviar. O que separa demo de produto são os estados — loading, erro, vazio — que muita gente esquece.
E a regra de ouro: o navegador fala com seu servidor (/api/chat), nunca com api.openai.com. A chave fica onde pertence.
HTML + CSS mínimo
<div id="chat">
<div id="messages" aria-live="polite"></div>
<form id="form">
<input id="input" type="text" placeholder="Digite sua pergunta..." autocomplete="off" />
<button type="submit">Enviar</button>
</form>
</div>
aria-live="polite" ajuda leitores de tela quando novas mensagens aparecem — acessibilidade desde o MVP.
JavaScript (cliente)
const messagesEl = document.getElementById('messages');
const form = document.getElementById('form');
const input = document.getElementById('input');
function append(role, text) {
const div = document.createElement('div');
div.className = `msg msg--${role}`;
div.textContent = text;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
input.value = '';
append('user', text);
append('assistant', '...'); // placeholder loading
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text }),
});
const data = await res.json();
messagesEl.lastChild.textContent = data.reply ?? data.error;
} catch {
messagesEl.lastChild.textContent = 'Erro de conexão. Tente novamente.';
}
});
Fluxo que o usuário sente:
Digita pergunta → vê sua mensagem → vê "..." → recebe resposta (ou erro claro).
Sem o "...", parece que o site travou. Loading é parte da conversa.
Estados obrigatórios
| Estado | UX |
|---|---|
| Idle | Input habilitado |
| Loading | Botão disabled, indicador visual |
| Error | Mensagem clara, input reabilitado |
| Empty | Sugestão: "Pergunte sobre pedidos ou produtos" |
Diálogo de UX:
Stakeholder: "O bot demorou 8 segundos e eu cliquei de novo."
Dev: "Desabilito o botão enquanto carrega e mostro indicador."
Stakeholder: "Agora parece profissional."
Segurança no frontend
- Sanitize exibição (
textContent, nãoinnerHTMLcom resposta da IA) - Limite tamanho do input (ex: 2.000 chars)
- CSRF token se usar cookies de sessão
O modelo pode, em teoria, devolver HTML ou script em resposta maliciosa induzida. textContent trata tudo como texto — sem executar nada.
React/Vue — mesmo princípio
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
async function send(text) {
setMessages(m => [...m, { role: 'user', content: text }]);
setLoading(true);
try {
const { reply } = await fetch('/api/chat', { ... }).then(r => r.json());
setMessages(m => [...m, { role: 'assistant', content: reply }]);
} finally {
setLoading(false);
}
}
Framework muda; arquitetura não: UI → seu backend → LLM.
Resumo
- UI simples: lista + input + submit
- Frontend → seu
/api/chat, não OpenAI - Loading e erro são parte do produto, não detalhe
textContent/ escape para evitar XSS na resposta do modelo
Interface pronta na tela — falta o cérebro no servidor. Na próxima aula montamos o endpoint que valida, chama o LLM e devolve JSON padronizado.