openvibers/openvibe.shared Commits
What Vibe did, as chips you can undo; a timer Vibe starts shows at once (4.79.0) (#232)
Changes
@@@ -4,6 +4,16 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semvebreaking change to any exported module, browser global or served file name is a new major.A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).++## 4.79.0 — 2026-11-01++++- **What Vibe did, as chips you can undo** (plan L48 V2). Under a Vibe answer that set or stopped a reminder or a++ timer (OpenVibe.Chat's `results` on the message), the Dock's Vibe tab shows a chip: "Reminder set: Stretch · Every++ 2 hours", "Timer: 25 min focus". **Undo** for a day (Chat's `POST /api/vibe/undo`), **Open** for a reminder; an undone++ one reads "stopped" and is not offered again.++- **A timer Vibe starts shows at once.** When Vibe's answer started a timer ("Vibe, a 25-minute focus timer"), the Dock++ reads its state again, so the pill counts down without waiting for the next poll. The Vibe tab's mount takes++ `refresh`.++## 4.78.0 — 2026-11-01- **`kit-layout.css`: the fluid scale, the layout primitives and a classless base** (plan L54 K1/K2). A file of its@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v4.78.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.79.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.78.0",++ "version": "4.79.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -26,6 +26,20 @@.ovdk-mic { width: 40px; height: 40px; flex: none; margin-left: -8px; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center; background: transparent; color: var(--text-muted, #8b8b96); touch-action: none; }.ovdk-mic:hover { color: var(--text-primary, #ececf1); }.ovdk-mic.is-on { color: var(--on-accent, #fff); background: var(--live-red, #e5484d); box-shadow: 0 0 0 4px color-mix(in srgb, var(--live-red, #e5484d) 28%, transparent); }++ .ovdk-res { display: grid; gap: 6px; margin-top: 6px; max-width: 100%; }++ .ovdk-res-c { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 4px 10px; padding: 8px 10px; border-radius: 12px;++ border: 1px solid color-mix(in srgb, var(--accent, #3b82f6) 35%, var(--border, rgba(255,255,255,.12))); background: color-mix(in srgb, var(--accent, #3b82f6) 8%, transparent); }++ .ovdk-res-c .ovdk-i { color: var(--accent, #3b82f6); }++ .ovdk-res-t { min-width: 0; font-size: 13px; line-height: 1.35; color: var(--text-primary, #ececf1); }++ .ovdk-res-t b { font-weight: 750; }++ .ovdk-res-t small { display: block; color: var(--text-muted, #8b8b96); font-size: 12px; }++ .ovdk-res-a { display: flex; gap: 4px; }++ .ovdk-res-a button, .ovdk-res-a a { min-height: 30px; padding: 0 10px; border-radius: 999px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: transparent;++ color: var(--text-primary, #ececf1); font: inherit; font-size: 12.5px; font-weight: 650; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; }++ .ovdk-res-a button:hover, .ovdk-res-a a:hover { border-color: var(--accent, #3b82f6); }++ .ovdk-res-a button:disabled { opacity: .55; cursor: default; }++ .ovdk-res-c.is-stopped { border-color: var(--border, rgba(255,255,255,.12)); background: transparent; }++ .ovdk-res-c.is-stopped .ovdk-i, .ovdk-res-c.is-stopped .ovdk-res-t { color: var(--text-muted, #8b8b96); }@media (prefers-reduced-motion: reduce) { .ovdk-typing i { animation: none; } }`;let ctx = null, G = {};@@@ -54,11 +68,50 @@});return span;}++ const DAY = 24 * 3600 * 1000;++ /** One thing Vibe did (Chat's results on its answer): what, when, and Undo / Open. */++ function resultChip(m, r, i) {++ const set = r.action === 'set';++ const c = h('div', `ovdk-res-c${set ? '' : ' is-stopped'}`);++ c.append(icon(r.kind === 'timer' ? G.clock : G.bell));++ const t = h('div', 'ovdk-res-t');++ const what = r.kind === 'timer' ? (set ? 'Timer' : 'Timer stopped') : (set ? 'Reminder set' : 'Reminder stopped');++ t.append(h('b', '', `${what}: `), String(r.title || ''));++ if (r.when && set) t.append(h('small', '', String(r.when)));++ const a = h('div', 'ovdk-res-a');++ const young = !m.at || Date.now() - Date.parse(m.at) < DAY;++ if (set && young && Number(m.id) > 0) {++ const undo = h('button', '', 'Undo'); undo.type = 'button';++ undo.setAttribute('aria-label', `Undo: ${what} ${r.title || ''}`.trim());++ undo.addEventListener('click', async () => {++ undo.disabled = true;++ try {++ const out = await ctx.chatCall('/api/vibe/undo', { method: 'POST', body: JSON.stringify({ message_id: Number(m.id), index: i }) });++ m.results = (out && out.results) || m.results.map((x, k) => (k === i ? { ...x, action: 'stopped' } : x));++ c.replaceWith(resultChip(m, m.results[i], i));++ if (r.kind === 'timer' && ctx.refresh) ctx.refresh();++ } catch (e) {++ undo.disabled = false; undo.textContent = e && e.status === 410 ? 'Too old to undo' : 'Try again';++ if (e && (e.status === 409 || e.status === 410)) undo.disabled = true;++ }++ });++ a.append(undo);++ }++ if (r.url && /^https:\/\//.test(r.url)) { const o = h('a', '', 'Open'); o.href = r.url; o.target = '_blank'; o.rel = 'noopener'; a.append(o); }++ c.append(t, a);++ return c;++ }++ function results(m) {++ const box = h('div', 'ovdk-res');++ m.results.slice(0, 4).forEach((r, i) => { if (r && (r.kind === 'reminder' || r.kind === 'timer')) box.append(resultChip(m, r, i)); });++ return box;++ }function vbubble(m, prev) {const mine = m.from === 'you';const li = h('li', `ovdk-msg${mine ? ' is-mine' : ' is-vibe'}${prev && prev.from === m.from ? ' is-cont' : ''}`);if (!mine && !(prev && prev.from === m.from)) li.append(h('span', 'ovdk-who', 'Vibe'));li.append(linkified(m.text));++ if (!mine && Array.isArray(m.results) && m.results.length) li.append(results(m));if (m.at) li.title = new Date(m.at).toLocaleString();return li;}@@@ -129,6 +182,7 @@list.insertBefore(vbubble(m, vibe.msgs[vibe.msgs.length - 1]), typing.parentNode === list ? typing : null);vibe.msgs.push(m); vibe.last = Math.max(vibe.last, Number(m.id) || 0);if (m.from === 'vibe') answered = true;++ if (!first && Array.isArray(m.results) && m.results.some((r) => r && r.kind === 'timer') && ctx.refresh) ctx.refresh();});if (answered || Date.now() > vibe.waitUntil) { typing.remove(); vibe.waitUntil = 0; }if (first && !vibe.msgs.length) {@@@ -434,7 +434,7 @@panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';const ok = await load('dock-vibe.js', () => !!(root.OpenVibeDockVibe && root.OpenVibeDockVibe.mount));if (!ok) { panel.innerHTML = '<div class="ovdk-none">Vibe did not load. Check your connection, then open it again.</div>'; return; }-- vibe.api = root.OpenVibeDockVibe.mount(panel, { chatCall, active: () => isOpen() && tab === 'vibe', styles: () => (el && el.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles'), icons: { vibe: G.vibe, send: G.send } });++ vibe.api = root.OpenVibeDockVibe.mount(panel, { chatCall, active: () => isOpen() && tab === 'vibe', styles: () => (el && el.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles'), icons: { vibe: G.vibe, send: G.send, clock: G.clock, bell: G.bell }, refresh: () => read() });return;}if (id === 'chats') { chats.panel = panel; mounts.chats = mounts.chats || { refresh: null }; return state().chat && chats.open !== state().chat ? openChat(state().chat, false) : chats.open ? openChat(chats.open, false) : chatList(); }@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.78.0",++ "version": "4.79.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -56,7 +56,7 @@ let scenario = {};const puts = [], decided = [], said = [], reads = [];// Chat's Vibe API for the Dock (OpenVibe.Chat server/rooms/vibe-api.js): one chat, Vibe answering a moment later.let vibeChat = null;--const vibeMsgs = [], asked = [], listened = [];++const vibeMsgs = [], asked = [], listened = [], undone = [];const defaultState = () => ({ open: false, tab: 'activity', pinned: false, agent: null, chat: null, place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'bottom-right', width: null, height: null } }, updated_at: iso(0) });const PAGE = (o) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tasks</title><link rel="icon" href="data:,">@@@ -106,6 +106,14 @@ const server = http.createServer((req, res) => {const after = Number(u.searchParams.get('after')) || 0;return json(200, { chat: vibeChat, messages: vibeMsgs.filter((m) => m.id > after) });}++ if (u.pathname === '/api/vibe/undo' && req.method === 'POST') {++ const b = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');++ undone.push(b);++ const m = vibeMsgs.find((x) => x.id === b.message_id);++ if (!m || !m.results || !m.results[b.index]) return json(404, { error: 'No such result' });++ m.results = m.results.map((r, k) => (k === b.index ? { ...r, action: 'stopped' } : r));++ return json(200, { results: m.results, undone: { kind: 'reminder', id: m.results[b.index].id, title: m.results[b.index].title } });++ }if (u.pathname === '/api/vibe/listen' && req.method === 'POST') {listened.push({ type: req.headers['content-type'], bytes: Buffer.concat(chunks).length, lang: u.searchParams.get('lang'), auth: req.headers.authorization });return json(200, { text: 'remind me to drink water at 3' });@@@ -116,7 +124,8 @@ const server = http.createServer((req, res) => {vibeChat = vibeChat || { slug: 'v1', name: 'Vibe', url: 'https://openvibe.chat/vibe/v1' };const mine = { id: 500 + vibeMsgs.length, from: 'you', text: b.message, at: iso(0) };vibeMsgs.push(mine);-- setTimeout(() => vibeMsgs.push({ id: 600 + vibeMsgs.length, from: 'vibe', text: 'Reminder set: Stretch, every 2 hours. See them at https://openvibe.today/reminders', at: iso(0) }), 700);++ setTimeout(() => vibeMsgs.push({ id: 600 + vibeMsgs.length, from: 'vibe', text: 'Reminder set: Stretch, every 2 hours. See them at https://openvibe.today/reminders', at: iso(0),++ results: [{ kind: 'reminder', action: 'set', id: 'rem_01J8Z3V9Q6N1X2Y3Z4A5B6C7E1', title: 'Stretch', when: 'Every 2 hours, 9 am–5 pm', url: 'https://openvibe.today/me#reminders' }] }), 700);return json(201, { chat: vibeChat, message: mine });}if (u.pathname === '/api/notifications/unread-count') return json(200, { count: 2 });@@@ -359,6 +368,17 @@ async function drag(p, from, to) {const answer = await until(p, "(() => { const m = document.querySelector('.ovdk [data-panel=vibe] .ovdk-msg.is-vibe:not(.ovdk-typing)'); return m ? { text: m.innerText, link: (m.querySelector('a') || {}).href || null } : null; })()", 'Vibe\'s answer', 6000);assert.match(answer.text, /Reminder set: Stretch, every 2 hours/);assert.strictEqual(answer.link, 'https://openvibe.today/reminders', 'an https link is a link');++ // What Vibe did, under its answer: a reminder chip with Undo and Open (plan L48 V2, result chips).++ const chip = await until(p, "(() => { const c = document.querySelector('.ovdk [data-panel=vibe] .ovdk-res-c'); return c ? { text: c.innerText, open: (c.querySelector('a') || {}).href, undo: !!c.querySelector('button') } : null; })()", 'the result chip');++ assert.match(chip.text, /Reminder set: Stretch/);++ assert.match(chip.text, /Every 2 hours, 9 am–5 pm/);++ assert.strictEqual(chip.open, 'https://openvibe.today/me#reminders');++ assert.ok(chip.undo, 'Undo while it can be undone');++ await p.evaluate("document.querySelector('.ovdk .ovdk-res-c button').click()");++ await until(p, "!!document.querySelector('.ovdk .ovdk-res-c.is-stopped')", 'the chip says it is stopped');++ assert.deepStrictEqual(undone[0], { message_id: vibeMsgs.find((m) => m.from === 'vibe').id, index: 0 }, 'undo names the answer and the result');++ assert.match(await p.evaluate("document.querySelector('.ovdk .ovdk-res-c').innerText"), /Reminder stopped: Stretch/);++ assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk .ovdk-res-c button')"), false, 'and is not offered again');assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk .ovdk-typing')"), false, 'the writing dots go when it answers');assert.strictEqual(await p.evaluate("document.querySelector('.ovdk .ovdk-away').href"), 'https://openvibe.chat/vibe/v1', 'Open in Chat goes to the same chat');await wait(900);
@@ -4,6 +4,16 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
@@ -4,6 +4,16 @@ All notable changes to `openvibe-shared`. Versions follow [semver](https://semve
breaking change to any exported module, browser global or served file name is a new major.
breaking change to any exported module, browser global or served file name is a new major.
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
+## 4.79.0 — 2026-11-01
+
+- **What Vibe did, as chips you can undo** (plan L48 V2). Under a Vibe answer that set or stopped a reminder or a
+ timer (OpenVibe.Chat's `results` on the message), the Dock's Vibe tab shows a chip: "Reminder set: Stretch · Every
+ 2 hours", "Timer: 25 min focus". **Undo** for a day (Chat's `POST /api/vibe/undo`), **Open** for a reminder; an undone
+ one reads "stopped" and is not offered again.
+- **A timer Vibe starts shows at once.** When Vibe's answer started a timer ("Vibe, a 25-minute focus timer"), the Dock
+ reads its state again, so the pill counts down without waiting for the next poll. The Vibe tab's mount takes
+ `refresh`.
+
## 4.78.0 — 2026-11-01
## 4.78.0 — 2026-11-01
- **`kit-layout.css`: the fluid scale, the layout primitives and a classless base** (plan L54 K1/K2). A file of its
- **`kit-layout.css`: the fluid scale, the layout primitives and a classless base** (plan L54 K1/K2). A file of its
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v4.78.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.79.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.78.0",
+ "version": "4.79.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -26,6 +26,20 @@
@@ -26,6 +26,20 @@
.ovdk-mic { width: 40px; height: 40px; flex: none; margin-left: -8px; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center; background: transparent; color: var(--text-muted, #8b8b96); touch-action: none; }
.ovdk-mic { width: 40px; height: 40px; flex: none; margin-left: -8px; border: 0; border-radius: 12px; cursor: pointer; display: grid; place-items: center; background: transparent; color: var(--text-muted, #8b8b96); touch-action: none; }
.ovdk-mic:hover { color: var(--text-primary, #ececf1); }
.ovdk-mic:hover { color: var(--text-primary, #ececf1); }
.ovdk-mic.is-on { color: var(--on-accent, #fff); background: var(--live-red, #e5484d); box-shadow: 0 0 0 4px color-mix(in srgb, var(--live-red, #e5484d) 28%, transparent); }
.ovdk-mic.is-on { color: var(--on-accent, #fff); background: var(--live-red, #e5484d); box-shadow: 0 0 0 4px color-mix(in srgb, var(--live-red, #e5484d) 28%, transparent); }
+ .ovdk-res { display: grid; gap: 6px; margin-top: 6px; max-width: 100%; }
+ .ovdk-res-c { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 4px 10px; padding: 8px 10px; border-radius: 12px;
+ border: 1px solid color-mix(in srgb, var(--accent, #3b82f6) 35%, var(--border, rgba(255,255,255,.12))); background: color-mix(in srgb, var(--accent, #3b82f6) 8%, transparent); }
+ .ovdk-res-c .ovdk-i { color: var(--accent, #3b82f6); }
+ .ovdk-res-t { min-width: 0; font-size: 13px; line-height: 1.35; color: var(--text-primary, #ececf1); }
+ .ovdk-res-t b { font-weight: 750; }
+ .ovdk-res-t small { display: block; color: var(--text-muted, #8b8b96); font-size: 12px; }
+ .ovdk-res-a { display: flex; gap: 4px; }
+ .ovdk-res-a button, .ovdk-res-a a { min-height: 30px; padding: 0 10px; border-radius: 999px; border: 1px solid var(--border, rgba(255,255,255,.12)); background: transparent;
+ color: var(--text-primary, #ececf1); font: inherit; font-size: 12.5px; font-weight: 650; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; }
+ .ovdk-res-a button:hover, .ovdk-res-a a:hover { border-color: var(--accent, #3b82f6); }
+ .ovdk-res-a button:disabled { opacity: .55; cursor: default; }
+ .ovdk-res-c.is-stopped { border-color: var(--border, rgba(255,255,255,.12)); background: transparent; }
+ .ovdk-res-c.is-stopped .ovdk-i, .ovdk-res-c.is-stopped .ovdk-res-t { color: var(--text-muted, #8b8b96); }
@media (prefers-reduced-motion: reduce) { .ovdk-typing i { animation: none; } }
@media (prefers-reduced-motion: reduce) { .ovdk-typing i { animation: none; } }
`;
`;
let ctx = null, G = {};
let ctx = null, G = {};
@@ -54,11 +68,50 @@
@@ -54,11 +68,50 @@
});
});
return span;
return span;
}
}
+ const DAY = 24 * 3600 * 1000;
+ /** One thing Vibe did (Chat's results on its answer): what, when, and Undo / Open. */
+ function resultChip(m, r, i) {
+ const set = r.action === 'set';
+ const c = h('div', `ovdk-res-c${set ? '' : ' is-stopped'}`);
+ c.append(icon(r.kind === 'timer' ? G.clock : G.bell));
+ const t = h('div', 'ovdk-res-t');
+ const what = r.kind === 'timer' ? (set ? 'Timer' : 'Timer stopped') : (set ? 'Reminder set' : 'Reminder stopped');
+ t.append(h('b', '', `${what}: `), String(r.title || ''));
+ if (r.when && set) t.append(h('small', '', String(r.when)));
+ const a = h('div', 'ovdk-res-a');
+ const young = !m.at || Date.now() - Date.parse(m.at) < DAY;
+ if (set && young && Number(m.id) > 0) {
+ const undo = h('button', '', 'Undo'); undo.type = 'button';
+ undo.setAttribute('aria-label', `Undo: ${what} ${r.title || ''}`.trim());
+ undo.addEventListener('click', async () => {
+ undo.disabled = true;
+ try {
+ const out = await ctx.chatCall('/api/vibe/undo', { method: 'POST', body: JSON.stringify({ message_id: Number(m.id), index: i }) });
+ m.results = (out && out.results) || m.results.map((x, k) => (k === i ? { ...x, action: 'stopped' } : x));
+ c.replaceWith(resultChip(m, m.results[i], i));
+ if (r.kind === 'timer' && ctx.refresh) ctx.refresh();
+ } catch (e) {
+ undo.disabled = false; undo.textContent = e && e.status === 410 ? 'Too old to undo' : 'Try again';
+ if (e && (e.status === 409 || e.status === 410)) undo.disabled = true;
+ }
+ });
+ a.append(undo);
+ }
+ if (r.url && /^https:\/\//.test(r.url)) { const o = h('a', '', 'Open'); o.href = r.url; o.target = '_blank'; o.rel = 'noopener'; a.append(o); }
+ c.append(t, a);
+ return c;
+ }
+ function results(m) {
+ const box = h('div', 'ovdk-res');
+ m.results.slice(0, 4).forEach((r, i) => { if (r && (r.kind === 'reminder' || r.kind === 'timer')) box.append(resultChip(m, r, i)); });
+ return box;
+ }
function vbubble(m, prev) {
function vbubble(m, prev) {
const mine = m.from === 'you';
const mine = m.from === 'you';
const li = h('li', `ovdk-msg${mine ? ' is-mine' : ' is-vibe'}${prev && prev.from === m.from ? ' is-cont' : ''}`);
const li = h('li', `ovdk-msg${mine ? ' is-mine' : ' is-vibe'}${prev && prev.from === m.from ? ' is-cont' : ''}`);
if (!mine && !(prev && prev.from === m.from)) li.append(h('span', 'ovdk-who', 'Vibe'));
if (!mine && !(prev && prev.from === m.from)) li.append(h('span', 'ovdk-who', 'Vibe'));
li.append(linkified(m.text));
li.append(linkified(m.text));
+ if (!mine && Array.isArray(m.results) && m.results.length) li.append(results(m));
if (m.at) li.title = new Date(m.at).toLocaleString();
if (m.at) li.title = new Date(m.at).toLocaleString();
return li;
return li;
}
}
@@ -129,6 +182,7 @@
@@ -129,6 +182,7 @@
list.insertBefore(vbubble(m, vibe.msgs[vibe.msgs.length - 1]), typing.parentNode === list ? typing : null);
list.insertBefore(vbubble(m, vibe.msgs[vibe.msgs.length - 1]), typing.parentNode === list ? typing : null);
vibe.msgs.push(m); vibe.last = Math.max(vibe.last, Number(m.id) || 0);
vibe.msgs.push(m); vibe.last = Math.max(vibe.last, Number(m.id) || 0);
if (m.from === 'vibe') answered = true;
if (m.from === 'vibe') answered = true;
+ if (!first && Array.isArray(m.results) && m.results.some((r) => r && r.kind === 'timer') && ctx.refresh) ctx.refresh();
});
});
if (answered || Date.now() > vibe.waitUntil) { typing.remove(); vibe.waitUntil = 0; }
if (answered || Date.now() > vibe.waitUntil) { typing.remove(); vibe.waitUntil = 0; }
if (first && !vibe.msgs.length) {
if (first && !vibe.msgs.length) {
@@ -434,7 +434,7 @@
@@ -434,7 +434,7 @@
panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';
panel.innerHTML = '<div class="ovdk-wait"><span></span></div>';
const ok = await load('dock-vibe.js', () => !!(root.OpenVibeDockVibe && root.OpenVibeDockVibe.mount));
const ok = await load('dock-vibe.js', () => !!(root.OpenVibeDockVibe && root.OpenVibeDockVibe.mount));
if (!ok) { panel.innerHTML = '<div class="ovdk-none">Vibe did not load. Check your connection, then open it again.</div>'; return; }
if (!ok) { panel.innerHTML = '<div class="ovdk-none">Vibe did not load. Check your connection, then open it again.</div>'; return; }
- vibe.api = root.OpenVibeDockVibe.mount(panel, { chatCall, active: () => isOpen() && tab === 'vibe', styles: () => (el && el.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles'), icons: { vibe: G.vibe, send: G.send } });
+ vibe.api = root.OpenVibeDockVibe.mount(panel, { chatCall, active: () => isOpen() && tab === 'vibe', styles: () => (el && el.ownerDocument.getElementById('ovdk-styles')) || document.getElementById('ovdk-styles'), icons: { vibe: G.vibe, send: G.send, clock: G.clock, bell: G.bell }, refresh: () => read() });
return;
return;
}
}
if (id === 'chats') { chats.panel = panel; mounts.chats = mounts.chats || { refresh: null }; return state().chat && chats.open !== state().chat ? openChat(state().chat, false) : chats.open ? openChat(chats.open, false) : chatList(); }
if (id === 'chats') { chats.panel = panel; mounts.chats = mounts.chats || { refresh: null }; return state().chat && chats.open !== state().chat ? openChat(state().chat, false) : chats.open ? openChat(chats.open, false) : chatList(); }
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.78.0",
+ "version": "4.79.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -56,7 +56,7 @@ let scenario = {};
@@ -56,7 +56,7 @@ let scenario = {};
const puts = [], decided = [], said = [], reads = [];
const puts = [], decided = [], said = [], reads = [];
// Chat's Vibe API for the Dock (OpenVibe.Chat server/rooms/vibe-api.js): one chat, Vibe answering a moment later.
// Chat's Vibe API for the Dock (OpenVibe.Chat server/rooms/vibe-api.js): one chat, Vibe answering a moment later.
let vibeChat = null;
let vibeChat = null;
-const vibeMsgs = [], asked = [], listened = [];
+const vibeMsgs = [], asked = [], listened = [], undone = [];
const defaultState = () => ({ open: false, tab: 'activity', pinned: false, agent: null, chat: null, place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'bottom-right', width: null, height: null } }, updated_at: iso(0) });
const defaultState = () => ({ open: false, tab: 'activity', pinned: false, agent: null, chat: null, place: { phone: { corner: 'bottom-right' }, desktop: { corner: 'bottom-right', width: null, height: null } }, updated_at: iso(0) });
const PAGE = (o) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tasks</title><link rel="icon" href="data:,">
const PAGE = (o) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Tasks</title><link rel="icon" href="data:,">
@@ -106,6 +106,14 @@ const server = http.createServer((req, res) => {
@@ -106,6 +106,14 @@ const server = http.createServer((req, res) => {
const after = Number(u.searchParams.get('after')) || 0;
const after = Number(u.searchParams.get('after')) || 0;
return json(200, { chat: vibeChat, messages: vibeMsgs.filter((m) => m.id > after) });
return json(200, { chat: vibeChat, messages: vibeMsgs.filter((m) => m.id > after) });
}
}
+ if (u.pathname === '/api/vibe/undo' && req.method === 'POST') {
+ const b = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
+ undone.push(b);
+ const m = vibeMsgs.find((x) => x.id === b.message_id);
+ if (!m || !m.results || !m.results[b.index]) return json(404, { error: 'No such result' });
+ m.results = m.results.map((r, k) => (k === b.index ? { ...r, action: 'stopped' } : r));
+ return json(200, { results: m.results, undone: { kind: 'reminder', id: m.results[b.index].id, title: m.results[b.index].title } });
+ }
if (u.pathname === '/api/vibe/listen' && req.method === 'POST') {
if (u.pathname === '/api/vibe/listen' && req.method === 'POST') {
listened.push({ type: req.headers['content-type'], bytes: Buffer.concat(chunks).length, lang: u.searchParams.get('lang'), auth: req.headers.authorization });
listened.push({ type: req.headers['content-type'], bytes: Buffer.concat(chunks).length, lang: u.searchParams.get('lang'), auth: req.headers.authorization });
return json(200, { text: 'remind me to drink water at 3' });
return json(200, { text: 'remind me to drink water at 3' });
@@ -116,7 +124,8 @@ const server = http.createServer((req, res) => {
@@ -116,7 +124,8 @@ const server = http.createServer((req, res) => {
vibeChat = vibeChat || { slug: 'v1', name: 'Vibe', url: 'https://openvibe.chat/vibe/v1' };
vibeChat = vibeChat || { slug: 'v1', name: 'Vibe', url: 'https://openvibe.chat/vibe/v1' };
const mine = { id: 500 + vibeMsgs.length, from: 'you', text: b.message, at: iso(0) };
const mine = { id: 500 + vibeMsgs.length, from: 'you', text: b.message, at: iso(0) };
vibeMsgs.push(mine);
vibeMsgs.push(mine);
- setTimeout(() => vibeMsgs.push({ id: 600 + vibeMsgs.length, from: 'vibe', text: 'Reminder set: Stretch, every 2 hours. See them at https://openvibe.today/reminders', at: iso(0) }), 700);
+ setTimeout(() => vibeMsgs.push({ id: 600 + vibeMsgs.length, from: 'vibe', text: 'Reminder set: Stretch, every 2 hours. See them at https://openvibe.today/reminders', at: iso(0),
+ results: [{ kind: 'reminder', action: 'set', id: 'rem_01J8Z3V9Q6N1X2Y3Z4A5B6C7E1', title: 'Stretch', when: 'Every 2 hours, 9 am–5 pm', url: 'https://openvibe.today/me#reminders' }] }), 700);
return json(201, { chat: vibeChat, message: mine });
return json(201, { chat: vibeChat, message: mine });
}
}
if (u.pathname === '/api/notifications/unread-count') return json(200, { count: 2 });
if (u.pathname === '/api/notifications/unread-count') return json(200, { count: 2 });
@@ -359,6 +368,17 @@ async function drag(p, from, to) {
@@ -359,6 +368,17 @@ async function drag(p, from, to) {
const answer = await until(p, "(() => { const m = document.querySelector('.ovdk [data-panel=vibe] .ovdk-msg.is-vibe:not(.ovdk-typing)'); return m ? { text: m.innerText, link: (m.querySelector('a') || {}).href || null } : null; })()", 'Vibe\'s answer', 6000);
const answer = await until(p, "(() => { const m = document.querySelector('.ovdk [data-panel=vibe] .ovdk-msg.is-vibe:not(.ovdk-typing)'); return m ? { text: m.innerText, link: (m.querySelector('a') || {}).href || null } : null; })()", 'Vibe\'s answer', 6000);
assert.match(answer.text, /Reminder set: Stretch, every 2 hours/);
assert.match(answer.text, /Reminder set: Stretch, every 2 hours/);
assert.strictEqual(answer.link, 'https://openvibe.today/reminders', 'an https link is a link');
assert.strictEqual(answer.link, 'https://openvibe.today/reminders', 'an https link is a link');
+ // What Vibe did, under its answer: a reminder chip with Undo and Open (plan L48 V2, result chips).
+ const chip = await until(p, "(() => { const c = document.querySelector('.ovdk [data-panel=vibe] .ovdk-res-c'); return c ? { text: c.innerText, open: (c.querySelector('a') || {}).href, undo: !!c.querySelector('button') } : null; })()", 'the result chip');
+ assert.match(chip.text, /Reminder set: Stretch/);
+ assert.match(chip.text, /Every 2 hours, 9 am–5 pm/);
+ assert.strictEqual(chip.open, 'https://openvibe.today/me#reminders');
+ assert.ok(chip.undo, 'Undo while it can be undone');
+ await p.evaluate("document.querySelector('.ovdk .ovdk-res-c button').click()");
+ await until(p, "!!document.querySelector('.ovdk .ovdk-res-c.is-stopped')", 'the chip says it is stopped');
+ assert.deepStrictEqual(undone[0], { message_id: vibeMsgs.find((m) => m.from === 'vibe').id, index: 0 }, 'undo names the answer and the result');
+ assert.match(await p.evaluate("document.querySelector('.ovdk .ovdk-res-c').innerText"), /Reminder stopped: Stretch/);
+ assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk .ovdk-res-c button')"), false, 'and is not offered again');
assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk .ovdk-typing')"), false, 'the writing dots go when it answers');
assert.strictEqual(await p.evaluate("!!document.querySelector('.ovdk .ovdk-typing')"), false, 'the writing dots go when it answers');
assert.strictEqual(await p.evaluate("document.querySelector('.ovdk .ovdk-away').href"), 'https://openvibe.chat/vibe/v1', 'Open in Chat goes to the same chat');
assert.strictEqual(await p.evaluate("document.querySelector('.ovdk .ovdk-away').href"), 'https://openvibe.chat/vibe/v1', 'Open in Chat goes to the same chat');
await wait(900);
await wait(900);