Construindo um Chatbot

Frontend + IA

Interface mínima de chat — campo, envio, loading e exibição de respostas.

Intermediário 25 min 25 pontos Leitura 0%

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ão innerHTML com 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.