import { InteractiveSpeakingExercise } from "@/interfaces/exam"; import { CommonProps } from "."; import { useEffect, useState } from "react"; import { BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill } from "react-icons/bs"; import dynamic from "next/dynamic"; import Button from "../Low/Button"; import useExamStore from "@/stores/examStore"; import { downloadBlob } from "@/utils/evaluation"; import axios from "axios"; const Waveform = dynamic(() => import("../Waveform"), { ssr: false }); const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), { ssr: false, }); export default function InteractiveSpeaking({ id, title, first_title, second_title, examID, type, prompts, userSolutions, onNext, onBack, isPractice = false, preview = false }: InteractiveSpeakingExercise & CommonProps) { const [recordingDuration, setRecordingDuration] = useState(0); const [isRecording, setIsRecording] = useState(false); const [mediaBlob, setMediaBlob] = useState(); const [answers, setAnswers] = useState<{ prompt: string; blob: string; questionIndex: number }[]>([]); const [isLoading, setIsLoading] = useState(false); const { questionIndex, setQuestionIndex } = useExamStore((state) => state); const { userSolutions: storeUserSolutions, setUserSolutions } = useExamStore((state) => state); const hasExamEnded = useExamStore((state) => state.hasExamEnded); const back = async () => { setIsLoading(true); const answer = await saveAnswer(questionIndex); if (questionIndex - 1 >= 0) { setQuestionIndex(questionIndex - 1); setIsLoading(false); return; } setIsLoading(false); onBack({ exercise: id, solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer], score: { correct: 100, total: 100, missing: 0 }, type, isPractice }); }; const next = async () => { setIsLoading(true); const answer = await saveAnswer(questionIndex); if (questionIndex + 1 < prompts.length) { setQuestionIndex(questionIndex + 1); setIsLoading(false); return; } setIsLoading(false); setQuestionIndex(0); onNext({ exercise: id, solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer], score: { correct: 100, total: 100, missing: 0 }, type, isPractice }); }; useEffect(() => { if (userSolutions.length > 0 && answers.length === 0) { const solutions = userSolutions as unknown as typeof answers; setAnswers(solutions); if (!mediaBlob) setMediaBlob(solutions.find((x) => x.questionIndex === questionIndex)?.blob); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [userSolutions, mediaBlob, answers]); useEffect(() => { if (hasExamEnded) { const answer = { questionIndex, prompt: prompts[questionIndex].text, blob: mediaBlob!, }; onNext({ exercise: id, solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer], score: { correct: 100, total: 100, missing: 0 }, type, }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasExamEnded]); useEffect(() => { let recordingInterval: NodeJS.Timer | undefined = undefined; if (isRecording) { recordingInterval = setInterval(() => setRecordingDuration((prev) => prev + 1), 1000); } else if (recordingInterval) { clearInterval(recordingInterval); } return () => { if (recordingInterval) clearInterval(recordingInterval); }; }, [isRecording]); useEffect(() => { if (questionIndex <= answers.length - 1) { const blob = answers.find((x) => x.questionIndex === questionIndex)?.blob; setMediaBlob(blob); } }, [answers, questionIndex]); const saveAnswer = async (index: number) => { const answer = { questionIndex, prompt: prompts[questionIndex].text, blob: mediaBlob!, }; setAnswers((prev) => [...prev.filter((x) => x.questionIndex !== index), answer]); setMediaBlob(undefined); setUserSolutions([ ...storeUserSolutions.filter((x) => x.exercise !== id), { exercise: id, solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer], score: { correct: 100, total: 100, missing: 0 }, module: "speaking", exam: examID, type, isPractice }, ]); return answer; }; return (
{!!first_title && !!second_title ? `${first_title} & ${second_title}` : title}
{prompts && prompts.length > 0 && (
)}
setMediaBlob(blob)} render={({ status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl }) => (

Record your answer:

{status === "idle" && ( <>
{status === "idle" && ( { setRecordingDuration(0); startRecording(); setIsRecording(true); }} className="h-5 w-5 text-mti-gray-cool cursor-pointer" /> )} )} {status === "recording" && ( <>
{Math.floor(recordingDuration / 60) .toString(10) .padStart(2, "0")} : {Math.floor(recordingDuration % 60) .toString(10) .padStart(2, "0")}
{ setIsRecording(false); pauseRecording(); }} className="text-red-500 w-8 h-8 cursor-pointer" /> { setIsRecording(false); stopRecording(); }} className="text-mti-purple-light w-8 h-8 cursor-pointer" />
)} {status === "paused" && ( <>
{Math.floor(recordingDuration / 60) .toString(10) .padStart(2, "0")} : {Math.floor(recordingDuration % 60) .toString(10) .padStart(2, "0")}
{ setIsRecording(true); resumeRecording(); }} className="text-mti-purple-light w-8 h-8 cursor-pointer" /> { setIsRecording(false); stopRecording(); }} className="text-mti-purple-light w-8 h-8 cursor-pointer" />
)} {status === "stopped" && mediaBlobUrl && ( <>
{ setRecordingDuration(0); clearBlobUrl(); setMediaBlob(undefined); }} /> { clearBlobUrl(); setRecordingDuration(0); startRecording(); setIsRecording(true); setMediaBlob(undefined); }} className="h-5 w-5 text-mti-gray-cool cursor-pointer" />
)}
)} />
{preview ? ( ) : ( )}
); }