/* Whisperkey - custom frontend for gr.Server */ async function api(name, data) { const res = await fetch(`/gradio_api/call/${name}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data }), }); if (!res.ok) throw new Error(`API ${name} failed: ${res.status}`); const { event_id } = await res.json(); const stream = await fetch(`/gradio_api/call/${name}/${event_id}`); const reader = stream.body.getReader(); const dec = new TextDecoder(); let buf = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += dec.decode(value, { stream: true }); } const lines = buf.trim().split('\n').filter(Boolean); const last = JSON.parse(lines[lines.length - 1].replace(/^data:\s*/, '')); // gr.Server wraps each handler's return in a JSON array of outputs; we have one output per handler. return Array.isArray(last) ? last[0] : last; } const state = { sessionId: null, playerName: '', busy: false }; function $(id) { return document.getElementById(id); } function renderChat(chat) { const log = $('chat-log'); log.innerHTML = ''; (chat || []).forEach((m) => { const div = document.createElement('div'); div.className = `chat-msg ${m.role === 'user' ? 'user' : 'bot'}`; if (m.role === 'user') { div.textContent = m.content; } else { div.innerHTML = m.content; } log.appendChild(div); }); log.scrollTop = log.scrollHeight; } function renderModels(models, selected) { const row = $('model-row'); row.innerHTML = ''; (models || []).forEach((m) => { const label = document.createElement('label'); const input = document.createElement('input'); input.type = 'radio'; input.name = 'model'; input.value = m.id; input.checked = m.id === selected; input.addEventListener('change', async () => { if (!state.sessionId || !input.checked) return; const out = await api('set_model', [state.sessionId, m.id]); $('hud').innerHTML = out.hud_html || ''; }); label.append(input, document.createTextNode(m.label)); row.appendChild(label); }); } function setBusy(on) { state.busy = on; $('send-btn').disabled = on; $('guess-btn').disabled = on; } async function apply(out) { if (out.error) { $('result-area').textContent = out.error; return; } state.sessionId = out.session_id || state.sessionId; if (out.hud_html) $('hud').innerHTML = out.hud_html; if (out.chat) renderChat(out.chat); if (out.result_html !== undefined) $('result-area').innerHTML = out.result_html || ''; if (out.hud_html && (out.hud_html.includes('flash-win') || out.hud_html.includes('scorecard'))) { if (window.dojoConfetti) window.dojoConfetti(); if (window.dojoChime) window.dojoChime(); } if (out.hud_html && out.hud_html.includes('flash-block') && window.dojoBuzz) window.dojoBuzz(); } async function init() { const out = await api('init_game', []); state.sessionId = out.session_id; $('hud').innerHTML = out.hud_html || ''; renderChat(out.chat || []); renderModels(out.models, out.default_model); if (out.demo_mode) $('demo-banner').style.display = 'block'; } async function send() { const msg = $('msg-input').value.trim(); if (!msg || state.busy) return; setBusy(true); const stir = document.createElement('div'); stir.className = 'chat-msg bot stirring'; stir.innerHTML = ' 🌫️ the guardian stirs…'; $('chat-log').appendChild(stir); $('msg-input').value = ''; try { const out = await api('send_message', [state.sessionId, msg, state.playerName]); await apply(out); } catch (err) { stir.remove(); $('result-area').textContent = String(err); } finally { setBusy(false); } } async function guess() { const g = $('guess-input').value.trim(); if (!g || state.busy) return; setBusy(true); try { const out = await api('submit_guess', [state.sessionId, g, state.playerName]); $('guess-input').value = ''; await apply(out); } finally { setBusy(false); } } async function action(name) { if (state.busy) return; setBusy(true); try { await apply(await api(name, [state.sessionId])); } finally { setBusy(false); } } async function loadLeaderboard() { const rows = await api('get_leaderboard', []); const tbody = $('lb-body'); tbody.innerHTML = ''; (rows || []).forEach((r) => { const tr = document.createElement('tr'); r.forEach((c) => { const td = document.createElement('td'); td.textContent = c; tr.appendChild(td); }); tbody.appendChild(tr); }); } function bindTabs() { document.querySelectorAll('.tab-btn').forEach((btn) => { btn.addEventListener('click', () => { document.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach((p) => p.classList.remove('active')); btn.classList.add('active'); $(`panel-${btn.dataset.tab}`).classList.add('active'); if (btn.dataset.tab === 'leaderboard') loadLeaderboard(); }); }); } document.addEventListener('DOMContentLoaded', () => { bindTabs(); $('send-btn').addEventListener('click', send); $('guess-btn').addEventListener('click', guess); $('msg-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } }); $('guess-input').addEventListener('keydown', (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); guess(); } }); $('name-input').addEventListener('input', (e) => { state.playerName = e.target.value; }); $('restart-level').addEventListener('click', () => action('restart_level')); $('restart-game').addEventListener('click', () => action('restart_game')); $('concede').addEventListener('click', () => action('concede')); $('lb-refresh').addEventListener('click', loadLeaderboard); document.addEventListener('keydown', (e) => { if (e.key === '/' && !['INPUT','TEXTAREA'].includes((document.activeElement||{}).tagName)) { e.preventDefault(); $('msg-input').focus(); } }); init().catch((err) => { $('result-area').textContent = String(err); }); });