openvibers/openvibe.chat Commits

Chat: choose Link machines for models and parties (#469)

OP OpenVibers committed 3 days ago · 876731ce04 · 8 files, +19 −13

* Chat: choose Link computers for models and parties

* Merge remote-tracking branch 'origin/main' into machines-one-place-chat

# Conflicts:
#	server/rooms/parties.js
#	server/web/party-pages.js
#	test/party-media.test.js

Changes

ddiff --git a/public/web/chat.css b/public/web/chat.css
iindex 1713986..99b394d 100644
---- a/public/web/chat.css
++++ b/public/web/chat.css
@@@ -83,7 +83,6 @@ button[disabled] { opacity: .6; cursor: default; }
.oc-ai-form:has(select[name="provider"] option[value="node"]:checked) .oc-not-node,
.oc-ai-form:not(:has(select[name="provider"] option[value="node"]:checked)) .oc-if-node,
.oc-ai-form:not(:has(select[name="provider"] option[value="openai"]:checked)) .oc-if-openai { display: none; }
--.oc-ai-form input[name="machine"] { display: block; width: 100%; max-width: 36ch; box-sizing: border-box; margin-top: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.92em; } /* a whole dev_ id, even at 320 px */
@media (max-width: 560px) {
.oc-main { padding: 12px 12px 40px; }
.oc-feed { max-height: 60vh; }
ddiff --git a/server/rooms/parties.js b/server/rooms/parties.js
iindex 96c4813..5d1da4f 100644
---- a/server/rooms/parties.js
++++ b/server/rooms/parties.js
@@@ -178,10 +178,11 @@ async function create(user, input = {}) {
return await createAgentStream(user, input);
}
--async function createDesktop(user, { title = 'Shared computer', desktop_hosting: hosting } = {}) {
++async function createDesktop(user, { title = 'Shared computer', desktop_hosting: hosting, desktop_node_id: nodeId } = {}) {
if (!user) fail(401, 'party.sign_in', 'Sign in to start a shared computer');
if (!/^usr_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(user.subject_id || ''))) fail(409, 'party.no_subject', 'This account has no OpenVibe subject yet; sign in again');
if (hosting !== 'link' && hosting !== 'openvibe') fail(422, 'party.hosting', 'Choose your computer or an OpenVibe computer');
++ if (hosting === 'link' && !/^lnk_[0-9A-HJKMNP-TV-Z]{26}$/.test(String(nodeId || ''))) fail(422, 'party.machine', 'Choose one of your Link computers');
const name = titleOf(title);
const raw = secret();
const slug = `party-${crypto.randomBytes(4).toString('hex')}`;
@@@ -192,9 +193,9 @@ async function createDesktop(user, { title = 'Shared computer', desktop_hosting:
let room;
try {
room = await rooms.create(user, { name: name.slice(0, 40), slug, kind: 'party', visibility: 'private', topic: name, party: true });
-- await db.run(`INSERT INTO room_parties (room_id, agent_stream_id, vod_id, host_user_id, host_subject, mode, playing, listed, join_secret_hash, party_kind, desktop_hosting, desktop_state)
-- VALUES (?, NULL, NULL, ?, ?, 'live', false, false, ?, 'desktop', ?, 'waiting')`,
-- [room.id, user.id, user.subject_id, sha256(raw), hosting]);
++ await db.run(`INSERT INTO room_parties (room_id, agent_stream_id, vod_id, host_user_id, host_subject, mode, playing, listed, join_secret_hash, party_kind, desktop_hosting, desktop_state, desktop_node_id)
++ VALUES (?, NULL, NULL, ?, ?, 'live', false, false, ?, 'desktop', ?, 'waiting', ?)`,
++ [room.id, user.id, user.subject_id, sha256(raw), hosting, hosting === 'link' ? nodeId : null]);
if (placed) await pool.bind(placed.hold.id, room.id);
await controlLease.take({ id: room.id, slug }, user.subject_id, { watchers: [] });
} catch (err) {
ddiff --git a/server/web/layout.js b/server/web/layout.js
iindex 84c8175..71fdbcd 100644
---- a/server/web/layout.js
++++ b/server/web/layout.js
@@@ -175,6 +175,7 @@ ${o.party ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<link
${(o.scripts || []).map((n) => `<script src="${asset(n)}" defer></script>`).join('\n')}
<script src="${ovServe.url('theme-loader.js')}" defer></script>
<script src="${ovServe.url('navbar.js')}" defer></script>
++${String(o.body || '').includes('data-ov-machines') ? '<script src="/shared/machines.js" defer></script>' : ''}
<script src="${ovServe.url('footer.js')}" defer></script>
${o.script ? `<script src="${asset('chat-history.js')}" defer></script>
<script src="${asset('chat.js')}" defer></script>` : ''}
ddiff --git a/server/web/pages.js b/server/web/pages.js
iindex e1a675a..28af955 100644
---- a/server/web/pages.js
++++ b/server/web/pages.js
@@@ -801,7 +801,7 @@ ${offered ? notice('ok', `Save to let your AI members think with ${offered.model
${unreachable ? notice('error', 'OpenVibe.AI did not answer, so your key cannot be shown or changed right now.') : `${key ? `<p><strong>${esc(credentials.label(key))}</strong> <span class="oc-muted">· ${keyLine}${vibeOwn ? ' · Vibe thinks with it' : ''}</span></p>` : '<p class="oc-muted">No key yet.</p>'}
<form class="oc-form oc-ai-form" method="post" action="/settings/ai-key">
<label>Provider <select name="provider">${opt('openai', 'OpenAI or OpenAI-compatible', provider)}${opt('anthropic', 'Anthropic', provider)}${opt('link', 'A model on my own computer', provider)}</select></label>
--<label class="oc-if-node">Computer <input name="machine" maxlength="40" pattern="dev_[0-9A-Z]{26}" placeholder="dev_… (a model on your own computer only)" value="${esc(machineValue === 'any' ? '' : machineValue)}" autocomplete="off" spellcheck="false"></label>
++<div class="oc-if-node" data-ov-machines data-needs="models" data-name="machine" data-value="${esc(machineValue === 'any' ? '' : machineValue)}"></div>
<label class="oc-check oc-if-node"><input type="checkbox" name="machine_any" value="1"${machineValue === 'any' ? ' checked' : ''}> Any of my computers that is on with this model (the fastest first, the next when one is busy)</label>
<label class="oc-if-openai">Endpoint <input name="base_url" type="url" maxlength="300" inputmode="url" placeholder="https://openrouter.ai/api/v1 (leave empty for OpenAI itself)" value="${esc((key && key.base_url) || '')}"></label>
<label>Model <input name="model" required maxlength="120" placeholder="gpt-4o-mini, or llama3.1:8b on your computer" value="${esc(modelValue)}" autocomplete="off" spellcheck="false"></label>
ddiff --git a/server/web/party-pages.js b/server/web/party-pages.js
iindex b212d32..450dff6 100644
---- a/server/web/party-pages.js
++++ b/server/web/party-pages.js
@@@ -233,8 +233,10 @@ ${grid}
return page(req, res, 200, { actor, title: 'Start a shared computer on OpenVibe', path: '/new', robots: 'noindex, nofollow', footer: 'compact', partyHome: true,
body: `<section class="oc-party-confirm pty-confirm"><h1>Start a shared computer on OpenVibe?</h1><p>OpenVibe runs a temporary browser for this party on one of its own computers. People you invite can watch, and you can pass the remote.</p><p class="oc-muted">${esc(line)}</p><form method="post" action="/new"><input type="hidden" name="desktop_hosting" value="openvibe"><button class="oc-button" type="submit">Start the party</button> <a href="/" class="oc-muted">Not now</a></form></section>` });
}
++ const machine = String(req.query.machine || '');
++ const node = String(req.query.desktop_node_id || '');
return page(req, res, 200, { actor, title: 'Start a shared computer', path: '/new', robots: 'noindex, nofollow', footer: 'compact', partyHome: true,
-- body: `<section class="oc-party-confirm pty-confirm"><h1>Start a shared computer?</h1><p>Link runs a temporary browser in a container on your computer. People you invite can watch, and you can pass the remote.</p><p class="oc-muted">It needs the OpenVibe Link app on this computer. <a href="https://openvibe.link">Get Link</a></p><form method="post" action="/new"><input type="hidden" name="desktop_hosting" value="link"><button class="oc-button" type="submit">Start the party</button> <a href="/" class="oc-muted">Not now</a></form></section>` });
++ body: `<section class="oc-party-confirm pty-confirm"><h1>Start a shared computer?</h1><p>Link runs a temporary browser in a container on your selected computer. People you invite can watch, and you can pass the remote.</p><form method="post" action="/new"><div data-ov-machines data-needs="browser" data-name="machine" data-principal-name="desktop_node_id" data-value="${esc(machine)}" data-principal-value="${esc(node)}"></div><input type="hidden" name="desktop_hosting" value="link"><button class="oc-button" type="submit">Start the party</button> <a href="/" class="oc-muted">Not now</a></form></section>` });
}
if (req.query.radio) {
let chosen;
@@@ -278,7 +280,7 @@ ${source ? `<input type="hidden" name="source_url" value="${esc(source)}">` : ''
if ((origin && origin !== base) || (fetchSite && fetchSite !== 'same-origin')) return res.status(403).type('text/plain').send('Start a party from openvibe.party itself.');
if (req.body && req.body.desktop_hosting) {
try {
-- const out = await parties.createDesktop(actor.user, { desktop_hosting: String(req.body.desktop_hosting), title: 'Shared computer' });
++ const out = await parties.createDesktop(actor.user, { desktop_hosting: String(req.body.desktop_hosting), desktop_node_id: String(req.body.desktop_node_id || ''), title: 'Shared computer' });
return res.redirect(303, `/p/${encodeURIComponent(out.party.id)}?k=${encodeURIComponent(out.join_secret)}`);
} catch (err) {
return res.redirect(303, `/?error=${encodeURIComponent(err && err.status && err.message ? err.message : 'The shared computer could not be started')}`);
ddiff --git a/test/party-media.test.js b/test/party-media.test.js
iindex 11e7876..ab0e386 100644
---- a/test/party-media.test.js
++++ b/test/party-media.test.js
@@@ -152,8 +152,9 @@ t('agent-stream parties are unchanged', async () => {
});
t('a desktop party gates members, claims one Link machine, and issues member tickets', async () => {
++ const nodeId = ids.newId('link');
assert.strictEqual((await api('POST', '/parties', bob, { desktop_hosting: 'openvibe' })).status, 503);
-- const made = await api('POST', '/parties', bob, { desktop_hosting: 'link', title: 'Shared computer' });
++ const made = await api('POST', '/parties', bob, { desktop_hosting: 'link', desktop_node_id: nodeId, title: 'Shared computer' });
assert.strictEqual(made.status, 201, made.text);
const slug = made.body.party.id;
assert.strictEqual(made.body.party.kind, 'desktop');
@@@ -162,7 +163,6 @@ t('a desktop party gates members, claims one Link machine, and issues member tic
const seen = await api('GET', `/parties/${slug}`, bob);
assert.strictEqual(seen.body.party.lease.holder.subject, bob.subject_id);
assert.strictEqual((await api('GET', `/parties/${slug}/desktop/ticket`, ann)).status, 404);
-- const nodeId = ids.newId('link');
const now = Math.floor(Date.now() / 1000);
const nodeToken = (owner) => serviceAuth.signServiceToken({ iss: h.ISS, sub: `link:${nodeId}`, actor_type: 'link', on_behalf_of: owner,
aud: ['openvibe.chat'], cap: [], iat: now, exp: now + 300, jti: `party_${owner}` }, h.keys.privateKey);
@@@ -317,6 +317,9 @@ t('the home offers OpenVibe.Video\'s latest, and a pasted watch page plays its f
assert.match(home.text, /OpenVibe computers are not on yet/, 'the plain truth while the owner pool is off');
assert.match(home.text, /name="desktop" value="link" class="pty-host"><strong>On my computer<\/strong>/, 'hosting on my computer is one of two plain choices');
assert.match(home.text, /value="openvibe" class="pty-host" disabled aria-describedby="pty-openvibe-availability"/, 'On OpenVibe cannot be chosen while no OpenVibe machine is free');
++ const linkAsk = await raw('GET', '/new?desktop=link', { cookie: cookieOf(ann) });
++ assert.strictEqual(linkAsk.status, 200, linkAsk.text.slice(0, 300));
++ assert.match(linkAsk.text, /data-ov-machines[^>]*data-needs="browser"[^>]*data-name="machine"[^>]*data-principal-name="desktop_node_id"/, 'a party host comes from the Link machine picker');
// A pasted watch page: the confirmation names it, and the party plays its file with the page as its source.
const asks = await raw('GET', `/new?media=${encodeURIComponent(PAGE)}`, { cookie: cookieOf(ann) });
ddiff --git a/test/party-music.test.js b/test/party-music.test.js
iindex 84ebe00..9db1eb6 100644
---- a/test/party-music.test.js
++++ b/test/party-music.test.js
@@@ -62,7 +62,7 @@ t('boot with the Radio catalogue', async () => {
});
t('a shared computer starts empty, and only the room’s remote holder can start its music', async () => {
-- const made = await api('POST', '/', ann, { desktop_hosting: 'link', title: 'Music room' });
++ const made = await api('POST', '/', ann, { desktop_hosting: 'link', desktop_node_id: ids.newId('link'), title: 'Music room' });
assert.strictEqual(made.status, 201, made.text);
party = made.body.party;
key = made.body.join_secret;
ddiff --git a/test/room-ai-models.test.js b/test/room-ai-models.test.js
iindex f9abe59..23a720b 100644
---- a/test/room-ai-models.test.js
++++ b/test/room-ai-models.test.js
@@@ -171,8 +171,8 @@ t('a model on your own computer: the machine and no key; the settings form comes
const page = await web('GET', `/settings?own=link&machine=${MACHINE}&model=qwen2.5:7b`, cy);
const sect = page.text.slice(page.text.indexOf('id="oc-ai-key"'));
assert.ok(sect.includes('<option value="link" selected>A model on my own computer</option>'), 'own computer chosen');
-- assert.ok(sect.includes(`name="machine" maxlength="40"`) && sect.includes(`value="${MACHINE}"`));
-- assert.ok(sect.includes('class="oc-form oc-ai-form"') && sect.includes('<label class="oc-if-node">Computer') && sect.includes('<label class="oc-not-node">API key') && sect.includes('<label class="oc-if-openai">Endpoint'), 'each field says which providers take it (chat.css hides the rest)');
++ assert.ok(sect.includes(`data-name="machine"`) && sect.includes(`data-value="${MACHINE}"`) && sect.includes('data-needs="models"'));
++ assert.ok(sect.includes('class="oc-form oc-ai-form"') && sect.includes('class="oc-if-node" data-ov-machines') && sect.includes('<label class="oc-not-node">API key') && sect.includes('<label class="oc-if-openai">Endpoint'), 'each field says which providers take it (chat.css hides the rest)');
assert.ok(sect.includes('value="qwen2.5:7b"'), 'the model from the link');
assert.ok(sect.includes('Save to let your AI members think with qwen2.5:7b on your own computer.'));
assert.ok(sect.includes('your own computer · llama3.1:8b') && sect.includes('nothing spent'), 'what is saved now');