import { useEffect, useRef, useCallback } from "react"; import type { TranscriptSegment } from "../api/videos"; interface TranscriptSidebarProps { segments: TranscriptSegment[]; currentTime: number; onSeek: (time: number) => void; } /** * Format seconds as MM:SS. */ function formatTimestamp(seconds: number): string { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; } /** * Binary search to find the active segment index. * Returns the index of the segment where start_time <= currentTime < end_time, * or -1 if no segment is active. * Assumes segments are sorted by start_time. */ function findActiveSegment( segments: TranscriptSegment[], currentTime: number, ): number { if (segments.length === 0) return -1; let lo = 0; let hi = segments.length - 1; let result = -1; while (lo <= hi) { const mid = (lo + hi) >>> 1; const seg = segments[mid]; if (seg && seg.start_time <= currentTime) { result = mid; lo = mid + 1; } else { hi = mid - 1; } } // result is the last segment whose start_time <= currentTime // verify currentTime < end_time for that segment const active = result >= 0 ? segments[result] : undefined; if (active && currentTime < active.end_time) { return result; } return -1; } /** * Scrollable transcript sidebar synced to video playback. * Active segment highlights with cyan border and auto-scrolls into view. * Clicking a segment seeks the video to that timestamp. */ export default function TranscriptSidebar({ segments, currentTime, onSeek, }: TranscriptSidebarProps) { const containerRef = useRef(null); const activeIndexRef = useRef(-1); const activeIndex = findActiveSegment(segments, currentTime); // Auto-scroll active segment into view useEffect(() => { if (activeIndex < 0 || activeIndex === activeIndexRef.current) return; activeIndexRef.current = activeIndex; const container = containerRef.current; if (!container) return; const el = container.querySelector( `[data-segment-index="${activeIndex}"]`, ); if (el) { el.scrollIntoView({ behavior: "smooth", block: "nearest" }); } }, [activeIndex]); const handleClick = useCallback( (startTime: number) => { onSeek(startTime); }, [onSeek], ); if (segments.length === 0) { return ( ); } return ( ); }