/* global React, Icon, API */ const AGENT_PROFILE_DIRTY_FIELDS = new Set(); let AGENT_PROFILE_SYNCING = false; function registerAgentProfileDirtyTracking() { AGENT_PROFILE_DIRTY_FIELDS.clear(); } function setAgentProfileControlValue(id, value, prop = 'value') { const el = document.getElementById(id); if (!el || AGENT_PROFILE_DIRTY_FIELDS.has(id)) return; if (prop === 'checked') el.checked = !!value; else el.value = value == null ? '' : value; } window.AGENT_PROFILE_DIRTY_FIELDS = AGENT_PROFILE_DIRTY_FIELDS; window.registerAgentProfileDirtyTracking = registerAgentProfileDirtyTracking; window.setAgentProfileControlValue = setAgentProfileControlValue; const studioToast = window.studioToast || function(m, t) { if (window.STUDIO?.toast) window.STUDIO.toast(m, t); }; const studioForm = window.studioForm || function(c) { return window.STUDIO?.form ? window.STUDIO.form(c) : Promise.resolve(null); }; const studioConfirm = window.studioConfirm || function(c) { return window.STUDIO?.confirm ? window.STUDIO.confirm(c) : Promise.resolve(false); }; async function copyToClipboard(text) { if (navigator.clipboard?.writeText) { try { await navigator.clipboard.writeText(text); return true; } catch (err) {} } const ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); return ok; } function setupBadgeClass(status) { if (status === 'production') return 'ok'; if (status === 'paused') return 'warn'; if (status === 'ready') return 'accent'; return 'warn'; } const SETUP_STAGE_META = { created: { tab: 'instructions', title: 'Bot criado', body: 'Nome e estrutura inicial existem. Você pode ajustar depois.' }, personality: { tab: 'instructions', title: 'Personalidade', body: 'Defina como o bot fala, quais limites segue e qual IA usa.' }, goals: { tab: 'instructions', title: 'Objetivos', body: 'Liste o que o bot precisa conseguir durante a conversa.' }, kb: { tab: 'knowledge', title: 'Conhecimento', body: 'Adicione documentos, URLs ou textos que o bot deve consultar.' }, channels: { tab: 'connections', title: 'WhatsApp ou API', body: 'Receba mensagens reais por WhatsApp, API ou site. Conecte WhatsApp quando quiser repassar informacoes para alvos selecionados.' }, tested: { tab: 'test', title: 'Teste', body: 'Teste antes de publicar. Use o sandbox para validar tom e respostas.' }, review: { tab: 'setup', title: 'Pendências', body: 'Revise bloqueios obrigatórios e recomendações antes de publicar.' }, publish: { tab: 'setup', title: 'Publicar', body: 'Libera o bot para responder mensagens reais. Pode pausar depois.' }, }; 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 normalizePublicSlugInput(value) { return String(value || '') .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .replace(/-{2,}/g, '-') .slice(0, 80); } window.AgentProfilePage = function AgentProfilePage({ agentId, onBack, initialTab, onTabChange }) { const agent = (window.MOCK.agents || []).find(a => String(a.id) === String(agentId)) || (window.MOCK.agents || [])[0]; const [tab, setTab] = React.useState(initialTab || 'setup'); const [publicLinkInfo, setPublicLinkInfo] = React.useState(null); const [publicLinkBusy, setPublicLinkBusy] = React.useState(''); React.useEffect(() => { if (!agent) return; setTab(initialTab || (agent.isPublished ? 'instructions' : 'setup')); }, [agent?.id, initialTab]); if (!agent) { return ( <>
Inteligência

Agente

Nenhum agente criado ainda.

Crie um agente para abrir o perfil.
); } const tabs = [ { id: 'setup', label: 'Setup' }, { id: 'instructions', label: 'Instruções' }, { id: 'test', label: 'Teste' }, { id: 'knowledge', label: 'Conhecimento' }, { id: 'tools', label: 'Ferramentas' }, { id: 'conditionals', label: 'Condicionais' }, { id: 'conversations', label: 'Conversas' }, { id: 'logs', label: 'Logs' }, { id: 'connections', label: 'Conexões' }, ]; const status = agent.statusTag || (agent.status === 'online' ? 'ok' : ['connecting', 'not_found'].includes(agent.status) ? 'warn' : agent.status === 'offline' ? 'bad' : ''); function selectTab(nextTab) { setTab(nextTab); if (onTabChange) onTabChange(nextTab); } async function resolvePublicTestLink(action) { if (publicLinkBusy) return; let publicTestWindow = null; if (action === 'open') { publicTestWindow = window.open(`/chat/${agent.raw?.public_slug || agent.id}`, '_blank'); if (publicTestWindow) publicTestWindow.opener = null; } setPublicLinkBusy(action); try { const info = await API.getAgentPublicTestLink(agent.id); setPublicLinkInfo(info); if (action === 'copy') { if (!info.can_share) { studioToast(info.message || 'Configure um tunnel antes de compartilhar este link.', 'bad'); return; } const copied = await copyToClipboard(info.url); studioToast(copied ? 'Link copiado.' : 'Não foi possível copiar.', copied ? '' : 'bad'); } else { if (publicTestWindow) publicTestWindow.location.href = info.url; else window.open(info.url, '_blank', 'noopener'); if (!info.can_share) studioToast('Teste aberto localmente. Para compartilhar, configure um tunnel público.', 'bad'); } } catch (err) { if (publicTestWindow) publicTestWindow.close(); studioToast(err.message || String(err), 'bad'); } finally { setPublicLinkBusy(''); } } return ( <>
Inteligência - {agent.role}

{agent.name}

Modelo {agent.model} {' - '}canal {agent.channel}{' - '}{agent.kbSources} fontes{' - '}{agent.tools} ferramentas

{agent.statusLabel || agent.status}
{agent.conversations.toLocaleString('pt-BR')} conversas - {agent.tickets} tickets
{publicLinkInfo?.requires_tunnel && !publicLinkInfo?.can_share && (
Localhost detectado. Configure PUBLIC_CHAT_BASE_URL ou PUBLIC_TEST_BASE_URL para compartilhar com domínio próprio.
)}
{tabs.map(t => ( ))}
{tab === 'setup' && } {tab === 'instructions' && } {tab === 'test' && } {tab === 'connections' && } {tab === 'knowledge' && } {tab === 'tools' && } {tab === 'conditionals' && } {tab === 'conversations' && } {tab === 'logs' && }
); }; function SetupTab({ agent, setTab }) { const [readiness, setReadiness] = React.useState(null); const [loading, setLoading] = React.useState(true); const [busy, setBusy] = React.useState(''); const statusLabel = readiness ? window.setupStatusLabel(readiness.stage) : agent.setupLabel; const statusClass = setupBadgeClass(readiness?.stage || agent.setupStatus); const steps = readiness?.steps || []; const activeStage = (steps.find(step => !step.done) || steps[steps.length - 1] || {}).id; const blockers = readiness?.blockers || []; const required = blockers.filter(item => item.severity === 'required'); const recommended = blockers.filter(item => item.severity !== 'required'); const percent = readiness?.percent ?? Math.round(((agent.setupProgress?.completed_count || 0) / Math.max(agent.setupProgress?.total_count || 1, 1)) * 100); async function load() { setLoading(true); try { setReadiness(await API.getAgentReadiness(agent.id)); } catch (err) { studioToast(err.message || String(err), 'bad'); } finally { setLoading(false); } } React.useEffect(() => { load(); }, [agent.id]); function goStage(stage) { setTab(SETUP_STAGE_META[stage]?.tab || 'instructions'); } async function publish() { if (busy) return; setBusy('publish'); try { await API.publishAgent(agent.id); await window.STUDIO.refresh(); await load(); studioToast('Bot publicado. Ele já pode responder mensagens reais.'); } catch (err) { studioToast(err.message || String(err), 'bad'); await load(); } finally { setBusy(''); } } async function pause() { if (busy) return; setBusy('pause'); try { await API.unpublishAgent(agent.id); await window.STUDIO.refresh(); await load(); studioToast('Bot pausado. Mensagens reais não seráo respondidas pela IA.'); } catch (err) { studioToast(err.message || String(err), 'bad'); } finally { setBusy(''); } } const doneCount = steps.filter(s => s.done).length; return (
{/* Hero status strip */}
{loading ? '…' : `${percent}%`}
{statusLabel}
{doneCount} de {steps.length} etapas concluídas
{readiness?.is_published ? : }
{blockers.length > 0 && (
{required.length > 0 && {required.length} obrigatória{required.length > 1 ? 's' : ''}} {required.map((item, idx) => ( ))} {recommended.map((item, idx) => ( ))}
)}
{steps.map((step, idx) => { const meta = SETUP_STAGE_META[step.id] || {}; const isCurrent = step.id === activeStage; const stepBlockers = blockers.filter(b => b.stage === step.id); return ( ); })}
{!readiness?.can_publish &&
Resolva pendências obrigatórias para publicar.
}
); } function InstructionsTab({ agent }) { const raw = agent.raw || {}; const [form, setForm] = React.useState(() => formFromAgent(agent)); const [saving, setSaving] = React.useState(false); React.useEffect(() => { AGENT_PROFILE_SYNCING = true; registerAgentProfileDirtyTracking(); setForm(formFromAgent(agent)); AGENT_PROFILE_SYNCING = false; }, [agent.id]); function set(name, value, id) { if (!AGENT_PROFILE_SYNCING && id) AGENT_PROFILE_DIRTY_FIELDS.add(id); setForm(prev => ({ ...prev, [name]: value })); } function setModelPreset(presetId) { const preset = window.WA.modelPresetById(presetId); setForm(prev => ({ ...prev, model_preset: preset.id, ai_model: preset.model, temperature: preset.temperature })); } async function save() { if (!form.instance_name.trim()) { studioToast('Informe o nome interno do agente.', 'bad'); return; } if (!/^(?=.*[a-z])[a-z0-9]+(?:-[a-z0-9]+)*$/.test(form.public_slug || '')) { studioToast('Use um slug público com letras minúsculas, números e hífens.', 'bad'); return; } setSaving(true); try { const payload = { name: form.name, public_slug: form.public_slug, instance_name: form.instance_name, description: form.description, ai_model: form.ai_model, temperature: Number(form.temperature || 0.7), system_prompt: form.system_prompt, negative_rules: lines(form.negative_rules), conversation_goals: lines(form.conversation_goals), final_goal: form.final_goal, handoff_config: { enabled: form.handoff_enabled }, }; const apiKey = String(form.ai_api_key || '').trim(); if (apiKey) payload.ai_api_key = apiKey; await API.updateAgent(agent.id, payload); if (apiKey) setForm(prev => ({ ...prev, ai_api_key: '' })); await window.STUDIO.refresh(); studioToast('Agente salvo.'); } catch (err) { studioToast(err.message || String(err), 'bad'); } finally { setSaving(false); } } return ( <>
Identidade
configuração
set('name', v)} helper="Nome visivel no painel. Pode mudar depois." required/> set('public_slug', normalizePublicSlugInput(v))} helper="Usado no link público. Ex: sm-lead-assistant." required/> set('instance_name', v, 'ap-instance')} helper="Apelido usado para vincular WhatsApp. Use letras, números e hífen."/> set('description', v)} multiline helper="Resumo para a equipe entender o papel deste bot."/>
Modelo & parametros
set('ai_api_key', v, 'ap-key')} placeholder={raw.has_api_key ? 'Manter atual se vazio' : 'Cole a chave do provedor'} helper={raw.has_api_key ? 'Chave atual configurada. Preencha apenas para substituir.' : 'Use a chave do provedor escolhido ou configure a chave global no .env.'} autoComplete="new-password" /> set('temperature', v)} helper="Baixa = mais consistente. Alta = mais livre."/>
Prompt principal
Como escrever
Diga quem o bot representa, como deve falar, o que deve fazer e quando deve chamar humano.