import React, { useMemo } from 'react'; import type { AudioTimelineSegment } from '../../services/audioService'; interface WaveformHeatmapProps { timeline: AudioTimelineSegment[]; duration: number; onSegmentClick?: (segment: AudioTimelineSegment) => void; } /** Deterministic pseudo-random heights seeded by segment index + bar index. * This prevents flicker on every React re-render caused by Math.random(). */ function deterministicBarHeight(segIdx: number, barIdx: number, aiScore: number): number { // Simple seeded sequence: use combination of indices to produce variation const seed = (segIdx * 7 + barIdx * 13 + Math.floor(aiScore)) % 100; const base = 15 + (seed % 55); // 15–70% // Scale by ai_score so high-risk segments look taller const scaled = base * (0.4 + (aiScore / 100) * 0.6); return Math.min(95, Math.max(10, scaled)); } const LEVEL_COLORS: Record = { critical: 'var(--accent-red)', high: 'var(--accent-orange)', medium: 'var(--accent-yellow)', low: '#00E5CC', }; const WaveformHeatmap: React.FC = ({ timeline, duration, onSegmentClick }) => { if (!timeline || timeline.length === 0) return null; // Memoize bar heights so they don't change on re-renders const barHeights = useMemo(() => { return timeline.map((seg, sIdx) => Array.from({ length: 8 }, (_, bIdx) => deterministicBarHeight(sIdx, bIdx, seg.ai_score) ) ); }, [timeline]); const [hoveredSeg, setHoveredSeg] = React.useState(null); return (
{/* Time ruler */}
0.0s {duration > 0 && {(duration / 2).toFixed(1)}s} {duration.toFixed(1)}s
{/* Heatmap bars */}
{timeline.map((seg, idx) => { const color = LEVEL_COLORS[seg.level] ?? '#00E5CC'; const isHovered = hoveredSeg === idx; return (
onSegmentClick?.(seg)} onMouseEnter={() => setHoveredSeg(idx)} onMouseLeave={() => setHoveredSeg(null)} className="flex-1 h-full flex flex-col-reverse justify-start items-center cursor-pointer relative group" title={`Seg ${seg.segment}: ${seg.ai_score}% AI (${seg.start_sec.toFixed(1)}s – ${seg.end_sec.toFixed(1)}s)`} >
{barHeights[idx].map((h, bIdx) => (
))}
{/* Bottom threat indicator bar */}
50 ? `0 0 6px ${color}` : 'none', }} /> {/* Hover tooltip */} {isHovered && (
{seg.ai_score}% AI
{seg.start_sec.toFixed(1)}–{seg.end_sec.toFixed(1)}s
)}
); })} {/* Overlay gradient */}
{/* Legend */}
{[ { label: 'Authentic', color: '#00E5CC' }, { label: 'Suspicious', color: 'var(--accent-yellow)' }, { label: 'High Risk', color: 'var(--accent-orange)' }, { label: 'Synthetic', color: 'var(--accent-red)' }, ].map(({ label, color }) => (
{label}
))}
); }; export default WaveformHeatmap;