/* ============================================================ ChatWidget.jsx — ARIA · Assistente AR Fluxo: captura de lead → confirmação → conversa com agente conectado ao n8n via webhook. Suporta anexos e áudio. ============================================================ */ /* ⚠️ SUBSTITUA pela sua URL real de webhook do n8n. Exemplo de produção: https://n8n.suaempresa.com.br/webhook/agente-ar O site envia um POST JSON para esta URL com o lead e cada mensagem, e usa a resposta (campo "reply" / "output" / "message") como fala do agente. */ const AR_WEBHOOK_URL = "https://SEU-N8N.exemplo.com/webhook/agente-ar"; async function arPostWebhook(payload) { try { const res = await fetch(AR_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error("status " + res.status); const data = await res.json().catch(() => ({})); return data.reply || data.output || data.message || data.text || null; } catch (e) { return null; // webhook fictício / offline → usa resposta de fallback } } const ChatWidget = () => { const [open, setOpen] = React.useState(false); const [stage, setStage] = React.useState("form"); // 'form' | 'chat' const [lead, setLead] = React.useState({ nome: "", empresa: "", whatsapp: "", email: "" }); const [errors, setErrors] = React.useState({}); const [msgs, setMsgs] = React.useState([]); const [draft, setDraft] = React.useState(""); const [typing, setTyping] = React.useState(false); // audio const [recording, setRecording] = React.useState(false); const [recSecs, setRecSecs] = React.useState(0); const recRef = React.useRef(null); const chunksRef = React.useRef([]); const recTimer = React.useRef(null); const feedRef = React.useRef(null); const fileRef = React.useRef(null); React.useEffect(() => { if (feedRef.current) feedRef.current.scrollTop = feedRef.current.scrollHeight; }, [msgs, typing, open, stage]); React.useEffect(() => { window.lucide && window.lucide.createIcons(); }); const fallbackReplies = [ "Perfeito. Posso qualificar seus leads, responder dúvidas e encaminhar oportunidades para sua equipe.", "Conseguimos integrar com WhatsApp, Instagram, site, CRM, Telegram e outros sistemas.", "Posso te mostrar como funciona uma automação aplicada ao seu caso. Qual canal você mais usa hoje?", "Ótimo! Vou registrar isso e um especialista da AR dá sequência com você.", ]; const replyIdx = React.useRef(0); const pushAgent = async (userText) => { setTyping(true); const reply = await arPostWebhook({ tipo: "mensagem", lead, mensagem: userText, historico: msgs }); const wait = reply ? 200 : 900; // simula latência quando offline setTimeout(() => { setTyping(false); const text = reply || fallbackReplies[replyIdx.current++ % fallbackReplies.length]; setMsgs(m => [...m, { from: "agent", type: "text", text }]); }, wait); }; const sendText = (text) => { const t = (text ?? draft).trim(); if (!t) return; setMsgs(m => [...m, { from: "user", type: "text", text: t }]); setDraft(""); pushAgent(t); }; const validate = () => { const e = {}; if (!lead.nome.trim()) e.nome = true; if (!lead.whatsapp.trim()) e.whatsapp = true; if (!lead.email.trim() || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(lead.email)) e.email = true; setErrors(e); return Object.keys(e).length === 0; }; const submitForm = (ev) => { ev.preventDefault(); if (!validate()) return; arPostWebhook({ tipo: "lead", lead }); // registra o lead no n8n setStage("chat"); const nome = lead.nome.split(" ")[0]; setMsgs([ { from: "system", type: "text", text: `Dados recebidos: ${lead.nome}${lead.empresa ? " · " + lead.empresa : ""} · ${lead.whatsapp} · ${lead.email}` }, { from: "agent", type: "text", text: `Prazer, ${nome}! Recebi seus dados e já estamos conectados. Sou a ARIA, agente de IA da Agência AR. Como posso ajudar a sua empresa hoje?` }, ]); }; const onFile = (ev) => { const file = ev.target.files && ev.target.files[0]; if (!file) return; const isImg = file.type.startsWith("image/"); const url = isImg ? URL.createObjectURL(file) : null; setMsgs(m => [...m, { from: "user", type: "file", fileName: file.name, isImg, url, size: file.size }]); ev.target.value = ""; arPostWebhook({ tipo: "anexo", lead, arquivo: file.name }); pushAgent("[anexo] " + file.name); }; const startRec = async () => { if (!navigator.mediaDevices || !window.MediaRecorder) { setMsgs(m => [...m, { from: "system", type: "text", text: "Gravação de áudio não suportada neste navegador." }]); return; } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mr = new MediaRecorder(stream); chunksRef.current = []; mr.ondataavailable = e => e.data.size && chunksRef.current.push(e.data); mr.onstop = () => { const blob = new Blob(chunksRef.current, { type: "audio/webm" }); const url = URL.createObjectURL(blob); setMsgs(m => [...m, { from: "user", type: "audio", url, secs: recSecs }]); stream.getTracks().forEach(t => t.stop()); arPostWebhook({ tipo: "audio", lead }); pushAgent("[áudio]"); }; mr.start(); recRef.current = mr; setRecording(true); setRecSecs(0); recTimer.current = setInterval(() => setRecSecs(s => s + 1), 1000); } catch (e) { setMsgs(m => [...m, { from: "system", type: "text", text: "Permissão de microfone negada." }]); } }; const stopRec = (save = true) => { clearInterval(recTimer.current); setRecording(false); if (recRef.current) { if (!save) recRef.current.onstop = () => recRef.current.stream && null; try { recRef.current.stop(); } catch (e) {} } }; const downloadTranscript = () => { const lines = [ "Conversa — Agência AR (ARIA)", "Data: " + new Date().toLocaleString("pt-BR"), "----------------------------------------", `Nome: ${lead.nome || "-"}`, `Empresa: ${lead.empresa || "-"}`, `WhatsApp: ${lead.whatsapp || "-"}`, `E-mail: ${lead.email || "-"}`, "----------------------------------------", "", ]; msgs.forEach(m => { const who = m.from === "agent" ? "ARIA" : m.from === "user" ? (lead.nome || "Você") : "Sistema"; let body = m.text; if (m.type === "file") body = "[anexo] " + m.fileName; if (m.type === "audio") body = "[áudio enviado]"; lines.push(`${who}: ${body}`); }); const blob = new Blob([lines.join("\n")], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "conversa-agencia-ar.txt"; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); }; const fmt = (s) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; const HeaderBtn = ({ icon, onClick, title }) => ( ); return ( {!open && ( )} {open && (
{/* Header */}
AR
ARIA · Assistente AR
online para ajudar sua empresa
{stage === "chat" && } setOpen(false)} title="Fechar chat" />
{/* LEAD FORM */} {stage === "form" && (
Antes de começar, deixe seus dados. Assim a ARIA já te atende com contexto e nossa equipe consegue dar sequência.
setLead({ ...lead, nome: v })} placeholder="Seu nome" /> setLead({ ...lead, empresa: v })} placeholder="Nome da empresa (opcional)" /> setLead({ ...lead, whatsapp: v })} placeholder="(00) 00000-0000" /> setLead({ ...lead, email: v })} placeholder="voce@empresa.com.br" type="email" />
Seus dados são usados apenas para o atendimento.
)} {/* CHAT */} {stage === "chat" && (
{msgs.map((m, i) => )} {typing && (
{[0, 1, 2].map(d => )}
)}
{/* Input bar */} {recording ? (
gravando · {fmt(recSecs)}
) : (
{ e.preventDefault(); sendText(); }} style={{ padding: 11, borderTop: "1px solid rgba(255,255,255,0.06)", display: "flex", gap: 7, alignItems: "center" }}> setDraft(e.target.value)} placeholder="Escreva sua mensagem…" style={{ flex: 1, minWidth: 0, background: "var(--ar-bg-card)", border: "1px solid rgba(255,255,255,0.10)", borderRadius: 12, padding: "11px 14px", fontFamily: "var(--ar-font-body)", fontSize: 14, color: "var(--ar-text-main)", outline: "none", }} /> {draft.trim() ? ( ) : ( )}
)}
)}
)}
); }; const iconBtn = (bg, color) => ({ width: 42, height: 42, borderRadius: 12, border: "none", cursor: "pointer", flexShrink: 0, background: bg, color, display: "flex", alignItems: "center", justifyContent: "center", }); const ChatField = ({ label, value, onChange, placeholder, err, type = "text" }) => { const [focus, setFocus] = React.useState(false); return ( ); }; const ChatMessage = ({ m }) => { if (m.from === "system") { return (
{m.text}
); } const agent = m.from === "agent"; const base = { alignSelf: agent ? "flex-start" : "flex-end", maxWidth: "84%", background: agent ? "var(--ar-bg-card)" : "linear-gradient(135deg,#005EFF,#00D4FF)", border: agent ? "1px solid rgba(255,255,255,0.06)" : "none", color: agent ? "var(--ar-text-main)" : "#fff", borderRadius: agent ? "14px 14px 14px 4px" : "14px 14px 4px 14px", padding: "10px 13px", fontFamily: "var(--ar-font-body)", fontSize: 13.5, lineHeight: 1.5, boxShadow: agent ? "none" : "0 4px 16px rgba(0,94,255,0.3)", }; if (m.type === "file") { return (
{m.isImg && m.url ? {m.fileName} : null} {m.fileName}
); } if (m.type === "audio") { return (
); } return
{m.text}
; }; window.ChatWidget = ChatWidget;