// Main App const useEscapeToClose = (active, onClose) => { React.useEffect(() => { if (!active) return; const handler = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [active, onClose]); }; window.useEscapeToClose = useEscapeToClose; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "density": "cozy", "accent": "blue", "showStats": true }/*EDITMODE-END*/; const ACCENT_COLORS = { blue: "#7B9DEC", green: "#7BC773", purple: "#B589E0", amber: "#F0B45C", }; const AGENT_DEFAULTS = { avatarColor: '#7B9DEC', description: '', personality: '', quote: '', lastRun: '', runsToday: 0, successRate: 0, pendingApprovals: 0, }; const authHeader = (token) => ({ Authorization: `Bearer ${token}` }); const enrichCron = (cron) => ({ ...cron, agentId: cron.agent_id, natural: typeof window.cronToHuman === 'function' ? window.cronToHuman(cron.expr) : cron.expr, next: '—', countdown: '—', }); const EmptyDashboardState = () => (
API conectada · sem agentes

Registrar primeiro agente

O frontend não usa mais dados fictícios. Quando algum app chamar register_agent() ou o mirror publicar jobs, eles aparecerão aqui.

AGENT_HUB_URL AGENT_HUB_API_KEY AGENT_ID
); const App = () => { const [agents, setAgents] = React.useState([]); const [loading, setLoading] = React.useState(true); const [authError, setAuthError] = React.useState(null); const [crons, setCrons] = React.useState([]); const [activeId, setActiveId] = React.useState(null); const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS); const [toast, setToast] = React.useState(null); const [runningAgentIds, setRunningAgentIds] = React.useState(() => new Set()); const [confirmRunAgentId, setConfirmRunAgentId] = React.useState(null); useEscapeToClose(!!confirmRunAgentId, () => setConfirmRunAgentId(null)); // D-07: view toggle — 'org' | 'detail', persisted in localStorage const [view, setViewRaw] = React.useState(() => { try { return localStorage.getItem('hub_view') || 'detail'; } catch { return 'detail'; } }); const setView = (v) => { setViewRaw(v); try { localStorage.setItem('hub_view', v); } catch {} }; // Drawer state for org-chart node click const [drawerAgentId, setDrawerAgentId] = React.useState(null); // D-06: chat scope const [chatScope, setChatScope] = React.useState({ type: 'all', agentId: null }); // Executions drill-down drawer const [executionsDrawerAgentId, setExecutionsDrawerAgentId] = React.useState(null); const [orgChartView, setOrgChartView] = React.useState(() => window.OrgChart || null); const onToast = React.useCallback((msg) => { setToast(msg); }, []); const handleRunAgent = React.useCallback(async (agent) => { const tok = localStorage.getItem('hub_token'); if (!tok) { throw new Error('sessão expirada'); } const body = { message: `Disparo manual pelo hub para ${agent.name}.`, }; if (agent.id === 'my-daily') { body.push = false; } if (agent.id === 'slide-maker') { body.prompt = agent.description || agent.role || 'Disparo manual pelo hub.'; } setRunningAgentIds((prev) => { const next = new Set(prev); next.add(agent.id); return next; }); try { const response = await fetch(`/api/v1/agents/${agent.id}/run`, { method: 'POST', headers: { ...authHeader(tok), 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data.detail || data.reply || `Erro ${response.status}`); } return data; } finally { setRunningAgentIds((prev) => { const next = new Set(prev); next.delete(agent.id); return next; }); } }, []); const handleRequestRun = React.useCallback((agent) => { if (!agent) return; setConfirmRunAgentId(agent.id); }, []); const handleConfirmRun = React.useCallback(async () => { const ag = agents.find(a => a.id === confirmRunAgentId); if (!ag) { setConfirmRunAgentId(null); return; } // Close modal immediately — runningAgentIds Set drives the "Executando..." pill on AgentHero. setConfirmRunAgentId(null); onToast(`Disparando ${ag.name}…`); try { const result = await handleRunAgent(ag); const reply = result?.reply || result?.response || result?.status || 'execução concluída'; onToast(`${ag.name}: ${String(reply).slice(0, 140)}`); } catch (err) { onToast(`${ag.name} — falha: ${err?.message || 'erro ao executar'}`); } }, [agents, confirmRunAgentId, handleRunAgent, onToast]); const loadAgents = React.useCallback((token) => { return fetch('/api/v1/agents', { headers: authHeader(token) }) .then((r) => { if (r.status === 401) { localStorage.removeItem('hub_token'); window.location = '/login'; return null; } if (!r.ok) { setAuthError(`Erro ${r.status}`); return null; } return r.json(); }) .then((data) => { if (!Array.isArray(data)) return; const withDefaults = data.map(a => ({ ...AGENT_DEFAULTS, capabilities: a.integrations || [], traits: a.traits || [], ...a, })); setAgents(withDefaults); }) .catch(() => setAuthError('Erro de rede')); }, []); const handleReorder = React.useCallback((orderedIds) => { const tok = localStorage.getItem('hub_token'); if (!tok || !Array.isArray(orderedIds) || orderedIds.length === 0) return; // Optimistic: reorder local state so the UI doesn't flicker back. setAgents((prev) => { const byId = {}; prev.forEach(a => { byId[a.id] = a; }); const reordered = orderedIds.map(id => byId[id]).filter(Boolean); const rest = prev.filter(a => !orderedIds.includes(a.id)); return [...reordered, ...rest]; }); fetch('/api/v1/agents/reorder', { method: 'POST', headers: { ...authHeader(tok), 'Content-Type': 'application/json' }, body: JSON.stringify({ order: orderedIds }), }).then((r) => { if (!r.ok) { // Server rejected — re-fetch to resync (rollback optimistic update). loadAgents(tok); } }).catch(() => loadAgents(tok)); }, []); const loadCrons = React.useCallback((token) => { return fetch('/api/v1/crons', { headers: authHeader(token) }) .then(r => r.ok ? r.json() : []) .then(data => { if (Array.isArray(data)) setCrons(data.map(enrichCron)); }) .catch(() => {}); }, []); const handleToggleCron = React.useCallback((cronId) => { const tok = localStorage.getItem('hub_token'); const cron = crons.find(c => c.id === cronId); if (!cron) return; const newEnabled = !cron.enabled; setCrons(prev => prev.map(c => c.id === cronId ? { ...c, enabled: newEnabled } : c)); fetch(`/api/v1/crons/${cronId}`, { method: 'PATCH', headers: { ...authHeader(tok), 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: newEnabled }), }).catch(() => loadCrons(tok)); }, [crons, loadCrons]); const handleDeleteCron = React.useCallback((cronId) => { const tok = localStorage.getItem('hub_token'); setCrons(prev => prev.filter(c => c.id !== cronId)); fetch(`/api/v1/crons/${cronId}`, { method: 'DELETE', headers: authHeader(tok), }).catch(() => loadCrons(tok)); }, [loadCrons]); const handleAddCron = React.useCallback((text, expr, agentId) => { const tok = localStorage.getItem('hub_token'); fetch('/api/v1/crons', { method: 'POST', headers: { ...authHeader(tok), 'Content-Type': 'application/json' }, body: JSON.stringify({ agent_id: agentId, title: text.split('\n')[0].slice(0, 60), description: text, expr: expr || '0 9 * * *', }), }) .then(r => r.ok ? r.json() : null) .then(cron => { if (cron) setCrons(prev => [...prev, enrichCron(cron)]); }) .catch(() => {}); }, []); React.useEffect(() => { const token = localStorage.getItem('hub_token'); if (!token) { window.location = '/login'; return; } Promise.all([loadAgents(token), loadCrons(token)]).then(() => setLoading(false)); // auto-refresh agents a cada 30s const interval = setInterval(() => loadAgents(token), 30000); return () => clearInterval(interval); }, [loadAgents, loadCrons]); React.useEffect(() => { if (window.OrgChart) { setOrgChartView(() => window.OrgChart); return; } const interval = setInterval(() => { if (!window.OrgChart) return; setOrgChartView(() => window.OrgChart); clearInterval(interval); }, 50); const timeout = setTimeout(() => clearInterval(interval), 5000); return () => { clearInterval(interval); clearTimeout(timeout); }; }, []); React.useEffect(() => { if (agents.length > 0 && activeId === null) setActiveId(agents[0].id); }, [agents, activeId]); React.useEffect(() => { document.body.dataset.density = tweaks.density; document.documentElement.style.setProperty("--d-brand", ACCENT_COLORS[tweaks.accent] || ACCENT_COLORS.blue); const c = ACCENT_COLORS[tweaks.accent] || ACCENT_COLORS.blue; const r = parseInt(c.slice(1,3),16), g = parseInt(c.slice(3,5),16), b = parseInt(c.slice(5,7),16); document.documentElement.style.setProperty("--d-brand-soft", `rgba(${r},${g},${b},0.14)`); }, [tweaks.density, tweaks.accent]); const active = agents.find(a => a.id === activeId); const pendingCount = agents.reduce((s, a) => s + (a.pendingApprovals || 0), 0); // Agent lookup map for drawer const agentById = React.useMemo(() => { const m = {}; agents.forEach(a => { m[a.id] = a; }); return m; }, [agents]); const drawerAgent = drawerAgentId ? agentById[drawerAgentId] : null; if (loading) return
Carregando agentes...
; if (authError) return
{authError}
; return (
{/* Sidebar only in detail view */} {view === 'detail' && ( )}
{view === 'org' ? ( orgChartView ? ( React.createElement(orgChartView, { agents, crons, onRunAgent: handleRunAgent, onSelect: (id) => { setDrawerAgentId(id); setChatScope({ type: 'agent', agentId: id }); }, setChatScope, }) ) : (
Carregando visualização

Preparando organograma

O dashboard está aguardando o componente orgchart.jsx.

) ) : (
{active ? ( <> {tweaks.showStats && ( setExecutionsDrawerAgentId(active.id)} /> )}
handleAddCron(text, expr, active.id)} /> ) : ( )}
)} {/* Chat always visible below */}
{/* Agent drawer (from org-chart node click) */} {drawerAgent && ( setDrawerAgentId(null)} onToast={onToast} onStatClick={(agentId) => setExecutionsDrawerAgentId(agentId)} onRunAgent={handleRunAgent} onToggleCron={handleToggleCron} onDeleteCron={handleDeleteCron} onAddCron={handleAddCron} isRunning={runningAgentIds.has(drawerAgent.id)} /> )} {/* Executions drill-down drawer */} {executionsDrawerAgentId && ( setExecutionsDrawerAgentId(null)} /> )} setTweak("density", v)} /> setTweak("accent", v)} /> setTweak("showStats", v)} /> {toast && setToast(null)} />} {confirmRunAgentId && (() => { const Modal = window.ConfirmRunModal; const ag = agents.find(a => a.id === confirmRunAgentId); if (!Modal || !ag) return null; return ( setConfirmRunAgentId(null)} onConfirm={handleConfirmRun} /> ); })()}
); }; ReactDOM.createRoot(document.getElementById("root")).render();