Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
11 kB · 223 lines
HTML
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>jev-facade monitor</title><style> :root { --bg: #14110f; --panel: #1d1916; --line: #2e2823; --text: #eadfd3; --dim: #8f8377; --grace: #e8896b; --trip: #6fa8dc; --ok: #8fc98a; --warn: #e2b04a; --bad: #e0645c; --mono: ui-monospace, "Cascadia Mono", Consolas, monospace; } * { box-sizing: border-box; } body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.45 "Segoe UI", system-ui, sans-serif; } header { position: sticky; top: 0; z-index: 2; background: var(--bg); border-bottom: 1px solid var(--line); padding: 12px 16px; } .bar { display: flex; flex-wrap: wrap; gap: 8px 20px; align-items: baseline; max-width: 980px; margin: 0 auto; } h1 { font-size: 15px; margin: 0; letter-spacing: .04em; font-weight: 600; } .dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--bad); margin-right: 6px; } .dot.live { background: var(--ok); } .stat { color: var(--dim); font-size: 13px; } .stat b { color: var(--text); font-weight: 600; font-family: var(--mono); } form { display: flex; gap: 8px; max-width: 980px; margin: 10px auto 0; } input { flex: 1; min-width: 0; background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px; font: inherit; } input:focus { outline: 1px solid var(--dim); } button { background: var(--panel); color: var(--text); border: 1px solid var(--line); border-radius: 6px; padding: 8px 14px; font: inherit; cursor: pointer; } main { max-width: 980px; margin: 0 auto; padding: 12px 16px 60px; } .empty { color: var(--dim); padding: 40px 0; text-align: center; } .event { border: 1px solid var(--line); background: var(--panel); border-radius: 8px; margin-bottom: 8px; border-left: 3px solid var(--ok); } .event.deflect { border-left-color: var(--warn); } .event.error { border-left-color: var(--bad); } .event.fresh { animation: flash 1.2s ease-out; } @keyframes flash { from { background: #3a3128; } to { background: var(--panel); } } summary { list-style: none; cursor: pointer; padding: 10px 12px; display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; } summary::-webkit-details-marker { display: none; } .said { font-size: 17px; overflow-wrap: anywhere; } .said::before { content: "\201C"; color: var(--dim); } .said::after { content: "\201D"; color: var(--dim); } .meta { color: var(--dim); font: 12px var(--mono); text-align: right; white-space: nowrap; } .meta .slow { color: var(--warn); } .chips { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px; } .chip { font: 13px var(--mono); padding: 2px 8px; border-radius: 999px; border: 1px solid var(--line); background: var(--bg); } .chip .p { color: var(--dim); } .chip.grace { border-color: var(--grace); } .chip.trip { border-color: var(--trip); } .chip.deflect { border-color: var(--warn); color: var(--warn); } .chip.error { border-color: var(--bad); color: var(--bad); } .to-grace { color: var(--grace); } .to-trip { color: var(--trip); } .detail { border-top: 1px solid var(--line); padding: 10px 12px 12px; } .ctx { color: var(--dim); font-size: 13px; margin-bottom: 8px; overflow-wrap: anywhere; } .ctx code { font-family: var(--mono); color: var(--text); } .questions { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 10px 16px; } .q h3 { margin: 0 0 3px; font: 600 12px var(--mono); color: var(--dim); display: flex; justify-content: space-between; } .q.used h3 { color: var(--text); } .row { display: grid; grid-template-columns: 1fr 38px; gap: 6px; font: 12px var(--mono); position: relative; padding: 1px 4px; } .row i { position: absolute; inset: 0 auto 0 0; background: #3b332c; border-radius: 3px; z-index: 0; } .row span { position: relative; z-index: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .row span:last-child { text-align: right; color: var(--dim); } .wire { margin-top: 10px; font: 12px var(--mono); color: var(--dim); white-space: pre; }</style></head><body><header> <div class="bar"> <h1><span class="dot" id="dot"></span>jev-facade</h1> <span class="stat">model <b id="model">-</b></span> <span class="stat">lines <b id="count">0</b></span> <span class="stat">understood <b id="understood">-</b></span> <span class="stat">median <b id="median">-</b></span> <span class="stat">errors <b id="errors">0</b></span> </div> <form id="try"> <input id="line" placeholder="Try a line without the game..." autocomplete="off"> <button>Send</button> </form></header><main> <div class="empty" id="empty">Waiting for the player to say something.</div> <div id="feed"></div></main><script>const feed = document.getElementById('feed');const seen = new Set();const stats = { count: 0, understood: 0, errors: 0, latencies: [] };const WHO = { 0: 'grace', 1: 'trip' };
function el(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node;}
function chip(act) { const who = WHO[act.char_id]; const node = el('span', 'chip' + (who ? ' ' + who : '')); if (act.da_id === 43) { node.className = 'chip deflect'; node.textContent = 'deflect (not understood)'; return node; } node.append(act.name); const params = act.param_names.filter(Boolean); if (params.length) node.append(el('span', 'p', '(' + params.join(', ') + ')')); if (who) node.append(' ', el('span', 'to-' + who, '→ ' + who)); node.append(' ', el('span', 'p', act.probability.toFixed(2))); return node;}
function question(name, answer, used) { const box = el('div', 'q' + (used ? ' used' : '')); const head = el('h3'); // Jev's own confidence: how peaked the distribution is. Shown, never acted on. const confidence = answer.confidence === null ? '' : 'conf ' + answer.confidence.toFixed(2); head.append(el('span', '', name), el('span', '', confidence)); box.append(head); for (const [option, p] of answer.top) { const row = el('div', 'row'); const fill = el('i'); fill.style.width = Math.round(p * 100) + '%'; row.append(fill, el('span', '', option), el('span', '', p.toFixed(2))); box.append(row); } return box;}
function render(event, fresh) { if (seen.has(event.id)) return; seen.add(event.id); document.getElementById('empty').hidden = true;
const kind = event.error ? 'error' : event.understood ? '' : 'deflect'; const card = el('details', 'event ' + kind + (fresh ? ' fresh' : '')); const summary = el('summary'); summary.append(el('div', 'said', event.text || '(empty)')); const meta = el('div', 'meta'); const ms = el('span', event.elapsed_ms > 600 ? 'slow' : '', event.elapsed_ms + ' ms'); meta.append(new Date(event.time * 1000).toLocaleTimeString(), ' ', ms, ' ', event.source); summary.append(meta); const chips = el('div', 'chips'); if (event.error) chips.append(el('span', 'chip error', event.error)); // SystemDoesntUnderstand rides along with every line; only show it when it is all there is. const real = event.acts.filter(act => act.da_id !== 43); (real.length ? real : event.acts).forEach(act => chips.append(chip(act))); summary.append(chips); card.append(summary);
const detail = el('div', 'detail'); if (event.said.length || event.contexts.length) { const ctx = el('div', 'ctx'); if (event.said.length) ctx.append('they just said: ', el('code', '', event.said.join(' / ')), ' '); if (event.contexts.length) ctx.append(' listening for: ', el('code', '', event.contexts.join(', '))); detail.append(ctx); } // Parameter questions are always asked; dim the ones no emitted act read. const read = new Set(['act', 'addressee', 'reference']); event.acts.forEach(act => act.param_families.forEach(family => read.add(family))); const grid = el('div', 'questions'); for (const [name, answer] of Object.entries(event.answers)) { grid.append(question(name, answer, read.has(name))); } detail.append(grid); detail.append(el('div', 'wire', 'sent to game (id charID p1 p2 p3):\n' + event.acts.map(a => [a.da_id, a.char_id, a.param1, a.param2, a.param3].join(' ')).join('\n'))); card.append(detail); feed.prepend(card);
stats.count++; if (event.error) stats.errors++; else stats.latencies.push(event.elapsed_ms); if (event.understood) stats.understood++; const sorted = [...stats.latencies].sort((a, b) => a - b); document.getElementById('count').textContent = stats.count; document.getElementById('errors').textContent = stats.errors; document.getElementById('understood').textContent = Math.round(100 * stats.understood / stats.count) + '%'; document.getElementById('median').textContent = sorted.length ? sorted[sorted.length >> 1] + ' ms' : '-';}
async function start() { const health = await (await fetch('/health')).json(); document.getElementById('model').textContent = health.model;
// The server subscribes a stream before answering it, so once it is open // every later event will arrive on it. Fetch history only then, hold live // events until it is drawn so the feed stays in order, and let render() drop // the overlap by id. Runs again on every reconnect (a restarted sidecar), so // nothing recorded while the page was disconnected is missed. let historyDrawn = false; let held = []; const stream = new EventSource('/events'); const dot = document.getElementById('dot'); stream.onerror = () => dot.classList.remove('live'); stream.onmessage = message => { const event = JSON.parse(message.data); if (historyDrawn) render(event, true); else held.push(event); }; stream.onopen = async () => { dot.classList.add('live'); historyDrawn = false; (await (await fetch('/history')).json()).forEach(event => render(event, false)); historyDrawn = true; held.forEach(event => render(event, true)); held = []; };}
document.getElementById('try').addEventListener('submit', submitted => { submitted.preventDefault(); const input = document.getElementById('line'); const text = input.value.trim(); if (!text) return; input.value = ''; fetch('/classify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, source: 'monitor' }) });});
start();</script></body></html>