From 3574ae8a4392a8e838d68171772296b72a9aa476 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Fri, 22 May 2026 10:04:23 -0400 Subject: [PATCH] feat(ui): wire screens to live API data; add thumbnail lazy-loading: visuals.jsx --- services/web-ui/public/visuals.jsx | 102 ++++++++++++++++------------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/services/web-ui/public/visuals.jsx b/services/web-ui/public/visuals.jsx index 754d245..b69988f 100644 --- a/services/web-ui/public/visuals.jsx +++ b/services/web-ui/public/visuals.jsx @@ -1,32 +1,47 @@ -// visuals.jsx - reusable visual elements: thumbnails, waveforms, sparklines, filmstrips +// visuals.jsx - reusable visual elements -function AssetThumb({ asset, size = "md" }) { - const aspect = size === "tall" ? "9 / 16" : "16 / 9"; - const seed = asset.seed || 1; +const _thumbCache = new Map(); - if (asset.type === "audio") { +function AssetThumb({ asset, size = 'md' }) { + const aspect = size === 'tall' ? '9 / 16' : '16 / 9'; + const [thumbUrl, setThumbUrl] = React.useState(_thumbCache.get(asset.id) || null); + + React.useEffect(() => { + if (!asset.id || thumbUrl || !asset.thumbnail_s3_key) return; + let cancelled = false; + fetch('/api/v1/assets/' + asset.id + '/thumbnail', { credentials: 'include' }) + .then(r => r.ok ? r.json() : null) + .then(d => { if (!cancelled && d && d.url) { _thumbCache.set(asset.id, d.url); setThumbUrl(d.url); } }) + .catch(() => {}); + return () => { cancelled = true; }; + }, [asset.id, asset.thumbnail_s3_key]); + + if (asset.type === 'audio' || asset.media_type === 'audio') { return (
- -
- -
+ +
); } return ( -
- +
+ {thumbUrl + ? + : } + {asset.status === 'live' && ( +
+ )}
); } function FauxFrame({ seed }) { - return
; + return
; } -function Waveform({ seed = 1, color = "var(--accent)", className = "" }) { +function Waveform({ seed = 1, color = 'var(--accent)', className = '' }) { const bars = 60; const pts = React.useMemo(() => { return Array.from({ length: bars }).map((_, i) => { @@ -34,9 +49,8 @@ function Waveform({ seed = 1, color = "var(--accent)", className = "" }) { return Math.max(0.1, Math.min(1, 0.5 + n * 0.5)); }); }, [seed]); - return ( - + {pts.map((p, i) => ( ))} @@ -48,17 +62,14 @@ function LiveStrip({ seed = 1, count = 8 }) { return (
{Array.from({ length: count }).map((_, i) => ( -
+
))} -
- - NOW -
+
NOW
); } -function Sparkline({ data, color = "var(--accent)", height = 28, fill = true }) { +function Sparkline({ data, color = 'var(--accent)', height = 28, fill = true }) { const max = Math.max(...data, 1); const min = Math.min(...data, 0); const range = max - min || 1; @@ -66,11 +77,11 @@ function Sparkline({ data, color = "var(--accent)", height = 28, fill = true }) const pts = data.map((d, i) => { const x = (i / (data.length - 1)) * w; const y = height - ((d - min) / range) * height; - return `${x},${y}`; - }).join(" "); - const area = `0,${height} ${pts} ${w},${height}`; + return x + ',' + y; + }).join(' '); + const area = '0,' + height + ' ' + pts + ' ' + w + ',' + height; return ( - + {fill && } @@ -80,12 +91,12 @@ function Sparkline({ data, color = "var(--accent)", height = 28, fill = true }) function AudioMeter({ level = 0.7, peak = 0.85, vertical = false }) { const segs = 20; return ( -
+
{Array.from({ length: segs }).map((_, i) => { const v = i / segs; const on = v < level; - const color = v < 0.6 ? "var(--success)" : v < 0.85 ? "var(--warning)" : "var(--danger)"; - return
; + const color = v < 0.6 ? 'var(--success)' : v < 0.85 ? 'var(--warning)' : 'var(--danger)'; + return
; })}
); @@ -93,24 +104,23 @@ function AudioMeter({ level = 0.7, peak = 0.85, vertical = false }) { function StatusDot({ status }) { const map = { - online: { color: "var(--success)", pulse: false }, - recording: { color: "var(--live)", pulse: true }, - armed: { color: "var(--accent)", pulse: false }, - idle: { color: "var(--text-4)", pulse: false }, - error: { color: "var(--danger)", pulse: true }, - offline: { color: "var(--text-4)", pulse: false }, - processing: { color: "var(--warning)", pulse: true }, - ready: { color: "var(--success)", pulse: false }, - live: { color: "var(--live)", pulse: true }, - queued: { color: "var(--text-3)", pulse: false }, - running: { color: "var(--accent)", pulse: true }, - done: { color: "var(--success)", pulse: false }, - failed: { color: "var(--danger)", pulse: false }, + online: { color: 'var(--success)', pulse: false }, + recording: { color: 'var(--live)', pulse: true }, + armed: { color: 'var(--accent)', pulse: false }, + idle: { color: 'var(--text-4)', pulse: false }, + error: { color: 'var(--danger)', pulse: true }, + offline: { color: 'var(--text-4)', pulse: false }, + processing: { color: 'var(--warning)', pulse: true }, + ready: { color: 'var(--success)', pulse: false }, + live: { color: 'var(--live)', pulse: true }, + queued: { color: 'var(--text-3)', pulse: false }, + running: { color: 'var(--accent)', pulse: true }, + done: { color: 'var(--success)', pulse: false }, + failed: { color: 'var(--danger)', pulse: false }, + stopped: { color: 'var(--text-4)', pulse: false }, }; - const s = map[status] || { color: "var(--text-3)" }; - return ( - - ); + const s = map[status] || { color: 'var(--text-3)' }; + return ; } function Elapsed({ seconds, live = false }) { @@ -123,7 +133,7 @@ function Elapsed({ seconds, live = false }) { const h = Math.floor(t / 3600); const m = Math.floor((t % 3600) / 60); const s = t % 60; - return {String(h).padStart(2, "0")}:{String(m).padStart(2, "0")}:{String(s).padStart(2, "0")}; + return {String(h).padStart(2,'0')}:{String(m).padStart(2,'0')}:{String(s).padStart(2,'0')}; } Object.assign(window, { AssetThumb, FauxFrame, Waveform, LiveStrip, Sparkline, AudioMeter, StatusDot, Elapsed });