diff --git a/docs/observer/dashboard.html b/docs/observer/dashboard.html index ba803556..173c9421 100644 --- a/docs/observer/dashboard.html +++ b/docs/observer/dashboard.html @@ -34,7 +34,15 @@

Топология мозга: 124 узла, рёбра, 11 размеченных дизайн-конфликтов. Это нулевое состояние холста — без оверлеев.

-
+
+
+ + + + +
+
+
diff --git a/docs/observer/dashboard.js b/docs/observer/dashboard.js index 3fcb76dd..0c582739 100644 --- a/docs/observer/dashboard.js +++ b/docs/observer/dashboard.js @@ -1,4 +1,4 @@ -import { parseEpisodes } from './dashboard-core.js'; +import { parseEpisodes, filterEpisodes, attributeNodes } from './dashboard-core.js'; const AGD = window.AGD; let episodes = []; @@ -61,6 +61,64 @@ views.map = function renderMapView() { } }; +views.replay = function renderReplayView() { + const filter = { + classification: document.getElementById('f-classification').value || undefined, + outcome: document.getElementById('f-outcome').value || undefined, + withErrors: document.getElementById('f-errors').checked || undefined, + }; + const list = filterEpisodes(episodes, filter); + const ul = document.getElementById('replay-episodes'); + ul.innerHTML = ''; + list.forEach((ep) => { + const li = document.createElement('li'); + li.textContent = `${ep.startedAt} · ${ep.taskClassification || '—'} · ${ep.outcome}` + + (ep.errorCount ? ` · ⚠${ep.errorCount}` : ''); + li.addEventListener('click', () => selectEpisode(ep)); + ul.appendChild(li); + }); +}; + +function selectEpisode(ep) { + const attr = attributeNodes(ep); + window.__graph.nodes.update( + AGD.NODES.map((n) => ({ + id: n.id, + color: attr.nodeIds.includes(n.id) + ? { background: '#268bd2', border: '#93a1a1' } + : { background: '#2a2a3a', border: '#444' }, + })) + ); + const d = document.getElementById('replay-detail'); + const prov = ep.decisionProvenance; + const provLine = prov && prov.kind === 'user_directed_method' + ? `перенаправление: выбран ${prov.node || '?'}, автономно был бы ${prov.claude_would_have_chosen || '?'}` + : prov ? prov.kind : '—'; + const env = ep.environment || {}; + d.innerHTML = ` +

${ep.taskClassification || '—'} · ${ep.pathType || '—'} · ${ep.outcome}

+

provenance: ${provLine}

+

hard-floor: ${ep.hardFloor.invoked ? (ep.hardFloor.rules || []).join(', ') : 'нет'}

+

окружение: economy=${env.economy_level ?? '—'} · ${env.model || '—'} · turn ${env.session_turn ?? '—'}${env.post_compaction ? ' · post-compaction' : ''}${env.parallel_session ? ' · parallel' : ''}

+

атрибутировано узлов: ${attr.attributed} из ${attr.signals} сигналов

+

События

+
    ${ep.events.map((e) => `
  1. ${eventLine(e)}
  2. `).join('')}
`; +} + +function eventLine(e) { + switch (e.kind) { + case 'skill_invoked': return `skill: ${e.skill}`; + case 'error': return `error: ${e.message || ''}`; + case 'retry': return 'retry'; + case 'interrupt': return 'interrupt'; + case 'hook_fired': return `hooks (${Object.keys(e.counts || {}).length} типов, errors ${e.errors || 0})`; + case 'tool_summary': return `инструменты: ${Object.entries(e.counts || {}).map(([k, v]) => `${k}×${v}`).join(', ')}`; + case 'time_burn': return `time_burn: ${e.duration_ms} ms`; + case 'parse_gap': return `parse_gap: ${e.broken}/${e.total}`; + default: return e.kind; + } +} + function switchView(name) { activeView = name; for (const v of ['map', 'replay', 'feed', 'aggregate']) { @@ -79,6 +137,11 @@ async function boot() { document.querySelectorAll('#tabbar button').forEach((b) => { b.addEventListener('click', () => switchView(b.dataset.view)); }); + ['f-classification', 'f-outcome', 'f-errors'].forEach((id) => { + document.getElementById(id).addEventListener('change', () => { + if (activeView === 'replay') views.replay(); + }); + }); await loadEpisodes(); switchView('map'); }