import {SpeakingExercise} from "@/interfaces/exam"; import {CommonProps} from "."; import {Fragment, 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 Speaking({id, title, text, video_url, type, prompts, userSolutions, onNext, onBack}: SpeakingExercise & CommonProps) { const [recordingDuration, setRecordingDuration] = useState(0); const [isRecording, setIsRecording] = useState(false); const [mediaBlob, setMediaBlob] = useState(); const [audioURL, setAudioURL] = useState(); const [isLoading, setIsLoading] = useState(false); const hasExamEnded = useExamStore((state) => state.hasExamEnded); const saveToStorage = async () => { if (mediaBlob && mediaBlob.startsWith("blob")) { const blobBuffer = await downloadBlob(mediaBlob); const audioFile = new File([blobBuffer], "audio.wav", {type: "audio/wav"}); const seed = Math.random().toString().replace("0.", ""); const formData = new FormData(); formData.append("audio", audioFile, `${seed}.wav`); formData.append("root", "speaking_recordings"); const config = { headers: { "Content-Type": "audio/wav", }, }; const response = await axios.post<{path: string}>("/api/storage/insert", formData, config); if (audioURL) await axios.post("/api/storage/delete", {path: audioURL}); return response.data.path; } return undefined; }; useEffect(() => { if (userSolutions.length > 0) { const {solution} = userSolutions[0] as {solution?: string}; if (solution && !mediaBlob) setMediaBlob(solution); if (solution && !solution.startsWith("blob")) setAudioURL(solution); } }, [userSolutions, mediaBlob]); useEffect(() => { if (hasExamEnded) next(); // 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]); const next = async () => { setIsLoading(true); const storagePath = await saveToStorage(); setIsLoading(false); onNext({ exercise: id, solutions: storagePath ? [{id, solution: storagePath}] : [], score: {correct: 100, total: 100, missing: 0}, type, }); }; const back = async () => { setIsLoading(true); const storagePath = await saveToStorage(); setIsLoading(false); onBack({ exercise: id, solutions: storagePath ? [{id, solution: storagePath}] : [], score: {correct: 100, total: 100, missing: 0}, type, }); }; return (
{title} {!video_url && ( {text.split("\\n").map((line, index) => ( {line}
))}
)}
{video_url && (
)} {prompts && prompts.length > 0 && (
You should talk about the following things:
{prompts.map((x, index) => (
  • {x}
  • ))}
    )}
    setMediaBlob(blob)} render={({status, startRecording, stopRecording, pauseRecording, resumeRecording, clearBlobUrl, mediaBlobUrl}) => (

    Record your answer:

    {status === "idle" && !mediaBlob && ( <>
    {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) || (status === "idle" && mediaBlob)) && ( <>
    { setRecordingDuration(0); clearBlobUrl(); setMediaBlob(undefined); }} /> { clearBlobUrl(); setRecordingDuration(0); startRecording(); setIsRecording(true); setMediaBlob(undefined); }} className="h-5 w-5 text-mti-gray-cool cursor-pointer" />
    )}
    )} />
    ); }