OR-1 dataflow CPU sketch
at main 827 lines 24 kB view raw
1import cytoscape from "cytoscape"; 2import elk from "cytoscape-elk"; 3import svg from "cytoscape-svg"; 4import type { 5 MonitorNode, MonitorEdge, MonitorUpdate, GraphLoadedMessage, 6 ResetMessage, ServerMessage, SystemState, 7} from "./types"; 8import { monitorStylesheet } from "./style"; 9import { logicalLayout, physicalLayout } from "@common/layout"; 10import { exportSvg, exportPng, copyPng } from "@common/export"; 11import { createConnection } from "./connection"; 12import type { MonitorConnection } from "./connection"; 13import { clearEventLog, updateEventLog, setEventFilter, updateStateInspector, displayNodeMatchingStore, displaySMNodeState } from "./panels"; 14import { rp } from "./palette"; 15 16// Register plugins 17cytoscape.use(elk); 18cytoscape.use(svg); 19 20const ROUTING_CATEGORY = "routing"; 21 22// Global state 23let cy: cytoscape.Core; 24let isPhysicalLayout = true; 25let currentState: SystemState | null = null; 26let currentNodes: ReadonlyArray<MonitorNode> = []; 27let currentEdges: ReadonlyArray<MonitorEdge> = []; 28 29/** 30 * Route edges with intelligent bezier curves to avoid node collisions. 31 * Ported from dfgraph/frontend/src/main.ts. 32 */ 33function routeEdges(selector?: string): void { 34 const nodes = cy.nodes().not(":parent").not(".port-node"); 35 const nodePositions: Array<{ x: number; y: number }> = []; 36 nodes.forEach((n) => { 37 nodePositions.push({ x: n.position("x"), y: n.position("y") }); 38 }); 39 40 let leftCount = 0; 41 let rightCount = 0; 42 43 const edges = selector ? cy.edges(selector) : cy.edges(); 44 edges.forEach((edge) => { 45 const sy = edge.source().position("y"); 46 const ty = edge.target().position("y"); 47 const sx = edge.source().position("x"); 48 const tx = edge.target().position("x"); 49 const span = Math.abs(ty - sy); 50 51 if (span < 80) { 52 edge.style({ "curve-style": "bezier" }); 53 return; 54 } 55 56 const blocked = nodePositions.some((p) => { 57 return p.y > Math.min(sy, ty) + 25 && p.y < Math.max(sy, ty) - 25; 58 }); 59 60 if (!blocked) { 61 edge.style({ "curve-style": "bezier" }); 62 return; 63 } 64 65 const avgX = (sx + tx) / 2; 66 const centerX = (Math.min(...nodePositions.map((p) => p.x)) + Math.max(...nodePositions.map((p) => p.x))) / 2; 67 const goLeft = avgX >= centerX; 68 69 const baseOffset = 30; 70 const stagger = goLeft ? 12 * leftCount : 12 * rightCount; 71 const offset = (goLeft ? -1 : 1) * (baseOffset + stagger); 72 73 if (goLeft) leftCount++; 74 else rightCount++; 75 76 edge.style({ 77 "curve-style": "unbundled-bezier", 78 "control-point-distances": [offset * 0.6, offset, offset * 0.6], 79 "control-point-weights": [0.2, 0.5, 0.8], 80 }); 81 }); 82} 83 84/** 85 * Build Cytoscape elements for logical view (flat, no PE clustering). 86 */ 87function buildLogicalElements( 88 nodes: ReadonlyArray<MonitorNode>, 89 edges: ReadonlyArray<MonitorEdge>, 90): cytoscape.ElementDefinition[] { 91 const elements: cytoscape.ElementDefinition[] = []; 92 93 for (const node of nodes) { 94 const label = node.label 95 ? node.label 96 : node.const !== null 97 ? `${node.opcode}\n${node.const}` 98 : node.opcode; 99 100 elements.push({ 101 data: { 102 id: node.id, 103 label, 104 colour: node.colour, 105 category: node.category, 106 pe: node.pe, 107 iram_offset: node.iram_offset, 108 ctx: node.ctx, 109 synthetic: node.synthetic ?? false, 110 sm_id: (node as any).sm_id ?? null, 111 }, 112 }); 113 } 114 115 for (const edge of edges) { 116 const sourceNode = nodes.find((n) => n.id === edge.source); 117 let sourceLabel: string | undefined; 118 if (sourceNode && sourceNode.category === ROUTING_CATEGORY && edge.source_port) { 119 sourceLabel = edge.source_port === "L" ? "T" : "F"; 120 } 121 122 elements.push({ 123 data: { 124 id: `${edge.source}->${edge.target}:${edge.port}`, 125 source: edge.source, 126 target: edge.target, 127 targetLabel: edge.port, 128 sourceLabel: sourceLabel ?? "", 129 }, 130 classes: edge.synthetic ? "synthetic" : undefined, 131 }); 132 } 133 134 return elements; 135} 136 137/** 138 * Build Cytoscape elements for physical view with bus topology. 139 * Ported from dfgraph/frontend/src/main.ts buildPhysicalElements(). 140 */ 141function buildPhysicalElements( 142 nodes: ReadonlyArray<MonitorNode>, 143 edges: ReadonlyArray<MonitorEdge>, 144): cytoscape.ElementDefinition[] { 145 const elements: cytoscape.ElementDefinition[] = []; 146 147 // Build edge target set for seed const detection 148 const edgeTargets = new Set<string>(); 149 for (const edge of edges) { 150 edgeTargets.add(edge.target); 151 } 152 153 const seedConstIds = new Set<string>(); 154 const nodePeMap = new Map<string, number | null>(); 155 const peIds = new Set<number>(); 156 157 for (const node of nodes) { 158 nodePeMap.set(node.id, node.pe); 159 if (node.pe !== null) peIds.add(node.pe); 160 161 if (node.category === "config" && node.opcode === "const" && !edgeTargets.has(node.id)) { 162 seedConstIds.add(node.id); 163 } 164 } 165 166 // Create PE cluster parent nodes 167 for (const peId of peIds) { 168 elements.push({ 169 data: { id: `pe-${peId}`, label: `PE ${peId}` }, 170 classes: "pe-cluster", 171 }); 172 } 173 174 // Create operation nodes 175 for (const node of nodes) { 176 const isSeedConst = seedConstIds.has(node.id); 177 const label = node.label 178 ? node.label 179 : node.const !== null 180 ? `${node.opcode}\n${node.const}` 181 : node.opcode; 182 183 const el: cytoscape.ElementDefinition = { 184 data: { 185 id: node.id, 186 label, 187 colour: node.colour, 188 category: node.category, 189 pe: node.pe, 190 iram_offset: node.iram_offset, 191 ctx: node.ctx, 192 synthetic: node.synthetic ?? false, 193 sm_id: (node as any).sm_id ?? null, 194 }, 195 classes: 196 [isSeedConst ? "seed-const" : undefined].filter(Boolean).join(" ") || undefined, 197 }; 198 199 // Synthetic SM nodes and seed consts float freely 200 if (!node.synthetic && !isSeedConst && node.pe !== null) { 201 el.data.parent = `pe-${node.pe}`; 202 } 203 204 elements.push(el); 205 } 206 207 // Scan cross-PE edges to find PE pairs for bus topology 208 type PePair = { srcPe: number; tgtPe: number }; 209 const pePairKey = (src: number, tgt: number) => `${src}->${tgt}`; 210 const crossPePairs = new Map<string, PePair>(); 211 const busEdgeCounts = new Map<string, number>(); 212 213 for (const edge of edges) { 214 if (seedConstIds.has(edge.source) || edge.synthetic) continue; 215 const srcPe = nodePeMap.get(edge.source) ?? null; 216 const tgtPe = nodePeMap.get(edge.target) ?? null; 217 if (srcPe !== null && tgtPe !== null && srcPe !== tgtPe) { 218 const key = pePairKey(srcPe, tgtPe); 219 if (!crossPePairs.has(key)) { 220 crossPePairs.set(key, { srcPe, tgtPe }); 221 } 222 busEdgeCounts.set(key, (busEdgeCounts.get(key) ?? 0) + 1); 223 } 224 } 225 226 // Create port nodes for each directed PE pair 227 for (const [, pair] of crossPePairs) { 228 elements.push({ 229 data: { 230 id: `port-${pair.srcPe}-to-${pair.tgtPe}-exit`, 231 parent: `pe-${pair.srcPe}`, 232 }, 233 classes: "port-node", 234 }); 235 elements.push({ 236 data: { 237 id: `port-${pair.srcPe}-to-${pair.tgtPe}-entry`, 238 parent: `pe-${pair.tgtPe}`, 239 }, 240 classes: "port-node", 241 }); 242 } 243 244 // Create bus edges (one per PE pair) 245 for (const [key, pair] of crossPePairs) { 246 const count = busEdgeCounts.get(key) ?? 1; 247 elements.push({ 248 data: { 249 id: `bus-${pair.srcPe}-to-${pair.tgtPe}`, 250 source: `port-${pair.srcPe}-to-${pair.tgtPe}-exit`, 251 target: `port-${pair.srcPe}-to-${pair.tgtPe}-entry`, 252 label: count > 1 ? `×${count}` : "", 253 }, 254 classes: "physical bus-segment", 255 }); 256 } 257 258 // Create edges with proper splitting 259 for (const edge of edges) { 260 const srcPe = nodePeMap.get(edge.source) ?? null; 261 const tgtPe = nodePeMap.get(edge.target) ?? null; 262 263 if (edge.synthetic) { 264 // Synthetic edges (SM request/return) — never bundled 265 elements.push({ 266 data: { 267 id: `${edge.source}->${edge.target}:${edge.port}`, 268 source: edge.source, 269 target: edge.target, 270 targetLabel: edge.port, 271 sourceLabel: "", 272 }, 273 classes: "physical synthetic", 274 }); 275 } else if (seedConstIds.has(edge.source)) { 276 // Seed const: direct edge, never bundled 277 elements.push({ 278 data: { 279 id: `${edge.source}->${edge.target}:${edge.port}`, 280 source: edge.source, 281 target: edge.target, 282 targetLabel: edge.port, 283 sourceLabel: "", 284 }, 285 classes: "seed-edge physical", 286 }); 287 } else if (srcPe !== null && tgtPe !== null && srcPe !== tgtPe) { 288 // Cross-PE: split into exit segment + entry segment (bus already created) 289 elements.push({ 290 data: { 291 id: `${edge.source}->exit-${srcPe}-to-${tgtPe}:${edge.port}`, 292 source: edge.source, 293 target: `port-${srcPe}-to-${tgtPe}-exit`, 294 sourceLabel: "", 295 targetLabel: "", 296 }, 297 classes: "physical exit-segment", 298 }); 299 elements.push({ 300 data: { 301 id: `entry-${srcPe}-to-${tgtPe}->${edge.target}:${edge.port}`, 302 source: `port-${srcPe}-to-${tgtPe}-entry`, 303 target: edge.target, 304 targetLabel: edge.port, 305 }, 306 classes: "physical entry-segment", 307 }); 308 } else { 309 // Intra-PE or unplaced 310 const sourceNode = nodes.find((n) => n.id === edge.source); 311 let sourceLabel: string | undefined; 312 if (sourceNode && sourceNode.category === ROUTING_CATEGORY && edge.source_port) { 313 sourceLabel = edge.source_port === "L" ? "T" : "F"; 314 } 315 316 elements.push({ 317 data: { 318 id: `${edge.source}->${edge.target}:${edge.port}`, 319 source: edge.source, 320 target: edge.target, 321 targetLabel: edge.port, 322 sourceLabel: sourceLabel ?? "", 323 }, 324 classes: "physical intra-pe", 325 }); 326 } 327 } 328 329 return elements; 330} 331 332/** 333 * Initialize Cytoscape on the graph container. 334 */ 335function initializeCytoscape(): void { 336 const container = document.getElementById("graph"); 337 if (!container) { 338 console.error("Graph container not found"); 339 return; 340 } 341 342 cy = cytoscape({ 343 container, 344 style: [...monitorStylesheet], 345 elements: [], 346 maxZoom: 2.0, 347 minZoom: 0.1, 348 }); 349} 350 351/** 352 * Render graph from loaded data. 353 */ 354function renderGraph( 355 nodes: ReadonlyArray<MonitorNode>, 356 edges: ReadonlyArray<MonitorEdge>, 357): void { 358 currentNodes = nodes; 359 currentEdges = edges; 360 361 const elements = isPhysicalLayout 362 ? buildPhysicalElements(nodes, edges) 363 : buildLogicalElements(nodes, edges); 364 365 cy.batch(() => { 366 cy.elements().remove(); 367 cy.add(elements); 368 }); 369 370 const layoutOptions = isPhysicalLayout 371 ? physicalLayout() 372 : logicalLayout(); 373 374 const layout = cy.layout(layoutOptions); 375 layout.on("layoutstop", () => { 376 if (isPhysicalLayout) { 377 routeEdges(".intra-pe, .seed-edge"); 378 } else { 379 routeEdges(); 380 } 381 cy.fit(undefined, 40); 382 }); 383 layout.run(); 384} 385 386/** 387 * Update graph execution state from monitor update message. 388 */ 389function updateGraphExecution( 390 nodes: ReadonlyArray<MonitorNode>, 391 edges: ReadonlyArray<MonitorEdge>, 392 state: SystemState, 393): void { 394 // Clear previous execution classes 395 cy.nodes().removeClass(["active", "executed", "matched", "half-matched", "cell-written"]); 396 cy.edges().removeClass("token-flow"); 397 398 // Apply execution state to nodes 399 for (const node of nodes) { 400 const cyNode = cy.getElementById(node.id); 401 if (!cyNode || cyNode.empty()) { 402 if (node.synthetic) { 403 console.warn("[Monitor] SM node not found in cy:", node.id, "label:", node.label); 404 } 405 continue; 406 } 407 408 if (node.active) cyNode.addClass("active"); 409 if (node.matched) cyNode.addClass("matched"); 410 if (node.executed) cyNode.addClass("executed"); 411 if (node.cell_written) cyNode.addClass("cell-written"); 412 413 // Update SM node labels with live state 414 if (node.synthetic) { 415 if (node.label) { 416 cyNode.data("label", node.label); 417 } 418 } 419 420 // Check for half-matched: look in matching store 421 if (node.pe !== null && node.iram_offset !== null) { 422 const peId = node.pe.toString(); 423 const peState = state.pes?.[peId]; 424 if (peState && node.ctx !== null) { 425 const ctx = node.ctx; 426 const ms = peState.matching_store?.[ctx]; 427 if (ms && node.iram_offset < ms.length && ms[node.iram_offset]?.occupied) { 428 cyNode.addClass("half-matched"); 429 } 430 } 431 } 432 } 433 434 // Apply token flow to edges — need to handle both edge ID formats 435 for (const monitorEdge of edges) { 436 if (monitorEdge.token_flow) { 437 // Try both ID formats (logical uses source->target, physical uses source->target:port) 438 const logicalId = `${monitorEdge.source}->${monitorEdge.target}`; 439 const physicalId = `${monitorEdge.source}->${monitorEdge.target}:${monitorEdge.port}`; 440 const cyEdge = cy.getElementById(physicalId); 441 if (cyEdge && !cyEdge.empty()) { 442 cyEdge.addClass("token-flow"); 443 } else { 444 const fallback = cy.getElementById(logicalId); 445 if (fallback && !fallback.empty()) { 446 fallback.addClass("token-flow"); 447 } 448 } 449 } 450 } 451} 452 453/** 454 * Handle graph_loaded message from server. 455 */ 456function handleGraphLoaded(msg: GraphLoadedMessage): void { 457 console.log("[Monitor] Graph loaded"); 458 currentState = msg.state; 459 460 renderGraph(msg.graph.nodes, msg.graph.edges); 461 462 // Initialize panels — clear event log for fresh program 463 clearEventLog(); 464 updateEventLog([], { 465 container: document.getElementById("event-log") || document.createElement("div"), 466 maxEvents: 1000, 467 }); 468 469 updateStateInspector(msg.state, { 470 container: document.getElementById("state-inspector") || document.createElement("div"), 471 }); 472 473 // Update sim time 474 updateSimTime(msg.sim_time, false); 475} 476 477/** 478 * Handle monitor_update message from server. 479 */ 480function handleMonitorUpdate(msg: MonitorUpdate): void { 481 currentState = msg.state; 482 483 // Update graph execution state 484 updateGraphExecution(msg.graph.nodes, msg.graph.edges, msg.state); 485 486 // Update panels 487 const eventLogContainer = document.getElementById("event-log"); 488 if (eventLogContainer) { 489 updateEventLog(msg.events, { 490 container: eventLogContainer, 491 maxEvents: 1000, 492 }); 493 } 494 495 const stateInspectorContainer = document.getElementById("state-inspector"); 496 if (stateInspectorContainer) { 497 updateStateInspector(msg.state, { 498 container: stateInspectorContainer, 499 }); 500 } 501 502 updateSimTime(msg.sim_time, msg.finished); 503} 504 505/** 506 * Handle reset message from server. 507 */ 508function handleReset(msg: ResetMessage): void { 509 console.log("[Monitor] Simulation reset"); 510 511 // Clear the graph 512 cy.elements().remove(); 513 514 // Reset event log 515 clearEventLog(); 516 const eventLogContainer = document.getElementById("event-log"); 517 if (eventLogContainer) { 518 updateEventLog([], { 519 container: eventLogContainer, 520 maxEvents: 1000, 521 }); 522 } 523 524 // Clear state inspector 525 const stateInspectorContainer = document.getElementById("state-inspector"); 526 if (stateInspectorContainer) { 527 stateInspectorContainer.innerHTML = ""; 528 } 529 530 // Update simulation time 531 updateSimTime(msg.sim_time, false); 532} 533 534/** 535 * Update simulation time display. 536 */ 537function updateSimTime(time: number, finished: boolean): void { 538 const timeDisplay = document.getElementById("sim-time"); 539 if (!timeDisplay) return; 540 541 timeDisplay.textContent = `t=${time.toFixed(3)}`; 542 543 const finishedBadge = document.getElementById("finished-badge"); 544 if (finishedBadge) { 545 finishedBadge.style.display = finished ? "inline" : "none"; 546 } 547} 548 549/** 550 * Handle layout toggle. 551 */ 552function toggleLayout(): void { 553 isPhysicalLayout = !isPhysicalLayout; 554 const button = document.getElementById("layout-toggle"); 555 if (button) { 556 button.textContent = isPhysicalLayout ? "Logical Layout" : "Physical Layout"; 557 } 558 559 // Re-render graph with stored data 560 if (currentNodes.length > 0) { 561 renderGraph(currentNodes, currentEdges); 562 } 563} 564 565/** 566 * Handle node click to show matching store (AC3.11). 567 */ 568function setupNodeClickHandler(): void { 569 cy.on("tap", "node", (event: any) => { 570 const node = event.target; 571 const nodeData = node.data(); 572 573 // Highlight this node 574 cy.elements().removeClass("highlighted"); 575 node.addClass("highlighted"); 576 577 const stateInspectorContainer = document.getElementById("state-inspector"); 578 if (!currentState || !stateInspectorContainer) return; 579 580 // SM node clicked: show SM state detail 581 if (nodeData.synthetic && nodeData.sm_id !== null) { 582 const smIdStr = nodeData.sm_id.toString(); 583 const smState = currentState.sms?.[smIdStr]; 584 if (smState) { 585 displaySMNodeState(nodeData.sm_id, smState, stateInspectorContainer); 586 } 587 return; 588 } 589 590 // PE node clicked: show matching store info (AC3.11) 591 if (nodeData.pe !== null && nodeData.ctx !== null && nodeData.iram_offset !== null) { 592 const peId = nodeData.pe; 593 const peIdStr = peId.toString(); 594 const peState = currentState.pes?.[peIdStr]; 595 if (peState) { 596 const ms = peState.matching_store?.[nodeData.ctx]; 597 if (ms && nodeData.iram_offset < ms.length) { 598 const entry = ms[nodeData.iram_offset]; 599 displayNodeMatchingStore(peId, nodeData.ctx, nodeData.iram_offset, entry, stateInspectorContainer); 600 } 601 } 602 } 603 }); 604} 605 606/** 607 * Setup control handlers. 608 */ 609function setupControlHandlers(connection: MonitorConnection): void { 610 // Load file button 611 const fileInput = document.getElementById("file-input") as HTMLInputElement; 612 const loadButton = document.getElementById("load-button"); 613 if (loadButton) { 614 loadButton.addEventListener("click", () => { 615 fileInput?.click(); 616 }); 617 } 618 619 if (fileInput) { 620 fileInput.addEventListener("change", (e) => { 621 const file = (e.target as HTMLInputElement).files?.[0]; 622 if (file) { 623 const reader = new FileReader(); 624 reader.onload = (event) => { 625 const source = event.target?.result as string; 626 connection.send({ cmd: "load", source }); 627 }; 628 reader.readAsText(file); 629 } 630 }); 631 } 632 633 // Step tick button 634 const stepTickButton = document.getElementById("step-tick-button"); 635 if (stepTickButton) { 636 stepTickButton.addEventListener("click", () => { 637 connection.send({ cmd: "step_tick" }); 638 }); 639 } 640 641 // Step event button 642 const stepEventButton = document.getElementById("step-event-button"); 643 if (stepEventButton) { 644 stepEventButton.addEventListener("click", () => { 645 connection.send({ cmd: "step_event" }); 646 }); 647 } 648 649 // Run until button 650 const runUntilButton = document.getElementById("run-until-button"); 651 const runUntilInput = document.getElementById("run-until-input") as HTMLInputElement; 652 if (runUntilButton) { 653 runUntilButton.addEventListener("click", () => { 654 const until = parseFloat(runUntilInput?.value || "0"); 655 if (!isNaN(until)) { 656 connection.send({ cmd: "run_until", until }); 657 } 658 }); 659 } 660 661 // Send token button 662 const sendButton = document.getElementById("send-button"); 663 if (sendButton) { 664 sendButton.addEventListener("click", () => { 665 const target = parseInt( 666 (document.getElementById("send-target") as HTMLInputElement)?.value || "0" 667 ); 668 const offset = parseInt( 669 (document.getElementById("send-offset") as HTMLInputElement)?.value || "0" 670 ); 671 const ctx = parseInt( 672 (document.getElementById("send-ctx") as HTMLInputElement)?.value || "0" 673 ); 674 const data = parseInt( 675 (document.getElementById("send-data") as HTMLInputElement)?.value || "0" 676 ); 677 678 if (!isNaN(target) && !isNaN(offset) && !isNaN(ctx) && !isNaN(data)) { 679 connection.send({ cmd: "send", target, offset, ctx, data }); 680 } 681 }); 682 } 683 684 // Inject token button 685 const injectButton = document.getElementById("inject-button"); 686 if (injectButton) { 687 injectButton.addEventListener("click", () => { 688 const target = parseInt( 689 (document.getElementById("inject-target") as HTMLInputElement)?.value || "0" 690 ); 691 const offset = parseInt( 692 (document.getElementById("inject-offset") as HTMLInputElement)?.value || "0" 693 ); 694 const ctx = parseInt( 695 (document.getElementById("inject-ctx") as HTMLInputElement)?.value || "0" 696 ); 697 const data = parseInt( 698 (document.getElementById("inject-data") as HTMLInputElement)?.value || "0" 699 ); 700 701 if (!isNaN(target) && !isNaN(offset) && !isNaN(ctx) && !isNaN(data)) { 702 connection.send({ cmd: "inject", target, offset, ctx, data }); 703 } 704 }); 705 } 706 707 // Reset button 708 const resetButton = document.getElementById("reset-button"); 709 if (resetButton) { 710 resetButton.addEventListener("click", () => { 711 connection.send({ cmd: "reset", reload: true }); 712 }); 713 } 714 715 // Layout toggle 716 const layoutToggle = document.getElementById("layout-toggle"); 717 if (layoutToggle) { 718 layoutToggle.addEventListener("click", toggleLayout); 719 } 720 721 // Export buttons 722 const exportSvgButton = document.getElementById("export-svg-button"); 723 if (exportSvgButton) { 724 exportSvgButton.addEventListener("click", () => { 725 exportSvg(cy); 726 }); 727 } 728 729 const exportPngButton = document.getElementById("export-png-button"); 730 if (exportPngButton) { 731 exportPngButton.addEventListener("click", async () => { 732 try { 733 await exportPng(cy); 734 } catch (err) { 735 console.error("Export PNG failed:", err); 736 } 737 }); 738 } 739 740 const copyPngButton = document.getElementById("copy-png-button"); 741 if (copyPngButton) { 742 copyPngButton.addEventListener("click", async () => { 743 try { 744 await copyPng(cy); 745 } catch (err) { 746 console.error("Copy PNG failed:", err); 747 } 748 }); 749 } 750 751 // Event filter 752 const filterComponentSelect = document.getElementById("filter-component") as HTMLSelectElement; 753 const filterTypeSelect = document.getElementById("filter-type") as HTMLSelectElement; 754 755 function updateFilter(): void { 756 const logContainer = document.getElementById("event-log"); 757 setEventFilter( 758 { 759 component: filterComponentSelect?.value || null, 760 eventType: filterTypeSelect?.value || null, 761 }, 762 logContainer 763 ); 764 } 765 766 if (filterComponentSelect) { 767 filterComponentSelect.addEventListener("change", updateFilter); 768 } 769 if (filterTypeSelect) { 770 filterTypeSelect.addEventListener("change", updateFilter); 771 } 772} 773 774/** 775 * Main initialization. 776 */ 777function main(): void { 778 initializeCytoscape(); 779 setupNodeClickHandler(); 780 781 // Determine WebSocket URL 782 const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; 783 const host = window.location.host; 784 const wsUrl = `${protocol}//${host}/ws`; 785 786 console.log("[Monitor] Connecting to", wsUrl); 787 788 const connection = createConnection({ 789 url: wsUrl, 790 onMessage: (message: ServerMessage) => { 791 if (message.type === "graph_loaded") { 792 handleGraphLoaded(message); 793 } else if (message.type === "monitor_update") { 794 handleMonitorUpdate(message); 795 } else if (message.type === "reset") { 796 handleReset(message); 797 } else if (message.type === "error") { 798 console.error("[Monitor] Server error:", message.message); 799 } 800 }, 801 onConnect: () => { 802 console.log("[Monitor] Connected"); 803 const statusEl = document.getElementById("connection-status"); 804 if (statusEl) { 805 statusEl.textContent = "Connected"; 806 statusEl.style.color = rp.foam; 807 } 808 }, 809 onDisconnect: () => { 810 console.log("[Monitor] Disconnected"); 811 const statusEl = document.getElementById("connection-status"); 812 if (statusEl) { 813 statusEl.textContent = "Disconnected"; 814 statusEl.style.color = rp.love; 815 } 816 }, 817 }); 818 819 setupControlHandlers(connection); 820} 821 822// Start on DOM ready 823if (document.readyState === "loading") { 824 document.addEventListener("DOMContentLoaded", main); 825} else { 826 main(); 827}