Files
skills/orchestrator/templates/page-graph-charts.js
T

189 lines
10 KiB
JavaScript
Raw Normal View History

function orbit() {
const nodes = state.graph.nodes;
positions = layout(nodes);
const svg = chartFrame(400, "Orbit: operator in the middle; whoever works or radios together moves closer, the silent drift outward");
svg.append(...RINGS.map((r) => s("circle", {class: "ring", r})));
const edgeLayer = s("g", {});
const halos = new Map();
for (const link of aggregate()) {
const from = positions.get(link.source);
const shade = tone(link.cls);
const words = `${link.count}× ${CLASSES[link.cls] || link.cls}`;
if (!from) {
continue;
}
if (link.target === "*") {
const index = halos.get(link.source) || 0;
halos.set(link.source, index + 1);
edgeLayer.append(s("circle", {class: `halo t-${shade}`, cx: fixed(from.x), cy: fixed(from.y), r: radiusOf(link.source) + 7 + index * 7, "stroke-width": width(link.count)},
s("title", {}, `${link.source} to all: ${words}`)));
continue;
}
const to = positions.get(link.target);
if (!to || to === from) {
continue;
}
const control = Space.bend(from, to);
const start = toward(from, control, radiusOf(link.source) + 2);
const end = toward(to, control, radiusOf(link.target) + 4);
edgeLayer.append(s("path", {class: `edge t-${shade}`, d: `M${fixed(start.x)},${fixed(start.y)} Q${fixed(control.x)},${fixed(control.y)} ${fixed(end.x)},${fixed(end.y)}`, "stroke-width": fixed(width(link.count)), "marker-end": `url(#tip-${shade})`},
s("title", {}, `${link.source} → ${link.target}: ${words}`)));
}
const nodeLayer = s("g", {}, nodes.map((node) => nodeMark(node, positions.get(node.id), node.kind === "operator" || node.kind === "subagent" ? null : RADIUS[node.kind] + 9)));
const pulseLayer = s("g", {});
svg.append(edgeLayer, nodeLayer, pulseLayer);
graphDom.pulses = pulseLayer;
return svg;
}
function chord() {
const nodes = state.graph.nodes.filter((node) => node.kind !== "subagent");
const fresh = targets();
const radius = 250;
const step = (2 * Math.PI) / Math.max(nodes.length, 1);
const spots = new Map(nodes.map((node) => {
const angle = spotOf(node.id, fresh).chord;
return [node.id, {x: radius * Math.cos(angle), y: radius * Math.sin(angle), angle}];
}));
const svg = chartFrame(450, "Chord: messages between agents, ordered by closeness");
const arcs = s("g", {});
const point = (angle, r) => `${fixed(r * Math.cos(angle))},${fixed(r * Math.sin(angle))}`;
for (const node of nodes) {
const at = spots.get(node.id);
const half = Math.min(step * 0.36, 0.5);
arcs.append(s("path", {class: `group-arc k-${node.kind}`, d: `M${point(at.angle - half, radius + 14)} A${radius + 14},${radius + 14} 0 0 1 ${point(at.angle + half, radius + 14)}`}));
}
const pairs = new Map();
const fans = new Map();
for (const link of aggregate()) {
const shade = tone(link.cls);
if (!spots.has(link.source)) {
continue;
}
if (link.target === "*") {
const key = `${link.source}\u0000${shade}`;
fans.set(key, {source: link.source, shade, count: (fans.has(key) ? fans.get(key).count : 0) + link.count});
continue;
}
if (!spots.has(link.target) || link.target === link.source) {
continue;
}
const [a, b] = [link.source, link.target].sort();
const key = `${a}\u0000${b}\u0000${shade}`;
pairs.set(key, {a, b, shade, count: (pairs.has(key) ? pairs.get(key).count : 0) + link.count});
}
const lanes = LEGEND.map(([name]) => name);
for (const pair of pairs.values()) {
const from = spots.get(pair.a);
const to = spots.get(pair.b);
const pull = 0.06 + 0.12 * lanes.indexOf(pair.shade);
const control = {x: ((from.x + to.x) / 2) * pull, y: ((from.y + to.y) / 2) * pull};
const start = toward(from, control, RADIUS.session + 2);
const end = toward(to, control, RADIUS.session + 2);
arcs.append(s("path", {class: `edge t-${pair.shade}`, d: `M${fixed(start.x)},${fixed(start.y)} Q${fixed(control.x)},${fixed(control.y)} ${fixed(end.x)},${fixed(end.y)}`, "stroke-width": fixed(width(pair.count))},
s("title", {}, `${pair.a} ↔ ${pair.b}: ${pair.count}× ${LEGEND.find(([name]) => name === pair.shade)[1]}`)));
}
const offsets = new Map();
for (const fan of fans.values()) {
const at = spots.get(fan.source);
const index = offsets.get(fan.source) || 0;
offsets.set(fan.source, index + 1);
const angle = at.angle + (index - 1) * 0.05;
const length = Math.min(8 + 6 * Math.log2(1 + fan.count), 40);
arcs.append(s("line", {class: `tick t-${fan.shade}`, x1: fixed((radius + 22) * Math.cos(angle)), y1: fixed((radius + 22) * Math.sin(angle)), x2: fixed((radius + 22 + length) * Math.cos(angle)), y2: fixed((radius + 22 + length) * Math.sin(angle)), "stroke-width": 4},
s("title", {}, `${fan.source} to all: ${fan.count}×`)));
}
svg.append(arcs, s("g", {}, nodes.map((node) => nodeMark(node, spots.get(node.id), 74))));
return svg;
}
function swimlane() {
const rank = new Map([HUMAN, ...state.graph.layout.order].map((id, index) => [id, index]));
const lanes = state.graph.nodes.filter((node) => node.kind !== "subagent").sort((a, b) => (rank.has(a.id) ? rank.get(a.id) : 1e6) - (rank.has(b.id) ? rank.get(b.id) : 1e6));
const begin = state.since;
const end = Math.max(state.until, state.since + 1);
const left = 160;
const right = 24;
const top = 40;
const lane = 36;
const full = 1120;
const height = top + lanes.length * lane + 12;
const x = (ts) => left + ((ts - begin) / (end - begin)) * (full - left - right);
const y = new Map(lanes.map((node, index) => [node.id, top + index * lane + lane / 2]));
const svg = s("svg", {viewBox: `0 0 ${full} ${height}`, class: "chart", role: "img", "aria-label": "Swimlane: messages over time"}, defs());
lanes.forEach((node, index) => {
svg.append(s("rect", {class: index % 2 ? "lane odd" : "lane", x: 0, y: top + index * lane, width: full, height: lane}));
svg.append(s("text", {class: "lane-label", x: 14, y: top + index * lane + lane / 2, dy: "0.35em", "text-anchor": "start", "data-pick": node.id, on: {click: () => select(node.id)}}, node.label));
});
for (let index = 0; index <= 6; index += 1) {
const ts = begin + ((end - begin) * index) / 6;
svg.append(s("line", {class: "grid", x1: fixed(x(ts)), x2: fixed(x(ts)), y1: top - 6, y2: height - 8}));
svg.append(s("text", {class: "axis", x: fixed(x(ts)), y: top - 14, "text-anchor": index === 0 ? "start" : index === 6 ? "end" : "middle"}, (end - begin > DAY ? absolute : clock).format(stampOf(ts))));
}
if (live()) {
svg.append(s("line", {class: "now", x1: fixed(x(end)), x2: fixed(x(end)), y1: top - 6, y2: height - 8}));
}
const marks = s("g", {});
const arrows = (cls, ts, from, to, words) => {
const x0 = fixed(x(ts));
const y0 = y.get(from);
if (y0 === undefined) {
return;
}
const shade = tone(cls);
const hint = s("title", {}, `${clock.format(stampOf(ts))} · ${dash(labelOf(from))} → ${to.length ? to.map(labelOf).join(", ") : "all"}\n${words}`);
if (!to.length) {
const rays = [[-6, -13], [0, -15], [6, -13], [-6, 13], [0, 15], [6, 13]].map(([dx, dy]) => `M${x0},${y0} l${dx},${dy}`).join(" ");
marks.append(s("g", {class: `mark t-${shade}`}, hint, s("path", {d: rays}), s("circle", {cx: x0, cy: y0, r: 3.5})));
return;
}
const bundle = s("g", {class: `mark t-${shade}`}, hint, s("circle", {cx: x0, cy: y0, r: 3}));
for (const target of to) {
const y1 = y.get(target);
if (y1 !== undefined && y1 !== y0) {
bundle.append(s("line", {x1: x0, y1: y0, x2: x0, y2: y1 > y0 ? y1 - 6 : y1 + 6, "marker-end": `url(#tip-${shade})`}));
}
}
marks.append(bundle);
};
for (const row of state.radio) {
arrows(row.class, row.ts, row.source, row.targets, row.text.length > 120 ? `${row.text.slice(0, 120)}…` : row.text);
}
for (const grant of state.grants) {
arrows("grant", grant.ts, chairKey(), [grant.target], `Queue grant ${grant.kind} (${grant.id})`);
}
svg.append(marks);
return svg;
}
function legend() {
return h("div", {class: "legend"}, LEGEND.map(([name, label]) => h("span", {}, h("i", {class: `swatch t-${name}`}), label)),
h("span", {class: "muted"}, "Line width = count · closeness = recent collaboration"));
}
function nodePopup(node) {
const messages = state.radio.filter((row) => row.source === node.id || row.targets.includes(node.id)).slice(-10).reverse();
return [
popupHead(null, node.label, h("span", {class: "muted"}, NODE_KINDS[node.kind])),
h("div", {class: "popup-body"}, h("h3", {}, "Latest messages", counter(messages.length)),
messages.length ? h("ul", {class: "mini"}, messages.map((row) => h("li", {},
h("div", {class: "row"}, klass(row.class), when(row.ts)),
h("span", {class: "muted"}, `${dash(row.from)} → ${targetsOf(row)}`),
h("span", {}, row.text)))) : h("p", {class: "muted"}, "No messages in the chosen time range.")),
];
}
function viewSwitch(screen) {
return h("div", {class: "segmented", role: "group", "aria-label": "View"}, Nav.viewsOf(screen).map(([id, name]) => h("button", {
type: "button", class: "icon-only", ...named(name), "aria-pressed": String(ui.views[screen] === id), on: {click: () => go(screen, id)},
}, icon(Nav.ICONS[id]))));
}
function agentsScreen() {
const view = ui.views.agents;
const stageBox = h("div", {class: "stage"});
const toolbar = agentsToolbar();
graphDom = {stage: stageBox, pulses: null, toolbar};
const body = view === "tree" ? spawnScreen() : view === "hierarchy" ? hierarchyScreen() : h("div", {class: "card stage-card"}, stageBox, view === "3d" ? null : legend());
return h("div", {class: "graph"}, toolbar.bar, toolbar.spot, h("div", {class: "card controls"}, viewSwitch("agents"), view === "3d" ? layoutTools("agents") : null), body);
}