openvibers/openvibe.mediahub Commits
Show agent tests cost and work time on VOD playback (#396)
Changes
@@@ -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:9before that. The height is capped so the title under the player stays on screen. */@@@ -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();@@@ -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))}
@@ -119,7 +119,8 @@ body:has(> main.ovv-page) .openvibe-navbar.ovnav-top { background-color: var(--b
@@ -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-current[data-status="failed"]::before, .ovv-agent-tick[data-status="failed"] { background: #fb7185; }
.ovv-agent-ticks { display: flex; gap: 3px; height: 3px; }
.ovv-agent-ticks { display: flex; gap: 3px; height: 3px; }
.ovv-agent-tick { flex: 1; background: #60a5fa; border-radius: 2px; }
.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; }
.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
/* 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. */
before that. The height is capped so the title under the player stays on screen. */
@@ -30,6 +30,7 @@
@@ -30,6 +30,7 @@
if (!Array.isArray(steps)) return;
if (!Array.isArray(steps)) return;
const current = box.querySelector('.ovv-agent-current');
const current = box.querySelector('.ovv-agent-current');
const timeline = box.querySelector('.ovv-agent-ticks');
const timeline = box.querySelector('.ovv-agent-ticks');
+ const meta = box.querySelector('.ovv-agent-meta');
let shown = -2;
let shown = -2;
function sync() {
function sync() {
let index = -1;
let index = -1;
@@ -37,9 +38,15 @@
@@ -37,9 +38,15 @@
if (steps[i].at <= video.currentTime + .25) index = i;
if (steps[i].at <= video.currentTime + .25) index = i;
else break;
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;
if (index === shown) return;
shown = index;
shown = index;
- const step = index >= 0 ? steps[index] : null;
current.textContent = step ? step.title : 'Waiting for the first step';
current.textContent = step ? step.title : 'Waiting for the first step';
current.setAttribute('data-status', step ? step.status : 'started');
current.setAttribute('data-status', step ? step.status : 'started');
timeline.replaceChildren();
timeline.replaceChildren();
@@ -1515,7 +1515,7 @@ ${found ? sections.map((x) => (x.cards.length || kind ? html`<section class="ovv
@@ -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-col ovv-col--main">
<div class="ovv-stage">
<div class="ovv-stage">
${savedNotice(req)}${ownOnly ? notice('Private: only you (and OpenVibe staff) can see this recording.', 'info') : ''}
${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">
<div class="ovv-head">
${titleLine('Stream recording', vod.title || 'Stream recording')}
${titleLine('Stream recording', vod.title || 'Stream recording')}
${labelsUi.chips(labelsUi.inForce(vod))}
${labelsUi.chips(labelsUi.inForce(vod))}