Compare commits

..

3 Commits

Author SHA1 Message Date
pasketti
ae624da7c2 Upgrade to rom-control v2 Client API; rename app.js → commander.js; hide incomplete telepresence/animator pages 2026-04-23 02:13:03 -04:00
pasketti
56578e59f9 Merge remote-tracking branch 'origin/Home-Menu' 2026-04-23 01:45:28 -04:00
pasketti
07ef1a2fad Add LLM_HEADERS support for custom request headers
Reads LLM_HEADERS as a JSON object from .env and merges it into every
LLM request alongside the existing Authorization header. Useful for
endpoints that require non-standard headers (e.g. x-openclaw-agent-id).
LLM_API_KEY continues to be sent without the "Bearer" prefix in .env.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 14:54:59 -04:00
14 changed files with 454 additions and 8740 deletions

View File

@@ -4,8 +4,12 @@ LLM_ENDPOINT=http://localhost:11434/v1/chat/completions
# Model name passed to the endpoint # Model name passed to the endpoint
LLM_MODEL=llama3 LLM_MODEL=llama3
# Optional API key (sent as Bearer token) — leave blank for local servers # Optional API key — value is sent as "Bearer <value>", do NOT include the word "Bearer" here
LLM_API_KEY= LLM_API_KEY=
# Optional extra headers sent with every LLM request, as a JSON object
# Example: LLM_HEADERS={"x-openclaw-agent-id":"jibo"}
LLM_HEADERS=
# Default system prompt for the voice AI loop # Default system prompt for the voice AI loop
LLM_SYSTEM_PROMPT=You are Jibo, a friendly social robot. Keep responses brief and conversational. LLM_SYSTEM_PROMPT=You are Jibo, a friendly social robot. Keep responses brief and conversational.

62
package-lock.json generated
View File

@@ -1,51 +1,19 @@
{ {
"name": "re-commander", "name": "re-commander",
"version": "1.0.0", "version": "2.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "re-commander", "name": "re-commander",
"version": "1.0.0", "version": "2.0.0",
"dependencies": { "dependencies": {
"@theatre/core": "^0.7.2",
"@theatre/studio": "^0.7.2",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^4.18.2", "express": "^4.18.2",
"golden-layout": "^2.6.0", "rom-control": "^2.0.0",
"ws": "^8.14.2" "ws": "^8.14.2"
} }
}, },
"node_modules/@theatre/core": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@theatre/core/-/core-0.7.2.tgz",
"integrity": "sha512-IDQa/6WY7mIJAtsSd4EgNcM0IUZkl+FrqZ8DdYiCVTFap9ARDNmrngJOeFjJOsnnaHlc5GdEB/jj7fsjbIrAzQ==",
"license": "Apache-2.0",
"dependencies": {
"@theatre/dataverse": "0.7.2"
}
},
"node_modules/@theatre/dataverse": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@theatre/dataverse/-/dataverse-0.7.2.tgz",
"integrity": "sha512-YyfoyX7EyhFUY2OM5fsM0LPrs1SdgLwpiTMkkvTIoZLdOwvQhstjYq4Yz/8ZncJlRoTWvakfmgvCaBN+QuBYxg==",
"license": "Apache-2.0",
"dependencies": {
"lodash-es": "^4.17.21"
}
},
"node_modules/@theatre/studio": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/@theatre/studio/-/studio-0.7.2.tgz",
"integrity": "sha512-p6LTKzJWVlcHkpGzIlNHh9AkGbB3E+0q9Pjxv+OJoTDe1IK+CMKW695Wp+1//lB4vfC9qShe4z/p+Zaj1q8KtA==",
"license": "AGPL-3.0-only",
"dependencies": {
"@theatre/dataverse": "0.7.2"
},
"peerDependencies": {
"@theatre/core": "*"
}
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -404,12 +372,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/golden-layout": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/golden-layout/-/golden-layout-2.6.0.tgz",
"integrity": "sha512-sIVQCiRWOymHbVD1Aw/T9/ijbPYAVGBlgGYd1N9MRKfcyBNSpjr87Vg9nSHm+RCT8ELrvK8IJYJV0QRJuVUkCQ==",
"license": "MIT"
},
"node_modules/gopd": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -493,12 +455,6 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/lodash-es": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT"
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -674,6 +630,18 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/rom-control": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/rom-control/-/rom-control-2.0.0.tgz",
"integrity": "sha512-mENZI9Cf8fUzB02X1tTNGn4HUlCEpASds9YZQvbp/T5LJoCYCrgryLAE7OCIkLa+4Ob+NlO1jBK84n8/zR7/tg==",
"license": "MIT",
"dependencies": {
"ws": "^8.14.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",

View File

@@ -1,17 +1,15 @@
{ {
"name": "re-commander", "name": "re-commander",
"version": "1.0.0", "version": "2.0.0",
"description": "Jibo ROM Commander — local Node.js recreation using port 8160", "description": "Jibo ROM Commander — built on the rom-control module",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "node server.js" "start": "node server.js"
}, },
"dependencies": { "dependencies": {
"@theatre/core": "^0.7.2",
"@theatre/studio": "^0.7.2",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^4.18.2", "express": "^4.18.2",
"golden-layout": "^2.6.0", "rom-control": "^2.0.0",
"ws": "^8.14.2" "ws": "^8.14.2"
} }
} }

View File

