From 5ad6f9fd62efb981b260648471e769114968ab22 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 00:56:49 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20model=20comparison=20viewer=20=E2=80=94?= =?UTF-8?q?=20side-by-side=208B=20/=2014B=20/=2036B=20inference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a full-screen overlay panel allowing side-by-side comparison of three model sizes (8B, 14B, 36B). Key features: - Three-column grid: each column shows model label, badge (FAST / BALANCED / DEEP), live metrics (tok/s, latency ms, VRAM GB), and a typewriter-streamed response output - Prompt input row with RUN button; Enter key also triggers inference - Simulated inference with per-model latency stagger and character-stream speed matching realistic tok/s baselines - [M] keyboard shortcut toggles the panel; Escape closes it - HUD ⚖ button in the top-right control strip - Matches existing dark-space holographic theme and CSS design system - Hides cleanly in photo mode; no interference with sovereignty buffer Fixes #282 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 204 ++++++++++++++++++++++++++++++++++++++++++++++ index.html | 54 +++++++++++++ style.css | 234 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 492 insertions(+) diff --git a/app.js b/app.js index e86b38a..d60d12a 100644 --- a/app.js +++ b/app.js @@ -1453,6 +1453,210 @@ document.addEventListener('keydown', (e) => { sovereigntyBufferTimer = setTimeout(() => { sovereigntyBuffer = ''; }, 3000); }); +// === MODEL COMPARISON VIEWER === +// Side-by-side inference viewer for 8B, 14B, and 36B models. +// Uses simulated responses with realistic per-model characteristics +// (speed, latency, memory, verbosity) since there is no local inference backend. + +const MODEL_SPECS = { + '8b': { tps: 85, latMs: 210, memGb: 5.2, badgeColor: '#44ddaa' }, + '14b': { tps: 52, latMs: 380, memGb: 8.9, badgeColor: '#ffcc44' }, + '36b': { tps: 24, latMs: 870, memGb: 22.4, badgeColor: '#ff66aa' }, +}; + +// Canned response fragments keyed by prompt theme — cycled for demo purposes. +const MODEL_RESPONSES = { + '8b': [ + 'A fast, compact response. The 8B model prioritises throughput over depth, giving quick answers with solid accuracy on common tasks.', + 'Short context window and fewer parameters mean this model excels at summarisation, classification, and single-turn Q&A.', + 'Inference is snappy — ideal for high-volume pipelines where latency matters more than nuance.', + ], + '14b': [ + 'A balanced 14B model delivers noticeably richer reasoning. It can hold more context and produces more coherent multi-step answers than the 8B tier.', + 'Good trade-off between speed and quality: fast enough for interactive use, smart enough for light analytical tasks and code generation.', + 'Memory overhead stays under 10 GB, making it deployable on a single consumer GPU with room to spare.', + ], + '36b': [ + 'At 36B parameters the model enters a different reasoning tier. Responses are longer, more nuanced, and better at multi-hop inference — at the cost of roughly 4× the latency of the 8B model.\n\nFor sovereign infrastructure, the extra depth is worth it when correctness matters more than speed.', + 'Larger weights capture more rare-domain knowledge. Code generation, legal summarisation, and philosophical dialogue all improve markedly over smaller models.', + 'The 36B tier is well-suited for batch workloads where latency is tolerable: nightly analysis, long-document synthesis, or chain-of-thought reasoning pipelines.', + ], +}; + +let modelCompareOpen = false; +let modelCompareRunning = false; +let _modelCompareTimers = []; + +const modelComparePanel = document.getElementById('model-compare'); +const modelCompareInput = /** @type {HTMLInputElement} */ (document.getElementById('model-compare-input')); +const modelCompareRunBtn = document.getElementById('model-compare-run'); +const modelCompareCloseBtn = document.getElementById('model-compare-close'); +const modelCompareToggleBtn = document.getElementById('model-compare-toggle'); + +/** + * Opens or closes the model comparison viewer. + * @param {boolean} [force] - If provided, sets open state explicitly. + */ +function toggleModelCompare(force) { + modelCompareOpen = (force !== undefined) ? force : !modelCompareOpen; + if (modelCompareOpen) { + modelComparePanel.classList.remove('model-compare-hidden'); + modelCompareToggleBtn.classList.add('active'); + modelCompareInput.focus(); + } else { + modelComparePanel.classList.add('model-compare-hidden'); + modelCompareToggleBtn.classList.remove('active'); + } +} + +/** + * Clears all in-progress typewriter timers. + */ +function clearModelTimers() { + _modelCompareTimers.forEach(id => clearTimeout(id)); + _modelCompareTimers = []; +} + +/** + * Resets all model card outputs to idle state. + */ +function resetModelOutputs() { + clearModelTimers(); + modelCompareRunning = false; + modelCompareRunBtn.disabled = false; + for (const model of ['8b', '14b', '36b']) { + const out = document.querySelector(`.model-output[data-output="${model}"]`); + if (out) { out.textContent = ''; out.classList.remove('running'); } + for (const key of ['tps', 'lat', 'mem']) { + const el = document.querySelector(`.model-card[data-model="${model}"] .metric-val[data-key="${key}"]`); + if (el) el.textContent = '—'; + } + } +} + +/** + * Typewriter-streams text into an element, then calls onDone when finished. + * @param {HTMLElement} el + * @param {string} text + * @param {number} delay - ms between characters + * @param {Function} onDone + */ +function typewriterStream(el, text, delay, onDone) { + let i = 0; + const cursor = document.createElement('span'); + cursor.className = 'cursor'; + el.appendChild(cursor); + + function step() { + if (i < text.length) { + cursor.insertAdjacentText('beforebegin', text[i]); + i++; + const id = setTimeout(step, delay); + _modelCompareTimers.push(id); + } else { + cursor.remove(); + if (onDone) onDone(); + } + } + step(); +} + +/** + * Runs a comparison inference for all three model sizes. + */ +function runModelComparison() { + if (modelCompareRunning) return; + const prompt = modelCompareInput.value.trim(); + if (!prompt) { + modelCompareInput.focus(); + return; + } + + resetModelOutputs(); + modelCompareRunning = true; + modelCompareRunBtn.disabled = true; + + // Pick response index deterministically from prompt length for variety + const idx = prompt.length % 3; + + const models = ['8b', '14b', '36b']; + let doneCount = 0; + + for (const model of models) { + const spec = MODEL_SPECS[model]; + const responseText = MODEL_RESPONSES[model][idx]; + + // Jitter metrics slightly around baseline + const jitter = () => (Math.random() - 0.5) * 0.1; + const tps = Math.round(spec.tps * (1 + jitter())); + const lat = Math.round(spec.latMs * (1 + jitter())); + const mem = (spec.memGb * (1 + jitter() * 0.5)).toFixed(1); + + // Show metrics immediately + const card = document.querySelector(`.model-card[data-model="${model}"]`); + card.querySelector('.metric-val[data-key="tps"]').textContent = tps; + card.querySelector('.metric-val[data-key="lat"]').textContent = lat; + card.querySelector('.metric-val[data-key="mem"]').textContent = mem; + + const out = card.querySelector('.model-output'); + out.classList.add('running'); + + // Stagger start by latency to simulate real inference delays + const startDelay = spec.latMs; + const charDelay = Math.round(1000 / spec.tps); + + const startId = setTimeout(() => { + typewriterStream(out, responseText, charDelay, () => { + out.classList.remove('running'); + doneCount++; + if (doneCount === models.length) { + modelCompareRunning = false; + modelCompareRunBtn.disabled = false; + } + }); + }, startDelay); + _modelCompareTimers.push(startId); + } +} + +// Wire up controls +if (modelCompareCloseBtn) { + modelCompareCloseBtn.addEventListener('click', () => { + clearModelTimers(); + resetModelOutputs(); + toggleModelCompare(false); + }); +} + +if (modelCompareToggleBtn) { + modelCompareToggleBtn.addEventListener('click', () => toggleModelCompare()); +} + +if (modelCompareRunBtn) { + modelCompareRunBtn.addEventListener('click', runModelComparison); +} + +if (modelCompareInput) { + modelCompareInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') runModelComparison(); + if (e.key === 'Escape') { + clearModelTimers(); + resetModelOutputs(); + toggleModelCompare(false); + } + e.stopPropagation(); // prevent sovereignty buffer accumulation + }); +} + +// [M] key toggles the panel (only when input is not focused) +document.addEventListener('keydown', (e) => { + if (e.metaKey || e.ctrlKey || e.altKey) return; + if (document.activeElement === modelCompareInput) return; + if (e.key === 'm' || e.key === 'M') { + toggleModelCompare(); + } +}); + window.addEventListener('beforeunload', () => { wsClient.disconnect(); }); diff --git a/index.html b/index.html index 73dd69a..cebd186 100644 --- a/index.html +++ b/index.html @@ -36,6 +36,9 @@ + @@ -51,6 +54,57 @@
⚡ SOVEREIGNTY ⚡
+ + +