diff --git a/llm_team_ui.py b/llm_team_ui.py index 164df5f..8c98c9d 100644 --- a/llm_team_ui.py +++ b/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';