/* global React, ReactDOM, Sidebar, TopBar, Dashboard, EcosystemPage, SystemAssistantsPage, AgentsPage, AgentProfilePage, ConversationsPage, TicketsPage, ChannelsPage, KnowledgePage, ToolsPage, SquadsPage, TablesPage, LogsPage, TeamPage, TagsPage, useStudioData, API, THEME_KEY, Icon */ const { useState: useAppState, useEffect: useAppEffect } = React; const GLOBAL_GUIDE_WELCOME = { role: 'assistant', content: 'Olá. Posso te orientar em qualquer tela do sistema.' }; const HASH_PAGE_IDS = new Set([ 'dashboard', 'ecosystem', 'assistants', 'agents', 'conversations', 'tickets', 'channels', 'kb', 'tools', 'squads', 'tables', 'logs', 'team', 'tags', ]); const GUIDE_QUICK_PROMPTS = ['O que posso fazer nesta tela?', 'Qual é o próximo passo?', 'Estou perdido, me guia.']; const GUIDE_PAGE_META = { dashboard: { module: 'Operação', title: 'Dashboard', purpose: 'Acompanhar indicadores, conversas, tickets e sinais gerais da operação.', actions: ['Ver indicadores', 'Abrir conversas', 'Abrir tickets'], docs: ['concepts/ecosystem-ux-redesign'] }, ecosystem: { module: 'Início', title: 'Mapa do Ecossistema', purpose: 'Entender dependências entre dados, conhecimento, agentes, canais e atendimento humano.', actions: ['Seguir recomendações', 'Abrir módulos incompletos', 'Validar prontidão'], docs: ['concepts/ecosystem-ux-redesign'] }, assistants: { module: 'Início', title: 'Assistentes do Sistema', purpose: 'Testar agentes, avaliar diagnóstico e gerar melhorias revisáveis.', actions: ['Rodar Testador', 'Enviar diagnóstico ao Otimizador', 'Aplicar seções selecionadas'], docs: ['concepts/system-assistants', 'concepts/agent-test-and-optimizer-experience'] }, agents: { module: 'Inteligência', title: 'Agentes', purpose: 'Criar, encontrar e abrir agentes para configuração completa.', actions: ['Novo bot de IA', 'Abrir perfil', 'Revisar publicação'], docs: ['concepts/agent-guided-setup'] }, 'agent-profile': { module: 'Inteligência', title: 'Perfil do Agente', purpose: 'Configurar um agente específico e acompanhar prontidão, testes, conhecimento, ferramentas e logs.', actions: ['Editar configuração', 'Trocar abas', 'Testar', 'Publicar quando pronto'], docs: ['concepts/agent-guided-setup', 'concepts/agent-test-and-optimizer-experience'] }, conversations: { module: 'Operação', title: 'Conversas', purpose: 'Acompanhar atendimentos e histórico de mensagens.', actions: ['Abrir conversa', 'Filtrar atendimento', 'Ver repasse'], docs: ['concepts/ecosystem-ux-redesign'] }, tickets: { module: 'Operação', title: 'Tickets', purpose: 'Priorizar casos que precisam de acompanhamento humano.', actions: ['Alterar status', 'Atribuir responsável', 'Abrir conversa'], docs: ['concepts/ecosystem-ux-redesign'] }, channels: { module: 'Orquestração', title: 'Canais', purpose: 'Conectar WhatsApp/API e vincular entradas a agentes ou squads.', actions: ['Criar canal', 'Gerar QR', 'Vincular agente'], docs: ['concepts/ecosystem-ux-redesign'] }, kb: { module: 'Inteligência', title: 'Conhecimento', purpose: 'Gerenciar bases, fontes e consultas de teste.', actions: ['Criar base', 'Adicionar fonte', 'Testar consulta'], docs: ['concepts/system-assistants'] }, tools: { module: 'Inteligência', title: 'Ferramentas', purpose: 'Configurar capacidades controladas dos agentes.', actions: ['Habilitar ferramenta', 'Revisar escopo', 'Vincular agente'], docs: ['concepts/ecosystem-ux-redesign'] }, squads: { module: 'Orquestração', title: 'Squads', purpose: 'Organizar agentes em times com gerente, especialistas e fallback.', actions: ['Criar squad', 'Adicionar membros', 'Revisar roteamento'], docs: ['concepts/ecosystem-ux-redesign'] }, tables: { module: 'Dados', title: 'Tabelas', purpose: 'Manter dados estruturados consultáveis pela operação e pelos agentes.', actions: ['Criar tabela', 'Editar campos', 'Abrir tela cheia'], docs: ['concepts/admin-table-editor'] }, logs: { module: 'Telemetria', title: 'Logs', purpose: 'Auditar eventos, execuções e falhas.', actions: ['Filtrar evento', 'Ver status', 'Identificar origem'], docs: ['concepts/frontend-architecture'] }, team: { module: 'CRM', title: 'Atendentes e Times', purpose: 'Configurar responsáveis humanos e distribuição de atendimento.', actions: ['Criar atendente', 'Pausar disponibilidade', 'Ajustar time'], docs: ['concepts/ecosystem-ux-redesign'] }, tags: { module: 'CRM', title: 'Tags', purpose: 'Classificar conversas, tickets e leads.', actions: ['Criar tag', 'Organizar categorias', 'Revisar uso'], docs: ['concepts/ecosystem-ux-redesign'] }, }; const GUIDE_AGENT_TAB_META = { setup: { title: 'Setup', purpose: 'Conferir identidade, modelo, canal/API, repasse e prontidão inicial.', actions: ['Corrigir pendências', 'Validar configurações básicas'] }, instructions: { title: 'Instruções', purpose: 'Definir prompt, tom, regras negativas, objetivos e objetivo final.', actions: ['Editar prompt', 'Salvar instruções', 'Enviar para teste'] }, test: { title: 'Teste', purpose: 'Simular conversas antes de publicar.', actions: ['Enviar mensagem de teste', 'Limpar conversas de teste', 'Abrir Testador automático'] }, knowledge: { title: 'Conhecimento', purpose: 'Vincular bases e fontes ao agente.', actions: ['Vincular base', 'Revisar fontes', 'Testar consulta'] }, tools: { title: 'Ferramentas', purpose: 'Controlar capacidades extras do agente.', actions: ['Habilitar ferramenta', 'Salvar capacidades'] }, conditionals: { title: 'Condicionais', purpose: 'Definir regras especiais por situação.', actions: ['Criar condição', 'Editar prioridade'] }, conversations: { title: 'Conversas', purpose: 'Ver conversas relacionadas ao agente.', actions: ['Abrir histórico', 'Localizar falhas'] }, logs: { title: 'Logs', purpose: 'Auditar execuções e erros deste agente.', actions: ['Ver erro', 'Comparar execuções'] }, connections: { title: 'Conexões', purpose: 'Revisar canal, instância e vínculo operacional.', actions: ['Ver status', 'Abrir canal'] }, }; function visibleTextList(selector, limit = 8) { try { return Array.from(document.querySelectorAll(selector)) .filter(el => (el.offsetParent || el.getClientRects().length) && !el.closest('.global-guide-panel')) .map(el => (el.innerText || el.getAttribute('aria-label') || el.title || '').replace(/\s+/g, ' ').trim()) .filter(Boolean) .filter((item, idx, arr) => arr.indexOf(item) === idx) .slice(0, limit); } catch (err) { return []; } } function guideCounts(data = {}) { return { agents: (data.agents || []).length, conversations: (data.conversations || []).length, tickets: (data.tickets || []).length, channels: (data.channels || []).length, knowledge_bases: (data.knowledge || []).length, tools: (data.tools || []).length, squads: (data.squads || []).length, tables: (data.tables || []).length, }; } function buildGuideScreenContext({ data, page, breadcrumbs, agentId, tab, loading, error, toast }) { const meta = GUIDE_PAGE_META[page] || { module: 'Sistema', title: 'Tela atual', purpose: 'Orientar a operação do sistema.', actions: [] }; const agent = (data.agents || []).find(item => String(item.id) === String(agentId)); const tabMeta = page === 'agent-profile' ? GUIDE_AGENT_TAB_META[tab || 'setup'] : null; return { page, route: window.location.hash || '#dashboard', page_label: [...(breadcrumbs || [])].join(' / '), module: meta.module, breadcrumbs: breadcrumbs || [], purpose: tabMeta ? `${meta.purpose} Aba atual: ${tabMeta.purpose}` : meta.purpose, tab: page === 'agent-profile' ? (tab || 'setup') : undefined, tab_label: tabMeta?.title, documentation: meta.docs || [], expected_actions: [...(meta.actions || []), ...(tabMeta?.actions || [])], visible_actions: visibleTextList('button, a.tb-btn, .assistant-switch-card', 12), visible_panels: visibleTextList('.panel-title, .create-title, .rt-label, h1, h2', 12), visible_fields: visibleTextList('.field-label, label > span', 12), visible_statuses: visibleTextList('.pill, .sync-state, .toast, .assistant-error-card', 12), selected_agent: agent ? { id: agent.id, name: agent.name, description: agent.description || agent.role || '', published: !!agent.isPublished, setup_status: agent.setup_status || agent.raw?.setup_status || '', } : null, state: { loading: !!loading, backend_error: error || '', toast: typeof toast === 'string' ? toast : toast?.message || '', counts: guideCounts(data), }, permissions: { can_create_agent: true, can_edit_current_agent: page === 'agent-profile' && !!agent, can_run_tester: page === 'assistants' || page === 'agent-profile', can_apply_optimizer: page === 'assistants', }, }; } function readAdminHashRoute() { const raw = decodeURIComponent((window.location.hash || '').replace(/^#\/?/, '').trim()); const parts = raw.split('/').map(part => part.trim()).filter(Boolean); if (!parts.length) return { page: 'dashboard', agentId: null, tab: 'setup' }; if (parts[0] === 'agent' && parts[1]) { return { page: 'agent-profile', agentId: parts[1], tab: parts[2] || 'setup' }; } if (HASH_PAGE_IDS.has(parts[0])) return { page: parts[0], agentId: null, tab: 'setup' }; return { page: 'dashboard', agentId: null, tab: 'setup' }; } function adminHashForRoute(page, agentId, tab) { if (page === 'agent-profile' && agentId) { return `#agent/${encodeURIComponent(agentId)}/${encodeURIComponent(tab || 'setup')}`; } if (page && page !== 'dashboard' && HASH_PAGE_IDS.has(page)) return `#${page}`; return ''; } function writeAdminHash(page, agentId, tab) { const hash = adminHashForRoute(page, agentId, tab); const target = `${window.location.pathname}${window.location.search}${hash}`; const current = `${window.location.pathname}${window.location.search}${window.location.hash}`; if (target !== current) window.history.pushState(null, '', target); } function applyTheme(theme) { document.documentElement.setAttribute('data-theme', theme); try { localStorage.setItem(window.THEME_KEY || THEME_KEY || 'was-theme', theme); } catch (e) {} } function initShellControls() { document.addEventListener('keydown', event => { if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); document.querySelector('.topbar-search input')?.focus(); } }, { once: true }); } function App() { const { data, loading, error, refresh } = useStudioData(); const initialRoute = React.useMemo(() => readAdminHashRoute(), []); const [page, setRawPage] = useAppState(initialRoute.page); const [menuOpen, setMenuOpen] = useAppState(false); const [agentId, setAgentId] = useAppState(initialRoute.agentId); const [agentProfileTab, setAgentProfileTab] = useAppState(initialRoute.tab || 'setup'); const [theme, setTheme] = useAppState(() => { try { return localStorage.getItem(window.THEME_KEY || 'was-theme') || 'light'; } catch (e) { return 'light'; } }); const [toast, setToast] = useAppState(''); const [formDialog, setFormDialog] = useAppState(null); const [confirmDialog, setConfirmDialog] = useAppState(null); const [globalGuideOpen, setGlobalGuideOpen] = useAppState(false); const [globalGuideDraft, setGlobalGuideDraft] = useAppState(''); const [globalGuideMessages, setGlobalGuideMessages] = useAppState([GLOBAL_GUIDE_WELCOME]); const [globalGuideLoading, setGlobalGuideLoading] = useAppState(false); const [globalGuideError, setGlobalGuideError] = useAppState(''); window.MOCK = data; const showToast = (message, type = '') => setToast({ message, type }); const openFormDialog = config => new Promise(resolve => { setFormDialog({ ...config, resolve }); }); const openConfirmDialog = config => new Promise(resolve => { setConfirmDialog({ ...config, resolve }); }); window.STUDIO = { api: API, refresh, toast: showToast, form: openFormDialog, confirm: openConfirmDialog }; window.refreshAll = refresh; useAppEffect(() => { applyTheme(theme); }, [theme]); useAppEffect(() => initShellControls(), []); useAppEffect(() => { window.scrollTo(0, 0); }, [page]); useAppEffect(() => { window.navigate = id => go(id); return () => { if (window.navigate) delete window.navigate; }; }, []); useAppEffect(() => { function syncFromHash() { const route = readAdminHashRoute(); setRawPage(route.page); if (route.page === 'agent-profile') { setAgentId(route.agentId); setAgentProfileTab(route.tab || 'setup'); } setMenuOpen(false); } window.addEventListener('hashchange', syncFromHash); return () => window.removeEventListener('hashchange', syncFromHash); }, []); useAppEffect(() => { if (!toast) return undefined; const timer = setTimeout(() => setToast(''), 3500); return () => clearTimeout(timer); }, [toast]); const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark'); const setPage = id => { const nextPage = typeof id === 'function' ? id(page) : id; const safePage = HASH_PAGE_IDS.has(nextPage) ? nextPage : 'dashboard'; setRawPage(safePage); setMenuOpen(false); writeAdminHash(safePage); }; const go = id => { setPage(id); }; const openAgent = (id, tab = 'setup') => { setAgentId(id); setAgentProfileTab(tab); setRawPage('agent-profile'); setMenuOpen(false); writeAdminHash('agent-profile', id, tab); }; const setAgentTab = tab => { setAgentProfileTab(tab || 'setup'); if (page === 'agent-profile' && agentId) writeAdminHash('agent-profile', agentId, tab || 'setup'); }; async function createAgentQuick() { const values = await openFormDialog({ title: 'Novo bot de IA', submitLabel: 'Criar bot', description: 'Cria um rascunho e abre o setup guiado.', fields: [ { name: 'name', label: 'Nome do bot', required: true, helper: 'Nome visivel para você identificar o assistente.' }, { name: 'description', label: 'Descrição curta', type: 'textarea', helper: 'Opcional. O que este bot deve fazer?' }, { name: 'ai_api_key', label: 'Chave de API do agente', type: 'password', placeholder: 'sk-..., gsk_... ou chave NVIDIA', helper: 'Opcional quando a chave global do provedor já está configurada.' }, ], }); if (!values) return; const name = values.name.trim(); const preset = window.WA?.modelPresetById?.(window.WA.DEFAULT_MODEL_PRESET) || { model: 'gpt-4o-mini', temperature: 0.7 }; const payload = { name, instance_name: name, description: values.description || 'Novo agente WhatsApp', ai_api_key: String(values.ai_api_key || '').trim(), system_prompt: window.WA?.DEFAULT_PROMPT || `## Identidade Você e um assistente virtual inteligente e prestativo. Seu nome e ${name}. Você representa uma empresa profissional e esta aqui para ajudar o usuário com clareza e eficiência. ## Tom de Voz - Sejá cordial, direto e objetivo. - Use linguagem simples e acessivel. - Evite respostas longas demais. ## Objetivo Principal Ajudar o usuário a encontrar a informação que precisa, resolver seu problema ou direciona-lo para o próximo passo correto.`, ai_model: preset.model, temperature: preset.temperature, negative_rules: [], conversation_goals: ['Entender necessidade', 'Resolver no primeiro contato'], final_goal: 'Resolver atendimento com qualidade.', handoff_config: { enabled: true }, tools: { web_search: { enabled: false }, knowledge_base: { enabled: true }, contextual_memory: { enabled: true }, ticket_creation: { enabled: true }, }, }; try { const created = await API.createAgent(payload); await refresh(); const createdId = created?.agent_id || created?.id; showToast('Bot criado. Complete o setup para publicar.'); if (createdId) openAgent(createdId, 'setup'); else setPage('agents'); } catch (err) { showToast(err.message || String(err), 'bad'); } } function onSearch(event) { if (event.key !== 'Enter') return; const query = event.currentTarget.value.trim().toLowerCase(); if (!query) return; const agent = (window.MOCK.agents || []).find(item => `${item.name} ${item.role} ${item.channel}`.toLowerCase().includes(query)); if (agent) { openAgent(agent.id); return; } const ticket = (window.MOCK.tickets || []).find(item => `${item.id} ${item.client} ${item.summary}`.toLowerCase().includes(query)); if (ticket) setPage('tickets'); else setPage('conversations'); } async function submitGlobalGuide(text, event) { event && event.preventDefault(); const message = String(text || globalGuideDraft).trim(); if (!message || globalGuideLoading) return; const nextMessages = [...globalGuideMessages, { role: 'user', content: message }]; setGlobalGuideMessages(nextMessages); setGlobalGuideDraft(''); setGlobalGuideLoading(true); setGlobalGuideError(''); try { const context = buildGuideScreenContext({ data, page, breadcrumbs: crumbs, agentId, tab: agentProfileTab, loading, error, toast }); const res = await API.systemGuideChat({ message, history: globalGuideMessages.slice(-8), context: { page, ...context } }); setGlobalGuideMessages([...nextMessages, { role: 'assistant', content: res.reply || res.message || 'Não consegui responder agora.' }]); } catch (err) { const friendly = 'O Guia não conseguiu responder agora. Verifique a conexão dos assistentes e tente novamente.'; setGlobalGuideError(friendly); setGlobalGuideMessages([...nextMessages, { role: 'assistant', content: friendly }]); } finally { setGlobalGuideLoading(false); } } function sendGlobalGuide(event) { return submitGlobalGuide(null, event); } function askGlobalGuide(prompt) { return submitGlobalGuide(prompt); } function clearGlobalGuide() { setGlobalGuideMessages([GLOBAL_GUIDE_WELCOME]); setGlobalGuideDraft(''); setGlobalGuideError(''); } const titleMap = { dashboard: ['Operação', 'Dashboard'], ecosystem: ['Início', 'Mapa do Ecossistema'], assistants: ['Início', 'Assistentes'], agents: ['Inteligência', 'Agentes'], 'agent-profile': ['Inteligência', 'Perfil'], conversations: ['Operação', 'Conversas'], tickets: ['Operação', 'Tickets'], channels: ['Dados', 'Canais'], kb: ['Inteligência', 'Conhecimento'], tools: ['Inteligência', 'Ferramentas'], squads: ['Inteligência', 'Squads'], tables: ['Dados', 'Tabelas'], logs: ['Telemetria', 'Logs'], team: ['CRM', 'Times'], tags: ['CRM', 'Tags'], }; const crumbs = ['Studio', ...(titleMap[page] || ['-'])]; let content; switch (page) { case 'dashboard': content = ; break; case 'ecosystem': content = ; break; case 'assistants': content = ; break; case 'agents': content = ; break; case 'agent-profile': content = setPage('agents')}/>; break; case 'conversations': content = ; break; case 'tickets': content = ; break; case 'channels': content = ; break; case 'kb': content = ; break; case 'tools': content = ; break; case 'squads': content = ; break; case 'tables': content = ; break; case 'logs': content = ; break; case 'team': content = ; break; case 'tags': content = ; break; default: content = ; } return (
setMenuOpen(false)}/>
setMenuOpen(true)} theme={theme} toggleTheme={toggleTheme} onNewAgent={createAgentQuick} loading={loading} error={error} onSearch={onSearch} onGuide={() => setGlobalGuideOpen(value => !value)} guideOpen={globalGuideOpen} /> {error &&
Backend não respondeu: {error}
} {toast &&
{toast.message}
} {loading &&
Sincronizando dados...
} {content} { formDialog?.resolve(null); setFormDialog(null); }} onSubmit={values => { formDialog?.resolve(values); setFormDialog(null); }}/> { confirmDialog?.resolve(false); setConfirmDialog(null); }} onConfirm={() => { confirmDialog?.resolve(true); setConfirmDialog(null); }}/>
); } function FormDialog({ dialog, onClose, onSubmit }) { const [values, setValues] = React.useState({}); React.useEffect(() => { if (!dialog) return; const next = {}; (dialog.fields || []).forEach(field => { next[field.name] = field.value ?? field.defaultValue ?? ''; }); setValues(next); }, [dialog]); if (!dialog) return null; function submit(event) { event.preventDefault(); const missing = (dialog.fields || []).find(field => field.required && !String(values[field.name] || '').trim()); if (missing) return; onSubmit(values); } return (
{dialog.title}
{dialog.description &&
{dialog.description}
}
{(dialog.fields || []).map(field => (