Zhen Ye Claude Opus 4.6 (1M context) commited on
Commit
61c239d
·
1 Parent(s): 4ff16d7

feat(demo): add multi-LLM explainability graph replacing metrics tab

Browse files

Replace the METRICS drawer tab with an EXPLAIN tab that renders an
interactive D3-based interpretability tree. When a track is inspected,
the frontend calls GET /inspect/explain/{job_id}/{track_id} and
visualizes the multi-LLM consensus (GPT-4o, Claude, Gemini) as a
hierarchical graph with category nodes, feature leaves, validator
badges, tooltips, and a consensus progress bar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. demo/index.html +354 -6
demo/index.html CHANGED
@@ -6,6 +6,9 @@
6
  <title>ISR Command Center</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
8
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
 
 
 
9
  <style>
10
  /* ── CSS Design System ─────────────────────────────────────────── */
11
 
@@ -892,7 +895,7 @@
892
 
893
  /* Processing: only TRACKS tab available */
894
  body[data-state="processing"] .drawer-tab[data-tab="inspect"],
895
- body[data-state="processing"] .drawer-tab[data-tab="metrics"] {
896
  opacity: 0.3;
897
  pointer-events: none;
898
  }
@@ -1765,6 +1768,70 @@
1765
  min-width: 1280px;
1766
  }
1767
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1768
  </style>
1769
  </head>
1770
  <body>
@@ -1827,7 +1894,7 @@
1827
  <div class="drawer-tabs">
1828
  <button class="drawer-tab active" data-tab="tracks">TRACKS</button>
1829
  <button class="drawer-tab" data-tab="inspect">INSPECT</button>
1830
- <button class="drawer-tab" data-tab="metrics">METRICS</button>
1831
  </div>
1832
  <div id="configPanel" class="drawer-section">
1833
  <div class="config-group">
@@ -1875,7 +1942,7 @@
1875
  </div>
1876
  <div id="tracksPanel" class="drawer-section"></div>
1877
  <div id="inspectPanel" class="drawer-section hidden"></div>
1878
- <div id="metricsPanel" class="drawer-section hidden"></div>
1879
  </aside>
1880
  </main>
1881
 
@@ -3160,6 +3227,7 @@
3160
  }
3161
 
3162
  function renderMetricsPanel() {
 
3163
  const panel = document.getElementById('metricsPanel');
3164
  if (!panel) return;
3165
 
@@ -3205,6 +3273,7 @@
3205
  }
3206
 
