'use strict'; // ── WebSocket to server ────────────────────────────────────────────────────── let ws; let connected = false; let sessionActive = false; let videoActive = false; 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 || '?'; if (evt === 'onVideoReady' && body.URI) { startVideoFeed(body.URI); } 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'); if (dot) dot.className = ok ? 'ok' : ''; if (lbl) lbl.textContent = label; } // ── Event Log ──────────────────────────────────────────────────────────────── const MAX_LOG = 100; function logEvent(eventName, body, txId) { const log = document.getElementById('event-log'); const now = new Date().toLocaleTimeString(); // Pick color class based on event type let cls = 'evt'; if (eventName.toLowerCase().includes('error')) cls = 'evt-error'; else if (eventName.toLowerCase().includes('play')) cls = 'evt-success'; else if (eventName.toLowerCase().includes('stop')) cls = 'evt-error'; else if (eventName.toLowerCase().includes('selected')) cls = 'evt-entity'; // Extract detail from body let detail = ''; if (body && body.animation) detail = ' "' + body.animation + '"'; else if (body && body.error) detail = ' — ' + body.error; else if (body && body.uri) detail = ' ' + body.uri; const el = document.createElement('div'); el.className = 'log-entry'; el.innerHTML = `${now} ${eventName}${detail}`; if (log) { log.prepend(el); // Trim log while (log.children.length > MAX_LOG) log.removeChild(log.lastChild); } console.log(`[${now}]`, eventName, body); } // ── Video Helpers ─────────────────────────────────────────────────────────── function startVideoFeed(uri) { logEvent('Video feed ready', { uri }, null); } function stopVideoFeed() { logEvent('Video stopped', {}, null); } // ── GoldenLayout Setup ─────────────────────────────────────────────────────── const layoutConfig = { root: { type: 'row', children: [ { type: 'component', componentType: 'library', componentState: { title: 'Animation Library' }, width: 20, title: 'Library' }, { type: 'column', children: [ { type: 'component', componentType: 'canvas', componentState: { title: 'Canvas' }, title: 'Canvas', height: 70 } ], width: 60 }, { type: 'column', children: [ { type: 'stack', activeItemIndex: 0, width: 20, children: [ { type: 'component', componentType: 'inspector', componentState: { title: 'Inspector' }, title: 'Inspector' }, { type: 'component', componentType: 'log', componentState: { title: 'Log' }, title: 'Log' } ] } ] } ] } }; // ── Initialization ────────────────────────────────────────────────────────── function setupComponentRegistry() { const componentRegistry = new GoldenLayout.ComponentRegistry(); componentRegistry.registerComponent('library', (container, state) => { const content = document.querySelector('#tpl-library').content.cloneNode(true); container.element.appendChild(content); const libraryList = container.element.querySelector('#library-list'); const animations = [ 'Eye_Blink_01', 'Eye_Blink_02', 'Eye_Double_Blink', 'eye_happy_01', 'eye_happy_02', 'eye_sad_01', 'eye_scared_00', 'Eye_Curious_01', 'Eye_Look_Center', 'Eye_Look_Left', 'Eye_Look_Right', 'Eye_Look_Up', 'Eye_Look_Down', 'Gesture_Wave', 'Gesture_Nod' ]; animations.forEach(anim => { const item = document.createElement('div'); item.className = 'library-item'; item.textContent = anim; item.addEventListener('click', () => { document.querySelectorAll('.library-item').forEach(i => i.classList.remove('selected')); item.classList.add('selected'); const inspectorInput = document.querySelector('.inspector-field input'); if (inspectorInput) inspectorInput.value = anim; logEvent('Animation selected', { animation: anim }, null); }); libraryList.appendChild(item); }); }); componentRegistry.registerComponent('canvas', (container, state) => { const content = document.querySelector('#tpl-canvas').content.cloneNode(true); container.element.appendChild(content); }); componentRegistry.registerComponent('inspector', (container, state) => { const content = document.querySelector('#tpl-inspector').content.cloneNode(true); container.element.appendChild(content); const applyBtn = container.element.querySelector('.inspector-field button'); if (applyBtn) { applyBtn.addEventListener('click', async () => { const animName = container.element.querySelector('.inspector-field input').value; if (!animName) { alert('Please select an animation'); return; } const inputs = container.element.querySelectorAll('.inspector-field input'); const duration = parseInt(inputs[1]?.value) || 500; const repeat = parseInt(inputs[2]?.value) || 1; const delay = parseInt(inputs[3]?.value) || 0; await post('/api/display/anim', { name: animName, duration, repeat, delay }); logEvent('Applied animation', { animation: animName, duration, repeat, delay }, null); }); } }); componentRegistry.registerComponent('log', (container, state) => { const content = document.querySelector('#tpl-log').content.cloneNode(true); container.element.appendChild(content); }); return componentRegistry; } function setupToolbarButtons() { const btn = (id, fn) => { const el = document.getElementById(id); if (el) el.addEventListener('click', fn); }; btn('btn-new', () => alert('New animation - Coming soon')); btn('btn-open', () => alert('Open animation - Coming soon')); btn('btn-save', () => alert('Save animation - Coming soon')); btn('btn-undo', () => logEvent('Undo', {}, null)); btn('btn-redo', () => logEvent('Redo', {}, null)); btn('btn-play', async () => { const inspectorInput = document.querySelector('.inspector-field input'); const animName = inspectorInput ? inspectorInput.value : 'greeting'; await post('/api/display/anim', { name: animName }); logEvent('Playing animation', { animation: animName }, null); }); btn('btn-pause', () => { logEvent('Pause animation', {}, null); }); btn('btn-stop', () => { post('/api/cancel', {}); logEvent('Stop animation', {}, null); }); btn('btn-timeline', () => alert('Timeline view - Coming soon')); btn('btn-preview', async () => { await post('/api/video/stop', {}); await new Promise(resolve => setTimeout(resolve, 100)); await post('/api/video/start', { duration: 0 }); logEvent('Preview video started', {}, null); }); btn('btn-settings', () => alert('Settings - Coming soon')); btn('btn-back-home', () => { post('/api/video/stop', {}); window.location.href = 'index.html'; }); } // ── Initialization ────────────────────────────────────────────────────────── function waitForGoldenLayout(callback) { if (typeof GoldenLayout === 'undefined') { setTimeout(() => waitForGoldenLayout(callback), 50); } else { callback(); } } document.addEventListener('DOMContentLoaded', () => { waitForGoldenLayout(() => { // Setup components const componentRegistry = setupComponentRegistry(); // Initialize GoldenLayout const layout = new GoldenLayout(layoutConfig, componentRegistry, document.querySelector('#layout-root')); layout.init(); // Setup toolbar setupToolbarButtons(); // Connect to server connectWS(); logEvent('Animator initialized with GoldenLayout', {}, null); }); });