(function () { 'use strict'; // ── DOM refs ────────────────────────────────────────────────────────────── const body = document.body; const wsDot = document.getElementById('ws-dot'); const stateLabel = document.getElementById('state-label'); const progressFill = document.getElementById('progress-fill'); const progressLabel = document.getElementById('progress-label'); const svModel = document.getElementById('sv-model'); const svState = document.getElementById('sv-state'); const svPrompt = document.getElementById('sv-prompt'); const svSpeed = document.getElementById('sv-speed'); const svTokens = document.getElementById('sv-tokens'); const svLoadStage = document.getElementById('sv-load-stage'); const svReqs = document.getElementById('sv-reqs'); // ── State class management ──────────────────────────────────────────────── const ALL_STATES = [ 'offline','starting','loading','warming_up','idle', 'sleeping','waiting','processing_prompt','generating','unloading' ]; function applyState(stateName) { ALL_STATES.forEach(s => body.classList.remove('state-' + s)); body.classList.add('state-' + (ALL_STATES.includes(stateName) ? stateName : 'offline')); } // ── DOM update ──────────────────────────────────────────────────────────── function fmt(val, unit) { return val != null ? val.toFixed(1) + ' ' + unit : '—'; } function truncate(str, max) { return str.length > max ? '…' + str.slice(-(max - 1)) : str; } function update(msg) { const { state, model, loading, metrics, request_count } = msg; applyState(state); stateLabel.textContent = state.replace('_', ' ').toUpperCase(); svState.textContent = state.replace('_', ' '); svModel.textContent = model ? truncate(model, 22) : '—'; svReqs.textContent = request_count ?? 0; // Loading progress if (loading) { const pct = Math.round((loading.progress ?? 0) * 100); progressFill.style.width = pct + '%'; progressLabel.textContent = loading.current + ' ' + pct + '%'; svLoadStage.textContent = loading.current; } else { progressFill.style.width = '0%'; progressLabel.textContent = ''; svLoadStage.textContent = '—'; } // Metrics if (metrics) { svPrompt.textContent = fmt(metrics.prompt_speed, 't/s'); svSpeed.textContent = fmt(metrics.gen_speed, 't/s'); svTokens.textContent = metrics.n_decoded != null ? metrics.n_decoded + ' tok' : '—'; } else { svPrompt.textContent = svSpeed.textContent = svTokens.textContent = '—'; } // Pulse dot on message wsDot.classList.remove('pulse'); void wsDot.offsetWidth; // force reflow to restart animation wsDot.classList.add('pulse'); } // ── WebSocket with auto-reconnect ───────────────────────────────────────── let ws = null; let retryDelay = 1000; const MAX_DELAY = 30000; function connect() { const url = 'ws://' + location.host + '/ws'; ws = new WebSocket(url); ws.addEventListener('open', () => { wsDot.className = 'connected'; retryDelay = 1000; }); ws.addEventListener('message', (event) => { try { update(JSON.parse(event.data)); } catch (e) { console.error('llamagochi: bad message', e); } }); ws.addEventListener('close', () => { wsDot.className = ''; applyState('offline'); stateLabel.textContent = 'DISCONNECTED'; setTimeout(connect, retryDelay); retryDelay = Math.min(retryDelay * 2, MAX_DELAY); }); ws.addEventListener('error', () => { ws.close(); }); } connect(); })();