// OrgChart — React Flow central view + AgentDrawer + ExecutionsDrawer // D-01: uses window.jsxRuntime shim (loaded before this script in Dashboard.html) // D-02: agents grouped heuristically by role/traits into categories // ── Status → border color ────────────────────────────────────────────────── const STATUS_COLORS = { idle: '#7BC773', running: '#7B9DEC', stale: '#F0B45C', error: '#EE6E6E', offline: '#7C8592', }; const statusColor = (s) => STATUS_COLORS[s] || STATUS_COLORS.idle; // ── Category heuristic (D-02) ───────────────────────────────────────────── const deriveCategory = (agent) => { const hay = ((agent.role || '') + ' ' + (agent.traits || []).join(' ')).toLowerCase(); if (/e[-\s]?mail|inbox|gmail/.test(hay)) return 'Inbox'; if (/briefing|matinal|diário|diario/.test(hay)) return 'Briefing'; if (/pesquisa|mídia|media|notícia|noticia/.test(hay)) return 'Pesquisa'; if (/aula|apresenta|slide|powerpoint/.test(hay)) return 'Conteúdo'; if (/clínico|clinico|onco|paciente/.test(hay)) return 'Clínico'; return 'Outros'; }; // Category order determines row Y const CATEGORY_ORDER = ['Briefing', 'Inbox', 'Pesquisa', 'Conteúdo', 'Clínico', 'Outros']; const EMPTY_AGENTS = []; // ── Hand-coded dependency edges (D-02 note: future deps from I/O observado) ── // Format: [sourceId, targetId] const STATIC_EDGES_RAW = [ // leader → all category representatives (will be expanded from agents list) ]; // ── Layout helpers ──────────────────────────────────────────────────────── const NODE_W = 200; const NODE_H = 90; const H_GAP = 220; const V_GAP = 190; const buildLayout = (agents) => { // Group by category const groups = {}; CATEGORY_ORDER.forEach(c => { groups[c] = []; }); agents.forEach(a => { const cat = deriveCategory(a); if (!groups[cat]) groups[cat] = []; groups[cat].push(a); }); const nodes = []; const edges = []; // Leader node const totalCols = Math.max( ...Object.values(groups).map(g => g.length), 1 ); const leaderX = ((totalCols - 1) * H_GAP) / 2; nodes.push({ id: '_leader', type: 'agentNode', position: { x: leaderX, y: 0 }, data: { id: '_leader', name: 'Vinícius', role: 'Orquestrador', status: 'idle', statusLabel: 'Líder', photo: null, avatarColor: '#B589E0', pendingApprovals: 0, lastRun: '', cadence: null, isLeader: true, model: 'none', }, }); let rowIdx = 0; CATEGORY_ORDER.forEach(cat => { const group = groups[cat]; if (!group || group.length === 0) return; const rowY = (rowIdx + 1) * V_GAP; const groupW = (group.length - 1) * H_GAP; const startX = leaderX - groupW / 2; group.forEach((agent, colIdx) => { const x = startX + colIdx * H_GAP; nodes.push({ id: agent.id, type: 'agentNode', position: { x, y: rowY }, data: { ...agent, cadence: null, // enriched externally via crons }, }); // every agent reports to the leader — no floating nodes edges.push({ id: `_leader->${agent.id}`, source: '_leader', target: agent.id, style: { stroke: '#3A3F4C', strokeWidth: 1.5 }, animated: false, }); }); rowIdx++; }); return { nodes, edges }; }; // ── Custom Agent Node ───────────────────────────────────────────────────── const AgentNode = ({ data, selected }) => { const bcolor = statusColor(data.status); const FlowHandle = window.ReactFlow && window.ReactFlow.Handle; const Badge = window.ModelBadge; return (
{FlowHandle && ( <> )} {/* Avatar */}
{data.photo ? ( {data.name} { e.currentTarget.style.display = 'none'; }} /> ) : (
{typeof window.initials === 'function' ? window.initials(data.name) : data.name[0]}
)}
{/* Name + Role */}
{data.name}
{!data.isLeader && Badge && (
)} {!data.isLeader && (
{data.cadence || 'on-demand'}
)}
{/* Pending badge */} {data.pendingApprovals > 0 && (
{data.pendingApprovals}
)}
); }; // Close on Escape — shared by both drawers (must run unconditionally; safe even if onClose is undefined) const useEscapeToClose = (onClose) => { React.useEffect(() => { if (typeof onClose !== 'function') return; const onKey = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onClose]); }; // ── AgentDrawer ─────────────────────────────────────────────────────────── const AgentDrawer = ({ agent, crons, onClose, onToast, onStatClick, onRunAgent, onToggleCron, onDeleteCron, onAddCron, isRunning = false }) => { useEscapeToClose(onClose); if (!agent) return null; const Hero = window.AgentHero; const Stats = window.StatStrip; const Profiles = window.ProfileCards; const Crons = window.CronSection; const noop = () => {}; return ( <>
{agent.name}
{Hero && ( )} {Stats && ( onStatClick(agent.id) : undefined} /> )}
{Profiles && }
{Crons && ( onAddCron(text, expr, agent.id) : noop} /> )}
); }; // ── ExecutionsDrawer ────────────────────────────────────────────────────── const ExecutionsDrawer = ({ agentId, onClose }) => { const [rows, setRows] = React.useState(null); const [err, setErr] = React.useState(null); React.useEffect(() => { if (!agentId) return; setRows(null); setErr(null); const tok = localStorage.getItem('hub_token'); fetch(`/api/v1/agents/${agentId}/executions?limit=50`, { headers: { Authorization: `Bearer ${tok}` }, }) .then(r => { if (!r.ok) throw new Error(r.status); return r.json(); }) .then(data => setRows(Array.isArray(data) ? data : [])) .catch(e => setErr('Erro ao carregar execuções: ' + e.message)); }, [agentId]); useEscapeToClose(onClose); if (!agentId) return null; const relTime = (ms) => { if (!ms) return '—'; if (ms < 1000) return ms + 'ms'; return (ms / 1000).toFixed(1) + 's'; }; return ( <>
Execuções recentes
{err &&
{err}
} {!err && rows === null && (
Carregando...
)} {!err && rows !== null && rows.length === 0 && (
Nenhuma execução registrada ainda.
O audit log está sendo construído — fica disponível em breve.
)} {!err && rows !== null && rows.length > 0 && ( {rows.map(r => ( ))}
Horário Duração Tokens Custo Status Erro
{r.started_at ? new Date(r.started_at).toLocaleString('pt-BR') : '—'} {relTime(r.duration_ms)} {(r.tokens_in != null || r.tokens_out != null) ? ((r.tokens_in || 0) + (r.tokens_out || 0)).toLocaleString('pt-BR') : '—'} {r.cost_cents != null ? '$' + (r.cost_cents / 100).toFixed(2) : '—'} {r.status || '—'} {r.error || '—'}
)}
); }; // ── OrgChart (main export) ──────────────────────────────────────────────── const OrgChart = ({ agents, crons, onSelect, setChatScope }) => { const RF = window.ReactFlow; const safeAgents = Array.isArray(agents) ? agents : EMPTY_AGENTS; // Build cadence map from crons const cadenceByAgent = React.useMemo(() => { const map = {}; if (!Array.isArray(crons)) return map; crons.forEach(c => { const aid = c.agentId || c.agent_id; if (!aid || !c.enabled) return; if (!map[aid]) map[aid] = c.natural || c.expr; }); return map; }, [crons]); const { nodes: baseNodes, edges } = React.useMemo(() => buildLayout(safeAgents), [safeAgents]); // Inject cadence into node data const nodes = React.useMemo(() => baseNodes.map(n => ({ ...n, data: { ...n.data, cadence: cadenceByAgent[n.data.id] || null, }, })), [baseNodes, cadenceByAgent]); const nodeTypes = React.useMemo(() => ({ agentNode: AgentNode }), []); const onNodeClick = React.useCallback((event, node) => { if (node.id === '_leader') return; if (typeof onSelect === 'function') onSelect(node.id); if (typeof setChatScope === 'function') { setChatScope({ type: 'agent', agentId: node.id }); } }, [onSelect, setChatScope]); if (safeAgents.length === 0) { return (
Catálogo vazio

Registrar primeiro agente

Nenhum agente real foi retornado por /api/v1/agents. Configure um app com AGENT_HUB_URL, AGENT_HUB_API_KEY e AGENT_ID, ou rode o mirror launchd.

from agent_hub_sdk import register_agent
); } if (!RF) { return (
React Flow não carregou. Verifique a conexão com o CDN.
); } const { ReactFlow, Background, Controls } = RF; return (
); }; window.OrgChart = OrgChart; window.AgentDrawer = AgentDrawer; window.ExecutionsDrawer = ExecutionsDrawer;