@@ -1,694 +0,0 @@
<!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</title>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Material Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Golden Layout -->
<link rel="stylesheet" href="https://golden-layout.com/assets/css/goldenlayout-base.css">
<link rel="stylesheet" href="https://golden-layout.com/assets/css/goldenlayout-dark-theme.css">
<script src="https://cdn.jsdelivr.net/npm/golden-layout@2/dist/umd/index.js"></script>
<!-- Theatre.js (temporarily disabled) -->
<!-- <script src="https://unpkg.com/@theatre/core@0.7.2/dist/theatre-core.umd.js"></script>
<script src="https://unpkg.com/@theatre/studio@0.7.2/dist/theatre-studio.umd.js"></script> -->
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
background: #121212;
color: #ffffff;
overflow: hidden;
height: 100vh;
display: flex;
flex-direction: column;
}
/* Material Design Top Bar */
.top-bar {
height: 64px;
background: #1e1e1e;
border-bottom: 1px solid #333;
display: flex;
align-items: center;
padding: 0 16px;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
z-index: 1000;
flex-shrink: 0;
}
.logo-section {
display: flex;
align-items: center;
margin-right: 32px;
}
.logo {
width: 32px;
height: 32px;
background: linear-gradient(45deg, #2196F3, #1976D2);
border-radius: 8px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
font-size: 16px;
}
.app-title {
font-size: 20px;
font-weight: 500;
color: #ffffff;
}
/* Dropdown Menus */
.dropdown {
position: relative;
margin-right: 8px;
}
.dropdown-button {
background: transparent;
border: none;
color: #ffffff;
padding: 8px 16px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-radius: 4px;
transition: background 0.2s;
display: flex;
align-items: center;
gap: 4px;
}
.dropdown-button:hover {
background: rgba(255,255,255,0.1);
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
background: #2d2d2d;
border: 1px solid #444;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
min-width: 160px;
display: none;
z-index: 1001;
}
.dropdown-menu.show {
display: block;
}
.dropdown-item {
padding: 12px 16px;
cursor: pointer;
transition: background 0.2s;
font-size: 14px;
color: #ffffff;
}
.dropdown-item:hover {
background: rgba(255,255,255,0.1);
}
.dropdown-divider {
height: 1px;
background: #444;
margin: 4px 0;
}
/* Connection Status */
.connection-status {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
margin-right: 16px;
padding: 6px 12px;
background: rgba(255,255,255,0.05);
border-radius: 16px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #666;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #4CAF50;
}
.status-dot.disconnected {
background: #f44336;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-text {
font-size: 12px;
color: #ccc;
}
/* Action Buttons */
.action-buttons {
display: flex;
align-items: center;
gap: 8px;
margin-right: 16px;
}
.icon-button {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: rgba(255,255,255,0.1);
color: #ffffff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.icon-button:hover {
background: rgba(255,255,255,0.2);
transform: scale(1.05);
}
/* Animation Name Section */
.animation-section {
display: flex;
align-items: center;
gap: 8px;
}
.animation-input {
background: rgba(255,255,255,0.1);
border: 1px solid #444;
border-radius: 4px;
padding: 8px 12px;
color: #ffffff;
font-size: 14px;
width: 200px;
transition: border-color 0.2s;
}
.animation-input:focus {
outline: none;
border-color: #2196F3;
}
.export-button {
background: #2196F3;
border: none;
color: white;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
gap: 4px;
}
.export-button:hover {
background: #1976D2;
}
/* Golden Layout Container */
#layout-container {
flex: 1;
width: 100%;
background: #1a1a1a;
}
/* Golden Layout Customization */
.gl_container {
background: #1a1a1a !important;
}
.gl_tab {
background: #2d2d2d !important;
color: #ffffff !important;
border-bottom: 2px solid #444 !important;
padding: 8px 12px !important;
font-size: 12px !important;
}
.gl_tab.gl_active {
background: #2196F3 !important;
color: #ffffff !important;
border-bottom: 2px solid #2196F3 !important;
}
.gl_tab:hover {
background: #333333 !important;
}
.gl_tabBar {
background: #2d2d2d !important;
border-bottom: 1px solid #444 !important;
}
.gl_header {
background: #2d2d2d !important;
border-bottom: 1px solid #444 !important;
padding: 6px !important;
}
.gl_splitter {
background: #444 !important;
}
</style>
</head>
<body>
<!-- Material Design Top Bar -->
<header class="top-bar">
<!-- Logo/Title Section -->
<div class="logo-section">
<div class="logo">R</div>
<div class="app-title">Re-Commander Animator</div>
</div>
<!-- File Menu -->
<div class="dropdown">
<button class="dropdown-button" onclick="toggleDropdown('file-menu')">
File <span class="material-icons" style="font-size: 16px;">arrow_drop_down</span>
</button>
<div class="dropdown-menu" id="file-menu">
<div class="dropdown-item">New Animation</div>
<div class="dropdown-item">Open Animation</div>
<div class="dropdown-item">Save Animation</div>
<div class="dropdown-item">Save As...</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item">Import</div>
<div class="dropdown-item">Export</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item">Exit</div>
</div>
</div>
<!-- Edit Menu -->
<div class="dropdown">
<button class="dropdown-button" onclick="toggleDropdown('edit-menu')">
Edit <span class="material-icons" style="font-size: 16px;">arrow_drop_down</span>
</button>
<div class="dropdown-menu" id="edit-menu">
<div class="dropdown-item">Undo</div>
<div class="dropdown-item">Redo</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item">Cut</div>
<div class="dropdown-item">Copy</div>
<div class="dropdown-item">Paste</div>
<div class="dropdown-item">Delete</div>
<div class="dropdown-divider"></div>
<div class="dropdown-item">Select All</div>
<div class="dropdown-item">Preferences</div>
</div>
</div>
<!-- Connection Status -->
<div class="connection-status">
<div class="status-dot disconnected" id="status-dot"></div>
<span class="status-text" id="status-text">Disconnected</span>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<button class="icon-button" title="Upload">
<span class="material-icons">upload</span>
</button>
<button class="icon-button" title="Settings">
<span class="material-icons">settings</span>
</button>
</div>
<!-- Animation Name & Export -->
<div class="animation-section">
<input type="text" class="animation-input" placeholder="Animation name..." id="animation-name">
<button class="export-button">
<span class="material-icons" style="font-size: 16px;">download</span>
Export
</button>
</div>
</header>
<!-- Golden Layout Container -->
<div id="layout-container"></div>
<script src="https://cdn.jsdelivr.net/npm/golden-layout@2/dist/umd/index.js"></script>
<script>
// Dropdown functionality
function toggleDropdown(menuId) {
const menu = document.getElementById(menuId);
const allMenus = document.querySelectorAll('.dropdown-menu');
allMenus.forEach(m => {
if (m.id !== menuId) {
m.classList.remove('show');
}
});
menu.classList.toggle('show');
}
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.dropdown')) {
document.querySelectorAll('.dropdown-menu').forEach(menu => {
menu.classList.remove('show');
});
}
});
// Simulate connection status
let connected = false;
function updateConnectionStatus() {
const dot = document.getElementById('status-dot');
const text = document.getElementById('status-text');
connected = !connected;
if (connected) {
dot.className = 'status-dot connected';
text.textContent = 'Connected to Jibo';
} else {
dot.className = 'status-dot disconnected';
text.textContent = 'Disconnected';
}
}
// Update connection status every 3 seconds for demo
setInterval(updateConnectionStatus, 3000);
// Basic Timeline Initialization (without Theatre.js for now)
function initializeTheatreTimeline(containerElement) {
console.log('Initializing basic timeline...');
// Add playback controls
const playBtn = containerElement.querySelector('#play-btn');
const pauseBtn = containerElement.querySelector('#pause-btn');
const stopBtn = containerElement.querySelector('#stop-btn');
const timeDisplay = containerElement.querySelector('#time-display');
const timelineContainer = containerElement.querySelector('#theatre-timeline');
let isPlaying = false;
let startTime = 0;
let currentTime = 0;
let animationFrame = null;
// Create basic timeline visualization
if (timelineContainer) {
timelineContainer.innerHTML = `
<div style="padding: 20px; height: 100%; background: #2d2d2d; border-radius: 4px; position: relative; overflow: hidden;">
<div style="position: absolute; top: 20px; left: 20px; right: 20px; height: 2px; background: #444;">
<div id="timeline-progress" style="width: 0%; height: 100%; background: #2196F3; transition: width 0.1s;"></div>
</div>
<div style="position: absolute; top: 40px; left: 20px; right: 20px; display: flex; justify-content: space-between; font-size: 10px; color: #666;">
<span>0s</span>
<span>1s</span>
<span>2s</span>
<span>3s</span>
<span>4s</span>
<span>5s</span>
</div>
<div style="position: absolute; top: 80px; left: 20px; right: 20px; bottom: 20px;">
<div style="font-size: 12px; color: #ccc; margin-bottom: 10px;">Timeline Ready</div>
<div style="background: #1e1e1e; padding: 10px; border-radius: 4px; font-size: 11px; color: #666;">
Basic timeline component - Theatre.js integration pending
</div>
</div>
</div>
`;
}
function updateTime() {
if (isPlaying) {
currentTime = (Date.now() - startTime) / 1000;
timeDisplay.textContent = currentTime.toFixed(2) + 's';
// Update progress bar
const progress = Math.min((currentTime / 5) * 100, 100); // 5 second timeline
const progressBar = containerElement.querySelector('#timeline-progress');
if (progressBar) {
progressBar.style.width = progress + '%';
}
animationFrame = requestAnimationFrame(updateTime);
}
}
if (playBtn) {
playBtn.addEventListener('click', () => {
if (!isPlaying) {
isPlaying = true;
startTime = Date.now() - (currentTime * 1000);
updateTime();
playBtn.style.background = '#666';
pauseBtn.style.background = '#FF9800';
}
});
}
if (pauseBtn) {
pauseBtn.addEventListener('click', () => {
isPlaying = false;
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
playBtn.style.background = '#4CAF50';
pauseBtn.style.background = '#666';
});
}
if (stopBtn) {
stopBtn.addEventListener('click', () => {
isPlaying = false;
currentTime = 0;
startTime = Date.now();
timeDisplay.textContent = '0.00s';
// Reset progress bar
const progressBar = containerElement.querySelector('#timeline-progress');
if (progressBar) {
progressBar.style.width = '0%';
}
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
playBtn.style.background = '#4CAF50';
pauseBtn.style.background = '#FF9800';
});
}
console.log('Basic timeline initialized successfully');
}
// Initialize Golden Layout
document.addEventListener('DOMContentLoaded', () => {
// Wait for GoldenLayout to be available
function waitForGoldenLayout() {
if (typeof GoldenLayout === 'undefined') {
console.log('GoldenLayout not yet available, retrying...');
setTimeout(waitForGoldenLayout, 100);
return;
}
console.log('GoldenLayout found, initializing...');
const config = {
root: {
type: 'row',
children: [
{
type: 'component',
componentType: 'timeline',
componentState: { title: 'Timeline' },
title: 'Timeline',
width: 30
},
{
type: 'column',
children: [
{
type: 'component',
componentType: 'canvas',
componentState: { title: 'Canvas' },
title: 'Canvas',
height: 60
},
{
type: 'component',
componentType: 'properties',
componentState: { title: 'Properties' },
title: 'Properties',
height: 40
}
],
width: 40
},
{
type: 'column',
children: [
{
type: 'component',
componentType: 'library',
componentState: { title: 'Animation Library' },
title: 'Library',
height: 50
},
{
type: 'component',
componentType: 'preview',
componentState: { title: 'Preview' },
title: 'Preview',
height: 50
}
],
width: 30
}
]
}
};
const componentRegistry = new GoldenLayout.ComponentRegistry();
componentRegistry.registerComponent('timeline', (container, state) => {
container.element.innerHTML = `
<div style="padding: 20px; color: white; height: 100%; overflow-y: auto;">
<h3>${state.title}</h3>
<div id="theatre-timeline" style="margin-top: 16px; height: 300px; background: #2d2d2d; border-radius: 4px; position: relative;"></div>
<div id="theatre-controls" style="margin-top: 16px; display: flex; gap: 8px; align-items: center;">
<button id="play-btn" style="background: #4CAF50; border: none; color: white; padding: 8px 16px; border-radius: 4px; cursor: pointer;">
<span class="material-icons" style="font-size: 16px; vertical-align: middle;">play_arrow</span> Play
</button>
<button id="pause-btn" style="background: #FF9800; border: none; color: white; padding: 8px 16px; border-radius: 4px; cursor: pointer;">
<span class="material-icons" style="font-size: 16px; vertical-align: middle;">pause</span> Pause
</button>
<button id="stop-btn" style="background: #f44336; border: none; color: white; padding: 8px 16px; border-radius: 4px; cursor: pointer;">
<span class="material-icons" style="font-size: 16px; vertical-align: middle;">stop</span> Stop
</button>
<span style="margin-left: 16px; font-size: 12px; color: #ccc;">Time: <span id="time-display">0.00s</span></span>
</div>
<div style="margin-top: 16px;">
<h4 style="margin-bottom: 8px; font-size: 14px; color: #ccc;">Animation Tracks</h4>
<div id="tracks-list" style="background: #2d2d2d; padding: 12px; border-radius: 4px;">
<div style="padding: 8px; margin-bottom: 4px; background: #1e1e1e; border-radius: 2px; font-size: 12px;">
📍 Position Track
</div>
<div style="padding: 8px; margin-bottom: 4px; background: #1e1e1e; border-radius: 2px; font-size: 12px;">
🎭 Animation Track
</div>
<div style="padding: 8px; margin-bottom: 4px; background: #1e1e1e; border-radius: 2px; font-size: 12px;">
🎵 Audio Track
</div>
</div>
</div>
</div>
`;
// Initialize Theatre.js timeline in this container
setTimeout(() => {
if (typeof Theatre !== 'undefined') {
initializeTheatreTimeline(container.element);
}
}, 100);
});
componentRegistry.registerComponent('canvas', (container, state) => {
container.element.innerHTML = `
<div style="padding: 20px; color: white; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<h3>${state.title}</h3>
<div style="margin-top: 20px; width: 200px; height: 200px; background: #2d2d2d; border: 2px dashed #444; border-radius: 8px; display: flex; align-items: center; justify-content: center;">
<span style="color: #666;">Animation Preview Area</span>
</div>
</div>
`;
});
componentRegistry.registerComponent('properties', (container, state) => {
container.element.innerHTML = `
<div style="padding: 20px; color: white; height: 100%; overflow-y: auto;">
<h3>${state.title}</h3>
<div style="margin-top: 16px;">
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: #ccc;">Duration (ms)</label>
<input type="number" value="1000" style="width: 100%; padding: 8px; background: #2d2d2d; border: 1px solid #444; border-radius: 4px; color: white;">
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 4px; font-size: 12px; color: #ccc;">Easing</label>
<select style="width: 100%; padding: 8px; background: #2d2d2d; border: 1px solid #444; border-radius: 4px; color: white;">
<option>Linear</option>
<option>Ease In</option>
<option>Ease Out</option>
<option>Ease In Out</option>
</select>
</div>
</div>
</div>
`;
});
componentRegistry.registerComponent('library', (container, state) => {
container.element.innerHTML = `
<div style="padding: 20px; color: white; height: 100%; overflow-y: auto;">
<h3>${state.title}</h3>
<div style="margin-top: 16px;">
<div style="background: #2d2d2d; padding: 12px; border-radius: 4px; margin-bottom: 8px; cursor: pointer;">Eye Blink</div>
<div style="background: #2d2d2d; padding: 12px; border-radius: 4px; margin-bottom: 8px; cursor: pointer;">Head Nod</div>
<div style="background: #2d2d2d; padding: 12px; border-radius: 4px; margin-bottom: 8px; cursor: pointer;">Wave</div>
<div style="background: #2d2d2d; padding: 12px; border-radius: 4px; margin-bottom: 8px; cursor: pointer;">Dance</div>
</div>
</div>
`;
});
componentRegistry.registerComponent('preview', (container, state) => {
container.element.innerHTML = `
<div style="padding: 20px; color: white; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<h3>${state.title}</h3>
<div style="margin-top: 20px; text-align: center;">
<span class="material-icons" style="font-size: 48px; color: #666;">play_circle</span>
<div style="margin-top: 8px; color: #666;">Click to preview</div>
</div>
</div>
`;
});
const layout = new GoldenLayout(config, componentRegistry, document.querySelector('#layout-container'));
layout.init();
}
// Start the initialization process
waitForGoldenLayout();
});
</script>
</body>
</html>

