MiniCPM5-2B-WebGPU-Pi-HTTP / app /src /runtime-meter.mjs
Mike0021's picture
Enable browser HTTP commands with CORS-aware errors and bounded requests
39371ea verified
Raw
History Blame Contribute Delete
4.79 kB
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');
// Measure the intrinsic contents so CSS can animate between numeric widths.
// This also follows font changes without reserving empty space while idle.
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);
// Keep all twelve positions visible; dots mark gaps without inventing samples.
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');
// If worker updates stop arriving, do not leave an old speed looking live.
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…');
}
},
};
}