openvibers/openvibe.news Commits
News: empty briefs and live lists draw the kit's empty state (#268)
Changes
@@@ -172,7 +172,7 @@ function createPublicRoutes(ctx) {const canMake = () => Boolean(briefs && config.briefs.enabled && ctx.ai && ctx.ai.enabled);router.get('/briefs', wrap(async (req, res) => {-- send(req, res, 200, { title: 'AI briefs', description: 'What the reports on a subject say, summarized by AI with every paragraph linked to its reports.', decision: noindex('/briefs'), canonical: `${config.baseUrl}/briefs`, localStyles: ['css/briefs.css'], body: briefPages.briefsIndex({ briefs: briefs ? await briefs.latest(40) : [] }) }, { cacheable: true });++ send(req, res, 200, { title: 'AI briefs', description: 'What the reports on a subject say, summarized by AI with every paragraph linked to its reports.', decision: noindex('/briefs'), canonical: `${config.baseUrl}/briefs`, localStyles: ['css/briefs.css'], styles: ['kit.css'], body: briefPages.briefsIndex({ briefs: briefs ? await briefs.latest(40) : [] }) }, { cacheable: true });}));router.get('/briefs/:id', wrap(async (req, res) => {@@@ -227,7 +227,7 @@ function createPublicRoutes(ctx) {const breaking = new Set(list.filter((c) => c.items.filter((i) => i.published_at && now - Date.parse(i.published_at) <= 2 * 3_600_000).length >= 3).map((c) => c.id));const briefIds = new Set(briefs ? (await briefs.latest(200)).map((b) => b.cluster_id) : []);res.set('Refresh', '60');-- send(req, res, 200, { title: 'Live', description: 'The newest reports from the sources OpenVibe.News follows, as they arrive, with what is breaking.', decision: noindex('/live'), canonical: `${config.baseUrl}/live`, localStyles: ['css/briefs.css'], body: briefPages.livePage({ clusters: list, briefIds, breaking }) }, { cacheable: true });++ send(req, res, 200, { title: 'Live', description: 'The newest reports from the sources OpenVibe.News follows, as they arrive, with what is breaking.', decision: noindex('/live'), canonical: `${config.baseUrl}/live`, localStyles: ['css/briefs.css'], styles: ['kit.css'], body: briefPages.livePage({ clusters: list, briefIds, breaking }) }, { cacheable: true });}));router.get('/topics', wrap(async (req, res) => {@@@ -6,6 +6,7 @@* reports' own headlines and links are the outlets'.*/const ssr = require('openvibe-publishing/ssr');++const kit = require('openvibe-shared/kit');const { time, httpHref } = require('./pages');const { html: h, raw } = ssr;@@@ -89,7 +90,7 @@ function briefsIndex({ briefs }) {return String(h`<header class="site-header"><h1>AI briefs</h1><p class="lede">What the reports on a subject say, summarized by OpenVibe.AI with every paragraph linked to its reports, and labelled as AI. Editors' stories are on <a href="/">the front page</a>.</p></header>${briefs.length ? h`<ol class="story-list">${briefs.map((b) => h`<li class="story-item"><h2 class="story-item-title"><a href="/briefs/${b.cluster_id}">${b.headline || b.label}</a> <span class="badge badge-ai">AI</span></h2><p class="meta">${time(b.window_end)} · from ${b.item_count} reports${b.outlets ? ` by ${b.outlets} outlets` : ''}</p>${b.paragraphs[0] ? h`<p class="summary">${b.paragraphs[0].text.slice(0, 280)}${b.paragraphs[0].text.length > 280 ? '…' : ''}</p>` : ''}</li>`)}</ol>`-- : h`<p class="empty">No briefs yet. One is written once two or more independent outlets have reported a subject.</p>`}`);++ : raw(kit.empty({ icon: 'book', title: 'No briefs yet', text: 'One is written once two or more independent outlets have reported a subject.', action: { href: '/live', label: 'See the newest reports', icon: 'live' } }))}`);}/** /live: the newest reports as they arrive, grouped, with what is breaking. */@@@ -98,7 +99,7 @@ function livePage({ clusters, briefIds, breaking }) {${clusters.length ? h`<ol class="wire-list wide live-list">${clusters.map((c) => h`<li class="wire-cluster${breaking.has(c.id) ? ' is-breaking' : ''}"><p class="wire-lead">${breaking.has(c.id) ? h`<span class="badge badge-breaking">Breaking</span> ` : ''}<strong>${c.label}</strong>${briefIds.has(c.id) ? h` · <a href="/briefs/${c.id}">AI brief</a>` : ''}</p><ul class="wire-items">${c.items.slice(0, 5).map((i) => h`<li>${httpHref(i.url) ? h`<a href="${httpHref(i.url)}" rel="nofollow noopener" target="_blank">${i.headline}</a>` : i.headline} <span class="meta">${i.outlet} · ${time(i.published_at ? Date.parse(i.published_at) : null)}</span></li>`)}</ul></li>`)}</ol>`-- : h`<p class="empty">Nothing new from the sources yet.</p>`}`);++ : raw(kit.empty({ icon: 'live', title: 'Nothing new from the sources yet', text: 'New reports appear here as the sources publish them.', action: { href: '/wire', label: 'Read the wire', icon: 'list' } }))}`);}module.exports = { briefPage, briefsIndex, livePage, AI_NOTE, TONES };@@@ -18,6 +18,13 @@ const SAME = { 'sec-fetch-site': 'same-origin' };try {launchReports(t.sources);await t.pull();++ await check('an empty briefs list draws the kit empty state and one button', async () => {++ const r = await t.get('/briefs');++ assert.strictEqual(r.status, 200);++ assert.match(r.text, /class="ovk-empty"/, 'the kit empty state is used, not a bare sentence');++ assert.match(r.text, /No briefs yet/);++ assert.match(r.text, /class="ovk-btn[^"]*" href="\/live"/, 'the empty state offers one button forward');++ });const clusterId = (await t.db().prepare("SELECT cluster_id, count(*) AS n FROM news_source_items WHERE status = 'active' GROUP BY cluster_id ORDER BY n DESC").get()).cluster_id;await check('the sweep writes a cited summary for a cluster with two or more reports; uncited claims are dropped', async () => {
@@ -172,7 +172,7 @@ function createPublicRoutes(ctx) {
@@ -172,7 +172,7 @@ function createPublicRoutes(ctx) {
const canMake = () => Boolean(briefs && config.briefs.enabled && ctx.ai && ctx.ai.enabled);
const canMake = () => Boolean(briefs && config.briefs.enabled && ctx.ai && ctx.ai.enabled);
router.get('/briefs', wrap(async (req, res) => {
router.get('/briefs', wrap(async (req, res) => {
- send(req, res, 200, { title: 'AI briefs', description: 'What the reports on a subject say, summarized by AI with every paragraph linked to its reports.', decision: noindex('/briefs'), canonical: `${config.baseUrl}/briefs`, localStyles: ['css/briefs.css'], body: briefPages.briefsIndex({ briefs: briefs ? await briefs.latest(40) : [] }) }, { cacheable: true });
+ send(req, res, 200, { title: 'AI briefs', description: 'What the reports on a subject say, summarized by AI with every paragraph linked to its reports.', decision: noindex('/briefs'), canonical: `${config.baseUrl}/briefs`, localStyles: ['css/briefs.css'], styles: ['kit.css'], body: briefPages.briefsIndex({ briefs: briefs ? await briefs.latest(40) : [] }) }, { cacheable: true });
}));
}));
router.get('/briefs/:id', wrap(async (req, res) => {
router.get('/briefs/:id', wrap(async (req, res) => {
@@ -227,7 +227,7 @@ function createPublicRoutes(ctx) {
@@ -227,7 +227,7 @@ function createPublicRoutes(ctx) {
const breaking = new Set(list.filter((c) => c.items.filter((i) => i.published_at && now - Date.parse(i.published_at) <= 2 * 3_600_000).length >= 3).map((c) => c.id));
const breaking = new Set(list.filter((c) => c.items.filter((i) => i.published_at && now - Date.parse(i.published_at) <= 2 * 3_600_000).length >= 3).map((c) => c.id));
const briefIds = new Set(briefs ? (await briefs.latest(200)).map((b) => b.cluster_id) : []);
const briefIds = new Set(briefs ? (await briefs.latest(200)).map((b) => b.cluster_id) : []);
res.set('Refresh', '60');
res.set('Refresh', '60');
- send(req, res, 200, { title: 'Live', description: 'The newest reports from the sources OpenVibe.News follows, as they arrive, with what is breaking.', decision: noindex('/live'), canonical: `${config.baseUrl}/live`, localStyles: ['css/briefs.css'], body: briefPages.livePage({ clusters: list, briefIds, breaking }) }, { cacheable: true });
+ send(req, res, 200, { title: 'Live', description: 'The newest reports from the sources OpenVibe.News follows, as they arrive, with what is breaking.', decision: noindex('/live'), canonical: `${config.baseUrl}/live`, localStyles: ['css/briefs.css'], styles: ['kit.css'], body: briefPages.livePage({ clusters: list, briefIds, breaking }) }, { cacheable: true });
}));
}));
router.get('/topics', wrap(async (req, res) => {
router.get('/topics', wrap(async (req, res) => {
@@ -6,6 +6,7 @@
@@ -6,6 +6,7 @@
* reports' own headlines and links are the outlets'.
* reports' own headlines and links are the outlets'.
*/
*/
const ssr = require('openvibe-publishing/ssr');
const ssr = require('openvibe-publishing/ssr');
+const kit = require('openvibe-shared/kit');
const { time, httpHref } = require('./pages');
const { time, httpHref } = require('./pages');
const { html: h, raw } = ssr;
const { html: h, raw } = ssr;
@@ -89,7 +90,7 @@ function briefsIndex({ briefs }) {
@@ -89,7 +90,7 @@ function briefsIndex({ briefs }) {
return String(h`<header class="site-header"><h1>AI briefs</h1><p class="lede">What the reports on a subject say, summarized by OpenVibe.AI with every paragraph linked to its reports, and labelled as AI. Editors' stories are on <a href="/">the front page</a>.</p></header>
return String(h`<header class="site-header"><h1>AI briefs</h1><p class="lede">What the reports on a subject say, summarized by OpenVibe.AI with every paragraph linked to its reports, and labelled as AI. Editors' stories are on <a href="/">the front page</a>.</p></header>
${briefs.length ? h`<ol class="story-list">${briefs.map((b) => h`<li class="story-item"><h2 class="story-item-title"><a href="/briefs/${b.cluster_id}">${b.headline || b.label}</a> <span class="badge badge-ai">AI</span></h2>
${briefs.length ? h`<ol class="story-list">${briefs.map((b) => h`<li class="story-item"><h2 class="story-item-title"><a href="/briefs/${b.cluster_id}">${b.headline || b.label}</a> <span class="badge badge-ai">AI</span></h2>
<p class="meta">${time(b.window_end)} · from ${b.item_count} reports${b.outlets ? ` by ${b.outlets} outlets` : ''}</p>${b.paragraphs[0] ? h`<p class="summary">${b.paragraphs[0].text.slice(0, 280)}${b.paragraphs[0].text.length > 280 ? '…' : ''}</p>` : ''}</li>`)}</ol>`
<p class="meta">${time(b.window_end)} · from ${b.item_count} reports${b.outlets ? ` by ${b.outlets} outlets` : ''}</p>${b.paragraphs[0] ? h`<p class="summary">${b.paragraphs[0].text.slice(0, 280)}${b.paragraphs[0].text.length > 280 ? '…' : ''}</p>` : ''}</li>`)}</ol>`
- : h`<p class="empty">No briefs yet. One is written once two or more independent outlets have reported a subject.</p>`}`);
+ : raw(kit.empty({ icon: 'book', title: 'No briefs yet', text: 'One is written once two or more independent outlets have reported a subject.', action: { href: '/live', label: 'See the newest reports', icon: 'live' } }))}`);
}
}
/** /live: the newest reports as they arrive, grouped, with what is breaking. */
/** /live: the newest reports as they arrive, grouped, with what is breaking. */
@@ -98,7 +99,7 @@ function livePage({ clusters, briefIds, breaking }) {
@@ -98,7 +99,7 @@ function livePage({ clusters, briefIds, breaking }) {
${clusters.length ? h`<ol class="wire-list wide live-list">${clusters.map((c) => h`<li class="wire-cluster${breaking.has(c.id) ? ' is-breaking' : ''}">
${clusters.length ? h`<ol class="wire-list wide live-list">${clusters.map((c) => h`<li class="wire-cluster${breaking.has(c.id) ? ' is-breaking' : ''}">
<p class="wire-lead">${breaking.has(c.id) ? h`<span class="badge badge-breaking">Breaking</span> ` : ''}<strong>${c.label}</strong>${briefIds.has(c.id) ? h` · <a href="/briefs/${c.id}">AI brief</a>` : ''}</p>
<p class="wire-lead">${breaking.has(c.id) ? h`<span class="badge badge-breaking">Breaking</span> ` : ''}<strong>${c.label}</strong>${briefIds.has(c.id) ? h` · <a href="/briefs/${c.id}">AI brief</a>` : ''}</p>
<ul class="wire-items">${c.items.slice(0, 5).map((i) => h`<li>${httpHref(i.url) ? h`<a href="${httpHref(i.url)}" rel="nofollow noopener" target="_blank">${i.headline}</a>` : i.headline} <span class="meta">${i.outlet} · ${time(i.published_at ? Date.parse(i.published_at) : null)}</span></li>`)}</ul></li>`)}</ol>`
<ul class="wire-items">${c.items.slice(0, 5).map((i) => h`<li>${httpHref(i.url) ? h`<a href="${httpHref(i.url)}" rel="nofollow noopener" target="_blank">${i.headline}</a>` : i.headline} <span class="meta">${i.outlet} · ${time(i.published_at ? Date.parse(i.published_at) : null)}</span></li>`)}</ul></li>`)}</ol>`
- : h`<p class="empty">Nothing new from the sources yet.</p>`}`);
+ : raw(kit.empty({ icon: 'live', title: 'Nothing new from the sources yet', text: 'New reports appear here as the sources publish them.', action: { href: '/wire', label: 'Read the wire', icon: 'list' } }))}`);
}
}
module.exports = { briefPage, briefsIndex, livePage, AI_NOTE, TONES };
module.exports = { briefPage, briefsIndex, livePage, AI_NOTE, TONES };
@@ -18,6 +18,13 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
@@ -18,6 +18,13 @@ const SAME = { 'sec-fetch-site': 'same-origin' };
try {
try {
launchReports(t.sources);
launchReports(t.sources);
await t.pull();
await t.pull();
+ await check('an empty briefs list draws the kit empty state and one button', async () => {
+ const r = await t.get('/briefs');
+ assert.strictEqual(r.status, 200);
+ assert.match(r.text, /class="ovk-empty"/, 'the kit empty state is used, not a bare sentence');
+ assert.match(r.text, /No briefs yet/);
+ assert.match(r.text, /class="ovk-btn[^"]*" href="\/live"/, 'the empty state offers one button forward');
+ });
const clusterId = (await t.db().prepare("SELECT cluster_id, count(*) AS n FROM news_source_items WHERE status = 'active' GROUP BY cluster_id ORDER BY n DESC").get()).cluster_id;
const clusterId = (await t.db().prepare("SELECT cluster_id, count(*) AS n FROM news_source_items WHERE status = 'active' GROUP BY cluster_id ORDER BY n DESC").get()).cluster_id;
await check('the sweep writes a cited summary for a cluster with two or more reports; uncited claims are dropped', async () => {
await check('the sweep writes a cited summary for a cluster with two or more reports; uncited claims are dropped', async () => {