// visuals.jsx - reusable visual elements const _thumbCache = new Map(); 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
; } function Waveform({ seed = 1, color = 'var(--accent)', className = '' }) { const bars = 60; const pts = React.useMemo(() => { return Array.from({ length: bars }).map((_, i) => { const n = Math.sin(i * 0.7 + seed) * 0.5 + Math.sin(i * 2.1 + seed * 1.3) * 0.3 + Math.sin(i * 4.3 + seed * 0.7) * 0.2; return Math.max(0.1, Math.min(1, 0.5 + n * 0.5)); }); }, [seed]); return ( {pts.map((p, i) => ( ))} ); } function LiveStrip({ seed = 1, count = 8 }) { return (
{Array.from({ length: count }).map((_, i) => (
))}
NOW
); } 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; const w = 100; 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 ( {fill && } ); } 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
; })}
); } 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 }, stopped: { color: 'var(--text-4)', pulse: false }, }; const s = map[status] || { color: 'var(--text-3)' }; return ; } function Elapsed({ seconds, live = false }) { const [t, setT] = React.useState(seconds); React.useEffect(() => { if (!live) return; const i = setInterval(() => setT(x => x + 1), 1000); return () => clearInterval(i); }, [live]); 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')}; } Object.assign(window, { AssetThumb, FauxFrame, Waveform, LiveStrip, Sparkline, AudioMeter, StatusDot, Elapsed });