import React, { useEffect, useRef, useState } from "react"; import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, Box, List, ListItem, ListItemText, ListItemButton, Divider, Slider, CircularProgress, } from "@mui/material"; import ReactPlayer from "react-player"; import { RehearsalSessionData, getTimeInHoursMinutes, getTimeInSeconds, } from "@/app/interfaces"; import SpeechBubble from "../../../participant/layout/shared/components/SpeechBubble"; interface RehearsalSessionDialogProps { open: boolean; onClose: (event: object, reason: string) => void; data: RehearsalSessionData | null; } const RehearsalSessionDialog: React.FC = ({ open, onClose, data, }) => { // ReactPlayer ref const playerRef = useRef(null); const audioRef = useRef(null); // Feedback & self-rating const [feedback, setFeedBack] = useState([]); const [selfRating, setSelfRating] = useState([]); const [currentFeedback, setCurrentFeedback] = useState(""); // Video states const [isReady, setIsReady] = useState(false); const [isBuffering, setIsBuffering] = useState(false); // --- MAIN STATES FOR TWO-PLAY LOGIC --- // 1) "isHiddenPlayback": The first, fully muted background playback with black screen const [isHiddenPlayback, setIsHiddenPlayback] = useState(true); // 2) "isPlaying": The normal user-driven play/pause after hidden playback finishes const [isPlaying, setIsPlaying] = useState(false); // Slider progress fraction (0..1) const [playedFraction, setPlayedFraction] = useState(0); // Video total duration (seconds) const [videoDuration, setVideoDuration] = useState(0); // --- ROTATING MESSAGES FOR SPINNER --- const hiddenPlaybackMessages = [ "Preparing video resources...", "Loading your presentation...", "Almost ready...", ]; const [messageIndex, setMessageIndex] = useState(0); // The video/audio URLs let videoSrc: string | undefined; let audioSrc: string | undefined; if (data?.video_recording) { videoSrc = `${process.env.NEXT_PUBLIC_MEDIA_URL}${data.video_recording}`; } if (data?.audio_recording) { audioSrc = `${process.env.NEXT_PUBLIC_MEDIA_URL}${data.audio_recording}`; } // Parse the JSON for feedback and self-rating useEffect(() => { if (data) { try { const parsedFeedbackList = data.feedback_list?.toString() ? JSON.parse(data.feedback_list.toString()) : []; const parsedSelfRating = data.self_rating?.toString() ? JSON.parse(data.self_rating.toString()) : []; setFeedBack(parsedFeedbackList); setSelfRating(parsedSelfRating); } catch (error) { console.error("Failed to parse feedback or self-rating:", error); } } }, [data]); /** * Whenever the dialog "open" becomes true, we want to * reset to the initial states: * - hidden playback is active * - video is at 0 * - the user hasn't pressed play yet */ useEffect(() => { if (open) { // Reset all states setIsHiddenPlayback(true); setIsPlaying(false); setIsReady(false); setIsBuffering(false); setPlayedFraction(0); setVideoDuration(0); setCurrentFeedback(""); } }, [open]); /** * Also rotate text messages while hidden playback is active. * We'll cycle them every 2 seconds. */ useEffect(() => { let intervalId: NodeJS.Timeout | null = null; if (isHiddenPlayback) { // Start from first message setMessageIndex(0); intervalId = setInterval(() => { setMessageIndex((prev) => (prev + 1) % hiddenPlaybackMessages.length); }, 2000); } else { // If no longer hidden playback, stop rotating messages setMessageIndex(0); } return () => { if (intervalId) clearInterval(intervalId); }; }, [isHiddenPlayback]); /** * Called when metadata is loaded => we know the total duration in seconds */ const handleDuration = (secs: number) => { setVideoDuration(secs); }; /** * onProgress: called repeatedly (every progressInterval ms) during playback */ const handleProgress = (state: { played: number; // fraction 0..1 playedSeconds: number; loaded: number; loadedSeconds: number; }) => { // Use the fraction from ReactPlayer directly setPlayedFraction(state.played); // Show feedback messages only during the second watch (not hidden playback) if (!isHiddenPlayback && feedback.length > 0) { const currentTime = state.playedSeconds; const currentLog = feedback.find((log) => { const startInSecs = timeStringToSeconds(log.start); const endInSecs = timeStringToSeconds(log.end); return currentTime >= startInSecs && currentTime <= endInSecs; }); if (currentLog) { setCurrentFeedback(currentLog.annotations.feedbackMessage); } else { setCurrentFeedback(""); } } }; /** * Called when the video ends. If it's hidden playback, that means * the entire video was auto-played in the background => switch to visible mode. */ const handleEnded = () => { if (isHiddenPlayback) { // End hidden playback setIsHiddenPlayback(false); // Seek back to start playerRef.current?.seekTo(0, "seconds"); setPlayedFraction(0); // Start paused => user can press "Play" for second watch setIsPlaying(false); } else { // Normal end of second watch setPlayedFraction(1); } }; /** * Convert "00:00:12.817" => numeric seconds */ const timeStringToSeconds = (timeString: string): number => { const [hh, mm, sss] = timeString.split(":"); const [whole, frac] = sss.split("."); return ( Number(hh) * 3600 + Number(mm) * 60 + Number(whole) + (frac ? Number(frac) / 1000 : 0) ); }; /** * Seek the video to a specific second for Mistakes list */ const handleVideoSeek = (time: number) => { if (playerRef.current) { playerRef.current.seekTo(time, "seconds"); } }; /** * Seek the audio to a specific second */ const handleAudioSeek = (time: number) => { if (audioRef.current) { audioRef.current.currentTime = time; audioRef.current.play(); } }; /** * Toggle normal play/pause (only relevant after hidden playback ends) */ const handleTogglePlay = () => { // If user tries to click while buffering, do nothing if (isBuffering || isHiddenPlayback) return; setIsPlaying((prev) => !prev); }; /** * Called when user drags the slider */ const handleSeekChange = (newValue: number | number[]) => { if (!Array.isArray(newValue) && playerRef.current) { const fraction = newValue / 100; setPlayedFraction(fraction); playerRef.current.seekTo(fraction, "fraction"); } }; // If data is null, show error if (data === null) { return ( onClose(e, "closeButtonClick")} maxWidth="sm" fullWidth > Error An error occurred while fetching the rehearsal session data. ); } // The video is effectively playing if hidden playback is active, // or if the user has pressed play in the second watch const videoPlaying = isHiddenPlayback || isPlaying; return ( onClose(event, reason)} maxWidth="lg" fullWidth disableEscapeKeyDown disableAutoFocus > Presentation Rehearsal {/* LEFT SIDE: Mistakes, Self Reflection, etc. */} Duration {data?.duration != null ? getTimeInHoursMinutes(data.duration) : "Presentation not completed yet."} Mistakes {feedback && feedback.length > 0 ? ( {feedback.map((mistake, index) => ( handleVideoSeek(getTimeInSeconds(mistake.start)) } > {index < feedback.length - 1 && } ))} ) : ( Feedback not found )} Self Reflection {selfRating && selfRating.length > 0 ? ( {selfRating.map((question, idx) => ( {idx < selfRating.length - 1 && } ))} ) : ( Self Reflection not found )} {/* RIGHT SIDE: Video + Audio */} Video {/* CUSTOM CONTROLS: displayed only after hidden playback ends */} {!isHiddenPlayback && ( handleSeekChange(val)} sx={{ flexGrow: 1 }} disabled={isBuffering} /> )} Audio ); }; export default RehearsalSessionDialog;