Spaces:
Runtime error
Runtime error
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 filesReplace 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>
- 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="
|
| 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="
|
| 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="
|
| 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('
|
| 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 === '
|
| 4410 |
-
document.getElementById('
|
| 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 |
|