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 .prog-substep { max-width: 70%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.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::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); }
|
||||
@ -1281,6 +1288,10 @@ let _runStartTime = 0;
|
||||
let _runTimer = null;
|
||||
let _runEventCount = 0;
|
||||
let _runResponseCount = 0;
|
||||
let _runTotalChars = 0;
|
||||
let _runModelsUsed = new Set();
|
||||
let _runErrors = 0;
|
||||
let _runKeepAlives = 0;
|
||||
|
||||
function formatElapsed(ms) {
|
||||
const s = Math.floor(ms / 1000);
|
||||
@ -1288,66 +1299,145 @@ function formatElapsed(ms) {
|
||||
return Math.floor(s/60) + 'm ' + (s%60) + 's';
|
||||
}
|
||||
|
||||
function updateProgressTime() {
|
||||
const el = document.getElementById('prog-time');
|
||||
function formatBytes(chars) {
|
||||
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);
|
||||
const ev = document.getElementById('prog-events');
|
||||
if (ev) ev.textContent = _runEventCount + ' events / ' + _runResponseCount + ' responses';
|
||||
var m;
|
||||
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() {
|
||||
const config = buildConfig();
|
||||
var config = buildConfig();
|
||||
if (!config) return;
|
||||
const btn = document.getElementById('run-btn');
|
||||
var btn = document.getElementById('run-btn');
|
||||
btn.disabled = true; btn.textContent = 'Running...';
|
||||
const output = document.getElementById('output');
|
||||
var output = document.getElementById('output');
|
||||
_runStartTime = Date.now();
|
||||
_runEventCount = 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.id = 'run-progress';
|
||||
progEl.textContent = '';
|
||||
const header = document.createElement('div');
|
||||
|
||||
// Header row
|
||||
var header = document.createElement('div');
|
||||
header.className = 'progress-header';
|
||||
const modeLabel = document.createElement('span');
|
||||
var modeLabel = document.createElement('span');
|
||||
modeLabel.className = 'prog-mode';
|
||||
modeLabel.textContent = currentMode;
|
||||
const timeLabel = document.createElement('span');
|
||||
var timeLabel = document.createElement('span');
|
||||
timeLabel.className = 'prog-time';
|
||||
timeLabel.id = 'prog-time';
|
||||
timeLabel.textContent = '0s';
|
||||
header.appendChild(modeLabel);
|
||||
header.appendChild(timeLabel);
|
||||
progEl.appendChild(header);
|
||||
const track = document.createElement('div');
|
||||
|
||||
// Progress bar
|
||||
var track = document.createElement('div');
|
||||
track.className = 'progress-track';
|
||||
const fill = document.createElement('div');
|
||||
var fill = document.createElement('div');
|
||||
fill.className = 'progress-fill';
|
||||
fill.id = 'prog-fill';
|
||||
fill.style.width = '2%';
|
||||
track.appendChild(fill);
|
||||
progEl.appendChild(track);
|
||||
const stepsDiv = document.createElement('div');
|
||||
|
||||
// Step indicators
|
||||
var stepsDiv = document.createElement('div');
|
||||
stepsDiv.className = 'progress-steps';
|
||||
stepsDiv.id = 'prog-steps';
|
||||
progEl.appendChild(stepsDiv);
|
||||
const detail = document.createElement('div');
|
||||
|
||||
// Substep detail
|
||||
var detail = document.createElement('div');
|
||||
detail.className = 'progress-detail';
|
||||
const substep = document.createElement('span');
|
||||
var substep = document.createElement('span');
|
||||
substep.className = 'prog-substep';
|
||||
substep.id = 'prog-substep';
|
||||
substep.textContent = 'Initializing...';
|
||||
const stats = document.createElement('span');
|
||||
var stats = document.createElement('span');
|
||||
stats.className = 'prog-stats';
|
||||
stats.id = 'prog-events';
|
||||
stats.textContent = '0 events';
|
||||
stats.textContent = '';
|
||||
detail.appendChild(substep);
|
||||
detail.appendChild(stats);
|
||||
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.appendChild(progEl);
|
||||
_runTimer = setInterval(updateProgressTime, 1000);
|
||||
_runTimer = setInterval(updateProgressMetrics, 500);
|
||||
try {
|
||||
const resp = await fetch('/api/run', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(config) });
|
||||
if (!resp.ok) {
|
||||
@ -1365,6 +1455,7 @@ async function runTeam() {
|
||||
buffer = lines.pop();
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) { try { _runEventCount++; handleEvent(JSON.parse(line.slice(6))); } catch(e) {} }
|
||||
else if (line.indexOf('keepalive') >= 0) { _runKeepAlives++; }
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
@ -1376,16 +1467,20 @@ async function runTeam() {
|
||||
output.appendChild(errDiv);
|
||||
}
|
||||
clearInterval(_runTimer);
|
||||
const prog = document.getElementById('run-progress');
|
||||
updateProgressMetrics();
|
||||
var prog = document.getElementById('run-progress');
|
||||
if (prog) {
|
||||
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)'; }
|
||||
const sub = document.getElementById('prog-substep');
|
||||
if (sub) sub.textContent = 'Complete — ' + formatElapsed(Date.now() - _runStartTime) + ' — ' + _runResponseCount + ' responses';
|
||||
const allSteps = prog.querySelectorAll('.progress-step');
|
||||
var sub = document.getElementById('prog-substep');
|
||||
if (sub) sub.textContent = 'Complete — ' + formatElapsed(Date.now() - _runStartTime) + ' — ' + _runResponseCount + ' responses — ~' + estimateTokens(_runTotalChars) + ' tokens';
|
||||
var allSteps = prog.querySelectorAll('.progress-step');
|
||||
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';
|
||||
}
|
||||
@ -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 === 'response') {
|
||||
_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();
|
||||
// Phase labels — show when role changes
|
||||
const role = evt.role || 'response';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user