openvibers/openvibe.chat Commits
Add Vibe in one click: a room's or channel's AI members get OpenVibe's assistant, asked with @vibe (#140)
Changes
@@@ -126,6 +126,9 @@ function dmMessage(m, me) {const mine = me && m.sender_id === me.id;return `<li class="oc-msg${mine ? ' oc-mine' : ''}" data-id="${Number(m.id) || 0}"><time class="oc-time" datetime="${t.iso}">${t.text}</time> <span class="oc-name">${esc(m.display_name || m.username)}</span> <span class="oc-text">${linkify(m.message)}</span></li>`;}++// Vibe, OpenVibe's assistant, as a room's AI member (one click on the room's settings; a person's own chats with it are++// server/rooms/vibe.js): how it should be in a group.++const VIBE_IN_ROOMS = 'Vibe, OpenVibe\'s assistant: helpful, quick and friendly. Answers whoever asks, and helps the group decide, plan and find things, with links.';const notice = (kind, text) => `<p class="oc-notice oc-${kind}" role="${kind === 'error' ? 'alert' : 'status'}">${esc(text)}</p>`;/**@@@ -795,6 +798,7 @@ ${a ? `<label class="oc-check"><input type="checkbox" name="enabled" value="1"${${aiMembers.enabled() ? '' : notice('error', 'AI members are switched off on this server right now.')}<p class="oc-muted">An AI member answers when someone writes its name with an @ (for example @${esc(ais[0] ? ais[0].handle : 'pantry')}). It reads the room's recent messages and the notes you give it, through OpenVibe.AI, with OpenVibe's model or with your own key's (several members with different models make a multi-model group chat). It cannot do anything outside the chat. At most ${aiMembers.MAX_PER_ROOM}.</p>${ais.map((a) => `<section class="oc-ai-member"><h3><span class="oc-badge oc-ai">AI</span> ${esc(a.name)} <span class="oc-muted">@${esc(a.handle)} · ${esc(aiMembers.modelOf(a))}</span>${a.link_service === 'food' ? ' <span class="oc-badge">knows the household\'s pantry (OpenVibe.Food)</span>' : ''}</h3>${aiForm(a)}<form class="oc-inline" method="post" action="/r/${esc(room.slug)}/ai-members/${Number(a.id)}/delete"><button type="submit" class="oc-link">Remove ${esc(a.name)}</button></form></section>`).join('')}++${ais.length < aiMembers.MAX_PER_ROOM && !ais.some((a) => a.handle === 'vibe') ? `<form class="oc-form oc-ai-quick" method="post" action="/r/${esc(room.slug)}/ai-members"><input type="hidden" name="name" value="Vibe"><input type="hidden" name="persona" value="${esc(VIBE_IN_ROOMS)}"><p><button type="submit">Add Vibe</button> <span class="oc-muted">OpenVibe's assistant joins the room: anyone here can ask it with @vibe.</span></p></form>` : ''}${ais.length < aiMembers.MAX_PER_ROOM ? aiForm(null) : ''}` : '';if (room.server_id) {return await page(req, res, 200, {@@@ -119,6 +119,14 @@ t('the pages: AI messages are badged and not profile links; settings add and remassert.ok(page.text.includes('<code>@pantry</code>'), 'the room says how to ask it');const settings = await web('GET', '/r/our-kitchen/settings', ann);assert.ok(settings.text.includes('id="oc-ai"') && settings.text.includes('Notes it always knows'));++ // One click adds Vibe, OpenVibe's assistant, as a member anyone can ask with @vibe; then the button goes.++ assert.ok(settings.text.includes('<input type="hidden" name="name" value="Vibe">') && settings.text.includes('Add Vibe</button>'));++ const vibeAdd = await web('POST', '/r/our-kitchen/ai-members', ann, { name: 'Vibe', persona: 'Vibe, OpenVibe\'s assistant' });++ assert.strictEqual(vibeAdd.status, 303, vibeAdd.text.slice(0, 200));++ const vibeMember = (await api('GET', '/our-kitchen/ai-members', ann)).body.ai_members.find((m) => m.handle === 'vibe');++ assert.ok(vibeMember, 'Vibe is a member');++ assert.ok(!(await web('GET', '/r/our-kitchen/settings', ann)).text.includes('Add Vibe</button>'), 'no second Vibe');++ assert.strictEqual((await web('POST', `/r/our-kitchen/ai-members/${vibeMember.id}/delete`, ann, { go: '1' })).status, 303);const add = await web('POST', '/r/our-kitchen/ai-members', ann, { name: 'Coach', persona: 'Keeps us honest' });assert.strictEqual(add.status, 303, add.text.slice(0, 200));const coach = (await api('GET', '/our-kitchen/ai-members', ann)).body.ai_members.find((m) => m.handle === 'coach');
@@ -126,6 +126,9 @@ function dmMessage(m, me) {
@@ -126,6 +126,9 @@ function dmMessage(m, me) {
const mine = me && m.sender_id === me.id;
const mine = me && m.sender_id === me.id;
return `<li class="oc-msg${mine ? ' oc-mine' : ''}" data-id="${Number(m.id) || 0}"><time class="oc-time" datetime="${t.iso}">${t.text}</time> <span class="oc-name">${esc(m.display_name || m.username)}</span> <span class="oc-text">${linkify(m.message)}</span></li>`;
return `<li class="oc-msg${mine ? ' oc-mine' : ''}" data-id="${Number(m.id) || 0}"><time class="oc-time" datetime="${t.iso}">${t.text}</time> <span class="oc-name">${esc(m.display_name || m.username)}</span> <span class="oc-text">${linkify(m.message)}</span></li>`;
}
}
+// Vibe, OpenVibe's assistant, as a room's AI member (one click on the room's settings; a person's own chats with it are
+// server/rooms/vibe.js): how it should be in a group.
+const VIBE_IN_ROOMS = 'Vibe, OpenVibe\'s assistant: helpful, quick and friendly. Answers whoever asks, and helps the group decide, plan and find things, with links.';
const notice = (kind, text) => `<p class="oc-notice oc-${kind}" role="${kind === 'error' ? 'alert' : 'status'}">${esc(text)}</p>`;
const notice = (kind, text) => `<p class="oc-notice oc-${kind}" role="${kind === 'error' ? 'alert' : 'status'}">${esc(text)}</p>`;
/**
/**
@@ -795,6 +798,7 @@ ${a ? `<label class="oc-check"><input type="checkbox" name="enabled" value="1"${
@@ -795,6 +798,7 @@ ${a ? `<label class="oc-check"><input type="checkbox" name="enabled" value="1"${
${aiMembers.enabled() ? '' : notice('error', 'AI members are switched off on this server right now.')}
${aiMembers.enabled() ? '' : notice('error', 'AI members are switched off on this server right now.')}
<p class="oc-muted">An AI member answers when someone writes its name with an @ (for example @${esc(ais[0] ? ais[0].handle : 'pantry')}). It reads the room's recent messages and the notes you give it, through OpenVibe.AI, with OpenVibe's model or with your own key's (several members with different models make a multi-model group chat). It cannot do anything outside the chat. At most ${aiMembers.MAX_PER_ROOM}.</p>
<p class="oc-muted">An AI member answers when someone writes its name with an @ (for example @${esc(ais[0] ? ais[0].handle : 'pantry')}). It reads the room's recent messages and the notes you give it, through OpenVibe.AI, with OpenVibe's model or with your own key's (several members with different models make a multi-model group chat). It cannot do anything outside the chat. At most ${aiMembers.MAX_PER_ROOM}.</p>
${ais.map((a) => `<section class="oc-ai-member"><h3><span class="oc-badge oc-ai">AI</span> ${esc(a.name)} <span class="oc-muted">@${esc(a.handle)} · ${esc(aiMembers.modelOf(a))}</span>${a.link_service === 'food' ? ' <span class="oc-badge">knows the household\'s pantry (OpenVibe.Food)</span>' : ''}</h3>${aiForm(a)}<form class="oc-inline" method="post" action="/r/${esc(room.slug)}/ai-members/${Number(a.id)}/delete"><button type="submit" class="oc-link">Remove ${esc(a.name)}</button></form></section>`).join('')}
${ais.map((a) => `<section class="oc-ai-member"><h3><span class="oc-badge oc-ai">AI</span> ${esc(a.name)} <span class="oc-muted">@${esc(a.handle)} · ${esc(aiMembers.modelOf(a))}</span>${a.link_service === 'food' ? ' <span class="oc-badge">knows the household\'s pantry (OpenVibe.Food)</span>' : ''}</h3>${aiForm(a)}<form class="oc-inline" method="post" action="/r/${esc(room.slug)}/ai-members/${Number(a.id)}/delete"><button type="submit" class="oc-link">Remove ${esc(a.name)}</button></form></section>`).join('')}
+${ais.length < aiMembers.MAX_PER_ROOM && !ais.some((a) => a.handle === 'vibe') ? `<form class="oc-form oc-ai-quick" method="post" action="/r/${esc(room.slug)}/ai-members"><input type="hidden" name="name" value="Vibe"><input type="hidden" name="persona" value="${esc(VIBE_IN_ROOMS)}"><p><button type="submit">Add Vibe</button> <span class="oc-muted">OpenVibe's assistant joins the room: anyone here can ask it with @vibe.</span></p></form>` : ''}
${ais.length < aiMembers.MAX_PER_ROOM ? aiForm(null) : ''}` : '';
${ais.length < aiMembers.MAX_PER_ROOM ? aiForm(null) : ''}` : '';
if (room.server_id) {
if (room.server_id) {
return await page(req, res, 200, {
return await page(req, res, 200, {
@@ -119,6 +119,14 @@ t('the pages: AI messages are badged and not profile links; settings add and rem
@@ -119,6 +119,14 @@ t('the pages: AI messages are badged and not profile links; settings add and rem
assert.ok(page.text.includes('<code>@pantry</code>'), 'the room says how to ask it');
assert.ok(page.text.includes('<code>@pantry</code>'), 'the room says how to ask it');
const settings = await web('GET', '/r/our-kitchen/settings', ann);
const settings = await web('GET', '/r/our-kitchen/settings', ann);
assert.ok(settings.text.includes('id="oc-ai"') && settings.text.includes('Notes it always knows'));
assert.ok(settings.text.includes('id="oc-ai"') && settings.text.includes('Notes it always knows'));
+ // One click adds Vibe, OpenVibe's assistant, as a member anyone can ask with @vibe; then the button goes.
+ assert.ok(settings.text.includes('<input type="hidden" name="name" value="Vibe">') && settings.text.includes('Add Vibe</button>'));
+ const vibeAdd = await web('POST', '/r/our-kitchen/ai-members', ann, { name: 'Vibe', persona: 'Vibe, OpenVibe\'s assistant' });
+ assert.strictEqual(vibeAdd.status, 303, vibeAdd.text.slice(0, 200));
+ const vibeMember = (await api('GET', '/our-kitchen/ai-members', ann)).body.ai_members.find((m) => m.handle === 'vibe');
+ assert.ok(vibeMember, 'Vibe is a member');
+ assert.ok(!(await web('GET', '/r/our-kitchen/settings', ann)).text.includes('Add Vibe</button>'), 'no second Vibe');
+ assert.strictEqual((await web('POST', `/r/our-kitchen/ai-members/${vibeMember.id}/delete`, ann, { go: '1' })).status, 303);
const add = await web('POST', '/r/our-kitchen/ai-members', ann, { name: 'Coach', persona: 'Keeps us honest' });
const add = await web('POST', '/r/our-kitchen/ai-members', ann, { name: 'Coach', persona: 'Keeps us honest' });
assert.strictEqual(add.status, 303, add.text.slice(0, 200));
assert.strictEqual(add.status, 303, add.text.slice(0, 200));
const coach = (await api('GET', '/our-kitchen/ai-members', ann)).body.ai_members.find((m) => m.handle === 'coach');
const coach = (await api('GET', '/our-kitchen/ai-members', ann)).body.ai_members.find((m) => m.handle === 'coach');