/* global React, Icon, Spark, API */ let QR_REFRESH_TIMER = null; function clearQrRefreshTimer() { if (QR_REFRESH_TIMER) clearTimeout(QR_REFRESH_TIMER); QR_REFRESH_TIMER = null; window.QR_REFRESH_TIMER = null; } function scheduleQrRefresh(callback) { clearQrRefreshTimer(); QR_REFRESH_TIMER = setTimeout(() => { if (typeof callback === 'function') callback(); }, 35000); window.QR_REFRESH_TIMER = QR_REFRESH_TIMER; } function studioToast(message, type = '') { if (window.STUDIO?.toast) window.STUDIO.toast(message, type); } function studioConfirm(config) { return window.STUDIO?.confirm ? window.STUDIO.confirm(config) : Promise.resolve(false); } function parseKbSources(text) { return String(text || '').split(/\n+/).map(line => line.trim()).filter(Boolean).map(line => { if (/^https?:\/\//i.test(line)) return { source_type: 'url', uri: line }; return { source_type: 'text', content: line }; }); } function readKbFiles(files) { const list = Array.from(files || []); return Promise.all(list.map(file => new Promise(resolve => { const reader = new FileReader(); reader.onload = () => resolve({ source_type: 'file', uri: file.name, mime_type: file.type || 'application/octet-stream', content: reader.result }); reader.onerror = () => resolve(null); reader.readAsDataURL(file); }))).then(items => items.filter(Boolean)); } function parseMemoryVariables(text) { const raw = String(text || '').trim(); if (!raw) return []; const normalized = raw.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); const found = []; const add = (name, description) => { if (!found.some(item => item.name === name)) found.push({ name, type: 'text', description, persistent: true, save_to_table: true }); }; if (/@?nome|nome do usuário|nome do cliente|como se chama/.test(normalized)) add('nome', 'Nome ou forma de tratamento do cliente.'); if (/@?numero|telefone|whatsapp|celular/.test(normalized)) add('numero', 'Telefone ou número de WhatsApp do cliente.'); if (/@?email|e-mail|mail/.test(normalized)) add('email', 'Email do cliente para retorno.'); if (/@?problema|dúvida|dificuldade|erro|motivo|precisa|necessidade/.test(normalized)) add('problema', 'Problema, dúvida ou necessidade principal do cliente.'); if (/queria falar|quer falar|falar sobre|assunto|pauta|tema/.test(normalized)) add('assunto', 'Assunto que o cliente quer tratar.'); if (/empresa|organi[sz]ação|instituicao|negócio/.test(normalized)) add('empresa', 'Empresa ou organização do cliente.'); if (/cpf|cnpj|documento/.test(normalized)) add('documento', 'Documento informado pelo cliente quando necessário.'); if (found.length) return found; return raw.split(/\n|,|;/).map(line => line.trim()).filter(Boolean).slice(0, 6).map(line => { const clean = line.replace(/^@+/, '').trim(); const rawName = clean.split(/[:,-]/)[0].trim() || clean; const name = rawName.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/\b(do|da|de|o|a|os|as|ele|ela|cliente|usuário)\b/g, '').replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, ''); return { name: name || 'info_cliente', type: 'text', description: `Informação do cliente: ${clean}`, persistent: true, save_to_table: true }; }); } function manualFormFromTemplate(templateId = 'default') { const template = window.WA.agentTemplateById(templateId); const preset = window.WA.modelPresetById(template.model_preset || window.WA.DEFAULT_MODEL_PRESET); return { template_id: template.id, name: template.name || '', instance_name: template.instance_name || '', description: template.description || '', ai_model: template.ai_model || preset.model || window.WA.DEFAULT_MODEL, model_preset: template.model_preset || preset.id, ai_api_key: '', temperature: template.temperature ?? preset.temperature ?? 0.7, system_prompt: template.system_prompt || window.WA.DEFAULT_PROMPT, kb_sources: template.kb_sources || '', client_info: template.client_info || '', negative_rules: template.negative_rules || [], conversation_goals: template.conversation_goals || [], final_goal: template.final_goal || '', handoff_config: template.handoff_config || { enabled: false, conditions: [], next_action: '' }, }; } window.AgentsPage = function AgentsPage({ onOpenAgent, onNewAgent }) { const [q, setQ] = React.useState(''); const [view, setView] = React.useState('grid'); const [statusFilter, setStatusFilter] = React.useState('all'); const [modalMode, setModalMode] = React.useState(null); const [manualForm, setManualForm] = React.useState(() => manualFormFromTemplate()); const [manualKbFiles, setManualKbFiles] = React.useState([]); const [detailIndex, setDetailIndex] = React.useState(0); const [detailAnswers, setDetailAnswers] = React.useState({}); const [detailWorking, setDetailWorking] = React.useState(false); const [detailError, setDetailError] = React.useState(''); const [detailCreated, setDetailCreated] = React.useState(null); const [createdGuide, setCreatedGuide] = React.useState(null); const [qrAgent, setQrAgent] = React.useState(null); const [qrData, setQrData] = React.useState(null); const [qrLoading, setQrLoading] = React.useState(false); const [qrCountdown, setQrCountdown] = React.useState(30); const data = window.MOCK || {}; const agents = data.agents || []; const list = agents.filter(agent => { const text = `${agent.name} ${agent.role} ${agent.model} ${agent.channel}`.toLowerCase(); const statusMatch = statusFilter === 'all' || (statusFilter === 'online' && agent.status === 'online') || (statusFilter === 'attention' && agent.status !== 'online') || (statusFilter === 'disconnected' && ['offline', 'not_found', 'expired'].includes(agent.status)); return statusMatch && text.includes(q.toLowerCase()); }); const counts = { online: agents.filter(a => a.status === 'online').length, attention: agents.filter(a => a.status !== 'online').length, disconnected: agents.filter(a => ['offline', 'not_found', 'expired'].includes(a.status)).length, }; async function deleteAgent(agent) { if (!(await studioConfirm({ title: 'Excluir agente', message: `Excluir agente "${agent.name}"?`, confirmLabel: 'Excluir', danger: true }))) return; try { await API.deleteAgent(agent.id); await window.STUDIO.refresh(); studioToast('Agente excluido.'); } catch (err) { studioToast(err.message || String(err), 'bad'); } } async function createManualAgent() { const name = manualForm.name.trim(); if (!name) { studioToast('Informe o nome do agente.', 'bad'); return; } try { const fileSources = await readKbFiles(manualKbFiles); const kbSources = [...parseKbSources(manualForm.kb_sources), ...fileSources]; const memoryVariables = parseMemoryVariables(manualForm.client_info); const created = await API.createAgent({ name, instance_name: manualForm.instance_name.trim() || name, description: manualForm.description.trim(), ai_model: manualForm.ai_model || window.WA.DEFAULT_MODEL, ai_api_key: String(manualForm.ai_api_key || '').trim(), temperature: Number(manualForm.temperature ?? 0.7), system_prompt: manualForm.system_prompt.trim() || window.WA.DEFAULT_PROMPT, negative_rules: manualForm.negative_rules || [], conversation_goals: (manualForm.conversation_goals || []).length ? manualForm.conversation_goals : memoryVariables.map(item => `Coletar ${item.name} quando fizer sentido.`), final_goal: manualForm.final_goal || '', handoff_config: manualForm.handoff_config || { enabled: false, conditions: [], next_action: '' }, kb_sources: kbSources, tools: { contextual_memory: { enabled: true, variables: memoryVariables }, knowledge_base: { enabled: kbSources.length > 0 }, ticket_creation: { enabled: true }, }, }); setManualForm(manualFormFromTemplate()); setManualKbFiles([]); setModalMode(null); await window.STUDIO.refresh(); setCreatedGuide({ id: created?.agent_id || created?.id, name, kbCount: kbSources.length, memoryCount: memoryVariables.length }); studioToast(created?.post_create_error ? `Agente criado, mas fontes/ferramentas falharam: ${created.post_create_error}` : 'Agente criado.', created?.post_create_error ? 'bad' : ''); } catch (err) { studioToast(err.message || String(err), 'bad'); } } function applyManualTemplate(templateId) { setManualForm(manualFormFromTemplate(templateId)); setManualKbFiles([]); } function applyModelPreset(presetId) { const preset = window.WA.modelPresetById(presetId); setManualForm(prev => ({ ...prev, model_preset: preset.id, ai_model: preset.model, temperature: preset.temperature })); } function startDetailedAgentBuilder() { setDetailIndex(0); setDetailAnswers({ kb_sources: '', kb_files: [] }); setDetailError(''); setDetailCreated(null); setDetailWorking(false); setModalMode('detail'); } async function nextDetailedBuilder() { if (detailWorking || detailCreated) return; const questions = window.WA.DETAIL_BUILDER_QUESTIONS; if (detailIndex < questions.length - 1) { setDetailError(''); setDetailIndex(detailIndex + 1); return; } setDetailWorking(true); setDetailError(''); try { const builderQuestions = questions.filter(qn => qn.kind !== 'kb'); const description = builderQuestions.map(qn => `${qn.label}\n${detailAnswers[qn.id] || ''}`).join('\n\n'); const fileSources = await readKbFiles(detailAnswers.kb_files); const kbSources = [...parseKbSources(detailAnswers.kb_sources), ...fileSources]; const memoryVariables = parseMemoryVariables(detailAnswers.client_info); const preview = await API.agentBuilderPreview({ description, temperature: 0.2, kb_sources: kbSources }).catch(() => ({ fallback: true, description })); const name = preview.name || titleFromDescription(description); const created = await API.createAgent({ name, instance_name: `${name}-${Math.random().toString(36).slice(2, 7)}`, description: preview.description || description.slice(0, 240), ai_model: window.WA.DEFAULT_MODEL, temperature: Number(preview.temperature ?? 0.7), system_prompt: preview.system_prompt || window.WA.DEFAULT_PROMPT, negative_rules: normalizeBuilderList(preview.instructions?.dont || preview.constraints || preview.negative_rules), conversation_goals: normalizeBuilderList(preview.conversation_goals || preview.goals), final_goal: preview.final_goal || '', handoff_config: preview.handoff_config || { enabled: true, conditions: ['pedido de humano', 'baixa confiança'], next_action: 'criar ticket' }, conditional_prompts: preview.conditional_prompts || [], kb_sources: kbSources, tools: toolsFromSuggested(preview.suggested_tools || [], memoryVariables, kbSources.length > 0), contextual_memory: { enabled: true }, }); const fresh = await window.STUDIO.refresh(); const agentId = created?.agent_id || created?.id || (fresh?.agents || []).find(agent => agent.name === name)?.id; setDetailCreated({ id: agentId, name, kbCount: kbSources.length, memoryCount: memoryVariables.length }); studioToast(created?.post_create_error ? `Agente criado, mas fontes/ferramentas falharam: ${created.post_create_error}` : 'Agente criado pelo builder.', created?.post_create_error ? 'bad' : ''); } catch (err) { setDetailError(err.message || String(err)); studioToast(err.message || String(err), 'bad'); } finally { setDetailWorking(false); } } function closeDetailedBuilder() { setModalMode(null); setDetailCreated(null); setDetailError(''); } function openCreatedAgent() { const id = detailCreated?.id; closeDetailedBuilder(); if (id) onOpenAgent(id); } async function openQr(agent) { setQrAgent(agent); setQrData(null); await refreshQr(agent); } async function refreshQr(agent = qrAgent) { if (!agent) return; setQrLoading(true); try { const data = await API.getQr(agent.id); setQrData(data); setQrCountdown(30); scheduleQrRefresh(() => refreshQr(agent)); } catch (err) { setQrData({ error: err.message || String(err) }); } finally { setQrLoading(false); } } function closeQr() { clearQrRefreshTimer(); setQrAgent(null); setQrData(null); } React.useEffect(() => { if (!qrAgent) return undefined; const timer = setInterval(() => setQrCountdown(value => Math.max(value - 1, 0)), 1000); return () => clearInterval(timer); }, [qrAgent?.id]); React.useEffect(() => { if (!qrAgent) return undefined; const timer = setInterval(() => window.STUDIO?.refresh?.().catch(() => {}), 5000); return () => clearInterval(timer); }, [qrAgent?.id]); return ( <>
Assistentes que respondem no site e no WhatsApp. Novos bots ficam em rascunho até o setup terminar.
| Bot de IA | Setup | Modelo | Canal | Conversas | Tickets | Latência | Tendencia | Status | |
|---|---|---|---|---|---|---|---|---|---|
| {agent.setupLabel || (agent.isPublished ? 'Em produção' : 'Rascunho')} | {agent.model} | {agent.channel} | {agent.conversations.toLocaleString('pt-BR')} | {agent.tickets} | {agent.latency ? agent.latency + ' ms' : '--'} | {agent.statusLabel || agent.status} |
|
||
| Nenhum bot encontrado. | |||||||||
Cole links, textos curtos ou anexe arquivos. Se deixar vazio, o agente será criado sem KB inicial.