// 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 ? (

{ 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}
/>
)}
{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 && (
| Horário |
Duração |
Tokens |
Custo |
Status |
Erro |
{rows.map(r => (
|
{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;