openvibers/openvibe.chat Commits
Chat header: the room's AI overview sits among the tools, with its own mark (#529)
Changes
@@@ -638,6 +638,10 @@ h2 { font-size: 1.15rem; margin: 1.4em 0 .5em; }.oc-snip > pre { max-height: 50vh; overflow: auto; padding-right: 48px; }/* A header's "more" menu (a conversation's Block): a small panel under its button. */.oc-more { position: relative; }++/* The room's AI overview sits in the header's tools (it wrapped onto a row of its own: .oc-more is also the grid of++ "more" links above). Opus, 2026-11-11. */++.oc-chan-tools .oc-more { display: inline-flex; margin: 0; grid-template-columns: none; }++.oc-chan-tools .oc-insight-pop { width: min(340px, calc(100vw - 32px)); }.oc-more > summary { list-style: none; }.oc-more > summary::-webkit-details-marker { display: none; }.oc-pop { position: absolute; right: 0; top: calc(100% + 4px); z-index: 20; min-width: 200px; padding: 6px; border-radius: 12px; border: 1px solid var(--oc-line); background: var(--oc-panel); box-shadow: 0 12px 28px rgba(0, 0, 0, .35); }@@@ -17,7 +17,7 @@ function insightBlock(insight) {const off = insight && insight.state === 'off';const when = o.updated_at ? ui.when(o.updated_at) : null;const meta = [o.people ? `${o.people} ${o.people === 1 ? 'person' : 'people'}` : '', o.messages ? `${o.messages} messages` : '', when && when.text ? `updated ${when.text}` : ''].filter(Boolean);-- return `<details class="oc-more oc-insight"><summary class="oc-icon-btn" aria-label="AI overview of this room" title="AI overview">${IC.spark}</summary><div class="oc-pop oc-insight-pop">`++ return `<details class="oc-more oc-insight"><summary class="oc-icon-btn" aria-label="AI overview of this room" title="AI overview">${IC.steps}</summary><div class="oc-pop oc-insight-pop">`+ (ready ? `<p>${esc(o.text)}</p>` : `<p class="oc-muted">${off ? 'AI insight is off for this room.' : 'Quiet right now: there is nothing new to summarise.'}</p>`)+ (topics.length ? `<ul class="oc-insight-topics">${topics.map((t) => `<li>${esc(t.label)} <span class="oc-muted">${Number(t.count) || 0}</span></li>`).join('')}</ul>` : '')+ (events.length ? `<ol class="oc-insight-timeline">${events.map((e) => `<li><time>${esc(ui.when(e.at).text || '')}</time> <strong>${esc(e.title)}</strong>${e.text ? ` <span class="oc-muted">${esc(e.text)}</span>` : ''}</li>`).join('')}</ol>` : '')@@@ -324,7 +324,7 @@ function createWebRoutes({ config }) {rail: shell.rail({ global: true, dmUnread: dms.reduce((n, d) => n + d.unread, 0), servers: shell.railServers(mineServers) }),side: shell.sideHome({ dms, friends: contacts.friends, requests: contacts.requests, you: { name: me.display_name || me.username, status: 'Online', avatarUrl: me.avatar_url || '' } }),sideLabel: 'Home',-- main: `<header class="oc-chan-head">${shell.toList}<h1 id="oc-h" class="oc-chan-title">Global chat</h1><span class="oc-muted oc-chan-topic">Everyone on OpenVibe${home.peopleLine(activity)}</span><span class="oc-chan-tools"><span class="oc-live" id="oc-live" hidden>● live</span><a class="oc-icon-btn" href="/vibe" aria-label="Chat with Vibe" title="Chat with Vibe">${IC.spark}</a><a class="oc-icon-btn" href="/settings" aria-label="Chat settings" title="Chat settings">${IC.gear}</a></span>${insightHtml}</header>++ main: `<header class="oc-chan-head">${shell.toList}<h1 id="oc-h" class="oc-chan-title">Global chat</h1><span class="oc-muted oc-chan-topic">Everyone on OpenVibe${home.peopleLine(activity)}</span><span class="oc-chan-tools"><span class="oc-live" id="oc-live" hidden>● live</span>${insightHtml}<a class="oc-icon-btn" href="/vibe" aria-label="Chat with Vibe" title="Chat with Vibe">${IC.spark}</a><a class="oc-icon-btn" href="/settings" aria-label="Chat settings" title="Chat settings">${IC.gear}</a></span></header>${error}${feedList}${composer}`,nnew file mode 100644@@@ -0,0 +1,13 @@++'use strict';++// The room's AI overview in a header (server/web/insight-block.js): its own mark (a list), not the sparkle that opens++// Vibe beside it, and the page puts it among the header's tools rather than on a row of its own.++const assert = require('node:assert/strict');++const fs = require('node:fs');++const path = require('node:path');++const { insightBlock } = require('../server/web/insight-block');++const html = insightBlock({ state: 'ready', overview: { text: 'People are planning a game night.', topics: [], people: 3, messages: 12 }, timeline: [] });++assert.match(html, /aria-label="AI overview of this room"/);++assert.ok(html.includes('M9 6h11M9 12h11M9 18h11'), 'the overview has the list mark');++const pages = fs.readFileSync(path.join(__dirname, '../server/web/pages.js'), 'utf8');++assert.match(pages, /<span class="oc-chan-tools"><span class="oc-live" id="oc-live" hidden>● live<\/span>\$\{insightHtml\}/, 'Global chat puts it among the tools');++console.log('insight-block-icon: the overview has its own mark and sits among the header tools');
@@ -638,6 +638,10 @@ h2 { font-size: 1.15rem; margin: 1.4em 0 .5em; }
@@ -638,6 +638,10 @@ h2 { font-size: 1.15rem; margin: 1.4em 0 .5em; }
.oc-snip > pre { max-height: 50vh; overflow: auto; padding-right: 48px; }
.oc-snip > pre { max-height: 50vh; overflow: auto; padding-right: 48px; }
/* A header's "more" menu (a conversation's Block): a small panel under its button. */
/* A header's "more" menu (a conversation's Block): a small panel under its button. */
.oc-more { position: relative; }
.oc-more { position: relative; }
+/* The room's AI overview sits in the header's tools (it wrapped onto a row of its own: .oc-more is also the grid of
+ "more" links above). Opus, 2026-11-11. */
+.oc-chan-tools .oc-more { display: inline-flex; margin: 0; grid-template-columns: none; }
+.oc-chan-tools .oc-insight-pop { width: min(340px, calc(100vw - 32px)); }
.oc-more > summary { list-style: none; }
.oc-more > summary { list-style: none; }
.oc-more > summary::-webkit-details-marker { display: none; }
.oc-more > summary::-webkit-details-marker { display: none; }
.oc-pop { position: absolute; right: 0; top: calc(100% + 4px); z-index: 20; min-width: 200px; padding: 6px; border-radius: 12px; border: 1px solid var(--oc-line); background: var(--oc-panel); box-shadow: 0 12px 28px rgba(0, 0, 0, .35); }
.oc-pop { position: absolute; right: 0; top: calc(100% + 4px); z-index: 20; min-width: 200px; padding: 6px; border-radius: 12px; border: 1px solid var(--oc-line); background: var(--oc-panel); box-shadow: 0 12px 28px rgba(0, 0, 0, .35); }
@@ -17,7 +17,7 @@ function insightBlock(insight) {
@@ -17,7 +17,7 @@ function insightBlock(insight) {
const off = insight && insight.state === 'off';
const off = insight && insight.state === 'off';
const when = o.updated_at ? ui.when(o.updated_at) : null;
const when = o.updated_at ? ui.when(o.updated_at) : null;
const meta = [o.people ? `${o.people} ${o.people === 1 ? 'person' : 'people'}` : '', o.messages ? `${o.messages} messages` : '', when && when.text ? `updated ${when.text}` : ''].filter(Boolean);
const meta = [o.people ? `${o.people} ${o.people === 1 ? 'person' : 'people'}` : '', o.messages ? `${o.messages} messages` : '', when && when.text ? `updated ${when.text}` : ''].filter(Boolean);
- return `<details class="oc-more oc-insight"><summary class="oc-icon-btn" aria-label="AI overview of this room" title="AI overview">${IC.spark}</summary><div class="oc-pop oc-insight-pop">`
+ return `<details class="oc-more oc-insight"><summary class="oc-icon-btn" aria-label="AI overview of this room" title="AI overview">${IC.steps}</summary><div class="oc-pop oc-insight-pop">`
+ (ready ? `<p>${esc(o.text)}</p>` : `<p class="oc-muted">${off ? 'AI insight is off for this room.' : 'Quiet right now: there is nothing new to summarise.'}</p>`)
+ (ready ? `<p>${esc(o.text)}</p>` : `<p class="oc-muted">${off ? 'AI insight is off for this room.' : 'Quiet right now: there is nothing new to summarise.'}</p>`)
+ (topics.length ? `<ul class="oc-insight-topics">${topics.map((t) => `<li>${esc(t.label)} <span class="oc-muted">${Number(t.count) || 0}</span></li>`).join('')}</ul>` : '')
+ (topics.length ? `<ul class="oc-insight-topics">${topics.map((t) => `<li>${esc(t.label)} <span class="oc-muted">${Number(t.count) || 0}</span></li>`).join('')}</ul>` : '')
+ (events.length ? `<ol class="oc-insight-timeline">${events.map((e) => `<li><time>${esc(ui.when(e.at).text || '')}</time> <strong>${esc(e.title)}</strong>${e.text ? ` <span class="oc-muted">${esc(e.text)}</span>` : ''}</li>`).join('')}</ol>` : '')
+ (events.length ? `<ol class="oc-insight-timeline">${events.map((e) => `<li><time>${esc(ui.when(e.at).text || '')}</time> <strong>${esc(e.title)}</strong>${e.text ? ` <span class="oc-muted">${esc(e.text)}</span>` : ''}</li>`).join('')}</ol>` : '')
@@ -324,7 +324,7 @@ function createWebRoutes({ config }) {
@@ -324,7 +324,7 @@ function createWebRoutes({ config }) {
rail: shell.rail({ global: true, dmUnread: dms.reduce((n, d) => n + d.unread, 0), servers: shell.railServers(mineServers) }),
rail: shell.rail({ global: true, dmUnread: dms.reduce((n, d) => n + d.unread, 0), servers: shell.railServers(mineServers) }),
side: shell.sideHome({ dms, friends: contacts.friends, requests: contacts.requests, you: { name: me.display_name || me.username, status: 'Online', avatarUrl: me.avatar_url || '' } }),
side: shell.sideHome({ dms, friends: contacts.friends, requests: contacts.requests, you: { name: me.display_name || me.username, status: 'Online', avatarUrl: me.avatar_url || '' } }),
sideLabel: 'Home',
sideLabel: 'Home',
- main: `<header class="oc-chan-head">${shell.toList}<h1 id="oc-h" class="oc-chan-title">Global chat</h1><span class="oc-muted oc-chan-topic">Everyone on OpenVibe${home.peopleLine(activity)}</span><span class="oc-chan-tools"><span class="oc-live" id="oc-live" hidden>● live</span><a class="oc-icon-btn" href="/vibe" aria-label="Chat with Vibe" title="Chat with Vibe">${IC.spark}</a><a class="oc-icon-btn" href="/settings" aria-label="Chat settings" title="Chat settings">${IC.gear}</a></span>${insightHtml}</header>
+ main: `<header class="oc-chan-head">${shell.toList}<h1 id="oc-h" class="oc-chan-title">Global chat</h1><span class="oc-muted oc-chan-topic">Everyone on OpenVibe${home.peopleLine(activity)}</span><span class="oc-chan-tools"><span class="oc-live" id="oc-live" hidden>● live</span>${insightHtml}<a class="oc-icon-btn" href="/vibe" aria-label="Chat with Vibe" title="Chat with Vibe">${IC.spark}</a><a class="oc-icon-btn" href="/settings" aria-label="Chat settings" title="Chat settings">${IC.gear}</a></span></header>
${error}
${error}
${feedList}
${feedList}
${composer}`,
${composer}`,
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+'use strict';
+// The room's AI overview in a header (server/web/insight-block.js): its own mark (a list), not the sparkle that opens
+// Vibe beside it, and the page puts it among the header's tools rather than on a row of its own.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { insightBlock } = require('../server/web/insight-block');
+const html = insightBlock({ state: 'ready', overview: { text: 'People are planning a game night.', topics: [], people: 3, messages: 12 }, timeline: [] });
+assert.match(html, /aria-label="AI overview of this room"/);
+assert.ok(html.includes('M9 6h11M9 12h11M9 18h11'), 'the overview has the list mark');
+const pages = fs.readFileSync(path.join(__dirname, '../server/web/pages.js'), 'utf8');
+assert.match(pages, /<span class="oc-chan-tools"><span class="oc-live" id="oc-live" hidden>● live<\/span>\$\{insightHtml\}/, 'Global chat puts it among the tools');
+console.log('insight-block-icon: the overview has its own mark and sits among the header tools');