Robot Ip input & cache
This commit is contained in:
145
public/animator.html
Normal file
145
public/animator.html
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Re-Commander — Animator Mode</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div id="status-dot"></div>
|
||||||
|
<h1>Re<span>-Commander</span> — Animator</h1>
|
||||||
|
<span id="status-label">Connecting…</span>
|
||||||
|
<span id="hotword-indicator"></span>
|
||||||
|
<button id="btn-back-home" style="margin-left: auto; padding: 5px 12px; font-size: 12px;">← Back to Home</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
|
||||||
|
<!-- ── LEFT PANEL ── -->
|
||||||
|
<div class="left-panel">
|
||||||
|
|
||||||
|
<!-- Animation Presets -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Animation Library</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Select Animation</label>
|
||||||
|
<select id="anim-select">
|
||||||
|
<optgroup label="── Blinks">
|
||||||
|
<option>Eye_Blink_01</option>
|
||||||
|
<option>Eye_Blink_02</option>
|
||||||
|
<option>Eye_Double_Blink_01</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="── Expressions">
|
||||||
|
<option>eye_happy_00</option>
|
||||||
|
<option>eye_happy_01</option>
|
||||||
|
<option>eye_sad_01</option>
|
||||||
|
<option>eye_scared_00</option>
|
||||||
|
<option>Eye_Curious_01</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="── Poses">
|
||||||
|
<option>Eye_Look_Center_Middle_01</option>
|
||||||
|
<option>Eye_Look_Left_Middle_01</option>
|
||||||
|
<option>Eye_Look_Right_Middle_01</option>
|
||||||
|
<option>Eye_Look_Center_Up_01</option>
|
||||||
|
<option>Eye_Look_Center_Down_01</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button id="btn-play-anim" style="width:100%;">▶ Play Animation</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Animation Sequence -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Animation Sequence</div>
|
||||||
|
<div class="field">
|
||||||
|
<textarea id="sequence-list" rows="6" placeholder="Animation1 Animation2 Animation3" style="font-family: monospace; font-size: 12px;"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="controls-row">
|
||||||
|
<button id="btn-play-sequence" style="flex:1;">▶ Play Sequence</button>
|
||||||
|
<button id="btn-stop-sequence" class="danger" style="flex:1;">■ Stop</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timeline -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Timeline</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Repeat Count</label>
|
||||||
|
<input type="number" id="repeat-count" value="1" min="1">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Delay (ms)</label>
|
||||||
|
<input type="number" id="anim-delay" value="500" min="0" step="100">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /left-panel -->
|
||||||
|
|
||||||
|
<!-- ── CENTER PANEL ── -->
|
||||||
|
<div class="center-panel">
|
||||||
|
|
||||||
|
<!-- Camera feed -->
|
||||||
|
<div id="camera-wrap">
|
||||||
|
<img id="camera-feed" alt="Jibo camera" style="display:none;">
|
||||||
|
<div id="camera-no-feed">
|
||||||
|
<div style="font-size:40px;">📹</div>
|
||||||
|
<div>Animation Preview</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted);">Start video to see animations being played</div>
|
||||||
|
</div>
|
||||||
|
<div id="click-dot"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Playback Controls -->
|
||||||
|
<div class="controls-row" style="justify-content:center; gap:10px; margin-top:20px;">
|
||||||
|
<button id="btn-video-start" style="flex-basis: auto;">▶ Start Video</button>
|
||||||
|
<button id="btn-video-stop" class="danger" style="flex-basis: auto;">■ Stop</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /center-panel -->
|
||||||
|
|
||||||
|
<!-- ── RIGHT PANEL ── -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" data-tab="tab-camera">Camera</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-log">Log</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Camera tab -->
|
||||||
|
<div class="tab-panel active" id="tab-camera">
|
||||||
|
<div class="section-title">Video Stream</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Resolution</label>
|
||||||
|
<select id="photo-res">
|
||||||
|
<option value="highRes">HighRes</option>
|
||||||
|
<option value="medRes">MedRes</option>
|
||||||
|
<option value="lowRes">LowRes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button id="btn-photo" style="width:100%;margin-bottom:10px;">📷 Take Photo</button>
|
||||||
|
<div id="photo-strip"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log tab -->
|
||||||
|
<div class="tab-panel" id="tab-log" style="padding:0;display:flex;flex-direction:column;flex:1;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);">
|
||||||
|
<span class="section-title" style="margin:0;">Animation Log</span>
|
||||||
|
<button id="btn-clear-log" style="font-size:11px;padding:3px 8px;">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div id="event-log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /right-panel -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Photo modal -->
|
||||||
|
<div id="photo-modal">
|
||||||
|
<div id="photo-modal-close">✕</div>
|
||||||
|
<img id="photo-modal-img" src="" alt="Photo">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="animator.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
220
public/animator.js
Normal file
220
public/animator.js
Normal file
@@ -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 = `<span class="ts">${now}</span> <span class="${cls}">${eventName}</span>${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();
|
||||||
@@ -566,9 +566,26 @@ function flashHotword(utterance, score) {
|
|||||||
hotwordTimer = setTimeout(() => el.classList.remove('active'), 3000);
|
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();
|
connectWS();
|
||||||
|
initializeMode();
|
||||||
|
|
||||||
// Populate LLM fields from server config (.env defaults)
|
// Populate LLM fields from server config (.env defaults)
|
||||||
get('/api/config').then(cfg => {
|
get('/api/config').then(cfg => {
|
||||||
|
|||||||
286
public/commander.html
Normal file
286
public/commander.html
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Re-Commander — Commander Mode</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div id="status-dot"></div>
|
||||||
|
<h1>Re<span>-Commander</span></h1>
|
||||||
|
<span id="status-label">Connecting…</span>
|
||||||
|
<span id="hotword-indicator"></span>
|
||||||
|
<button id="btn-back-home" style="margin-left: auto; padding: 5px 12px; font-size: 12px;">← Back to Home</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
|
||||||
|
<!-- ── LEFT PANEL ── -->
|
||||||
|
<div class="left-panel">
|
||||||
|
|
||||||
|
<!-- Look / Navigation -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Head Navigation</div>
|
||||||
|
<div style="display:flex; gap:16px; align-items:center; margin-bottom:8px;">
|
||||||
|
<div class="arrow-pad">
|
||||||
|
<div></div>
|
||||||
|
<button id="btn-up" title="Look up (↑)">UP</button>
|
||||||
|
<div></div>
|
||||||
|
<button id="btn-left" title="Look left (←)">←</button>
|
||||||
|
<div></div>
|
||||||
|
<button id="btn-right" title="Look right (→)">→</button>
|
||||||
|
<div></div>
|
||||||
|
<button id="btn-down" title="Look down (↓)">↓</button>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Say -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Say</div>
|
||||||
|
<div class="field">
|
||||||
|
<textarea id="say-text" rows="2" placeholder="Hello! I am Jibo."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="controls-row">
|
||||||
|
<button id="btn-say">▶ Say</button>
|
||||||
|
<button id="btn-say-cancel" class="danger">✕ Stop</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Listen -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Listen</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Max speech</label>
|
||||||
|
<input type="number" id="listen-max-speech" value="10000" step="1000">
|
||||||
|
<label>ms</label>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>No-speech TO</label>
|
||||||
|
<input type="number" id="listen-max-nosp" value="5000" step="1000">
|
||||||
|
<label>ms</label>
|
||||||
|
</div>
|
||||||
|
<div class="controls-row" style="margin-bottom:8px;">
|
||||||
|
<button id="btn-listen">🎙 Listen</button>
|
||||||
|
<button id="btn-listen-cancel" class="danger">✕ Cancel</button>
|
||||||
|
</div>
|
||||||
|
<div id="listen-result">(result appears here)</div>
|
||||||
|
<div style="margin-top:8px;display:flex;align-items:center;gap:6px;">
|
||||||
|
<input type="checkbox" id="auto-listen-toggle" style="width:auto;">
|
||||||
|
<label for="auto-listen-toggle" style="margin:0;cursor:pointer;">Auto-listen on hotword</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Voice AI -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Voice AI</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
||||||
|
<input type="checkbox" id="llm-toggle" style="width:auto;">
|
||||||
|
<label for="llm-toggle" style="margin:0;cursor:pointer;">LLM voice loop</label>
|
||||||
|
<button id="btn-llm-clear" style="margin-left:auto;font-size:11px;padding:2px 8px;" title="Clear conversation history">↺ Clear</button>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Completions endpoint</label>
|
||||||
|
<input type="text" id="llm-endpoint" placeholder="http://localhost:11434/v1/chat/completions">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Model</label>
|
||||||
|
<input type="text" id="llm-model" placeholder="llama3">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>System prompt</label>
|
||||||
|
<textarea id="llm-system-prompt" rows="3" placeholder="You are Jibo, a friendly social robot. Keep responses brief and conversational."></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="llm-status" style="font-size:11px;color:var(--muted);font-style:italic;min-height:16px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Attention -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Attention Mode</div>
|
||||||
|
<div class="attention-grid">
|
||||||
|
<button class="attn-btn" data-mode="OFF">Off</button>
|
||||||
|
<button class="attn-btn" data-mode="IDLE">Idle</button>
|
||||||
|
<button class="attn-btn" data-mode="DISENGAGE">Disengage</button>
|
||||||
|
<button class="attn-btn" data-mode="ENGAGED">Engaged</button>
|
||||||
|
<button class="attn-btn" data-mode="SPEAKING">Speaking</button>
|
||||||
|
<button class="attn-btn" data-mode="FIXATED">Fixated</button>
|
||||||
|
<button class="attn-btn" data-mode="ATTRACTABLE">Attractable</button>
|
||||||
|
<button class="attn-btn" data-mode="COMMAND">Command</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Volume -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Volume</div>
|
||||||
|
<div class="row">
|
||||||
|
<input type="range" id="volume-slider" min="0" max="1" step="0.05" value="0.75">
|
||||||
|
<span id="volume-label" style="min-width:32px;text-align:right;font-size:12px;">75%</span>
|
||||||
|
</div>
|
||||||
|
<button id="btn-set-volume">Set Volume</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /left-panel -->
|
||||||
|
|
||||||
|
<!-- ── CENTER PANEL ── -->
|
||||||
|
<div class="center-panel">
|
||||||
|
|
||||||
|
<!-- Camera feed -->
|
||||||
|
<div id="camera-wrap">
|
||||||
|
<img id="camera-feed" alt="Jibo camera" style="display:none;">
|
||||||
|
<div id="camera-no-feed">
|
||||||
|
<div style="font-size:40px;">📷</div>
|
||||||
|
<div>No camera feed</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted);">Use the Camera tab to start video or take a photo</div>
|
||||||
|
</div>
|
||||||
|
<div id="click-dot"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Arrow pad (compact, centered under feed) -->
|
||||||
|
<div class="controls-row" style="justify-content:center; gap:10px;">
|
||||||
|
<div style="display:flex;flex-direction:column;align-items:center;gap:4px;">
|
||||||
|
<div style="font-size:11px;color:var(--muted);">Click on feed → look there</div>
|
||||||
|
<div style="display:flex;gap:6px;align-items:center;">
|
||||||
|
<span style="font-size:12px;color:var(--muted);">Track:</span>
|
||||||
|
<input type="checkbox" id="track-flag" style="width:auto;">
|
||||||
|
<span id="video-status"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Photos -->
|
||||||
|
<div style="width:100%;max-width:640px;">
|
||||||
|
<div class="section-title" style="margin-bottom:6px;">Photo Strip</div>
|
||||||
|
<div id="photo-strip"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /center-panel -->
|
||||||
|
|
||||||
|
<!-- ── RIGHT PANEL ── -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" data-tab="tab-camera">Camera</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-display">Display</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-entities">Entities</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-log">Log</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Camera tab -->
|
||||||
|
<div class="tab-panel active" id="tab-camera">
|
||||||
|
<div class="section-title">Video Stream</div>
|
||||||
|
<div class="video-controls" style="margin-bottom:8px;">
|
||||||
|
<button id="btn-video-start">▶ Start Video</button>
|
||||||
|
<button id="btn-video-stop" class="danger">■ Stop</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title" style="margin-top:10px;">Take Photo</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Camera</label>
|
||||||
|
<select id="photo-camera">
|
||||||
|
<option value="right">Right</option>
|
||||||
|
<option value="left">Left</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Resolution</label>
|
||||||
|
<select id="photo-res">
|
||||||
|
<option value="highRes">HighRes</option>
|
||||||
|
<option value="medRes">MedRes</option>
|
||||||
|
<option value="lowRes">LowRes</option>
|
||||||
|
<option value="microRes">MicroRes</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button id="btn-photo" style="width:100%;margin-bottom:10px;">📷 Take Photo</button>
|
||||||
|
|
||||||
|
<div class="section-title" style="margin-top:10px;">Subscriptions</div>
|
||||||
|
<div class="controls-row">
|
||||||
|
<button id="btn-sub-entity" class="active">👤 Entity</button>
|
||||||
|
<button id="btn-sub-motion" class="active">〰 Motion</button>
|
||||||
|
<button id="btn-sub-headtouch" class="active">✋ Touch</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Display tab -->
|
||||||
|
<div class="tab-panel" id="tab-display">
|
||||||
|
<div class="section-title">Eye</div>
|
||||||
|
<button id="btn-eye" style="width:100%;margin-bottom:10px;">👁 Show Eye</button>
|
||||||
|
|
||||||
|
<div class="section-title">Play Animation</div>
|
||||||
|
<div class="field">
|
||||||
|
<select id="eye-anim-select">
|
||||||
|
<optgroup label="── Blinks">
|
||||||
|
<option>Eye_Blink_01</option>
|
||||||
|
<option>Eye_Blink_02</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="── Expressions">
|
||||||
|
<option>eye_happy_00</option>
|
||||||
|
<option>eye_happy_01</option>
|
||||||
|
<option>eye_sad_01</option>
|
||||||
|
<option>eye_scared_00</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button id="btn-play-anim" style="width:100%;margin-bottom:10px;">▶ Play Animation</button>
|
||||||
|
|
||||||
|
<div class="section-title" style="margin-top:4px;">Display Text</div>
|
||||||
|
<div class="field">
|
||||||
|
<textarea id="display-text" rows="2" placeholder="Text to show on screen…"></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="btn-display-text" style="width:100%;margin-bottom:10px;">Show Text</button>
|
||||||
|
|
||||||
|
<div class="section-title">Display Image</div>
|
||||||
|
<div class="field">
|
||||||
|
<input type="text" id="display-img-src" placeholder="http://… image URL">
|
||||||
|
</div>
|
||||||
|
<button id="btn-display-image" style="width:100%;">Show Image</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Entities tab -->
|
||||||
|
<div class="tab-panel" id="tab-entities">
|
||||||
|
<div class="section-title">Detected Entities</div>
|
||||||
|
<div id="entity-list"><div style="color:var(--muted);font-size:12px;">Waiting for entity events…</div></div>
|
||||||
|
<div class="section-title" style="margin-top:12px;">Head Touch</div>
|
||||||
|
<div id="head-touch-display" style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px;">
|
||||||
|
<div class="pad-indicator" data-pad="frontLeft">FL</div>
|
||||||
|
<div class="pad-indicator" data-pad="middleLeft">ML</div>
|
||||||
|
<div class="pad-indicator" data-pad="backLeft">BL</div>
|
||||||
|
<div class="pad-indicator" data-pad="frontRight">FR</div>
|
||||||
|
<div class="pad-indicator" data-pad="middleRight">MR</div>
|
||||||
|
<div class="pad-indicator" data-pad="backRight">BR</div>
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.pad-indicator {
|
||||||
|
background: var(--surface2); border: 1px solid var(--border);
|
||||||
|
border-radius: 4px; padding: 6px; text-align: center;
|
||||||
|
font-size: 11px; color: var(--muted); transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.pad-indicator.active { background: var(--accent); color: #000; border-color: var(--accent); }
|
||||||
|
</style>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log tab -->
|
||||||
|
<div class="tab-panel" id="tab-log" style="padding:0;display:flex;flex-direction:column;flex:1;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);">
|
||||||
|
<span class="section-title" style="margin:0;">Event Log</span>
|
||||||
|
<button id="btn-clear-log" style="font-size:11px;padding:3px 8px;">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div id="event-log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /right-panel -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Photo modal -->
|
||||||
|
<div id="photo-modal">
|
||||||
|
<div id="photo-modal-close">✕</div>
|
||||||
|
<img id="photo-modal-img" src="" alt="Photo">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
215
public/home.js
Normal file
215
public/home.js
Normal file
@@ -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 = '<div style="color:var(--muted);font-size:12px;text-align:center;padding:20px 0;">No saved connections</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = connections.map((conn, idx) => `
|
||||||
|
<div class="connection-item" data-index="${idx}">
|
||||||
|
<div class="connection-info">
|
||||||
|
<div class="connection-name">${conn.name}</div>
|
||||||
|
<div class="connection-ip">${conn.ip}</div>
|
||||||
|
</div>
|
||||||
|
<button class="connection-delete" data-index="${idx}">Delete</button>
|
||||||
|
</div>
|
||||||
|
`).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();
|
||||||
@@ -3,8 +3,220 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Re-Commander — Jibo</title>
|
<title>Re-Commander — Robot Selection</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<style>
|
||||||
|
body { display: flex; flex-direction: column; }
|
||||||
|
.home-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.home-content {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 2px solid var(--accent);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 550px;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
.home-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.home-title span {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.home-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.selection-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.selection-label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Previous Connections List */
|
||||||
|
.connections-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.connection-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.connection-item:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.connection-item.selected {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: #000;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.connection-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.connection-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.connection-ip {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.connection-item.selected .connection-ip {
|
||||||
|
color: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
.connection-delete {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: var(--danger);
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 8px;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.connection-delete:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* New Connection Input */
|
||||||
|
.new-connection {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.new-connection input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.new-connection input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
#btn-add-connection {
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: var(--accent);
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #000;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
#btn-add-connection:hover {
|
||||||
|
box-shadow: 0 0 12px rgba(0, 212, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mode Selection */
|
||||||
|
.mode-buttons-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.mode-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.mode-btn:hover {
|
||||||
|
background: var(--accent2);
|
||||||
|
border-color: var(--accent);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
.mode-btn.active {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: #000;
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 0 16px rgba(0, 212, 255, 0.4);
|
||||||
|
}
|
||||||
|
.mode-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.mode-label {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn-launch {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin-top: 28px;
|
||||||
|
background: var(--accent);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #000;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
#btn-launch:hover:not(:disabled) {
|
||||||
|
box-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
#btn-launch:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -12,581 +224,52 @@
|
|||||||
<div id="status-dot"></div>
|
<div id="status-dot"></div>
|
||||||
<h1>Re<span>-Commander</span></h1>
|
<h1>Re<span>-Commander</span></h1>
|
||||||
<span id="status-label">Connecting…</span>
|
<span id="status-label">Connecting…</span>
|
||||||
<span id="hotword-indicator"></span>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="main">
|
<div class="home-container">
|
||||||
|
<div class="home-content">
|
||||||
<!-- ── LEFT PANEL ── -->
|
<div class="home-title">Re<span>-Commander</span></div>
|
||||||
<div class="left-panel">
|
<div class="home-subtitle">Select your robot and operating mode</div>
|
||||||
|
|
||||||
<!-- Look / Navigation -->
|
<!-- Robot Selection -->
|
||||||
<div class="section">
|
<div class="selection-group">
|
||||||
<div class="section-title">Head Navigation</div>
|
<label class="selection-label">Previous Connections</label>
|
||||||
<div style="display:flex; gap:16px; align-items:center; margin-bottom:8px;">
|
<div class="connections-list" id="connections-list">
|
||||||
<div class="arrow-pad">
|
<div style="color:var(--muted);font-size:12px;text-align:center;padding:20px 0;">No saved connections</div>
|
||||||
<div></div>
|
</div>
|
||||||
<button id="btn-up" title="Look up (↑)">↑</button>
|
|
||||||
<div></div>
|
<label class="selection-label" style="margin-top:16px;">Add New Robot</label>
|
||||||
<button id="btn-left" title="Look left (←)">←</button>
|
<div class="new-connection">
|
||||||
<div></div>
|
<input type="text" id="robot-ip" placeholder="192.168.1.100 or robot.local">
|
||||||
<button id="btn-right" title="Look right (→)">→</button>
|
<button id="btn-add-connection">Add</button>
|
||||||
<div></div>
|
|
||||||
<button id="btn-down" title="Look down (↓)">↓</button>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Say -->
|
<!-- Mode Selection -->
|
||||||
<div class="section">
|
<div class="selection-group">
|
||||||
<div class="section-title">Say</div>
|
<label class="selection-label"> Mode</label>
|
||||||
<div class="field">
|
<div class="mode-buttons-grid">
|
||||||
<textarea id="say-text" rows="2" placeholder="Hello! I am Jibo."></textarea>
|
<button class="mode-btn" data-mode="commander" title="Commander Mode">
|
||||||
</div>
|
<span class="mode-icon">🎮</span>
|
||||||
<div class="controls-row">
|
<span class="mode-label">Commander</span>
|
||||||
<button id="btn-say">▶ Say</button>
|
</button>
|
||||||
<button id="btn-say-cancel" class="danger">✕ Stop</button>
|
<button class="mode-btn" data-mode="animator" title="Animator Mode">
|
||||||
|
<span class="mode-icon">🎬</span>
|
||||||
|
<span class="mode-label">Animator</span>
|
||||||
|
</button>
|
||||||
|
<button class="mode-btn" data-mode="telepresence" title="Telepresence Mode">
|
||||||
|
<span class="mode-icon">👁</span>
|
||||||
|
<span class="mode-label">Telepresence</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Listen -->
|
<!-- Launch Button -->
|
||||||
<div class="section">
|
<button id="btn-launch" disabled>▶ Launch Session</button>
|
||||||
<div class="section-title">Listen</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<label>Max speech</label>
|
|
||||||
<input type="number" id="listen-max-speech" value="10000" step="1000">
|
|
||||||
<label>ms</label>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label>No-speech TO</label>
|
|
||||||
<input type="number" id="listen-max-nosp" value="5000" step="1000">
|
|
||||||
<label>ms</label>
|
|
||||||
</div>
|
|
||||||
<div class="controls-row" style="margin-bottom:8px;">
|
|
||||||
<button id="btn-listen">🎙 Listen</button>
|
|
||||||
<button id="btn-listen-cancel" class="danger">✕ Cancel</button>
|
|
||||||
</div>
|
|
||||||
<div id="listen-result">(result appears here)</div>
|
|
||||||
<div style="margin-top:8px;display:flex;align-items:center;gap:6px;">
|
|
||||||
<input type="checkbox" id="auto-listen-toggle" style="width:auto;">
|
|
||||||
<label for="auto-listen-toggle" style="margin:0;cursor:pointer;">Auto-listen on hotword</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Voice AI -->
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-title">Voice AI</div>
|
|
||||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
|
||||||
<input type="checkbox" id="llm-toggle" style="width:auto;">
|
|
||||||
<label for="llm-toggle" style="margin:0;cursor:pointer;">LLM voice loop</label>
|
|
||||||
<button id="btn-llm-clear" style="margin-left:auto;font-size:11px;padding:2px 8px;" title="Clear conversation history">↺ Clear</button>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Completions endpoint</label>
|
|
||||||
<input type="text" id="llm-endpoint" placeholder="http://localhost:11434/v1/chat/completions">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>Model</label>
|
|
||||||
<input type="text" id="llm-model" placeholder="llama3">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>System prompt</label>
|
|
||||||
<textarea id="llm-system-prompt" rows="3" placeholder="You are Jibo, a friendly social robot. Keep responses brief and conversational."></textarea>
|
|
||||||
</div>
|
|
||||||
<div id="llm-status" style="font-size:11px;color:var(--muted);font-style:italic;min-height:16px;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Attention -->
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-title">Attention Mode</div>
|
|
||||||
<div class="attention-grid">
|
|
||||||
<button class="attn-btn" data-mode="OFF">Off</button>
|
|
||||||
<button class="attn-btn" data-mode="IDLE">Idle</button>
|
|
||||||
<button class="attn-btn" data-mode="DISENGAGE">Disengage</button>
|
|
||||||
<button class="attn-btn" data-mode="ENGAGED">Engaged</button>
|
|
||||||
<button class="attn-btn" data-mode="SPEAKING">Speaking</button>
|
|
||||||
<button class="attn-btn" data-mode="FIXATED">Fixated</button>
|
|
||||||
<button class="attn-btn" data-mode="ATTRACTABLE">Attractable</button>
|
|
||||||
<button class="attn-btn" data-mode="COMMAND">Command</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Volume -->
|
|
||||||
<div class="section">
|
|
||||||
<div class="section-title">Volume</div>
|
|
||||||
<div class="row">
|
|
||||||
<input type="range" id="volume-slider" min="0" max="1" step="0.05" value="0.75">
|
|
||||||
<span id="volume-label" style="min-width:32px;text-align:right;font-size:12px;">75%</span>
|
|
||||||
</div>
|
|
||||||
<button id="btn-set-volume">Set Volume</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div><!-- /left-panel -->
|
|
||||||
|
|
||||||
<!-- ── CENTER PANEL ── -->
|
|
||||||
<div class="center-panel">
|
|
||||||
|
|
||||||
<!-- Camera feed -->
|
|
||||||
<div id="camera-wrap">
|
|
||||||
<img id="camera-feed" alt="Jibo camera" style="display:none;">
|
|
||||||
<div id="camera-no-feed">
|
|
||||||
<div style="font-size:40px;">📷</div>
|
|
||||||
<div>No camera feed</div>
|
|
||||||
<div style="font-size:11px;color:var(--muted);">Use the Camera tab to start video or take a photo</div>
|
|
||||||
</div>
|
|
||||||
<div id="click-dot"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Arrow pad (compact, centered under feed) -->
|
|
||||||
<div class="controls-row" style="justify-content:center; gap:10px;">
|
|
||||||
<div style="display:flex;flex-direction:column;align-items:center;gap:4px;">
|
|
||||||
<div style="font-size:11px;color:var(--muted);">Click on feed → look there</div>
|
|
||||||
<div style="display:flex;gap:6px;align-items:center;">
|
|
||||||
<span style="font-size:12px;color:var(--muted);">Track:</span>
|
|
||||||
<input type="checkbox" id="track-flag" style="width:auto;">
|
|
||||||
<span id="video-status"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Photos -->
|
|
||||||
<div style="width:100%;max-width:640px;">
|
|
||||||
<div class="section-title" style="margin-bottom:6px;">Photo Strip</div>
|
|
||||||
<div id="photo-strip"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div><!-- /center-panel -->
|
|
||||||
|
|
||||||
<!-- ── RIGHT PANEL ── -->
|
|
||||||
<div class="right-panel">
|
|
||||||
<div class="tabs">
|
|
||||||
<button class="tab-btn active" data-tab="tab-camera">Camera</button>
|
|
||||||
<button class="tab-btn" data-tab="tab-display">Display</button>
|
|
||||||
<button class="tab-btn" data-tab="tab-entities">Entities</button>
|
|
||||||
<button class="tab-btn" data-tab="tab-log">Log</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Camera tab -->
|
|
||||||
<div class="tab-panel active" id="tab-camera">
|
|
||||||
<div class="section-title">Video Stream</div>
|
|
||||||
<div class="video-controls" style="margin-bottom:8px;">
|
|
||||||
<button id="btn-video-start">▶ Start Video</button>
|
|
||||||
<button id="btn-video-stop" class="danger">■ Stop</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-title" style="margin-top:10px;">Take Photo</div>
|
|
||||||
<div class="row">
|
|
||||||
<label>Camera</label>
|
|
||||||
<select id="photo-camera">
|
|
||||||
<option value="right">Right</option>
|
|
||||||
<option value="left">Left</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<label>Resolution</label>
|
|
||||||
<select id="photo-res">
|
|
||||||
<option value="highRes">HighRes</option>
|
|
||||||
<option value="medRes">MedRes</option>
|
|
||||||
<option value="lowRes">LowRes</option>
|
|
||||||
<option value="microRes">MicroRes</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button id="btn-photo" style="width:100%;margin-bottom:10px;">📷 Take Photo</button>
|
|
||||||
|
|
||||||
<div class="section-title" style="margin-top:10px;">Subscriptions</div>
|
|
||||||
<div class="controls-row">
|
|
||||||
<button id="btn-sub-entity" class="active">👤 Entity</button>
|
|
||||||
<button id="btn-sub-motion" class="active">〰 Motion</button>
|
|
||||||
<button id="btn-sub-headtouch" class="active">✋ Touch</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Display tab -->
|
|
||||||
<div class="tab-panel" id="tab-display">
|
|
||||||
<div class="section-title">Eye</div>
|
|
||||||
<button id="btn-eye" style="width:100%;margin-bottom:10px;">👁 Show Eye</button>
|
|
||||||
|
|
||||||
<div class="section-title">Play Animation</div>
|
|
||||||
<div class="field">
|
|
||||||
<select id="eye-anim-select">
|
|
||||||
<optgroup label="── Blinks">
|
|
||||||
<option>Eye_Blink_01</option>
|
|
||||||
<option>Eye_Blink_02</option>
|
|
||||||
<option>Eye_Double_Blink_01</option>
|
|
||||||
<option>Eye_Double_Blink_02</option>
|
|
||||||
<option>Eye_Double_Blink_03</option>
|
|
||||||
<option>Eye_Long_Blink_01</option>
|
|
||||||
<option>Eye_Medium_Blink_01</option>
|
|
||||||
<option>Eye_Quick_Blink_01</option>
|
|
||||||
<option>Eye_Quick_Blink_02</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="── Expressions">
|
|
||||||
<option>Eye_Curious_01</option>
|
|
||||||
<option>Eye_Disgusted_01</option>
|
|
||||||
<option>Eye_Disgusted_02</option>
|
|
||||||
<option>eye_happy_00</option>
|
|
||||||
<option>eye_happy_01</option>
|
|
||||||
<option>eye_happy_02</option>
|
|
||||||
<option>eye_sad_01</option>
|
|
||||||
<option>eye_sad_02</option>
|
|
||||||
<option>eye_scared_00</option>
|
|
||||||
<option>eye_scared_01</option>
|
|
||||||
<option>eye_scared_02</option>
|
|
||||||
<option>eye_thinking_02</option>
|
|
||||||
<option>Confused_00</option>
|
|
||||||
<option>surprised_00</option>
|
|
||||||
<option>worried_01</option>
|
|
||||||
<option>worried_03</option>
|
|
||||||
<option>worried_04</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="── Eye Moves">
|
|
||||||
<option>Checking_00</option>
|
|
||||||
<option>Checking_00_02</option>
|
|
||||||
<option>Checking_01</option>
|
|
||||||
<option>checking-lr-high-01</option>
|
|
||||||
<option>checking-lr-low-01</option>
|
|
||||||
<option>checking-lr-mid-01</option>
|
|
||||||
<option>checking-c-highlow-01</option>
|
|
||||||
<option>checking-lr-lowhigh-01</option>
|
|
||||||
<option>checking-refresher-01</option>
|
|
||||||
<option>checking_08</option>
|
|
||||||
<option>Glance_Down_01</option>
|
|
||||||
<option>Glance_Left_01</option>
|
|
||||||
<option>Glance_Left_02</option>
|
|
||||||
<option>Glance_Right_01</option>
|
|
||||||
<option>Glance_Right_02</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="── Poses">
|
|
||||||
<option>Eye_Look_Center_Middle_01</option>
|
|
||||||
<option>Eye_Look_Center_Up_01</option>
|
|
||||||
<option>Eye_Look_Center_Down_01</option>
|
|
||||||
<option>Eye_Look_Left_Middle_01</option>
|
|
||||||
<option>Eye_Look_Left_Up_01</option>
|
|
||||||
<option>Eye_Look_Left_Down_01</option>
|
|
||||||
<option>Eye_Look_Right_Middle_01</option>
|
|
||||||
<option>Eye_Look_Right_Up_01</option>
|
|
||||||
<option>Eye_Look_Right_Down_01</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="── Global Eye">
|
|
||||||
<option>Close_To_Open_01</option>
|
|
||||||
<option>Open_To_Close_01</option>
|
|
||||||
<option>EyeToHappy_00</option>
|
|
||||||
<option>EyeToHappy_01</option>
|
|
||||||
<option>EyeToHappy_02</option>
|
|
||||||
<option>HappyToEye_00</option>
|
|
||||||
<option>HappyToEye_01</option>
|
|
||||||
<option>eye-bounce-01</option>
|
|
||||||
<option>eye-bounce-02</option>
|
|
||||||
<option>eye-closed</option>
|
|
||||||
<option>eye-laugh-00</option>
|
|
||||||
<option>eye-laugh-01</option>
|
|
||||||
<option>eye-pop-to-rest-01</option>
|
|
||||||
<option>eye-pop-to-rest-02</option>
|
|
||||||
<option>ahh-01</option>
|
|
||||||
<option>dilation</option>
|
|
||||||
<option>dilation-02</option>
|
|
||||||
<option>no-match-eye</option>
|
|
||||||
<option>quiver-01</option>
|
|
||||||
<option>quiver-02</option>
|
|
||||||
<option>quiver-03</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="── Transitions">
|
|
||||||
<option>eye-transition-blink-00</option>
|
|
||||||
<option>eye-transition-blink-02</option>
|
|
||||||
<option>eye-transition-fade-00</option>
|
|
||||||
<option>eye-closed-fade-to-black-01</option>
|
|
||||||
<option>eye-fade-from-black-00</option>
|
|
||||||
<option>eye-fade-from-black-01</option>
|
|
||||||
<option>dim-transition</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="── Globals">
|
|
||||||
<option>disengaged-to-engaged-01</option>
|
|
||||||
<option>disengaged-to-engaged-02</option>
|
|
||||||
<option>disengaged-to-listening-01</option>
|
|
||||||
<option>disengaged-to-listening-02</option>
|
|
||||||
<option>disengaged-to-listening-03</option>
|
|
||||||
<option>engaged-01</option>
|
|
||||||
<option>engaged-02</option>
|
|
||||||
<option>engaged-03</option>
|
|
||||||
<option>engaged-04</option>
|
|
||||||
<option>eye-center-to-up-01</option>
|
|
||||||
<option>eye-center-to-down-01</option>
|
|
||||||
<option>eye-up-to-center-01</option>
|
|
||||||
<option>eye-down-to-center-01</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="── Thinking">
|
|
||||||
<option>Thinking_Dots_03</option>
|
|
||||||
<option>eye_thinking_02</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="── Misc">
|
|
||||||
<option>Sleeping_Idle_01</option>
|
|
||||||
<option>end-listening-01</option>
|
|
||||||
<option>eye-default</option>
|
|
||||||
<option>Exclamation_00</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="── Emoji (Classic)">
|
|
||||||
<option>Emoji_Airplane</option>
|
|
||||||
<option>Emoji_Ant</option>
|
|
||||||
<option>Emoji_Apple</option>
|
|
||||||
<option>Emoji_Art</option>
|
|
||||||
<option>Emoji_Baby</option>
|
|
||||||
<option>Emoji_Baseball</option>
|
|
||||||
<option>Emoji_Basketball</option>
|
|
||||||
<option>Emoji_Beer</option>
|
|
||||||
<option>Emoji_Bicycle</option>
|
|
||||||
<option>Emoji_Bird_Blue</option>
|
|
||||||
<option>Emoji_Book</option>
|
|
||||||
<option>Emoji_Bowling</option>
|
|
||||||
<option>Emoji_Boxing</option>
|
|
||||||
<option>Emoji_Brain</option>
|
|
||||||
<option>Emoji_Burger</option>
|
|
||||||
<option>Emoji_Cake</option>
|
|
||||||
<option>Emoji_Camera</option>
|
|
||||||
<option>Emoji_Car</option>
|
|
||||||
<option>Emoji_Cat</option>
|
|
||||||
<option>Emoji_Checkmark</option>
|
|
||||||
<option>Emoji_ChristmasTree</option>
|
|
||||||
<option>Emoji_Clap</option>
|
|
||||||
<option>Emoji_Coffee</option>
|
|
||||||
<option>Emoji_Computer</option>
|
|
||||||
<option>Emoji_Dog</option>
|
|
||||||
<option>Emoji_Drawing</option>
|
|
||||||
<option>Emoji_Earth</option>
|
|
||||||
<option>Emoji_ExclamationBlue</option>
|
|
||||||
<option>Emoji_ExclamationRed</option>
|
|
||||||
<option>Emoji_ExclamationYellow</option>
|
|
||||||
<option>Emoji_Fire</option>
|
|
||||||
<option>Emoji_Fireworks</option>
|
|
||||||
<option>Emoji_Fish</option>
|
|
||||||
<option>Emoji_Football</option>
|
|
||||||
<option>Emoji_Gift</option>
|
|
||||||
<option>Emoji_Golf</option>
|
|
||||||
<option>Emoji_Halo</option>
|
|
||||||
<option>Emoji_HeartArrow</option>
|
|
||||||
<option>Emoji_HeartBlue</option>
|
|
||||||
<option>Emoji_HeartRed</option>
|
|
||||||
<option>Emoji_Hockey</option>
|
|
||||||
<option>Emoji_Hotdog</option>
|
|
||||||
<option>Emoji_House</option>
|
|
||||||
<option>Emoji_IceCream</option>
|
|
||||||
<option>Emoji_Lightbulb</option>
|
|
||||||
<option>Emoji_LightningBolt</option>
|
|
||||||
<option>Emoji_Lock</option>
|
|
||||||
<option>Emoji_Lunch</option>
|
|
||||||
<option>Emoji_Magic</option>
|
|
||||||
<option>Emoji_Money</option>
|
|
||||||
<option>Emoji_Moon</option>
|
|
||||||
<option>Emoji_Mountain</option>
|
|
||||||
<option>Emoji_Music</option>
|
|
||||||
<option>Emoji_PartyBlue</option>
|
|
||||||
<option>Emoji_PartyPink</option>
|
|
||||||
<option>Emoji_Penguin</option>
|
|
||||||
<option>Emoji_Pizza</option>
|
|
||||||
<option>Emoji_Planet</option>
|
|
||||||
<option>Emoji_Question</option>
|
|
||||||
<option>Emoji_Rainbow</option>
|
|
||||||
<option>Emoji_Robot</option>
|
|
||||||
<option>Emoji_Rocket</option>
|
|
||||||
<option>Emoji_Running</option>
|
|
||||||
<option>Emoji_Santa</option>
|
|
||||||
<option>Emoji_Shark</option>
|
|
||||||
<option>Emoji_Snowflake</option>
|
|
||||||
<option>Emoji_Snowman</option>
|
|
||||||
<option>Emoji_Soccer</option>
|
|
||||||
<option>Emoji_Star</option>
|
|
||||||
<option>Emoji_Sun</option>
|
|
||||||
<option>Emoji_Sunglasses</option>
|
|
||||||
<option>Emoji_Sushi</option>
|
|
||||||
<option>Emoji_Taco</option>
|
|
||||||
<option>Emoji_Tennis</option>
|
|
||||||
<option>Emoji_ThumbsDown</option>
|
|
||||||
<option>Emoji_ThumbsUp</option>
|
|
||||||
<option>Emoji_Tools</option>
|
|
||||||
<option>Emoji_Tree</option>
|
|
||||||
<option>Emoji_Truck</option>
|
|
||||||
<option>Emoji_Umbrella</option>
|
|
||||||
<option>Emoji_VideoGame</option>
|
|
||||||
<option>Emoji_Watermelon</option>
|
|
||||||
<option>Emoji_Waving</option>
|
|
||||||
<option>Emoji_Whale</option>
|
|
||||||
<option>Emoji_Wine</option>
|
|
||||||
</optgroup>
|
|
||||||
<optgroup label="── Emoji (HF)">
|
|
||||||
<option>emoji-airplane-hf-01</option>
|
|
||||||
<option>emoji-alligator-hf-01</option>
|
|
||||||
<option>emoji-ant-hf-01</option>
|
|
||||||
<option>emoji-apple-red-hf-01</option>
|
|
||||||
<option>emoji-baby-hf-01</option>
|
|
||||||
<option>emoji-balloons-hf-01</option>
|
|
||||||
<option>emoji-bandaid-hf-01</option>
|
|
||||||
<option>emoji-baseball-hf-01</option>
|
|
||||||
<option>emoji-basketball-hf-01</option>
|
|
||||||
<option>emoji-beach-hf-01</option>
|
|
||||||
<option>emoji-beer-hf-01</option>
|
|
||||||
<option>emoji-bike-hf-01</option>
|
|
||||||
<option>emoji-bird-blue-hf-01</option>
|
|
||||||
<option>emoji-book-hf-01</option>
|
|
||||||
<option>emoji-bowling-hf-01</option>
|
|
||||||
<option>emoji-boxing-hf-01</option>
|
|
||||||
<option>emoji-breakfast-hf-01</option>
|
|
||||||
<option>emoji-broken-heart-hf-01</option>
|
|
||||||
<option>emoji-bunny-hf-01</option>
|
|
||||||
<option>emoji-burger-hf-01</option>
|
|
||||||
<option>emoji-cake-hf-01</option>
|
|
||||||
<option>emoji-camera-hf-01</option>
|
|
||||||
<option>emoji-car-hf-01</option>
|
|
||||||
<option>emoji-cat-hf-01</option>
|
|
||||||
<option>emoji-checkmark-hf-01</option>
|
|
||||||
<option>emoji-christmas-tree-hf-01</option>
|
|
||||||
<option>emoji-computer-chip-hf-01</option>
|
|
||||||
<option>emoji-cow-hf-01</option>
|
|
||||||
<option>emoji-diamond-hf-01</option>
|
|
||||||
<option>emoji-dinner-hf-01</option>
|
|
||||||
<option>emoji-disco-ball-hf-01</option>
|
|
||||||
<option>emoji-dog-hf-01</option>
|
|
||||||
<option>emoji-dress-hf-01</option>
|
|
||||||
<option>emoji-earth-hf-01</option>
|
|
||||||
<option>emoji-easter-egg-hf-01</option>
|
|
||||||
<option>emoji-elephant-hf-01</option>
|
|
||||||
<option>emoji-exclamation-mark-red-hf-01</option>
|
|
||||||
<option>emoji-exclamation-mark-yellow-hf-01</option>
|
|
||||||
<option>emoji-fire-hf-01</option>
|
|
||||||
<option>emoji-fish-hf-01</option>
|
|
||||||
<option>emoji-fishing-hf-01</option>
|
|
||||||
<option>emoji-flamingo-hf-01</option>
|
|
||||||
<option>emoji-flower-pink-hf-01</option>
|
|
||||||
<option>emoji-football-hf-01</option>
|
|
||||||
<option>emoji-ghost-hf-01</option>
|
|
||||||
<option>emoji-golf-hf-01</option>
|
|
||||||
<option>emoji-guitar-hf-01</option>
|
|
||||||
<option>emoji-halo-hf-01</option>
|
|
||||||
<option>emoji-hat-hf-01</option>
|
|
||||||
<option>emoji-heart-hf-01</option>
|
|
||||||
<option>emoji-hockey-hf-01</option>
|
|
||||||
<option>emoji-hot-dog-hf-01</option>
|
|
||||||
<option>emoji-house-hf-01</option>
|
|
||||||
<option>emoji-ice-cream-hf-01</option>
|
|
||||||
<option>emoji-ice-cube-hf-01</option>
|
|
||||||
<option>emoji-jack-o-lantern-hf-01</option>
|
|
||||||
<option>emoji-laptop-hf-01</option>
|
|
||||||
<option>emoji-light-bulb-hf-01</option>
|
|
||||||
<option>emoji-lightning-hf-01</option>
|
|
||||||
<option>emoji-lock-hf-01</option>
|
|
||||||
<option>emoji-magic-hf-01</option>
|
|
||||||
<option>emoji-magnifying-glass-hf-01</option>
|
|
||||||
<option>emoji-microphone-hf-01</option>
|
|
||||||
<option>emoji-money-hf-01</option>
|
|
||||||
<option>emoji-monkey-hf-01</option>
|
|
||||||
<option>emoji-moon-hf-01</option>
|
|
||||||
<option>emoji-mountains-hf-01</option>
|
|
||||||
<option>emoji-mouse-hf-01</option>
|
|
||||||
<option>emoji-music-hf-01</option>
|
|
||||||
<option>emoji-new-years-hf-01</option>
|
|
||||||
<option>emoji-ocean-hf-01</option>
|
|
||||||
<option>emoji-outlet-hf-01</option>
|
|
||||||
<option>emoji-pants-hf-01</option>
|
|
||||||
<option>emoji-party-hf-01</option>
|
|
||||||
<option>emoji-penguin-hf-01</option>
|
|
||||||
<option>emoji-phone-hf-01</option>
|
|
||||||
<option>emoji-pig-hf-01</option>
|
|
||||||
<option>emoji-pizza-hf-01</option>
|
|
||||||
<option>emoji-popcorn-hf-01</option>
|
|
||||||
<option>emoji-pretzel-hf-01</option>
|
|
||||||
<option>emoji-pumpkin-hf-01</option>
|
|
||||||
<option>emoji-puzzle-piece-hf-01</option>
|
|
||||||
<option>emoji-question-mark-hf-01</option>
|
|
||||||
<option>emoji-rainbow-hf-01</option>
|
|
||||||
<option>emoji-robot-hf-01</option>
|
|
||||||
<option>emoji-rocket-hf-01</option>
|
|
||||||
<option>emoji-running-hf-01</option>
|
|
||||||
<option>emoji-santa-hf-01</option>
|
|
||||||
<option>emoji-school-bus-hf-01</option>
|
|
||||||
<option>emoji-sheep-hf-01</option>
|
|
||||||
<option>emoji-shopping-hf-01</option>
|
|
||||||
<option>emoji-snorkel-hf-01</option>
|
|
||||||
<option>emoji-snowflake-hf-01</option>
|
|
||||||
<option>emoji-soccer-hf-01</option>
|
|
||||||
<option>emoji-star-hf-01</option>
|
|
||||||
<option>emoji-stork-hf-01</option>
|
|
||||||
<option>emoji-sun-hf-01</option>
|
|
||||||
<option>emoji-sushi-hf-01</option>
|
|
||||||
<option>emoji-taco-hf-01</option>
|
|
||||||
<option>emoji-tennis-hf-01</option>
|
|
||||||
<option>emoji-thunder-hf-01</option>
|
|
||||||
<option>emoji-toaster-hf-01</option>
|
|
||||||
<option>emoji-toilet-paper-hf-01</option>
|
|
||||||
<option>emoji-tools-hf-01</option>
|
|
||||||
<option>emoji-trash-can-hf-01</option>
|
|
||||||
<option>emoji-tree-hf-01</option>
|
|
||||||
<option>emoji-truck-hf-01</option>
|
|
||||||
<option>emoji-turtle-hf-01</option>
|
|
||||||
<option>emoji-tv-hf-01</option>
|
|
||||||
<option>emoji-umbrella-hf-01</option>
|
|
||||||
<option>emoji-watermelon-hf-01</option>
|
|
||||||
<option>emoji-whale-hf-01</option>
|
|
||||||
<option>emoji-wine-hf-01</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button id="btn-play-anim" style="width:100%;margin-bottom:10px;">▶ Play Animation</button>
|
|
||||||
|
|
||||||
<div class="section-title" style="margin-top:4px;">Display Text</div>
|
|
||||||
<div class="field">
|
|
||||||
<textarea id="display-text" rows="2" placeholder="Text to show on screen…"></textarea>
|
|
||||||
</div>
|
|
||||||
<button id="btn-display-text" style="width:100%;margin-bottom:10px;">Show Text</button>
|
|
||||||
|
|
||||||
<div class="section-title">Display Image</div>
|
|
||||||
<div class="field">
|
|
||||||
<input type="text" id="display-img-src" placeholder="http://… image URL">
|
|
||||||
</div>
|
|
||||||
<button id="btn-display-image" style="width:100%;">Show Image</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Entities tab -->
|
|
||||||
<div class="tab-panel" id="tab-entities">
|
|
||||||
<div class="section-title">Detected Entities</div>
|
|
||||||
<div id="entity-list"><div style="color:var(--muted);font-size:12px;">Waiting for entity events…</div></div>
|
|
||||||
<div class="section-title" style="margin-top:12px;">Head Touch</div>
|
|
||||||
<div id="head-touch-display" style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px;">
|
|
||||||
<div class="pad-indicator" data-pad="frontLeft">FL</div>
|
|
||||||
<div class="pad-indicator" data-pad="middleLeft">ML</div>
|
|
||||||
<div class="pad-indicator" data-pad="backLeft">BL</div>
|
|
||||||
<div class="pad-indicator" data-pad="frontRight">FR</div>
|
|
||||||
<div class="pad-indicator" data-pad="middleRight">MR</div>
|
|
||||||
<div class="pad-indicator" data-pad="backRight">BR</div>
|
|
||||||
</div>
|
|
||||||
<style>
|
|
||||||
.pad-indicator {
|
|
||||||
background: var(--surface2); border: 1px solid var(--border);
|
|
||||||
border-radius: 4px; padding: 6px; text-align: center;
|
|
||||||
font-size: 11px; color: var(--muted); transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.pad-indicator.active { background: var(--accent); color: #000; border-color: var(--accent); }
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Log tab -->
|
|
||||||
<div class="tab-panel" id="tab-log" style="padding:0;display:flex;flex-direction:column;flex:1;">
|
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);">
|
|
||||||
<span class="section-title" style="margin:0;">Event Log</span>
|
|
||||||
<button id="btn-clear-log" style="font-size:11px;padding:3px 8px;">Clear</button>
|
|
||||||
</div>
|
|
||||||
<div id="event-log"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div><!-- /right-panel -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Photo modal -->
|
<script src="home.js"></script>
|
||||||
<div id="photo-modal">
|
|
||||||
<div id="photo-modal-close">✕</div>
|
|
||||||
<img id="photo-modal-img" src="" alt="Photo">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="app.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -374,3 +374,71 @@ label { font-size: 12px; color: var(--muted); display: block; margin-bottom: 3px
|
|||||||
/* ── Video area ── */
|
/* ── Video area ── */
|
||||||
.video-controls { display: flex; gap: 6px; align-items: center; }
|
.video-controls { display: flex; gap: 6px; align-items: center; }
|
||||||
#video-status { font-size: 12px; color: var(--muted); }
|
#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;
|
||||||
|
}
|
||||||
|
|||||||
143
public/telepresence.html
Normal file
143
public/telepresence.html
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Re-Commander — Telepresence Mode</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div id="status-dot"></div>
|
||||||
|
<h1>Re<span>-Commander</span> — Telepresence</h1>
|
||||||
|
<span id="status-label">Connecting…</span>
|
||||||
|
<span id="hotword-indicator"></span>
|
||||||
|
<button id="btn-back-home" style="margin-left: auto; padding: 5px 12px; font-size: 12px;">← Back to Home</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
|
||||||
|
<!-- ── LEFT PANEL ── -->
|
||||||
|
<div class="left-panel">
|
||||||
|
|
||||||
|
<!-- Telepresence Controls -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Presence Controls</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>User Name</label>
|
||||||
|
<input type="text" id="user-name" placeholder="Your name">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Status</label>
|
||||||
|
<select id="presence-status">
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="away">Away</option>
|
||||||
|
<option value="busy">Busy</option>
|
||||||
|
<option value="offline">Offline</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button id="btn-start-telepresence" style="width:100%;">🔴 Start Broadcasting</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Head Navigation -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Head Navigation</div>
|
||||||
|
<div style="display:flex; gap:16px; align-items:center; margin-bottom:8px;">
|
||||||
|
<div class="arrow-pad">
|
||||||
|
<div></div>
|
||||||
|
<button id="btn-up" title="Look up (↑)">UP</button>
|
||||||
|
<div></div>
|
||||||
|
<button id="btn-left" title="Look left (←)">←</button>
|
||||||
|
<div></div>
|
||||||
|
<button id="btn-right" title="Look right (→)">→</button>
|
||||||
|
<div></div>
|
||||||
|
<button id="btn-down" title="Look down (↓)">↓</button>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Say -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Say</div>
|
||||||
|
<div class="field">
|
||||||
|
<textarea id="say-text" rows="2" placeholder="Hello! I am present via telepresence."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="controls-row">
|
||||||
|
<button id="btn-say">🎤 Say</button>
|
||||||
|
<button id="btn-say-cancel" class="danger">✕ Stop</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Audio Settings -->
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Audio</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Microphone Level</label>
|
||||||
|
<input type="range" id="mic-level" min="0" max="1" step="0.05" value="0.75">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<label>Speaker Volume</label>
|
||||||
|
<input type="range" id="speaker-volume" min="0" max="1" step="0.05" value="0.75">
|
||||||
|
<span id="volume-label" style="min-width:32px;text-align:right;font-size:12px;">75%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /left-panel -->
|
||||||
|
|
||||||
|
<!-- ── CENTER PANEL ── -->
|
||||||
|
<div class="center-panel">
|
||||||
|
|
||||||
|
<!-- Camera feed (main telepresence view) -->
|
||||||
|
<div id="camera-wrap">
|
||||||
|
<img id="camera-feed" alt="Jibo camera" style="display:none;">
|
||||||
|
<div id="camera-no-feed">
|
||||||
|
<div style="font-size:40px;">📹</div>
|
||||||
|
<div>Telepresence View</div>
|
||||||
|
<div style="font-size:11px;color:var(--muted);">Start broadcasting to see live camera feed</div>
|
||||||
|
</div>
|
||||||
|
<div id="click-dot"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Remote View / User Info -->
|
||||||
|
<div class="controls-row" style="justify-content:center; gap:10px; margin-top:20px; flex-direction: column; align-items: center;">
|
||||||
|
<div id="remote-status" style="font-size:13px;color:var(--text);padding:10px;background:var(--surface2);border-radius:6px;border:1px solid var(--border);min-width:300px;text-align:center;">
|
||||||
|
Waiting to start telepresence...
|
||||||
|
</div>
|
||||||
|
<button id="btn-video-start" style="flex-basis: auto;">▶ Start Video</button>
|
||||||
|
<button id="btn-video-stop" class="danger" style="flex-basis: auto;">■ Stop</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /center-panel -->
|
||||||
|
|
||||||
|
<!-- ── RIGHT PANEL ── -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-btn active" data-tab="tab-participants">Participants</button>
|
||||||
|
<button class="tab-btn" data-tab="tab-log">Log</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Participants tab -->
|
||||||
|
<div class="tab-panel active" id="tab-participants">
|
||||||
|
<div class="section-title">Remote Users</div>
|
||||||
|
<div id="participants-list" style="font-size:12px;">
|
||||||
|
<div style="color:var(--muted);">No remote users connected</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Log tab -->
|
||||||
|
<div class="tab-panel" id="tab-log" style="padding:0;display:flex;flex-direction:column;flex:1;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-bottom:1px solid var(--border);">
|
||||||
|
<span class="section-title" style="margin:0;">Session Log</span>
|
||||||
|
<button id="btn-clear-log" style="font-size:11px;padding:3px 8px;">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div id="event-log"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /right-panel -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="telepresence.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
242
public/telepresence.js
Normal file
242
public/telepresence.js
Normal file
@@ -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 =
|
||||||
|
`<strong>${userName}</strong> 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 = `<span class="ts">${now}</span> <span class="${cls}">${eventName}</span>${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();
|
||||||
@@ -11,7 +11,7 @@ const fs = require('fs');
|
|||||||
|
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const JIBO_HOST = '192.168.1.217';
|
const JIBO_HOST = '192.168.1.10';
|
||||||
const JIBO_PORT = 8160;
|
const JIBO_PORT = 8160;
|
||||||
const APP_PORT = process.env.PORT || 3000;
|
const APP_PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user