Compare commits
1 Commits
Home-Menu
...
07ef1a2fad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07ef1a2fad |
@@ -4,8 +4,12 @@ LLM_ENDPOINT=http://localhost:11434/v1/chat/completions
|
||||
# Model name passed to the endpoint
|
||||
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=
|
||||
|
||||
# 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
|
||||
LLM_SYSTEM_PROMPT=You are Jibo, a friendly social robot. Keep responses brief and conversational.
|
||||
|
||||
45
package-lock.json
generated
45
package-lock.json
generated
@@ -8,44 +8,11 @@
|
||||
"name": "re-commander",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@theatre/core": "^0.7.2",
|
||||
"@theatre/studio": "^0.7.2",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^4.18.2",
|
||||
"golden-layout": "^2.6.0",
|
||||
"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": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -404,12 +371,6 @@
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -493,12 +454,6 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@theatre/core": "^0.7.2",
|
||||
"@theatre/studio": "^0.7.2",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^4.18.2",
|
||||
"golden-layout": "^2.6.0",
|
||||
"ws": "^8.14.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,554 +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>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
header {
|
||||
flex-shrink: 0;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.header-bar h1 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.header-status span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── Toolbar ── */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-group button {
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.toolbar-group button:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Top toolbar buttons - File, Edit, Playback, etc */
|
||||
.toolbar-group:nth-child(1) button,
|
||||
.toolbar-group:nth-child(2) button,
|
||||
.toolbar-group:nth-child(3) button,
|
||||
.toolbar-group:nth-child(5) button {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 2px solid var(--accent);
|
||||
background: linear-gradient(135deg, var(--accent)20, var(--accent)5);
|
||||
color: var(--accent);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.toolbar-group:nth-child(1) button:hover,
|
||||
.toolbar-group:nth-child(2) button:hover,
|
||||
.toolbar-group:nth-child(3) button:hover,
|
||||
.toolbar-group:nth-child(5) button:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(255,255,0,0.3);
|
||||
}
|
||||
|
||||
.toolbar-sep {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: var(--border);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ── Main Container ── */
|
||||
.main-container {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr 300px;
|
||||
gap: 1px;
|
||||
background: var(--border);
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Left Sidebar ── */
|
||||
.left-panel {
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 12px;
|
||||
background: var(--surface2);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.library-item {
|
||||
padding: 10px;
|
||||
margin-bottom: 6px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.library-item:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.library-item.selected {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* ── Center Area ── */
|
||||
.center-panel {
|
||||
background: #000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.center-tabs {
|
||||
display: flex;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.center-tab {
|
||||
padding: 10px 16px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.center-tab.active {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.center-tab:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.center-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--muted);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
/* ── Right Sidebar ── */
|
||||
.right-panel {
|
||||
background: var(--surface);
|
||||
border-left: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.right-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.right-tab {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right-tab.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
.right-tab:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.right-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.right-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inspector-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.inspector-field label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inspector-field input,
|
||||
.inspector-field select,
|
||||
.inspector-field textarea {
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.inspector-field input:focus,
|
||||
.inspector-field select:focus,
|
||||
.inspector-field textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 6px;
|
||||
margin-bottom: 4px;
|
||||
background: var(--surface2);
|
||||
border-left: 2px solid var(--accent);
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ── Bottom Status Bar ── */
|
||||
.status-bar {
|
||||
flex-shrink: 0;
|
||||
background: var(--surface2);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 8px 16px;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ── Header ── -->
|
||||
<header>
|
||||
<div class="header-bar">
|
||||
<div id="status-dot"></div>
|
||||
<h1>Re<span>-Commander</span> — Animator</h1>
|
||||
<div class="header-status">
|
||||
<span id="status-label">Connecting…</span>
|
||||
<span id="hotword-indicator"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<!-- File Operations -->
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-new">📄 New</button>
|
||||
<button id="btn-open">📂 Open</button>
|
||||
<button id="btn-save">💾 Save</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<!-- Edit -->
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-undo">↶ Undo</button>
|
||||
<button id="btn-redo">↷ Redo</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<!-- Animation Control -->
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-play" style="background:var(--success);border-color:var(--success);">Play</button>
|
||||
<button id="btn-pause">Pause</button>
|
||||
<button id="btn-stop" class="danger">Stop</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-timeline">Timeline</button>
|
||||
<button id="btn-preview">Preview</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-spacer"></div>
|
||||
|
||||
<!-- Right Side -->
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-settings">Settings</button>
|
||||
<button id="btn-back-home">← Home</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ── Main Content ── -->
|
||||
<div class="main-container">
|
||||
|
||||
<!-- Left Sidebar - Library -->
|
||||
<div class="left-panel">
|
||||
<div class="panel-header">Animation Library</div>
|
||||
<div class="panel-content">
|
||||
<div class="library-item">Eye_Blink_01</div>
|
||||
<div class="library-item">Eye_Blink_02</div>
|
||||
<div class="library-item">Eye_Double_Blink</div>
|
||||
<div class="library-item">eye_happy_01</div>
|
||||
<div class="library-item">eye_happy_02</div>
|
||||
<div class="library-item">eye_sad_01</div>
|
||||
<div class="library-item">eye_scared_00</div>
|
||||
<div class="library-item">Eye_Curious_01</div>
|
||||
<div class="library-item">Eye_Look_Center</div>
|
||||
<div class="library-item">Eye_Look_Left</div>
|
||||
<div class="library-item">Eye_Look_Right</div>
|
||||
<div class="library-item">Eye_Look_Up</div>
|
||||
<div class="library-item">Eye_Look_Down</div>
|
||||
<div class="library-item">Gesture_Wave</div>
|
||||
<div class="library-item">Gesture_Nod</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center - Canvas/Preview Area -->
|
||||
<div class="center-panel">
|
||||
<div class="center-tabs">
|
||||
<div class="center-tab active" data-tab="canvas">Canvas</div>
|
||||
<div class="center-tab" data-tab="preview">🎬 Preview</div>
|
||||
</div>
|
||||
<div class="center-content">
|
||||
Imagine a 3d jibo model in a theater kinda thing
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar - Properties & Log -->
|
||||
<div class="right-panel">
|
||||
<div class="right-tabs">
|
||||
<button class="right-tab active" data-panel="inspector">Inspector</button>
|
||||
<button class="right-tab" data-panel="properties">Properties</button>
|
||||
<button class="right-tab" data-panel="log">Log</button>
|
||||
</div>
|
||||
|
||||
<!-- Inspector Panel -->
|
||||
<div class="right-content active" id="inspector-panel">
|
||||
<div class="inspector-field">
|
||||
<label>Animation Name</label>
|
||||
<input type="text" placeholder="Select animation...">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Duration (ms)</label>
|
||||
<input type="number" value="500" min="0">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Repeat Count</label>
|
||||
<input type="number" value="1" min="1">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Delay (ms)</label>
|
||||
<input type="number" value="0" min="0" step="100">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Easing</label>
|
||||
<select>
|
||||
<option>Linear</option>
|
||||
<option>Ease In</option>
|
||||
<option>Ease Out</option>
|
||||
<option>Ease In Out</option>
|
||||
</select>
|
||||
</div>
|
||||
<button style="width:100%; padding:10px; margin-top:12px; background:var(--accent); color:#000; border:none; border-radius:4px; font-weight:600; cursor:pointer;">✨ Apply Animation</button>
|
||||
</div>
|
||||
|
||||
<!-- Properties Panel -->
|
||||
<div class="right-content" id="properties-panel">
|
||||
<div class="inspector-field">
|
||||
<label>Preview Mode</label>
|
||||
<select>
|
||||
<option>Normal</option>
|
||||
<option>Loop</option>
|
||||
<option>Once</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Resolution</label>
|
||||
<select>
|
||||
<option>High</option>
|
||||
<option>Medium</option>
|
||||
<option>Low</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Camera Angle</label>
|
||||
<input type="range" min="0" max="360" value="0">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Playback Speed</label>
|
||||
<input type="range" min="0.5" max="2" step="0.1" value="1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Panel -->
|
||||
<div class="right-content" id="log-panel">
|
||||
<div id="event-log" style="font-size:11px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Status Bar ── -->
|
||||
<div class="status-bar">
|
||||
<div class="status-item">
|
||||
<span>Mode:</span>
|
||||
<span class="status-value">Virtual Robot</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>Animation:</span>
|
||||
<span class="status-value" id="status-animation">None</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>Status:</span>
|
||||
<span class="status-value" id="status-playback">Ready</span>
|
||||
</div>
|
||||
<div class="status-item" style="margin-left:auto;">
|
||||
<span id="status-info">Ready for animation</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="animator.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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>
|
||||
@@ -1,416 +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>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<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">
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#app-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
header {
|
||||
flex-shrink: 0;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
#status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--muted);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
#status-dot.ok {
|
||||
background: var(--success);
|
||||
box-shadow: 0 0 6px var(--success);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#status-label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ── Toolbar ── */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-group button {
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.toolbar-group button:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Top toolbar buttons - File, Edit, Playback, etc */
|
||||
.toolbar-group:nth-child(1) button,
|
||||
.toolbar-group:nth-child(2) button,
|
||||
.toolbar-group:nth-child(3) button,
|
||||
.toolbar-group:nth-child(4) button,
|
||||
.toolbar-group:nth-child(6) button {
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
border: 2px solid var(--accent);
|
||||
background: linear-gradient(135deg, var(--accent)20, var(--accent)5);
|
||||
color: var(--accent);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.toolbar-group:nth-child(1) button:hover,
|
||||
.toolbar-group:nth-child(2) button:hover,
|
||||
.toolbar-group:nth-child(3) button:hover,
|
||||
.toolbar-group:nth-child(4) button:hover,
|
||||
.toolbar-group:nth-child(6) button:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 8px rgba(255,255,0,0.3);
|
||||
}
|
||||
|
||||
.toolbar-sep {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: var(--border);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* ── GoldenLayout Customization ── */
|
||||
#layout-root {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gl_container {
|
||||
background: var(--surface) !important;
|
||||
}
|
||||
|
||||
.gl_tab {
|
||||
background: var(--surface2) !important;
|
||||
color: var(--text) !important;
|
||||
border-bottom: 2px solid var(--border) !important;
|
||||
padding: 8px 12px !important;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.gl_tab.gl_active {
|
||||
background: var(--accent) !important;
|
||||
color: #000 !important;
|
||||
border-bottom: 2px solid var(--accent) !important;
|
||||
}
|
||||
|
||||
.gl_tab:hover {
|
||||
background: var(--surface) !important;
|
||||
}
|
||||
|
||||
.gl_tabBar {
|
||||
background: var(--surface2) !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
.gl_header {
|
||||
background: var(--surface) !important;
|
||||
border-bottom: 1px solid var(--border) !important;
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
.gl_splitter {
|
||||
background: var(--border) !important;
|
||||
}
|
||||
|
||||
/* ── Panel Content ── */
|
||||
.component-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.component-content > div:first-child {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.library-item {
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.library-item:hover {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.library-item.selected {
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.inspector-field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.inspector-field label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.inspector-field input,
|
||||
.inspector-field select,
|
||||
.inspector-field textarea {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.inspector-field input:focus,
|
||||
.inspector-field select:focus,
|
||||
.inspector-field textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 4px rgba(255, 255, 0, 0.3);
|
||||
}
|
||||
|
||||
.inspector-field button {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
margin-top: 8px;
|
||||
background: var(--accent);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.inspector-field button:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 2px 6px rgba(255,255,0,0.4);
|
||||
}
|
||||
|
||||
.canvas-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 48px;
|
||||
color: var(--muted);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
#event-log {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 11px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 2px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.log-entry .ts { color: var(--muted); }
|
||||
.log-entry .evt { color: var(--accent); }
|
||||
.log-entry .evt-error { color: var(--danger); }
|
||||
.log-entry .evt-motion { color: var(--warn); }
|
||||
.log-entry .evt-entity { color: var(--success); }
|
||||
.log-entry .evt-success { color: var(--success); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app-container">
|
||||
<!-- Header -->
|
||||
<header>
|
||||
<div id="status-dot"></div>
|
||||
<h1>Re-Commander — Animator</h1>
|
||||
<span id="status-label">Connecting…</span>
|
||||
</header>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-new">📄 New</button>
|
||||
<button id="btn-open">📂 Open</button>
|
||||
<button id="btn-save">💾 Save</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-undo">↶ Undo</button>
|
||||
<button id="btn-redo">↷ Redo</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-play" style="background:var(--success);border-color:var(--success);color:#000;">▶ Play</button>
|
||||
<button id="btn-pause">⏸ Pause</button>
|
||||
<button id="btn-stop" class="danger">⏹ Stop</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-timeline">📊 Timeline</button>
|
||||
<button id="btn-preview">👁 Preview</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-spacer"></div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<button id="btn-settings">⚙️ Settings</button>
|
||||
<button id="btn-back-home">← Home</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GoldenLayout Container -->
|
||||
<div id="layout-root"></div>
|
||||
</div>
|
||||
|
||||
<!-- Library Component Template -->
|
||||
<template id="tpl-library">
|
||||
<div class="component-content">
|
||||
<div id="library-list"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Canvas Component Template -->
|
||||
<template id="tpl-canvas">
|
||||
<div class="component-content">
|
||||
<div class="canvas-placeholder">🤖</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Inspector Component Template -->
|
||||
<template id="tpl-inspector">
|
||||
<div class="component-content">
|
||||
<div>
|
||||
<div class="inspector-field">
|
||||
<label>Animation Name</label>
|
||||
<input type="text" placeholder="Select animation...">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Duration (ms)</label>
|
||||
<input type="number" value="500" min="0">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Repeat Count</label>
|
||||
<input type="number" value="1" min="1">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Delay (ms)</label>
|
||||
<input type="number" value="0" min="0" step="100">
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<label>Easing</label>
|
||||
<select>
|
||||
<option>Linear</option>
|
||||
<option>Ease In</option>
|
||||
<option>Ease Out</option>
|
||||
<option>Ease In Out</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inspector-field">
|
||||
<button>✨ Apply Animation</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Log Component Template -->
|
||||
<template id="tpl-log">
|
||||
<div class="component-content">
|
||||
<div id="event-log"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/golden-layout@2/dist/umd/index.js" defer></script>
|
||||
<script src="animator.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,323 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// ── WebSocket to server ──────────────────────────────────────────────────────
|
||||
|
||||
let ws;
|
||||
let connected = false;
|
||||
let sessionActive = false;
|
||||
let videoActive = false;
|
||||
|
||||
function connectWS() {
|
||||
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
ws = new WebSocket(`${proto}://${location.host}/ws`);
|
||||
|
||||
ws.onopen = () => {
|
||||
connected = true;
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
connected = false;
|
||||
setStatus(false, 'Disconnected — reconnecting…');
|
||||
setTimeout(connectWS, 2000);
|
||||
};
|
||||
|
||||
ws.onerror = () => {};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
let msg;
|
||||
try { msg = JSON.parse(e.data); } catch { return; }
|
||||
handleServerMessage(msg);
|
||||
};
|
||||
}
|
||||
|
||||
function handleServerMessage(msg) {
|
||||
if (msg.type === 'status') {
|
||||
sessionActive = !!msg.sessionID;
|
||||
setStatus(msg.connected && sessionActive,
|
||||
msg.connected
|
||||
? (sessionActive ? 'Connected • ' + msg.sessionID.slice(0, 8) + '…' : 'Connecting…')
|
||||
: 'Disconnected');
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.type === 'jiboEvent') {
|
||||
handleJiboEvent(msg.body, msg.txId);
|
||||
}
|
||||
}
|
||||
|
||||
function handleJiboEvent(body, txId) {
|
||||
if (!body) return;
|
||||
const evt = body.Event || body.ResponseString || '?';
|
||||
|
||||
if (evt === 'onVideoReady' && body.URI) {
|
||||
startVideoFeed(body.URI);
|
||||
}
|
||||
|
||||
logEvent(evt, body, txId);
|
||||
}
|
||||
|
||||
// ── REST helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function api(method, path, body) {
|
||||
try {
|
||||
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
||||
if (body) opts.body = JSON.stringify(body);
|
||||
const res = await fetch(path, opts);
|
||||
return await res.json();
|
||||
} catch (err) {
|
||||
logEvent('api-error: ' + path, { error: err.message }, null);
|
||||
}
|
||||
}
|
||||
|
||||
const post = (path, body) => api('POST', path, body);
|
||||
const get = (path) => api('GET', path);
|
||||
|
||||
// ── Status ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function setStatus(ok, label) {
|
||||
const dot = document.getElementById('status-dot');
|
||||
const lbl = document.getElementById('status-label');
|
||||
if (dot) dot.className = ok ? 'ok' : '';
|
||||
if (lbl) lbl.textContent = label;
|
||||
}
|
||||
|
||||
// ── Event Log ────────────────────────────────────────────────────────────────
|
||||
|
||||
const MAX_LOG = 100;
|
||||
|
||||
function logEvent(eventName, body, txId) {
|
||||
const log = document.getElementById('event-log');
|
||||
const now = new Date().toLocaleTimeString();
|
||||
|
||||
// Pick color class based on event type
|
||||
let cls = 'evt';
|
||||
if (eventName.toLowerCase().includes('error')) cls = 'evt-error';
|
||||
else if (eventName.toLowerCase().includes('play')) cls = 'evt-success';
|
||||
else if (eventName.toLowerCase().includes('stop')) cls = 'evt-error';
|
||||
else if (eventName.toLowerCase().includes('selected')) cls = 'evt-entity';
|
||||
|
||||
// Extract detail from body
|
||||
let detail = '';
|
||||
if (body && body.animation) detail = ' "' + body.animation + '"';
|
||||
else if (body && body.error) detail = ' — ' + body.error;
|
||||
else if (body && body.uri) detail = ' ' + body.uri;
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'log-entry';
|
||||
el.innerHTML = `<span class="ts">${now}</span> <span class="${cls}">${eventName}</span>${detail}`;
|
||||
|
||||
if (log) {
|
||||
log.prepend(el);
|
||||
// Trim log
|
||||
while (log.children.length > MAX_LOG) log.removeChild(log.lastChild);
|
||||
}
|
||||
|
||||
console.log(`[${now}]`, eventName, body);
|
||||
}
|
||||
|
||||
// ── Video Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
function startVideoFeed(uri) {
|
||||
logEvent('Video feed ready', { uri }, null);
|
||||
}
|
||||
|
||||
function stopVideoFeed() {
|
||||
logEvent('Video stopped', {}, null);
|
||||
}
|
||||
|
||||
// ── GoldenLayout Setup ───────────────────────────────────────────────────────
|
||||
|
||||
const layoutConfig = {
|
||||
root: {
|
||||
type: 'row',
|
||||
children: [
|
||||
{
|
||||
type: 'component',
|
||||
componentType: 'library',
|
||||
componentState: { title: 'Animation Library' },
|
||||
width: 20,
|
||||
title: 'Library'
|
||||
},
|
||||
{
|
||||
type: 'column',
|
||||
children: [
|
||||
{
|
||||
type: 'component',
|
||||
componentType: 'canvas',
|
||||
componentState: { title: 'Canvas' },
|
||||
title: 'Canvas',
|
||||
height: 70
|
||||
}
|
||||
],
|
||||
width: 60
|
||||
},
|
||||
{
|
||||
type: 'column',
|
||||
children: [
|
||||
{
|
||||
type: 'stack',
|
||||
activeItemIndex: 0,
|
||||
width: 20,
|
||||
children: [
|
||||
{
|
||||
type: 'component',
|
||||
componentType: 'inspector',
|
||||
componentState: { title: 'Inspector' },
|
||||
title: 'Inspector'
|
||||
},
|
||||
{
|
||||
type: 'component',
|
||||
componentType: 'log',
|
||||
componentState: { title: 'Log' },
|
||||
title: 'Log'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// ── Initialization ──────────────────────────────────────────────────────────
|
||||
|
||||
function setupComponentRegistry() {
|
||||
const componentRegistry = new GoldenLayout.ComponentRegistry();
|
||||
|
||||
componentRegistry.registerComponent('library', (container, state) => {
|
||||
const content = document.querySelector('#tpl-library').content.cloneNode(true);
|
||||
container.element.appendChild(content);
|
||||
|
||||
const libraryList = container.element.querySelector('#library-list');
|
||||
const animations = [
|
||||
'Eye_Blink_01', 'Eye_Blink_02', 'Eye_Double_Blink', 'eye_happy_01', 'eye_happy_02',
|
||||
'eye_sad_01', 'eye_scared_00', 'Eye_Curious_01', 'Eye_Look_Center', 'Eye_Look_Left',
|
||||
'Eye_Look_Right', 'Eye_Look_Up', 'Eye_Look_Down', 'Gesture_Wave', 'Gesture_Nod'
|
||||
];
|
||||
|
||||
animations.forEach(anim => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'library-item';
|
||||
item.textContent = anim;
|
||||
item.addEventListener('click', () => {
|
||||
document.querySelectorAll('.library-item').forEach(i => i.classList.remove('selected'));
|
||||
item.classList.add('selected');
|
||||
|
||||
const inspectorInput = document.querySelector('.inspector-field input');
|
||||
if (inspectorInput) inspectorInput.value = anim;
|
||||
|
||||
logEvent('Animation selected', { animation: anim }, null);
|
||||
});
|
||||
libraryList.appendChild(item);
|
||||
});
|
||||
});
|
||||
|
||||
componentRegistry.registerComponent('canvas', (container, state) => {
|
||||
const content = document.querySelector('#tpl-canvas').content.cloneNode(true);
|
||||
container.element.appendChild(content);
|
||||
});
|
||||
|
||||
componentRegistry.registerComponent('inspector', (container, state) => {
|
||||
const content = document.querySelector('#tpl-inspector').content.cloneNode(true);
|
||||
container.element.appendChild(content);
|
||||
|
||||
const applyBtn = container.element.querySelector('.inspector-field button');
|
||||
if (applyBtn) {
|
||||
applyBtn.addEventListener('click', async () => {
|
||||
const animName = container.element.querySelector('.inspector-field input').value;
|
||||
if (!animName) {
|
||||
alert('Please select an animation');
|
||||
return;
|
||||
}
|
||||
|
||||
const inputs = container.element.querySelectorAll('.inspector-field input');
|
||||
const duration = parseInt(inputs[1]?.value) || 500;
|
||||
const repeat = parseInt(inputs[2]?.value) || 1;
|
||||
const delay = parseInt(inputs[3]?.value) || 0;
|
||||
|
||||
await post('/api/display/anim', { name: animName, duration, repeat, delay });
|
||||
logEvent('Applied animation', { animation: animName, duration, repeat, delay }, null);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
componentRegistry.registerComponent('log', (container, state) => {
|
||||
const content = document.querySelector('#tpl-log').content.cloneNode(true);
|
||||
container.element.appendChild(content);
|
||||
});
|
||||
|
||||
return componentRegistry;
|
||||
}
|
||||
|
||||
function setupToolbarButtons() {
|
||||
const btn = (id, fn) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener('click', fn);
|
||||
};
|
||||
|
||||
btn('btn-new', () => alert('New animation - Coming soon'));
|
||||
btn('btn-open', () => alert('Open animation - Coming soon'));
|
||||
btn('btn-save', () => alert('Save animation - Coming soon'));
|
||||
btn('btn-undo', () => logEvent('Undo', {}, null));
|
||||
btn('btn-redo', () => logEvent('Redo', {}, null));
|
||||
|
||||
btn('btn-play', async () => {
|
||||
const inspectorInput = document.querySelector('.inspector-field input');
|
||||
const animName = inspectorInput ? inspectorInput.value : 'greeting';
|
||||
await post('/api/display/anim', { name: animName });
|
||||
logEvent('Playing animation', { animation: animName }, null);
|
||||
});
|
||||
|
||||
btn('btn-pause', () => {
|
||||
logEvent('Pause animation', {}, null);
|
||||
});
|
||||
|
||||
btn('btn-stop', () => {
|
||||
post('/api/cancel', {});
|
||||
logEvent('Stop animation', {}, null);
|
||||
});
|
||||
|
||||
btn('btn-timeline', () => alert('Timeline view - Coming soon'));
|
||||
|
||||
btn('btn-preview', async () => {
|
||||
await post('/api/video/stop', {});
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await post('/api/video/start', { duration: 0 });
|
||||
logEvent('Preview video started', {}, null);
|
||||
});
|
||||
|
||||
btn('btn-settings', () => alert('Settings - Coming soon'));
|
||||
|
||||
btn('btn-back-home', () => {
|
||||
post('/api/video/stop', {});
|
||||
window.location.href = 'index.html';
|
||||
});
|
||||
}
|
||||
|
||||
// ── Initialization ──────────────────────────────────────────────────────────
|
||||
|
||||
function waitForGoldenLayout(callback) {
|
||||
if (typeof GoldenLayout === 'undefined') {
|
||||
setTimeout(() => waitForGoldenLayout(callback), 50);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
waitForGoldenLayout(() => {
|
||||
// Setup components
|
||||
const componentRegistry = setupComponentRegistry();
|
||||
|
||||
// Initialize GoldenLayout
|
||||
const layout = new GoldenLayout(layoutConfig, componentRegistry, document.querySelector('#layout-root'));
|
||||
layout.init();
|
||||
|
||||
// Setup toolbar
|
||||
setupToolbarButtons();
|
||||
|
||||
// Connect to server
|
||||
connectWS();
|
||||
logEvent('Animator initialized with GoldenLayout', {}, null);
|
||||
});
|
||||
});
|
||||
@@ -566,26 +566,9 @@ function flashHotword(utterance, score) {
|
||||
hotwordTimer = setTimeout(() => el.classList.remove('active'), 3000);
|
||||
}
|
||||
|
||||
// ── 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 ───────────────────────────────────────────────────────────
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
connectWS();
|
||||
initializeMode();
|
||||
|
||||
// Populate LLM fields from server config (.env defaults)
|
||||
get('/api/config').then(cfg => {
|
||||
|
||||
@@ -1,286 +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 — 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>
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
215
public/home.js
215
public/home.js
@@ -1,215 +0,0 @@
|
||||
'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,220 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Re-Commander — Robot Selection</title>
|
||||
<title>Re-Commander — Jibo</title>
|
||||
<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>
|
||||
<body>
|
||||
|
||||
@@ -224,52 +12,581 @@
|
||||
<div id="status-dot"></div>
|
||||
<h1>Re<span>-Commander</span></h1>
|
||||
<span id="status-label">Connecting…</span>
|
||||
<span id="hotword-indicator"></span>
|
||||
</header>
|
||||
|
||||
<div class="home-container">
|
||||
<div class="home-content">
|
||||
<div class="home-title">Re<span>-Commander</span></div>
|
||||
<div class="home-subtitle">Select your robot and operating mode</div>
|
||||
|
||||
<!-- Robot Selection -->
|
||||
<div class="selection-group">
|
||||
<label class="selection-label">Previous Connections</label>
|
||||
<div class="connections-list" id="connections-list">
|
||||
<div style="color:var(--muted);font-size:12px;text-align:center;padding:20px 0;">No saved connections</div>
|
||||
</div>
|
||||
|
||||
<label class="selection-label" style="margin-top:16px;">Add New Robot</label>
|
||||
<div class="new-connection">
|
||||
<input type="text" id="robot-ip" placeholder="192.168.1.100 or robot.local">
|
||||
<button id="btn-add-connection">Add</button>
|
||||
<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 (↑)">↑</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>
|
||||
|
||||
<!-- Mode Selection -->
|
||||
<div class="selection-group">
|
||||
<label class="selection-label"> Mode</label>
|
||||
<div class="mode-buttons-grid">
|
||||
<button class="mode-btn" data-mode="commander" title="Commander Mode">
|
||||
<span class="mode-icon">🎮</span>
|
||||
<span class="mode-label">Commander</span>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Launch Button -->
|
||||
<button id="btn-launch" disabled>▶ Launch Session</button>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<script src="home.js"></script>
|
||||
<!-- 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>
|
||||
|
||||
@@ -273,7 +273,6 @@ label { font-size: 12px; color: var(--muted); display: block; margin-bottom: 3px
|
||||
.log-entry .evt-error { color: var(--danger); }
|
||||
.log-entry .evt-motion { color: var(--warn); }
|
||||
.log-entry .evt-entity { color: var(--success); }
|
||||
.log-entry .evt-success { color: var(--success); }
|
||||
|
||||
/* ── Tabs ── */
|
||||
.tabs {
|
||||
@@ -375,71 +374,3 @@ label { font-size: 12px; color: var(--muted); display: block; margin-bottom: 3px
|
||||
/* ── Video area ── */
|
||||
.video-controls { display: flex; gap: 6px; align-items: center; }
|
||||
#video-status { font-size: 12px; color: var(--muted); }
|
||||
|
||||
/* ── Menu Section (Robot & Mode) ── */
|
||||
.menu-section {
|
||||
background: linear-gradient(135deg, var(--surface2), var(--surface));
|
||||
border-bottom: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
#robot-select {
|
||||
width: 100%;
|
||||
padding: 7px 8px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#robot-select:hover { border-color: var(--accent); }
|
||||
#robot-select:focus { border-color: var(--accent); background: var(--surface); }
|
||||
|
||||
/* ── Mode Buttons ── */
|
||||
.mode-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 5px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background 0.15s, border-color 0.15s, transform 0.1s;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mode-btn:hover {
|
||||
background: var(--accent2);
|
||||
border-color: var(--accent);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #000;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 0 10px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.mode-icon {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mode-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,194 +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 — Telepresence Mode</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<style>
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.telecom-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Full-screen video area */
|
||||
.video-fullscreen {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #000;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#camera-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
#camera-feed {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#camera-no-feed {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Top Control Bar */
|
||||
.telecom-top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#btn-back-home {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.telecom-title {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.telecom-status {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Bottom Control Bar */
|
||||
.telecom-bottom-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.telecom-bottom-bar button {
|
||||
padding: 8px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#video-quality-select {
|
||||
padding: 6px 8px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#video-quality-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
</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>
|
||||
</header>
|
||||
|
||||
<div class="telecom-container">
|
||||
|
||||
<!-- Top Control Bar -->
|
||||
<div class="telecom-top-bar">
|
||||
<button id="btn-back-home">← Back to Home</button>
|
||||
<div class="telecom-title">🎥 Telepresence Stream</div>
|
||||
<div class="telecom-status" id="telecom-status">Ready</div>
|
||||
</div>
|
||||
|
||||
<!-- Full-screen Video -->
|
||||
<div class="video-fullscreen">
|
||||
<div id="camera-wrap">
|
||||
<img id="camera-feed" alt="Telepresence feed" style="display:none;">
|
||||
<div id="camera-no-feed">
|
||||
<div style="font-size:40px;">📹</div>
|
||||
<div>No camera feed</div>
|
||||
<div style="font-size:11px;">Click restart video below to start broadcasting</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Control Bar -->
|
||||
<div class="telecom-bottom-bar">
|
||||
<div class="bar-group">
|
||||
<label class="bar-label">Quality:</label>
|
||||
<select id="video-quality-select">
|
||||
<option value="highRes">High</option>
|
||||
<option value="medRes" selected>Medium</option>
|
||||
<option value="lowRes">Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="bar-group" style="margin-left:auto; gap: 8px;">
|
||||
<button id="btn-play-eye-anim">👁 Play Eye</button>
|
||||
<button id="btn-video-start" style="background:var(--success);border-color:var(--success);">▶ Start Video</button>
|
||||
<button id="btn-video-stop" class="danger">■ Stop</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="telepresence.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,242 +0,0 @@
|
||||
'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();
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -11,7 +11,7 @@ const fs = require('fs');
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const JIBO_HOST = '192.168.1.10';
|
||||
const JIBO_HOST = '192.168.1.217';
|
||||
const JIBO_PORT = 8160;
|
||||
const APP_PORT = process.env.PORT || 3000;
|
||||
|
||||
@@ -922,6 +922,10 @@ app.post('/api/llm/chat', async (req, res) => {
|
||||
|
||||
const headers = {};
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
try {
|
||||
const extra = process.env.LLM_HEADERS ? JSON.parse(process.env.LLM_HEADERS) : {};
|
||||
Object.assign(headers, extra);
|
||||
} catch { console.warn('[llm] LLM_HEADERS is not valid JSON — ignored'); }
|
||||
|
||||
try {
|
||||
const result = await httpPost(url, headers, { model: mdl, messages: allMessages, stream: false });
|
||||
|
||||
Reference in New Issue
Block a user