/* @jsxRuntime classic */
/* @jsx React.createElement */
// Orchestra — Context Map (Bench Phase-3, task 86banqh9k)
// Read-only visualization: Knowledge Base entry → affected Wright → category (the Bench),
// with freshness/dirty state. "Reconcile" on a dirty Wright jumps to The Bench (Operating Context).
// Loaded by index.html as a separate text/babel script; exposes window.ContextMapPage.
(function () {
const { useState, useEffect, useMemo } = React;
// ── API helper (mirrors app.jsx's api()) ──────────────────────────────────
function toCamel(s) { return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase()); }
function deepCamel(obj) {
if (Array.isArray(obj)) return obj.map(deepCamel);
if (obj && typeof obj === 'object') {
const out = {};
for (const k of Object.keys(obj)) out[toCamel(k)] = deepCamel(obj[k]);
return out;
}
return obj;
}
async function api(method, path) {
const res = await fetch(path, { method });
if (!res.ok) { const t = await res.text().catch(() => ''); throw new Error(`${res.status}: ${t}`); }
return deepCamel(await res.json());
}
// Category slugs come back from the Bench summary as camelCased dict keys
// (e.g. "data_model" → "dataModel" after deepCamel). Humanize for display.
function camelToLabel(s) {
if (!s) return 'General';
const spaced = s.replace(/([A-Z])/g, ' $1');
return (spaced.charAt(0).toUpperCase() + spaced.slice(1)).trim();
}
function trunc(s, n) { return s && s.length > n ? s.slice(0, n - 1) + '…' : (s || ''); }
function bezier(x1, y1, x2, y2) {
const cx = (x1 + x2) / 2;
return `M${x1},${y1} C${cx},${y1} ${cx},${y2} ${x2},${y2}`;
}
function ContextMapPage({ agents, navigateTo }) {
const [summaries, setSummaries] = useState(null); // null = loading
const [dirty, setDirty] = useState([]);
const [kbEntries, setKbEntries] = useState([]);
useEffect(() => {
injectStyles();
Promise.all([
api('GET', '/api/bench').catch(() => ({ summaries: [] })),
api('GET', '/api/bench/dirty').catch(() => ({ dirty: [] })),
api('GET', '/api/org-context/entries').catch(() => []),
]).then(([benchRes, dirtyRes, entries]) => {
setSummaries(benchRes.summaries || []);
setDirty(dirtyRes.dirty || []);
setKbEntries(entries || []);
});
}, []);
const agentRoles = {};
(agents || []).forEach(a => { agentRoles[a.id] = a.role || a.id; });
const dirtySet = useMemo(() => {
const s = new Set();
dirty.forEach(d => s.add(`${d.agentId}::${toCamel(d.category || '')}`));
return s;
}, [dirty]);
const dirtyAgentIds = useMemo(() => new Set(dirty.map(d => d.agentId)), [dirty]);
const kbById = {};
kbEntries.forEach(e => { kbById[e.id] = e; });
const dirtyEntryIds = useMemo(() => {
const s = new Set();
dirty.forEach(d => { if (d.sourceEntryId) s.add(d.sourceEntryId); });
return s;
}, [dirty]);
const liveKbNodes = Array.from(dirtyEntryIds)
.map(id => kbById[id])
.filter(e => e && e.kind !== 'group');
const activeAgents = (summaries || []).filter(s => s.totalCount > 0);
const wrightIds = activeAgents.map(s => s.agentId);
const categorySet = {};
activeAgents.forEach(s => {
Object.entries(s.categories || {}).forEach(([cat, count]) => {
categorySet[cat] = (categorySet[cat] || 0) + count;
});
});
const categoryNames = Object.keys(categorySet).sort();
if (summaries === null) {
return (
);
}
if (wrightIds.length === 0) {
return (
Context Map
Visualizes Knowledge Base → Wright → category edges feeding the Bench.
No Bench activity yet. Items appear here once Builds complete review gates,
or the Knowledge Base propagates entries into a Wright's Bench.
);
}
// ── SVG layout: 3 columns — pending KB entries | Wrights | categories ──
const NODE_W = 178, NODE_H = 34, ROW_GAP = 10, PAD_Y = 24, PAD_X = 20, COL_GAP = 140;
const COL1_X = PAD_X;
const COL2_X = COL1_X + NODE_W + COL_GAP;
const COL3_X = COL2_X + NODE_W + COL_GAP;
const SVG_W = COL3_X + NODE_W + PAD_X;
const kbIds = liveKbNodes.map(e => e.id);
const blockH = (arr) => (arr.length ? arr.length * (NODE_H + ROW_GAP) - ROW_GAP : 0);
const SVG_H = Math.max(blockH(kbIds), blockH(wrightIds), blockH(categoryNames), NODE_H) + PAD_Y * 2;
function colYs(ids) {
const ys = {};
const startY = (SVG_H - blockH(ids)) / 2;
ids.forEach((id, i) => { ys[id] = startY + i * (NODE_H + ROW_GAP) + NODE_H / 2; });
return ys;
}
const kbYs = colYs(kbIds);
const wrightYs = colYs(wrightIds);
const catYs = colYs(categoryNames);
const kbEdges = [];
dirty.forEach(d => {
if (d.sourceEntryId && kbById[d.sourceEntryId] && kbYs[d.sourceEntryId] !== undefined
&& wrightYs[d.agentId] !== undefined) {
kbEdges.push({ kbId: d.sourceEntryId, agentId: d.agentId });
}
});
const catEdges = [];
activeAgents.forEach(s => {
Object.keys(s.categories || {}).forEach(cat => {
if (catYs[cat] !== undefined) {
catEdges.push({ agentId: s.agentId, cat, dirty: dirtySet.has(`${s.agentId}::${cat}`) });
}
});
});
return (
Context Map
Knowledge Base → Wright → category edges feeding the Bench. Read-only —
amber dashed edges are dirty (pending reconcile); gold edges are fresh.
{dirtyAgentIds.size > 0 && (
navigateTo && navigateTo('bench')}>
⟳ Reconcile in The Bench
)}
{kbEdges.map(({ kbId, agentId }) => (
))}
{catEdges.map(({ agentId, cat, dirty: isDirty }) => (
))}
{liveKbNodes.map(e => (
{trunc(e.title || `Entry #${e.id}`, 22)}
))}
{wrightIds.map(id => {
const isDirty = dirtyAgentIds.has(id);
return (
{ if (isDirty && navigateTo) navigateTo('bench'); }}>
{trunc(agentRoles[id] || id, isDirty ? 16 : 22)}
{isDirty && (
⟳
)}
);
})}
{categoryNames.map(cat => (
{trunc(camelToLabel(cat), 20)}
{categorySet[cat]}
))}
Left: Knowledge Base entries pending propagation ({liveKbNodes.length}) · Middle: Wrights
({wrightIds.length}) · Right: categories — the Bench ({categoryNames.length}) · Click a
dirty (⟳) Wright to reconcile.
{liveKbNodes.length === 0 && (
No Knowledge Base entries are currently pending propagation — every Wright's Bench
reflects the latest Knowledge Base state. Wright→category edges below show existing
Bench contents.
)}
);
}
function injectStyles() {
if (document.getElementById('cm-styles')) return;
const el = document.createElement('style');
el.id = 'cm-styles';
el.textContent = `
.cm-root { height:100%; overflow-y:auto; overflow-x:hidden; padding:24px; }
.cm-head-row { display:flex; align-items:flex-start; justify-content:space-between; gap:16px; flex-wrap:wrap; margin-bottom:16px; }
.cm-title { font-weight:600; font-size:1.05rem; }
.cm-sub { font-size:0.82rem; color:#748090; margin-top:4px; line-height:1.55; max-width:620px; }
.cm-loading { color:#748090; font-size:0.85rem; margin-top:8px; }
.cm-empty { background:#0f1525; border:1px solid #1b2640; border-radius:10px; padding:24px;
color:#748090; font-size:0.85rem; line-height:1.6; max-width:520px; margin-top:16px; }
.cm-caption { font-size:0.75rem; color:#748090; margin-top:10px; }
.cm-note { font-size:0.78rem; color:#748090; margin-top:14px; max-width:600px; line-height:1.55; }
`;
document.head.appendChild(el);
}
window.ContextMapPage = ContextMapPage;
})();