'use strict'; // ── WebSocket to server ────────────────────────────────────────────────────── let ws; let connected = false; let sessionActive = false; let videoActive = false; let telepresenceActive = false; let lastSayTx = null; function connectWS() { const proto = location.protocol === 'https:' ? 'wss' : 'ws'; ws = new WebSocket(`${proto}://${location.host}/ws`); ws.onopen = () => { connected = true; }; ws.onclose = () => { connected = false; setStatus(false, 'Disconnected — reconnecting…'); setTimeout(connectWS, 2000); }; ws.onerror = () => {}; ws.onmessage = (e) => { let msg; try { msg = JSON.parse(e.data); } catch { return; } handleServerMessage(msg); }; } function handleServerMessage(msg) { if (msg.type === 'status') { sessionActive = !!msg.sessionID; setStatus(msg.connected && sessionActive, msg.connected ? (sessionActive ? 'Connected • ' + msg.sessionID.slice(0, 8) + '…' : 'Connecting…') : 'Disconnected'); return; } if (msg.type === 'jiboEvent') { handleJiboEvent(msg.body, msg.txId); } } function handleJiboEvent(body, txId) { if (!body) return; const evt = body.Event || body.ResponseString || '?'; logEvent(evt, body, txId); } // ── REST helpers ───────────────────────────────────────────────────────────── async function api(method, path, body) { try { const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (body) opts.body = JSON.stringify(body); const res = await fetch(path, opts); return await res.json(); } catch (err) { logEvent('api-error: ' + path, { error: err.message }, null); } } const post = (path, body) => api('POST', path, body); const get = (path) => api('GET', path); // ── Status ─────────────────────────────────────────────────────────────────── function setStatus(ok, label) { const dot = document.getElementById('status-dot'); const lbl = document.getElementById('status-label'); dot.className = ok ? 'ok' : ''; lbl.textContent = label; } // ── Telepresence Controls ──────────────────────────────────────────────────── document.getElementById('btn-start-telepresence').addEventListener('click', () => { const userName = document.getElementById('user-name').value.trim(); const status = document.getElementById('presence-status').value; if (!userName) { alert('Please enter your name'); return; } telepresenceActive = true; document.getElementById('btn-start-telepresence').textContent = '🔴 Broadcasting...'; document.getElementById('btn-start-telepresence').disabled = true; document.getElementById('remote-status').innerHTML = `${userName} is now broadcasting (${status})`; logEvent('Telepresence Started', { user: userName, status: status }, null); post('/api/telepresence/start', { userName, status }); }); // ── Head Navigation ────────────────────────────────────────────────────────── const STEP_FRAC = 0.30; const HORIZ_CENTER_Y = 0.38; let moveGen = 0; let moveHeld = false; async function runMoveLoop(dir, gen) { while (moveHeld && gen === moveGen) { let pctX, pctY; if (dir === 'left') { pctX = 0.5 - STEP_FRAC; pctY = HORIZ_CENTER_Y; } else if (dir === 'right') { pctX = 0.5 + STEP_FRAC; pctY = HORIZ_CENTER_Y; } else if (dir === 'up') { pctX = 0.5; pctY = 0.5 - STEP_FRAC; } else { pctX = 0.5; pctY = 0.5 + STEP_FRAC; } const x = Math.round((1 - pctX) * 640); const y = Math.round(pctY * 480); await post('/api/look/step', { x, y }); } } function startMove(dir) { moveHeld = true; runMoveLoop(dir, ++moveGen); } function stopMove() { moveHeld = false; } function addMoveButton(id, dir) { const btn = document.getElementById(id); if (!btn) return; btn.addEventListener('pointerdown', (e) => { e.preventDefault(); startMove(dir); }); btn.addEventListener('pointerup', stopMove); btn.addEventListener('pointercancel', stopMove); btn.addEventListener('pointerleave', stopMove); } addMoveButton('btn-up', 'up'); addMoveButton('btn-down', 'down'); addMoveButton('btn-left', 'left'); addMoveButton('btn-right', 'right'); // ── Say ─────────────────────────────────────────────────────────────────────── document.getElementById('btn-say').addEventListener('click', async () => { const text = document.getElementById('say-text').value.trim(); if (!text) return; const r = await post('/api/say', { text }); if (r) lastSayTx = r.txId; }); document.getElementById('btn-say-cancel').addEventListener('click', () => { if (lastSayTx) post('/api/cancel', { txId: lastSayTx }); }); // ── Camera / Video ──────────────────────────────────────────────────────────── let videoTxId = null; document.getElementById('btn-video-start').addEventListener('click', async () => { const r = await post('/api/video/start', { duration: 0 }); if (r) videoTxId = r.txId; }); document.getElementById('btn-video-stop').addEventListener('click', () => { post('/api/video/stop', {}); stopVideoFeed(); }); function startVideoFeed(uri) { const feed = document.getElementById('camera-feed'); const noFeed = document.getElementById('camera-no-feed'); feed.src = '/proxy/stream?uri=' + encodeURIComponent(uri); feed.style.display = 'block'; noFeed.style.display = 'none'; videoActive = true; } function stopVideoFeed() { const feed = document.getElementById('camera-feed'); feed.src = ''; feed.style.display = 'none'; document.getElementById('camera-no-feed').style.display = ''; videoActive = false; } // ── Volume Control ──────────────────────────────────────────────────────────── document.getElementById('speaker-volume').addEventListener('input', function () { document.getElementById('volume-label').textContent = Math.round(this.value * 100) + '%'; }); // ── Tabs ────────────────────────────────────────────────────────────────────── document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', function () { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); this.classList.add('active'); document.getElementById(this.dataset.tab).classList.add('active'); }); }); // ── Event log ───────────────────────────────────────────────────────────────── const MAX_LOG = 200; function logEvent(eventName, body, txId) { const log = document.getElementById('event-log'); const now = new Date().toLocaleTimeString(); let cls = 'evt'; if (eventName.toLowerCase().includes('error')) cls = 'evt-error'; let detail = ''; if (body.user) detail = ' [' + body.user + ']'; const el = document.createElement('div'); el.className = 'log-entry'; el.innerHTML = `${now} ${eventName}${detail}`; log.prepend(el); while (log.children.length > MAX_LOG) log.removeChild(log.lastChild); } document.getElementById('btn-clear-log').addEventListener('click', () => { document.getElementById('event-log').innerHTML = ''; }); // ── Back to home ────────────────────────────────────────────────────────────── document.getElementById('btn-back-home')?.addEventListener('click', () => { window.location.href = 'index.html'; }); // ── Initialization ──────────────────────────────────────────────────────────── connectWS();