3207
  async function computeRealMetrics(jobId, status, summary) {
 
3208
  const metricsPanel = document.getElementById('metricsPanel');
3209
  if (!metricsPanel) return;
3210
 
@@ -3298,6 +3367,277 @@
3298
  </div>`;
3299
  }
3300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3301
  /* ================================================================
3302
  * TASK 10: Inspect State — 2x2 Quad View
3303
  * ================================================================ */
@@ -3447,6 +3787,14 @@
3447
  switchDrawerTab('inspect');
3448
  }
3449
 
 
 
 
 
 
 
 
 
3450
  // Wire back button
3451
  document.getElementById('inspectBack').addEventListener('click', exitInspectState);
3452
 
@@ -4396,7 +4744,7 @@
4396
  document.getElementById('configPanel').classList.add('hidden');
4397
  document.getElementById('tracksPanel').classList.add('hidden');
4398
  document.getElementById('inspectPanel').classList.add('hidden');
4399
- document.getElementById('metricsPanel').classList.add('hidden');
4400
 
4401
  if (tabName === 'tracks' || tabName === 'config') {
4402
  // Config panel only shows in ready state
@@ -4406,8 +4754,8 @@
4406
  document.getElementById('tracksPanel').classList.remove('hidden');
4407
  } else if (tabName === 'inspect') {
4408
  document.getElementById('inspectPanel').classList.remove('hidden');
4409
- } else if (tabName === 'metrics') {
4410
- document.getElementById('metricsPanel').classList.remove('hidden');
4411
  }
4412
  }
4413
 
 
6
  <title>ISR Command Center</title>
7
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
8
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/d3-hierarchy@3/dist/d3-hierarchy.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/d3-path@3/dist/d3-path.min.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/d3-shape@3/dist/d3-shape.min.js"></script>
12
  <style>
13
  /* ── CSS Design System ─────────────────────────────────────────── */
14
 
 
895
 
896
  /* Processing: only TRACKS tab available */
897
  body[data-state="processing"] .drawer-tab[data-tab="inspect"],
898
+ body[data-state="processing"] .drawer-tab[data-tab="explain"] {
899
  opacity: 0.3;
900
  pointer-events: none;
901
  }
 
1768
  min-width: 1280px;
1769
  }
1770
  }
1771
+
1772
+ /* ── Explainability Graph ─────────────────────────────────────── */
1773
+ #explainPanel {
1774
+ overflow-y: auto;
1775
+ padding: 0;
1776
+ }
1777
+
1778
+ .explain-svg {
1779
+ display: block;
1780
+ width: 100%;
1781
+ font-family: 'Inter', -apple-system, sans-serif;
1782
+ }
1783
+ .explain-svg text {
1784
+ pointer-events: none;
1785
+ user-select: none;
1786
+ }
1787
+ .explain-svg g { cursor: pointer; }
1788
+
1789
+ .explain-loading {
1790
+ display: flex;
1791
+ align-items: center;
1792
+ justify-content: center;
1793
+ gap: 10px;
1794
+ padding: 32px 16px;
1795
+ color: var(--text-secondary);
1796
+ font-size: 11px;
1797
+ }
1798
+ .explain-spinner {
1799
+ width: 16px; height: 16px;
1800
+ border: 2px solid var(--panel-border);
1801
+ border-top-color: #7c3aed;
1802
+ border-radius: 50%;
1803
+ animation: exSpin 0.8s linear infinite;
1804
+ }
1805
+ @keyframes exSpin { to { transform: rotate(360deg); } }
1806
+ .explain-loading span { animation: exPulse 2s ease-in-out infinite; }
1807
+ @keyframes exPulse { 0%,100% { opacity: 0.6; } 50% { opacity: 1; } }
1808
+
1809
+ .explain-error {
1810
+ padding: 24px 16px;
1811
+ color: var(--danger);
1812
+ font-size: 11px;
1813
+ text-align: center;
1814
+ }
1815
+
1816
+ .explain-tooltip {
1817
+ position: absolute;
1818
+ z-index: 1000;
1819
+ background: rgba(30, 41, 59, 0.97);
1820
+ border: 1px solid rgba(51, 65, 85, 0.8);
1821
+ border-radius: 6px;
1822
+ padding: 10px 12px;
1823
+ max-width: 260px;
1824
+ font-size: 10px;
1825
+ color: var(--text-primary);
1826
+ line-height: 1.5;
1827
+ pointer-events: none;
1828
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5);
1829
+ }
1830
+ .explain-tooltip strong { color: #f8fafc; font-size: 11px; }
1831
+ .tip-section { margin-top: 5px; padding-top: 4px; border-top: 1px solid rgba(51,65,85,0.6); }
1832
+ .tip-label { font-weight: 600; color: var(--text-secondary); }
1833
+ .tip-agree .tip-label { color: var(--success); }
1834
+ .tip-disagree .tip-label { color: var(--danger); }
1835
  </style>
1836
  </head>
1837
  <body>
 
1894
  <div class="drawer-tabs">
1895
  <button class="drawer-tab active" data-tab="tracks">TRACKS</button>
1896
  <button class="drawer-tab" data-tab="inspect">INSPECT</button>
1897
+ <button class="drawer-tab" data-tab="explain">EXPLAIN</button>
1898
  </div>
1899
  <div id="configPanel" class="drawer-section">
1900
  <div class="config-group">
 
1942
  </div>
1943
  <div id="tracksPanel" class="drawer-section"></div>
1944
  <div id="inspectPanel" class="drawer-section hidden"></div>
1945
+ <div id="explainPanel" class="drawer-section hidden"></div>
1946
  </aside>
1947
  </main>
1948
 
 
3227
  }
3228
 
3229
  function renderMetricsPanel() {
3230
+ return; // Replaced by explainability graph
3231
  const panel = document.getElementById('metricsPanel');
3232
  if (!panel) return;
3233
 
 
3273
  }
3274
 
3275
  async function computeRealMetrics(jobId, status, summary) {
3276
+ return; // Replaced by explainability graph
3277
  const metricsPanel = document.getElementById('metricsPanel');
3278
  if (!metricsPanel) return;
3279
 
 
3367
  </div>`;
3368
  }
