Files
portal/docs/observer/dashboard.js
T

152 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { parseEpisodes, filterEpisodes, attributeNodes } from './dashboard-core.js';
const AGD = window.AGD;
let episodes = [];
let skipped = 0;
let network = null;
// ── data loading ──────────────────────────────────────────────
async function loadEpisodes() {
const files = await fetch('/api/episodes').then((r) => r.json());
const all = [];
let skip = 0;
for (const f of files) {
const url = '/docs/observer/' + f;
const text = await fetch(url).then((r) => (r.ok ? r.text() : ''));
const r = parseEpisodes(text);
all.push(...r.episodes);
skip += r.skipped;
}
all.sort((a, b) => String(a.startedAt).localeCompare(String(b.startedAt)));
episodes = all;
skipped = skip;
document.getElementById('status').textContent =
`${episodes.length} эпизодов · ${skipped} пропущено`;
}
// ── graph banner ──────────────────────────────────────────────
function renderGraph() {
const nodes = new vis.DataSet(AGD.NODES);
const edges = new vis.DataSet(AGD.EDGES);
network = new vis.Network(
document.getElementById('network'),
{ nodes, edges },
{
groups: AGD.GROUPS,
nodes: { shape: 'dot', borderWidth: 2, font: { multi: 'html' } },
edges: { smooth: { type: 'continuous', roundness: 0.5 } },
physics: { enabled: false },
interaction: { hover: true, tooltipDelay: 400 },
}
);
network.once('afterDrawing', () => network.fit());
return { nodes, edges };
}
// ── view switching ────────────────────────────────────────────
const views = {};
let activeView = 'map';
views.map = function renderMapView() {
// Plain mode: clear any overlay coloring applied by other views.
window.__graph.nodes.update(AGD.NODES.map((n) => ({ id: n.id, color: undefined })));
// List the design-time conflict edges (dashed edges carry an emoji label).
const conflicts = AGD.EDGES.filter((e) => e.dashes === true);
const ul = document.getElementById('map-conflicts');
ul.innerHTML = '';
for (const c of conflicts) {
const li = document.createElement('li');
li.textContent = `${c.label || '•'} ${c.from}${c.to}: ${c.title || ''}`;
ul.appendChild(li);
}
};
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']) {
document.getElementById('view-' + v).style.display = v === name ? 'block' : 'none';
}
document.querySelectorAll('#tabbar button').forEach((b) => {
b.classList.toggle('active', b.dataset.view === name);
});
if (views[name]) views[name]();
}
// ── boot ──────────────────────────────────────────────────────
async function boot() {
const gds = renderGraph();
window.__graph = { network, ...gds };
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');
}
export function getEpisodes() { return episodes; }
export { views, switchView };
boot();