import QuestionsModal from "@/components/QuestionsModal"; import { renderExercise } from "@/components/Exercises"; import Button from "@/components/Low/Button"; import ModuleTitle from "@/components/Medium/ModuleTitle"; import { renderSolution } from "@/components/Solutions"; import { Module } from "@/interfaces"; import { Exercise, FillBlanksMCOption, LevelExam, MultipleChoiceExercise, UserSolution } from "@/interfaces/exam"; import useExamStore, { usePersistentExamStore } from "@/stores/exam"; import { countExercises } from "@/utils/moduleUtils"; import clsx from "clsx"; import { use, useCallback, useEffect, useMemo, useRef, useState } from "react"; import TextComponent from "./TextComponent"; import PartDivider from "../Navigation/SectionDivider"; import Timer from "@/components/Medium/Timer"; import shuffleExamExercise from "./Shuffle"; import { Tab } from "@headlessui/react"; import Modal from "@/components/Modal"; import { typeCheckWordsMC } from "@/utils/type.check"; import SectionNavbar from "../Navigation/SectionNavbar"; import AudioPlayer from "@/components/Low/AudioPlayer"; import { ExamProps } from "../types"; import { answeredEveryQuestionInPart } from "../utils/answeredEveryQuestion"; import useExamTimer from "@/hooks/useExamTimer"; import ProgressButtons from "../components/ProgressButtons"; import useExamNavigation from "../Navigation/useExamNavigation"; import { calculateExerciseIndex } from "../utils/calculateExerciseIndex"; import { defaultExamUserSolutions } from "@/utils/exams"; import PracticeModal from "@/components/PracticeModal"; const Level: React.FC> = ({ exam, showSolutions = false, preview = false }) => { const updateTimers = useExamTimer(exam.module, preview || showSolutions); const userSolutionRef = useRef<(() => UserSolution) | null>(null); const [solutionWasUpdated, setSolutionWasUpdated] = useState(false); const examState = useExamStore((state) => state); const persistentExamState = usePersistentExamStore((state) => state); const { userSolutions, partIndex, exerciseIndex, questionIndex, shuffles, setTimeIsUp, setBgColor, setUserSolutions, setPartIndex, setExerciseIndex, setQuestionIndex, setShuffles, flags, timeSpentCurrentModule, dispatch, } = !preview ? examState : persistentExamState; const { finalizeModule, timeIsUp } = flags; const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60); // In case client want to switch back const textRenderDisabled = true; const [timesListened, setTimesListened] = useState(0); const [showSubmissionModal, setShowSubmissionModal] = useState(false); const [showQuestionsModal, setShowQuestionsModal] = useState(false); const [continueAnyways, setContinueAnyways] = useState(false); const [textRender, setTextRender] = useState(false); const [questionModalKwargs, setQuestionModalKwargs] = useState<{ type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined; }>({ type: "blankQuestions", onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } }); const modalKwargs = () => { const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = { type: "blankQuestions", unanswered: false, onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } } }; if (partIndex === exam.parts.length - 1) { kwargs.type = "submit" kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestionInPart(exam, partIndex, userSolutions)); kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }; } setQuestionModalKwargs(kwargs); } const { nextExercise, previousExercise, showPartDivider, setShowPartDivider, seenParts, setSeenParts, startNow, setStartNow } = useExamNavigation( { exam, module: "level", showBlankModal: showQuestionsModal, setShowBlankModal: setShowQuestionsModal, showSolutions, preview, disableBetweenParts: true, modalBetweenParts: true, modalKwargs } ); const hasPractice = useMemo(() => { if (partIndex > -1 && partIndex < exam.parts.length && !showPartDivider) { return exam.parts[partIndex].exercises.some(e => e.isPractice) } return false }, [partIndex, showPartDivider, exam.parts]) const registerSolution = useCallback((updateSolution: () => UserSolution) => { userSolutionRef.current = updateSolution; setSolutionWasUpdated(true); }, []); useEffect(() => { if (preview) { setUserSolutions(defaultExamUserSolutions(exam)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined); const [contextWordLines, setContextWordLines] = useState(undefined); const [totalLines, setTotalLines] = useState(0); useEffect(() => { if (showSolutions) { const solutionShuffles = userSolutions.map(solution => ({ exerciseID: solution.exercise, shuffles: solution.shuffleMaps || [] })); setShuffles(solutionShuffles); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const currentExercise = useMemo(() => { let exercise = exam.parts[partIndex].exercises[exerciseIndex]; exercise = { ...exercise, userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], }; exercise = shuffleExamExercise(exam.shuffle, exercise, showSolutions, userSolutions, shuffles, setShuffles); return exercise; // eslint-disable-next-line react-hooks/exhaustive-deps }, [partIndex, exerciseIndex]); useEffect(() => { if (solutionWasUpdated && userSolutionRef.current) { const solution = userSolutionRef.current(); setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level" as Module, exam: exam.id, shuffleMaps: exam.shuffle ? [...shuffles.find((x) => x.exerciseID == currentExercise?.id)?.shuffles!] : [] }]); setSolutionWasUpdated(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [solutionWasUpdated]); useEffect(() => { if (finalizeModule || timeIsUp) { updateTimers(); if (timeIsUp) setTimeIsUp(false); dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: false } }) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [finalizeModule, timeIsUp]) const renderAudioPlayer = () => (
{exam?.parts[partIndex]?.audio?.source ? ( <>

Please listen to the following audio attentively.

{(() => { const audioRepeatTimes = exam?.parts[partIndex]?.audio?.repeatableTimes; return audioRepeatTimes && audioRepeatTimes > 0 ? `You will only be allowed to listen to the audio ${audioRepeatTimes - timesListened} time(s).` : "You may listen to the audio as many times as you would like."; })()}
setTimesListened((prev) => prev + 1)} disabled={exam?.parts[partIndex]?.audio?.repeatableTimes != null && timesListened === exam.parts[partIndex]?.audio?.repeatableTimes} disablePause />
) : ( This section will be displayed the audio once it has been generated. )}
); const renderText = () => ( <>
<>
{textRender && !textRenderDisabled ? ( <>

Please read the following excerpt attentively, you will then be asked questions about the text you've read.

You will be allowed to read the text while doing the exercises ) : (

Answer the questions on the right based on what you've read.

)}
{(exam.parts[partIndex].context || exam.parts[partIndex].text) && }
{textRender && !textRenderDisabled && (
)} ); const partLabel = () => { const partCategory = exam.parts[partIndex].category ? ` (${exam.parts[partIndex].category})` : ''; if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words)) return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})${partCategory}\n\n${currentExercise.prompt}` if (currentExercise?.type === "multipleChoice") { return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})${partCategory}\n\n${currentExercise.prompt}` } if (typeof exam.parts[partIndex].context === "string") { const nextExercise = exam.parts[partIndex].exercises[0] as MultipleChoiceExercise; return `Part ${partIndex + 1} (Questions ${nextExercise.questions[0].id} - ${nextExercise.questions[nextExercise.questions.length - 1].id})${partCategory}\n\n${nextExercise.prompt}` } } useEffect(() => { const regex = /.*?['"](.*?)['"] in line (\d+)\?$/; const findMatch = (index: number) => { if (currentExercise && currentExercise.type === "multipleChoice" && currentExercise!.questions[index]) { const match = currentExercise!.questions[index].prompt.match(regex); if (match) { return { match: match[1], originalLine: match[2] } } } return; } // if the client for some whatever random reason decides // to add more questions update this const numberOfQuestions = 2; if (exam.parts[partIndex].context) { const hits = Array.from({ length: numberOfQuestions }).reduce<{ match: string, originalLine: string }[]>((acc, _, i) => { const result = findMatch(questionIndex + i); if (!!result) { acc.push(result); } return acc; }, []); if (hits.length > 0) { setContextWords(hits) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentExercise, questionIndex, totalLines]); useEffect(() => { if ( currentExercise && currentExercise.type === "multipleChoice" && exam.parts[partIndex].context && contextWordLines ) { if (contextWordLines.length > 0) { contextWordLines.forEach((n, i) => { if (contextWords && contextWords[i] && n !== -1) { const updatedPrompt = currentExercise!.questions[questionIndex + i].prompt.replace( `in line ${contextWords[i].originalLine}`, `in line ${n}` ); currentExercise!.questions[questionIndex + i].prompt = updatedPrompt; } }) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [contextWordLines]); useEffect(() => { if (continueAnyways) { setContinueAnyways(false); nextExercise(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [continueAnyways]); const mcNavKwargs = { userSolutions: userSolutions, exam: exam, partIndex: partIndex, showSolutions: showSolutions, setExerciseIndex: setExerciseIndex, setPartIndex: setPartIndex, runOnClick: setQuestionIndex } const progressButtons = useMemo(() => // Do not remove the ()=> in handle next nextExercise()} /> , [nextExercise, previousExercise]); return ( <>
{ }} title={"Confirm Submission"} > <>

Are you sure you want to proceed with the submission?

{ (!showPartDivider && !startNow) && } {(showPartDivider || (startNow && partIndex === 0)) ? { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); setSeenParts(prev => new Set(prev).add(partIndex)); }} /> : ( <> {exam.parts.length > 1 && } x.exercises))} disableTimer={showSolutions} showTimer={false} preview={preview} {...mcNavKwargs} />
{textRender && !textRenderDisabled ? renderText() : <> {exam.parts[partIndex]?.context && renderText()} {exam.parts[partIndex]?.audio && renderAudioPlayer()} {(showSolutions) ? currentExercise && renderSolution(currentExercise, progressButtons, progressButtons) : currentExercise && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons) } }
)}
); } export default Level;