Sticky progress bar, phase labels, auto-scroll

- Progress panel is now position:sticky at top of output — always visible
- Phase labels (─── scouting ───, ─── researching ───, etc.) appear
  between response cards when the pipeline role changes
- Auto-scroll to latest response card as they arrive
- Completion state shows response count and fades after 5s
- Clear previous errors: all 'input stream' errors were caused by
  service restarts during in-flight runs, not code bugs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-03-26 01:30:53 -05:00
parent c124b01681
commit 9eaac813df

View File

@ -645,15 +645,16 @@ HTML = r"""
.status-bar { display: flex; align-items: center; gap: 8px; padding: 10px 14px; background: rgba(0,0,0,0.3); border: 2px solid var(--border); border-radius: 2px; font-size: 11px; color: var(--text2); font-family: 'JetBrains Mono', monospace; text-transform: uppercase; letter-spacing: 0.5px; }
.spinner { width: 14px; height: 14px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.progress-panel { background: rgba(0,0,0,0.3); border: 2px solid var(--border); border-radius: 2px; padding: 14px; margin-bottom: 10px; }
.progress-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.progress-panel { background: rgba(8,9,12,0.95); border: 2px solid var(--border); border-radius: 2px; padding: 12px 14px; position: sticky; top: 0; z-index: 50; backdrop-filter: blur(20px); margin-bottom: 10px; transition: opacity 2s, box-shadow 0.3s; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
.progress-panel.done { box-shadow: 0 2px 10px rgba(0,0,0,0.3); }
.progress-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.progress-header .prog-mode { font-family: 'JetBrains Mono', monospace; font-size: 10px; text-transform: uppercase; letter-spacing: 1.5px; color: var(--accent); font-weight: 700; }
.progress-header .prog-time { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text2); letter-spacing: 0.5px; }
.progress-track { height: 6px; background: rgba(0,0,0,0.4); border: 1px solid var(--border); border-radius: 1px; overflow: hidden; margin-bottom: 8px; }
.progress-track { height: 6px; background: rgba(0,0,0,0.4); border: 1px solid var(--border); border-radius: 1px; overflow: hidden; margin-bottom: 6px; }
.progress-fill { height: 100%; background: var(--accent); transition: width 0.4s ease; box-shadow: 0 0 10px rgba(226,181,90,0.3); position: relative; }
.progress-fill::after { content: ''; position: absolute; right: 0; top: 0; bottom: 0; width: 20px; background: linear-gradient(90deg, transparent, var(--accent2)); animation: progress-shimmer 1.5s ease-in-out infinite; }
@keyframes progress-shimmer { 0%,100% { opacity: 0.3; } 50% { opacity: 1; } }
.progress-steps { display: flex; gap: 4px; margin-bottom: 8px; }
.progress-steps { display: flex; gap: 4px; margin-bottom: 6px; }
.progress-step { flex: 1; height: 3px; background: rgba(0,0,0,0.4); border-radius: 1px; transition: background 0.3s; }
.progress-step.done { background: var(--accent); }
.progress-step.active { background: var(--accent); animation: step-pulse 1s ease-in-out infinite; }
@ -661,6 +662,9 @@ HTML = r"""
.progress-detail { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text2); 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: var(--text2); opacity: 0.6; }
.phase-label { font-family: 'JetBrains Mono', monospace; font-size: 9px; text-transform: uppercase; letter-spacing: 2px; color: var(--accent); padding: 10px 0 6px; opacity: 0.6; display: flex; align-items: center; gap: 8px; }
.phase-label::before { content: ''; flex: 0 0 12px; height: 1px; background: var(--accent); opacity: 0.4; }
.phase-label::after { content: ''; flex: 1; height: 1px; background: var(--accent); opacity: 0.15; }
.sample-prompts { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
.sample-chip { background: rgba(0,0,0,0.3); border: 1px solid var(--border); border-radius: 2px; padding: 6px 12px; font-size: 11px; color: var(--text2); cursor: pointer; transition: all 0.15s; line-height: 1.4; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sample-chip:hover { border-color: var(--accent); color: var(--accent); background: var(--glow); }
@ -1374,13 +1378,14 @@ async function runTeam() {
clearInterval(_runTimer);
const prog = document.getElementById('run-progress');
if (prog) {
prog.classList.add('done');
const fillEl = document.getElementById('prog-fill');
if (fillEl) { fillEl.style.width = '100%'; fillEl.style.boxShadow = '0 0 16px rgba(74,222,128,0.4)'; fillEl.style.background = 'var(--green)'; }
const sub = document.getElementById('prog-substep');
if (sub) sub.textContent = 'Complete — ' + formatElapsed(Date.now() - _runStartTime);
if (sub) sub.textContent = 'Complete — ' + formatElapsed(Date.now() - _runStartTime) + '' + _runResponseCount + ' responses';
const allSteps = prog.querySelectorAll('.progress-step');
allSteps.forEach(function(s) { s.className = 'progress-step done'; });
setTimeout(function() { if (prog.parentNode) { prog.style.opacity = '0.4'; prog.style.transition = 'opacity 2s'; } }, 3000);
setTimeout(function() { if (prog.parentNode) { prog.style.opacity = '0.5'; } }, 5000);
}
btn.disabled = false; btn.textContent = 'Run Team';
}
@ -1390,6 +1395,7 @@ function handleEvent(evt) {
if (evt.type === 'clear') {
const prog = document.getElementById('run-progress');
output.textContent = '';
output.dataset.lastPhase = '';
if (prog) output.appendChild(prog);
return;
}
@ -1435,6 +1441,18 @@ function handleEvent(evt) {
if (evt.type === 'response') {
_runResponseCount++;
const bar = output.querySelector('.status-bar'); if (bar) bar.remove();
// Phase labels show when role changes
const role = evt.role || 'response';
const PHASE_MAP = {scout:'scouting',researcher:'researching',respondent:'models responding','fact-checker':'fact-checking',synthesis:'synthesizing',judge:'judging',error:'error',coder:'coding',reviewer:'reviewing',tester:'testing',attacker:'red teaming',patcher:'patching',survivor:'surviving','chaos-agent':'chaos round','mesh-360':'360 synthesis'};
const phaseName = PHASE_MAP[role] || role;
const lastPhase = output.dataset.lastPhase || '';
if (phaseName !== lastPhase && role !== 'error') {
output.dataset.lastPhase = phaseName;
var label = document.createElement('div');
label.className = 'phase-label';
label.textContent = phaseName;
output.appendChild(label);
}
const mi = availableModels.findIndex(m => m.name === evt.model);
const color = COLORS[(mi >= 0 ? mi : 0) % COLORS.length];
const displayName = mi >= 0 ? (availableModels[mi].display_name || evt.model) : evt.model;
@ -1449,6 +1467,8 @@ function handleEvent(evt) {
card.dataset.role = evt.role || '';
card.dataset.displayName = displayName;
output.appendChild(card);
// Auto-scroll to latest response
card.scrollIntoView({behavior: 'smooth', block: 'nearest'});
}
}