openvibers/openvibe.chat Commits

"Your AI key" shows only the fields the chosen provider takes (a computer: no endpoint, key or budget), and the computer field holds a whole dev_ id at 320 px (#210)

OP OpenVibers committed 18 days ago · a3e0838064 · 3 files, +13 −6

Changes

ddiff --git a/public/web/chat.css b/public/web/chat.css
iindex cf0a9af..4a8008e 100644
---- a/public/web/chat.css
++++ b/public/web/chat.css
@@@ -74,6 +74,12 @@ button[disabled] { opacity: .6; cursor: default; }
.oc-form h2 { font-size: 1rem; margin: 18px 0 4px; }
.oc-form select { margin-left: 8px; max-width: calc(100% - 8px); } /* its margin counts too: a full-width select stuck 8 px out on phones */
.oc-form input[type=url], .oc-form textarea { display: block; width: 100%; margin-top: 4px; } /* an address or a paragraph needs the width to be read */
++/* "Your AI key": only the fields the chosen provider takes (a model on your own computer has no endpoint, key or budget;
++ only OpenAI-compatible providers take an endpoint). Without :has() every field shows, as before. */
++.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/web/pages.js b/server/web/pages.js
iindex d8b9abb..6eaf56d 100644
---- a/server/web/pages.js
++++ b/server/web/pages.js
@@@ -513,13 +513,13 @@ ${await aiKeySection(actor, req.query || {})}`,
<p class="oc-muted">AI members you add to a room, your Vibe among them, can think with your own provider and model instead of OpenVibe's: OpenAI, Anthropic, any OpenAI-compatible endpoint (OpenRouter, Groq, Together, your own server), or <strong>a model on your own computer</strong> (Ollama, LM Studio or llama.cpp through the OpenVibe Node, free, with no port opened: turn on "Let my AI use this computer's models" for it on <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>). A key is kept encrypted in OpenVibe.AI and never shown again; the room's messages go to your provider or your computer when one of those members answers, and a key's replies count against your daily budget, not the room's limit.</p>
${offered ? notice('ok', `Save to let your AI members think with ${offered.model || 'a model'} on your own computer.`) : ''}
${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" 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('node', 'A model on my own computer', provider)}</select></label>
--<label>Computer <input name="machine" maxlength="40" pattern="dev_[0-9A-Z]{26}" placeholder="dev_… (a model on your own computer only)" value="${esc(machineValue)}" autocomplete="off" spellcheck="false"></label>
--<label>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-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)}" autocomplete="off" spellcheck="false"></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>API key <input name="api_key" type="password" maxlength="400" autocomplete="off" spellcheck="false" placeholder="${key ? 'Leave empty to keep the current key' : 'sk-… (none for your own computer)'}"></label>
--<label>Daily budget, US dollars <input name="budget_usd_per_day" type="number" min="0" max="1000" step="0.5" value="${key && key.budget_usd_per_day != null ? key.budget_usd_per_day : 1}"> <span class="oc-muted">0 means no cap</span></label>
++<label class="oc-not-node">API key <input name="api_key" type="password" maxlength="400" autocomplete="off" spellcheck="false" placeholder="${key ? 'Leave empty to keep the current key' : 'sk-… (none for your own computer)'}"></label>
++<label class="oc-not-node">Daily budget, US dollars <input name="budget_usd_per_day" type="number" min="0" max="1000" step="0.5" value="${key && key.budget_usd_per_day != null ? key.budget_usd_per_day : 1}"> <span class="oc-muted">0 means no cap</span></label>
${vibeBox}
<p><button type="submit">${key ? 'Save' : 'Add key'}</button></p>
</form>
@@@ -529,7 +529,7 @@ ${key ? `<form class="oc-inline" method="post" action="/settings/ai-key/delete">
const actor = await needUser(req, res); if (!actor) return;
const b = req.body || {};
try {
-- const key = await credentials.put(actor.user.subject_id, { provider: b.provider, base_url: b.base_url, api_key: b.api_key, model: b.model, budget_usd_per_day: b.budget_usd_per_day, machine: b.machine });
++ const key = await credentials.put(actor.user.subject_id, { provider: b.provider, base_url: b.provider === 'openai' ? b.base_url : undefined, api_key: b.api_key, model: b.model, budget_usd_per_day: b.budget_usd_per_day, machine: b.machine });
await require('../rooms/ai-members').relabelKey(actor.user.subject_id, key, actor.user);
const vibe = require('../rooms/vibe');
if (vibe.enabled()) await vibe.setEngine(actor.user, b.vibe === '1');
ddiff --git a/test/room-ai-models.test.js b/test/room-ai-models.test.js
iindex cf9b47a..b84f471 100644
---- a/test/room-ai-models.test.js
++++ b/test/room-ai-models.test.js
@@@ -172,6 +172,7 @@ t('a model on your own computer: the machine and no key; the settings form comes
const sect = page.text.slice(page.text.indexOf('id="oc-ai-key"'));
assert.ok(sect.includes('<option value="node" 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('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');