openvibers/openvibe.bot Commits
A machine's models say how fast they answer there (Node v0.29.0's ai_tokens_per_s: "about 15 tokens a second", Answering now; computer.ai_speed in the API); a link styled as a button centres its wrapped text (#191)
Changes
@@@ -25,6 +25,7 @@ button, .button, input, select { font: inherit; min-height: var(--tap); min-widtbutton, .button {display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem; padding: 0 1rem;border: 1px solid var(--line); background: var(--raised); color: var(--text); text-decoration: none; cursor: pointer;++ text-align: center; /* a link styled as a button wraps its text like a button does */touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background 0.12s, border-color 0.12s, transform 0.06s;}button:hover:not(:disabled), .button:hover { border-color: #46505d; }@@@ -438,6 +438,9 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, submwaiting: Number.isInteger(sensors.computer_waiting) ? sensors.computer_waiting : null,// Desks running now (the Node's telemetry): where an agent's next desk has room (Desks plan D6, placement).desks_running: Number.isInteger(sensors.computer_desks) ? sensors.computer_desks : null,++ // How fast its last model answered there (Node v0.29.0's ai plugin): for choosing where a model runs.++ ai_speed: typeof sensors.ai_tokens_per_s === 'number' && sensors.ai_tokens_per_s > 0 && typeof sensors.ai_model === 'string'++ ? { model: sensors.ai_model.slice(0, 120), tokens_per_s: sensors.ai_tokens_per_s } : null,} : null,};};@@@ -753,7 +753,10 @@ function renderMachinesPage({ machines = [], pairing = null }) {const computerRow = (m) => {const c = computerOf(m);if (!c) return '';++ // "about 15 tokens a second" (with which model, when it offers several), measured there by the ai plugin.++ const speed = m.ai_speed ? `about ${m.ai_speed.tokens_per_s >= 10 ? Math.round(m.ai_speed.tokens_per_s) : m.ai_speed.tokens_per_s} tokens a second${c.ai.models.length > 1 ? ` with ${m.ai_speed.model}` : ''}` : '';const aiRow = c.ai ? `<div class="machine-ai"><span class="pill">${c.ai.ready ? 'Models' : 'Models: no server answers'}</span>${c.ai.models.length ? `<span class="muted">${esc(c.ai.models.join(', '))}</span>` : ''}`++ + (m.ai_busy ? ' <span class="pill">Answering now</span>' : '') + (speed ? `<span class="muted">${esc(speed)}</span>` : '')+ `<form method="post" action="/machines/${esc(m.id)}/ai-models"><input type="hidden" name="on" value="${m.ai_models ? '0' : '1'}">`+ `<button type="submit" class="button quiet" aria-pressed="${m.ai_models ? 'true' : 'false'}">${m.ai_models ? 'Stop my AI using this computer\'s models' : 'Let my AI use this computer\'s models'}</button></form>`// Turned on: OpenVibe.Chat's "Your AI key" comes filled in with this computer and its first model (saved only there).@@@ -581,6 +581,10 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limicomputer_screen_stream: sensors.computer_screen_stream === 'live' ? 'live' : 'off',computer_desks: Number.isInteger(sensors.computer_desks) ? sensors.computer_desks : null,ai_models: !!d.ai_models,++ // How fast its last model answered there (Node v0.29.0's ai plugin), and whether one is answering now.++ ai_speed: typeof sensors.ai_tokens_per_s === 'number' && sensors.ai_tokens_per_s > 0 && typeof sensors.ai_model === 'string'++ ? { model: sensors.ai_model.slice(0, 120), tokens_per_s: sensors.ai_tokens_per_s } : null,++ ai_busy: sensors.ai_busy === 1,};}));// ── The live screen (plan T14): the owner watches their machine's screen on their own private stream ──@@@ -436,12 +436,19 @@ const { boot, check, done } = require('./helpers/app');assert.strictEqual((await cmd(own.deviceId, { kind: 'ai', value: { model: 'qwen2.5-1.5b-instruct', messages: [{ role: 'user', content: 'hi' }] } }, { user: alex })).json.result, 'ack');const noExec = await cmd(own.deviceId, { kind: 'exec', value: { argv: ['id'] } }, { user: alex });assert.strictEqual(noExec.json.code, 'bot.computer_off', 'no terminal without the computer plugin');++ // Node v0.29.0's ai plugin says how fast its last model answered there (after the 2 Hz telemetry window).++ await t.wait(600);++ own.c.send(frame('telemetry', { sensors: { ai_busy: 0, ai_model: 'qwen2.5-1.5b-instruct', ai_tokens_per_s: 14.6 } }));++ await t.wait(600);++ const fast = (await t.call('GET', '/api/v1/machines', { user: alex })).json.machines.find((m) => m.id === own.deviceId);++ assert.deepStrictEqual(fast.computer.ai_speed, { model: 'qwen2.5-1.5b-instruct', tokens_per_s: 14.6 }, 'its speed, in the API');let ownCard = '';for (let i = 0; i < 20 && !ownCard.includes('qwen2.5-1.5b-instruct'); i++) {if (i) await t.wait(100);ownCard = await (await fetch(`${t.base}/machines`, { headers: cookie(alex) })).text();}assert.ok(ownCard.includes('qwen2.5-1.5b-instruct'), 'the card shows its models');++ assert.ok(ownCard.includes('about 15 tokens a second'), 'and how fast they answer there');own.c.close();// The machines page: its models and the switch.const html = await (await fetch(`${t.base}/machines`, { headers: cookie(alex) })).text();
@@ -25,6 +25,7 @@ button, .button, input, select { font: inherit; min-height: var(--tap); min-widt
@@ -25,6 +25,7 @@ button, .button, input, select { font: inherit; min-height: var(--tap); min-widt
button, .button {
button, .button {
display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem; padding: 0 1rem;
display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem; padding: 0 1rem;
border: 1px solid var(--line); background: var(--raised); color: var(--text); text-decoration: none; cursor: pointer;
border: 1px solid var(--line); background: var(--raised); color: var(--text); text-decoration: none; cursor: pointer;
+ text-align: center; /* a link styled as a button wraps its text like a button does */
touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background 0.12s, border-color 0.12s, transform 0.06s;
touch-action: manipulation; -webkit-tap-highlight-color: transparent; transition: background 0.12s, border-color 0.12s, transform 0.06s;
}
}
button:hover:not(:disabled), .button:hover { border-color: #46505d; }
button:hover:not(:disabled), .button:hover { border-color: #46505d; }
@@ -438,6 +438,9 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, subm
@@ -438,6 +438,9 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config, subm
waiting: Number.isInteger(sensors.computer_waiting) ? sensors.computer_waiting : null,
waiting: Number.isInteger(sensors.computer_waiting) ? sensors.computer_waiting : null,
// Desks running now (the Node's telemetry): where an agent's next desk has room (Desks plan D6, placement).
// Desks running now (the Node's telemetry): where an agent's next desk has room (Desks plan D6, placement).
desks_running: Number.isInteger(sensors.computer_desks) ? sensors.computer_desks : null,
desks_running: Number.isInteger(sensors.computer_desks) ? sensors.computer_desks : null,
+ // How fast its last model answered there (Node v0.29.0's ai plugin): for choosing where a model runs.
+ ai_speed: typeof sensors.ai_tokens_per_s === 'number' && sensors.ai_tokens_per_s > 0 && typeof sensors.ai_model === 'string'
+ ? { model: sensors.ai_model.slice(0, 120), tokens_per_s: sensors.ai_tokens_per_s } : null,
} : null,
} : null,
};
};
};
};
@@ -753,7 +753,10 @@ function renderMachinesPage({ machines = [], pairing = null }) {
@@ -753,7 +753,10 @@ function renderMachinesPage({ machines = [], pairing = null }) {
const computerRow = (m) => {
const computerRow = (m) => {
const c = computerOf(m);
const c = computerOf(m);
if (!c) return '';
if (!c) return '';
+ // "about 15 tokens a second" (with which model, when it offers several), measured there by the ai plugin.
+ const speed = m.ai_speed ? `about ${m.ai_speed.tokens_per_s >= 10 ? Math.round(m.ai_speed.tokens_per_s) : m.ai_speed.tokens_per_s} tokens a second${c.ai.models.length > 1 ? ` with ${m.ai_speed.model}` : ''}` : '';
const aiRow = c.ai ? `<div class="machine-ai"><span class="pill">${c.ai.ready ? 'Models' : 'Models: no server answers'}</span>${c.ai.models.length ? `<span class="muted">${esc(c.ai.models.join(', '))}</span>` : ''}`
const aiRow = c.ai ? `<div class="machine-ai"><span class="pill">${c.ai.ready ? 'Models' : 'Models: no server answers'}</span>${c.ai.models.length ? `<span class="muted">${esc(c.ai.models.join(', '))}</span>` : ''}`
+ + (m.ai_busy ? ' <span class="pill">Answering now</span>' : '') + (speed ? `<span class="muted">${esc(speed)}</span>` : '')
+ `<form method="post" action="/machines/${esc(m.id)}/ai-models"><input type="hidden" name="on" value="${m.ai_models ? '0' : '1'}">`
+ `<form method="post" action="/machines/${esc(m.id)}/ai-models"><input type="hidden" name="on" value="${m.ai_models ? '0' : '1'}">`
+ `<button type="submit" class="button quiet" aria-pressed="${m.ai_models ? 'true' : 'false'}">${m.ai_models ? 'Stop my AI using this computer\'s models' : 'Let my AI use this computer\'s models'}</button></form>`
+ `<button type="submit" class="button quiet" aria-pressed="${m.ai_models ? 'true' : 'false'}">${m.ai_models ? 'Stop my AI using this computer\'s models' : 'Let my AI use this computer\'s models'}</button></form>`
// Turned on: OpenVibe.Chat's "Your AI key" comes filled in with this computer and its first model (saved only there).
// Turned on: OpenVibe.Chat's "Your AI key" comes filled in with this computer and its first model (saved only there).
@@ -581,6 +581,10 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
@@ -581,6 +581,10 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
computer_screen_stream: sensors.computer_screen_stream === 'live' ? 'live' : 'off',
computer_screen_stream: sensors.computer_screen_stream === 'live' ? 'live' : 'off',
computer_desks: Number.isInteger(sensors.computer_desks) ? sensors.computer_desks : null,
computer_desks: Number.isInteger(sensors.computer_desks) ? sensors.computer_desks : null,
ai_models: !!d.ai_models,
ai_models: !!d.ai_models,
+ // How fast its last model answered there (Node v0.29.0's ai plugin), and whether one is answering now.
+ ai_speed: typeof sensors.ai_tokens_per_s === 'number' && sensors.ai_tokens_per_s > 0 && typeof sensors.ai_model === 'string'
+ ? { model: sensors.ai_model.slice(0, 120), tokens_per_s: sensors.ai_tokens_per_s } : null,
+ ai_busy: sensors.ai_busy === 1,
};
};
}));
}));
// ── The live screen (plan T14): the owner watches their machine's screen on their own private stream ──
// ── The live screen (plan T14): the owner watches their machine's screen on their own private stream ──
@@ -436,12 +436,19 @@ const { boot, check, done } = require('./helpers/app');
@@ -436,12 +436,19 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual((await cmd(own.deviceId, { kind: 'ai', value: { model: 'qwen2.5-1.5b-instruct', messages: [{ role: 'user', content: 'hi' }] } }, { user: alex })).json.result, 'ack');
assert.strictEqual((await cmd(own.deviceId, { kind: 'ai', value: { model: 'qwen2.5-1.5b-instruct', messages: [{ role: 'user', content: 'hi' }] } }, { user: alex })).json.result, 'ack');
const noExec = await cmd(own.deviceId, { kind: 'exec', value: { argv: ['id'] } }, { user: alex });
const noExec = await cmd(own.deviceId, { kind: 'exec', value: { argv: ['id'] } }, { user: alex });
assert.strictEqual(noExec.json.code, 'bot.computer_off', 'no terminal without the computer plugin');
assert.strictEqual(noExec.json.code, 'bot.computer_off', 'no terminal without the computer plugin');
+ // Node v0.29.0's ai plugin says how fast its last model answered there (after the 2 Hz telemetry window).
+ await t.wait(600);
+ own.c.send(frame('telemetry', { sensors: { ai_busy: 0, ai_model: 'qwen2.5-1.5b-instruct', ai_tokens_per_s: 14.6 } }));
+ await t.wait(600);
+ const fast = (await t.call('GET', '/api/v1/machines', { user: alex })).json.machines.find((m) => m.id === own.deviceId);
+ assert.deepStrictEqual(fast.computer.ai_speed, { model: 'qwen2.5-1.5b-instruct', tokens_per_s: 14.6 }, 'its speed, in the API');
let ownCard = '';
let ownCard = '';
for (let i = 0; i < 20 && !ownCard.includes('qwen2.5-1.5b-instruct'); i++) {
for (let i = 0; i < 20 && !ownCard.includes('qwen2.5-1.5b-instruct'); i++) {
if (i) await t.wait(100);
if (i) await t.wait(100);
ownCard = await (await fetch(`${t.base}/machines`, { headers: cookie(alex) })).text();
ownCard = await (await fetch(`${t.base}/machines`, { headers: cookie(alex) })).text();
}
}
assert.ok(ownCard.includes('qwen2.5-1.5b-instruct'), 'the card shows its models');
assert.ok(ownCard.includes('qwen2.5-1.5b-instruct'), 'the card shows its models');
+ assert.ok(ownCard.includes('about 15 tokens a second'), 'and how fast they answer there');
own.c.close();
own.c.close();
// The machines page: its models and the switch.
// The machines page: its models and the switch.
const html = await (await fetch(`${t.base}/machines`, { headers: cookie(alex) })).text();
const html = await (await fetch(`${t.base}/machines`, { headers: cookie(alex) })).text();