/* global React, Icon, API */
const { useState: useExtraState, useEffect: useExtraEffect } = React;
function studioData() {
return window.MOCK || {};
}
function refreshStudio() {
return window.STUDIO && window.STUDIO.refresh ? window.STUDIO.refresh() : Promise.resolve();
}
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); };
function agentName(id) {
const agent = (studioData().agents || []).find(item => String(item.id) === String(id));
return agent ? agent.name : '--';
}
function statusTag(value) {
if (['online', 'open', 'ok', 'finished'].includes(value)) return 'ok';
if (['connecting', 'qr-pendente', 'in_progress', 'warn', 'pausado', 'not_found'].includes(value)) return 'warn';
if (['unanswered', 'offline', 'expired', 'bad', 'error'].includes(value)) return 'bad';
return '';
}
function channelStatusInfo(instanceName) {
const inst = (studioData().instances || []).find(item => item.id === instanceName || item.name === instanceName || item.raw?.name === instanceName);
return whatsappInstanceUiState(inst, instanceName);
}
function channelStatus(instanceName) {
return channelStatusInfo(instanceName).status;
}
function firstAgentId() {
return (studioData().agents || [])[0]?.id || null;
}
function assistantInitialAgentId(agents) {
const stored = window.localStorage?.getItem('sm_assistant_focus_agent_id');
if (stored && agents.some(agent => String(agent.id) === String(stored))) return String(stored);
return String(agents[0]?.id || '');
}
const TESTER_FLOW_STEPS = [
'Preparando cenários',
'Lendo configuração',
'Verificando materiais',
'Enviando perguntas',
'Analisando respostas',
'Gerando diagnóstico',
'Sugerindo melhorias',
];
const OPTIMIZER_FLOW_STEPS = [
'Analisando configuração',
'Revisando testes',
'Verificando conhecimento',
'Identificando falhas',
'Gerando melhorias',
'Comparando versões',
'Preparando histórico',
];
function useAssistantFlow(active, labels) {
const [current, setCurrent] = useExtraState(0);
useExtraEffect(() => {
if (!active) {
setCurrent(0);
return undefined;
}
setCurrent(0);
const timer = window.setInterval(() => {
setCurrent(prev => Math.min(prev + 1, labels.length - 1));
}, 950);
return () => window.clearInterval(timer);
}, [active, labels.length]);
return labels.map((label, index) => ({
label,
status: active ? (index < current ? 'done' : index === current ? 'running' : 'pending') : 'pending',
}));
}
function flowState(labels, active, finished, runningSteps) {
if (active) return runningSteps;
return labels.map(label => ({ label, status: finished ? 'done' : 'pending' }));
}
function fieldNames(table) {
const fields = table?.schema_json?.fields;
return Array.isArray(fields) ? fields.map(f => f.name || f.id).filter(Boolean) : [];
}
async function runAction(fn) {
try {
await fn();
await refreshStudio();
} catch (err) {
studioToast(err.message || String(err), 'bad');
}
}
function PageTitle({ eyebrow, title, sub, meta, action }) {
return (
);
}
function MiniKpi({ label, value, sub }) {
return (
{label}
{value}
{sub &&
{sub}
}
);
}
function EmptyRow({ cols, text }) {
return {text} ;
}
function prettyPageValue(value) {
return window.prettyStudioValue ? window.prettyStudioValue(value) : String(value == null ? '' : value);
}
function truncateText(value, max = 220) {
const text = typeof value === 'object' && value !== null ? prettyPageValue(value) : String(value == null ? '' : value);
return text.length > max ? text.slice(0, max - 1).trimEnd() + '...' : text;
}
function pickDisplayColumns(columns) {
const priority = ['conversation_id', 'agent_id', 'agent_name', 'customer_phone', 'customer_name', 'event', 'summary', 'created_at'];
const picked = priority.filter(col => columns.includes(col));
columns.forEach(col => {
if (picked.length < 6 && !picked.includes(col)) picked.push(col);
});
return picked.slice(0, 6);
}
function recordCellClass(col) {
return /summary|transcript|metadata|message|content/i.test(col) ? 'record-long' : '';
}
const ECOSYSTEM_STEPS = [
['tables', 'Dados'],
['knowledge_bases', 'Conhecimento'],
['agents', 'Agentes'],
['squads', 'Squads'],
['tools', 'Ferramentas'],
['channels', 'Canais'],
['teams', 'Time humano'],
['tested', 'Teste'],
['published', 'Produção'],
];
function ecosystemRoute(id) {
if (id === 'knowledge_bases') return 'kb';
if (id === 'teams') return 'team';
if (id === 'published' || id === 'tested') return 'agents';
if (id === 'attendance') return 'team';
return id;
}
function ecosystemStateLabel(percent) {
if (percent >= 85) return 'Pronto para produção assistida';
if (percent >= 55) return 'Setup em validação';
if (percent >= 25) return 'Fundação em montagem';
return 'Começo guiado';
}
window.EcosystemPage = function EcosystemPage({ setPage, onOpenAgent }) {
const [status, setStatus] = useExtraState(null);
const [loading, setLoading] = useExtraState(true);
const [error, setError] = useExtraState('');
const data = studioData();
const agents = data.agents || [];
const progress = status?.setup_progress || {};
const readyCount = ECOSYSTEM_STEPS.filter(([id]) => !!progress[id]).length;
const percent = Math.round((readyCount / ECOSYSTEM_STEPS.length) * 100);
async function load() {
setLoading(true);
setError('');
try {
setStatus(await API.getEcosystemStatus());
} catch (err) {
setError(err.message || String(err));
setStatus(null);
} finally {
setLoading(false);
}
}
useExtraEffect(() => { load(); }, []);
const recs = status?.recommendations || [];
const stateLabel = ecosystemStateLabel(percent);
const publishedCount = agents.filter(agent => agent.isPublished).length;
const openTicketCount = (data.tickets || []).filter(ticket => ['unanswered', 'in_progress'].includes(ticket.rawStatus || ticket.raw?.status)).length;
const publicConversationCount = (data.conversations || []).filter(conv => conv.channel === 'public' || conv.raw?.state?.source === 'public_test' || String(conv.number || '').startsWith('pub-')).length;
return (
<>
setPage('agents')}>Gerenciar agentes {loading ? 'Atualizando...' : 'Atualizar mapa'} }
/>
{error &&
Falha ao carregar mapa. Atualize novamente ou verifique o backend. {error}
}
Saúde operacional
{agents.length ? stateLabel : 'Nenhum agente criado'}
{agents.length ? `${readyCount} de ${ECOSYSTEM_STEPS.length} etapas gerais estão prontas. A leitura principal agora é por agente, sem painel genérico misturado com cards antigos.` : 'Crie o primeiro bot para liberar o mapa por agente, testes internos, link público e checklist de publicação.'}
Agentes {agents.length}
Publicados {publishedCount}
Filas abertas {openTicketCount}
Testes públicos {publicConversationCount}
{!agents.length ? (
Crie o primeiro agente para montar o ecossistema.
Depois disso o mapa passa a mostrar canais, conhecimento, ferramentas, filas, atendimentos, testes, publicação e pendências por agente.
setPage('agents')}> Criar bot de IA
setPage('assistants')}>Abrir Guia
) : (
{agents.map(agent => {
const agentId = String(agent.id);
const agentConvs = (data.conversations || []).filter(conv => String(conv.agentId || conv.agent_id || conv.raw?.agent_id || '') === agentId);
const publicConvs = agentConvs.filter(conv => conv.channel === 'public' || conv.raw?.state?.source === 'public_test' || String(conv.number || '').startsWith('pub-'));
const agentTickets = (data.tickets || []).filter(ticket => String(ticket.raw?.agent_id || ticket.agent_id || '') === agentId);
const openTickets = agentTickets.filter(ticket => ['unanswered', 'in_progress'].includes(ticket.rawStatus || ticket.raw?.status));
const agentRuns = (data.runs || []).filter(run => String(run.agent_id || '') === agentId);
const testRuns = agentRuns.filter(run => run.is_test || ['agent_test', 'sandbox', 'system_assistant_tester'].includes(run.input?.source));
const agentKbs = (data.knowledge || []).filter(kb => String(kb.agent_id || '') === agentId);
const agentTools = (data.tools || []).filter(tool => String(tool.agent_id || '') === agentId);
const agentChannels = (data.channels || []).filter(channel => String(channel.agent_id || '') === agentId || channel.instance_name === agent.channel || channel.instance_name === agent.raw?.instance_token);
const agentRecs = recs.filter(item => String(item.agent_id || '') === agentId);
const channelState = agent.statusTag || statusTag(agent.status);
const pendingItems = [
!(agent.kbSources || agentKbs.length) && 'Adicionar conhecimento',
!agentTools.length && 'Ativar ferramentas',
!agentChannels.length && 'Vincular canal',
!testRuns.length && 'Rodar teste',
!agent.isPublished && 'Publicar quando pronto',
...agentRecs.map(item => item.message),
].filter(Boolean).slice(0, 4);
return (
{(agent.name || '?')[0].toUpperCase()}
{agent.name} {agent.model || 'modelo não informado'}
onOpenAgent ? onOpenAgent(agent.id, 'setup') : setPage('agents')}>Setup
onOpenAgent ? onOpenAgent(agent.id, 'test') : setPage('agents')}>Teste
onOpenAgent ? onOpenAgent(agent.id, agent.isPublished ? 'conversations' : 'setup') : setPage('agents')}>{agent.isPublished ? 'Atendimentos' : 'Publicar'}
Status {agent.setupLabel || 'Rascunho'}
Canais {agentChannels.length || (agent.channel && agent.channel !== '--' ? 1 : 0)} · {agent.statusLabel || 'sem conexão'}
Conhecimento {agent.kbSources || agentKbs.length} base(s)
Ferramentas {agent.tools || agentTools.length} ativa(s)
Filas {openTickets.length} abertas
Atendimentos {agentConvs.length} threads
Testes {testRuns.length} internos · {publicConvs.length} públicos
Publicação {agent.isPublished ? 'Publicado' : 'Rascunho'}
Canais
{agentChannels.length ? agentChannels.slice(0, 3).map(channel => setPage('channels')}>{channel.name || channel.instance_name || 'Canal'} {channel.agent_name || agent.name} ) : onOpenAgent ? onOpenAgent(agent.id, 'connections') : setPage('channels')}>Vincular WhatsApp ou site }
Pendências
{pendingItems.length ? pendingItems.map((item, idx) =>
onOpenAgent ? onOpenAgent(agent.id, 'setup') : setPage('agents')}>{item} ) :
Sem pendências críticas
}
);
})}
)}
Recomendações do sistema
Priorizadas a partir das configurações atuais.
{recs.map((item, idx) => (
setPage(ecosystemRoute(item.action || item.module || 'dashboard'))}>
{String(idx + 1).padStart(2, '0')}
{item.type === 'success' ? 'Pronto' : item.type === 'warning' ? 'Atenção' : 'Sugestão'}
{item.message}
Abrir
))}
{!recs.length &&
Nenhuma recomendação pendente. Continue monitorando conversas e testes antes de escalar o uso.
}
>
);
};
const PATCH_LABELS = {
description: 'Descrição',
system_prompt: 'Prompt do agente',
negative_rules: 'Regras negativas',
conversation_goals: 'Objetivos da conversa',
final_goal: 'Objetivo final',
handoff_config: 'Repasse humano',
};
function patchPreview(value) {
if (Array.isArray(value)) return value.map(item => typeof item === 'string' ? item : prettyPageValue(item)).join('\n');
if (value && typeof value === 'object') return prettyPageValue(value);
return String(value || '');
}
function currentPatchPreview(value) {
if (value == null || value === '') return '(vazio)';
if (typeof value === 'string') return value;
return prettyPageValue(value);
}
function assistantFriendlyError(err) {
const raw = String(err?.message || err || '');
if (/list.*mapping|object is not a mapping/i.test(raw)) return 'O Otimizador recebeu uma resposta em formato inesperado. Gere a prévia novamente; o sistema agora normaliza esse retorno antes de aplicar.';
if (/internal server error/i.test(raw)) return 'O assistente encontrou uma falha interna. Corrija o diagnóstico do agente e tente novamente.';
if (/timeout|timed out/i.test(raw)) return 'O modelo demorou para responder. Tente novamente em alguns instantes.';
if (/api|key|chave|unauthorized|401/i.test(raw)) return 'A chave do modelo não respondeu corretamente. Verifique as credenciais dos assistentes.';
return raw || 'Não foi possível concluir a ação agora.';
}
function testerMessageText(message) {
if (message == null || message === '') return '';
if (typeof message === 'string') return message;
if (typeof message.content === 'string') return message.content;
if (typeof message.message === 'string') return message.message;
if (typeof message.text === 'string') return message.text;
return prettyPageValue(message);
}
function testerScenarioMessages(item) {
const scenario = item?.scenario || {};
const input = item?.input || {};
const candidates = [
scenario.messages,
item?.messages,
input.messages,
scenario.message,
scenario.input,
input.message,
item?.message,
item?.prompt,
];
const raw = candidates.find(value => Array.isArray(value) ? value.length : value);
const list = Array.isArray(raw) ? raw : [raw];
return list.map(testerMessageText).filter(Boolean);
}
function asList(value) {
if (Array.isArray(value)) return value.filter(item => item !== null && item !== undefined && item !== '');
if (value === null || value === undefined || value === '') return [];
if (typeof value === 'object') {
const itemKeys = ['problem', 'finding', 'summary', 'recommendation', 'reason', 'name', 'passed', 'scenario', 'status', 'evidence', 'message', 'value'];
if (itemKeys.some(key => Object.prototype.hasOwnProperty.call(value, key))) return [value];
return Object.entries(value)
.filter(([, item]) => item !== null && item !== undefined && item !== '')
.map(([key, item]) => {
if (item && typeof item === 'object' && !Array.isArray(item)) return { name: key, ...item };
return { name: key, value: item };
});
}
return [value];
}
function reportItemText(item) {
if (item === null || item === undefined || item === '') return '';
if (typeof item === 'boolean') return item ? 'Confirmado.' : '';
if (typeof item === 'string' || typeof item === 'number') return String(item);
if (typeof item !== 'object') return String(item);
if (Object.prototype.hasOwnProperty.call(item, 'value') && item.name) return `${item.name}: ${testerMessageText(item.value)}`;
const label = item.problem || item.finding || item.summary || item.recommendation || item.reason || item.name || item.status || item.evidence || item.message;
const context = item.scenario || item.category || item.severity;
if (context && label && context !== label) return `${context}: ${label}`;
if (label) return String(label);
const fields = ['what', 'impact', 'expected_behavior', 'tester_message', 'agent_reply_summary', 'rating']
.map(key => item[key] ? `${key.replace(/_/g, ' ')}: ${testerMessageText(item[key])}` : '')
.filter(Boolean);
return fields.join('\n');
}
function issueText(item) {
if (!item || typeof item !== 'object') return reportItemText(item);
const severity = item.severity || item.priority || 'médio';
const problem = item.problem || item.finding || item.message || item.evidence || 'problema identificado';
const scenario = item.scenario ? `${item.scenario}: ` : '';
return `${severity} - ${scenario}${problem}`;
}
function scenarioEvaluationText(item) {
if (!item || typeof item !== 'object') return reportItemText(item);
const lines = [
`${item.scenario || item.name || 'Cenário'}: ${item.finding || item.problem || item.status || item.evidence || 'avaliado'}`,
];
if (item.tester_message) lines.push(`Pergunta: ${item.tester_message}`);
if (item.agent_reply_summary) lines.push(`Resposta: ${item.agent_reply_summary}`);
if (item.rating) lines.push(`Avaliação: ${item.rating}`);
if (item.expected_behavior) lines.push(`Esperado: ${item.expected_behavior}`);
return lines.join('\n');
}
function scoreValue(value) {
const parsed = Number.parseFloat(String(value ?? 0).replace('%', ''));
if (!Number.isFinite(parsed)) return 0;
return Math.max(0, Math.min(100, Math.round(parsed)));
}
function criterionInfo(item) {
if (!item || typeof item !== 'object') return { label: reportItemText(item), passed: null };
const explicitPassed = Object.prototype.hasOwnProperty.call(item, 'passed') ? item.passed : item.value;
return {
label: item.name || item.label || item.criterion || reportItemText(item),
passed: explicitPassed === false || item.status === 'failed' || item.ok === false ? false : true,
};
}
function cleanReportText(value) {
const text = String(value || '').trim();
if (!text) return '';
if (/^(true|false|null|undefined|\[object object\])$/i.test(text)) return '';
return text;
}
function reportItems(items, emptyFallback) {
const list = asList(items).map(reportItemText).map(cleanReportText).filter(Boolean);
return list.length ? list : emptyFallback ? [emptyFallback] : [];
}
function preserveReportItems(items) {
return asList(items)
.map(item => typeof item === 'boolean' ? '' : reportItemText(item))
.map(cleanReportText)
.filter(Boolean);
}
function recommendationPriority(text, tone) {
const value = String(text || '').toLowerCase();
if (tone === 'positive' || tone === 'preserve') return '';
if (/(timeout|sil[eê]ncio|falha|cr[ií]tico|risco|n[aã]o respondeu|ausente|kb|base de conhecimento|invent)/.test(value)) return 'Alta';
if (/(implementar|adicionar|corrigir|validar|reduzir|refinar|ajustar|treinar)/.test(value)) return 'Média';
return tone === 'next' ? 'Baixa' : '';
}
function reportPriorityClass(priority) {
if (priority === 'Alta' || priority === 'Crítico') return 'high';
if (priority === 'Média') return 'medium';
if (priority === 'Baixa') return 'low';
return 'neutral';
}
function reportToneIcon(tone) {
if (tone === 'positive' || tone === 'preserve') return 'check';
if (tone === 'risk') return 'x';
if (tone === 'next') return 'arrow';
if (tone === 'knowledge') return 'kb';
if (tone === 'recommendation') return 'tools';
return 'logs';
}
function reportItemParts(text, tone) {
let clean = String(text || '').replace(/\r\n/g, '\n').trim();
let priority = '';
const prefix = clean.match(/^(cr[ií]tico|alta|m[eé]dia|media|m[eé]dio|medio|baixa)\s*[-:]\s*/i);
if (prefix) {
priority = /cr/i.test(prefix[1]) ? 'Crítico' : /alt/i.test(prefix[1]) ? 'Alta' : /m[eé]dia|media|m[eé]dio|medio/i.test(prefix[1]) ? 'Média' : 'Baixa';
clean = clean.slice(prefix[0].length).trim();
}
const suffix = clean.match(/\s+[-–—:]?\s*(cr[ií]tico|alta|m[eé]dia|media|m[eé]dio|medio|baixa)$/i);
if (suffix && clean.length > suffix[0].length + 12) {
priority = priority || (/cr/i.test(suffix[1]) ? 'Crítico' : /alt/i.test(suffix[1]) ? 'Alta' : /m[eé]dia|media|m[eé]dio|medio/i.test(suffix[1]) ? 'Média' : 'Baixa');
clean = clean.slice(0, clean.length - suffix[0].length).trim();
}
priority = priority || recommendationPriority(clean, tone);
const lines = clean.split('\n').map(line => line.trim()).filter(Boolean);
let title = lines.shift() || clean || 'Item';
const body = [];
const colon = title.indexOf(':');
if (colon > 0 && colon < 42) {
const before = title.slice(0, colon).trim();
const after = title.slice(colon + 1).trim();
title = before;
if (after) body.push(after);
}
body.push(...lines);
return { title, body, priority };
}
function scoreBand(score) {
if (score >= 85) return { label: 'Excelente', tone: 'positive', readiness: 'Pronto para uso assistido' };
if (score >= 70) return { label: 'Bom', tone: 'positive', readiness: 'Pronto com pequenos ajustes' };
if (score >= 50) return { label: 'Precisa de ajustes', tone: 'warn', readiness: 'Revisão recomendada' };
return { label: 'Crítico', tone: 'risk', readiness: 'Não publicar sem correção' };
}
function testerHistoryStatusClass(status) {
const value = String(status || '').toLowerCase();
if (['completed', 'done', 'ok'].includes(value)) return 'ok';
if (['started', 'running', 'pending'].includes(value)) return 'warn';
if (['failed', 'error'].includes(value)) return 'bad';
return '';
}
function testerHistoryDate(value) {
return window.fmtDate ? window.fmtDate(value) : (value || '--');
}
function testerHistoryScore(item) {
const score = scoreValue(item?.score);
return item?.score == null ? '--' : `${score}%`;
}
function testerHistoryMessages(detail) {
const direct = Array.isArray(detail?.messages) ? detail.messages : [];
if (direct.length) return direct;
return asList(detail?.output?.transcripts).flatMap((item, idx) => {
const scenario = item?.scenario || {};
const label = scenario.name || `Cenário ${idx + 1}`;
return [
scenario.message && { role: 'user', content: scenario.message, scenario: label, latency_ms: item.latency_ms },
item.reply && { role: 'assistant', content: item.reply, scenario: label, latency_ms: item.latency_ms },
item.error && { role: 'system', content: item.error, scenario: label, latency_ms: item.latency_ms },
].filter(Boolean);
});
}
window.SystemAssistantsPage = function SystemAssistantsPage({ onOpenAgent, setPage }) {
const data = studioData();
const agents = data.agents || [];
const [tab, setTab] = useExtraState('tester');
const [agentId, setAgentId] = useExtraState(() => assistantInitialAgentId(agents));
const [testerMode, setTesterMode] = useExtraState('quick');
const [testerObjective, setTesterObjective] = useExtraState('');
const [testerLoading, setTesterLoading] = useExtraState(false);
const [testerReport, setTesterReport] = useExtraState(null);
const [optimizerLoading, setOptimizerLoading] = useExtraState(false);
const [optimizerPreview, setOptimizerPreview] = useExtraState(null);
const [selectedSections, setSelectedSections] = useExtraState({});
const [agentVersions, setAgentVersions] = useExtraState([]);
const [assistantError, setAssistantError] = useExtraState('');
const [testerHistory, setTesterHistory] = useExtraState({ items: [], total: 0, limit: 6, offset: 0 });
const [testerHistoryLimit, setTesterHistoryLimit] = useExtraState(6);
const [testerHistoryLoading, setTesterHistoryLoading] = useExtraState(false);
const [testerHistoryOpen, setTesterHistoryOpen] = useExtraState(true);
const [testerHistoryDetail, setTesterHistoryDetail] = useExtraState(null);
const [testerHistoryDetailLoading, setTesterHistoryDetailLoading] = useExtraState(false);
const selectedAgent = agents.find(agent => String(agent.id) === String(agentId));
const testerFlowRunning = useAssistantFlow(testerLoading, TESTER_FLOW_STEPS);
const optimizerFlowRunning = useAssistantFlow(optimizerLoading, OPTIMIZER_FLOW_STEPS);
const testerFlowSteps = flowState(TESTER_FLOW_STEPS, testerLoading, !!testerReport, testerFlowRunning);
const optimizerFlowSteps = flowState(OPTIMIZER_FLOW_STEPS, optimizerLoading, !!optimizerPreview, optimizerFlowRunning);
useExtraEffect(() => {
if (!agentId && agents[0]?.id) setAgentId(assistantInitialAgentId(agents));
}, [agents.length]);
useExtraEffect(() => {
if (agentId) window.localStorage?.setItem('sm_assistant_focus_agent_id', String(agentId));
}, [agentId]);
useExtraEffect(() => {
if (!agentId) {
setAgentVersions([]);
return undefined;
}
let alive = true;
API.getAgentVersions(Number(agentId))
.then(rows => { if (alive) setAgentVersions(rows || []); })
.catch(() => { if (alive) setAgentVersions([]); });
return () => { alive = false; };
}, [agentId]);
async function loadTesterHistory(limit = testerHistoryLimit) {
if (!agentId) {
setTesterHistory({ items: [], total: 0, limit, offset: 0 });
return { items: [], total: 0, limit, offset: 0 };
}
setTesterHistoryLoading(true);
try {
const data = await API.getSystemTesterHistory({ agent_id: Number(agentId), limit, offset: 0 });
setTesterHistory(data || { items: [], total: 0, limit, offset: 0 });
return data;
} catch (err) {
setTesterHistory({ items: [], total: 0, limit, offset: 0 });
return null;
} finally {
setTesterHistoryLoading(false);
}
}
useExtraEffect(() => {
let alive = true;
if (!agentId || tab !== 'tester') return undefined;
setTesterHistoryLoading(true);
API.getSystemTesterHistory({ agent_id: Number(agentId), limit: testerHistoryLimit, offset: 0 })
.then(data => { if (alive) setTesterHistory(data || { items: [], total: 0, limit: testerHistoryLimit, offset: 0 }); })
.catch(() => { if (alive) setTesterHistory({ items: [], total: 0, limit: testerHistoryLimit, offset: 0 }); })
.finally(() => { if (alive) setTesterHistoryLoading(false); });
return () => { alive = false; };
}, [agentId, tab, testerHistoryLimit]);
async function refreshVersions() {
if (!agentId) return [];
const rows = await API.getAgentVersions(Number(agentId));
setAgentVersions(rows || []);
return rows || [];
}
async function openTesterHistoryDetail(item) {
if (!item?.id) return;
setTesterHistoryDetailLoading(true);
try {
const detail = await API.getSystemTesterHistoryDetail(item.id);
setTesterHistoryDetail(detail);
} catch (err) {
studioToast(err.message || String(err), 'bad');
} finally {
setTesterHistoryDetailLoading(false);
}
}
function loadMoreTesterHistory() {
setTesterHistoryLimit(value => Math.min(value + 6, 48));
}
async function runTester() {
if (!agentId) return studioToast('Selecione um agente para testar.', 'bad');
setTesterLoading(true);
setTesterReport(null);
setAssistantError('');
try {
const res = await API.runSystemTester({ agent_id: Number(agentId), mode: testerMode, objective: testerObjective });
setTesterReport(res);
setTesterHistoryLimit(6);
loadTesterHistory(6).catch(() => {});
studioToast('Teste interno concluído.');
} catch (err) {
const friendly = assistantFriendlyError(err);
setAssistantError(friendly);
studioToast(friendly, 'bad');
} finally {
setTesterLoading(false);
}
}
async function previewOptimizer(report = testerReport) {
if (!agentId) return studioToast('Selecione um agente para otimizar.', 'bad');
setOptimizerLoading(true);
setOptimizerPreview(null);
setAssistantError('');
try {
const res = await API.previewSystemOptimizer({ agent_id: Number(agentId), tester_report: report || null });
const sections = {};
Object.keys(res.patch || {}).forEach(key => { sections[key] = true; });
setSelectedSections(sections);
setOptimizerPreview(res);
setAgentVersions(res.versions || agentVersions);
setTab('optimizer');
studioToast('Prévia de melhoria gerada.');
} catch (err) {
const friendly = assistantFriendlyError(err);
setAssistantError(friendly);
studioToast(friendly, 'bad');
} finally {
setOptimizerLoading(false);
}
}
async function applyOptimizer() {
if (!optimizerPreview?.patch) return;
const sections = Object.keys(selectedSections).filter(key => selectedSections[key]);
if (!sections.length) return studioToast('Selecione ao menos uma seção para aplicar.', 'bad');
const ok = await studioConfirm({
title: 'Aplicar melhorias no agente?',
message: 'As seções selecionadas serão salvas no agente. Se ele estiver publicado, revise o comportamento antes de novos atendimentos reais.',
confirmLabel: 'Aplicar melhorias',
danger: false,
});
if (!ok) return;
try {
const res = await API.applySystemOptimizer({ agent_id: Number(agentId), patch: optimizerPreview.patch, sections });
await refreshStudio();
setAgentVersions(res.versions || await refreshVersions());
studioToast(`Melhorias aplicadas: ${(res.applied || []).length}`);
if (onOpenAgent) onOpenAgent(Number(agentId), 'instructions');
} catch (err) {
const friendly = assistantFriendlyError(err);
setAssistantError(friendly);
studioToast(friendly, 'bad');
}
}
async function restoreVersion(version) {
if (!version?.id || !agentId) return;
const ok = await studioConfirm({
title: 'Restaurar versão do agente?',
message: `Restaurar a versão "${version.label || version.id}"? O estado atual será salvo antes da restauração.`,
confirmLabel: 'Restaurar',
danger: false,
});
if (!ok) return;
try {
const res = await API.restoreAgentVersion(Number(agentId), version.id);
await refreshStudio();
await refreshVersions();
studioToast(`Versão restaurada: ${(res.restored || []).length} seção(ões).`);
if (onOpenAgent) onOpenAgent(Number(agentId), 'instructions');
} catch (err) {
const friendly = assistantFriendlyError(err);
setAssistantError(friendly);
studioToast(friendly, 'bad');
}
}
const patchEntries = Object.entries(optimizerPreview?.patch || {});
const testerScore = scoreValue(testerReport?.score);
const assistantCards = [
{ id: 'tester', label: 'Testador de Agente', status: testerLoading ? 'testando' : testerReport ? `${testerScore}% no último teste` : 'pronto', detail: 'Executa cenários automáticos contra o agente selecionado.' },
{ id: 'optimizer', label: 'Otimizador de Agente', status: optimizerLoading ? 'gerando' : optimizerPreview ? 'prévia gerada' : 'aguardando', detail: 'Propõe melhorias revisáveis antes de salvar.' },
];
const testerBand = scoreBand(testerScore);
const testerReportSections = testerReport ? [
{ key: 'strengths', title: 'Forças', icon: 'check', tone: 'positive', items: testerReport.strengths, empty: 'Nenhuma força específica registrada.' },
{ key: 'issues', title: 'Problemas', icon: 'x', tone: 'risk', items: asList(testerReport.issues).map(issueText), empty: 'Nenhum problema crítico registrado.' },
{ key: 'improvements', title: 'Melhorias', icon: 'arrow', tone: 'improvement', items: testerReport.improvements, empty: 'Nenhuma melhoria objetiva registrada.' },
{ key: 'next_tests', title: 'Próximos testes', icon: 'refresh', tone: 'next', items: testerReport.next_tests, empty: 'Nenhum próximo teste sugerido.' },
{ key: 'kb', title: 'Uso da KB', icon: 'kb', tone: 'knowledge', items: testerReport.kb_findings, empty: 'A base de conhecimento não trouxe achados específicos neste teste.' },
{ key: 'tone', title: 'Tom e escopo', icon: 'assistants', tone: 'neutral', items: testerReport.tone_findings, empty: 'Nenhuma observação específica sobre tom e escopo.' },
{ key: 'scenarios', title: 'Avaliação por cenário', icon: 'logs', tone: 'scenario', items: asList(testerReport.scenario_evaluations).map(scenarioEvaluationText), empty: 'Nenhuma avaliação por cenário registrada.' },
{ key: 'preserve', title: 'O que preservar', icon: 'check', tone: 'preserve', items: preserveReportItems(testerReport.preserve), empty: 'Nenhum ponto específico informado para preservação.' },
{ key: 'optimizer', title: 'Recomendações para o Otimizador', icon: 'tools', tone: 'recommendation', items: testerReport.optimizer_recommendations, empty: 'Nenhuma recomendação específica para o Otimizador.' },
] : [];
return (
<>
setPage('ecosystem')}>Abrir mapa setTab('tester')}>Testar agente }
/>
{assistantError &&
{assistantError}Se persistir, rode o teste automático ou confira as credenciais do modelo.
}
Operação assistida
Dois assistentes internos, um fluxo de melhoria.
Use o Testador para auditar um agente real e o Otimizador para aplicar somente ajustes revisados. O Guia do Sistema está disponível na barra superior.
{assistantCards.map(card => (
setTab(card.id)}>
{card.id === 'tester' ? '01' : '02'}
{card.label}
{card.status}
{card.detail}
))}
{tab === 'tester' && (
Testador de Agente
Roda cenários internos, avalia respostas, ferramentas, KBs e pontos de melhoria.
Agente
setAgentId(e.target.value)}>
Selecione
{agents.map(agent => {agent.name} )}
Modo
setTesterMode(e.target.value)}>
Rápido
Profundo
Objetivo do teste
{testerLoading ? 'Testando...' : 'Rodar teste'}
{selectedAgent &&
onOpenAgent && onOpenAgent(Number(agentId), 'test')}>Abrir aba Teste do perfil }
setTesterHistoryOpen(value => !value)}
onOpenDetail={openTesterHistoryDetail}
onRefresh={() => loadTesterHistory().catch(() => {})}
onLoadMore={loadMoreTesterHistory}
detailLoading={testerHistoryDetailLoading}
/>
{testerLoading ? (
) : !testerReport ? (
) : (
<>
Diagnóstico de {selectedAgent?.name || 'agente'}
{testerReport.summary}
{testerReport.score_0_10 !== undefined &&
Nota final: {testerReport.score_0_10}/10
}
= 75 ? 'pill-ok' : testerScore >= 50 ? 'pill-warn' : 'pill-bad'}`}>{testerReport.quality_label || testerBand.label}
previewOptimizer(testerReport)}>Enviar para otimização
Prontidão
{testerBand.readiness}
{testerBand.label}
Alertas
{asList(testerReport.issues).length}
problemas encontrados
Melhorias
{asList(testerReport.improvements).length}
ações recomendadas
Cenários
{asList(testerReport.scenario_evaluations).length || asList(testerReport.transcripts).length}
rodadas avaliadas
{asList(testerReport.transcripts).length > 0 && (
Conversa simulada
{asList(testerReport.transcripts).map((item, idx) => (
Rodada {idx + 1}
{item?.scenario?.name || `Cenário ${idx + 1}`}
{item?.status || 'avaliado'}
O testador enviou
{(testerScenarioMessages(item).length ? testerScenarioMessages(item) : ['Mensagem enviada não registrada neste relatório.']).map((msg, mi) => (
))}
{item?.scenario?.expectation && (
Critério esperado
{item.scenario.expectation}
)}
O agente respondeu
}/>
))}
)}
{asList(testerReport.criteria).map((c, i) => {
const info = criterionInfo(c);
return {info.label} ;
})}
{!asList(testerReport.criteria).length && ['KB', 'Ferramentas', 'Tom', 'Handoff'].map(c => (
{c}
))}
{testerReportSections.map(section => (
))}
>
)}
)}
{tab === 'optimizer' && (
Otimizador de Agente
Gera uma proposta de melhoria e só aplica o que você selecionar.
Agente
setAgentId(e.target.value)}>
Selecione
{agents.map(agent => {agent.name} )}
previewOptimizer(testerReport)} disabled={optimizerLoading || !agentId}>{optimizerLoading ? 'Gerando...' : 'Gerar melhoria'}
{testerReport &&
Usando o diagnóstico mais recente do Testador como contexto.
}
{optimizerLoading ? (
) : !optimizerPreview ? (
) : (
<>
Prévia de melhoria
{optimizerPreview.summary}
reversível
{patchEntries.filter(([key]) => selectedSections[key]).length} selecionada{patchEntries.filter(([key]) => selectedSections[key]).length === 1 ? '' : 's'}
{ setOptimizerPreview(null); setSelectedSections({}); studioToast('Prévia rejeitada.'); }}>Rejeitar prévia
Aplicar selecionadas
{optimizerPreview.warning &&
Aviso: {optimizerPreview.warning}
}
{patchEntries.map(([key, value]) => {
const current = selectedAgent?.raw?.[key];
const reason = optimizerPreview.patch_reasons?.[key];
return (
setSelectedSections(prev => ({ ...prev, [key]: e.target.checked }))}/>
{PATCH_LABELS[key] || key}
{selectedSections[key] ? 'Será aplicado' : 'Ignorado'}
{reason &&
{reason}
}
Atual
{currentPatchPreview(current)}
Proposto
{patchPreview(value)}
);
})}
{!patchEntries.length &&
Nenhuma alteração proposta.
}
>
)}
)}
{testerHistoryDetail && (
setTesterHistoryDetail(null)}
onUseReport={() => {
if (testerHistoryDetail.output) {
setTesterReport(testerHistoryDetail.output);
setTab('tester');
setTesterHistoryDetail(null);
studioToast('Diagnóstico anterior reaberto.');
}
}}
/>
)}
>
);
};
function TesterHistoryModule({ history, loading, open, onToggle, onOpenDetail, onRefresh, onLoadMore, detailLoading }) {
const items = Array.isArray(history?.items) ? history.items : [];
const total = Number(history?.total || 0);
return (
Testes anteriores
{loading ? '...' : total}
{open && (
{items.length ? `${items.length} de ${total}` : 'Sem registros'}
Atualizar
{loading && !items.length &&
Carregando testes...
}
{!loading && !items.length && (
)}
{items.map(item => (
{item.agent_name}
{item.status || 'registrado'}
{testerHistoryDate(item.created_at)} - {item.mode || 'quick'} - {testerHistoryScore(item)}
{item.first_message || item.summary || 'Teste registrado.'}
{item.quality_label || item.result || 'Resultado pendente'}
onOpenDetail(item)} disabled={detailLoading}>Detalhes
))}
{items.length < total && (
Carregar mais
)}
)}
);
}
function TesterHistoryDetailModal({ detail, onClose, onUseReport }) {
const messages = testerHistoryMessages(detail);
const meta = detail.metadata || {};
return (
Detalhes do teste
{detail.agent_name} - {testerHistoryDate(detail.created_at)} - {detail.mode}
Reabrir diagnóstico
Fechar
Status {detail.status}
Resultado {detail.quality_label || '--'}
Latência {detail.latency_ms != null ? `${detail.latency_ms} ms` : '--'}
Cenários {detail.scenarios_count || meta.transcript_count || 0}
Erros {detail.errors_count || 0}
Modelo {meta.model || '--'}
{detail.objective && Objetivo {detail.objective}
}
Metadados
Run #{detail.run_id || detail.id}
Agente {detail.agent_id ? `#${detail.agent_id}` : '--'}
Assistente {meta.assistant_kind || 'tester'}
Análise {meta.analysis_mode || detail.mode || '--'}
Resumo
{detail.summary || 'Sem resumo registrado.'}
Conversa completa
{messages.map((message, idx) => (
: message.role === 'system' ? null : 'T'}
meta={{message.scenario || ''}{message.latency_ms ? ` - ${message.latency_ms} ms` : ''} }
/>
))}
{!messages.length && Nenhuma mensagem registrada neste teste.
}
{!!asList(detail.output?.issues).length && }
);
}
function ReportList({ title, items, icon = 'logs', tone = 'neutral', empty = 'Sem itens.' }) {
const rawList = reportItems(items);
const list = rawList.length ? rawList : [empty];
const isEmpty = !rawList.length;
return (
{title}
{rawList.length} {rawList.length === 1 ? 'item' : 'itens'}
{list.map((item, idx) => {
const parts = reportItemParts(item, tone);
const priority = isEmpty ? '' : parts.priority;
return (
{parts.title}
{priority && {priority} }
{parts.body.map((line, lineIdx) => {line} )}
);
})}
);
}
function VersionHistory({ versions, onRestore }) {
const list = Array.isArray(versions) ? versions : [];
return (
Histórico reversível
Cada aplicação salva a versão anterior do agente.
{list.length ? list.slice(0, 6).map(version => (
{version.label || `Versão ${version.id}`}
{version.source} - {window.fmtShortDate ? window.fmtShortDate(version.created_at) : version.created_at}
onRestore(version)}>Restaurar
)) :
Nenhuma versão salva ainda.
}
);
}
window.ChannelsPage = function ChannelsPage() {
const [q, setQ] = useExtraState('');
const [qrByChannel, setQrByChannel] = useExtraState({});
const [connectingId, setConnectingId] = useExtraState(null);
const data = studioData();
const channels = (data.channels || []).filter(item => `${item.name} ${item.instance_name} ${item.agent_name || ''}`.toLowerCase().includes(q.toLowerCase()));
const online = channels.filter(item => channelStatus(item.instance_name) === 'online').length;
const agentOptions = [{ value: '', label: 'Selecione depois' }, ...(data.agents || []).map(agent => ({ value: String(agent.id), label: agent.name }))];
async function createChannel() {
const values = await studioForm({
title: 'Novo canal',
description: 'Escolha a instância WhatsApp, o agente responsável e o delay antes de conectar.',
submitLabel: 'Criar canal',
fields: [
{ name: 'name', label: 'Nome do canal', required: true },
{ name: 'instance_name', label: 'Identificador WhatsApp', helper: 'Vazio usa o nome do canal.' },
{ name: 'agent_id', label: 'Agente responsável', type: 'select', options: agentOptions },
{ name: 'description', label: 'Descrição', type: 'textarea' },
{ name: 'custom_prompt', label: 'Orientação específica', type: 'textarea' },
{ name: 'reply_delay', label: 'Delay em segundos', type: 'number', defaultValue: '0' },
],
});
if (!values) return;
const name = values.name.trim();
runAction(() => API.createChannel({
name,
instance_name: values.instance_name?.trim() || name,
description: values.description || '',
custom_prompt: values.custom_prompt || '',
reply_delay: Number(values.reply_delay || 0),
agent_id: values.agent_id ? Number(values.agent_id) : null,
}));
}
async function editChannel(channel) {
const values = await studioForm({
title: 'Alterar canal',
submitLabel: 'Salvar canal',
fields: [
{ name: 'name', label: 'Nome do canal', required: true, defaultValue: channel.name || '' },
{ name: 'instance_name', label: 'Identificador WhatsApp', defaultValue: channel.instance_name || '' },
{ name: 'agent_id', label: 'Agente responsável', type: 'select', defaultValue: channel.agent_id ? String(channel.agent_id) : '', options: agentOptions },
{ name: 'description', label: 'Descrição', type: 'textarea', defaultValue: channel.description || '' },
{ name: 'custom_prompt', label: 'Orientação específica', type: 'textarea', defaultValue: channel.custom_prompt || '' },
{ name: 'reply_delay', label: 'Delay em segundos', type: 'number', defaultValue: String(channel.reply_delay || 0) },
],
});
if (!values) return;
runAction(() => API.updateChannel(channel.id, {
name: values.name.trim(),
instance_name: values.instance_name?.trim() || values.name.trim(),
description: values.description || '',
custom_prompt: values.custom_prompt || '',
agent_id: values.agent_id ? Number(values.agent_id) : null,
reply_delay: Number(values.reply_delay || 0),
}));
}
async function connectChannel(channel) {
setConnectingId(channel.id);
try {
const result = await API.connectChannel(channel.id);
setQrByChannel(prev => ({ ...prev, [channel.id]: result }));
await refreshStudio();
studioToast('QR do canal gerado.');
} catch (err) {
setQrByChannel(prev => ({ ...prev, [channel.id]: { error: err.message || String(err) } }));
studioToast(err.message || String(err), 'bad');
} finally {
setConnectingId(null);
}
}
return (
<>
Novo canal}
/>
c.agent_id).length)}/>
{channels.map(channel => {
const st = channelStatusInfo(channel.instance_name);
const qr = qrByChannel[channel.id] || {};
const qrImage = qr.base64 || qr.qrcode || qr.qrCode || qr.code;
return (
W {channel.name}
{st.label}
Instância {channel.instance_name}
Responsável {channel.agent_name || agentName(channel.agent_id) || 'Não atribuído'}
Delay {channel.reply_delay || 0}s
{qrImage && String(qrImage).startsWith('data:image')
?
:
{qr.error || (st.status === 'online' ? 'Conectado. QR não necessário.' : 'Gere o QR para conectar este canal.')}
}
{channel.webhook}
editChannel(channel)}>Alterar
connectChannel(channel)} disabled={connectingId === channel.id}>{connectingId === channel.id ? 'Gerando...' : 'QR inline'}
runAction(() => API.disconnectChannel(channel.id))}>Desconectar WhatsApp
(await studioConfirm({ title:'Excluir canal', message:`Excluir ${channel.name}?`, confirmLabel:'Excluir', danger:true })) && runAction(() => API.deleteChannel(channel.id))}>Excluir
);
})}
{!channels.length &&
Nenhum canal encontrado. Crie um canal e vincule a um agente para receber mensagens reais. Novo canal
}
>
);
};
window.KnowledgePage = function KnowledgePage() {
const [q, setQ] = useExtraState('');
const [agentFilter, setAgentFilter] = useExtraState('');
const [selected, setSelected] = useExtraState(null);
const [kbSources, setKbSources] = useExtraState([]);
const [testQuery, setTestQuery] = useExtraState('');
const [testResult, setTestResult] = useExtraState(null);
const [testLoading, setTestLoading] = useExtraState(false);
const data = studioData();
const agentOptions = [{ value: '', label: 'Global / sem agente' }, ...(data.agents || []).map(agent => ({ value: String(agent.id), label: agent.name }))];
const knowledge = (data.knowledge || [])
.filter(item => !agentFilter || (agentFilter === '__global' ? !item.agent_id : String(item.agent_id || '') === agentFilter))
.filter(item => `${item.name} ${item.description || ''} ${item.tenant_id || ''} ${agentName(item.agent_id)}`.toLowerCase().includes(q.toLowerCase()));
const selectedKb = knowledge.find(item => String(item.id) === String(selected)) || knowledge[0] || null;
useExtraEffect(() => {
setTestResult(null);
if (!selectedKb?.id) { setKbSources([]); return; }
API.getKnowledgeBaseSources(selectedKb.id).then(setKbSources).catch(() => setKbSources([]));
}, [selectedKb?.id]);
async function createBase() {
const values = await studioForm({
title: 'Nova base de conhecimento',
description: 'Cria uma base geral. Vinculos por agente ficam no perfil do agente.',
submitLabel: 'Criar base',
fields: [
{ name: 'name', label: 'Nome da base', required: true },
{ name: 'agent_id', label: 'Vincular ao agente', type: 'select', options: agentOptions },
{ name: 'description', label: 'Descrição', type: 'textarea' },
{ name: 'tenant_id', label: 'Tenant', helper: 'Opcional.' },
],
});
if (!values) return;
runAction(() => API.createKnowledgeBase({
name: values.name.trim(),
description: values.description || 'Base geral criada pelo Studio',
agent_id: values.agent_id ? Number(values.agent_id) : null,
tenant_id: values.tenant_id || undefined,
}));
}
async function editBase(item) {
const values = await studioForm({
title: 'Alterar base de conhecimento',
submitLabel: 'Salvar base',
fields: [
{ name: 'name', label: 'Nome da base', required: true, defaultValue: item.name || '' },
{ name: 'agent_id', label: 'Vincular ao agente', type: 'select', defaultValue: item.agent_id ? String(item.agent_id) : '', options: agentOptions },
{ name: 'description', label: 'Descrição', type: 'textarea', defaultValue: item.description || '' },
{ name: 'tenant_id', label: 'Tenant', defaultValue: item.tenant_id || '' },
],
});
if (!values) return;
runAction(() => API.updateKnowledgeBase(item.id, {
name: values.name.trim(),
description: values.description || '',
agent_id: values.agent_id ? Number(values.agent_id) : null,
tenant_id: values.tenant_id || null,
}));
}
async function deleteBase(item) {
return (await studioConfirm({ title:'Excluir base', message:`Excluir ${item.name}? Fontes e chunks tambem seráo removidos.`, confirmLabel:'Excluir', danger:true })) && runAction(() => API.deleteKnowledgeBase(item.id));
}
async function runKbTest() {
if (!selectedKb || !testQuery.trim() || testLoading) return;
setTestLoading(true);
setTestResult(null);
try {
setTestResult(await API.testKnowledgeBase(selectedKb.id, { query: testQuery.trim() }));
} catch (err) {
setTestResult({ error: err.message || String(err), chunks: [] });
} finally {
setTestLoading(false);
}
}
return (
<>
Nova base}
/>
k.agent_id).length)}/>
!k.agent_id).length)}/>
k.agent_id).filter(Boolean)).size)}/>
{knowledge.map(item => (
setSelected(item.id)}>
{item.name}
{item.agent_id ? agentName(item.agent_id) : 'Sistema'} - {item.description || 'Sem descrição'}
{item.agent_id ? 'agente' : 'global'}
))}
{!knowledge.length &&
Nenhuma base encontrada.
}
Perfil da KB
{selectedKb ? selectedKb.name : 'selecione uma base'}
{selectedKb &&
editBase(selectedKb)}>Alterar deleteBase(selectedKb)}>Excluir
}
{selectedKb ? (
Descrição {selectedKb.description || 'Sem descrição'}
Fontes
Fonte Tipo Status Chunks
{kbSources.map(src => {src.uri} {src.source_type} {src.status} {src.chunk_count || 0} )}
{!kbSources.length && }
Teste de busca
setTestQuery(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') runKbTest(); }} placeholder="Digite uma pergunta para testar a recuperação da KB..."/>
{testLoading ? 'Testando...' : 'Testar KB'}
{testResult?.error &&
{testResult.error}
}
{testResult && !testResult.error && (
Modo: {testResult.retrieval_mode || 'keyword_fallback'} - confiança {Math.round(Number(testResult.confidence || 0) * 100)}%
{(testResult.chunks || []).map(chunk => (
{chunk.title || 'Fonte'}
{chunk.uri || 'base interna'} {chunk.score != null ? `- score ${Number(chunk.score).toFixed(2)}` : ''}
{chunk.snippet || 'Sem trecho retornado.'}
))}
{!(testResult.chunks || []).length &&
Nenhum trecho retornado para esta pergunta.
}
)}
) :
Selecione uma KB.
}
>
);
};
window.ToolsPage = function ToolsPage() {
const [q, setQ] = useExtraState('');
const [selectedScope, setSelectedScope] = useExtraState('all');
const data = studioData();
const allTools = data.tools || [];
const systemTools = allTools.filter(item => !item.agent_id);
const agentScopes = (data.agents || [])
.map(agent => ({
id: `agent:${agent.id}`,
label: agent.name,
sub: agent.role || agent.channel || 'Agente',
count: allTools.filter(tool => String(tool.agent_id) === String(agent.id)).length,
agentId: agent.id,
}))
.filter(item => item.count > 0 || `${item.label} ${item.sub}`.toLowerCase().includes(q.toLowerCase()));
const scopes = [
{ id: 'all', label: 'Todas ferramentas', sub: 'Globais e por agente', count: allTools.length },
{ id: 'system', label: 'Sistema', sub: 'Ferramentas globais', count: systemTools.length },
...agentScopes,
];
const scopedTools = selectedScope === 'all'
? allTools
: selectedScope === 'system'
? systemTools
: selectedScope?.startsWith('agent:')
? allTools.filter(item => String(item.agent_id) === selectedScope.slice(6))
: [];
const tools = scopedTools.filter(item => `${item.name} ${item.tool_type} ${item.description || ''} ${agentName(item.agent_id)}`.toLowerCase().includes(q.toLowerCase()));
const selectedLabel = scopes.find(item => item.id === selectedScope)?.label || 'Selecione um escopo';
async function createTool() {
const values = await studioForm({
title: 'Nova ferramenta do sistema',
description: 'Cria uma ferramenta global. Ferramentas usadas por agente ficam no perfil do agente.',
submitLabel: 'Criar ferramenta',
fields: [
{ name: 'name', label: 'Nome', required: true },
{ name: 'agent_id', label: 'Agente que pode usar', type: 'select', required: true, options: (data.agents || []).map(agent => ({ value: String(agent.id), label: agent.name })) },
{ name: 'tool_type', label: 'Tipo', type: 'select', defaultValue: 'knowledge_base', options: [
{ value: 'web_search', label: 'web_search' },
{ value: 'knowledge_base', label: 'knowledge_base' },
{ value: 'data_table', label: 'data_table' },
{ value: 'http_api', label: 'http_api' },
{ value: 'python_runtime', label: 'python_runtime' },
{ value: 'ticket_creation', label: 'ticket_creation' },
{ value: 'contextual_memory', label: 'contextual_memory' },
]},
{ name: 'description', label: 'Descrição', type: 'textarea' },
],
});
if (!values) return;
runAction(() => API.createTool({
name: values.name.trim(),
agent_id: Number(values.agent_id),
tool_type: values.tool_type,
description: values.description || '',
enabled: true,
}));
}
async function editTool(tool) {
const values = await studioForm({
title: 'Alterar ferramenta',
submitLabel: 'Salvar ferramenta',
fields: [
{ name: 'name', label: 'Nome', required: true, defaultValue: tool.name || '' },
{ name: 'tool_type', label: 'Tipo', type: 'select', defaultValue: tool.tool_type || 'knowledge_base', options: [
{ value: 'web_search', label: 'web_search' },
{ value: 'knowledge_base', label: 'knowledge_base' },
{ value: 'data_table', label: 'data_table' },
{ value: 'http_api', label: 'http_api' },
{ value: 'python_runtime', label: 'python_runtime' },
{ value: 'ticket_creation', label: 'ticket_creation' },
{ value: 'contextual_memory', label: 'contextual_memory' },
]},
{ name: 'description', label: 'Descrição', type: 'textarea', defaultValue: tool.description || '' },
],
});
if (!values) return;
runAction(() => API.updateTool(tool.id, {
name: values.name.trim(),
tool_type: values.tool_type || tool.tool_type,
description: values.description || '',
}));
}
return (
<>
Nova ferramenta}
/>
{[
['knowledge_base', 'Busca em documentos, URLs e textos indexados.'],
['http_api', 'Chama CRM, ERP ou sistemas externos com parametros controlados.'],
['data_table', 'Consulta e grava dados nas tabelas internas.'],
['python_runtime', 'Executa logica e calculos customizados quando hábilitado.'],
].map(([type, description]) => (
{type}
{(data.tools || []).filter(t => t.tool_type === type).length}
{description}
catalogo
))}
{scopes.map(scope => (
setSelectedScope(scope.id)}>
{scope.label}
{scope.sub}
{scope.count}
))}
Ferramentas
{selectedLabel}
{selectedScope &&
{tools.length} visiveis }
{selectedScope ? (
{tools.map(tool => (
{tool.tool_type}
{tool.enabled ? 'ativa' : 'inativa'}
{tool.name}
{tool.description || 'Sem descrição'}
Escopo {tool.agent_id ? agentName(tool.agent_id) : 'Sistema'}
Aprovação {tool.requires_approval ? 'sim' : 'não'}
editTool(tool)}>Alterar
runAction(() => API.updateTool(tool.id, { enabled: !tool.enabled }))}>{tool.enabled ? 'Pausar' : 'Ativar'}
(await studioConfirm({ title:'Excluir ferramenta', message:`Excluir ${tool.name}?`, confirmLabel:'Excluir', danger:true })) && runAction(() => API.deleteTool(tool.id))}>Excluir
))}
{!tools.length &&
Nenhuma ferramenta neste escopo.
}
) : (
Escolha um agente, Sistema ou Todas ferramentas para ver detalhes.
)}
>
);
};
const SQUAD_TEMPLATES = {
suporte: { name: 'Suporte Escalonado', mode: 'hierarchical', config: { template: 'suporte', expected_roles: ['triagem', 'especialista', 'supervisor'] } },
vendas: { name: 'Vendas Consultivas', mode: 'hierarchical', config: { template: 'vendas', expected_roles: ['qualificador', 'consultor', 'closer'] } },
geral: { name: 'Atendimento Geral', mode: 'horizontal', config: { template: 'geral', expected_roles: ['suporte', 'vendas', 'técnico'] } },
};
window.SquadsPage = function SquadsPage() {
const data = studioData();
const [selected, setSelected] = useExtraState(null);
const [detail, setDetail] = useExtraState(null);
useExtraEffect(() => {
if (!selected) { setDetail(null); return; }
API.get(`/api/squads/${selected}`).then(setDetail).catch(() => setDetail(null));
}, [selected]);
async function createSquad() {
const values = await studioForm({
title: 'Novo squad',
submitLabel: 'Criar squad',
fields: [
{ name: 'template', label: 'Template', type: 'select', defaultValue: 'geral', options: [
{ value: 'suporte', label: 'Suporte Escalonado' },
{ value: 'vendas', label: 'Vendas Consultivas' },
{ value: 'geral', label: 'Atendimento Geral' },
]},
{ name: 'name', label: 'Nome do squad', required: true },
{ name: 'mode', label: 'Modo', type: 'select', defaultValue: 'hierarchical', options: [
{ value: 'hierarchical', label: 'Hierarquico' },
{ value: 'horizontal', label: 'Horizontal' },
]},
{ name: 'manager_agent_id', label: 'Gerente', type: 'select', options: [{ value:'', label:'Selecione depois' }, ...(data.agents || []).map(agent => ({ value:String(agent.id), label:agent.name }))] },
{ name: 'fallback_agent_id', label: 'Fallback', type: 'select', options: [{ value:'', label:'Selecione depois' }, ...(data.agents || []).map(agent => ({ value:String(agent.id), label:agent.name }))] },
],
});
if (!values) return;
const template = SQUAD_TEMPLATES[values.template] || SQUAD_TEMPLATES.geral;
runAction(() => API.createSquad({
name: values.name.trim() || template.name,
mode: values.mode || template.mode,
manager_agent_id: values.manager_agent_id ? Number(values.manager_agent_id) : null,
fallback_agent_id: values.fallback_agent_id ? Number(values.fallback_agent_id) : null,
config: template.config,
}));
}
function openSquadProfile(id) {
setSelected(id);
}
function deleteSquad(id) {
return runAction(() => API.deleteSquad(id));
}
async function editSquad() {
if (!detail) return;
const values = await studioForm({
title: 'Alterar squad',
submitLabel: 'Salvar squad',
fields: [
{ name: 'name', label: 'Nome do squad', required: true, defaultValue: detail.name || '' },
{ name: 'mode', label: 'Modo', type: 'select', defaultValue: detail.mode || 'hierarchical', options: [
{ value: 'hierarchical', label: 'Hierarquico' },
{ value: 'horizontal', label: 'Horizontal' },
]},
{ name: 'manager_agent_id', label: 'Gerente', type: 'select', defaultValue: detail.manager_agent_id ? String(detail.manager_agent_id) : '', options: [{ value:'', label:'Sem gerente' }, ...(data.agents || []).map(agent => ({ value:String(agent.id), label:agent.name }))] },
{ name: 'fallback_agent_id', label: 'Fallback', type: 'select', defaultValue: detail.fallback_agent_id ? String(detail.fallback_agent_id) : '', options: [{ value:'', label:'Sem fallback' }, ...(data.agents || []).map(agent => ({ value:String(agent.id), label:agent.name }))] },
],
});
if (!values) return;
runAction(async () => {
await API.updateSquad(selected, {
name: values.name.trim(),
mode: values.mode || 'hierarchical',
manager_agent_id: values.manager_agent_id ? Number(values.manager_agent_id) : null,
fallback_agent_id: values.fallback_agent_id ? Number(values.fallback_agent_id) : null,
});
setDetail(await API.get(`/api/squads/${selected}`));
});
}
function addMember() {
if (!selected) return;
const agentId = firstAgentId();
if (!agentId) {
studioToast('Crie um agente antes.', 'bad');
return;
}
runAction(async () => {
await API.post(`/api/squads/${selected}/members`, { agent_id: agentId, role: 'specialist', priority: 100 });
setDetail(await API.get(`/api/squads/${selected}`));
});
}
window.openSquadProfile = openSquadProfile;
window.deleteSquad = deleteSquad;
return (
<>
Novo squad}
/>
{(data.squads || []).map(squad => (
openSquadProfile(squad.id)}>
{squad.name}
{squad.mode} - {squad.member_count || 0} membros - manager {squad.manager_agent_name || '--'}
{squad.fallback_agent_name || 'sem fallback'}
))}
{!(data.squads || []).length &&
Nenhum squad criado.
}
Perfil do squad
{selected &&
Alterar
(await studioConfirm({ title:'Excluir squad', message:'Excluir este squad?', confirmLabel:'Excluir', danger:true })) && deleteSquad(selected)}>Excluir
}
{detail ? (
Modo {detail.mode}
Manager {detail.manager_agent_name || '--'}
Fallback {detail.fallback_agent_name || '--'}
Gerente {detail.manager_agent_name || 'Não definido'}
distribui para
{(detail.members || []).map(member => (
{member.role || 'specialist'} - P{member.priority || 100}
{member.agent_name || `Agente ${member.agent_id}`}
{member.routing_description || 'Sem regra de roteamento.'}
))}
{!(detail.members || []).length &&
Membros Nenhum agente
}
Fallback {detail.fallback_agent_name || 'Não definido'}
{[
[!!detail.manager_agent_id, 'Tem gerente'],
[(detail.members || []).length >= 2, 'Tem 2+ membros'],
[!!detail.fallback_agent_id, 'Tem fallback'],
[(detail.members || []).every(member => member.routing_description), 'Membros tem roteamento'],
].map(([done, label]) => (
{label}
))}
e.target.value && runAction(async () => {
await API.addMember(selected, { agent_id: Number(e.target.value), role: 'specialist', priority: 100 });
setDetail(await API.get(`/api/squads/${selected}`));
e.target.value = '';
})}>
Adicionar agente ao Squad
{(data.agents || []).map(agent => {agent.name} )}
Adicionar primeiro agente disponível
{(detail.members || []).map(member => (
{member.agent_name} {member.role} - prioridade {member.priority}
runAction(async () => {
await API.del(`/api/squad-members/${member.id}`);
setDetail(await API.get(`/api/squads/${selected}`));
})}>Remover
))}
) :
Selecione um squad.
}
>
);
};
const TABLE_TEMPLATES = {
clients: { name: 'Cadastro de Clientes', fields: ['nome', 'telefone', 'email', 'empresa', 'data_cadastro'] },
orders: { name: 'Registro de Pedidos', fields: ['cliente', 'produto', 'valor', 'status', 'data'] },
faq: { name: 'FAQ', fields: ['pergunta', 'resposta', 'categoria'] },
support: { name: 'Controle de Atendimentos', fields: ['protocolo', 'assunto', 'status', 'responsável', 'data'] },
};
window.TablesPage = function TablesPage() {
const data = studioData();
const [selected, setSelected] = useExtraState(null);
const [detail, setDetail] = useExtraState(null);
const [fullscreen, setFullscreen] = useExtraState(false);
useExtraEffect(() => {
if (!selected && (data.tables || []).length) setSelected((data.tables || [])[0].id);
}, [(data.tables || []).length, selected]);
useExtraEffect(() => {
if (!selected) { setDetail(null); return; }
API.get(`/api/data-tables/${selected}`).then(setDetail).catch(() => setDetail(null));
}, [selected]);
useExtraEffect(() => {
setFullscreen(false);
}, [selected]);
async function createTable() {
const values = await studioForm({
title: 'Nova tabela',
submitLabel: 'Criar tabela',
fields: [
{ name: 'template', label: 'Template', type: 'select', defaultValue: 'clients', options: [
{ value: 'clients', label: 'Cadastro de Clientes' },
{ value: 'orders', label: 'Registro de Pedidos' },
{ value: 'faq', label: 'FAQ / Perguntas Frequentes' },
{ value: 'support', label: 'Controle de Atendimentos' },
]},
{ name: 'name', label: 'Nome da tabela', required: true },
{ name: 'description', label: 'Descrição', type: 'textarea' },
],
});
if (!values) return;
const template = TABLE_TEMPLATES[values.template] || TABLE_TEMPLATES.clients;
runAction(() => API.createTable({
name: values.name.trim() || template.name,
description: values.description || template.name,
schema_json: { description: values.description || template.name, fields: template.fields.map(name => ({ name, type: name === 'valor' ? 'number' : 'text' })) },
records: [],
}));
}
function openTableCreateModal() {
createTable();
}
async function deleteSelectedTable() {
if (!selected || !(await studioConfirm({ title:'Excluir tabela', message:'Excluir tabela selecionada?', confirmLabel:'Excluir', danger:true }))) return Promise.resolve();
return runAction(async () => {
await API.deleteTable(selected);
setSelected(null);
setDetail(null);
});
}
async function editSelectedTable() {
if (!selected || !detail) return;
const values = await studioForm({
title: 'Alterar tabela',
submitLabel: 'Salvar tabela',
fields: [
{ name:'name', label:'Nome da tabela', required:true, defaultValue: detail.name || '' },
{ name:'description', label:'Descrição', type:'textarea', defaultValue: detail.schema_json?.description || detail.description || '' },
{ name:'tenant_id', label:'Tenant', defaultValue: detail.tenant_id || '' },
],
});
if (!values) return;
runAction(async () => {
await API.updateTable(selected, { name: values.name.trim(), description: values.description || '', tenant_id: values.tenant_id || null });
setDetail(await API.get(`/api/data-tables/${selected}`));
});
}
async function addColumn() {
if (!selected) return;
const values = await studioForm({ title:'Adicionar coluna', submitLabel:'Adicionar', fields:[
{ name:'name', label:'Nome da coluna', required:true },
{ name:'type', label:'Tipo', type:'select', defaultValue:'text', options:[
{ value:'text', label:'Texto' },
{ value:'number', label:'Número' },
{ value:'date', label:'Data' },
{ value:'boolean', label:'Booleano' },
]},
]});
if (!values) return;
runAction(async () => {
await API.addColumn(selected, { name: values.name.trim(), type: values.type || 'text' });
setDetail(await API.get(`/api/data-tables/${selected}`));
});
}
async function addRecord() {
if (!selected) return;
const values = await studioForm({ title:'Adicionar linha', description:'Informe JSON simples para criar a linha.', submitLabel:'Adicionar linha', fields:[
{ name:'data', label:'Dados da linha', type:'textarea', rows:8, defaultValue:'{"nome": "Novo registro"}', required:true },
]});
if (!values) return;
runAction(async () => {
await API.addRecord(selected, { data: JSON.parse(values.data) });
setDetail(await API.get(`/api/data-tables/${selected}`));
});
}
const columns = fieldNames(detail);
const displayColumns = pickDisplayColumns(columns);
const hiddenColumnCount = Math.max(0, columns.length - displayColumns.length);
const tableDescription = detail?.schema_json?.description || detail?.description || 'Sem descricao registrada.';
window.openTableCreateModal = openTableCreateModal;
window.deleteSelectedTable = deleteSelectedTable;
return (
<>
Nova tabela}
/>
Perfis de tabelas
{(data.tables || []).length} tabelas cadastradas
{(data.tables || []).map(table => {
const count = fieldNames(table).length;
const active = String(selected) === String(table.id);
return (
setSelected(table.id)}>
Tabela
{table.name}
{table.description || table.schema_json?.description || 'Dados operacionais'}
{count} colunas · {table.record_count || 0} linhas
);
})}
{!(data.tables || []).length &&
Nenhuma tabela criada.
}
{detail ? detail.name : 'Tabela'}
{detail ? `${columns.length} colunas - ${(detail.records || []).length} linhas carregadas` : 'selecione uma tabela'}
{detail &&
setFullscreen(!fullscreen)}>{fullscreen ? 'Sair da tela cheia' : 'Tela cheia'} Alterar tabela Excluir tabela Adicionar coluna Adicionar linha
}
{detail ? (
<>
Descricao {tableDescription}
Colunas {columns.length}
Linhas {(detail.records || []).length}
Campos visiveis {displayColumns.length}{hiddenColumnCount ? ` + ${hiddenColumnCount}` : ''}
{displayColumns.map(col => {col} )}{hiddenColumnCount > 0 && Extras }
{(detail.records || []).map(record => (
{displayColumns.map(col => {
const raw = (record.data || {})[col];
return {truncateText(raw, fullscreen ? 900 : 260)} ;
})}
{hiddenColumnCount > 0 && +{hiddenColumnCount} colunas }
(await studioConfirm({ title:'Excluir linha', message:'Excluir esta linha?', confirmLabel:'Excluir', danger:true })) && runAction(async () => {
await API.deleteRecord(record.id);
setDetail(await API.get(`/api/data-tables/${selected}`));
})}>Excluir
))}
{!(detail.records || []).length && }
>
) :
Selecione uma tabela para editar.
}
>
);
};
window.LogsPage = function LogsPage() {
const [mode, setMode] = useExtraState('runs');
const data = studioData();
const runs = data.runs || [];
const events = data.events || [];
return (
<>
{runs.length + events.length} registros}
/>
setMode('runs')}>Runs {runs.length}
setMode('events')}>Eventos {events.length}
{mode === 'runs' ? (
Hora Agente Conversa Status Latência Erro
{runs.slice(0, 200).map(run => (
{window.fmtTime ? window.fmtTime(run.created_at) : '--'}
{agentName(run.agent_id)}
{run.conversation_id || '--'}
{run.status || 'ok'}
{run.latency_ms ? `${run.latency_ms} ms` : '--'}
{run.error || '--'}
))}
{!runs.length && }
) : (
Hora Instância Evento Cliente Status Mensagem
{events.slice(0, 200).map(event => (
{window.fmtTime ? window.fmtTime(event.created_at) : '--'}
{event.instance || '--'}
{event.event || '--'}
{event.customer_phone || '--'}
{event.status || 'ok'}
{event.error || event.message_text || event.audio_transcription || '--'}
))}
{!events.length && }
)}
>
);
};
window.TeamPage = function TeamPage() {
const data = studioData();
const [selectedType, setSelectedType] = useExtraState('team');
const [selectedId, setSelectedId] = useExtraState(null);
const teams = data.teams || [];
const attendants = data.attendants || [];
const selectedTeam = teams.find(team => String(team.id) === String(selectedId)) || teams[0] || null;
const selectedAttendant = attendants.find(att => String(att.id) === String(selectedId)) || attendants[0] || null;
const profile = selectedType === 'attendant' ? selectedAttendant : selectedTeam;
const profileTeamAttendants = selectedType === 'attendant' ? attendants.filter(att => att.team_id === profile?.team_id) : [];
const profileTeamWeight = profileTeamAttendants.reduce((sum, att) => sum + Number(att.weight || 1), 0) || 1;
const profileWeightShare = selectedType === 'attendant' && profile ? Math.round((Number(profile.weight || 1) / profileTeamWeight) * 100) : 0;
useExtraEffect(() => {
if (selectedId) return;
if (teams[0]) { setSelectedType('team'); setSelectedId(teams[0].id); return; }
if (attendants[0]) { setSelectedType('attendant'); setSelectedId(attendants[0].id); }
}, [teams.length, attendants.length]);
async function createTeam() {
const values = await studioForm({ title:'Novo time', submitLabel:'Criar time', fields:[
{ name:'name', label:'Nome do time', required:true },
{ name:'description', label:'Descrição', type:'textarea' },
]});
if (!values) return;
runAction(() => API.createTeam({ name: values.name.trim(), description: values.description || 'Time criado pelo Studio' }));
}
async function editTeam(team) {
const values = await studioForm({ title:'Alterar time', submitLabel:'Salvar time', fields:[
{ name:'name', label:'Nome do time', required:true, defaultValue: team.name || '' },
{ name:'description', label:'Descrição', type:'textarea', defaultValue: team.description || '' },
]});
if (!values) return;
runAction(() => API.updateTeam(team.id, { name: values.name.trim(), description: values.description || '' }));
}
async function createAttendant() {
const team = (data.teams || [])[0];
if (!team) return studioToast('Crie um time antes.', 'bad');
const values = await studioForm({ title:'Novo atendente', submitLabel:'Criar atendente', fields:[
{ name:'name', label:'Nome do atendente', required:true },
{ name:'email', label:'Email' },
{ name:'team_id', label:'Time', type:'select', defaultValue:String(team.id), options:(data.teams || []).map(t => ({ value:String(t.id), label:t.name })) },
{ name:'weight', label:'Peso', type:'number', defaultValue:'1' },
]});
if (!values) return;
runAction(() => API.createAttendant({ name: values.name.trim(), email: values.email || undefined, team_id: Number(values.team_id), available: true, weight: Number(values.weight || 1) }));
}
async function editAttendant(att) {
const values = await studioForm({ title:'Alterar atendente', submitLabel:'Salvar atendente', fields:[
{ name:'name', label:'Nome do atendente', required:true, defaultValue: att.name || '' },
{ name:'email', label:'Email', defaultValue: att.email || '' },
{ name:'team_id', label:'Time', type:'select', defaultValue:String(att.team_id || (data.teams || [])[0]?.id || ''), options:(data.teams || []).map(t => ({ value:String(t.id), label:t.name })) },
{ name:'weight', label:'Peso', type:'number', defaultValue:String(att.weight || 1) },
]});
if (!values) return;
runAction(() => API.updateAttendant(att.id, { name: values.name.trim(), email: values.email || undefined, team_id: Number(values.team_id), weight: Number(values.weight || 1) }));
}
async function deleteAttendant(id) {
return (await studioConfirm({ title:'Excluir atendente', message:'Excluir atendente?', confirmLabel:'Excluir', danger:true })) && runAction(() => API.deleteAttendant(id));
}
window.deleteAttendant = deleteAttendant;
return (
<>
Novo time Novo atendente }
/>
{teams.map(team => (
{ setSelectedType('team'); setSelectedId(team.id); }}>
{team.name}
{team.description || 'Sem descrição'}
{attendants.filter(a => a.team_id === team.id).length} atendentes
))}
{!teams.length &&
Nenhum time criado.
}
Atendentes
{attendants.length}
{attendants.map(att => (
{ setSelectedType('attendant'); setSelectedId(att.id); }}>
{att.name} {att.team_name || '--'} - peso {att.weight}
{att.available ? 'disponível' : 'pausado'}
))}
{!attendants.length &&
Nenhum atendente criado.
}
{selectedType === 'attendant' ? 'Perfil do atendente' : 'Perfil do time'}
{profile?.name || 'selecione um item'}
{profile &&
{selectedType === 'attendant'
? <> editAttendant(profile)}>Alterar runAction(() => API.updateAttendant(profile.id, { available: !profile.available }))}>{profile.available ? 'Pausar' : 'Ativar'} deleteAttendant(profile.id)}>Excluir >
: editTeam(profile)}>Alterar }
}
{profile ? selectedType === 'attendant' ? (
Email {profile.email || '--'}
Distribuicao round-robin {profileWeightShare}% estimado
Peso {profile.weight || 1} dentro do time {profile.team_name || '--'}.
ID {profile.id}
) : (
a.team_id === profile.id).length)}/>
a.team_id === profile.id && a.available).length)}/>
Descrição {profile.description || 'Sem descrição'}
Atendentes do time
{attendants.filter(a => a.team_id === profile.id).map(att =>
{ setSelectedType('attendant'); setSelectedId(att.id); }}>{att.name} {att.email || '--'} {att.available ? 'online' : 'pausado'} )}
{!attendants.filter(a => a.team_id === profile.id).length &&
Time sem atendentes.
}
) :
Selecione um time ou atendente.
}
>
);
};
window.TagsPage = function TagsPage() {
const data = studioData();
async function createTag() {
const values = await studioForm({ title:'Nova tag', submitLabel:'Criar tag', fields:[
{ name:'name', label:'Nome da tag', required:true },
{ name:'color', label:'Cor hex', defaultValue:'#1d4ed8' },
{ name:'description', label:'Descrição', type:'textarea' },
]});
if (!values) return;
runAction(() => API.createTag({ name: values.name.trim(), color: values.color || '#1d4ed8', description: values.description || 'Tag criada pelo Studio' }));
}
async function editTag(tag) {
const values = await studioForm({ title:'Alterar tag', submitLabel:'Salvar tag', fields:[
{ name:'name', label:'Nome da tag', required:true, defaultValue: tag.name || '' },
{ name:'color', label:'Cor hex', defaultValue: tag.color || '#1d4ed8' },
{ name:'description', label:'Descrição', type:'textarea', defaultValue: tag.description || '' },
]});
if (!values) return;
runAction(() => API.updateTag(tag.id, { name: values.name.trim(), color: values.color || '#1d4ed8', description: values.description || '' }));
}
async function deleteTag(id) {
return (await studioConfirm({ title:'Excluir tag', message:'Excluir tag?', confirmLabel:'Excluir', danger:true })) && runAction(() => API.deleteTag(id));
}
window.deleteTag = deleteTag;
return (
<>
Nova tag}
/>
{(data.tags || []).map(tag => (
{tag.name}
{tag.description || 'Sem descrição'}
editTag(tag)}>Alterar
deleteTag(tag.id)}>Excluir
))}
{!(data.tags || []).length &&
Nenhuma tag criada.
}
>
);
};