Add live metrics dashboard to progress panel
8 real-time metrics in the progress panel: - Elapsed time (updates every 500ms) - Models active/total (tracks unique models as they respond) - Responses received (count) - Estimated tokens (~chars/4) - Data received (formatted KB) - SSE events (total protocol events) - Errors (turns red if > 0) - Heartbeats (keepalive count) Metrics update every 500ms during run. On completion, all metric values turn green. Magenta/purple theme for metric values, micro labels underneath. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c507ba1016
commit
1ac7a436e6
148
llm_team_ui.py
148
llm_team_ui.py
@ -662,6 +662,13 @@ HTML = r"""
|
|||||||
.progress-detail { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: #e0b0ff; display: flex; justify-content: space-between; }
|
.progress-detail { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: #e0b0ff; display: flex; justify-content: space-between; }
|
||||||
.progress-detail .prog-substep { max-width: 70%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.progress-detail .prog-substep { max-width: 70%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.progress-detail .prog-stats { color: #c084fc; opacity: 0.7; }
|
.progress-detail .prog-stats { color: #c084fc; opacity: 0.7; }
|
||||||
|
.prog-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(217,70,239,0.15); }
|
||||||
|
.prog-metric { text-align: center; padding: 4px 2px; }
|
||||||
|
.prog-metric .mv { font-family: 'JetBrains Mono', monospace; font-size: 14px; font-weight: 700; color: #f0abfc; line-height: 1; }
|
||||||
|
.prog-metric .ml { font-family: 'JetBrains Mono', monospace; font-size: 7px; text-transform: uppercase; letter-spacing: 1.5px; color: rgba(217,70,239,0.5); margin-top: 3px; }
|
||||||
|
.prog-metric.highlight .mv { color: #4ade80; }
|
||||||
|
.prog-metric.warn .mv { color: #f59e0b; }
|
||||||
|
.prog-metric.err .mv { color: #e05252; }
|
||||||
.phase-label { font-family: 'JetBrains Mono', monospace; font-size: 9px; text-transform: uppercase; letter-spacing: 2px; color: #4ade80; padding: 12px 0 6px; opacity: 0.8; display: flex; align-items: center; gap: 8px; }
|
.phase-label { font-family: 'JetBrains Mono', monospace; font-size: 9px; text-transform: uppercase; letter-spacing: 2px; color: #4ade80; padding: 12px 0 6px; opacity: 0.8; display: flex; align-items: center; gap: 8px; }
|
||||||
.phase-label::before { content: ''; flex: 0 0 12px; height: 2px; background: #4ade80; opacity: 0.6; }
|
.phase-label::before { content: ''; flex: 0 0 12px; height: 2px; background: #4ade80; opacity: 0.6; }
|
||||||
.phase-label::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, rgba(74,222,128,0.3), transparent); }
|
.phase-label::after { content: ''; flex: 1; height: 1px; background: linear-gradient(90deg, rgba(74,222,128,0.3), transparent); }
|
||||||
@ -1281,6 +1288,10 @@ let _runStartTime = 0;
|
|||||||
let _runTimer = null;
|
let _runTimer = null;
|
||||||
let _runEventCount = 0;
|
let _runEventCount = 0;
|
||||||
let _runResponseCount = 0;
|
let _runResponseCount = 0;
|
||||||
|
let _runTotalChars = 0;
|
||||||
|
let _runModelsUsed = new Set();
|
||||||
|
let _runErrors = 0;
|
||||||
|
let _runKeepAlives = 0;
|
||||||
|
|
||||||
function formatElapsed(ms) {
|
function formatElapsed(ms) {
|
||||||
const s = Math.floor(ms / 1000);
|
const s = Math.floor(ms / 1000);
|
||||||
@ -1288,66 +1299,145 @@ function formatElapsed(ms) {
|
|||||||
return Math.floor(s/60) + 'm ' + (s%60) + 's';
|
return Math.floor(s/60) + 'm ' + (s%60) + 's';
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProgressTime() {
|
function formatBytes(chars) {
|
||||||
const el = document.getElementById('prog-time');
|
if (chars < 1000) return chars + ' ch';
|
||||||
|
if (chars < 100000) return (chars/1000).toFixed(1) + 'K';
|
||||||
|
return (chars/1000).toFixed(0) + 'K';
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateTokens(chars) {
|
||||||
|
var t = Math.round(chars / 4);
|
||||||
|
if (t < 1000) return t.toString();
|
||||||
|
return (t/1000).toFixed(1) + 'K';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgressMetrics() {
|
||||||
|
var el = document.getElementById('prog-time');
|
||||||
if (el && _runStartTime) el.textContent = formatElapsed(Date.now() - _runStartTime);
|
if (el && _runStartTime) el.textContent = formatElapsed(Date.now() - _runStartTime);
|
||||||
const ev = document.getElementById('prog-events');
|
var m;
|
||||||
if (ev) ev.textContent = _runEventCount + ' events / ' + _runResponseCount + ' responses';
|
m = document.getElementById('pm-elapsed');
|
||||||
|
if (m) m.textContent = formatElapsed(Date.now() - _runStartTime);
|
||||||
|
m = document.getElementById('pm-models');
|
||||||
|
if (m) m.textContent = _runModelsUsed.size;
|
||||||
|
m = document.getElementById('pm-responses');
|
||||||
|
if (m) m.textContent = _runResponseCount;
|
||||||
|
m = document.getElementById('pm-tokens');
|
||||||
|
if (m) m.textContent = '~' + estimateTokens(_runTotalChars);
|
||||||
|
m = document.getElementById('pm-data');
|
||||||
|
if (m) m.textContent = formatBytes(_runTotalChars);
|
||||||
|
m = document.getElementById('pm-events');
|
||||||
|
if (m) m.textContent = _runEventCount;
|
||||||
|
m = document.getElementById('pm-errors');
|
||||||
|
if (m) { m.textContent = _runErrors; m.parentNode.className = _runErrors > 0 ? 'prog-metric err' : 'prog-metric'; }
|
||||||
|
m = document.getElementById('pm-heartbeat');
|
||||||
|
if (m) m.textContent = _runKeepAlives;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runTeam() {
|
async function runTeam() {
|
||||||
const config = buildConfig();
|
var config = buildConfig();
|
||||||
if (!config) return;
|
if (!config) return;
|
||||||
const btn = document.getElementById('run-btn');
|
var btn = document.getElementById('run-btn');
|
||||||
btn.disabled = true; btn.textContent = 'Running...';
|
btn.disabled = true; btn.textContent = 'Running...';
|
||||||
const output = document.getElementById('output');
|
var output = document.getElementById('output');
|
||||||
_runStartTime = Date.now();
|
_runStartTime = Date.now();
|
||||||
_runEventCount = 0;
|
_runEventCount = 0;
|
||||||
_runResponseCount = 0;
|
_runResponseCount = 0;
|
||||||
const progEl = document.createElement('div');
|
_runTotalChars = 0;
|
||||||
|
_runModelsUsed = new Set();
|
||||||
|
_runErrors = 0;
|
||||||
|
_runKeepAlives = 0;
|
||||||
|
|
||||||
|
// Count models in config
|
||||||
|
var cfgModels = config.models ? config.models.length : 0;
|
||||||
|
var totalModels = cfgModels;
|
||||||
|
if (config.synthesizer) totalModels++;
|
||||||
|
if (config.scout) totalModels++;
|
||||||
|
if (config.checker) totalModels++;
|
||||||
|
if (config.judge) totalModels++;
|
||||||
|
|
||||||
|
var progEl = document.createElement('div');
|
||||||
progEl.className = 'progress-panel';
|
progEl.className = 'progress-panel';
|
||||||
progEl.id = 'run-progress';
|
progEl.id = 'run-progress';
|
||||||
progEl.textContent = '';
|
progEl.textContent = '';
|
||||||
const header = document.createElement('div');
|
|
||||||
|
// Header row
|
||||||
|
var header = document.createElement('div');
|
||||||
header.className = 'progress-header';
|
header.className = 'progress-header';
|
||||||
const modeLabel = document.createElement('span');
|
var modeLabel = document.createElement('span');
|
||||||
modeLabel.className = 'prog-mode';
|
modeLabel.className = 'prog-mode';
|
||||||
modeLabel.textContent = currentMode;
|
modeLabel.textContent = currentMode;
|
||||||
const timeLabel = document.createElement('span');
|
var timeLabel = document.createElement('span');
|
||||||
timeLabel.className = 'prog-time';
|
timeLabel.className = 'prog-time';
|
||||||
timeLabel.id = 'prog-time';
|
timeLabel.id = 'prog-time';
|
||||||
timeLabel.textContent = '0s';
|
timeLabel.textContent = '0s';
|
||||||
header.appendChild(modeLabel);
|
header.appendChild(modeLabel);
|
||||||
header.appendChild(timeLabel);
|
header.appendChild(timeLabel);
|
||||||
progEl.appendChild(header);
|
progEl.appendChild(header);
|
||||||
const track = document.createElement('div');
|
|
||||||
|
// Progress bar
|
||||||
|
var track = document.createElement('div');
|
||||||
track.className = 'progress-track';
|
track.className = 'progress-track';
|
||||||
const fill = document.createElement('div');
|
var fill = document.createElement('div');
|
||||||
fill.className = 'progress-fill';
|
fill.className = 'progress-fill';
|
||||||
fill.id = 'prog-fill';
|
fill.id = 'prog-fill';
|
||||||
fill.style.width = '2%';
|
fill.style.width = '2%';
|
||||||
track.appendChild(fill);
|
track.appendChild(fill);
|
||||||
progEl.appendChild(track);
|
progEl.appendChild(track);
|
||||||
const stepsDiv = document.createElement('div');
|
|
||||||
|
// Step indicators
|
||||||
|
var stepsDiv = document.createElement('div');
|
||||||
stepsDiv.className = 'progress-steps';
|
stepsDiv.className = 'progress-steps';
|
||||||
stepsDiv.id = 'prog-steps';
|
stepsDiv.id = 'prog-steps';
|
||||||
progEl.appendChild(stepsDiv);
|
progEl.appendChild(stepsDiv);
|
||||||
const detail = document.createElement('div');
|
|
||||||
|
// Substep detail
|
||||||
|
var detail = document.createElement('div');
|
||||||
detail.className = 'progress-detail';
|
detail.className = 'progress-detail';
|
||||||
const substep = document.createElement('span');
|
var substep = document.createElement('span');
|
||||||
substep.className = 'prog-substep';
|
substep.className = 'prog-substep';
|
||||||
substep.id = 'prog-substep';
|
substep.id = 'prog-substep';
|
||||||
substep.textContent = 'Initializing...';
|
substep.textContent = 'Initializing...';
|
||||||
const stats = document.createElement('span');
|
var stats = document.createElement('span');
|
||||||
stats.className = 'prog-stats';
|
stats.className = 'prog-stats';
|
||||||
stats.id = 'prog-events';
|
stats.id = 'prog-events';
|
||||||
stats.textContent = '0 events';
|
stats.textContent = '';
|
||||||
detail.appendChild(substep);
|
detail.appendChild(substep);
|
||||||
detail.appendChild(stats);
|
detail.appendChild(stats);
|
||||||
progEl.appendChild(detail);
|
progEl.appendChild(detail);
|
||||||
|
|
||||||
|
// Metrics grid
|
||||||
|
var metrics = document.createElement('div');
|
||||||
|
metrics.className = 'prog-metrics';
|
||||||
|
metrics.id = 'prog-metrics';
|
||||||
|
var metricDefs = [
|
||||||
|
{id:'pm-elapsed', label:'Elapsed', val:'0s'},
|
||||||
|
{id:'pm-models', label:'Models', val:'0/' + totalModels},
|
||||||
|
{id:'pm-responses', label:'Responses', val:'0'},
|
||||||
|
{id:'pm-tokens', label:'Est. Tokens', val:'~0'},
|
||||||
|
{id:'pm-data', label:'Data Recv', val:'0 ch'},
|
||||||
|
{id:'pm-events', label:'SSE Events', val:'0'},
|
||||||
|
{id:'pm-errors', label:'Errors', val:'0'},
|
||||||
|
{id:'pm-heartbeat', label:'Heartbeats', val:'0'}
|
||||||
|
];
|
||||||
|
metricDefs.forEach(function(md) {
|
||||||
|
var box = document.createElement('div');
|
||||||
|
box.className = 'prog-metric';
|
||||||
|
var v = document.createElement('div');
|
||||||
|
v.className = 'mv';
|
||||||
|
v.id = md.id;
|
||||||
|
v.textContent = md.val;
|
||||||
|
var l = document.createElement('div');
|
||||||
|
l.className = 'ml';
|
||||||
|
l.textContent = md.label;
|
||||||
|
box.appendChild(v);
|
||||||
|
box.appendChild(l);
|
||||||
|
metrics.appendChild(box);
|
||||||
|
});
|
||||||
|
progEl.appendChild(metrics);
|
||||||
|
|
||||||
output.textContent = '';
|
output.textContent = '';
|
||||||
output.appendChild(progEl);
|
output.appendChild(progEl);
|
||||||
_runTimer = setInterval(updateProgressTime, 1000);
|
_runTimer = setInterval(updateProgressMetrics, 500);
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/run', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(config) });
|
const resp = await fetch('/api/run', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(config) });
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
@ -1365,6 +1455,7 @@ async function runTeam() {
|
|||||||
buffer = lines.pop();
|
buffer = lines.pop();
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ')) { try { _runEventCount++; handleEvent(JSON.parse(line.slice(6))); } catch(e) {} }
|
if (line.startsWith('data: ')) { try { _runEventCount++; handleEvent(JSON.parse(line.slice(6))); } catch(e) {} }
|
||||||
|
else if (line.indexOf('keepalive') >= 0) { _runKeepAlives++; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@ -1376,16 +1467,20 @@ async function runTeam() {
|
|||||||
output.appendChild(errDiv);
|
output.appendChild(errDiv);
|
||||||
}
|
}
|
||||||
clearInterval(_runTimer);
|
clearInterval(_runTimer);
|
||||||
const prog = document.getElementById('run-progress');
|
updateProgressMetrics();
|
||||||
|
var prog = document.getElementById('run-progress');
|
||||||
if (prog) {
|
if (prog) {
|
||||||
prog.classList.add('done');
|
prog.classList.add('done');
|
||||||
const fillEl = document.getElementById('prog-fill');
|
var fillEl = document.getElementById('prog-fill');
|
||||||
if (fillEl) { fillEl.style.width = '100%'; fillEl.style.boxShadow = '0 0 20px rgba(74,222,128,0.5)'; fillEl.style.background = 'linear-gradient(90deg, #4ade80, #22d3ee)'; }
|
if (fillEl) { fillEl.style.width = '100%'; fillEl.style.boxShadow = '0 0 20px rgba(74,222,128,0.5)'; fillEl.style.background = 'linear-gradient(90deg, #4ade80, #22d3ee)'; }
|
||||||
const sub = document.getElementById('prog-substep');
|
var sub = document.getElementById('prog-substep');
|
||||||
if (sub) sub.textContent = 'Complete — ' + formatElapsed(Date.now() - _runStartTime) + ' — ' + _runResponseCount + ' responses';
|
if (sub) sub.textContent = 'Complete — ' + formatElapsed(Date.now() - _runStartTime) + ' — ' + _runResponseCount + ' responses — ~' + estimateTokens(_runTotalChars) + ' tokens';
|
||||||
const allSteps = prog.querySelectorAll('.progress-step');
|
var allSteps = prog.querySelectorAll('.progress-step');
|
||||||
allSteps.forEach(function(s) { s.className = 'progress-step done'; });
|
allSteps.forEach(function(s) { s.className = 'progress-step done'; });
|
||||||
setTimeout(function() { if (prog.parentNode) { prog.style.opacity = '0.5'; } }, 5000);
|
// Turn metric values green on completion
|
||||||
|
var mvs = prog.querySelectorAll('.prog-metric');
|
||||||
|
mvs.forEach(function(m) { if (!m.classList.contains('err') || _runErrors === 0) m.classList.add('highlight'); });
|
||||||
|
setTimeout(function() { if (prog.parentNode) { prog.style.opacity = '0.6'; } }, 8000);
|
||||||
}
|
}
|
||||||
btn.disabled = false; btn.textContent = 'Run Team';
|
btn.disabled = false; btn.textContent = 'Run Team';
|
||||||
}
|
}
|
||||||
@ -1440,6 +1535,9 @@ function handleEvent(evt) {
|
|||||||
if (evt.type === 'done') { const bar = output.querySelector('.status-bar'); if (bar) bar.remove(); return; }
|
if (evt.type === 'done') { const bar = output.querySelector('.status-bar'); if (bar) bar.remove(); return; }
|
||||||
if (evt.type === 'response') {
|
if (evt.type === 'response') {
|
||||||
_runResponseCount++;
|
_runResponseCount++;
|
||||||
|
_runTotalChars += (evt.text || '').length;
|
||||||
|
if (evt.model) _runModelsUsed.add(evt.model);
|
||||||
|
if (evt.role === 'error') _runErrors++;
|
||||||
const bar = output.querySelector('.status-bar'); if (bar) bar.remove();
|
const bar = output.querySelector('.status-bar'); if (bar) bar.remove();
|
||||||
// Phase labels — show when role changes
|
// Phase labels — show when role changes
|
||||||
const role = evt.role || 'response';
|
const role = evt.role || 'response';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user