| export function createRuntimeMeter(element) { |
| const value = element.querySelector('.runtime-value'); |
| const chart = element.querySelector('.runtime-sparkline'); |
| const bars = element.querySelector('.runtime-bars'); |
| const announcement = element.querySelector('[role="status"]'); |
| const engine = element.querySelector('.runtime-engine'); |
| |
| |
| const content = element.querySelector('.runtime-content'); |
| const sizeObserver = new ResizeObserver(([entry]) => { |
| element.style.width = `calc(${Math.ceil(entry.contentRect.width)}px + 2 * var(--runtime-padding))`; |
| }); |
| sizeObserver.observe(content); |
| let loaded = false, phase, rates = [], staleTimer; |
| function show(nextPhase, label, description) { |
| clearTimeout(staleTimer); |
| if (nextPhase !== phase) announcement.textContent = description ?? `${label} in this browser.`; |
| phase = nextPhase; |
| engine.textContent = phase === 'tool' ? 'JavaScript' : 'WebGPU'; |
| element.dataset.state = phase; |
| value.textContent = label; |
| chart.setAttribute('hidden', ''); |
| element.title = description ?? `${label} in this browser.`; |
| if (phase !== 'generating') { rates = []; bars.replaceChildren(); } |
| } |
| function idle() { show('idle', loaded ? 'Ready' : 'Idle', loaded ? 'Model ready. Inference and tools run in this browser.' : 'Model loads when you send a message. Inference and tools run in this browser.'); } |
| function sample(rate) { |
| if (!Number.isFinite(rate) || rate < 0) { show('generating', 'Generating…'); return; } |
| show('generating', `${rate < 10 ? rate.toFixed(1) : Math.round(rate)} tok/s`, 'Live token generation speed, including thinking tokens. Prompt-processing time is excluded.'); |
| rates.push(rate); if (rates.length > 12) rates.shift(); |
| const ceiling = Math.max(40, ...rates); |
| |
| const slots = [...Array(12 - rates.length).fill(null), ...rates]; |
| bars.replaceChildren(...slots.map((rate, index) => { |
| if (rate === null || rate === 0) { |
| const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); |
| dot.setAttribute('cx', String(index * 4 + 1)); |
| dot.setAttribute('cy', '17'); dot.setAttribute('r', '1'); |
| dot.setAttribute('opacity', rate === null ? '.35' : '.6'); |
| return dot; |
| } |
| const bar = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); |
| const height = Math.max(2, Math.min(16, rate / ceiling * 16)); |
| bar.setAttribute('x', String(index * 4)); |
| bar.setAttribute('y', (18 - height).toFixed(2)); |
| bar.setAttribute('width', '2'); bar.setAttribute('height', height.toFixed(2)); bar.setAttribute('rx', '1'); |
| return bar; |
| })); |
| chart.removeAttribute('hidden'); |
| |
| staleTimer = setTimeout(() => { show('generating', 'Generating…'); rates = []; bars.replaceChildren(); }, 2000); |
| } |
| idle(); |
| return { |
| update(data) { |
| if (data.type === 'loaded') { loaded = true; idle(); } |
| else if (data.type === 'idle' || data.type === 'initialized') idle(); |
| else if (data.type === 'fatal') { loaded = false; show('error', 'Unavailable'); } |
| else if (data.type === 'busy') { |
| if (data.action === 'load') show('loading', 'Loading…', data.cachedOnly ? 'Loading local model…' : undefined); |
| else if (data.action === 'prompt') show('prefill', 'Reading prompt…'); |
| else if (data.action === 'shell') show('tool', 'Running bash…'); |
| else if (data.action === 'read') show('tool', 'Reading file…'); |
| else if (data.action === 'write') show('tool', 'Writing file…'); |
| else show('working', 'Working…'); |
| } else if (data.type === 'load_progress') { |
| const label = data.phase === 'download' && data.loaded < data.total ? `Downloading ${Math.round(data.loaded / data.total * 100)}%` : data.phase === 'warmup' ? 'Warming up…' : 'Preparing…'; |
| show('loading', label); |
| } else if (data.type === 'inference_activity') { |
| if (data.phase === 'prefill') show('prefill', 'Reading prompt…'); |
| else if (data.phase === 'decode') sample(data.rate); |
| else if (phase === 'generating' || phase === 'prefill') show('working', 'Working…'); |
| } else if (data.type === 'agent_event' && data.event.type === 'tool_execution_start') { |
| const labels = { bash: 'Running bash…', read: 'Reading file…', write: 'Writing file…', edit: 'Editing file…' }; |
| show('tool', labels[data.event.toolName] ?? 'Running tool…'); |
| } |
| }, |
| }; |
| } |
|
|