View File

@@ -81,7 +81,8 @@
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;"> <div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
<input type="checkbox" id="llm-toggle" style="width:auto;"> <input type="checkbox" id="llm-toggle" style="width:auto;">
<label for="llm-toggle" style="margin:0;cursor:pointer;">LLM voice loop</label> <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> <button id="btn-llm-cancel" class="danger" style="margin-left:auto;font-size:11px;padding:2px 8px;" title="Cancel active LLM request">✕ Stop</button>
<button id="btn-llm-clear" style="font-size:11px;padding:2px 8px;" title="Clear conversation history">↺ Clear</button>
</div> </div>
<div class="field"> <div class="field">
<label>Completions endpoint</label> <label>Completions endpoint</label>
@@ -281,6 +282,6 @@
<img id="photo-modal-img" src="" alt="Photo"> <img id="photo-modal-img" src="" alt="Photo">
</div> </div>
<script src="app.js"></script> <script src="commander.js"></script>
</body> </body>
</html> </html>

View File

@@ -59,7 +59,8 @@ function handleJiboEvent(body, txId) {
switch (body.Event) { switch (body.Event) {
case 'onHotWordHeard': case 'onHotWordHeard':
flashHotword(body.utterance || 'hey jibo', body.score); flashHotword(body.utterance || 'hey jibo', body.score);
if (document.getElementById('auto-listen-toggle').checked) doListen(); if (document.getElementById('auto-listen-toggle').checked)
post('/api/interrupt').then(() => doListen());
break; break;
case 'onStart': case 'onStart':
@@ -257,7 +258,7 @@ document.getElementById('btn-say').addEventListener('click', async () => {
}); });
document.getElementById('btn-say-cancel').addEventListener('click', () => { document.getElementById('btn-say-cancel').addEventListener('click', () => {
if (lastSayTx) post('/api/cancel', { txId: lastSayTx }); post('/api/say/cancel');
}); });
// ── Listen ──────────────────────────────────────────────────────────────────── // ── Listen ────────────────────────────────────────────────────────────────────
@@ -288,20 +289,28 @@ document.getElementById('btn-listen').addEventListener('click', doListen);
document.getElementById('btn-listen-cancel').addEventListener('click', () => { document.getElementById('btn-listen-cancel').addEventListener('click', () => {
clearListenTimeout(); clearListenTimeout();
if (lastListenTx) post('/api/cancel', { txId: lastListenTx }); post('/api/listen/cancel');
document.getElementById('listen-result').textContent = '(cancelled)'; document.getElementById('listen-result').textContent = '(cancelled)';
}); });
// ── Auto-listen + Voice AI ──────────────────────────────────────────────────── // ── Auto-listen + Voice AI ────────────────────────────────────────────────────
let llmHistory = []; // [{role:'user'|'assistant', content:string}] let llmHistory = []; // [{role:'user'|'assistant', content:string}]
let llmSessionMode = false; // true when server uses LLM_SESSION_KEY (OpenClaw session)
let llmTurnCount = 0;
function llmStatus(msg) { function llmStatus(msg) {
document.getElementById('llm-status').textContent = msg; document.getElementById('llm-status').textContent = msg;
} }
async function runLLMLoop(speechText) { async function runLLMLoop(speechText) {
llmHistory.push({ role: 'user', content: speechText }); // In session mode send only the latest message — history lives on the server.
// In history mode accumulate the full thread and send it each time.
const messages = llmSessionMode
? [{ role: 'user', content: speechText }]
: [...llmHistory, { role: 'user', content: speechText }];
if (!llmSessionMode) llmHistory.push({ role: 'user', content: speechText });
llmStatus('Thinking…'); llmStatus('Thinking…');
const endpoint = document.getElementById('llm-endpoint').value.trim(); const endpoint = document.getElementById('llm-endpoint').value.trim();
@@ -309,30 +318,45 @@ async function runLLMLoop(speechText) {
const systemPrompt = document.getElementById('llm-system-prompt').value.trim(); const systemPrompt = document.getElementById('llm-system-prompt').value.trim();
const r = await post('/api/llm/chat', { const r = await post('/api/llm/chat', {
messages: llmHistory, messages,
endpoint: endpoint || undefined, endpoint: endpoint || undefined,
model: model || undefined, model: model || undefined,
systemPrompt: systemPrompt || undefined, systemPrompt: systemPrompt || undefined,
}); });
if (!r || r.error) { if (!r || r.error) {
if (r?.error === 'cancelled') { llmStatus('Interrupted.'); if (!llmSessionMode) llmHistory.pop(); return; }
llmStatus('LLM error: ' + (r?.error || 'no response')); llmStatus('LLM error: ' + (r?.error || 'no response'));
llmHistory.pop(); // undo the user push so history stays consistent if (!llmSessionMode) llmHistory.pop();
return; return;
} }
const reply = r.reply; const reply = r.reply;
llmHistory.push({ role: 'assistant', content: reply }); if (!llmSessionMode) llmHistory.push({ role: 'assistant', content: reply });
llmStatus(`[${llmHistory.length / 2} turns] Last: "${reply.slice(0, 60)}${reply.length > 60 ? '…' : ''}"`); llmTurnCount++;
const modeTag = llmSessionMode ? 'session' : 'local';
llmStatus(`[${modeTag} · ${llmTurnCount} turns] Last: "${reply.slice(0, 50)}${reply.length > 50 ? '…' : ''}"`);
// Fill say box so user can see what Jibo is about to say // Fill say box so user can see what Jibo is about to say
document.getElementById('say-text').value = reply; document.getElementById('say-text').value = reply;
await post('/api/say', { text: reply }); const sayResult = await post('/api/say', { text: reply });
// If the reply ends with a question and wasn't interrupted, listen for the user's answer
const endsWithQuestion = /\?[^a-zA-Z0-9]*$/.test(reply.trim());
if (endsWithQuestion && !sayResult?.aborted && document.getElementById('llm-toggle').checked) {
doListen();
}
} }
document.getElementById('btn-llm-cancel').addEventListener('click', () => {
post('/api/llm/cancel');
llmStatus('Cancelled.');
});
document.getElementById('btn-llm-clear').addEventListener('click', () => { document.getElementById('btn-llm-clear').addEventListener('click', () => {
llmHistory = []; llmHistory = [];
llmStatus('Conversation cleared.'); llmTurnCount = 0;
llmStatus(llmSessionMode ? 'Session turn counter reset (server session persists).' : 'Conversation cleared.');
}); });
// ── Attention ───────────────────────────────────────────────────────────────── // ── Attention ─────────────────────────────────────────────────────────────────
@@ -566,26 +590,24 @@ function flashHotword(utterance, score) {
hotwordTimer = setTimeout(() => el.classList.remove('active'), 3000); hotwordTimer = setTimeout(() => el.classList.remove('active'), 3000);
} }
// ── Menu: Robot & Mode Selection ──────────────────────────────────────────── // ── Menu: Robot & Mode Selection ────────────────────────────────────────────
let selectedRobot = null; let selectedRobot = null;
let selectedMode = 'commander'; let selectedMode = 'commander';
// Get the selected robot and mode from sessionStorage
function initializeMode() { function initializeMode() {
selectedRobot = sessionStorage.getItem('selectedRobot') || 'jibo-001'; selectedRobot = sessionStorage.getItem('selectedRobot') || 'jibo-001';
selectedMode = sessionStorage.getItem('selectedMode') || 'commander'; selectedMode = sessionStorage.getItem('selectedMode') || 'commander';
} }
// Back to home button
document.getElementById('btn-back-home')?.addEventListener('click', () => { document.getElementById('btn-back-home')?.addEventListener('click', () => {
window.location.href = 'index.html'; window.location.href = 'index.html';
}); });
// ── Initialization ─────────────────────────────────────────────────────────── // ── Init ──────────────────────────────────────────────────────────────────────
connectWS();
initializeMode(); initializeMode();
connectWS();
// 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 => {
@@ -593,4 +615,8 @@ get('/api/config').then(cfg => {
if (cfg.llmEndpoint) document.getElementById('llm-endpoint').value = cfg.llmEndpoint; if (cfg.llmEndpoint) document.getElementById('llm-endpoint').value = cfg.llmEndpoint;
if (cfg.llmModel) document.getElementById('llm-model').value = cfg.llmModel; if (cfg.llmModel) document.getElementById('llm-model').value = cfg.llmModel;
if (cfg.llmSystemPrompt) document.getElementById('llm-system-prompt').value = cfg.llmSystemPrompt; if (cfg.llmSystemPrompt) document.getElementById('llm-system-prompt').value = cfg.llmSystemPrompt;
if (cfg.sessionMode) {
llmSessionMode = true;
llmStatus('Session mode (OpenClaw) — history managed server-side.');
}
}); });

