1 Commits

Author SHA1 Message Date
3910255f30 Needs fixing , animator UI 2026-05-12 00:30:45 +03:00
14 changed files with 8737 additions and 451 deletions

View File

@@ -4,12 +4,8 @@ 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 — value is sent as "Bearer <value>", do NOT include the word "Bearer" here # Optional API key (sent as Bearer token) — leave blank for local servers
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,19 +1,51 @@
{ {
"name": "re-commander", "name": "re-commander",
"version": "2.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "re-commander", "name": "re-commander",
"version": "2.0.0", "version": "1.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",
"rom-control": "^2.0.0", "golden-layout": "^2.6.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",
@@ -372,6 +404,12 @@
"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",
@@ -455,6 +493,12 @@
"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",
@@ -630,18 +674,6 @@
"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,15 +1,17 @@
{ {
"name": "re-commander", "name": "re-commander",
"version": "2.0.0", "version": "1.0.0",
"description": "Jibo ROM Commander — built on the rom-control module", "description": "Jibo ROM Commander — local Node.js recreation using port 8160",
"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",
"rom-control": "^2.0.2", "golden-layout": "^2.6.0",
"ws": "^8.14.2" "ws": "^8.14.2"
} }
} }

694
public/animator-simple.html Normal file
View File

@@ -0,0 +1,694 @@
<!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

@@ -59,8 +59,7 @@ 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) if (document.getElementById('auto-listen-toggle').checked) doListen();
post('/api/interrupt').then(() => doListen());
break; break;
case 'onStart': case 'onStart':
@@ -258,7 +257,7 @@ document.getElementById('btn-say').addEventListener('click', async () => {
}); });
document.getElementById('btn-say-cancel').addEventListener('click', () => { document.getElementById('btn-say-cancel').addEventListener('click', () => {
post('/api/say/cancel'); if (lastSayTx) post('/api/cancel', { txId: lastSayTx });
}); });
// ── Listen ──────────────────────────────────────────────────────────────────── // ── Listen ────────────────────────────────────────────────────────────────────
@@ -289,28 +288,20 @@ document.getElementById('btn-listen').addEventListener('click', doListen);
document.getElementById('btn-listen-cancel').addEventListener('click', () => { document.getElementById('btn-listen-cancel').addEventListener('click', () => {
clearListenTimeout(); clearListenTimeout();
post('/api/listen/cancel'); if (lastListenTx) post('/api/cancel', { txId: lastListenTx });
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) {
// In session mode send only the latest message — history lives on the server. llmHistory.push({ role: 'user', content: speechText });
// 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();
@@ -318,45 +309,30 @@ 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, messages: llmHistory,
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'));
if (!llmSessionMode) llmHistory.pop(); llmHistory.pop(); // undo the user push so history stays consistent
return; return;
} }
const reply = r.reply; const reply = r.reply;
if (!llmSessionMode) llmHistory.push({ role: 'assistant', content: reply }); llmHistory.push({ role: 'assistant', content: reply });
llmTurnCount++; llmStatus(`[${llmHistory.length / 2} turns] Last: "${reply.slice(0, 60)}${reply.length > 60 ? '…' : ''}"`);
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;
const sayResult = await post('/api/say', { text: reply }); 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 = [];
llmTurnCount = 0; llmStatus('Conversation cleared.');
llmStatus(llmSessionMode ? 'Session turn counter reset (server session persists).' : 'Conversation cleared.');
}); });
// ── Attention ───────────────────────────────────────────────────────────────── // ── Attention ─────────────────────────────────────────────────────────────────
@@ -590,24 +566,26 @@ 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';
}); });
// ── Init ────────────────────────────────────────────────────────────────────── // ── Initialization ───────────────────────────────────────────────────────────
initializeMode();
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 => {
@@ -615,8 +593,4 @@ 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

@@ -81,8 +81,7 @@
<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-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="margin-left:auto;font-size:11px;padding:2px 8px;" title="Clear conversation history">↺ Clear</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>
@@ -282,6 +281,6 @@
<img id="photo-modal-img" src="" alt="Photo"> <img id="photo-modal-img" src="" alt="Photo">
</div> </div>
<script src="commander.js"></script> <script src="app.js"></script>
</body> </body>
</html> </html>

40
public/golden-layout.js Normal file
View File

@@ -0,0 +1,40 @@
"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

@@ -0,0 +1,319 @@
.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

@@ -0,0 +1,139 @@
.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" style="display:none;"> <button class="mode-btn" data-mode="animator" title="Animator Mode">
<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" style="display:none;"> <button class="mode-btn" data-mode="telepresence" title="Telepresence Mode">
<span class="mode-icon">👁</span> <span class="mode-icon">👁</span>
<span class="mode-label">Telepresence</span> <span class="mode-label">Telepresence</span>
</button> </button>

View File

6597
public/theatre-core.js Normal file

File diff suppressed because it is too large Load Diff

105
public/theatre-studio.js Normal file

File diff suppressed because one or more lines are too long

1061
server.js

File diff suppressed because it is too large Load Diff