diff --git a/public/animator.html b/public/animator.html new file mode 100644 index 0000000..cf3242d --- /dev/null +++ b/public/animator.html @@ -0,0 +1,145 @@ + + + + + + Re-Commander — Animator Mode + + + + +
+
+

Re-Commander — Animator

+ Connecting… + + +
+ +
+ + +
+ + +
+
Animation Library
+
+ + +
+ +
+ + +
+
Animation Sequence
+
+ +
+
+ + +
+
+ + +
+
Timeline
+
+ + +
+
+ + +
+
+ +
+ + +
+ + +
+ +
+
📹
+
Animation Preview
+
Start video to see animations being played
+
+
+
+ + +
+ + +
+ +
+ + +
+
+ + +
+ + +
+
Video Stream
+
+ + +
+ +
+
+ + +
+
+ Animation Log + +
+
+
+ +
+
+ + +
+
+ Photo +
+ + + + diff --git a/public/animator.js b/public/animator.js new file mode 100644 index 0000000..b21bfbe --- /dev/null +++ b/public/animator.js @@ -0,0 +1,220 @@ +'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 || '?'; + 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; +} + +// ── Animation Control ──────────────────────────────────────────────────────── + +document.getElementById('btn-play-anim').addEventListener('click', () => { + const name = document.getElementById('anim-select').value; + if (name) { + post('/api/display/anim', { name }); + logEvent('Animation Played', { animation: name }, null); + } +}); + +document.getElementById('btn-play-sequence').addEventListener('click', () => { + const sequence = document.getElementById('sequence-list').value + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0); + + if (sequence.length === 0) { + alert('Please enter at least one animation'); + return; + } + + const repeatCount = parseInt(document.getElementById('repeat-count').value) || 1; + const delay = parseInt(document.getElementById('anim-delay').value) || 500; + + post('/api/display/anim-sequence', { sequence, repeatCount, delay }); + logEvent('Sequence Started', { count: sequence.length, repeats: repeatCount }, null); +}); + +document.getElementById('btn-stop-sequence').addEventListener('click', () => { + post('/api/cancel', {}); + logEvent('Sequence Stopped', {}, null); +}); + +// ── 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('video-status').textContent = 'Waiting for VideoReady…'; +}); + +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; +} + +// ── Take Photo ──────────────────────────────────────────────────────────────── + +document.getElementById('btn-photo').addEventListener('click', () => { + post('/api/photo', { + camera: 'right', + resolution: document.getElementById('photo-res').value + }); +}); + +function addPhoto(url) { + const strip = document.getElementById('photo-strip'); + const img = document.createElement('img'); + img.src = url; + img.title = url; + img.addEventListener('click', () => openPhotoModal(img.src)); + strip.prepend(img); +} + +// ── Photo modal ─────────────────────────────────────────────────────────────── + +function openPhotoModal(src) { + document.getElementById('photo-modal-img').src = src; + document.getElementById('photo-modal').classList.add('open'); +} + +document.getElementById('photo-modal').addEventListener('click', (e) => { + if (e.target === e.currentTarget || e.target.id === 'photo-modal-close') { + document.getElementById('photo-modal').classList.remove('open'); + } +}); + +// ── 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.animation) detail = ' ' + body.animation; + + 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(); diff --git a/public/app.js b/public/app.js index b104787..a0b5f0a 100644 --- a/public/app.js +++ b/public/app.js @@ -566,9 +566,26 @@ function flashHotword(utterance, score) { hotwordTimer = setTimeout(() => el.classList.remove('active'), 3000); } -// ── Init ────────────────────────────────────────────────────────────────────── +// ── Menu: Robot & Mode Selection ──────────────────────────────────────────── + +let selectedRobot = null; +let selectedMode = 'commander'; + +// Get the selected robot and mode from sessionStorage +function initializeMode() { + selectedRobot = sessionStorage.getItem('selectedRobot') || 'jibo-001'; + selectedMode = sessionStorage.getItem('selectedMode') || 'commander'; +} + +// Back to home button +document.getElementById('btn-back-home')?.addEventListener('click', () => { + window.location.href = 'index.html'; +}); + +// ── Initialization ─────────────────────────────────────────────────────────── connectWS(); +initializeMode(); // Populate LLM fields from server config (.env defaults) get('/api/config').then(cfg => { diff --git a/public/commander.html b/public/commander.html new file mode 100644 index 0000000..dc0c58a --- /dev/null +++ b/public/commander.html @@ -0,0 +1,286 @@ + + + + + + Re-Commander — Commander Mode + + + + +
+
+