View File

@@ -1,40 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VirtualLayout = exports.StyleConstants = exports.EventHub = exports.EventEmitter = exports.LayoutManager = exports.Stack = exports.RowOrColumn = exports.ContentItem = exports.ComponentItem = exports.GoldenLayout = exports.Tab = exports.Header = exports.DragSource = exports.BrowserPopout = exports.ComponentContainer = void 0;
const tslib_1 = require("tslib");
tslib_1.__exportStar(require("./ts/config/config"), exports);
tslib_1.__exportStar(require("./ts/config/resolved-config"), exports);
var component_container_1 = require("./ts/container/component-container");
Object.defineProperty(exports, "ComponentContainer", { enumerable: true, get: function () { return component_container_1.ComponentContainer; } });
var browser_popout_1 = require("./ts/controls/browser-popout");
Object.defineProperty(exports, "BrowserPopout", { enumerable: true, get: function () { return browser_popout_1.BrowserPopout; } });
var drag_source_1 = require("./ts/controls/drag-source");
Object.defineProperty(exports, "DragSource", { enumerable: true, get: function () { return drag_source_1.DragSource; } });
var header_1 = require("./ts/controls/header");
Object.defineProperty(exports, "Header", { enumerable: true, get: function () { return header_1.Header; } });
var tab_1 = require("./ts/controls/tab");
Object.defineProperty(exports, "Tab", { enumerable: true, get: function () { return tab_1.Tab; } });
tslib_1.__exportStar(require("./ts/errors/external-error"), exports);
var golden_layout_1 = require("./ts/golden-layout");
Object.defineProperty(exports, "GoldenLayout", { enumerable: true, get: function () { return golden_layout_1.GoldenLayout; } });
var component_item_1 = require("./ts/items/component-item");
Object.defineProperty(exports, "ComponentItem", { enumerable: true, get: function () { return component_item_1.ComponentItem; } });
var content_item_1 = require("./ts/items/content-item");
Object.defineProperty(exports, "ContentItem", { enumerable: true, get: function () { return content_item_1.ContentItem; } });
var row_or_column_1 = require("./ts/items/row-or-column");
Object.defineProperty(exports, "RowOrColumn", { enumerable: true, get: function () { return row_or_column_1.RowOrColumn; } });
var stack_1 = require("./ts/items/stack");
Object.defineProperty(exports, "Stack", { enumerable: true, get: function () { return stack_1.Stack; } });
var layout_manager_1 = require("./ts/layout-manager");
Object.defineProperty(exports, "LayoutManager", { enumerable: true, get: function () { return layout_manager_1.LayoutManager; } });
var event_emitter_1 = require("./ts/utils/event-emitter");
Object.defineProperty(exports, "EventEmitter", { enumerable: true, get: function () { return event_emitter_1.EventEmitter; } });
var event_hub_1 = require("./ts/utils/event-hub");
Object.defineProperty(exports, "EventHub", { enumerable: true, get: function () { return event_hub_1.EventHub; } });
tslib_1.__exportStar(require("./ts/utils/i18n-strings"), exports);
var style_constants_1 = require("./ts/utils/style-constants");
Object.defineProperty(exports, "StyleConstants", { enumerable: true, get: function () { return style_constants_1.StyleConstants; } });
tslib_1.__exportStar(require("./ts/utils/types"), exports);
var virtual_layout_1 = require("./ts/virtual-layout");
Object.defineProperty(exports, "VirtualLayout", { enumerable: true, get: function () { return virtual_layout_1.VirtualLayout; } });
//# sourceMappingURL=index.js.map