3369
 
3370
+ /* ================================================================
3371
+ * Explainability Graph — Multi-LLM Interpretability Tree
3372
+ * ================================================================ */
3373
+
3374
+ const EXPLAIN_COLORS = {
3375
+ Structure: '#3b82f6', Function: '#06b6d4', Material: '#f59e0b',
3376
+ Color: '#ef4444', Size: '#10b981', Type: '#8b5cf6',
3377
+ Motion: '#ec4899', Context: '#64748b', Shape: '#f97316', Markings: '#a855f7',
3378
+ };
3379
+ const LIGHTEN_MAP = {
3380
+ '#3b82f6':'#93c5fd','#06b6d4':'#a5f3fc','#f59e0b':'#fde68a',
3381
+ '#ef4444':'#fca5a5','#10b981':'#6ee7b7','#8b5cf6':'#c4b5fd',
3382
+ '#ec4899':'#f9a8d4','#64748b':'#94a3b8','#f97316':'#fdba74','#a855f7':'#d8b4fe',
3383
+ };
3384
+
3385
+ let _explainAbort = null;
3386
+ const _explainCache = {};
3387
+
3388
+ async function loadExplainability(jobId, trackId) {
3389
+ const panel = document.getElementById('explainPanel');
3390
+ if (!panel) return;
3391
+
3392
+ // Check cache
3393
+ if (_explainCache[trackId]) {
3394
+ renderExplainGraph(_explainCache[trackId], panel);
3395
+ return;
3396
+ }
3397
+
3398
+ // Abort previous
3399
+ if (_explainAbort) _explainAbort.abort();
3400
+ _explainAbort = new AbortController();
3401
+
3402
+ panel.innerHTML = '<div class="explain-loading"><div class="explain-spinner"></div><span>Analyzing with GPT-4o, Claude, and Gemini...</span></div>';
3403
+
3404
+ try {
3405
+ const resp = await fetch(`${API_BASE}/inspect/explain/${jobId}/${encodeURIComponent(trackId)}`, { signal: _explainAbort.signal });
3406
+ if (!resp.ok) {
3407
+ const body = await resp.json().catch(() => ({}));
3408
+ throw new Error(body.detail || `Explain failed: ${resp.status}`);
3409
+ }
3410
+ const data = await resp.json();
3411
+ _explainCache[trackId] = data;
3412
+ renderExplainGraph(data, panel);
3413
+ } catch (err) {
3414
+ if (err.name === 'AbortError') return;
3415
+ panel.innerHTML = `<div class="explain-error">${escHtml(err.message)}</div>`;
3416
+ }
3417
+ }
3418
+
3419
+ function escHtml(str) {
3420
+ const d = document.createElement('div');
3421
+ d.textContent = str || '';
3422
+ return d.innerHTML;
3423
+ }
3424
+
3425
+ function renderExplainGraph(data, container) {
3426
+ container.innerHTML = '';
3427
+ if (!data || !data.categories || data.categories.length === 0) {
3428
+ container.innerHTML = '<div class="explain-error">No explanation data</div>';
3429
+ return;
3430
+ }
3431
+
3432
+ const root = {
3433
+ name: (data.object || 'OBJECT').toUpperCase(),
3434
+ confidence: data.confidence,
3435
+ satisfies: data.satisfies,
3436
+ summary: data.reasoning_summary,
3437
+ children: data.categories.map(cat => ({
3438
+ name: cat.name,
3439
+ color: cat.color || EXPLAIN_COLORS[cat.name] || '#64748b',
3440
+ isCategory: true,
3441
+ children: (cat.features || []).map(f => ({
3442
+ name: f.name,
3443
+ value: f.value,
3444
+ reasoning: f.reasoning,
3445
+ validators: f.validators || {},
3446
+ consensus: f.consensus || 0,
3447
+ color: cat.color || EXPLAIN_COLORS[cat.name] || '#64748b',
3448
+ isFeature: true,
3449
+ })),
3450
+ })),
3451
+ };
3452
+
3453
+ const margin = { top: 30, right: 20, bottom: 60, left: 20 };
3454
+ const width = container.clientWidth || 340;
3455
+ const totalLeaves = root.children.reduce((s, c) => s + (c.children ? c.children.length : 1), 0);
3456
+ const height = Math.max(280, totalLeaves * 35 + 120);
3457
+
3458
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
3459
+ svg.setAttribute('width', width);
3460
+ svg.setAttribute('height', height);
3461
+ svg.setAttribute('class', 'explain-svg');
3462
+ container.appendChild(svg);
3463
+
3464
+ // Glow filter
3465
+ const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
3466
+ defs.innerHTML = '<filter id="exGlow"><feGaussianBlur stdDeviation="3" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>';
3467
+ svg.appendChild(defs);
3468
+
3469
+ // D3 layout
3470
+ const hierarchy = d3.hierarchy(root);
3471
+ const treeLayout = d3.tree().size([width - margin.left - margin.right, height - margin.top - margin.bottom]);
3472
+ treeLayout(hierarchy);
3473
+
3474
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
3475
+ g.setAttribute('transform', `translate(${margin.left},${margin.top})`);
3476
+ svg.appendChild(g);
3477
+
3478
+ // Edges
3479
+ hierarchy.links().forEach(link => {
3480
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3481
+ const sx = link.source.x, sy = link.source.y, tx = link.target.x, ty = link.target.y;
3482
+ const my = (sy + ty) / 2;
3483
+ path.setAttribute('d', `M${sx},${sy} C${sx},${my} ${tx},${my} ${tx},${ty}`);
3484
+ path.setAttribute('fill', 'none');
3485
+ path.setAttribute('stroke', link.target.data.color || '#3b82f6');
3486
+ path.setAttribute('stroke-width', link.source.depth === 0 ? '2.5' : '1.5');
3487
+ path.setAttribute('opacity', '0.5');
3488
+ path.setAttribute('filter', 'url(#exGlow)');
3489
+ g.appendChild(path);
3490
+ });
3491
+
3492
+ // Nodes
3493
+ hierarchy.descendants().forEach(node => {
3494
+ const d = node.data;
3495
+ const ng = document.createElementNS('http://www.w3.org/2000/svg', 'g');
3496
+ ng.setAttribute('transform', `translate(${node.x},${node.y})`);
3497
+
3498
+ if (node.depth === 0) {
3499
+ drawExRoot(ng, d);
3500
+ } else if (d.isCategory) {
3501
+ drawExCategory(ng, d);
3502
+ } else if (d.isFeature) {
3503
+ drawExFeature(ng, d);
3504
+ }
3505
+
3506
+ ng.addEventListener('mouseenter', (e) => showExTip(e, d, container));
3507
+ ng.addEventListener('mouseleave', () => hideExTip(container));
3508
+ g.appendChild(ng);
3509
+ });
3510
+
3511
+ // Consensus bar
3512
+ if (data.consensus_bar) drawExConsensus(svg, data.consensus_bar, width, height);
3513
+ }
3514
+
3515
+ function drawExRoot(g, d) {
3516
+ const w = 120, h = 32;
3517
+ const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
3518
+ r.setAttribute('x', -w/2); r.setAttribute('y', -h/2);
3519
+ r.setAttribute('width', w); r.setAttribute('height', h);
3520
+ r.setAttribute('rx', 8); r.setAttribute('fill', '#7c3aed');
3521
+ r.setAttribute('filter', 'url(#exGlow)');
3522
+ g.appendChild(r);
3523
+ const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
3524
+ t.setAttribute('text-anchor', 'middle'); t.setAttribute('dy', '0.35em');
3525
+ t.setAttribute('fill', 'white'); t.setAttribute('font-size', '11'); t.setAttribute('font-weight', '700');
3526
+ t.textContent = d.name + (d.confidence != null ? ` ${Math.round(d.confidence * 100)}%` : '');
3527
+ g.appendChild(t);
3528
+ }
3529
+
3530
+ function drawExCategory(g, d) {
3531
+ const w = 90, h = 26;
3532
+ const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
3533
+ r.setAttribute('x', -w/2); r.setAttribute('y', -h/2);
3534
+ r.setAttribute('width', w); r.setAttribute('height', h);
3535
+ r.setAttribute('rx', 6); r.setAttribute('fill', d.color || '#3b82f6');
3536
+ r.setAttribute('filter', 'url(#exGlow)'); r.setAttribute('opacity', '0.95');
3537
+ g.appendChild(r);
3538
+ const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
3539
+ t.setAttribute('text-anchor', 'middle'); t.setAttribute('dy', '0.35em');
3540
+ t.setAttribute('fill', 'white'); t.setAttribute('font-size', '9'); t.setAttribute('font-weight', '600');
3541
+ t.textContent = d.name;
3542
+ g.appendChild(t);
3543
+ }
3544
+
3545
+ function drawExFeature(g, d) {
3546
+ const w = 80, h = 22;
3547
+ const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
3548
+ r.setAttribute('x', -w/2); r.setAttribute('y', -h/2);
3549
+ r.setAttribute('width', w); r.setAttribute('height', h);
3550
+ r.setAttribute('rx', 5); r.setAttribute('fill', '#0f172a');
3551
+ r.setAttribute('stroke', d.color || '#3b82f6'); r.setAttribute('stroke-width', '1.5');
3552
+ g.appendChild(r);
3553
+ const t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
3554
+ t.setAttribute('text-anchor', 'middle'); t.setAttribute('dy', '0.35em');
3555
+ t.setAttribute('fill', LIGHTEN_MAP[d.color] || '#e2e8f0'); t.setAttribute('font-size', '7.5');
3556
+ t.textContent = d.name.length > 12 ? d.name.slice(0, 11) + '\u2026' : d.name;
3557
+ g.appendChild(t);
3558
+
3559
+ const vals = d.validators || {};
3560
+ const total = Object.keys(vals).length;
3561
+ if (total > 0) {
3562
+ const agreed = Object.values(vals).filter(v => v.agree).length;
3563
+ const badge = document.createElementNS('http://www.w3.org/2000/svg', 'text');
3564
+ badge.setAttribute('x', w/2 - 4); badge.setAttribute('y', -h/2 - 3);
3565
+ badge.setAttribute('text-anchor', 'end'); badge.setAttribute('font-size', '7');
3566
+ badge.setAttribute('fill', agreed === total ? '#4ade80' : '#f87171');
3567
+ badge.textContent = `${agreed}/${total}`;
3568
+ g.appendChild(badge);
3569
+ }
3570
+ }
3571
+
3572
+ function drawExConsensus(svg, bar, width, height) {
3573
+ const barY = height - 30, barW = width - 40, barX = 20, barH = 6;
3574
+ const ratio = bar.total_features > 0 ? bar.agreed / bar.total_features : 0;
3575
+
3576
+ const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
3577
+ bg.setAttribute('x', barX); bg.setAttribute('y', barY);
3578
+ bg.setAttribute('width', barW); bg.setAttribute('height', barH);
3579
+ bg.setAttribute('rx', 3); bg.setAttribute('fill', '#1e293b');
3580
+ svg.appendChild(bg);
3581
+
3582
+ const fill = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
3583
+ fill.setAttribute('x', barX); fill.setAttribute('y', barY);
3584
+ fill.setAttribute('width', barW * ratio); fill.setAttribute('height', barH);
3585
+ fill.setAttribute('rx', 3); fill.setAttribute('fill', '#7c3aed');
3586
+ fill.setAttribute('opacity', '0.8'); fill.setAttribute('filter', 'url(#exGlow)');
3587
+ svg.appendChild(fill);
3588
+
3589
+ const lb = document.createElementNS('http://www.w3.org/2000/svg', 'text');
3590
+ lb.setAttribute('x', barX); lb.setAttribute('y', barY + barH + 14);
3591
+ lb.setAttribute('fill', '#a78bfa'); lb.setAttribute('font-size', '9');
3592
+ lb.textContent = `${bar.agreed}/${bar.total_features} features agreed`;
3593
+ svg.appendChild(lb);
3594
+
3595
+ const vl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
3596
+ vl.setAttribute('x', barX + barW); vl.setAttribute('y', barY + barH + 14);
3597
+ vl.setAttribute('text-anchor', 'end'); vl.setAttribute('fill', '#6b7280'); vl.setAttribute('font-size', '9');
3598
+ vl.textContent = `${bar.validators_available}/2 validators`;
3599
+ svg.appendChild(vl);
3600
+ }
3601
+
3602
+ function showExTip(event, data, container) {
3603
+ hideExTip(container);
3604
+ if (!data.reasoning && !data.isCategory && !data.summary) return;
3605
+ const tip = document.createElement('div');
3606
+ tip.className = 'explain-tooltip';
3607
+
3608
+ if (data.isCategory) {
3609
+ const ch = data.children || [];
3610
+ const allOk = ch.filter(c => { const vs = Object.values(c.validators||{}); return vs.length > 0 && vs.every(v => v.agree); }).length;
3611
+ tip.innerHTML = `<strong>${escHtml(data.name)}</strong><br>${allOk}/${ch.length} features fully validated`;
3612
+ } else if (data.isFeature) {
3613
+ let html = `<strong>${escHtml(data.name)}</strong>`;
3614
+ if (data.reasoning) html += `<div class="tip-section"><span class="tip-label">GPT-4o:</span> ${escHtml(data.reasoning)}</div>`;
3615
+ const v = data.validators || {};
3616
+ if (v.claude) { const ic = v.claude.agree ? '\u2713' : '\u2717'; const cl = v.claude.agree ? 'tip-agree' : 'tip-disagree'; html += `<div class="tip-section ${cl}"><span class="tip-label">${ic} Claude:</span> ${escHtml(v.claude.note||'')}</div>`; }
3617
+ if (v.gemini) { const ic = v.gemini.agree ? '\u2713' : '\u2717'; const cl = v.gemini.agree ? 'tip-agree' : 'tip-disagree'; html += `<div class="tip-section ${cl}"><span class="tip-label">${ic} Gemini:</span> ${escHtml(v.gemini.note||'')}</div>`; }
3618
+ tip.innerHTML = html;
3619
+ } else if (data.summary) {
3620
+ tip.innerHTML = `<strong>${escHtml(data.name)}</strong><br>${escHtml(data.summary)}`;
3621
+ }
3622
+
3623
+ const rect = event.target.closest('g').getBoundingClientRect();
3624
+ const cr = container.getBoundingClientRect();
3625
+ tip.style.left = (rect.left - cr.left + rect.width/2) + 'px';
3626
+ tip.style.top = (rect.top - cr.top - 8) + 'px';
3627
+ tip.style.transform = 'translate(-50%, -100%)';
3628
+ container.appendChild(tip);
3629
+
3630
+ // Clamp
3631
+ const tr = tip.getBoundingClientRect();
3632
+ if (tr.left < cr.left) tip.style.left = parseFloat(tip.style.left) + (cr.left - tr.left + 4) + 'px';
3633
+ if (tr.right > cr.right) tip.style.left = parseFloat(tip.style.left) - (tr.right - cr.right + 4) + 'px';
3634
+ }
3635
+
3636
+ function hideExTip(container) {
3637
+ const old = container.querySelector('.explain-tooltip');
3638
+ if (old) old.remove();
3639
+ }
3640
+
3641
  /* ================================================================
3642
  * TASK 10: Inspect State — 2x2 Quad View
3643
  * ================================================================ */
 
