🤖 IA pausada nesta conversa — atendimento com {a.por || "a equipe"}
) : null;
})()}
{/* POPOVER CRM: funções do funil sem sair do WhatsApp — abre pelo
botão "CRM" do cabeçalho. Mover de etapa, ficha, agendar, tarefa. */}
{crmAberto && (() => {
const lead = leadDe(conv);
if (!lead) return null;
return (
Modo supervisão — somente {caixaDona ? caixaDona.nome.split(" ")[0] : "o dono da caixa"} responde por este número.
)}
>
)}
{/* DESCONECTAR O NÚMERO — confirmação (o motor faz o logout em ~1 min) */}
{confirmaDesconexao && (
setConfirmaDesconexao(false)}
footer={<> setConfirmaDesconexao(false)}>Cancelar {
setConfirmaDesconexao(false);
DB.waCaixaSet(teamId, caixaId, { status: "desconectar", solicitadoEm: new Date().toISOString() });
dispatch({ type: "TOAST", msg: "Desconectando o número… (leva até 1 minuto)" });
}}>Desconectar>}>
Desconectar o número {caixa.numero ? +{caixa.numero} : "desta caixa"}{caixaDona ? <> (caixa de {caixaDona.nome})> : ""}?
As conversas ficam guardadas no sistema; o número para de receber/enviar por aqui até alguém
conectar de novo pelo QR. O agente de IA e o follow-up desta caixa também param.
)}
{/* APAGAR CONVERSA — confirmação (remove as mensagens e os controles;
NÃO apaga nada no celular do cliente, só aqui no sistema) */}
{confirmaApagar && conv && (
setConfirmaApagar(false)}
footer={<> setConfirmaApagar(false)}>Cancelar {
const alvo = conv; setConfirmaApagar(false);
let n = 0;
for (const m of alvo.msgs) { if (m._id && DB.waMsgDelete) { await DB.waMsgDelete(teamId, m._id); n++; } }
if (DB.waConversaLimpar) await DB.waConversaLimpar(teamId, alvo.contato);
setConvKey(null); setThreadAberta(false);
dispatch({ type: "TOAST", msg: "Conversa apagada (" + n + " mensagens)." });
}}>Apagar de vez>}>
Apagar a conversa com {conv.nome} ({conv.msgs.length} mensagens)?
Isso remove o histórico do sistema (o celular do cliente não é afetado) e zera os controles de follow-up.
Não dá para desfazer.
)}
{/* FICHA DO LEAD (criar/editar) — o mesmo modal do CRM, dentro do WhatsApp.
Sem id = novo (vem pré-preenchido da conversa); com id = editar. */}
{leadModal && window.LeadModal && (
setLeadModal(null)}
onSave={(data) => {
if (leadModal.id) {
dispatch({ type: "UPDATE_LEAD", id: leadModal.id, patch: data });
dispatch({ type: "TOAST", msg: "Lead atualizado pelo WhatsApp." });
} else {
dispatch({ type: "ADD_LEAD", lead: data });
dispatch({ type: "TOAST", msg: "Contato virou lead no CRM. ✅" });
}
setLeadModal(null);
}}
onAgendar={(l) => { setLeadModal(null); dispatch({ type: "AGENDAR_LEAD", lead: l }); }}
onTarefa={(l) => { setLeadModal(null); setTarefaLead({ leadFixo: l }); }} />
)}
{/* TAREFA vinculada ao lead — o mesmo modal do CRM/Tarefas */}
{tarefaLead && window.TarefaModal && (
setTarefaLead(null)} />
)}
{/* Modal de conexão — ciente do canal (Oficial vs Não-oficial) */}
{qrModal && (
setQrModal(false)}
onCanal={(canal) => {
if (!DB || !DB.waCaixaSet) return;
// Evolution: pede o PROVISIONAMENTO — o motor (n8n) cria a instância
// e devolve o QR neste mesmo documento (aparece aqui em tempo real).
if (canal === "evolution" && caixa.status !== "conectado") {
DB.waCaixaSet(teamId, caixaId, { canal: canal, status: "provisionar", solicitadoEm: new Date().toISOString() });
} else {
DB.waCaixaSet(teamId, caixaId, { canal: canal, status: caixa.status || "desconectado" });
}
}} />
)}
{/* Modal do FOLLOW-UP — régua por fase do funil, com IA adaptando os modelos.
Config em config.followupWhats — o motor lê do banco a cada rodada. */}
{followModal && (
setFollowModal(false)}
onSave={(cfg) => { dispatch({ type: "SET_CONFIG", patch: { followupWhats: cfg } }); dispatch({ type: "TOAST", msg: "Follow-up salvo. O motor já segue a nova régua." }); setFollowModal(false); }} />
)}
{/* Modal do AGENTE DE IA — o prompt mora no BANCO (config.agenteWhats);
o motor (n8n) lê a cada mensagem. Editar aqui vale na hora. */}
{agenteModal && (
setAgenteModal(false)}
onSave={(cfg) => { dispatch({ type: "SET_CONFIG", patch: { agenteWhats: cfg } }); dispatch({ type: "TOAST", msg: "Agente de IA salvo. Já vale para as próximas mensagens." }); setAgenteModal(false); }} />
)}
{/* Modal de AUTOMAÇÕES — a ponte (motor) lê esta configuração */}
{autoModal && (
setAutoModal(false)}
onSave={(cfg) => { dispatch({ type: "SET_CONFIG", patch: { whatsAuto: cfg } }); dispatch({ type: "TOAST", msg: "Automações do WhatsApp salvas." }); setAutoModal(false); }} />
)}
);
}
/* ---- Modal de conexão: escolhe o CANAL e mostra o passo certo ----
• cloudapi = API Oficial (Meta Cloud API / Coexistência) — conexão feita no
n8n/Meta; aqui não tem QR, o número "aparece" quando o webhook recebe.
• evolution = não-oficial (Evolution/Baileys) — conexão por QR Code.
Os TOKENS ficam no n8n (servidor) — nunca nesta tela. */
function ConexaoWhatsModal({ caixa, onClose, onCanal }) {
const { useState } = React;
const [canal, setCanal] = useState(caixa.canal || null);
const escolher = (c) => { setCanal(c); onCanal(c); };
return (
Fechar}>
{!canal ? (
<>
Como este número vai conectar?
>
) : canal === "cloudapi" ? (
Canal: API Oficial
Este número usa a API Oficial da Meta (Coexistência). A conexão e os tokens ficam no n8n (servidor) — nunca aqui.
As conversas aparecem automaticamente assim que o webhook receber a 1ª mensagem. {caixa.numero ? <>Número: {caixa.numero}.> : Aguardando o número ser vinculado no n8n.}
) : (
CANAL: NÃO-OFICIAL (QR)
{caixa.qr ? (
Abra o WhatsApp no celular
⋮ > Dispositivos conectados
Conectar dispositivo → aponte a câmera
) : (
{caixa.status === "provisionar" || caixa.status === "conectando"
? <>⏳ Gerando o QR Code… o motor está criando a instância deste número (leva até 1 minuto). O código aparece aqui sozinho — não feche.>
: <>O motor (Evolution) ainda não gerou o QR desta caixa. Quando o n8n/Evolution ativar o número, o código aparece aqui.>}
>
)}
);
}
/* ---- AGENTE DE IA — o prompt fica no BANCO (config.agenteWhats do tenant).
O motor (n8n) lê a cada mensagem recebida: mudou aqui, valeu na hora.
Modos: sempre | fora do horário | desligado. Modelo: GPT (OpenAI). ---- */
function AgenteIAModal({ agente, onClose, onSave }) {
const { useState, useRef, useEffect } = React;
const PROMPT_EXEMPLO = "Você é o assistente comercial da {{empresa}}. Atenda com simpatia e objetividade, em português.\n\nSuas tarefas:\n1. Cumprimentar e entender o que o cliente precisa.\n2. Responder dúvidas sobre nossos serviços.\n3. Coletar nome e necessidade do cliente.\n4. Oferecer agendar uma conversa com a equipe comercial.\n\nRegras:\n- Nunca invente preços ou prazos; diga que a equipe confirma.\n- Se o cliente pedir um humano, diga que a equipe já vai assumir.\n- Respostas curtas (até 3 frases), tom cordial e profissional.";
const [f, setF] = useState({
ativo: !!agente.ativo,
modo: agente.modo || "fora_horario", // sempre | fora_horario
prompt: agente.prompt || PROMPT_EXEMPLO,
horaInicio: agente.horaInicio || "08:00",
horaFim: agente.horaFim || "18:00",
limiteDia: agente.limiteDia || 100, // teto de respostas/dia (controle de custo)
});
const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
// ---- SIMULADOR: converse com o agente ANTES de ligar no WhatsApp real ----
// Usa o prompt COMO ESTÁ NA TELA (nem precisa salvar). Nada é enviado ao
// WhatsApp nem gravado nas conversas — é um ensaio de bancada.
const [teste, setTeste] = useState([]); // [{de:'cliente'|'agente', texto}]
const [testeTxt, setTesteTxt] = useState("");
const [testeBusy, setTesteBusy] = useState(false);
const [testeErro, setTesteErro] = useState("");
const fimRef = useRef(null);
useEffect(() => { if (fimRef.current) fimRef.current.scrollIntoView({ behavior: "smooth" }); }, [teste, testeBusy]);
async function testar() {
const texto = testeTxt.trim();
if (!texto || testeBusy) return;
const novo = teste.concat([{ de: "cliente", texto }]);
setTeste(novo); setTesteTxt(""); setTesteErro(""); setTesteBusy(true);
try {
const r = await Promise.race([
fetch("https://webhook.kesassessoria.com/webhook/kes-agente-teste", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: String(f.prompt || "").slice(0, 6000), historico: novo.slice(-12) }),
}),
new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 30000)),
]);
const d = await r.json();
if (d && d.resposta) setTeste((t) => t.concat([{ de: "agente", texto: String(d.resposta) }]));
else setTesteErro("O motor respondeu sem texto — tente de novo.");
} catch (e) {
setTesteErro("O motor de teste não respondeu. Ele precisa estar ativo no n8n (workflow “Teste do Agente”).");
}
setTesteBusy(false);
}
return (
Cancelar onSave({ ...f, limiteDia: Number(f.limiteDia) || 100, modelo: "gpt-4o-mini" })}>Salvar agente>}>
A régua é por fase do funil: cliente parado na fase X segue os passos da fase X. Respondeu? O ciclo zera.
Mudou de fase? Vale a régua nova. Conversa assumida por humano não recebe follow-up automático.
{fases.map((fase) => {
const c = faseCfg(fase.id);
return (
{c.ativo && setFase(fase.id, { passos: p })} />}
);
})}
{f.semLead.ativo && set("semLead", { ...f.semLead, passos: p })} />}