'use client' import { useRef, useState, useEffect, useCallback } from 'react' import type { InteractiveVideoManifest, CheckpointEntry, CheckpointQuizResult, } from '@/lib/sdk/training/types' import { submitCheckpointQuiz } from '@/lib/sdk/training/api' interface Props { manifest: InteractiveVideoManifest assignmentId: string onAllCheckpointsPassed?: () => void } export default function InteractiveVideoPlayer({ manifest, assignmentId, onAllCheckpointsPassed }: Props) { const videoRef = useRef(null) const [currentCheckpoint, setCurrentCheckpoint] = useState(null) const [showOverlay, setShowOverlay] = useState(false) const [answers, setAnswers] = useState>({}) const [quizResult, setQuizResult] = useState(null) const [submitting, setSubmitting] = useState(false) const [passedCheckpoints, setPassedCheckpoints] = useState>(new Set()) const [currentTime, setCurrentTime] = useState(0) const [duration, setDuration] = useState(0) // Initialize passed checkpoints from manifest progress useEffect(() => { const passed = new Set() for (const cp of manifest.checkpoints) { if (cp.progress?.passed) { passed.add(cp.checkpoint_id) } } setPassedCheckpoints(passed) }, [manifest]) // Find next unpassed checkpoint const getNextUnpassedCheckpoint = useCallback((): CheckpointEntry | null => { for (const cp of manifest.checkpoints) { if (!passedCheckpoints.has(cp.checkpoint_id)) { return cp } } return null }, [manifest.checkpoints, passedCheckpoints]) // Time update handler — check for checkpoint triggers const handleTimeUpdate = useCallback(() => { if (!videoRef.current || showOverlay) return const time = videoRef.current.currentTime setCurrentTime(time) for (const cp of manifest.checkpoints) { if (passedCheckpoints.has(cp.checkpoint_id)) continue // Trigger checkpoint when video reaches its timestamp (within 0.5s) if (time >= cp.timestamp_seconds && time < cp.timestamp_seconds + 1.0) { videoRef.current.pause() setCurrentCheckpoint(cp) setShowOverlay(true) setAnswers({}) setQuizResult(null) break } } }, [manifest.checkpoints, passedCheckpoints, showOverlay]) // Seek protection — prevent skipping past unpassed checkpoints const handleSeeking = useCallback(() => { if (!videoRef.current) return const seekTarget = videoRef.current.currentTime const nextUnpassed = getNextUnpassedCheckpoint() if (nextUnpassed && seekTarget > nextUnpassed.timestamp_seconds) { videoRef.current.currentTime = nextUnpassed.timestamp_seconds - 0.5 } }, [getNextUnpassedCheckpoint]) // Submit checkpoint quiz async function handleSubmitQuiz() { if (!currentCheckpoint) return setSubmitting(true) try { const answerList = currentCheckpoint.questions.map((_, i) => answers[i] ?? -1) const result = await submitCheckpointQuiz( currentCheckpoint.checkpoint_id, assignmentId, answerList, ) setQuizResult(result) if (result.passed) { setPassedCheckpoints(prev => { const next = new Set(prev) next.add(currentCheckpoint.checkpoint_id) return next }) } } catch (e) { console.error('Checkpoint quiz submission failed:', e) } finally { setSubmitting(false) } } // Continue video after passing checkpoint function handleContinue() { setShowOverlay(false) setCurrentCheckpoint(null) setQuizResult(null) setAnswers({}) if (videoRef.current) { videoRef.current.play() } // Check if all checkpoints passed const allPassed = manifest.checkpoints.every(cp => passedCheckpoints.has(cp.checkpoint_id)) if (allPassed && onAllCheckpointsPassed) { onAllCheckpointsPassed() } } // Retry quiz function handleRetry() { setQuizResult(null) setAnswers({}) } // Resume to last unpassed checkpoint useEffect(() => { if (!videoRef.current || !manifest.checkpoints.length) return const nextUnpassed = getNextUnpassedCheckpoint() if (nextUnpassed && nextUnpassed.timestamp_seconds > 0) { // Start a bit before the checkpoint const startTime = Math.max(0, nextUnpassed.timestamp_seconds - 2) videoRef.current.currentTime = startTime } }, []) // Only on mount const handleLoadedMetadata = useCallback(() => { if (videoRef.current) { setDuration(videoRef.current.duration) } }, []) // Progress bar percentage const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0 return (
{/* Video element */}