methodgraph 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2350 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" data-theme="dark">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>methodgraph - Visual Execution Flow Debugger</title>
8
+ <style>
9
+ :root,
10
+ [data-theme="dark"] {
11
+ --bg-canvas: #090d16;
12
+ --bg-canvas-dots: rgba(255, 255, 255, 0.18);
13
+ --bg-surface: #0f172a;
14
+ --bg-card: #1e293b;
15
+ --bg-card-hover: #273549;
16
+ --bg-card-active: #1e2e4a;
17
+ --bg-pane: #0f172a;
18
+ --bg-subtle: rgba(255, 255, 255, 0.04);
19
+ --bg-header: rgba(15, 23, 42, 0.9);
20
+ --bg-dock: rgba(15, 23, 42, 0.92);
21
+ --bg-overlay: rgba(0, 0, 0, 0.25);
22
+
23
+ --border-subtle: #1e293b;
24
+ --border-card: #334155;
25
+ --border-active: #38bdf8;
26
+ --border-hover: #475569;
27
+
28
+ --text-primary: #f8fafc;
29
+ --text-secondary: #94a3b8;
30
+ --text-muted: #64748b;
31
+ --text-accent: #38bdf8;
32
+
33
+ --node-start: #10b981;
34
+ --node-end: #8b5cf6;
35
+ --node-success: #10b981;
36
+ --node-error: #f43f5e;
37
+ --node-warn: #f59e0b;
38
+ --node-cyan: #06b6d4;
39
+ --node-blue: #3b82f6;
40
+
41
+ --edge-default: #334155;
42
+ --marker-arrow: #475569;
43
+
44
+ --font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
45
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace;
46
+
47
+ --radius-sm: 6px;
48
+ --radius-md: 10px;
49
+ --radius-lg: 14px;
50
+ --radius-full: 9999px;
51
+
52
+ --shadow-card: 0 4px 20px -2px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
53
+ --shadow-glow: 0 0 25px rgba(56, 189, 248, 0.35);
54
+ --shadow-glow-err: 0 0 25px rgba(244, 63, 94, 0.4);
55
+ --shadow-glow-start: 0 0 20px rgba(16, 185, 129, 0.4);
56
+ }
57
+
58
+ [data-theme="light"] {
59
+ --bg-canvas: #ffffff;
60
+ --bg-canvas-dots: rgba(0, 0, 0, 0.15);
61
+ --bg-surface: #ffffff;
62
+ --bg-card: #ffffff;
63
+ --bg-card-hover: #f8fafc;
64
+ --bg-card-active: #f0f9ff;
65
+ --bg-pane: #ffffff;
66
+ --bg-subtle: #f8fafc;
67
+ --bg-header: #ffffff;
68
+ --bg-dock: #ffffff;
69
+ --bg-overlay: #f1f5f9;
70
+
71
+ --border-subtle: #f1f5f9;
72
+ --border-card: #e2e8f0;
73
+ --border-active: #0284c7;
74
+ --border-hover: #cbd5e1;
75
+
76
+ --text-primary: #0f172a;
77
+ --text-secondary: #334155;
78
+ --text-muted: #64748b;
79
+ --text-accent: #0284c7;
80
+
81
+ --node-start: #059669;
82
+ --node-end: #7c3aed;
83
+ --node-success: #059669;
84
+ --node-error: #e11d48;
85
+ --node-warn: #d97706;
86
+ --node-cyan: #0284c7;
87
+ --node-blue: #2563eb;
88
+
89
+ --edge-default: #94a3b8;
90
+ --marker-arrow: #64748b;
91
+
92
+ --shadow-card: 0 4px 16px -2px rgba(0, 0, 0, 0.06), 0 0 0 1px #e2e8f0;
93
+ --shadow-glow: 0 0 20px rgba(2, 132, 199, 0.25);
94
+ --shadow-glow-err: 0 0 20px rgba(225, 29, 72, 0.25);
95
+ --shadow-glow-start: 0 0 18px rgba(5, 150, 105, 0.25);
96
+ }
97
+
98
+ * {
99
+ box-sizing: border-box;
100
+ margin: 0;
101
+ padding: 0;
102
+ transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
103
+ }
104
+
105
+ body {
106
+ background-color: var(--bg-canvas);
107
+ color: var(--text-primary);
108
+ font-family: var(--font-sans);
109
+ height: 100vh;
110
+ width: 100vw;
111
+ overflow: hidden;
112
+ display: flex;
113
+ flex-direction: column;
114
+ user-select: none;
115
+ }
116
+
117
+ /* ─── APP HEADER ──────────────────────────────────────────── */
118
+ header {
119
+ height: 56px;
120
+ background: var(--bg-header);
121
+ backdrop-filter: blur(12px);
122
+ border-bottom: 1px solid var(--border-card);
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: space-between;
126
+ padding: 0 1.25rem;
127
+ z-index: 100;
128
+ flex-shrink: 0;
129
+ }
130
+
131
+ .brand-section {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 0.85rem;
135
+ }
136
+
137
+ .brand-icon {
138
+ width: 32px;
139
+ height: 32px;
140
+ background: linear-gradient(135deg, #38bdf8, #818cf8);
141
+ border-radius: 8px;
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ font-weight: 800;
146
+ font-size: 1rem;
147
+ color: #090d16;
148
+ box-shadow: 0 0 14px rgba(56, 189, 248, 0.4);
149
+ }
150
+
151
+ .brand-title {
152
+ display: flex;
153
+ flex-direction: column;
154
+ }
155
+
156
+ .brand-title h1 {
157
+ font-size: 1.05rem;
158
+ font-weight: 700;
159
+ letter-spacing: -0.2px;
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 0.5rem;
163
+ }
164
+
165
+ .brand-title .badge-tag {
166
+ font-size: 0.65rem;
167
+ font-weight: 700;
168
+ background: rgba(56, 189, 248, 0.15);
169
+ color: var(--text-accent);
170
+ padding: 0.1rem 0.4rem;
171
+ border-radius: var(--radius-full);
172
+ text-transform: uppercase;
173
+ letter-spacing: 0.5px;
174
+ border: 1px solid rgba(56, 189, 248, 0.3);
175
+ }
176
+
177
+ /* Top Nav View Switcher */
178
+ .view-switcher {
179
+ display: flex;
180
+ background: var(--bg-canvas);
181
+ padding: 3px;
182
+ border-radius: 8px;
183
+ border: 1px solid var(--border-card);
184
+ gap: 2px;
185
+ }
186
+
187
+ .view-tab-btn {
188
+ background: transparent;
189
+ border: none;
190
+ color: var(--text-secondary);
191
+ font-size: 0.8rem;
192
+ font-weight: 600;
193
+ padding: 0.35rem 0.85rem;
194
+ border-radius: 6px;
195
+ cursor: pointer;
196
+ display: flex;
197
+ align-items: center;
198
+ gap: 0.4rem;
199
+ transition: all 0.15s ease;
200
+ }
201
+
202
+ .view-tab-btn:hover {
203
+ color: var(--text-primary);
204
+ background: var(--bg-subtle);
205
+ }
206
+
207
+ .view-tab-btn.active {
208
+ background: var(--bg-card);
209
+ color: var(--text-accent);
210
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
211
+ }
212
+
213
+ /* Header Session Stats & Controls */
214
+ .session-stats {
215
+ display: flex;
216
+ align-items: center;
217
+ gap: 0.75rem;
218
+ }
219
+
220
+ .stat-pill {
221
+ background: var(--bg-card);
222
+ border: 1px solid var(--border-card);
223
+ border-radius: 6px;
224
+ padding: 0.25rem 0.65rem;
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 0.4rem;
228
+ font-size: 0.75rem;
229
+ font-family: var(--font-mono);
230
+ }
231
+
232
+ .stat-pill .label {
233
+ color: var(--text-muted);
234
+ text-transform: uppercase;
235
+ font-size: 0.65rem;
236
+ }
237
+
238
+ .stat-pill .val {
239
+ font-weight: 700;
240
+ color: var(--text-primary);
241
+ }
242
+
243
+ .status-badge {
244
+ font-size: 0.72rem;
245
+ font-weight: 700;
246
+ padding: 0.25rem 0.65rem;
247
+ border-radius: var(--radius-full);
248
+ display: flex;
249
+ align-items: center;
250
+ gap: 0.35rem;
251
+ text-transform: uppercase;
252
+ letter-spacing: 0.5px;
253
+ }
254
+
255
+ .status-badge.success {
256
+ background: rgba(16, 185, 129, 0.15);
257
+ color: var(--node-success);
258
+ border: 1px solid rgba(16, 185, 129, 0.3);
259
+ }
260
+
261
+ .status-badge.error {
262
+ background: rgba(244, 63, 94, 0.15);
263
+ color: var(--node-error);
264
+ border: 1px solid rgba(244, 63, 94, 0.3);
265
+ }
266
+
267
+ /* Header Action Buttons */
268
+ .header-action-btn {
269
+ background: var(--bg-card);
270
+ border: 1px solid var(--border-card);
271
+ color: var(--text-primary);
272
+ padding: 0.3rem 0.65rem;
273
+ border-radius: 6px;
274
+ font-size: 0.75rem;
275
+ font-weight: 600;
276
+ cursor: pointer;
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 0.4rem;
280
+ transition: all 0.15s ease;
281
+ }
282
+
283
+ .header-action-btn:hover {
284
+ border-color: var(--border-hover);
285
+ background: var(--bg-card-hover);
286
+ }
287
+
288
+ /* ─── MAIN WORKSPACE ───────────────────────────────────────── */
289
+ .workspace {
290
+ flex: 1;
291
+ display: flex;
292
+ position: relative;
293
+ overflow: hidden;
294
+ }
295
+
296
+ /* Views Container */
297
+ .view-panel {
298
+ flex: 1;
299
+ position: relative;
300
+ display: none;
301
+ height: 100%;
302
+ overflow: hidden;
303
+ }
304
+
305
+ .view-panel.active {
306
+ display: flex;
307
+ }
308
+
309
+ /* ─── FLOW CANVAS ───────────────────────────────── */
310
+ #flow-canvas-container {
311
+ flex: 1;
312
+ position: relative;
313
+ background-color: var(--bg-canvas);
314
+ background-image: radial-gradient(circle, var(--bg-canvas-dots) 1px, transparent 1px);
315
+ background-size: 24px 24px;
316
+ overflow: hidden;
317
+ cursor: grab;
318
+ }
319
+
320
+ #flow-canvas-container:active {
321
+ cursor: grabbing;
322
+ }
323
+
324
+ #graph-viewport {
325
+ position: absolute;
326
+ top: 0;
327
+ left: 0;
328
+ width: 100%;
329
+ height: 100%;
330
+ transform-origin: 0 0;
331
+ will-change: transform;
332
+ }
333
+
334
+ /* SVG Connections Layer */
335
+ #graph-svg-layer {
336
+ position: absolute;
337
+ top: 0;
338
+ left: 0;
339
+ width: 100%;
340
+ height: 100%;
341
+ overflow: visible;
342
+ pointer-events: none;
343
+ }
344
+
345
+ .flow-edge-path {
346
+ fill: none;
347
+ stroke: var(--edge-default);
348
+ stroke-width: 2px;
349
+ stroke-linecap: round;
350
+ transition: stroke 0.2s, stroke-width 0.2s;
351
+ }
352
+
353
+ .flow-edge-path.highlighted {
354
+ stroke: var(--text-accent);
355
+ stroke-width: 3px;
356
+ filter: drop-shadow(0 0 6px rgba(56, 189, 248, 0.6));
357
+ }
358
+
359
+ .flow-edge-path.error-flow {
360
+ stroke: var(--node-error);
361
+ stroke-width: 3px;
362
+ filter: drop-shadow(0 0 6px rgba(244, 63, 94, 0.6));
363
+ }
364
+
365
+ /* Animated Edge Pulse Flow */
366
+ .flow-edge-pulse {
367
+ fill: none;
368
+ stroke: var(--text-accent);
369
+ stroke-width: 3px;
370
+ stroke-dasharray: 8 16;
371
+ animation: dashPulse 1.2s linear infinite;
372
+ pointer-events: none;
373
+ opacity: 0;
374
+ }
375
+
376
+ .flow-edge-pulse.active {
377
+ opacity: 1;
378
+ }
379
+
380
+ @keyframes dashPulse {
381
+ from {
382
+ stroke-dashoffset: 24;
383
+ }
384
+
385
+ to {
386
+ stroke-dashoffset: 0;
387
+ }
388
+ }
389
+
390
+ /* HTML Nodes Layer */
391
+ #graph-nodes-layer {
392
+ position: absolute;
393
+ top: 0;
394
+ left: 0;
395
+ width: 100%;
396
+ height: 100%;
397
+ pointer-events: none;
398
+ }
399
+
400
+ /* ─── CARD NODES ─────────────────────────────────── */
401
+ .lg-node {
402
+ position: absolute;
403
+ pointer-events: auto;
404
+ cursor: pointer;
405
+ transition: transform 0.15s ease, box-shadow 0.2s ease, border-color 0.2s ease;
406
+ user-select: none;
407
+ }
408
+
409
+ /* Boundary Nodes: Start & End */
410
+ .lg-node-boundary {
411
+ padding: 0.65rem 1.25rem;
412
+ border-radius: var(--radius-full);
413
+ display: flex;
414
+ align-items: center;
415
+ gap: 0.6rem;
416
+ font-weight: 700;
417
+ font-size: 0.85rem;
418
+ letter-spacing: 0.5px;
419
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
420
+ border: 1.5px solid transparent;
421
+ }
422
+
423
+ .lg-node-boundary.start {
424
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(6, 78, 59, 0.3));
425
+ border-color: var(--node-start);
426
+ color: var(--node-start);
427
+ box-shadow: var(--shadow-glow-start);
428
+ }
429
+
430
+ .lg-node-boundary.end {
431
+ background: linear-gradient(135deg, rgba(139, 92, 246, 0.2), rgba(76, 29, 149, 0.3));
432
+ border-color: var(--node-end);
433
+ color: var(--node-end);
434
+ box-shadow: 0 0 20px rgba(139, 92, 246, 0.25);
435
+ }
436
+
437
+ /* Standard Function Card Node */
438
+ .lg-node-card {
439
+ width: 290px;
440
+ background: var(--bg-card);
441
+ border: 1.5px solid var(--border-card);
442
+ border-radius: var(--radius-md);
443
+ box-shadow: var(--shadow-card);
444
+ overflow: hidden;
445
+ display: flex;
446
+ flex-direction: column;
447
+ backdrop-filter: blur(8px);
448
+ }
449
+
450
+ .lg-node-card:hover {
451
+ border-color: var(--border-hover);
452
+ transform: translateY(-2px);
453
+ box-shadow: 0 8px 25px -4px rgba(0, 0, 0, 0.2), 0 0 0 1px var(--border-hover);
454
+ }
455
+
456
+ .lg-node-card.selected {
457
+ border-color: var(--border-active);
458
+ box-shadow: var(--shadow-glow), 0 8px 30px rgba(0, 0, 0, 0.25);
459
+ }
460
+
461
+ .lg-node-card.executing {
462
+ border-color: var(--text-accent);
463
+ animation: executingPulse 1.2s infinite alternate ease-in-out;
464
+ }
465
+
466
+ .lg-node-card.has-error {
467
+ border-color: var(--node-error);
468
+ }
469
+
470
+ .lg-node-card.has-error.selected {
471
+ box-shadow: var(--shadow-glow-err);
472
+ }
473
+
474
+ @keyframes executingPulse {
475
+ 0% {
476
+ box-shadow: 0 0 10px rgba(56, 189, 248, 0.3);
477
+ }
478
+
479
+ 100% {
480
+ box-shadow: 0 0 30px rgba(56, 189, 248, 0.8), 0 0 0 3px rgba(56, 189, 248, 0.4);
481
+ }
482
+ }
483
+
484
+ /* Node Card Header */
485
+ .lg-card-header {
486
+ padding: 0.6rem 0.85rem;
487
+ background: var(--bg-subtle);
488
+ border-bottom: 1px solid var(--border-subtle);
489
+ display: flex;
490
+ align-items: center;
491
+ justify-content: space-between;
492
+ gap: 0.5rem;
493
+ }
494
+
495
+ .lg-node-type-icon {
496
+ width: 22px;
497
+ height: 22px;
498
+ border-radius: 5px;
499
+ background: rgba(56, 189, 248, 0.15);
500
+ color: var(--text-accent);
501
+ display: flex;
502
+ align-items: center;
503
+ justify-content: center;
504
+ font-size: 0.75rem;
505
+ font-weight: 700;
506
+ flex-shrink: 0;
507
+ }
508
+
509
+ .lg-card-title-group {
510
+ flex: 1;
511
+ min-width: 0;
512
+ }
513
+
514
+ .lg-node-qualname {
515
+ font-size: 0.85rem;
516
+ font-weight: 700;
517
+ color: var(--text-primary);
518
+ white-space: nowrap;
519
+ overflow: hidden;
520
+ text-overflow: ellipsis;
521
+ font-family: var(--font-mono);
522
+ }
523
+
524
+ .lg-node-module {
525
+ font-size: 0.68rem;
526
+ color: var(--text-muted);
527
+ white-space: nowrap;
528
+ overflow: hidden;
529
+ text-overflow: ellipsis;
530
+ }
531
+
532
+ .lg-node-badge {
533
+ font-size: 0.65rem;
534
+ font-weight: 700;
535
+ padding: 0.15rem 0.45rem;
536
+ border-radius: 4px;
537
+ font-family: var(--font-mono);
538
+ flex-shrink: 0;
539
+ }
540
+
541
+ .lg-node-badge.badge-success {
542
+ background: rgba(16, 185, 129, 0.15);
543
+ color: var(--node-success);
544
+ border: 1px solid rgba(16, 185, 129, 0.3);
545
+ }
546
+
547
+ .lg-node-badge.badge-error {
548
+ background: rgba(244, 63, 94, 0.2);
549
+ color: var(--node-error);
550
+ border: 1px solid rgba(244, 63, 94, 0.4);
551
+ }
552
+
553
+ /* Node Card Body */
554
+ .lg-card-body {
555
+ padding: 0.65rem 0.85rem;
556
+ display: flex;
557
+ flex-direction: column;
558
+ gap: 0.45rem;
559
+ font-size: 0.74rem;
560
+ }
561
+
562
+ .lg-card-row {
563
+ display: flex;
564
+ align-items: flex-start;
565
+ gap: 0.4rem;
566
+ }
567
+
568
+ .lg-row-label {
569
+ color: var(--text-muted);
570
+ font-size: 0.68rem;
571
+ font-weight: 600;
572
+ text-transform: uppercase;
573
+ width: 48px;
574
+ flex-shrink: 0;
575
+ padding-top: 1px;
576
+ }
577
+
578
+ .lg-row-content {
579
+ flex: 1;
580
+ min-width: 0;
581
+ display: flex;
582
+ flex-wrap: wrap;
583
+ gap: 0.25rem;
584
+ }
585
+
586
+ .lg-arg-chip {
587
+ background: var(--bg-subtle);
588
+ border: 1px solid var(--border-subtle);
589
+ color: var(--text-secondary);
590
+ padding: 0.1rem 0.4rem;
591
+ border-radius: 4px;
592
+ font-family: var(--font-mono);
593
+ font-size: 0.68rem;
594
+ white-space: nowrap;
595
+ overflow: hidden;
596
+ text-overflow: ellipsis;
597
+ max-width: 195px;
598
+ }
599
+
600
+ .lg-arg-chip .chip-name {
601
+ color: var(--text-accent);
602
+ }
603
+
604
+ .lg-ret-snippet {
605
+ background: rgba(16, 185, 129, 0.08);
606
+ border: 1px solid rgba(16, 185, 129, 0.2);
607
+ color: var(--node-success);
608
+ padding: 0.15rem 0.45rem;
609
+ border-radius: 4px;
610
+ font-family: var(--font-mono);
611
+ font-size: 0.68rem;
612
+ white-space: nowrap;
613
+ overflow: hidden;
614
+ text-overflow: ellipsis;
615
+ max-width: 100%;
616
+ }
617
+
618
+ .lg-err-snippet {
619
+ background: rgba(244, 63, 94, 0.1);
620
+ border: 1px solid rgba(244, 63, 94, 0.3);
621
+ color: var(--node-error);
622
+ padding: 0.15rem 0.45rem;
623
+ border-radius: 4px;
624
+ font-family: var(--font-mono);
625
+ font-size: 0.68rem;
626
+ white-space: nowrap;
627
+ overflow: hidden;
628
+ text-overflow: ellipsis;
629
+ max-width: 100%;
630
+ }
631
+
632
+ /* Node Card Footer */
633
+ .lg-card-footer {
634
+ padding: 0.4rem 0.85rem;
635
+ background: var(--bg-overlay);
636
+ border-top: 1px solid var(--border-subtle);
637
+ display: flex;
638
+ align-items: center;
639
+ justify-content: space-between;
640
+ font-size: 0.68rem;
641
+ color: var(--text-muted);
642
+ font-family: var(--font-mono);
643
+ }
644
+
645
+ .lg-invocations-pill {
646
+ background: var(--bg-subtle);
647
+ border: 1px solid var(--border-subtle);
648
+ padding: 0.1rem 0.4rem;
649
+ border-radius: 3px;
650
+ font-weight: 700;
651
+ color: var(--text-primary);
652
+ }
653
+
654
+ /* Node Port Handles */
655
+ .lg-port-handle {
656
+ position: absolute;
657
+ width: 10px;
658
+ height: 10px;
659
+ background: var(--bg-card);
660
+ border: 2px solid var(--border-card);
661
+ border-radius: 50%;
662
+ z-index: 10;
663
+ }
664
+
665
+ .lg-port-handle.top {
666
+ top: -5px;
667
+ left: 50%;
668
+ transform: translateX(-50%);
669
+ }
670
+
671
+ .lg-port-handle.bottom {
672
+ bottom: -5px;
673
+ left: 50%;
674
+ transform: translateX(-50%);
675
+ }
676
+
677
+ .lg-port-handle.left {
678
+ left: -5px;
679
+ top: 50%;
680
+ transform: translateY(-50%);
681
+ }
682
+
683
+ .lg-port-handle.right {
684
+ right: -5px;
685
+ top: 50%;
686
+ transform: translateY(-50%);
687
+ }
688
+
689
+ /* ─── CANVAS FLOATING CONTROLS ─────────────────────────────── */
690
+ .floating-controls {
691
+ position: absolute;
692
+ top: 1.25rem;
693
+ left: 1.25rem;
694
+ display: flex;
695
+ gap: 0.5rem;
696
+ z-index: 20;
697
+ }
698
+
699
+ .control-group {
700
+ background: var(--bg-dock);
701
+ backdrop-filter: blur(10px);
702
+ border: 1px solid var(--border-card);
703
+ border-radius: 8px;
704
+ padding: 3px;
705
+ display: flex;
706
+ gap: 3px;
707
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
708
+ }
709
+
710
+ .canvas-btn {
711
+ background: transparent;
712
+ border: none;
713
+ color: var(--text-secondary);
714
+ width: 32px;
715
+ height: 32px;
716
+ border-radius: 6px;
717
+ cursor: pointer;
718
+ display: flex;
719
+ align-items: center;
720
+ justify-content: center;
721
+ font-size: 0.85rem;
722
+ transition: all 0.15s ease;
723
+ }
724
+
725
+ .canvas-btn:hover {
726
+ color: var(--text-primary);
727
+ background: var(--bg-subtle);
728
+ }
729
+
730
+ .canvas-btn.active {
731
+ background: var(--bg-card);
732
+ color: var(--text-accent);
733
+ }
734
+
735
+ /* ─── RUN PLAYBACK / WALKTHROUGH CONTROLLER ────────────────── */
736
+ .playback-dock {
737
+ position: absolute;
738
+ bottom: 1.25rem;
739
+ left: 50%;
740
+ transform: translateX(-50%);
741
+ background: var(--bg-dock);
742
+ backdrop-filter: blur(12px);
743
+ border: 1px solid var(--border-card);
744
+ border-radius: var(--radius-full);
745
+ padding: 0.4rem 1.25rem;
746
+ display: flex;
747
+ align-items: center;
748
+ gap: 1rem;
749
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2), 0 0 0 1px var(--border-subtle);
750
+ z-index: 30;
751
+ }
752
+
753
+ .playback-btn {
754
+ background: transparent;
755
+ border: none;
756
+ color: var(--text-secondary);
757
+ width: 32px;
758
+ height: 32px;
759
+ border-radius: 50%;
760
+ cursor: pointer;
761
+ display: flex;
762
+ align-items: center;
763
+ justify-content: center;
764
+ font-size: 0.9rem;
765
+ transition: all 0.15s ease;
766
+ }
767
+
768
+ .playback-btn:hover {
769
+ color: var(--text-primary);
770
+ background: var(--bg-subtle);
771
+ }
772
+
773
+ .playback-btn.primary {
774
+ background: linear-gradient(135deg, var(--node-cyan), var(--node-blue));
775
+ color: #ffffff;
776
+ font-size: 1rem;
777
+ font-weight: 800;
778
+ box-shadow: 0 0 12px rgba(6, 182, 212, 0.4);
779
+ }
780
+
781
+ .playback-info {
782
+ display: flex;
783
+ align-items: center;
784
+ gap: 0.6rem;
785
+ font-family: var(--font-mono);
786
+ font-size: 0.78rem;
787
+ color: var(--text-secondary);
788
+ min-width: 140px;
789
+ }
790
+
791
+ .playback-info .step-counter {
792
+ color: var(--text-accent);
793
+ font-weight: 700;
794
+ }
795
+
796
+ .playback-slider {
797
+ width: 140px;
798
+ accent-color: var(--text-accent);
799
+ cursor: pointer;
800
+ }
801
+
802
+ .playback-speed-select {
803
+ background: var(--bg-card);
804
+ border: 1px solid var(--border-card);
805
+ color: var(--text-secondary);
806
+ font-size: 0.72rem;
807
+ border-radius: 4px;
808
+ padding: 0.15rem 0.4rem;
809
+ cursor: pointer;
810
+ outline: none;
811
+ }
812
+
813
+ /* ─── STATE & RUN INSPECTOR (RIGHT SIDEBAR) ──────── */
814
+ .state-inspector-panel {
815
+ width: 440px;
816
+ background: var(--bg-surface);
817
+ border-left: 1px solid var(--border-card);
818
+ display: flex;
819
+ flex-direction: column;
820
+ height: 100%;
821
+ z-index: 50;
822
+ box-shadow: -6px 0 25px rgba(0, 0, 0, 0.15);
823
+ flex-shrink: 0;
824
+ transition: transform 0.25s ease, width 0.25s ease;
825
+ }
826
+
827
+ .state-inspector-panel.collapsed {
828
+ transform: translateX(100%);
829
+ position: absolute;
830
+ right: 0;
831
+ }
832
+
833
+ .inspector-header {
834
+ padding: 1rem 1.25rem;
835
+ border-bottom: 1px solid var(--border-card);
836
+ display: flex;
837
+ align-items: center;
838
+ justify-content: space-between;
839
+ background: var(--bg-subtle);
840
+ }
841
+
842
+ .inspector-title-area {
843
+ display: flex;
844
+ align-items: center;
845
+ gap: 0.6rem;
846
+ min-width: 0;
847
+ }
848
+
849
+ .inspector-title {
850
+ font-size: 0.95rem;
851
+ font-weight: 700;
852
+ color: var(--text-primary);
853
+ font-family: var(--font-mono);
854
+ white-space: nowrap;
855
+ overflow: hidden;
856
+ text-overflow: ellipsis;
857
+ }
858
+
859
+ .inspector-tabs {
860
+ display: flex;
861
+ border-bottom: 1px solid var(--border-subtle);
862
+ background: var(--bg-subtle);
863
+ padding: 0 1rem;
864
+ gap: 0.5rem;
865
+ }
866
+
867
+ .inspector-tab-btn {
868
+ background: transparent;
869
+ border: none;
870
+ color: var(--text-muted);
871
+ font-size: 0.78rem;
872
+ font-weight: 600;
873
+ padding: 0.6rem 0.6rem;
874
+ cursor: pointer;
875
+ border-bottom: 2px solid transparent;
876
+ transition: all 0.15s ease;
877
+ }
878
+
879
+ .inspector-tab-btn:hover {
880
+ color: var(--text-secondary);
881
+ }
882
+
883
+ .inspector-tab-btn.active {
884
+ color: var(--text-accent);
885
+ border-bottom-color: var(--text-accent);
886
+ }
887
+
888
+ .inspector-body {
889
+ flex: 1;
890
+ overflow-y: auto;
891
+ padding: 1.25rem;
892
+ display: flex;
893
+ flex-direction: column;
894
+ gap: 1.25rem;
895
+ }
896
+
897
+ /* State Inspector Section Cards */
898
+ .state-section-card {
899
+ background: var(--bg-card);
900
+ border: 1px solid var(--border-subtle);
901
+ border-radius: var(--radius-md);
902
+ overflow: hidden;
903
+ }
904
+
905
+ .state-section-header {
906
+ padding: 0.6rem 0.85rem;
907
+ background: var(--bg-overlay);
908
+ border-bottom: 1px solid var(--border-subtle);
909
+ display: flex;
910
+ align-items: center;
911
+ justify-content: space-between;
912
+ }
913
+
914
+ .state-section-title {
915
+ font-size: 0.72rem;
916
+ font-weight: 700;
917
+ text-transform: uppercase;
918
+ letter-spacing: 0.5px;
919
+ color: var(--text-secondary);
920
+ display: flex;
921
+ align-items: center;
922
+ gap: 0.4rem;
923
+ }
924
+
925
+ .copy-btn {
926
+ background: var(--bg-subtle);
927
+ border: 1px solid var(--border-subtle);
928
+ color: var(--text-muted);
929
+ padding: 0.15rem 0.45rem;
930
+ border-radius: 4px;
931
+ font-size: 0.68rem;
932
+ cursor: pointer;
933
+ transition: all 0.15s ease;
934
+ }
935
+
936
+ .copy-btn:hover {
937
+ color: var(--text-primary);
938
+ background: var(--bg-card-hover);
939
+ }
940
+
941
+ .state-section-content {
942
+ padding: 0.75rem 0.85rem;
943
+ font-family: var(--font-mono);
944
+ font-size: 0.8rem;
945
+ line-height: 1.5;
946
+ max-height: 260px;
947
+ overflow-y: auto;
948
+ }
949
+
950
+ /* JSON / Object Tree Formatter */
951
+ .tree-obj-key {
952
+ color: var(--text-accent);
953
+ }
954
+
955
+ .tree-obj-str {
956
+ color: var(--node-success);
957
+ }
958
+
959
+ .tree-obj-num {
960
+ color: var(--node-warn);
961
+ }
962
+
963
+ .tree-obj-bool {
964
+ color: #f472b6;
965
+ }
966
+
967
+ .tree-obj-type {
968
+ color: var(--text-muted);
969
+ font-size: 0.7rem;
970
+ font-style: italic;
971
+ }
972
+
973
+ .meta-grid {
974
+ display: grid;
975
+ grid-template-columns: repeat(2, 1fr);
976
+ gap: 0.6rem;
977
+ }
978
+
979
+ .meta-item {
980
+ background: var(--bg-subtle);
981
+ padding: 0.45rem 0.65rem;
982
+ border-radius: 6px;
983
+ border: 1px solid var(--border-subtle);
984
+ }
985
+
986
+ .meta-item .lbl {
987
+ font-size: 0.65rem;
988
+ color: var(--text-muted);
989
+ text-transform: uppercase;
990
+ margin-bottom: 2px;
991
+ }
992
+
993
+ .meta-item .val {
994
+ font-size: 0.78rem;
995
+ font-family: var(--font-mono);
996
+ color: var(--text-primary);
997
+ font-weight: 600;
998
+ white-space: nowrap;
999
+ overflow: hidden;
1000
+ text-overflow: ellipsis;
1001
+ }
1002
+
1003
+ /* ─── OTHER VIEWS (TIMELINE, TREE, DATA MATRIX) ───────────── */
1004
+ .generic-view-container {
1005
+ flex: 1;
1006
+ padding: 1.5rem 2rem;
1007
+ overflow-y: auto;
1008
+ background: var(--bg-canvas);
1009
+ }
1010
+
1011
+ /* Search & Filter Toolbars */
1012
+ .view-filter-bar {
1013
+ display: flex;
1014
+ align-items: center;
1015
+ justify-content: space-between;
1016
+ gap: 1rem;
1017
+ margin-bottom: 1.25rem;
1018
+ }
1019
+
1020
+ .search-input-box {
1021
+ position: relative;
1022
+ flex: 1;
1023
+ max-width: 450px;
1024
+ }
1025
+
1026
+ .search-input-box input {
1027
+ width: 100%;
1028
+ background: var(--bg-card);
1029
+ border: 1px solid var(--border-card);
1030
+ border-radius: 8px;
1031
+ padding: 0.5rem 1rem 0.5rem 2.25rem;
1032
+ color: var(--text-primary);
1033
+ font-size: 0.85rem;
1034
+ outline: none;
1035
+ }
1036
+
1037
+ .search-input-box input:focus {
1038
+ border-color: var(--text-accent);
1039
+ box-shadow: 0 0 10px rgba(56, 189, 248, 0.3);
1040
+ }
1041
+
1042
+ .search-input-box .icon {
1043
+ position: absolute;
1044
+ left: 0.75rem;
1045
+ top: 50%;
1046
+ transform: translateY(-50%);
1047
+ color: var(--text-muted);
1048
+ font-size: 0.85rem;
1049
+ }
1050
+
1051
+ /* Timeline Waterfall View */
1052
+ .timeline-card {
1053
+ background: var(--bg-surface);
1054
+ border: 1px solid var(--border-card);
1055
+ border-radius: var(--radius-md);
1056
+ padding: 1.25rem;
1057
+ overflow-x: auto;
1058
+ }
1059
+
1060
+ .timeline-row {
1061
+ display: flex;
1062
+ align-items: center;
1063
+ height: 36px;
1064
+ border-bottom: 1px solid var(--border-subtle);
1065
+ gap: 1rem;
1066
+ cursor: pointer;
1067
+ }
1068
+
1069
+ .timeline-row:hover {
1070
+ background: var(--bg-subtle);
1071
+ }
1072
+
1073
+ .timeline-node-name {
1074
+ width: 260px;
1075
+ font-family: var(--font-mono);
1076
+ font-size: 0.8rem;
1077
+ color: var(--text-primary);
1078
+ white-space: nowrap;
1079
+ overflow: hidden;
1080
+ text-overflow: ellipsis;
1081
+ }
1082
+
1083
+ .timeline-track-area {
1084
+ flex: 1;
1085
+ height: 100%;
1086
+ position: relative;
1087
+ background: var(--bg-overlay);
1088
+ border-radius: 4px;
1089
+ }
1090
+
1091
+ .timeline-span-bar {
1092
+ position: absolute;
1093
+ height: 20px;
1094
+ top: 8px;
1095
+ background: linear-gradient(90deg, #38bdf8, #818cf8);
1096
+ border-radius: 4px;
1097
+ box-shadow: 0 0 12px rgba(56, 189, 248, 0.3);
1098
+ display: flex;
1099
+ align-items: center;
1100
+ padding: 0 0.5rem;
1101
+ font-size: 0.68rem;
1102
+ font-family: var(--font-mono);
1103
+ color: #fff;
1104
+ white-space: nowrap;
1105
+ overflow: hidden;
1106
+ }
1107
+
1108
+ .timeline-span-bar.error-span {
1109
+ background: linear-gradient(90deg, #f43f5e, #fb7185);
1110
+ box-shadow: 0 0 12px rgba(244, 63, 94, 0.3);
1111
+ }
1112
+
1113
+ /* Call Tree View */
1114
+ .calltree-card {
1115
+ background: var(--bg-surface);
1116
+ border: 1px solid var(--border-card);
1117
+ border-radius: var(--radius-md);
1118
+ padding: 1.25rem;
1119
+ font-family: var(--font-mono);
1120
+ }
1121
+
1122
+ .tree-node-item {
1123
+ margin-left: 1.5rem;
1124
+ position: relative;
1125
+ }
1126
+
1127
+ .tree-node-item.root-item {
1128
+ margin-left: 0;
1129
+ }
1130
+
1131
+ .tree-node-row {
1132
+ display: flex;
1133
+ align-items: center;
1134
+ gap: 0.5rem;
1135
+ padding: 0.45rem 0.75rem;
1136
+ background: var(--bg-card);
1137
+ border: 1px solid var(--border-subtle);
1138
+ border-radius: 6px;
1139
+ margin-bottom: 0.35rem;
1140
+ cursor: pointer;
1141
+ transition: all 0.15s ease;
1142
+ }
1143
+
1144
+ .tree-node-row:hover {
1145
+ border-color: var(--text-accent);
1146
+ transform: translateX(2px);
1147
+ }
1148
+
1149
+ /* Data Matrix Table */
1150
+ .datamatrix-card {
1151
+ background: var(--bg-surface);
1152
+ border: 1px solid var(--border-card);
1153
+ border-radius: var(--radius-md);
1154
+ overflow: hidden;
1155
+ }
1156
+
1157
+ table.lg-table {
1158
+ width: 100%;
1159
+ border-collapse: collapse;
1160
+ font-size: 0.82rem;
1161
+ }
1162
+
1163
+ table.lg-table th {
1164
+ background: var(--bg-subtle);
1165
+ color: var(--text-muted);
1166
+ text-transform: uppercase;
1167
+ font-size: 0.68rem;
1168
+ font-weight: 700;
1169
+ letter-spacing: 0.5px;
1170
+ text-align: left;
1171
+ padding: 0.75rem 1rem;
1172
+ border-bottom: 1px solid var(--border-card);
1173
+ }
1174
+
1175
+ table.lg-table td {
1176
+ padding: 0.75rem 1rem;
1177
+ border-bottom: 1px solid var(--border-subtle);
1178
+ font-family: var(--font-mono);
1179
+ }
1180
+
1181
+ table.lg-table tr:hover {
1182
+ background: var(--bg-subtle);
1183
+ cursor: pointer;
1184
+ }
1185
+ </style>
1186
+ </head>
1187
+
1188
+ <body>
1189
+
1190
+ <!-- ─── HEADER ─────────────────────────────────────────────── -->
1191
+ <header>
1192
+ <div class="brand-section">
1193
+ <div class="brand-icon">⚡</div>
1194
+ <div class="brand-title">
1195
+ <h1>methodgraph</h1>
1196
+ </div>
1197
+ </div>
1198
+
1199
+ <!-- View Tabs -->
1200
+ <div class="view-switcher">
1201
+ <button class="view-tab-btn active" onclick="switchView('flow')">⚡ Flow Graph</button>
1202
+ <button class="view-tab-btn" onclick="switchView('timeline')">⏱️ Timeline Waterfall</button>
1203
+ <button class="view-tab-btn" onclick="switchView('tree')">🌳 Call Tree</button>
1204
+ <button class="view-tab-btn" onclick="switchView('table')">📊 Data Matrix</button>
1205
+ </div>
1206
+
1207
+ <!-- Session Statistics & Controls -->
1208
+ <div class="session-stats">
1209
+ <div class="stat-pill">
1210
+ <span class="label">Total Latency</span>
1211
+ <span class="val" id="stat-total-duration">0.00 ms</span>
1212
+ </div>
1213
+ <div class="stat-pill">
1214
+ <span class="label">Nodes</span>
1215
+ <span class="val" id="stat-nodes-count">0</span>
1216
+ </div>
1217
+ <div class="stat-pill">
1218
+ <span class="label">Runs</span>
1219
+ <span class="val" id="stat-runs-count">0</span>
1220
+ </div>
1221
+ <div class="status-badge success" id="session-status-badge">✓ COMPLETED</div>
1222
+
1223
+ <!-- Theme Toggle Button -->
1224
+ <button class="header-action-btn" id="btn-theme-toggle" onclick="toggleTheme()"
1225
+ title="Toggle Light / Dark Theme">
1226
+ <span id="theme-icon">🌙</span> <span id="theme-text">Dark</span>
1227
+ </button>
1228
+
1229
+ <!-- Inspector Toggle Button -->
1230
+ <button class="header-action-btn" id="btn-inspector-toggle" onclick="toggleInspectorCollapse()"
1231
+ title="Toggle Inspector Sidebar">
1232
+ <span>🔬</span> <span>Inspector</span>
1233
+ </button>
1234
+ </div>
1235
+ </header>
1236
+
1237
+ <!-- ─── MAIN WORKSPACE ────────────────────────────────────── -->
1238
+ <div class="workspace">
1239
+
1240
+ <!-- 1. FLOW VIEW (CANVAS) -->
1241
+ <div id="view-flow" class="view-panel active">
1242
+
1243
+ <div id="flow-canvas-container">
1244
+ <!-- Floating Controls Toolbar -->
1245
+ <div class="floating-controls">
1246
+ <div class="control-group">
1247
+ <button class="canvas-btn" onclick="zoomIn()" title="Zoom In">➕</button>
1248
+ <button class="canvas-btn" onclick="zoomOut()" title="Zoom Out">➖</button>
1249
+ <button class="canvas-btn" onclick="resetZoom()" title="Reset Zoom">🔍</button>
1250
+ <button class="canvas-btn" onclick="fitView()" title="Fit View">⛶</button>
1251
+ </div>
1252
+ <div class="control-group">
1253
+ <button class="canvas-btn active" id="btn-layout-tb" onclick="setLayout('TB')"
1254
+ title="Top to Bottom Layout">⬇️ TB</button>
1255
+ <button class="canvas-btn" id="btn-layout-lr" onclick="setLayout('LR')"
1256
+ title="Left to Right Layout">➡️ LR</button>
1257
+ </div>
1258
+ </div>
1259
+
1260
+ <!-- Playback Scrubber Bar -->
1261
+ <div class="playback-dock">
1262
+ <button class="playback-btn" onclick="stepBackward()" title="Previous Step">⏮</button>
1263
+ <button class="playback-btn primary" id="btn-play-pause" onclick="togglePlay()"
1264
+ title="Play / Pause Trace">▶</button>
1265
+ <button class="playback-btn" onclick="stepForward()" title="Next Step">⏭</button>
1266
+
1267
+ <div class="playback-info">
1268
+ <span>Step <span class="step-counter" id="lbl-current-step">1</span> / <span
1269
+ id="lbl-total-steps">1</span></span>
1270
+ </div>
1271
+
1272
+ <input type="range" class="playback-slider" id="playback-slider" min="1" max="1" value="1" step="1" list="playback-ticks"
1273
+ oninput="onScrub(this.value)">
1274
+ <datalist id="playback-ticks"></datalist>
1275
+
1276
+ <select class="playback-speed-select" id="playback-speed" onchange="changeSpeed(this.value)">
1277
+ <option value="1500">0.5x</option>
1278
+ <option value="800" selected>1x</option>
1279
+ <option value="400">2x</option>
1280
+ <option value="150">5x</option>
1281
+ </select>
1282
+ </div>
1283
+
1284
+ <!-- Canvas Viewport -->
1285
+ <div id="graph-viewport">
1286
+ <svg id="graph-svg-layer">
1287
+ <defs>
1288
+ <!-- Marker Arrowhead for Standard Edges -->
1289
+ <marker id="arrowhead" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6"
1290
+ markerHeight="6" orient="auto-start-reverse">
1291
+ <path d="M 0 1.5 L 10 5 L 0 8.5 z" fill="#64748b"></path>
1292
+ </marker>
1293
+ <!-- Marker Arrowhead for Highlighted Edges -->
1294
+ <marker id="arrowhead-highlight" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6"
1295
+ markerHeight="6" orient="auto-start-reverse">
1296
+ <path d="M 0 1.5 L 10 5 L 0 8.5 z" fill="#38bdf8"></path>
1297
+ </marker>
1298
+ <!-- Marker Arrowhead for Error Edges -->
1299
+ <marker id="arrowhead-error" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6"
1300
+ markerHeight="6" orient="auto-start-reverse">
1301
+ <path d="M 0 1.5 L 10 5 L 0 8.5 z" fill="#f43f5e"></path>
1302
+ </marker>
1303
+ </defs>
1304
+ <g id="svg-edges-group"></g>
1305
+ <g id="svg-pulses-group"></g>
1306
+ </svg>
1307
+ <div id="graph-nodes-layer"></div>
1308
+ </div>
1309
+ </div>
1310
+
1311
+ <!-- Right Sidebar: State & Node Inspector -->
1312
+ <div class="state-inspector-panel" id="inspector-panel">
1313
+ <div class="inspector-header">
1314
+ <div class="inspector-title-area">
1315
+ <span style="font-size: 1.1rem;">🔬</span>
1316
+ <div class="inspector-title" id="insp-header-title">Node Inspector</div>
1317
+ </div>
1318
+ <button class="copy-btn" onclick="toggleInspectorCollapse()">⇥</button>
1319
+ </div>
1320
+
1321
+ <div class="inspector-tabs">
1322
+ <button class="inspector-tab-btn active" onclick="switchInspectorTab('state')">State &
1323
+ Values</button>
1324
+ <button class="inspector-tab-btn" onclick="switchInspectorTab('metadata')">Execution Info</button>
1325
+ <button class="inspector-tab-btn" onclick="switchInspectorTab('history')">Runs History</button>
1326
+ </div>
1327
+
1328
+ <div class="inspector-body" id="insp-tab-state">
1329
+ <!-- Inputs Card -->
1330
+ <div class="state-section-card">
1331
+ <div class="state-section-header">
1332
+ <span class="state-section-title">📥 Inputs (Args & Kwargs)</span>
1333
+ <button class="copy-btn" onclick="copySectionContent('insp-args-content')">Copy</button>
1334
+ </div>
1335
+ <div class="state-section-content" id="insp-args-content">Select a node to inspect arguments.
1336
+ </div>
1337
+ </div>
1338
+
1339
+ <!-- Outputs Card -->
1340
+ <div class="state-section-card">
1341
+ <div class="state-section-header">
1342
+ <span class="state-section-title">📤 Output / Return Value</span>
1343
+ <button class="copy-btn" onclick="copySectionContent('insp-ret-content')">Copy</button>
1344
+ </div>
1345
+ <div class="state-section-content" id="insp-ret-content">Select a node to inspect return value.
1346
+ </div>
1347
+ </div>
1348
+
1349
+ <!-- Exception Card (if any) -->
1350
+ <div class="state-section-card" id="insp-exc-section"
1351
+ style="display: none; border-color: rgba(244, 63, 94, 0.4);">
1352
+ <div class="state-section-header" style="background: rgba(244, 63, 94, 0.15);">
1353
+ <span class="state-section-title" style="color: #fb7185;">⚠️ Exception Traceback</span>
1354
+ <button class="copy-btn" onclick="copySectionContent('insp-exc-content')">Copy</button>
1355
+ </div>
1356
+ <div class="state-section-content" id="insp-exc-content" style="color: #fca5a5;"></div>
1357
+ </div>
1358
+ </div>
1359
+
1360
+ <div class="inspector-body" id="insp-tab-metadata" style="display: none;">
1361
+ <div class="meta-grid">
1362
+ <div class="meta-item">
1363
+ <div class="lbl">Execution Time</div>
1364
+ <div class="val" id="meta-duration" style="color: var(--node-warn);">0 ms</div>
1365
+ </div>
1366
+ <div class="meta-item">
1367
+ <div class="lbl">Status</div>
1368
+ <div class="val" id="meta-status" style="color: var(--node-success);">SUCCESS</div>
1369
+ </div>
1370
+ <div class="meta-item">
1371
+ <div class="lbl">File Path</div>
1372
+ <div class="val" id="meta-file" title="">-</div>
1373
+ </div>
1374
+ <div class="meta-item">
1375
+ <div class="lbl">Line Number</div>
1376
+ <div class="val" id="meta-line">-</div>
1377
+ </div>
1378
+ <div class="meta-item">
1379
+ <div class="lbl">Call Depth</div>
1380
+ <div class="val" id="meta-depth">-</div>
1381
+ </div>
1382
+ <div class="meta-item">
1383
+ <div class="lbl">Call ID</div>
1384
+ <div class="val" id="meta-callid">-</div>
1385
+ </div>
1386
+ </div>
1387
+ </div>
1388
+
1389
+ <div class="inspector-body" id="insp-tab-history" style="display: none;">
1390
+ <div id="insp-history-list" style="display: flex; flex-direction: column; gap: 0.5rem;"></div>
1391
+ </div>
1392
+ </div>
1393
+ </div>
1394
+
1395
+ <!-- 2. TIMELINE WATERFALL VIEW -->
1396
+ <div id="view-timeline" class="view-panel">
1397
+ <div class="generic-view-container">
1398
+ <div class="view-filter-bar">
1399
+ <h2 style="font-size: 1.1rem; font-weight: 700;">⏱️ Execution Timeline (Trace Spans)</h2>
1400
+ </div>
1401
+ <div class="timeline-card" id="timeline-spans-container"></div>
1402
+ </div>
1403
+ </div>
1404
+
1405
+ <!-- 3. CALL TREE VIEW -->
1406
+ <div id="view-tree" class="view-panel">
1407
+ <div class="generic-view-container">
1408
+ <div class="view-filter-bar">
1409
+ <h2 style="font-size: 1.1rem; font-weight: 700;">🌳 Hierarchical Call Tree</h2>
1410
+ </div>
1411
+ <div class="calltree-card" id="calltree-root-container"></div>
1412
+ </div>
1413
+ </div>
1414
+
1415
+ <!-- 4. DATA MATRIX TABLE VIEW -->
1416
+ <div id="view-table" class="view-panel">
1417
+ <div class="generic-view-container">
1418
+ <div class="view-filter-bar">
1419
+ <div class="search-input-box">
1420
+ <span class="icon">🔍</span>
1421
+ <input type="text" id="table-search-input"
1422
+ placeholder="Search function, parameters, values, or errors..."
1423
+ oninput="handleTableSearch(this.value)">
1424
+ </div>
1425
+ </div>
1426
+ <div class="datamatrix-card">
1427
+ <table class="lg-table">
1428
+ <thead>
1429
+ <tr>
1430
+ <th>ID</th>
1431
+ <th>Function</th>
1432
+ <th>Passed Arguments</th>
1433
+ <th>Result / Return</th>
1434
+ <th>Duration</th>
1435
+ <th>Status</th>
1436
+ </tr>
1437
+ </thead>
1438
+ <tbody id="datamatrix-tbody"></tbody>
1439
+ </table>
1440
+ </div>
1441
+ </div>
1442
+ </div>
1443
+
1444
+ </div>
1445
+
1446
+ <!-- RAW TRACE DATA INJECTION -->
1447
+ <script id="trace-data" type="application/json">
1448
+ __DATA_PLACEHOLDER__
1449
+ </script>
1450
+
1451
+ <!-- ─── JAVASCRIPT ENGINE ─────────────────────────────────── -->
1452
+ <script>
1453
+ // ─── PARSE EMBEDDED DATA ───────────────────────────────────
1454
+ const rawTrace = JSON.parse(document.getElementById('trace-data').textContent);
1455
+ const { flat_records: records = [], tree = [], graph = {}, steps = [], total_calls = 0, total_session_ms = 0 } = rawTrace;
1456
+ const nodesData = graph.nodes || [];
1457
+ const edgesData = graph.edges || [];
1458
+
1459
+ // Global State
1460
+ let currentTheme = 'dark';
1461
+ let currentLayout = 'TB'; // 'TB' (top-to-bottom) or 'LR' (left-to-right)
1462
+ let transform = { x: 80, y: 80, scale: 0.9 };
1463
+ let isDragging = false;
1464
+ let startPan = { x: 0, y: 0 };
1465
+ let selectedNodeId = null;
1466
+ let selectedCallId = null;
1467
+
1468
+ // Playback State
1469
+ let currentStepIndex = 1;
1470
+ let isPlaying = false;
1471
+ let playbackInterval = null;
1472
+ let playbackSpeedMs = 800;
1473
+
1474
+ // Computed Node Coordinates Map
1475
+ let nodePositions = {};
1476
+ const NODE_WIDTH = 290;
1477
+ const NODE_HEIGHT = 135;
1478
+ const BOUNDARY_WIDTH = 130;
1479
+ const BOUNDARY_HEIGHT = 44;
1480
+
1481
+ // ─── THEME ENGINE (LIGHT & DARK) ───────────────────────────
1482
+ function initTheme() {
1483
+ try {
1484
+ const saved = localStorage.getItem('methodgraph_theme') || localStorage.getItem('methodviz_theme');
1485
+ if (saved === 'light' || saved === 'dark') {
1486
+ setTheme(saved);
1487
+ return;
1488
+ }
1489
+ } catch (e) { }
1490
+
1491
+ // Check system preference
1492
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) {
1493
+ setTheme('light');
1494
+ } else {
1495
+ setTheme('dark');
1496
+ }
1497
+ }
1498
+
1499
+ function toggleTheme() {
1500
+ setTheme(currentTheme === 'dark' ? 'light' : 'dark');
1501
+ }
1502
+
1503
+ function setTheme(theme) {
1504
+ currentTheme = theme;
1505
+ document.documentElement.setAttribute('data-theme', theme);
1506
+ const iconEl = document.getElementById('theme-icon');
1507
+ const textEl = document.getElementById('theme-text');
1508
+ if (iconEl && textEl) {
1509
+ iconEl.innerText = theme === 'dark' ? '☀️' : '🌙';
1510
+ textEl.innerText = theme === 'dark' ? 'Light' : 'Dark';
1511
+ }
1512
+ try {
1513
+ localStorage.setItem('methodgraph_theme', theme);
1514
+ } catch (e) { }
1515
+
1516
+ // Update markers colors
1517
+ const arrowPath = document.querySelector('#arrowhead path');
1518
+ if (arrowPath) {
1519
+ arrowPath.setAttribute('fill', theme === 'dark' ? '#475569' : '#64748b');
1520
+ }
1521
+ }
1522
+
1523
+ // ─── INITIALIZE HEADER STATS ───────────────────────────────
1524
+ function initStats() {
1525
+ document.getElementById('stat-total-duration').innerText = `${total_session_ms || 0} ms`;
1526
+ document.getElementById('stat-nodes-count').innerText = nodesData.length;
1527
+ document.getElementById('stat-runs-count').innerText = total_calls;
1528
+
1529
+ const hasException = records.some(r => r.exception);
1530
+ const badge = document.getElementById('session-status-badge');
1531
+ if (hasException) {
1532
+ badge.className = 'status-badge error';
1533
+ badge.innerText = '✕ ERROR / EXCEPTION';
1534
+ } else {
1535
+ badge.className = 'status-badge success';
1536
+ badge.innerText = '✓ COMPLETED';
1537
+ }
1538
+
1539
+ document.getElementById('lbl-total-steps').innerText = steps.length || 1;
1540
+ const slider = document.getElementById('playback-slider');
1541
+ const sliderMax = steps.length || 1;
1542
+ slider.max = sliderMax;
1543
+ slider.value = 1;
1544
+
1545
+ const datalist = document.getElementById('playback-ticks');
1546
+ if (datalist) {
1547
+ datalist.innerHTML = '';
1548
+ for (let i = 1; i <= sliderMax; i++) {
1549
+ const opt = document.createElement('option');
1550
+ opt.value = i;
1551
+ datalist.appendChild(opt);
1552
+ }
1553
+ }
1554
+ }
1555
+ initStats();
1556
+
1557
+ // ─── VIEW SWITCHING ─────────────────────────────────────────
1558
+ function switchView(viewId) {
1559
+ document.querySelectorAll('.view-tab-btn').forEach(b => b.classList.remove('active'));
1560
+ document.querySelectorAll('.view-panel').forEach(p => p.classList.remove('active'));
1561
+
1562
+ event.target.classList.add('active');
1563
+ const panel = document.getElementById(`view-${viewId}`);
1564
+ if (panel) panel.classList.add('active');
1565
+
1566
+ if (viewId === 'flow') {
1567
+ updateTransform();
1568
+ }
1569
+ }
1570
+
1571
+ // ─── GRAPH LAYOUT ALGORITHM (SUGIYAMA / LAYERED DAG) ───────
1572
+ function computeGraphLayout(orientation = 'TB') {
1573
+ nodePositions = {};
1574
+ if (!nodesData.length) return;
1575
+
1576
+ // 1. Build Adjacency and In-Degree maps
1577
+ const adj = {};
1578
+ const inDegree = {};
1579
+ const allNodeIds = ['__start__', ...nodesData.map(n => n.id), '__end__'];
1580
+
1581
+ allNodeIds.forEach(id => {
1582
+ adj[id] = [];
1583
+ inDegree[id] = 0;
1584
+ });
1585
+
1586
+ // Start connections
1587
+ const rootIds = graph.root_nodes || [nodesData[0].id];
1588
+ rootIds.forEach(rid => {
1589
+ if (adj['__start__']) adj['__start__'].push(rid);
1590
+ if (inDegree[rid] !== undefined) inDegree[rid]++;
1591
+ });
1592
+
1593
+ // Inter-node edges
1594
+ edgesData.forEach(e => {
1595
+ if (adj[e.source] && inDegree[e.target] !== undefined) {
1596
+ adj[e.source].push(e.target);
1597
+ inDegree[e.target]++;
1598
+ }
1599
+ });
1600
+
1601
+ // Leaf nodes to End connections
1602
+ const leafIds = graph.leaf_nodes || [nodesData[nodesData.length - 1].id];
1603
+ leafIds.forEach(lid => {
1604
+ if (adj[lid]) adj[lid].push('__end__');
1605
+ if (inDegree['__end__'] !== undefined) inDegree['__end__']++;
1606
+ });
1607
+
1608
+ // 2. Layer Assignment using iterative BFS with cycle detection
1609
+ const nodeLayer = { '__start__': 0 };
1610
+ const maxDepth = allNodeIds.length + 2;
1611
+
1612
+ const queue = [{ id: '__start__', layer: 0, path: new Set(['__start__']) }];
1613
+
1614
+ while (queue.length > 0) {
1615
+ const { id, layer, path } = queue.shift();
1616
+ if (layer >= maxDepth) continue;
1617
+
1618
+ const neighbors = adj[id] || [];
1619
+ neighbors.forEach(nxt => {
1620
+ if (nxt === id) return; // Skip self-loops in layering
1621
+
1622
+ const nextLayer = layer + 1;
1623
+ if (nextLayer > (nodeLayer[nxt] || 0)) {
1624
+ nodeLayer[nxt] = nextLayer;
1625
+ }
1626
+
1627
+ if (!path.has(nxt) && path.size < maxDepth) {
1628
+ const newPath = new Set(path);
1629
+ newPath.add(nxt);
1630
+ queue.push({ id: nxt, layer: nextLayer, path: newPath });
1631
+ }
1632
+ });
1633
+ }
1634
+
1635
+ // Ensure all nodes have a valid layer >= 1
1636
+ allNodeIds.forEach(id => {
1637
+ if (id !== '__start__' && (nodeLayer[id] === undefined || nodeLayer[id] <= 0)) {
1638
+ nodeLayer[id] = 1;
1639
+ }
1640
+ });
1641
+
1642
+ // Ensure __end__ is at the deepest layer
1643
+ let maxNodeLayer = 1;
1644
+ nodesData.forEach(n => {
1645
+ if (nodeLayer[n.id] && nodeLayer[n.id] > maxNodeLayer) {
1646
+ maxNodeLayer = nodeLayer[n.id];
1647
+ }
1648
+ });
1649
+ nodeLayer['__end__'] = Math.max(nodeLayer['__end__'] || 0, maxNodeLayer + 1);
1650
+
1651
+ // Group nodes by layer
1652
+ const layers = {};
1653
+ Object.entries(nodeLayer).forEach(([nid, l]) => {
1654
+ if (!layers[l]) layers[l] = [];
1655
+ if (!layers[l].includes(nid)) layers[l].push(nid);
1656
+ });
1657
+
1658
+ // 3. Compute Coordinates
1659
+ const layerKeys = Object.keys(layers).map(Number).sort((a, b) => a - b);
1660
+ const levelGap = orientation === 'TB' ? 200 : 380;
1661
+ const nodeGap = orientation === 'TB' ? 340 : 180;
1662
+
1663
+ layerKeys.forEach(l => {
1664
+ const layerNodes = layers[l];
1665
+ const totalInLayer = layerNodes.length;
1666
+
1667
+ layerNodes.forEach((nid, idx) => {
1668
+ const offset = (idx - (totalInLayer - 1) / 2) * nodeGap;
1669
+ const isBoundary = (nid === '__start__' || nid === '__end__');
1670
+ const w = isBoundary ? BOUNDARY_WIDTH : NODE_WIDTH;
1671
+ const h = isBoundary ? BOUNDARY_HEIGHT : NODE_HEIGHT;
1672
+
1673
+ if (orientation === 'TB') {
1674
+ nodePositions[nid] = {
1675
+ x: 450 + offset - (w / 2),
1676
+ y: 80 + l * levelGap,
1677
+ w,
1678
+ h,
1679
+ isBoundary,
1680
+ };
1681
+ } else {
1682
+ // Left to Right
1683
+ nodePositions[nid] = {
1684
+ x: 80 + l * levelGap,
1685
+ y: 350 + offset - (h / 2),
1686
+ w,
1687
+ h,
1688
+ isBoundary,
1689
+ };
1690
+ }
1691
+ });
1692
+ });
1693
+ }
1694
+
1695
+ // ─── RENDER GRAPH NODES & BEZIER EDGES ─────────────────────
1696
+ function renderGraphCanvas() {
1697
+ computeGraphLayout(currentLayout);
1698
+ const nodesContainer = document.getElementById('graph-nodes-layer');
1699
+ const edgesGroup = document.getElementById('svg-edges-group');
1700
+ const pulsesGroup = document.getElementById('svg-pulses-group');
1701
+
1702
+ nodesContainer.innerHTML = '';
1703
+ edgesGroup.innerHTML = '';
1704
+ pulsesGroup.innerHTML = '';
1705
+
1706
+ // 1. Render Boundary Nodes (__start__ and __end__)
1707
+ renderBoundaryNode('__start__', '▶ START', 'start');
1708
+ renderBoundaryNode('__end__', '⚑ END', 'end');
1709
+
1710
+ // 2. Render Function Card Nodes
1711
+ nodesData.forEach(n => {
1712
+ const pos = nodePositions[n.id];
1713
+ if (!pos) return;
1714
+
1715
+ const card = document.createElement('div');
1716
+ card.className = `lg-node lg-node-card ${n.exceptions_count > 0 ? 'has-error' : ''}`;
1717
+ card.id = `node-${sanitizeId(n.id)}`;
1718
+ card.style.left = `${pos.x}px`;
1719
+ card.style.top = `${pos.y}px`;
1720
+ card.style.width = `${pos.w}px`;
1721
+
1722
+ card.onclick = (e) => {
1723
+ e.stopPropagation();
1724
+ selectNode(n.id);
1725
+ };
1726
+
1727
+ // Node Port Handles
1728
+ const topPort = currentLayout === 'TB' ? 'top' : 'left';
1729
+ const botPort = currentLayout === 'TB' ? 'bottom' : 'right';
1730
+
1731
+ // Args & Return summary
1732
+ let argsChips = '';
1733
+ if (n.sample_args && n.sample_args.length > 0) {
1734
+ n.sample_args.slice(0, 2).forEach(a => {
1735
+ argsChips += `<span class="lg-arg-chip"><span class="chip-name">${escapeHtml(a.name)}:</span> ${escapeHtml(a.val_info?.value || '')}</span>`;
1736
+ });
1737
+ }
1738
+ if (n.sample_kwargs) {
1739
+ Object.entries(n.sample_kwargs).slice(0, 2).forEach(([k, v]) => {
1740
+ argsChips += `<span class="lg-arg-chip"><span class="chip-name">${escapeHtml(k)}:</span> ${escapeHtml(v.value || '')}</span>`;
1741
+ });
1742
+ }
1743
+ if (!argsChips) argsChips = '<span style="color:var(--text-muted); font-size:0.68rem;">(no arguments)</span>';
1744
+
1745
+ let returnBadge = '';
1746
+ if (n.sample_exception) {
1747
+ returnBadge = `<span class="lg-err-snippet">⚠️ ${escapeHtml(n.sample_exception.type)}: ${escapeHtml(n.sample_exception.message)}</span>`;
1748
+ } else if (n.sample_return) {
1749
+ returnBadge = `<span class="lg-ret-snippet">➜ ${escapeHtml(n.sample_return.value)}</span>`;
1750
+ } else {
1751
+ returnBadge = `<span class="lg-ret-snippet">➜ None</span>`;
1752
+ }
1753
+
1754
+ card.innerHTML = `
1755
+ <div class="lg-port-handle ${topPort}"></div>
1756
+ <div class="lg-port-handle ${botPort}"></div>
1757
+
1758
+ <div class="lg-card-header">
1759
+ <div class="lg-node-type-icon">fn</div>
1760
+ <div class="lg-card-title-group">
1761
+ <div class="lg-node-qualname" title="${escapeHtml(n.label)}">${escapeHtml(n.label)}</div>
1762
+ <div class="lg-node-module">${escapeHtml(n.module || 'main')}</div>
1763
+ </div>
1764
+ <span class="lg-node-badge ${n.exceptions_count > 0 ? 'badge-error' : 'badge-success'}">
1765
+ ${n.exceptions_count > 0 ? 'FAIL' : 'OK'}
1766
+ </span>
1767
+ </div>
1768
+
1769
+ <div class="lg-card-body">
1770
+ <div class="lg-card-row">
1771
+ <span class="lg-row-label">In:</span>
1772
+ <div class="lg-row-content">${argsChips}</div>
1773
+ </div>
1774
+ <div class="lg-card-row">
1775
+ <span class="lg-row-label">Out:</span>
1776
+ <div class="lg-row-content">${returnBadge}</div>
1777
+ </div>
1778
+ </div>
1779
+
1780
+ <div class="lg-card-footer">
1781
+ <span>⏱️ ${n.avg_duration_ms} ms</span>
1782
+ <span class="lg-invocations-pill">x${n.count} runs</span>
1783
+ </div>
1784
+ `;
1785
+
1786
+ nodesContainer.appendChild(card);
1787
+ });
1788
+
1789
+ // 3. Render Smooth Bezier Curves
1790
+ renderBezierEdges();
1791
+
1792
+ // Initial selection
1793
+ if (nodesData.length > 0) {
1794
+ selectNode(nodesData[0].id);
1795
+ }
1796
+ }
1797
+
1798
+ function renderBoundaryNode(id, label, type) {
1799
+ const pos = nodePositions[id];
1800
+ if (!pos) return;
1801
+
1802
+ const bNode = document.createElement('div');
1803
+ bNode.className = `lg-node lg-node-boundary ${type}`;
1804
+ bNode.id = `node-${sanitizeId(id)}`;
1805
+ bNode.style.left = `${pos.x}px`;
1806
+ bNode.style.top = `${pos.y}px`;
1807
+ bNode.style.width = `${pos.w}px`;
1808
+ bNode.style.height = `${pos.h}px`;
1809
+
1810
+ bNode.innerHTML = `<span>${label}</span>`;
1811
+ document.getElementById('graph-nodes-layer').appendChild(bNode);
1812
+ }
1813
+
1814
+ function renderBezierEdges() {
1815
+ const edgesGroup = document.getElementById('svg-edges-group');
1816
+ const pulsesGroup = document.getElementById('svg-pulses-group');
1817
+
1818
+ // Collect all connections: start->roots, regular edges, leaves->end
1819
+ const allEdges = [];
1820
+
1821
+ (graph.root_nodes || [nodesData[0]?.id]).forEach(rid => {
1822
+ if (rid) allEdges.push({ source: '__start__', target: rid, count: 1 });
1823
+ });
1824
+
1825
+ edgesData.forEach(e => allEdges.push(e));
1826
+
1827
+ (graph.leaf_nodes || [nodesData[nodesData.length - 1]?.id]).forEach(lid => {
1828
+ if (lid) allEdges.push({ source: lid, target: '__end__', count: 1 });
1829
+ });
1830
+
1831
+ allEdges.forEach((e, idx) => {
1832
+ const srcPos = nodePositions[e.source];
1833
+ const tgtPos = nodePositions[e.target];
1834
+ if (!srcPos || !tgtPos) return;
1835
+
1836
+ let x1, y1, x2, y2, cpx1, cpy1, cpx2, cpy2;
1837
+ let d;
1838
+
1839
+ if (e.source === e.target) {
1840
+ // Recursive self-loop edge
1841
+ if (currentLayout === 'TB') {
1842
+ const xStart = srcPos.x + srcPos.w;
1843
+ const yStart = srcPos.y + srcPos.h * 0.7;
1844
+ const xEnd = srcPos.x + srcPos.w * 0.8;
1845
+ const yEnd = srcPos.y;
1846
+ d = `M ${xStart} ${yStart} C ${xStart + 55} ${yStart + 35}, ${xEnd + 65} ${yEnd - 45}, ${xEnd} ${yEnd}`;
1847
+ } else {
1848
+ const xStart = srcPos.x + srcPos.w * 0.7;
1849
+ const yStart = srcPos.y + srcPos.h;
1850
+ const xEnd = srcPos.x;
1851
+ const yEnd = srcPos.y + srcPos.h * 0.7;
1852
+ d = `M ${xStart} ${yStart} C ${xStart + 35} ${yStart + 55}, ${xEnd - 45} ${yEnd + 65}, ${xEnd} ${yEnd}`;
1853
+ }
1854
+ } else if (currentLayout === 'TB') {
1855
+ x1 = srcPos.x + srcPos.w / 2;
1856
+ y1 = srcPos.y + srcPos.h;
1857
+ x2 = tgtPos.x + tgtPos.w / 2;
1858
+ y2 = tgtPos.y;
1859
+
1860
+ const dy = Math.max(Math.abs(y2 - y1) * 0.5, 40);
1861
+ cpx1 = x1;
1862
+ cpy1 = y1 + dy;
1863
+ cpx2 = x2;
1864
+ cpy2 = y2 - dy;
1865
+ d = `M ${x1} ${y1} C ${cpx1} ${cpy1}, ${cpx2} ${cpy2}, ${x2} ${y2}`;
1866
+ } else {
1867
+ // LR Layout
1868
+ x1 = srcPos.x + srcPos.w;
1869
+ y1 = srcPos.y + srcPos.h / 2;
1870
+ x2 = tgtPos.x;
1871
+ y2 = tgtPos.y + tgtPos.h / 2;
1872
+
1873
+ const dx = Math.max(Math.abs(x2 - x1) * 0.5, 40);
1874
+ cpx1 = x1 + dx;
1875
+ cpy1 = y1;
1876
+ cpx2 = x2 - dx;
1877
+ cpy2 = y2;
1878
+ d = `M ${x1} ${y1} C ${cpx1} ${cpy1}, ${cpx2} ${cpy2}, ${x2} ${y2}`;
1879
+ }
1880
+
1881
+ // Base Edge Path
1882
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1883
+ path.setAttribute('d', d);
1884
+ path.setAttribute('class', 'flow-edge-path');
1885
+ path.setAttribute('id', `edge-${sanitizeId(e.source)}-${sanitizeId(e.target)}`);
1886
+ path.setAttribute('marker-end', 'url(#arrowhead)');
1887
+ edgesGroup.appendChild(path);
1888
+
1889
+ // Animated Flow Pulse Path
1890
+ const pulse = document.createElementNS('http://www.w3.org/2000/svg', 'path');
1891
+ pulse.setAttribute('d', d);
1892
+ pulse.setAttribute('class', 'flow-edge-pulse');
1893
+ pulse.setAttribute('id', `pulse-${sanitizeId(e.source)}-${sanitizeId(e.target)}`);
1894
+ pulsesGroup.appendChild(pulse);
1895
+ });
1896
+ }
1897
+
1898
+ // ─── NODE SELECTION & STATE INSPECTOR ──────────────────────
1899
+ function selectNode(nodeId) {
1900
+ selectedNodeId = nodeId;
1901
+
1902
+ // Update node active classes
1903
+ document.querySelectorAll('.lg-node-card').forEach(c => c.classList.remove('selected'));
1904
+ const activeCard = document.getElementById(`node-${sanitizeId(nodeId)}`);
1905
+ if (activeCard) activeCard.classList.add('selected');
1906
+
1907
+ // Highlight incoming/outgoing edges
1908
+ document.querySelectorAll('.flow-edge-path').forEach(p => {
1909
+ p.classList.remove('highlighted');
1910
+ p.setAttribute('marker-end', 'url(#arrowhead)');
1911
+ });
1912
+
1913
+ edgesData.forEach(e => {
1914
+ if (e.source === nodeId || e.target === nodeId) {
1915
+ const edgeEl = document.getElementById(`edge-${sanitizeId(e.source)}-${sanitizeId(e.target)}`);
1916
+ if (edgeEl) {
1917
+ edgeEl.classList.add('highlighted');
1918
+ edgeEl.setAttribute('marker-end', 'url(#arrowhead-highlight)');
1919
+ }
1920
+ }
1921
+ });
1922
+
1923
+ // Fetch node data and update inspector
1924
+ const nodeInfo = nodesData.find(n => n.id === nodeId);
1925
+ if (!nodeInfo) return;
1926
+
1927
+ // Update Header
1928
+ document.getElementById('insp-header-title').innerText = `${nodeInfo.label}()`;
1929
+
1930
+ // Find matching step or latest record
1931
+ const latestRecord = records.find(r => r.call_id === nodeInfo.latest_call_id) || records[0];
1932
+ populateInspectorWithRecord(latestRecord, nodeInfo);
1933
+ }
1934
+
1935
+ function populateInspectorWithRecord(r, nodeInfo) {
1936
+ if (!r) return;
1937
+
1938
+ // Inputs Section
1939
+ const argsEl = document.getElementById('insp-args-content');
1940
+ let argsFormatted = '';
1941
+ if (r.args_data && r.args_data.length > 0) {
1942
+ r.args_data.forEach(a => {
1943
+ argsFormatted += `<div><span class="tree-obj-key">${escapeHtml(a.name)}</span> <span class="tree-obj-type">(${escapeHtml(a.val_info?.type || 'Any')})</span>: <span class="tree-obj-str">${escapeHtml(a.val_info?.value || 'None')}</span></div>`;
1944
+ });
1945
+ }
1946
+ if (r.kwargs_data && Object.keys(r.kwargs_data).length > 0) {
1947
+ Object.entries(r.kwargs_data).forEach(([k, v]) => {
1948
+ argsFormatted += `<div><span class="tree-obj-key">${escapeHtml(k)}</span> <span class="tree-obj-type">(${escapeHtml(v.type || 'Any')})</span>: <span class="tree-obj-str">${escapeHtml(v.value || 'None')}</span></div>`;
1949
+ });
1950
+ }
1951
+ argsEl.innerHTML = argsFormatted || '<div style="color:var(--text-muted);">(No arguments passed)</div>';
1952
+
1953
+ // Output Section
1954
+ const retEl = document.getElementById('insp-ret-content');
1955
+ if (r.return_val) {
1956
+ retEl.innerHTML = `<div><span class="tree-obj-type">Type: ${escapeHtml(r.return_val.type)}</span><br><span class="tree-obj-str">${escapeHtml(r.return_val.value)}</span></div>`;
1957
+ } else {
1958
+ retEl.innerHTML = '<span style="color:var(--text-muted);">None</span>';
1959
+ }
1960
+
1961
+ // Exception Section
1962
+ const excSection = document.getElementById('insp-exc-section');
1963
+ const excContent = document.getElementById('insp-exc-content');
1964
+ if (r.exception) {
1965
+ excSection.style.display = 'block';
1966
+ excContent.innerText = `[${r.exception.type}] ${r.exception.message}\n\n${r.exception.traceback || ''}`;
1967
+ } else {
1968
+ excSection.style.display = 'none';
1969
+ }
1970
+
1971
+ // Metadata Tab
1972
+ document.getElementById('meta-duration').innerText = `${r.duration_ms || 0} ms`;
1973
+ document.getElementById('meta-status').innerText = r.exception ? 'EXCEPTION' : 'SUCCESS';
1974
+ document.getElementById('meta-status').style.color = r.exception ? 'var(--node-error)' : 'var(--node-success)';
1975
+ let fileName = 'unknown';
1976
+ if (r.file_path) {
1977
+ fileName = r.file_path.split('\\').pop().split('/').pop();
1978
+ }
1979
+ document.getElementById('meta-file').innerText = fileName;
1980
+ document.getElementById('meta-file').title = r.file_path || '';
1981
+ document.getElementById('meta-line').innerText = r.line_no || '-';
1982
+ document.getElementById('meta-depth').innerText = r.depth || '0';
1983
+ document.getElementById('meta-callid').innerText = `#${r.call_id}`;
1984
+
1985
+ // Runs History Tab
1986
+ const historyList = document.getElementById('insp-history-list');
1987
+ historyList.innerHTML = '';
1988
+ const nodeRuns = records.filter(rec => `${rec.module}.${rec.qualname}` === nodeInfo.id || rec.qualname === nodeInfo.id);
1989
+ nodeRuns.forEach((run, idx) => {
1990
+ const item = document.createElement('div');
1991
+ item.className = 'meta-item';
1992
+ item.style.cursor = 'pointer';
1993
+ item.onclick = () => populateInspectorWithRecord(run, nodeInfo);
1994
+ item.innerHTML = `
1995
+ <div style="display:flex; justify-content:space-between;">
1996
+ <span style="font-weight:700; color:var(--text-accent);">Run #${idx + 1} (Call #${run.call_id})</span>
1997
+ <span style="color:${run.exception ? 'var(--node-error)' : 'var(--node-success)'};">${run.duration_ms} ms</span>
1998
+ </div>
1999
+ `;
2000
+ historyList.appendChild(item);
2001
+ });
2002
+ }
2003
+
2004
+ function switchInspectorTab(tabKey) {
2005
+ document.querySelectorAll('.inspector-tab-btn').forEach(b => b.classList.remove('active'));
2006
+ event.target.classList.add('active');
2007
+
2008
+ document.getElementById('insp-tab-state').style.display = tabKey === 'state' ? 'flex' : 'none';
2009
+ document.getElementById('insp-tab-metadata').style.display = tabKey === 'metadata' ? 'flex' : 'none';
2010
+ document.getElementById('insp-tab-history').style.display = tabKey === 'history' ? 'flex' : 'none';
2011
+ }
2012
+
2013
+ function toggleInspectorCollapse() {
2014
+ document.getElementById('inspector-panel').classList.toggle('collapsed');
2015
+ }
2016
+
2017
+ // ─── PLAYBACK & STEP TIME-TRAVEL CONTROLLER ────────────────
2018
+ function updateStep(stepIdx) {
2019
+ if (!steps.length) return;
2020
+ currentStepIndex = Math.max(1, Math.min(stepIdx, steps.length));
2021
+ const step = steps[currentStepIndex - 1];
2022
+
2023
+ document.getElementById('lbl-current-step').innerText = currentStepIndex;
2024
+ document.getElementById('playback-slider').value = currentStepIndex;
2025
+
2026
+ // Highlight current executing node
2027
+ document.querySelectorAll('.lg-node-card').forEach(c => c.classList.remove('executing'));
2028
+ const activeCard = document.getElementById(`node-${sanitizeId(step.node_id)}`);
2029
+ if (activeCard) {
2030
+ activeCard.classList.add('executing');
2031
+ }
2032
+
2033
+ // Animate Pulse on connecting edge
2034
+ document.querySelectorAll('.flow-edge-pulse').forEach(p => p.classList.remove('active'));
2035
+ if (step.parent_node_id) {
2036
+ const pulseEl = document.getElementById(`pulse-${sanitizeId(step.parent_node_id)}-${sanitizeId(step.node_id)}`);
2037
+ if (pulseEl) pulseEl.classList.add('active');
2038
+ }
2039
+
2040
+ // Select node and load its state
2041
+ selectNode(step.node_id);
2042
+ const r = records.find(x => x.call_id === step.call_id);
2043
+ const n = nodesData.find(x => x.id === step.node_id);
2044
+ if (r && n) {
2045
+ populateInspectorWithRecord(r, n);
2046
+ }
2047
+ }
2048
+
2049
+ function togglePlay() {
2050
+ if (isPlaying) {
2051
+ pausePlayback();
2052
+ } else {
2053
+ startPlayback();
2054
+ }
2055
+ }
2056
+
2057
+ function startPlayback() {
2058
+ isPlaying = true;
2059
+ document.getElementById('btn-play-pause').innerText = '⏸';
2060
+ if (currentStepIndex >= steps.length) currentStepIndex = 0;
2061
+
2062
+ playbackInterval = setInterval(() => {
2063
+ if (currentStepIndex < steps.length) {
2064
+ updateStep(currentStepIndex + 1);
2065
+ } else {
2066
+ pausePlayback();
2067
+ }
2068
+ }, playbackSpeedMs);
2069
+ }
2070
+
2071
+ function pausePlayback() {
2072
+ isPlaying = false;
2073
+ document.getElementById('btn-play-pause').innerText = '▶';
2074
+ if (playbackInterval) clearInterval(playbackInterval);
2075
+ }
2076
+
2077
+ function stepForward() {
2078
+ pausePlayback();
2079
+ if (currentStepIndex < steps.length) {
2080
+ updateStep(currentStepIndex + 1);
2081
+ }
2082
+ }
2083
+
2084
+ function stepBackward() {
2085
+ pausePlayback();
2086
+ if (currentStepIndex > 1) {
2087
+ updateStep(currentStepIndex - 1);
2088
+ }
2089
+ }
2090
+
2091
+ function onScrub(val) {
2092
+ pausePlayback();
2093
+ updateStep(parseInt(val, 10));
2094
+ }
2095
+
2096
+ function changeSpeed(val) {
2097
+ playbackSpeedMs = parseInt(val, 10);
2098
+ if (isPlaying) {
2099
+ pausePlayback();
2100
+ startPlayback();
2101
+ }
2102
+ }
2103
+
2104
+ // ─── ZOOM & PAN CANVAS ENGINE ──────────────────────────────
2105
+ const container = document.getElementById('flow-canvas-container');
2106
+ const viewport = document.getElementById('graph-viewport');
2107
+
2108
+ container.addEventListener('mousedown', (e) => {
2109
+ if (e.target.closest('.lg-node') || e.target.closest('.floating-controls') || e.target.closest('.playback-dock')) return;
2110
+ isDragging = true;
2111
+ startPan = { x: e.clientX - transform.x, y: e.clientY - transform.y };
2112
+ });
2113
+
2114
+ window.addEventListener('mousemove', (e) => {
2115
+ if (!isDragging) return;
2116
+ transform.x = e.clientX - startPan.x;
2117
+ transform.y = e.clientY - startPan.y;
2118
+ updateTransform();
2119
+ });
2120
+
2121
+ window.addEventListener('mouseup', () => { isDragging = false; });
2122
+
2123
+ container.addEventListener('wheel', (e) => {
2124
+ e.preventDefault();
2125
+ const zoomFactor = 1.08;
2126
+ const mouseX = e.clientX - container.getBoundingClientRect().left;
2127
+ const mouseY = e.clientY - container.getBoundingClientRect().top;
2128
+
2129
+ let newScale = e.deltaY < 0 ? transform.scale * zoomFactor : transform.scale / zoomFactor;
2130
+ newScale = Math.max(0.2, Math.min(2.5, newScale));
2131
+
2132
+ transform.x = mouseX - (mouseX - transform.x) * (newScale / transform.scale);
2133
+ transform.y = mouseY - (mouseY - transform.y) * (newScale / transform.scale);
2134
+ transform.scale = newScale;
2135
+
2136
+ updateTransform();
2137
+ }, { passive: false });
2138
+
2139
+ function updateTransform() {
2140
+ viewport.style.transform = `translate(${transform.x}px, ${transform.y}px) scale(${transform.scale})`;
2141
+ }
2142
+
2143
+ function zoomIn() {
2144
+ transform.scale = Math.min(2.5, transform.scale * 1.2);
2145
+ updateTransform();
2146
+ }
2147
+
2148
+ function zoomOut() {
2149
+ transform.scale = Math.max(0.2, transform.scale / 1.2);
2150
+ updateTransform();
2151
+ }
2152
+
2153
+ function resetZoom() {
2154
+ transform = { x: 80, y: 80, scale: 0.9 };
2155
+ updateTransform();
2156
+ }
2157
+
2158
+ function fitView() {
2159
+ const allPositions = Object.values(nodePositions);
2160
+ if (!allPositions.length) return;
2161
+
2162
+ const minX = Math.min(...allPositions.map(p => p.x));
2163
+ const maxX = Math.max(...allPositions.map(p => p.x + p.w));
2164
+ const minY = Math.min(...allPositions.map(p => p.y));
2165
+ const maxY = Math.max(...allPositions.map(p => p.y + p.h));
2166
+
2167
+ const rect = container.getBoundingClientRect();
2168
+ const graphW = maxX - minX + 160;
2169
+ const graphH = maxY - minY + 160;
2170
+
2171
+ const scaleX = rect.width / graphW;
2172
+ const scaleY = rect.height / graphH;
2173
+ const newScale = Math.max(0.3, Math.min(1.2, Math.min(scaleX, scaleY)));
2174
+
2175
+ transform.scale = newScale;
2176
+ transform.x = (rect.width - (maxX + minX) * newScale) / 2;
2177
+ transform.y = (rect.height - (maxY + minY) * newScale) / 2;
2178
+
2179
+ updateTransform();
2180
+ }
2181
+
2182
+ function setLayout(orientation) {
2183
+ currentLayout = orientation;
2184
+ document.getElementById('btn-layout-tb').classList.toggle('active', orientation === 'TB');
2185
+ document.getElementById('btn-layout-lr').classList.toggle('active', orientation === 'LR');
2186
+ renderGraphCanvas();
2187
+ fitView();
2188
+ }
2189
+
2190
+ // ─── TIMELINE WATERFALL RENDERER ───────────────────────────
2191
+ function renderTimeline() {
2192
+ const container = document.getElementById('timeline-spans-container');
2193
+ container.innerHTML = '';
2194
+ if (!records.length) return;
2195
+
2196
+ const minTime = Math.min(...records.map(r => r.start_time));
2197
+ const maxTime = Math.max(...records.map(r => r.end_time || r.start_time));
2198
+ const totalDur = maxTime - minTime || 0.001;
2199
+
2200
+ records.forEach(r => {
2201
+ const row = document.createElement('div');
2202
+ row.className = 'timeline-row';
2203
+ row.onclick = () => {
2204
+ switchView('flow');
2205
+ selectNode(`${r.module}.${r.qualname}`);
2206
+ };
2207
+
2208
+ const leftPct = ((r.start_time - minTime) / totalDur) * 100;
2209
+ const widthPct = Math.max(((r.duration_ms / 1000) / totalDur) * 100, 1.2);
2210
+
2211
+ row.innerHTML = `
2212
+ <div class="timeline-node-name" title="${escapeHtml(r.qualname)}">
2213
+ ${' '.repeat(r.depth)} <span style="color:var(--text-accent);">#${r.call_id}</span> ${escapeHtml(r.qualname)}
2214
+ </div>
2215
+ <div class="timeline-track-area">
2216
+ <div class="timeline-span-bar ${r.exception ? 'error-span' : ''}" style="left: ${leftPct}%; width: ${widthPct}%;">
2217
+ ${r.qualname} (${r.duration_ms} ms)
2218
+ </div>
2219
+ </div>
2220
+ `;
2221
+ container.appendChild(row);
2222
+ });
2223
+ }
2224
+
2225
+ // ─── CALL TREE RENDERER ─────────────────────────────────────
2226
+ function renderCallTree(nodes, containerEl, isRoot = true) {
2227
+ nodes.forEach(node => {
2228
+ const item = document.createElement('div');
2229
+ item.className = `tree-node-item ${isRoot ? 'root-item' : ''}`;
2230
+
2231
+ const row = document.createElement('div');
2232
+ row.className = 'tree-node-row';
2233
+ row.onclick = (e) => {
2234
+ e.stopPropagation();
2235
+ switchView('flow');
2236
+ selectNode(`${node.module}.${node.qualname}`);
2237
+ };
2238
+
2239
+ let argsText = '';
2240
+ node.args_data.forEach(a => {
2241
+ argsText += `<span class="lg-arg-chip">${escapeHtml(a.name)}: ${escapeHtml(a.val_info?.value || '')}</span> `;
2242
+ });
2243
+
2244
+ let statusBadge = node.exception
2245
+ ? `<span class="lg-node-badge badge-error">⚠️ ${escapeHtml(node.exception.type)}</span>`
2246
+ : `<span class="lg-node-badge badge-success">➜ ${escapeHtml(node.return_val?.value || 'None')}</span>`;
2247
+
2248
+ row.innerHTML = `
2249
+ <span style="color:var(--text-muted); font-size:0.75rem;">#${node.call_id}</span>
2250
+ <strong style="color:var(--text-accent);">${escapeHtml(node.qualname)}</strong>
2251
+ <div style="display:flex; gap:0.25rem; flex:1; overflow:hidden;">${argsText}</div>
2252
+ ${statusBadge}
2253
+ <span style="color:var(--node-warn); font-size:0.72rem; font-family:var(--font-mono);">${node.duration_ms} ms</span>
2254
+ `;
2255
+
2256
+ item.appendChild(row);
2257
+
2258
+ if (node.children && node.children.length > 0) {
2259
+ const childContainer = document.createElement('div');
2260
+ renderCallTree(node.children, childContainer, false);
2261
+ item.appendChild(childContainer);
2262
+ }
2263
+
2264
+ containerEl.appendChild(item);
2265
+ });
2266
+ }
2267
+
2268
+ // ─── DATA MATRIX TABLE ─────────────────────────────────────
2269
+ function renderDataMatrix(query = '') {
2270
+ const tbody = document.getElementById('datamatrix-tbody');
2271
+ tbody.innerHTML = '';
2272
+
2273
+ const lQuery = query.toLowerCase();
2274
+ const filtered = records.filter(r => {
2275
+ if (!query) return true;
2276
+ const fn = `${r.module}.${r.qualname}`.toLowerCase();
2277
+ const argsStr = JSON.stringify(r.args_data).toLowerCase() + JSON.stringify(r.kwargs_data).toLowerCase();
2278
+ const retStr = JSON.stringify(r.return_val || r.exception || '').toLowerCase();
2279
+ return fn.includes(lQuery) || argsStr.includes(lQuery) || retStr.includes(lQuery);
2280
+ });
2281
+
2282
+ filtered.forEach(r => {
2283
+ const tr = document.createElement('tr');
2284
+ tr.onclick = () => {
2285
+ switchView('flow');
2286
+ selectNode(`${r.module}.${r.qualname}`);
2287
+ };
2288
+
2289
+ let allArgs = r.args_data.map(a => `${a.name}=${a.val_info?.value}`).concat(
2290
+ Object.entries(r.kwargs_data).map(([k, v]) => `${k}=${v.value}`)
2291
+ ).join(', ');
2292
+
2293
+ let retDisplay = '';
2294
+ if (r.exception) {
2295
+ retDisplay = `<span style="color:var(--node-error);">⚠️ ${escapeHtml(r.exception.type)}: ${escapeHtml(r.exception.message)}</span>`;
2296
+ } else {
2297
+ retDisplay = `<span style="color:var(--node-success);">${escapeHtml(r.return_val?.value || 'None')}</span>`;
2298
+ }
2299
+
2300
+ tr.innerHTML = `
2301
+ <td>#${r.call_id}</td>
2302
+ <td><strong style="color:var(--text-accent);">${escapeHtml(r.qualname)}</strong> <span style="color:var(--text-muted); font-size:0.72rem;">(${escapeHtml(r.module || 'main')})</span></td>
2303
+ <td style="max-width:320px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${escapeHtml(allArgs)}</td>
2304
+ <td style="max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${retDisplay}</td>
2305
+ <td style="color:var(--node-warn);">${r.duration_ms} ms</td>
2306
+ <td><span class="lg-node-badge ${r.exception ? 'badge-error' : 'badge-success'}">${r.exception ? 'FAIL' : 'OK'}</span></td>
2307
+ `;
2308
+
2309
+ tbody.appendChild(tr);
2310
+ });
2311
+ }
2312
+
2313
+ function handleTableSearch(val) {
2314
+ renderDataMatrix(val);
2315
+ }
2316
+
2317
+ // ─── UTILITY HELPERS ───────────────────────────────────────
2318
+ function sanitizeId(str) {
2319
+ return String(str).replace(/[^a-zA-Z0-9_-]/g, '_');
2320
+ }
2321
+
2322
+ function escapeHtml(str) {
2323
+ if (typeof str !== 'string') str = String(str || '');
2324
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2325
+ }
2326
+
2327
+ function copySectionContent(elemId) {
2328
+ const el = document.getElementById(elemId);
2329
+ if (el) {
2330
+ navigator.clipboard.writeText(el.innerText);
2331
+ const btn = event.target;
2332
+ const original = btn.innerText;
2333
+ btn.innerText = 'Copied!';
2334
+ setTimeout(() => btn.innerText = original, 1500);
2335
+ }
2336
+ }
2337
+
2338
+ // ─── BOOTSTRAP APP ─────────────────────────────────────────
2339
+ window.addEventListener('DOMContentLoaded', () => {
2340
+ initTheme();
2341
+ renderGraphCanvas();
2342
+ renderTimeline();
2343
+ renderCallTree(tree, document.getElementById('calltree-root-container'));
2344
+ renderDataMatrix();
2345
+ setTimeout(fitView, 50);
2346
+ });
2347
+ </script>
2348
+ </body>
2349
+
2350
+ </html>