View File

@@ -1,319 +0,0 @@
.lm_root {
position: relative;
}
.lm_row > .lm_item {
float: left;
}
.lm_content {
overflow: hidden;
position: relative;
}
.lm_dragging,
.lm_dragging * {
cursor: move !important;
-webkit-user-select: none;
user-select: none;
}
.lm_maximised {
position: absolute;
top: 0;
left: 0;
z-index: 40;
}
.lm_maximise_placeholder {
display: none;
}
.lm_splitter {
position: relative;
z-index: 2;
touch-action: none;
}
.lm_splitter.lm_vertical .lm_drag_handle {
width: 100%;
position: absolute;
cursor: ns-resize;
touch-action: none;
-webkit-user-select: none;
user-select: none;
}
.lm_splitter.lm_horizontal {
float: left;
height: 100%;
}
.lm_splitter.lm_horizontal .lm_drag_handle {
height: 100%;
position: absolute;
cursor: ew-resize;
touch-action: none;
-webkit-user-select: none;
user-select: none;
}
.lm_header {
overflow: visible;
position: relative;
z-index: 1;
-webkit-user-select: none;
user-select: none;
}
.lm_header [class^=lm_] {
box-sizing: content-box !important;
}
.lm_header .lm_controls {
position: absolute;
right: 3px;
display: flex;
}
.lm_header .lm_controls > * {
cursor: pointer;
float: left;
width: 18px;
height: 18px;
text-align: center;
}
.lm_header .lm_tabs {
position: absolute;
display: flex;
}
.lm_header .lm_tab {
cursor: pointer;
float: left;
height: 14px;
margin-top: 1px;
padding: 0px 10px 5px;
padding-right: 25px;
position: relative;
touch-action: none;
}
.lm_header .lm_tab i {
width: 2px;
height: 19px;
position: absolute;
}
.lm_header .lm_tab i.lm_left {
top: 0;
left: -2px;
}
.lm_header .lm_tab i.lm_right {
top: 0;
right: -2px;
}
.lm_header .lm_tab .lm_title {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
}
.lm_header .lm_tab .lm_close_tab {
width: 14px;
height: 14px;
position: absolute;
top: 0;
right: 0;
text-align: center;
}
.lm_stack {
position: relative;
}
.lm_stack > .lm_items {
overflow: hidden;
}
.lm_stack.lm_left > .lm_items {
position: absolute;
left: 20px;
top: 0;
}
.lm_stack.lm_right > .lm_items {
position: absolute;
right: 20px;
top: 0;
}
.lm_stack.lm_right > .lm_header {
position: absolute;
right: 0;
top: 0;
}
.lm_stack.lm_bottom > .lm_items {
position: absolute;
bottom: 20px;
}
.lm_stack.lm_bottom > .lm_header {
position: absolute;
bottom: 0;
}
.lm_left.lm_stack .lm_header,
.lm_right.lm_stack .lm_header {
height: 100%;
}
.lm_left.lm_dragProxy .lm_header,
.lm_right.lm_dragProxy .lm_header,
.lm_left.lm_dragProxy .lm_items,
.lm_right.lm_dragProxy .lm_items {
float: left;
}
.lm_left.lm_dragProxy .lm_header,
.lm_right.lm_dragProxy .lm_header,
.lm_left.lm_stack .lm_header,
.lm_right.lm_stack .lm_header {
width: 20px;
vertical-align: top;
}
.lm_left.lm_dragProxy .lm_header .lm_tabs,
.lm_right.lm_dragProxy .lm_header .lm_tabs,
.lm_left.lm_stack .lm_header .lm_tabs,
.lm_right.lm_stack .lm_header .lm_tabs {
transform-origin: left top;
top: 0;
width: 1000px;
/*hack*/
}
.lm_left.lm_dragProxy .lm_header .lm_controls,
.lm_right.lm_dragProxy .lm_header .lm_controls,
.lm_left.lm_stack .lm_header .lm_controls,
.lm_right.lm_stack .lm_header .lm_controls {
bottom: 0;
flex-flow: column;
}
.lm_dragProxy.lm_left .lm_header .lm_tabs,
.lm_stack.lm_left .lm_header .lm_tabs {
transform: rotate(-90deg) scaleX(-1);
left: 0;
}
.lm_dragProxy.lm_left .lm_header .lm_tabs .lm_tab,
.lm_stack.lm_left .lm_header .lm_tabs .lm_tab {
transform: scaleX(-1);
margin-top: 1px;
}
.lm_dragProxy.lm_left .lm_header .lm_tabdropdown_list,
.lm_stack.lm_left .lm_header .lm_tabdropdown_list {
top: initial;
right: initial;
left: 20px;
}
.lm_dragProxy.lm_right .lm_content {
float: left;
}
.lm_dragProxy.lm_right .lm_header .lm_tabs,
.lm_stack.lm_right .lm_header .lm_tabs {
transform: rotate(90deg) scaleX(1);
left: 100%;
margin-left: 0;
}
.lm_dragProxy.lm_right .lm_header .lm_controls,
.lm_stack.lm_right .lm_header .lm_controls {
left: 3px;
}
.lm_dragProxy.lm_right .lm_header .lm_tabdropdown_list,
.lm_stack.lm_right .lm_header .lm_tabdropdown_list {
top: initial;
right: 20px;
}
.lm_dragProxy.lm_bottom .lm_header,
.lm_stack.lm_bottom .lm_header {
width: 100%;
}
.lm_dragProxy.lm_bottom .lm_header .lm_tab,
.lm_stack.lm_bottom .lm_header .lm_tab {
margin-top: 0;
border-top: none;
}
.lm_dragProxy.lm_bottom .lm_header .lm_controls,
.lm_stack.lm_bottom .lm_header .lm_controls {
top: 3px;
}
.lm_dragProxy.lm_bottom .lm_header .lm_tabdropdown_list,
.lm_stack.lm_bottom .lm_header .lm_tabdropdown_list {
top: initial;
bottom: 20px;
}
.lm_drop_tab_placeholder {
float: left;
width: 100px;
visibility: hidden;
}
.lm_header .lm_controls .lm_tabdropdown:before {
content: '';
width: 0;
height: 0;
vertical-align: middle;
display: inline-block;
border-top: 5px dashed;
border-right: 5px solid transparent;
border-left: 5px solid transparent;
color: white;
}
.lm_header .lm_tabdropdown_list {
position: absolute;
top: 20px;
right: 0;
z-index: 5;
overflow: hidden;
}
.lm_header .lm_tabdropdown_list .lm_tab {
clear: both;
padding-right: 10px;
margin: 0;
}
.lm_header .lm_tabdropdown_list .lm_tab .lm_title {
width: 100px;
}
.lm_header .lm_tabdropdown_list .lm_close_tab {
display: none !important;
}
/***********************************
* Drag Proxy
***********************************/
.lm_dragProxy {
position: absolute;
top: 0;
left: 0;
z-index: 30;
}
.lm_dragProxy .lm_header {
background: transparent;
}
.lm_dragProxy .lm_content {
border-top: none;
overflow: hidden;
}
.lm_dropTargetIndicator {
display: none;
position: absolute;
z-index: 35;
transition: all 200ms ease;
}
.lm_dropTargetIndicator .lm_inner {
width: 100%;
height: 100%;
position: relative;
top: 0;
left: 0;
}
.lm_transition_indicator {
display: none;
width: 20px;
height: 20px;
position: absolute;
top: 0;
left: 0;
z-index: 20;
}
.lm_popin {
width: 20px;
height: 20px;
position: absolute;
bottom: 0;
right: 0;
z-index: 9999;
}
.lm_popin > * {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.lm_popin > .lm_bg {
z-index: 10;
}
.lm_popin > .lm_icon {
z-index: 20;
}

View File

@@ -1,139 +0,0 @@
.lm_goldenlayout {
background: #000000;
}
.lm_content {
background: #222222;
border: 1px solid transparent;
}
.lm_dragProxy .lm_content {
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.9);
}
.lm_dropTargetIndicator {
box-shadow: inset 0 0 30px #000000;
outline: 1px dashed #cccccc;
}
.lm_dropTargetIndicator .lm_inner {
background: #000000;
opacity: 0.2;
}
.lm_splitter {
background: #000000;
opacity: 0.001;
transition: opacity 200ms ease;
}
.lm_splitter:hover,
.lm_splitter.lm_dragging {
background: #444444;
opacity: 1;
}
.lm_header {
height: 20px;
}
.lm_header .lm_tab {
font-family: Arial, sans-serif;
font-size: 12px;
color: #999999;
background: #111111;
box-shadow: 2px -2px 2px rgba(0, 0, 0, 0.3);
margin-right: 2px;
padding-bottom: 2px;
padding-top: 2px;
/*.lm_title // Present in LIGHT Theme
{
padding-top:1px;
}*/
}
.lm_header .lm_tab .lm_close_tab {
width: 11px;
height: 11px;
background-image: url('../../img/lm_close_white.png');
background-position: center center;
background-repeat: no-repeat;
top: 4px;
right: 6px;
opacity: 0.4;
}
.lm_header .lm_tab .lm_close_tab:hover {
opacity: 1;
}
.lm_header .lm_tab.lm_active {
border-bottom: none;
box-shadow: 0 -2px 2px #000000;
padding-bottom: 3px;
}
.lm_header .lm_tab.lm_active .lm_close_tab {
opacity: 1;
}
.lm_header .lm_tab.lm_active.lm_focused {
background-color: #354be3;
}
.lm_dragProxy.lm_right .lm_header .lm_tab.lm_active,
.lm_stack.lm_right .lm_header .lm_tab.lm_active {
box-shadow: 2px -2px 2px #000000;
}
.lm_dragProxy.lm_bottom .lm_header .lm_tab,
.lm_stack.lm_bottom .lm_header .lm_tab {
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.3);
}
.lm_dragProxy.lm_bottom .lm_header .lm_tab.lm_active,
.lm_stack.lm_bottom .lm_header .lm_tab.lm_active {
box-shadow: 0 2px 2px #000000;
}
.lm_selected .lm_header {
background-color: #452500;
}
.lm_tab:hover,
.lm_tab.lm_active {
background: #222222;
color: #dddddd;
}
.lm_header .lm_controls .lm_tabdropdown:before {
color: #ffffff;
}
.lm_controls > * {
position: relative;
background-position: center center;
background-repeat: no-repeat;
opacity: 0.4;
transition: opacity 300ms ease;
}
.lm_controls > *:hover {
opacity: 1;
}
.lm_controls .lm_popout {
background-image: url('../../img/lm_popout_white.png');
}
.lm_controls .lm_maximise {
background-image: url('../../img/lm_maximise_white.png');
}
.lm_controls .lm_close {
background-image: url('../../img/lm_close_white.png');
}
.lm_maximised .lm_header {
background-color: #000000;
}
.lm_maximised .lm_controls .lm_maximise {
background-image: url('../../img/lm_minimize_white.png');
}
.lm_transition_indicator {
background-color: #000000;
border: 1px dashed #555555;
}
.lm_popin {
cursor: pointer;
}
.lm_popin .lm_bg {
background: #ffffff;
opacity: 0.3;
}
.lm_popin .lm_icon {
background-image: url('../../img/lm_popin_white.png');
background-position: center center;
background-repeat: no-repeat;
border-left: 1px solid #eeeeee;
border-top: 1px solid #eeeeee;
opacity: 0.7;
}
.lm_popin:hover .lm_icon {
opacity: 1;
}

View File

@@ -253,11 +253,11 @@
<span class="mode-icon">🎮</span> <span class="mode-icon">🎮</span>
<span class="mode-label">Commander</span> <span class="mode-label">Commander</span>
</button> </button>
<button class="mode-btn" data-mode="animator" title="Animator Mode"> <button class="mode-btn" data-mode="animator" title="Animator Mode" style="display:none;">
<span class="mode-icon">🎬</span> <span class="mode-icon">🎬</span>
<span class="mode-label">Animator</span> <span class="mode-label">Animator</span>
</button> </button>
<button class="mode-btn" data-mode="telepresence" title="Telepresence Mode"> <button class="mode-btn" data-mode="telepresence" title="Telepresence Mode" style="display:none;">
<span class="mode-icon">👁</span> <span class="mode-icon">👁</span>
<span class="mode-label">Telepresence</span> <span class="mode-label">Telepresence</span>
</button> </button>

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1151
server.js

File diff suppressed because it is too large Load Diff