openvibers/openvibe.chat Commits
Chat: choose Link machines for models and parties (#469)
Changes
@@@ -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; }@@@ -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) {@@@ -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>` : ''}@@@ -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>@@@ -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')}`);@@@ -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 ticconst 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 fassert.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) });@@@ -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;@@@ -171,8 +171,8 @@ t('a model on your own computer: the machine and no key; the settings form comesconst 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');
@@ -83,7 +83,6 @@ button[disabled] { opacity: .6; cursor: default; }
@@ -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: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="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: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) {
@media (max-width: 560px) {
.oc-main { padding: 12px 12px 40px; }
.oc-main { padding: 12px 12px 40px; }
.oc-feed { max-height: 60vh; }
.oc-feed { max-height: 60vh; }
@@ -178,10 +178,11 @@ async function create(user, input = {}) {
@@ -178,10 +178,11 @@ async function create(user, input = {}) {
return await createAgentStream(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 (!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 (!/^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' && 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 name = titleOf(title);
const raw = secret();
const raw = secret();
const slug = `party-${crypto.randomBytes(4).toString('hex')}`;
const slug = `party-${crypto.randomBytes(4).toString('hex')}`;
@@ -192,9 +193,9 @@ async function createDesktop(user, { title = 'Shared computer', desktop_hosting:
@@ -192,9 +193,9 @@ async function createDesktop(user, { title = 'Shared computer', desktop_hosting:
let room;
let room;
try {
try {
room = await rooms.create(user, { name: name.slice(0, 40), slug, kind: 'party', visibility: 'private', topic: name, party: true });
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);
if (placed) await pool.bind(placed.hold.id, room.id);
await controlLease.take({ id: room.id, slug }, user.subject_id, { watchers: [] });
await controlLease.take({ id: room.id, slug }, user.subject_id, { watchers: [] });
} catch (err) {
} catch (err) {
@@ -175,6 +175,7 @@ ${o.party ? `<link rel="stylesheet" href="${ovServe.url('player.css')}">\n<link
@@ -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')}
${(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('theme-loader.js')}" defer></script>
<script src="${ovServe.url('navbar.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>
<script src="${ovServe.url('footer.js')}" defer></script>
${o.script ? `<script src="${asset('chat-history.js')}" defer></script>
${o.script ? `<script src="${asset('chat-history.js')}" defer></script>
<script src="${asset('chat.js')}" defer></script>` : ''}
<script src="${asset('chat.js')}" defer></script>` : ''}
@@ -801,7 +801,7 @@ ${offered ? notice('ok', `Save to let your AI members think with ${offered.model
@@ -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>'}
${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">
<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>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-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 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>
<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>
@@ -233,8 +233,10 @@ ${grid}
@@ -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,
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>` });
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,
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) {
if (req.query.radio) {
let chosen;
let chosen;
@@ -278,7 +280,7 @@ ${source ? `<input type="hidden" name="source_url" value="${esc(source)}">` : ''
@@ -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 ((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) {
if (req.body && req.body.desktop_hosting) {
try {
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)}`);
return res.redirect(303, `/p/${encodeURIComponent(out.party.id)}?k=${encodeURIComponent(out.join_secret)}`);
} catch (err) {
} catch (err) {
return res.redirect(303, `/?error=${encodeURIComponent(err && err.status && err.message ? err.message : 'The shared computer could not be started')}`);
return res.redirect(303, `/?error=${encodeURIComponent(err && err.status && err.message ? err.message : 'The shared computer could not be started')}`);
@@ -152,8 +152,9 @@ t('agent-stream parties are unchanged', async () => {
@@ -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 () => {
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);
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);
assert.strictEqual(made.status, 201, made.text);
const slug = made.body.party.id;
const slug = made.body.party.id;
assert.strictEqual(made.body.party.kind, 'desktop');
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
@@ -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);
const seen = await api('GET', `/parties/${slug}`, bob);
assert.strictEqual(seen.body.party.lease.holder.subject, bob.subject_id);
assert.strictEqual(seen.body.party.lease.holder.subject, bob.subject_id);
assert.strictEqual((await api('GET', `/parties/${slug}/desktop/ticket`, ann)).status, 404);
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 now = Math.floor(Date.now() / 1000);
const nodeToken = (owner) => serviceAuth.signServiceToken({ iss: h.ISS, sub: `link:${nodeId}`, actor_type: 'link', on_behalf_of: owner,
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);
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
@@ -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, /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, /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');
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.
// 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) });
const asks = await raw('GET', `/new?media=${encodeURIComponent(PAGE)}`, { cookie: cookieOf(ann) });
@@ -62,7 +62,7 @@ t('boot with the Radio catalogue', async () => {
@@ -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 () => {
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);
assert.strictEqual(made.status, 201, made.text);
party = made.body.party;
party = made.body.party;
key = made.body.join_secret;
key = made.body.join_secret;
@@ -171,8 +171,8 @@ t('a model on your own computer: the machine and no key; the settings form comes
@@ -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 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"'));
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('<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('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('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');
assert.ok(sect.includes('your own computer · llama3.1:8b') && sect.includes('nothing spent'), 'what is saved now');