Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b31a99817f | ||
|
|
ae624da7c2 | ||
|
|
56578e59f9 | ||
|
|
07ef1a2fad |
@@ -4,8 +4,12 @@ LLM_ENDPOINT=http://localhost:11434/v1/chat/completions
|
|||||||
# Model name passed to the endpoint
|
# Model name passed to the endpoint
|
||||||
LLM_MODEL=llama3
|
LLM_MODEL=llama3
|
||||||
|
|
||||||
# Optional API key (sent as Bearer token) — leave blank for local servers
|
# Optional API key — value is sent as "Bearer <value>", do NOT include the word "Bearer" here
|
||||||
LLM_API_KEY=
|
LLM_API_KEY=
|
||||||
|
|
||||||
|
# Optional extra headers sent with every LLM request, as a JSON object
|
||||||
|
# Example: LLM_HEADERS={"x-openclaw-agent-id":"jibo"}
|
||||||
|
LLM_HEADERS=
|
||||||
|
|
||||||
# Default system prompt for the voice AI loop
|
# Default system prompt for the voice AI loop
|
||||||
LLM_SYSTEM_PROMPT=You are Jibo, a friendly social robot. Keep responses brief and conversational.
|
LLM_SYSTEM_PROMPT=You are Jibo, a friendly social robot. Keep responses brief and conversational.
|
||||||
|
|||||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "re-commander",
|
"name": "re-commander",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "re-commander",
|
"name": "re-commander",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"golden-layout": "^2.6.0",
|
"rom-control": "^2.0.0",
|
||||||
"ws": "^8.14.2"
|
"ws": "^8.14.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -372,12 +372,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/golden-layout": {
|
|
||||||
"version": "2.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/golden-layout/-/golden-layout-2.6.0.tgz",
|
|
||||||
"integrity": "sha512-sIVQCiRWOymHbVD1Aw/T9/ijbPYAVGBlgGYd1N9MRKfcyBNSpjr87Vg9nSHm+RCT8ELrvK8IJYJV0QRJuVUkCQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -636,6 +630,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rom-control": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rom-control/-/rom-control-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-mENZI9Cf8fUzB02X1tTNGn4HUlCEpASds9YZQvbp/T5LJoCYCrgryLAE7OCIkLa+4Ob+NlO1jBK84n8/zR7/tg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.14.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "re-commander",
|
"name": "re-commander",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"description": "Jibo ROM Commander — local Node.js recreation using port 8160",
|
"description": "Jibo ROM Commander — built on the rom-control module",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"golden-layout": "^2.6.0",
|
"rom-control": "^2.0.2",
|
||||||
"ws": "^8.14.2"
|
"ws": "^8.14.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,8 @@
|
|||||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
||||||
<input type="checkbox" id="llm-toggle" style="width:auto;">
|
<input type="checkbox" id="llm-toggle" style="width:auto;">
|
||||||
<label for="llm-toggle" style="margin:0;cursor:pointer;">LLM voice loop</label>
|
<label for="llm-toggle" style="margin:0;cursor:pointer;">LLM voice loop</label>
|
||||||
<button id="btn-llm-clear" style="margin-left:auto;font-size:11px;padding:2px 8px;" title="Clear conversation history">↺ Clear</button>
|
<button id="btn-llm-cancel" class="danger" style="margin-left:auto;font-size:11px;padding:2px 8px;" title="Cancel active LLM request">✕ Stop</button>
|
||||||
|
<button id="btn-llm-clear" style="font-size:11px;padding:2px 8px;" title="Clear conversation history">↺ Clear</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Completions endpoint</label>
|
<label>Completions endpoint</label>
|
||||||
@@ -281,6 +282,6 @@
|
|||||||
<img id="photo-modal-img" src="" alt="Photo">
|
<img id="photo-modal-img" src="" alt="Photo">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="commander.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ function handleJiboEvent(body, txId) {
|
|||||||
switch (body.Event) {
|
switch (body.Event) {
|
||||||
case 'onHotWordHeard':
|
case 'onHotWordHeard':
|
||||||
flashHotword(body.utterance || 'hey jibo', body.score);
|
flashHotword(body.utterance || 'hey jibo', body.score);
|
||||||
if (document.getElementById('auto-listen-toggle').checked) doListen();
|
if (document.getElementById('auto-listen-toggle').checked)
|
||||||
|
post('/api/interrupt').then(() => doListen());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'onStart':
|
case 'onStart':
|
||||||
@@ -257,7 +258,7 @@ document.getElementById('btn-say').addEventListener('click', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('btn-say-cancel').addEventListener('click', () => {
|
document.getElementById('btn-say-cancel').addEventListener('click', () => {
|
||||||
if (lastSayTx) post('/api/cancel', { txId: lastSayTx });
|
post('/api/say/cancel');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Listen ────────────────────────────────────────────────────────────────────
|
// ── Listen ────────────────────────────────────────────────────────────────────
|
||||||
@@ -288,20 +289,28 @@ document.getElementById('btn-listen').addEventListener('click', doListen);
|
|||||||
|
|
||||||
document.getElementById('btn-listen-cancel').addEventListener('click', () => {
|
document.getElementById('btn-listen-cancel').addEventListener('click', () => {
|
||||||
clearListenTimeout();
|
clearListenTimeout();
|
||||||
if (lastListenTx) post('/api/cancel', { txId: lastListenTx });
|
post('/api/listen/cancel');
|
||||||
document.getElementById('listen-result').textContent = '(cancelled)';
|
document.getElementById('listen-result').textContent = '(cancelled)';
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Auto-listen + Voice AI ────────────────────────────────────────────────────
|
// ── Auto-listen + Voice AI ────────────────────────────────────────────────────
|
||||||
|
|
||||||
let llmHistory = []; // [{role:'user'|'assistant', content:string}]
|
let llmHistory = []; // [{role:'user'|'assistant', content:string}]
|
||||||
|
let llmSessionMode = false; // true when server uses LLM_SESSION_KEY (OpenClaw session)
|
||||||
|
let llmTurnCount = 0;
|
||||||
|
|
||||||
function llmStatus(msg) {
|
function llmStatus(msg) {
|
||||||
document.getElementById('llm-status').textContent = msg;
|
document.getElementById('llm-status').textContent = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runLLMLoop(speechText) {
|
async function runLLMLoop(speechText) {
|
||||||
llmHistory.push({ role: 'user', content: speechText });
|
// In session mode send only the latest message — history lives on the server.
|
||||||
|
// In history mode accumulate the full thread and send it each time.
|
||||||
|
const messages = llmSessionMode
|
||||||
|
? [{ role: 'user', content: speechText }]
|
||||||
|
: [...llmHistory, { role: 'user', content: speechText }];
|
||||||
|
|
||||||
|
if (!llmSessionMode) llmHistory.push({ role: 'user', content: speechText });
|
||||||
llmStatus('Thinking…');
|
llmStatus('Thinking…');
|
||||||
|
|
||||||
const endpoint = document.getElementById('llm-endpoint').value.trim();
|
const endpoint = document.getElementById('llm-endpoint').value.trim();
|
||||||
@@ -309,30 +318,45 @@ async function runLLMLoop(speechText) {
|
|||||||
const systemPrompt = document.getElementById('llm-system-prompt').value.trim();
|
const systemPrompt = document.getElementById('llm-system-prompt').value.trim();
|
||||||
|
|
||||||
const r = await post('/api/llm/chat', {
|
const r = await post('/api/llm/chat', {
|
||||||
messages: llmHistory,
|
messages,
|
||||||
endpoint: endpoint || undefined,
|
endpoint: endpoint || undefined,
|
||||||
model: model || undefined,
|
model: model || undefined,
|
||||||
systemPrompt: systemPrompt || undefined,
|
systemPrompt: systemPrompt || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!r || r.error) {
|
if (!r || r.error) {
|
||||||
|
if (r?.error === 'cancelled') { llmStatus('Interrupted.'); if (!llmSessionMode) llmHistory.pop(); return; }
|
||||||
llmStatus('LLM error: ' + (r?.error || 'no response'));
|
llmStatus('LLM error: ' + (r?.error || 'no response'));
|
||||||
llmHistory.pop(); // undo the user push so history stays consistent
|
if (!llmSessionMode) llmHistory.pop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reply = r.reply;
|
const reply = r.reply;
|
||||||
llmHistory.push({ role: 'assistant', content: reply });
|
if (!llmSessionMode) llmHistory.push({ role: 'assistant', content: reply });
|
||||||
llmStatus(`[${llmHistory.length / 2} turns] Last: "${reply.slice(0, 60)}${reply.length > 60 ? '…' : ''}"`);
|
llmTurnCount++;
|
||||||
|
const modeTag = llmSessionMode ? 'session' : 'local';
|
||||||
|
llmStatus(`[${modeTag} · ${llmTurnCount} turns] Last: "${reply.slice(0, 50)}${reply.length > 50 ? '…' : ''}"`);
|
||||||
|
|
||||||
// Fill say box so user can see what Jibo is about to say
|
// Fill say box so user can see what Jibo is about to say
|
||||||
document.getElementById('say-text').value = reply;
|
document.getElementById('say-text').value = reply;
|
||||||
await post('/api/say', { text: reply });
|
const sayResult = await post('/api/say', { text: reply });
|
||||||
|
|
||||||
|
// If the reply ends with a question and wasn't interrupted, listen for the user's answer
|
||||||
|
const endsWithQuestion = /\?[^a-zA-Z0-9]*$/.test(reply.trim());
|
||||||
|
if (endsWithQuestion && !sayResult?.aborted && document.getElementById('llm-toggle').checked) {
|
||||||
|
doListen();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-llm-cancel').addEventListener('click', () => {
|
||||||
|
post('/api/llm/cancel');
|
||||||
|
llmStatus('Cancelled.');
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('btn-llm-clear').addEventListener('click', () => {
|
document.getElementById('btn-llm-clear').addEventListener('click', () => {
|
||||||
llmHistory = [];
|
llmHistory = [];
|
||||||
llmStatus('Conversation cleared.');
|
llmTurnCount = 0;
|
||||||
|
llmStatus(llmSessionMode ? 'Session turn counter reset (server session persists).' : 'Conversation cleared.');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Attention ─────────────────────────────────────────────────────────────────
|
// ── Attention ─────────────────────────────────────────────────────────────────
|
||||||
@@ -566,26 +590,24 @@ function flashHotword(utterance, score) {
|
|||||||
hotwordTimer = setTimeout(() => el.classList.remove('active'), 3000);
|
hotwordTimer = setTimeout(() => el.classList.remove('active'), 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Menu: Robot & Mode Selection ────────────────────────────────────────────
|
// ── Menu: Robot & Mode Selection ─────────────────────────────────────────────
|
||||||
|
|
||||||
let selectedRobot = null;
|
let selectedRobot = null;
|
||||||
let selectedMode = 'commander';
|
let selectedMode = 'commander';
|
||||||
|
|
||||||
// Get the selected robot and mode from sessionStorage
|
|
||||||
function initializeMode() {
|
function initializeMode() {
|
||||||
selectedRobot = sessionStorage.getItem('selectedRobot') || 'jibo-001';
|
selectedRobot = sessionStorage.getItem('selectedRobot') || 'jibo-001';
|
||||||
selectedMode = sessionStorage.getItem('selectedMode') || 'commander';
|
selectedMode = sessionStorage.getItem('selectedMode') || 'commander';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back to home button
|
|
||||||
document.getElementById('btn-back-home')?.addEventListener('click', () => {
|
document.getElementById('btn-back-home')?.addEventListener('click', () => {
|
||||||
window.location.href = 'index.html';
|
window.location.href = 'index.html';
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Initialization ───────────────────────────────────────────────────────────
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
connectWS();
|
|
||||||
initializeMode();
|
initializeMode();
|
||||||
|
connectWS();
|
||||||
|
|
||||||
// Populate LLM fields from server config (.env defaults)
|
// Populate LLM fields from server config (.env defaults)
|
||||||
get('/api/config').then(cfg => {
|
get('/api/config').then(cfg => {
|
||||||
@@ -593,4 +615,8 @@ get('/api/config').then(cfg => {
|
|||||||
if (cfg.llmEndpoint) document.getElementById('llm-endpoint').value = cfg.llmEndpoint;
|
if (cfg.llmEndpoint) document.getElementById('llm-endpoint').value = cfg.llmEndpoint;
|
||||||
if (cfg.llmModel) document.getElementById('llm-model').value = cfg.llmModel;
|
if (cfg.llmModel) document.getElementById('llm-model').value = cfg.llmModel;
|
||||||
if (cfg.llmSystemPrompt) document.getElementById('llm-system-prompt').value = cfg.llmSystemPrompt;
|
if (cfg.llmSystemPrompt) document.getElementById('llm-system-prompt').value = cfg.llmSystemPrompt;
|
||||||
|
if (cfg.sessionMode) {
|
||||||
|
llmSessionMode = true;
|
||||||
|
llmStatus('Session mode (OpenClaw) — history managed server-side.');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -253,11 +253,11 @@
|
|||||||
<span class="mode-icon">🎮</span>
|
<span class="mode-icon">🎮</span>
|
||||||
<span class="mode-label">Commander</span>
|
<span class="mode-label">Commander</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="mode-btn" data-mode="animator" title="Animator Mode">
|
<button class="mode-btn" data-mode="animator" title="Animator Mode" style="display:none;">
|
||||||
<span class="mode-icon">🎬</span>
|
<span class="mode-icon">🎬</span>
|
||||||
<span class="mode-label">Animator</span>
|
<span class="mode-label">Animator</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="mode-btn" data-mode="telepresence" title="Telepresence Mode">
|
<button class="mode-btn" data-mode="telepresence" title="Telepresence Mode" style="display:none;">
|
||||||
<span class="mode-icon">👁</span>
|
<span class="mode-icon">👁</span>
|
||||||
<span class="mode-label">Telepresence</span>
|
<span class="mode-label">Telepresence</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user