// 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();