openvibers/openvibe.mediahub Commits

Show agent tests cost and work time on VOD playback (#396)

OP OpenVibers committed 2 days ago · 4a3d8a8aab · 3 files, +11 −3

Changes

ddiff --git a/public/css/video.css b/public/css/video.css
iindex c3dae11..d7ebc0c 100644
---- a/public/css/video.css
++++ b/public/css/video.css
@@@ -119,7 +119,8 @@ body:has(> main.ovv-page) .openvibe-navbar.ovnav-top { background-color: var(--b
.ovv-agent-current[data-status="failed"]::before, .ovv-agent-tick[data-status="failed"] { background: #fb7185; }
.ovv-agent-ticks { display: flex; gap: 3px; height: 3px; }
.ovv-agent-tick { flex: 1; background: #60a5fa; border-radius: 2px; }
--@media (max-width: 600px) { .ovv-agent-overlay { bottom: 40px; padding: 7px 9px; } .ovv-agent-overlay strong, .ovv-agent-task, .ovv-agent-ticks { display: none; } }
++.ovv-agent-meta { color: #b8c6d9; font-size: 11px; }
++@media (max-width: 600px) { .ovv-agent-overlay { bottom: 40px; padding: 7px 9px; } .ovv-agent-overlay strong, .ovv-agent-task, .ovv-agent-ticks, .ovv-agent-meta { display: none; } }
.ovv-player-wrap.ov-player-host { box-shadow: none; border-color: transparent; }
/* The picture's shape once it plays (video.js followShape: --ovv-ar), eased when a recording turns mid-stream; 16:9
before that. The height is capped so the title under the player stays on screen. */
ddiff --git a/public/js/video.js b/public/js/video.js
iindex 14c89ac..decc64e 100644
---- a/public/js/video.js
++++ b/public/js/video.js
@@@ -30,6 +30,7 @@
if (!Array.isArray(steps)) return;
const current = box.querySelector('.ovv-agent-current');
const timeline = box.querySelector('.ovv-agent-ticks');
++ const meta = box.querySelector('.ovv-agent-meta');
let shown = -2;
function sync() {
let index = -1;
@@@ -37,9 +38,15 @@
if (steps[i].at <= video.currentTime + .25) index = i;
else break;
}
++ const step = index >= 0 ? steps[index] : null;
++ const parts = [];
++ if (step && step.tests) parts.push(step.tests.passed + ' of ' + (step.tests.passed + step.tests.failed) + ' passing');
++ if (step && Number.isFinite(Number(step.cost))) parts.push('$' + Number(step.cost).toFixed(2));
++ parts.push('worked ' + Math.round(video.currentTime / 60) + ' min');
++ const line = parts.join(' · ');
++ if (meta && meta.textContent !== line) meta.textContent = line;
if (index === shown) return;
shown = index;
-- const step = index >= 0 ? steps[index] : null;
current.textContent = step ? step.title : 'Waiting for the first step';
current.setAttribute('data-status', step ? step.status : 'started');
timeline.replaceChildren();
ddiff --git a/server/video/pages.js b/server/video/pages.js
iindex 3e1c594..988343c 100644
---- a/server/video/pages.js
++++ b/server/video/pages.js
@@@ -1515,7 +1515,7 @@ ${found ? sections.map((x) => (x.cards.length || kind ? html`<section class="ovv
<div class="ovv-col ovv-col--main">
<div class="ovv-stage">
${savedNotice(req)}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this recording.', 'info') : ''}
--<div class="ovv-player-wrap"${labelsUi.attrs(labelsUi.inForce(vod), { owner: vod.username, opened: true })}><video class="ovv-player" data-ov-player${miniAttr(vod.title || 'Stream recording', channelName || vod.username, watchPath, fileOf(vod))} controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))}${avatarAttr(vod.username, config.networkUrl)}${shapeAttr} src="${fileOf(vod) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}>${segs.length ? html`<track kind="captions" label="Transcript (AI)" src="/v/${id}/captions.vtt" default>` : ''}${mems.length && !ownOnly ? html`<track kind="chapters" src="/v/${id}/chapters.vtt">` : ''}</video>${pipsTag(pips)}${agentSteps ? html`<div class="ovv-agent-overlay" data-ovv-agent="${JSON.stringify(agentSteps.map((s) => ({ at: Number(s.offset_seconds), title: s.title, status: s.status, tests: s.tests, cost: s.cost_usd })))}"><strong>AI agent</strong><span class="ovv-agent-task">${agent.task || vod.title}</span><span class="ovv-agent-current" aria-live="polite">Waiting for the first step</span><span class="ovv-agent-ticks" aria-label="Step timeline"></span></div>` : ''}</div>
++<div class="ovv-player-wrap"${labelsUi.attrs(labelsUi.inForce(vod), { owner: vod.username, opened: true })}><video class="ovv-player" data-ov-player${miniAttr(vod.title || 'Stream recording', channelName || vod.username, watchPath, fileOf(vod))} controls preload="metadata" playsinline${posterAttr(httpsUrl(vod.thumbnail_url))}${avatarAttr(vod.username, config.networkUrl)}${shapeAttr} src="${fileOf(vod) || ''}" data-ovv-video${nextAttr}${viewAttr}${hlsAttr}>${segs.length ? html`<track kind="captions" label="Transcript (AI)" src="/v/${id}/captions.vtt" default>` : ''}${mems.length && !ownOnly ? html`<track kind="chapters" src="/v/${id}/chapters.vtt">` : ''}</video>${pipsTag(pips)}${agentSteps ? html`<div class="ovv-agent-overlay" data-ovv-agent="${JSON.stringify(agentSteps.map((s) => ({ at: Number(s.offset_seconds), title: s.title, status: s.status, tests: s.tests, cost: s.cost_usd })))}"><strong>AI agent</strong><span class="ovv-agent-task">${agent.task || vod.title}</span><span class="ovv-agent-current" aria-live="polite">Waiting for the first step</span><span class="ovv-agent-ticks" aria-label="Step timeline"></span><span class="ovv-agent-meta"></span></div>` : ''}</div>
<div class="ovv-head">
${titleLine('Stream recording', vod.title || 'Stream recording')}
${labelsUi.chips(labelsUi.inForce(vod))}