openvibers/openvibe.live Commits

Live: AI recaps look like reports on the feed, not videos; Game leaves the navbar. (#49)

OP OpenVibers committed 8 Oct 2026 · 4be1e3f759 · 3 files, +63 −8

- A recap card has a short report band (the grade large, AI recap, After-show report, its live time and peak) over
  the stream's thumbnail blurred and tinted, a quoted excerpt and Read the recap: it no longer copies the 16:9 video
  card with a duration pill, which made recaps look like VODs and AI moments.
- The navbar drops Game (openvibe.games is linked from the home page and the network menu).


Claude-Session: https://claude.ai/code/session_01K8AYn3FF3HE7CPfM262qdh

Co-authored-by: Claude Opus 5.5 <[email protected]>

Changes

ddiff --git a/public/css/features/content.css b/public/css/features/content.css
iindex 83ca54dc..8390b827 100644
---- a/public/css/features/content.css
++++ b/public/css/features/content.css
@@@ -387,6 +387,51 @@ a.cf-chan:hover .cf-name { color: var(--text-primary); text-decoration: underlin
.cf-grade--b { color: #60a5fa; }
.cf-grade--c { color: #94a3b8; }
++/* ── AI recap: a report card, not a video card (content-feed.js recapCard) ── */
++.cf-card--recap { background: linear-gradient(180deg, color-mix(in srgb, var(--cf-ai) 7%, var(--bg-card)), var(--bg-card) 55%); }
++.cf-recap-band {
++ position: relative;
++ display: flex;
++ align-items: center;
++ gap: 14px;
++ min-height: 104px;
++ padding: 16px;
++ overflow: hidden;
++ color: inherit;
++ text-decoration: none;
++ isolation: isolate;
++ border-bottom: 1px dashed color-mix(in srgb, var(--cf-ai) 35%, var(--border));
++ background:
++ radial-gradient(120% 140% at 0% 0%, color-mix(in srgb, var(--cf-ai) 34%, transparent), transparent 62%),
++ radial-gradient(90% 120% at 100% 100%, color-mix(in srgb, var(--cf-ai-2) 22%, transparent), transparent 60%),
++ var(--bg-secondary);
++}
++.cf-recap-bg { position: absolute; inset: -12px; width: calc(100% + 24px); height: calc(100% + 24px); object-fit: cover; filter: blur(14px) saturate(1.2); opacity: .28; z-index: -1; }
++.cf-recap-grade {
++ flex: 0 0 auto;
++ width: 64px;
++ height: 64px;
++ display: grid;
++ place-items: center;
++ border-radius: 18px;
++ font-weight: 900;
++ font-size: 2rem;
++ line-height: 1;
++ background: rgba(8, 12, 22, 0.72);
++ border: 2px solid currentColor;
++ box-shadow: 0 8px 24px color-mix(in srgb, currentColor 22%, transparent);
++}
++.cf-recap-grade--none { color: var(--cf-ai); font-size: 1.5rem; }
++.cf-recap-head { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
++.cf-recap-label { font-size: .78rem; font-weight: 800; letter-spacing: .6px; text-transform: uppercase; color: var(--cf-ai); }
++.cf-recap-sub { font-size: .92rem; font-weight: 700; color: var(--text-primary); }
++.cf-recap-stats { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: .82rem; color: var(--text-secondary); }
++.cf-recap-stats b { color: var(--text-primary); font-variant-numeric: tabular-nums; }
++a.cf-recap-band:hover .cf-recap-sub { text-decoration: underline; text-underline-offset: 3px; }
++.cf-excerpt--recap { border-left: 2px solid color-mix(in srgb, var(--cf-ai) 45%, transparent); padding-left: 10px; font-style: italic; }
++.cf-recap-read { display: inline-flex; align-items: center; gap: 6px; margin-top: 8px; font-size: .84rem; font-weight: 700; color: var(--cf-ai); text-decoration: none; }
++.cf-recap-read:hover { text-decoration: underline; }
++
/* staff bulk-select wrapper (app.js _selWrap) around a feed card */
.cf-col > .sel-card-wrap { position: relative; }
.cf-col > .sel-card-wrap > .sel-card-check { z-index: 4; }
ddiff --git a/public/js/content-feed.js b/public/js/content-feed.js
iindex e3e27430..b3c795fb 100644
---- a/public/js/content-feed.js
++++ b/public/js/content-feed.js
@@@ -168,17 +168,28 @@
return el('article', { class: `cf-card cf-card--paste${it.ai ? ' cf-card--ai' : ''}` }, media, body);
}
++ // A recap is a report about a stream, not a video: a short report band (the grade, "AI recap", the stream's numbers)
++ // over the stream's thumbnail, blurred and tinted, instead of a 16:9 player frame with a duration pill.
function recapCard(it) {
-- const media = spa(it.href, { class: 'cf-media cf-media--recap', 'aria-label': `Read the recap: ${it.title}` },
-- it.thumbnail_url ? thumb(it.thumbnail_url, 'fa-clipboard-list') : el('span', { class: 'cf-thumb cf-thumb--ph' }, icon('fa-clipboard-list')),
-- aiBadge());
-- if (it.grade) media.append(el('span', { class: `cf-grade cf-grade--${it.grade.toLowerCase()}`, title: `Grade ${it.grade}`, text: it.grade }));
-- if (it.duration_seconds) media.append(el('span', { class: 'cf-pill cf-dur', text: duration(it.duration_seconds) }));
++ const band = spa(it.href, { class: 'cf-recap-band', 'aria-label': `Read the recap: ${it.title}` });
++ if (it.thumbnail_url) {
++ const bg = el('img', { class: 'cf-recap-bg', src: it.thumbnail_url, alt: '', loading: 'lazy', decoding: 'async' });
++ bg.addEventListener('error', () => bg.remove(), { once: true });
++ band.append(bg);
++ }
++ band.append(it.grade
++ ? el('span', { class: `cf-recap-grade cf-grade--${it.grade.toLowerCase()}`, title: `Grade ${it.grade}`, text: it.grade })
++ : el('span', { class: 'cf-recap-grade cf-recap-grade--none' }, icon('fa-clipboard-list')));
++ const stats = el('span', { class: 'cf-recap-stats' });
++ if (it.duration_seconds) stats.append(el('span', null, el('b', { text: duration(it.duration_seconds) }), ' live'));
++ if (it.views) stats.append(el('span', null, el('b', { text: compact(it.views) }), ' peak'));
++ band.append(el('span', { class: 'cf-recap-head' }, el('span', { class: 'cf-recap-label' }, icon('fa-wand-magic-sparkles'), ' AI recap'), el('span', { class: 'cf-recap-sub', text: 'After-show report' }), stats));
const body = el('div', { class: 'cf-body' }, spa(it.href, { class: 'cf-title', text: it.title }));
if (it.stream_title && it.stream_title !== it.title) body.append(el('div', { class: 'cf-streamtitle', text: it.stream_title }));
body.append(meta(it));
-- if (it.excerpt) body.append(el('p', { class: 'cf-excerpt', text: it.excerpt }));
-- return el('article', { class: 'cf-card cf-card--recap cf-card--ai' }, media, body);
++ if (it.excerpt) body.append(el('p', { class: 'cf-excerpt cf-excerpt--recap', text: it.excerpt }));
++ body.append(spa(it.href, { class: 'cf-recap-read' }, 'Read the recap ', icon('fa-arrow-right')));
++ return el('article', { class: 'cf-card cf-card--recap cf-card--ai' }, band, body);
}
const BUILD = { vod: vodCard, clip: clipCard, paste: pasteCard, recap: recapCard };
ddiff --git a/public/js/ov-navbar-live.js b/public/js/ov-navbar-live.js
iindex a7c0ab3e..003e0559 100644
---- a/public/js/ov-navbar-live.js
++++ b/public/js/ov-navbar-live.js
@@@ -26,7 +26,6 @@
{ id: 'search', page: 'search', label: 'Search', href: '/search', icon: 'fa-magnifying-glass' },
{ id: 'chat', page: 'chat', label: 'Chat', href: '/chat', icon: 'fa-comments' },
{ id: 'arena', page: 'arena', label: 'Arena', href: '/arena', icon: 'fa-hand-fist' },
-- { id: 'game', label: 'Game', href: 'https://openvibe.games', icon: 'fa-gamepad' },
{ id: 'broadcast', page: 'broadcast', elId: 'nav-broadcast', label: 'Go Live', href: '/broadcast', icon: 'fa-tower-broadcast', dot: false, dotId: 'nav-live-dot',
onClick: (e) => call('goLiveNav', e),
children: user ? [{ id: 'dashboard', label: 'Dashboard', href: '/dashboard', icon: 'fa-gauge-high', onClick: (e) => call('dashNav', e) }] : [] },