feat(brain): Разбор view UI — list + filters + trajectory highlight
This commit is contained in:
@@ -34,7 +34,15 @@
|
||||
<p>Топология мозга: 124 узла, рёбра, 11 размеченных дизайн-конфликтов. Это нулевое состояние холста — без оверлеев.</p>
|
||||
<ul id="map-conflicts"></ul>
|
||||
</div>
|
||||
<div class="view" id="view-replay"></div>
|
||||
<div class="view" id="view-replay">
|
||||
<div id="replay-list">
|
||||
<select id="f-classification"><option value="">все</option><option value="bugfix">bugfix</option><option value="feature">feature</option><option value="refactor">refactor</option><option value="docs">docs</option><option value="question">question</option><option value="other">other</option></select>
|
||||
<select id="f-outcome"><option value="">все</option><option value="success">success</option><option value="unknown">unknown</option><option value="failure">failure</option></select>
|
||||
<label><input type="checkbox" id="f-errors"> только с ошибками</label>
|
||||
<ul id="replay-episodes"></ul>
|
||||
</div>
|
||||
<div id="replay-detail"></div>
|
||||
</div>
|
||||
<div class="view" id="view-feed"></div>
|
||||
<div class="view" id="view-aggregate"></div>
|
||||
</section>
|
||||
|
||||
@@ -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 = `
|
||||
<h3>${ep.taskClassification || '—'} · ${ep.pathType || '—'} · ${ep.outcome}</h3>
|
||||
<p>provenance: ${provLine}</p>
|
||||
<p>hard-floor: ${ep.hardFloor.invoked ? (ep.hardFloor.rules || []).join(', ') : 'нет'}</p>
|
||||
<p>окружение: economy=${env.economy_level ?? '—'} · ${env.model || '—'} · turn ${env.session_turn ?? '—'}${env.post_compaction ? ' · post-compaction' : ''}${env.parallel_session ? ' · parallel' : ''}</p>
|
||||
<p>атрибутировано узлов: ${attr.attributed} из ${attr.signals} сигналов</p>
|
||||
<h4>События</h4>
|
||||
<ol>${ep.events.map((e) => `<li>${eventLine(e)}</li>`).join('')}</ol>`;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user