/* ═══════════════════════════════════════════════════ RasoSpeak — UI Rendering Layer All DOM mutations, phase animations, stats dashboard. ═══════════════════════════════════════════════════ */ /* ── VIEWS ──────────────────────────────────────────── */ function switchView(v) { ['script', 'live', 'stats', 'partner'].forEach(id => { document.getElementById(`view-${id}`)?.classList.toggle('active', id === v); document.getElementById(`nav-${id}`)?.classList.toggle('active', id === v); }); if (v === 'stats') renderStatsView(); } /* ── CHUNK LIST — SCRIPT STUDIO ─────────────────────── */ function renderChunkList() { const list = document.getElementById('chunk-list'); const count = document.getElementById('chunk-count'); if (!list) return; if (count) count.textContent = S.chunks.length ? `${S.chunks.length} chunks` : '—'; if (!S.chunks.length) { list.innerHTML = `

Paste a script to preview chunks here.

`; return; } list.innerHTML = S.chunks.map((c, i) => `
${i + 1} / ${S.chunks.length} ${c.split(' ').length}w
${escHtml(c)}
`).join(''); } /* ── CHUNK LIST — LIVE VIEW ─────────────────────────── */ function renderLiveChunkList() { const list = document.getElementById('live-chunk-list'); const cnt = document.getElementById('live-chunk-count'); if (!list) return; if (cnt) cnt.textContent = S.chunks.length ? `${S.chunks.length} chunks` : '—'; if (!S.chunks.length) { list.innerHTML = '

No script loaded

'; return; } list.innerHTML = S.chunks.map((c, i) => { const r = S.segResults[i]; const done = i < S.cur; const active = i === S.cur; const next = i === S.cur + 1; let cls = active ? 'border-primary-container bg-surface-block shadow-md scale-[1.02]' : done ? 'opacity-50 grayscale' : 'border-yc'; const badge = r ? `${Math.round(r.score * 100)}%` : done ? '—' : ''; const icon = active ? '▶' : done ? (r?.status === 'ok' ? '✓' : '✗') : ''; return `
${icon} ${i + 1} ${badge}
${escHtml(c)}
`; }).join(''); document.getElementById(`lc${S.cur}`)?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } /* ── STAGE TEXT ─────────────────────────────────────── */ function showChunkText(expected, spoken, result) { const el = document.getElementById('current-chunk'); // Updated ID to match index.html if (!el) return; if (!spoken) { el.innerHTML = expected.split(/(\s+)/).map(t => /^\s+$/.test(t) ? ' ' : `${escHtml(t)}` ).join(''); return; } const spkW = keyWords(spoken); el.innerHTML = expected.split(/(\s+)/).map(t => { if (/^\s+$/.test(t)) return ' '; const clean = t.toLowerCase().replace(/[^a-z0-9']/g, ''); if (clean.length <= 2 || STOP_WORDS.has(clean)) return `${escHtml(t)}`; const hit = spkW.some(sw => wordSim(clean, sw) >= (S.confidence || 0.75)); return `${escHtml(t)}`; }).join(''); } function updateSegDisplay() { const numEl = document.getElementById('seg-num'); const totalEl = document.getElementById('seg-total'); if (numEl) numEl.textContent = S.cur + 1; if (totalEl) totalEl.textContent = S.chunks.length || '—'; if (S.chunks[S.cur]) showChunkText(S.chunks[S.cur], null, null); } /* ── COMPARE DETAIL ─────────────────────────────────── */ function showCompareDetail(result) { const badge = document.getElementById('acc-badge'); if (badge) { badge.textContent = result.matchPct + '%'; badge.style.color = result.status === 'ok' ? 'var(--secondary)' : result.status === 'warn' ? 'var(--accent)' : 'var(--danger)'; } const det = document.getElementById('compare-detail'); if (!det) return; const barCol = result.status === 'ok' ? 'var(--secondary)' : result.status === 'warn' ? 'var(--accent)' : 'var(--danger)'; const matchHtml = (result.matchedWords || []).slice(0, 6) .map(w => `${escHtml(w)}`).join(''); const missHtml = (result.missedWords || []).slice(0, 5) .map(w => `${escHtml(w)}`).join(''); det.innerHTML = `
${matchHtml}${missHtml}
`; } /* ── PROGRESS ───────────────────────────────────────── */ function updateProgress(done) { const pct = S.chunks.length ? Math.round((done ? 1 : S.cur / S.chunks.length) * 100) : 0; const fill = document.getElementById('progress-bar'); const lbl = document.getElementById('progress-text'); if (fill) fill.style.width = pct + '%'; if (lbl) lbl.textContent = (done ? S.chunks.length : S.cur) + ' / ' + S.chunks.length + ' chunks'; } /* ── PHASE SYSTEM ───────────────────────────────────── */ const PHASE_CONFIG = { [PHASE.IDLE]: { label: 'Idle', color: 'var(--on-surface-variant)', status: 'muted', glow: null, tabId: null }, [PHASE.DELIVER]: { label: '🎧 Listen to earpiece…', color: 'var(--primary)', status: 'primary', glow: 'rgba(163,62,0,0.05)', tabId: 'deliver' }, [PHASE.LISTEN]: { label: '🎤 Speak to audience!', color: 'var(--secondary)', status: 'secondary', glow: 'rgba(93,95,90,0.05)', tabId: 'listen' }, [PHASE.COMPARE]: { label: '⚖️ Evaluating via GPU…', color: 'var(--primary-container)', status: 'accent', glow: 'rgba(255,102,0,0.05)', tabId: 'compare' }, [PHASE.CORRECT]: { label: '🔄 Correcting via GPU…', color: 'var(--error)', status: 'danger', glow: 'rgba(186,26,26,0.05)', tabId: 'correct' }, }; function setPhase(ph) { S.phase = ph; const cfg = PHASE_CONFIG[ph] || PHASE_CONFIG[PHASE.IDLE]; // Ambient glow const glow = document.getElementById('stage-ambient'); if (glow) { let glowColor = "transparent"; if (ph === PHASE.DELIVER) glowColor = "rgba(163,62,0,0.05)"; else if (ph === PHASE.LISTEN) glowColor = "rgba(93,95,90,0.05)"; else if (ph === PHASE.COMPARE) glowColor = "rgba(255,102,0,0.05)"; else if (ph === PHASE.CORRECT) glowColor = "rgba(186,26,26,0.05)"; glow.style.background = glowColor !== "transparent" ? `radial-gradient(ellipse 60% 50% at 50% 50%, ${glowColor} 0%, transparent 70%)` : ''; } // Status dot + text const dot = document.getElementById('status-dot'); const txt = document.getElementById('status-text'); if (dot) dot.className = `w-2 h-2 rounded-full ${ph === PHASE.IDLE ? 'bg-outline' : 'bg-primary-container animate-pulse'}`; if (txt) txt.textContent = `Status: ${cfg.label.replace(/^[^\w]+/, '')}`; // Update GPU agent indicator label updateAgentIndicator(ph); // Waveform colours if (typeof animateWaveformForPhase === 'function') animateWaveformForPhase(ph); } function updateAgentIndicator(ph) { const el = document.getElementById('agent-indicator-label'); if (!el) return; const labels = { [PHASE.IDLE]: 'Agents idle', [PHASE.DELIVER]: 'TTS → earpiece', [PHASE.LISTEN]: 'TranscriptionAgent (Whisper) active', [PHASE.COMPARE]: 'ScoringAgent (Qwen2.5-7B) running', [PHASE.CORRECT]: 'CoachingAgent (Qwen2.5-7B) running', }; el.textContent = labels[ph] || 'GPU Accelerator'; } function setCue(txt, color, active) { const label = document.getElementById('cue-label'); const dot = document.getElementById('cue-dot'); if (label) { label.textContent = txt; label.style.color = color || ''; } if (dot) { dot.style.background = color || 'var(--text-muted)'; dot.classList.toggle('pulsing', active); } } const STATUS_COLORS = { muted: 'var(--on-surface-variant)', primary: 'var(--primary)', secondary: 'var(--secondary)', accent: 'var(--primary-container)', danger: 'var(--error)', }; function setStatus(colorKey, txt) { const col = STATUS_COLORS[colorKey] || STATUS_COLORS.muted; const dot = document.getElementById('status-dot'); const label = document.getElementById('status-text'); if (dot) { dot.style.background = col; dot.style.boxShadow = colorKey !== 'muted' ? `0 0 7px ${col}` : 'none'; dot.classList.toggle('pulse', colorKey !== 'muted'); } if (label) { label.textContent = txt; label.style.color = col; } } /* ── COACH LOG ──────────────────────────────────────── */ const MAX_LOG = 35; function logCoach(type, icon, msg) { const log = document.getElementById('live-transcript'); if (!log) return; const d = document.createElement('div'); const time = new Date().toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); let typeClasses = ""; if (type === 'ok') typeClasses = "bg-[#E8F5E9] text-[#2E7D32]"; else if (type === 'warn') typeClasses = "bg-[#FFF8E1] text-[#F57F17]"; else if (type === 'error' || type === 'miss') typeClasses = "bg-[#FFEBEE] text-[#C62828]"; else typeClasses = "bg-[#F3E5F5] text-[#7B1FA2]"; d.className = `flex items-start gap-3 p-2 border-b border-yc hover:bg-surface-block transition-colors animate-fadeIn`; d.innerHTML = ` ${time} ${icon} ${msg} `; log.insertBefore(d, log.firstChild); while (log.children.length > MAX_LOG) log.removeChild(log.lastChild); } function clearLog() { const log = document.getElementById('live-transcript'); if (log) log.innerHTML = ''; } /* ── METRICS ────────────────────────────────────────── */ function setM(key, val) { const el = document.getElementById('m-' + key); if (el) el.textContent = val; } /* ── BUTTON HELPERS ─────────────────────────────────── */ function setMainBtn(icon) { const el = document.getElementById('play-icon'); if (el) el.textContent = icon; } function showEndBtn(show) { document.getElementById('btn-end')?.classList.toggle('hidden', !show); } function showTimer(show) { document.getElementById('header-timer')?.classList.toggle('hidden', !show); document.getElementById('footer-timer')?.classList.toggle('hidden', !show); } /* ── SETTINGS ───────────────────────────────────────── */ function setMode(btn) { S.mode = btn.dataset.mode; if (typeof APP !== 'undefined') APP.mode = S.mode; document.querySelectorAll('.mode-btn').forEach(b => b.classList.toggle('active', b === btn)); } function setStrict(btn) { S.strict = parseInt(btn.dataset.strict); if (typeof APP !== 'undefined') APP.strict = S.strict; document.querySelectorAll('.strict-btn').forEach(b => b.classList.toggle('active', b === btn)); } function setChunk(n) { S.chunkSize = n; if (typeof APP !== 'undefined') APP.chunkSize = n; document.querySelectorAll('.chunk-btn').forEach(b => b.classList.toggle('active', parseInt(b.dataset.size) === n)); const raw = document.getElementById('script-ta')?.value.trim(); if (raw) { buildChunks(raw); renderChunkList(); liveWC(); } } /* ── STATS VIEW ─────────────────────────────────────── */ function renderStatsView() { loadHistory(); const history = S.sessionHistory; const total = history.length; const avgAcc = total ? Math.round(history.reduce((a, s) => a + s.accuracy, 0) / total) : 0; const avgWpm = total ? Math.round(history.filter(s => s.wpm > 0).reduce((a, s) => a + s.wpm, 0) / Math.max(1, history.filter(s => s.wpm > 0).length)) : 0; const bestAcc = total ? Math.max(...history.map(s => s.accuracy)) : 0; const totalCorr = history.reduce((a, s) => a + (s.corrections || 0), 0); const gpuSessions = history.filter(s => s.backend?.includes('GPU')).length; const el = document.getElementById('stats-container'); if (!el) return; el.innerHTML = `

Session Analytics

${total} sessions recorded on GPU infrastructure.

Avg Accuracy ${avgAcc}%
Best Performance ${bestAcc}%
GPU Utilized ${gpuSessions}
${history.map(s => ` `).join('')}
Date Chunks Accuracy WPM Mode
${s.date} ${s.time} ${s.done}/${s.chunks} ${s.accuracy}% ${s.wpm || '—'} ${s.mode || '—'}
${total === 0 ? '
No session telemetry found.
' : ''}
`; } /* ── TOAST ──────────────────────────────────────────── */ let _toastTimer; function toast(msg) { const el = document.getElementById('toast'); if (!el) return; el.textContent = msg; el.classList.add('show'); clearTimeout(_toastTimer); _toastTimer = setTimeout(() => el.classList.remove('show'), 3800); } /* ── UTIL ───────────────────────────────────────────── */ function escHtml(str) { return String(str) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); }