3787
  switchDrawerTab('inspect');
3788
  }
3789
 
3790
+ // Trigger explainability graph
3791
+ if (hasRealData && STATE.jobId) {
3792
+ const tid = realTrack ? realTrack.track_id : trackId;
3793
+ loadExplainability(STATE.jobId, tid);
3794
+ // Auto-switch to EXPLAIN tab after a brief delay so user sees it loading
3795
+ setTimeout(() => switchDrawerTab('explain'), 300);
3796
+ }
3797
+
3798
  // Wire back button
3799
  document.getElementById('inspectBack').addEventListener('click', exitInspectState);
3800
 
 
4744
  document.getElementById('configPanel').classList.add('hidden');
4745
  document.getElementById('tracksPanel').classList.add('hidden');
4746
  document.getElementById('inspectPanel').classList.add('hidden');
4747
+ document.getElementById('explainPanel').classList.add('hidden');
4748
 
4749
  if (tabName === 'tracks' || tabName === 'config') {
4750
  // Config panel only shows in ready state
 
4754
  document.getElementById('tracksPanel').classList.remove('hidden');
4755
  } else if (tabName === 'inspect') {
4756
  document.getElementById('inspectPanel').classList.remove('hidden');
4757
+ } else if (tabName === 'explain') {
4758
+ document.getElementById('explainPanel').classList.remove('hidden');
4759
  }
4760
  }
4761