openvibers/openvibe.shared Commits

Navbar: Ask Vibe (openvibe.chat/vibe, the selection and page prefilled) replaces Ask Actor in the account menu (#110)

OP OpenVibers committed 19 days ago · 206ea01103 · 4 files, +106 −102

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex b31c6da..b8cb96c 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -6,6 +6,10 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## 4.13.0 — unreleased
++- **Ask Vibe from every site** (`navbar.js`). The account menu's "Ask Actor" is now **Ask Vibe**: it opens a new chat
++ with Vibe, OpenVibe's assistant, at `openvibe.chat/vibe`, with the text selected on the page and the page's address
++ filled in (never sent by itself; OpenVibe.Chat#165). `_ask.askVibeHref` replaces `askActorHref`;
++ `test/navbar-ask-vibe.test.js` replaces `navbar-ask-actor.test.js`.
- **Richer home pages for every site** (`showcase.js`, `showcase.css`, `docs/home-pages.md`). The owner, 2026-10-25:
"more rich content and different types… shared code so you can make nice home pages across all our different sites…
and other people can reuse too". About 30 sites build their homes from showcase.
ddiff --git a/navbar.js b/navbar.js
iindex ab24d10..0b0eb0c 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -365,10 +365,10 @@
const _selfSrc = (typeof document !== 'undefined' && document.currentScript && document.currentScript.src) || '';
let _lazyWidth = null;
let _iconState = 0; // 0 idle, 1 loading, 2 loaded, 3 unavailable
-- // "Ask Actor": the page's selected text (≤1000) and https address (no hash, ≤400) into Actor's console box.
++ // "Ask Vibe": the page's selected text (≤1000) and https address (no hash, ≤400) into a new chat with Vibe (openvibe.chat).
function selectedText() { try { const s = root.getSelection(); return s && !s.isCollapsed ? String(s).replace(/\s+/g, ' ').trim().slice(0, 1000) : ''; } catch { return ''; } }
-- function askActorHref(t) {
-- const c = 'https://openvibe.actor/console', u = new URL(location.href); u.hash = '';
++ function askVibeHref(t) {
++ const c = 'https://openvibe.chat/vibe', u = new URL(location.href); u.hash = '';
return t ? `${c}?ask=${encodeURIComponent(t)}${u.protocol === 'https:' && u.href.length <= 400 ? `&from=${encodeURIComponent(u.href)}` : ''}` : c;
}
function lazyWidth(name) {
@@@ -1509,7 +1509,7 @@
${!isAnon && u.username ? `<a href="https://openvibe.network/@${escapeAttr(u.username)}"><span class="icon">${navIcon('fa-id-card')}</span> My Profile</a>` : ''}
${!isAnon && u.username ? `<a href="https://openvibe.live/@${escapeAttr(u.username)}"><span class="icon">${navIcon('fa-tower-broadcast')}</span> My Channel</a>` : ''}
<a href="https://openvibe.network/my#notifications"><span class="icon">${navIcon('fa-bell')}</span> Notifications</a>
-- ${!isAnon ? `<a href="https://openvibe.actor/console" data-ov-ask-actor><span class="icon">${navIcon('fa-brain')}</span> Ask Actor</a>` : ''}
++ ${!isAnon ? `<a href="https://openvibe.chat/vibe" data-ov-ask><span class="icon">${navIcon('fa-brain')}</span> Ask Vibe</a>` : ''}
<div class="sep"></div>`}
<div class="ud-label">Display</div>
<div class="ud-scheme" role="group" aria-label="Light or dark" hidden><button type="button" data-ov-scheme="light" aria-pressed="false"><span class="icon">${navIcon('fa-sun')}</span> Light</button><button type="button" data-ov-scheme="dark" aria-pressed="false"><span class="icon">${navIcon('fa-moon')}</span> Dark</button><button type="button" data-ov-scheme="auto" aria-pressed="false" title="Follow this device's light or dark setting"><span class="icon">${navIcon('fa-circle-half-stroke')}</span> Auto</button></div>
@@@ -1537,12 +1537,12 @@
regPanel(dropdown, 'user-menu');
if (_config.recent !== false) renderRecent(dropdown.querySelector('#openvibe-recent'));
-- // Avatar click toggles dropdown (the selection for Ask Actor is read on pointerdown: the press can clear it)
++ // Avatar click toggles dropdown (the selection for Ask Vibe is read on pointerdown: the press can clear it)
const avatarBtn = nav.querySelector('#openvibe-avatar-btn'); let sel = null;
avatarBtn.addEventListener('pointerdown', () => { sel = selectedText(); });
avatarBtn.addEventListener('click', () => {
dropdown.classList.toggle('open');
-- const ask = dropdown.querySelector('[data-ov-ask-actor]'); if (ask) ask.href = askActorHref(sel ?? selectedText()); sel = null;
++ const ask = dropdown.querySelector('[data-ov-ask]'); if (ask) ask.href = askVibeHref(sel ?? selectedText()); sel = null;
if (dropdown.classList.contains('open')) { loadWallet(dropdown); dropdown.scrollTop = 0; if (root.OpenVibeIcons) root.OpenVibeIcons.mount(dropdown); else if (!document.getElementById('ov-icons-loader')) { const sc = document.createElement('script'); sc.id = 'ov-icons-loader'; sc.src = sibling('ov-icons.js'); sc.async = true; document.head.appendChild(sc); } }
});
@@@ -1750,7 +1750,7 @@
persistToken,
fillUrl,
},
-- _ask: { selectedText, askActorHref },
++ _ask: { selectedText, askVibeHref },
_wallet: { loadWallet, configure: (c) => { Object.assign(_config, c); _walletLoaded = false; } },
};
ddiff --git a/test/navbar-ask-actor.test.js b/test/navbar-ask-actor.test.js
ddeleted file mode 100644
iindex 6e917ae..0000000
---- a/test/navbar-ask-actor.test.js
++++ /dev/null
@@@ -1,95 +0,0 @@
--'use strict';
--// "Ask Actor" in the account menu (the OpenVibe Frame's button, plan T17): signed in only, it opens Actor's console
--// with the text selected on the page as the ask and the page as where it came from. The link part runs in plain node;
--// the selection part runs in a real headless Chrome (skipped without Chrome or with OV_SKIP_BROWSER=1): text selected,
--// then a real press on the avatar, must reach the link even though the press can clear the selection.
--const assert = require('assert');
--const fs = require('fs');
--const os = require('os');
--const path = require('path');
--const http = require('http');
--
--const ROOT = path.join(__dirname, '..');
--
--// ── The link, in plain node ──
--global.window = global;
--global.location = { href: 'https://openvibe.live/@someone#chat', hostname: 'openvibe.live', protocol: 'https:' };
--global.document = { readyState: 'complete', cookie: '', addEventListener() {}, querySelector() { return null }, getElementById() { return null } };
--const navbar = require(path.join(ROOT, 'navbar.js'));
--const { askActorHref } = navbar._ask;
--assert.strictEqual(askActorHref(''), 'https://openvibe.actor/console', 'no selection: the plain console');
--const href = new URL(askActorHref('what is this stream about?'));
--assert.strictEqual(href.origin + href.pathname, 'https://openvibe.actor/console');
--assert.strictEqual(href.searchParams.get('ask'), 'what is this stream about?');
--assert.strictEqual(href.searchParams.get('from'), 'https://openvibe.live/@someone', 'the page, without its hash');
--global.location.href = `https://openvibe.live/search?q=${'x'.repeat(420)}`;
--assert.strictEqual(new URL(askActorHref('hi')).searchParams.get('from'), null, 'a page address over 400 characters is left out');
--global.location.href = 'http://127.0.0.1:3000/';
--assert.strictEqual(new URL(askActorHref('hi')).searchParams.get('from'), null, 'only https pages are named');
--console.log('navbar-ask-actor: link ok');
--
--// ── The selection, in Chrome ──
--const h = require('../browser-harness');
--if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
-- console.log('navbar-ask-actor: browser part skipped (no Chrome; set CHROME_BIN)');
-- process.exit(0);
--}
--
--const page = (user) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Ask</title><link rel="icon" href="data:,">
--<script src="/shared/navbar.js" defer></script></head>
--<body><div id="navbar-mount"></div><main><p id="text">The rover drives
-- to the door.</p></main>
--<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: ${JSON.stringify(user)}, links: [], networkLinks: false, countViews: false, notifications: false, recent: false }); });</script>
--</body></html>`;
--const PAGES = { '/in': page({ id: 7, username: 'probe', display_name: 'Probe' }), '/out': page(null) };
--const server = http.createServer((req, res) => {
-- const u = new URL(req.url, 'http://x');
-- if (PAGES[u.pathname]) { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGES[u.pathname]); }
-- const m = /^\/shared\/([a-z0-9-]+\.js)$/.exec(u.pathname);
-- if (m && fs.existsSync(path.join(ROOT, m[1]))) { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, m[1]))); }
-- res.writeHead(404); return res.end();
--});
--
--(async () => {
-- await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
-- const base = `http://127.0.0.1:${server.address().port}`;
-- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-ask-actor-'));
-- const browser = await h.launch({ tmpDir });
-- try {
-- const context = await browser.newContext();
-- const p = await h.openPage(browser, context, { width: 1280, block: ['*openvibe.network*', '*openvibe.tools*', '*openvibe.media*', '*jsdelivr*'] });
-- const ready = `new Promise((r) => { const t = setInterval(() => { if (document.querySelector('nav.openvibe-navbar')) { clearInterval(t); r(true); } }, 20); })`;
-- const press = async () => {
-- const box = await p.evaluate(`(() => { const r = document.getElementById('openvibe-avatar-btn').getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; })()`);
-- await p.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: box.x, y: box.y, button: 'left', clickCount: 1 });
-- await p.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: box.x, y: box.y, button: 'left', clickCount: 1 });
-- };
-- const askHref = () => p.evaluate(`document.querySelector('[data-ov-ask-actor]') && document.querySelector('[data-ov-ask-actor]').href`);
--
-- await p.goto(`${base}/out`, { maxMs: 5000 });
-- await p.evaluate(ready);
-- assert.strictEqual(await p.evaluate(`!!document.querySelector('[data-ov-ask-actor]')`), false, 'signed out: no Ask Actor');
--
-- await p.goto(`${base}/in`, { maxMs: 5000 });
-- await p.evaluate(ready);
-- assert.strictEqual(await p.evaluate(`document.querySelector('[data-ov-ask-actor]').textContent.trim()`), 'Ask Actor');
--
-- // Nothing selected: the plain console.
-- await press();
-- assert.strictEqual(await askHref(), 'https://openvibe.actor/console');
-- await press();
--
-- // Text selected on the page, then a real press on the avatar: the selection is the ask (spaces folded).
-- await p.evaluate(`(() => { const r = document.createRange(); r.selectNodeContents(document.getElementById('text')); const s = getSelection(); s.removeAllRanges(); s.addRange(r); })()`);
-- await press();
-- const u = new URL(await askHref());
-- assert.strictEqual(u.searchParams.get('ask'), 'The rover drives to the door.');
-- assert.strictEqual(u.searchParams.get('from'), null, 'an http test page is not named');
-- assert.strictEqual(await p.evaluate(`document.querySelector('.openvibe-navbar-dropdown').classList.contains('open')`), true, 'the menu opened');
-- console.log('navbar-ask-actor: browser ok');
-- } finally {
-- await browser.close();
-- server.close();
-- fs.rmSync(tmpDir, { recursive: true, force: true });
-- }
--})().catch((e) => { console.error(e); process.exit(1); });
ddiff --git a/test/navbar-ask-vibe.test.js b/test/navbar-ask-vibe.test.js
nnew file mode 100644
iindex 0000000..9a18329
---- /dev/null
++++ b/test/navbar-ask-vibe.test.js
@@@ -0,0 +1,95 @@
++'use strict';
++// "Ask Vibe" in the account menu (the OpenVibe Frame's button): signed in only, it opens a new chat with Vibe (openvibe.chat/vibe)
++// with the text selected on the page as the ask and the page as where it came from. The link part runs in plain node;
++// the selection part runs in a real headless Chrome (skipped without Chrome or with OV_SKIP_BROWSER=1): text selected,
++// then a real press on the avatar, must reach the link even though the press can clear the selection.
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const http = require('http');
++
++const ROOT = path.join(__dirname, '..');
++
++// ── The link, in plain node ──
++global.window = global;
++global.location = { href: 'https://openvibe.live/@someone#chat', hostname: 'openvibe.live', protocol: 'https:' };
++global.document = { readyState: 'complete', cookie: '', addEventListener() {}, querySelector() { return null }, getElementById() { return null } };
++const navbar = require(path.join(ROOT, 'navbar.js'));
++const { askVibeHref } = navbar._ask;
++assert.strictEqual(askVibeHref(''), 'https://openvibe.chat/vibe', 'no selection: the plain console');
++const href = new URL(askVibeHref('what is this stream about?'));
++assert.strictEqual(href.origin + href.pathname, 'https://openvibe.chat/vibe');
++assert.strictEqual(href.searchParams.get('ask'), 'what is this stream about?');
++assert.strictEqual(href.searchParams.get('from'), 'https://openvibe.live/@someone', 'the page, without its hash');
++global.location.href = `https://openvibe.live/search?q=${'x'.repeat(420)}`;
++assert.strictEqual(new URL(askVibeHref('hi')).searchParams.get('from'), null, 'a page address over 400 characters is left out');
++global.location.href = 'http://127.0.0.1:3000/';
++assert.strictEqual(new URL(askVibeHref('hi')).searchParams.get('from'), null, 'only https pages are named');
++console.log('navbar-ask-vibe: link ok');
++
++// ── The selection, in Chrome ──
++const h = require('../browser-harness');
++if (process.env.OV_SKIP_BROWSER === '1' || !h.findChrome()) {
++ console.log('navbar-ask-vibe: browser part skipped (no Chrome; set CHROME_BIN)');
++ process.exit(0);
++}
++
++const page = (user) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Ask</title><link rel="icon" href="data:,">
++<script src="/shared/navbar.js" defer></script></head>
++<body><div id="navbar-mount"></div><main><p id="text">The rover drives
++ to the door.</p></main>
++<script>document.addEventListener('DOMContentLoaded', function () { OpenVibeNavbar.init({ service: 'site', user: ${JSON.stringify(user)}, links: [], networkLinks: false, countViews: false, notifications: false, recent: false }); });</script>
++</body></html>`;
++const PAGES = { '/in': page({ id: 7, username: 'probe', display_name: 'Probe' }), '/out': page(null) };
++const server = http.createServer((req, res) => {
++ const u = new URL(req.url, 'http://x');
++ if (PAGES[u.pathname]) { res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }); return res.end(PAGES[u.pathname]); }
++ const m = /^\/shared\/([a-z0-9-]+\.js)$/.exec(u.pathname);
++ if (m && fs.existsSync(path.join(ROOT, m[1]))) { res.writeHead(200, { 'content-type': 'application/javascript' }); return res.end(fs.readFileSync(path.join(ROOT, m[1]))); }
++ res.writeHead(404); return res.end();
++});
++
++(async () => {
++ await new Promise((ok) => server.listen(0, '127.0.0.1', ok));
++ const base = `http://127.0.0.1:${server.address().port}`;
++ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-ask-vibe-'));
++ const browser = await h.launch({ tmpDir });
++ try {
++ const context = await browser.newContext();
++ const p = await h.openPage(browser, context, { width: 1280, block: ['*openvibe.network*', '*openvibe.tools*', '*openvibe.media*', '*jsdelivr*'] });
++ const ready = `new Promise((r) => { const t = setInterval(() => { if (document.querySelector('nav.openvibe-navbar')) { clearInterval(t); r(true); } }, 20); })`;
++ const press = async () => {
++ const box = await p.evaluate(`(() => { const r = document.getElementById('openvibe-avatar-btn').getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; })()`);
++ await p.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: box.x, y: box.y, button: 'left', clickCount: 1 });
++ await p.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: box.x, y: box.y, button: 'left', clickCount: 1 });
++ };
++ const askHref = () => p.evaluate(`document.querySelector('[data-ov-ask]') && document.querySelector('[data-ov-ask]').href`);
++
++ await p.goto(`${base}/out`, { maxMs: 5000 });
++ await p.evaluate(ready);
++ assert.strictEqual(await p.evaluate(`!!document.querySelector('[data-ov-ask]')`), false, 'signed out: no Ask Vibe');
++
++ await p.goto(`${base}/in`, { maxMs: 5000 });
++ await p.evaluate(ready);
++ assert.strictEqual(await p.evaluate(`document.querySelector('[data-ov-ask]').textContent.trim()`), 'Ask Vibe');
++
++ // Nothing selected: the plain console.
++ await press();
++ assert.strictEqual(await askHref(), 'https://openvibe.chat/vibe');
++ await press();
++
++ // Text selected on the page, then a real press on the avatar: the selection is the ask (spaces folded).
++ await p.evaluate(`(() => { const r = document.createRange(); r.selectNodeContents(document.getElementById('text')); const s = getSelection(); s.removeAllRanges(); s.addRange(r); })()`);
++ await press();
++ const u = new URL(await askHref());
++ assert.strictEqual(u.searchParams.get('ask'), 'The rover drives to the door.');
++ assert.strictEqual(u.searchParams.get('from'), null, 'an http test page is not named');
++ assert.strictEqual(await p.evaluate(`document.querySelector('.openvibe-navbar-dropdown').classList.contains('open')`), true, 'the menu opened');
++ console.log('navbar-ask-vibe: browser ok');
++ } finally {
++ await browser.close();
++ server.close();
++ fs.rmSync(tmpDir, { recursive: true, force: true });
++ }
++})().catch((e) => { console.error(e); process.exit(1); });