function orbit() { const nodes = state.graph.nodes; positions = layout(nodes); const svg = chartFrame(400, "Orbit: Operator in der Mitte; wer zusammen arbeitet oder funkt, rückt zusammen, Stille driften nach außen"); 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} an alle: ${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: Nachrichten zwischen Agenten, Reihenfolge nach Nähe"); 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} an alle: ${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: Nachrichten über die Zeit"}, 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", 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(", ") : "alle"}\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-Freigabe ${grant.kind} (${grant.id})`); } svg.append(marks); return svg; } function legend() { const axis = graph3d() ? depth() : "none"; const words = { none: "Linienstärke = Anzahl · Nähe = Zusammenarbeit der letzten Zeit", time: "Weltlinie = Agent, Lage = Nähe der Zusammenarbeit zu ihrer Zeit · Bogen = ein Ereignis · Ebene = Stand der Wiedergabe", spawn: "Ebenen = Spawn-Tiefe · Bogen = Nachrichten im Zeitraum, Zahl im Stream", }; return h("div", {class: "legend"}, LEGEND.map(([name, label]) => h("span", {}, h("i", {class: `swatch t-${name}`}), label)), axis === "none" ? null : h("span", {}, h("i", {class: "swatch t-spawn"}), "Spawn"), h("span", {class: "muted"}, words[axis])); } function side() { const node = state.graph.nodes.find((entry) => entry.id === ui.node); if (!node) { const where = graph3d() ? "eine Weltlinie oder darunter auf ihren Namen" : "einen Knoten"; return [h("h2", {class: "panel-title"}, "Details"), h("p", {class: "muted"}, `Klicke auf ${where}: sein Panel zeigt Aufgaben, Claims, Subagenten und Verlauf.`)]; } const messages = state.radio.filter((row) => row.source === node.id || row.targets.includes(node.id)).slice(-10).reverse(); return [ h("div", {class: "panel-head"}, h("div", {}, h("h2", {class: "panel-title"}, node.label), h("span", {class: "muted"}, `${NODE_KINDS[node.kind]}${node.parent ? ` von ${labelOf(node.parent)}` : ""}`)), h("button", {type: "button", class: "btn small ghost", "aria-label": "Schließen", on: {click: () => select(node.id)}}, "×")), h("h3", {}, "Letzte Nachrichten", 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"}, "Keine Nachrichten im gewählten Zeitraum."), ]; } function viewSwitch(screen) { return h("div", {class: "segmented", role: "group", "aria-label": "Ansicht"}, Nav.VIEWS.map(([id, name]) => h("button", {type: "button", "aria-pressed": String(ui.views[screen] === id), on: {click: () => go(screen, id)}}, name))); } const later = (what) => emptyState(`${what} folgt`, "Diese Ansicht ist geplant und kommt mit dem nächsten Ausbau."); function agentsScreen() { const view = ui.views.agents; const stageBox = h("div", {class: "stage"}); const aside = h("aside", {class: "card panel", "aria-live": "polite"}); const toolbar = agentsToolbar(); graphDom = {stage: stageBox, aside, pulses: null, space: null, toolbar}; const body = view === "tree" ? later("Der Spawn-Baum") : h("div", {class: "graph-body"}, h("div", {class: "card stage-card"}, stageBox, legend()), aside); return h("div", {class: "graph"}, toolbar.bar, h("div", {class: "card controls"}, viewSwitch("agents"), view === "3d" ? spaceTools() : null), body); } function artifactsScreen() { const view = ui.views.artifacts; return h("div", {class: "graph"}, h("div", {class: "card controls"}, viewSwitch("artifacts")), view === "tree" ? treeScreen() : later(`Artifacts ${Nav.VIEWS.find(([id]) => id === view)[1]}`)); }