Re-Commander

+ Connecting… + + +
+ +
+ + +
+ + +
+
Head Navigation
+
+
+
+ +
+ +
+ +
+ +
+
+
+
+ + +
+
Say
+
+ +
+
+ + +
+
+ + +
+
Listen
+
+ + + +
+
+ + + +
+
+ + +
+
(result appears here)
+
+ + +
+
+ + +
+
Voice AI
+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
Attention Mode
+
+ + + + + + + + +
+
+ + +
+
Volume
+
+ + 75% +
+ +
+ +
+ + +
+ + +
+ +
+
📷
+
No camera feed
+
Use the Camera tab to start video or take a photo
+
+
+
+ + +
+
+
Click on feed → look there
+
+ Track: + + +
+
+
+ + +
+
Photo Strip
+
+
+ +
+ + +
+
+ + + + +
+ + +
+
Video Stream
+
+ + +
+ +
Take Photo
+
+ + +
+
+ + +
+ + +
Subscriptions
+
+ + + +
+ +
+ + +
+
Eye
+ + +
Play Animation
+
+ +
+ + +
Display Text
+
+ +
+ + +
Display Image
+
+ +
+ + +
+ + +
+
Detected Entities
+
Waiting for entity events…
+
Head Touch
+
+
FL
+
ML
+
BL
+
FR
+
MR
+
BR
+
+ +
+ + +
+
+ Event Log + +
+
+
+ +
+
+ + +
+
+ Photo +
+ + + + diff --git a/public/home.js b/public/home.js new file mode 100644 index 0000000..5643b88 --- /dev/null +++ b/public/home.js @@ -0,0 +1,215 @@ +'use strict'; + +// ── WebSocket Setup ────────────────────────────────────────────────────────── + +let ws; +let connected = false; + +function connectWS() { + const proto = location.protocol === 'https:' ? 'wss' : 'ws'; + ws = new WebSocket(`${proto}://${location.host}/ws`); + + ws.onopen = () => { + connected = true; + updateStatus(); + }; + + ws.onclose = () => { + connected = false; + updateStatus(); + 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') { + updateStatus(); + } +} + +function updateStatus() { + const dot = document.getElementById('status-dot'); + const lbl = document.getElementById('status-label'); + if (connected) { + dot.className = 'ok'; + lbl.textContent = 'Ready'; + } else { + dot.className = ''; + lbl.textContent = 'Connecting…'; + } +} + +// ── Connection Management ──────────────────────────────────────────────────── + +let selectedRobot = null; +let selectedMode = null; +let connections = []; + +const STORAGE_KEY = 're-commander-connections'; + +// Load connections from localStorage +function loadConnections() { + const stored = localStorage.getItem(STORAGE_KEY); + connections = stored ? JSON.parse(stored) : []; + renderConnections(); +} + +// Save connections to localStorage +function saveConnections() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(connections)); +} + +// Render connections list +function renderConnections() { + const list = document.getElementById('connections-list'); + + if (connections.length === 0) { + list.innerHTML = '
No saved connections
'; + return; + } + + list.innerHTML = connections.map((conn, idx) => ` +
+
+
${conn.name}
+
${conn.ip}
+
+ +
+ `).join(''); + + // Add click handlers to select connection + document.querySelectorAll('.connection-item').forEach(item => { + const idx = parseInt(item.dataset.index); + item.addEventListener('click', (e) => { + if (!e.target.classList.contains('connection-delete')) { + selectConnection(idx); + } + }); + }); + + // Add click handlers to delete button + document.querySelectorAll('.connection-delete').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const idx = parseInt(btn.dataset.index); + deleteConnection(idx); + }); + }); + + // Highlight selected connection + if (selectedRobot !== null && selectedRobot < connections.length) { + document.querySelector(`.connection-item[data-index="${selectedRobot}"]`)?.classList.add('selected'); + } +} + +function selectConnection(idx) { + selectedRobot = idx; + const conn = connections[idx]; + sessionStorage.setItem('selectedRobot', conn.ip); + sessionStorage.setItem('selectedRobotName', conn.name); + renderConnections(); + updateLaunchButton(); +} + +function deleteConnection(idx) { + connections.splice(idx, 1); + saveConnections(); + if (selectedRobot === idx) { + selectedRobot = null; + sessionStorage.removeItem('selectedRobot'); + sessionStorage.removeItem('selectedRobotName'); + } + renderConnections(); + updateLaunchButton(); +} + +// Add new connection +document.getElementById('btn-add-connection').addEventListener('click', () => { + const ip = document.getElementById('robot-ip').value.trim(); + + if (!ip) { + alert('Please enter a robot IP or hostname'); + return; + } + + // Check if already exists + if (connections.find(c => c.ip === ip)) { + alert('This connection already exists'); + return; + } + + // Generate a name + const name = `Robot (${ip})`; + connections.push({ name, ip }); + saveConnections(); + renderConnections(); + + // Clear input and select the new connection + document.getElementById('robot-ip').value = ''; + selectConnection(connections.length - 1); +}); + +// Handle Enter key in IP input +document.getElementById('robot-ip').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + document.getElementById('btn-add-connection').click(); + } +}); + +// ── Mode Selection ─────────────────────────────────────────────────────────── + +document.querySelectorAll('.mode-btn').forEach(btn => { + btn.addEventListener('click', function () { + const mode = this.dataset.mode; + selectedMode = mode; + + // Remove active class from all mode buttons + document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active')); + this.classList.add('active'); + + updateLaunchButton(); + }); +}); + +function updateLaunchButton() { + const btn = document.getElementById('btn-launch'); + if (selectedRobot !== null && selectedMode) { + btn.disabled = false; + } else { + btn.disabled = true; + } +} + +// ── Launch ─────────────────────────────────────────────────────────────────── + +document.getElementById('btn-launch').addEventListener('click', () => { + if (selectedRobot === null || !selectedMode) { + alert('Please select both a robot and a mode'); + return; + } + + const conn = connections[selectedRobot]; + + // Store selection in sessionStorage + sessionStorage.setItem('selectedRobot', conn.ip); + sessionStorage.setItem('selectedRobotName', conn.name); + sessionStorage.setItem('selectedMode', selectedMode); + + // Navigate to the appropriate page + const modeFile = selectedMode + '.html'; + window.location.href = modeFile; +}); + +// ── Initialization ─────────────────────────────────────────────────────────── + +connectWS(); +loadConnections(); diff --git a/public/index.html b/public/index.html index 67db010..59f1294 100644 --- a/public/index.html +++ b/public/index.html @@ -3,8 +3,220 @@ - Re-Commander — Jibo + Re-Commander — Robot Selection + @@ -12,581 +224,52 @@

Re-Commander

Connecting… - -
- - -
- - -
-
Head Navigation
-
-
-
- -
- -
- -
- -
-
+
+
+
Re-Commander
+
Select your robot and operating mode
+ + +
+ +
+
No saved connections
+
+ + +
+ +
- - -
-
Say
-
- -
-
- - + + +
+ +
+ + +
- - -
-
Listen
-
- - - -
-
- - - -
-
- - -
-
(result appears here)
-
- - -
-
- - -
-
Voice AI
-
- - - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
Attention Mode
-
- - - - - - - - -
-
- - -
-
Volume
-
- - 75% -
- -
- -
- - -
- - -
- -
-
📷
-
No camera feed
-
Use the Camera tab to start video or take a photo
-
-
-
- - -
-
-
Click on feed → look there
-
- Track: - - -
-
-
- - -
-
Photo Strip
-
-
- -
- - -
-
- - - - -
- - -
-
Video Stream
-
- - -
- -
Take Photo
-
- - -
-
- - -
- - -
Subscriptions
-
- - - -
- -
- - -
-
Eye
- - -
Play Animation
-
- -
- - -
Display Text
-
- -
- - -
Display Image
-
- -
- - -
- - -
-
Detected Entities
-
Waiting for entity events…
-
Head Touch
-
-
FL
-
ML
-
BL
-
FR
-
MR
-
BR
-
- -
- - -
-
- Event Log - -
-
-
- -
+ + + +
- -
-
- Photo -
+ - diff --git a/public/style.css b/public/style.css index e169858..c3c8aa9 100644 --- a/public/style.css +++ b/public/style.css @@ -374,3 +374,71 @@ label { font-size: 12px; color: var(--muted); display: block; margin-bottom: 3px /* ── Video area ── */ .video-controls { display: flex; gap: 6px; align-items: center; } #video-status { font-size: 12px; color: var(--muted); } + +/* ── Menu Section (Robot & Mode) ── */ +.menu-section { + background: linear-gradient(135deg, var(--surface2), var(--surface)); + border-bottom: 2px solid var(--accent); +} + +#robot-select { + width: 100%; + padding: 7px 8px; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 5px; + color: var(--text); + font-size: 13px; + cursor: pointer; +} +#robot-select:hover { border-color: var(--accent); } +#robot-select:focus { border-color: var(--accent); background: var(--surface); } + +/* ── Mode Buttons ── */ +.mode-buttons { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 8px; +} + +.mode-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 5px; + color: var(--text); + cursor: pointer; + font-size: 13px; + transition: background 0.15s, border-color 0.15s, transform 0.1s; + text-align: left; +} + +.mode-btn:hover { + background: var(--accent2); + border-color: var(--accent); + transform: translateX(2px); +} + +.mode-btn.active { + background: var(--accent); + border-color: var(--accent); + color: #000; + font-weight: 600; + box-shadow: 0 0 10px rgba(0, 212, 255, 0.3); +} + +.mode-icon { + font-size: 18px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.mode-label { + flex: 1; +} diff --git a/public/telepresence.html b/public/telepresence.html new file mode 100644 index 0000000..64e6c62 --- /dev/null +++ b/public/telepresence.html @@ -0,0 +1,143 @@ + + + + + + Re-Commander — Telepresence Mode + + + + +
+
+

Re-Commander — Telepresence

+ Connecting… + + +
+ +
+ + +
+ + +
+
Presence Controls
+
+ + +
+
+ + +
+ +
+ + +
+
Head Navigation
+
+
+
+ +
+ +
+ +
+ +
+
+
+
+ + +
+
Say
+
+ +
+
+ + +
+
+ + +
+
Audio
+
+ + +
+
+ + + 75% +
+
+ +
+ + +
+ + +
+ +
+
📹
+
Telepresence View
+
Start broadcasting to see live camera feed
+
+
+
+ + +
+
+ Waiting to start telepresence... +
+ + +
+ +
+ + +
+
+ + +
+ + +
+
Remote Users
+
+
No remote users connected
+
+
+ + +
+
+ Session Log + +
+
+
+ +
+
+ + + + diff --git a/public/telepresence.js b/public/telepresence.js new file mode 100644 index 0000000..4e75fee --- /dev/null +++ b/public/telepresence.js @@ -0,0 +1,242 @@ +'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(); diff --git a/server.js b/server.js index b6236cd..3f916aa 100644 --- a/server.js +++ b/server.js @@ -11,7 +11,7 @@ const fs = require('fs'); require('dotenv').config(); -const JIBO_HOST = '192.168.1.217'; +const JIBO_HOST = '192.168.1.10'; const JIBO_PORT = 8160; const APP_PORT = process.env.PORT || 3000;