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, MultipleChoiceQuestion, ShuffleMap, UserSolution } from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import { countExercises } from "@/utils/moduleUtils"; import clsx from "clsx"; import { use, useEffect, useState } from "react"; import TextComponent from "./TextComponent"; import PartDivider from "./PartDivider"; import Timer from "@/components/Medium/Timer"; import { Stat } from "@/interfaces/user"; interface Props { exam: LevelExam; showSolutions?: boolean; onFinish: (userSolutions: UserSolution[]) => void; editing?: boolean; partDividers?: boolean; } const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => { return Array.isArray(words) && words.every( word => word && typeof word === 'object' && 'id' in word && 'options' in word ); } export default function Level({ exam, showSolutions = false, onFinish, editing = false }: Props) { const levelBgColor = "bg-ielts-level-light"; const [multipleChoicesDone, setMultipleChoicesDone] = useState<{ id: string; amount: number }[]>([]); const [showQuestionsModal, setShowQuestionsModal] = useState(false); const { setBgColor } = useExamStore((state) => state); const { userSolutions, setUserSolutions } = useExamStore((state) => state); const { hasExamEnded, setHasExamEnded } = useExamStore((state) => state); const { partIndex, setPartIndex } = useExamStore((state) => state); const { exerciseIndex, setExerciseIndex } = useExamStore((state) => state); const [storeQuestionIndex, setStoreQuestionIndex] = useExamStore((state) => [state.questionIndex, state.setQuestionIndex]); const [shuffleMaps, setShuffleMaps] = useExamStore((state) => [state.shuffleMaps, state.setShuffleMaps]) const [currentExercise, setCurrentExercise] = useState(); const [showPartDivider, setShowPartDivider] = useState(typeof exam.parts[0].intro === "string" && !showSolutions); const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); const [contextWord, setContextWord] = useState(undefined); const [contextWordLine, setContextWordLine] = useState(undefined); useEffect(() => { if (showSolutions && exerciseIndex && userSolutions[exerciseIndex].shuffleMaps) { setShuffleMaps(userSolutions[exerciseIndex].shuffleMaps as ShuffleMap[]) } }, [showSolutions, exerciseIndex, setShuffleMaps, userSolutions]) useEffect(() => { if (hasExamEnded && exerciseIndex === -1) { setExerciseIndex(exerciseIndex + 1); } }, [hasExamEnded, exerciseIndex, setExerciseIndex]); const getExercise = () => { let exercise = exam.parts[partIndex]?.exercises[exerciseIndex]; if (!exercise) return undefined; exercise = { ...exercise, userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [], }; if (exam.shuffle && exercise.type === "multipleChoice" && !showSolutions) { console.log("Shuffling"); const exerciseShuffles = userSolutions[exerciseIndex].shuffleMaps; if (exerciseShuffles && exerciseShuffles.length == 0) { const newShuffleMaps: ShuffleMap[] = []; exercise.questions = exercise.questions.map(question => { const options = [...question.options]; let shuffledOptions = [...options].sort(() => Math.random() - 0.5); const newOptions = options.map((option, index) => ({ id: option.id, text: shuffledOptions[index].text })); const optionMapping = options.reduce<{ [key: string]: string }>((acc, originalOption) => { const shuffledPosition = newOptions.find(newOpt => newOpt.text === originalOption.text)?.id; if (shuffledPosition) { acc[shuffledPosition] = originalOption.id; } return acc; }, {}); newShuffleMaps.push({ id: question.id, map: optionMapping }); return { ...question, options: newOptions }; }); setShuffleMaps(newShuffleMaps); } else { console.log("retrieving shuffles"); exercise.questions = exercise.questions.map(question => { const questionShuffleMap = shuffleMaps.find(map => map.id === question.id); if (questionShuffleMap) { const newOptions = question.options.map(option => ({ id: option.id, text: question.options.find(o => questionShuffleMap.map[o.id] === option.id)?.text || option.text })); return { ...question, options: newOptions }; } return question; }); } } else if (exam.shuffle && exercise.type === "fillBlanks" && typeCheckWordsMC(exercise.words) && !showSolutions) { if (shuffleMaps.length === 0 && !showSolutions) { const newShuffleMaps: ShuffleMap[] = []; exercise.words = exercise.words.map(word => { if ('options' in word) { const options = { ...word.options }; const originalKeys = Object.keys(options); const shuffledKeys = [...originalKeys].sort(() => Math.random() - 0.5); const newOptions = shuffledKeys.reduce((acc, key, index) => { acc[key as keyof typeof options] = options[originalKeys[index] as keyof typeof options]; return acc; }, {} as { [key in keyof typeof options]: string }); const optionMapping = originalKeys.reduce((acc, key, index) => { acc[key as keyof typeof options] = shuffledKeys[index]; return acc; }, {} as { [key in keyof typeof options]: string }); newShuffleMaps.push({ id: word.id, map: optionMapping }); return { ...word, options: newOptions }; } return word; }); setShuffleMaps(newShuffleMaps); } } return exercise; }; useEffect(() => { if (exerciseIndex !== -1) { setCurrentExercise(getExercise()); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [partIndex, exerciseIndex, shuffleMaps, exam.parts[partIndex].context]); useEffect(() => { const regex = /.*?['"](.*?)['"] in line (\d+)\?$/; if (exerciseIndex !== -1 && currentExercise && currentExercise.type === "multipleChoice" && currentExercise.questions[storeQuestionIndex].prompt) { const match = currentExercise.questions[storeQuestionIndex].prompt.match(regex); if (match) { const word = match[1]; const originalLineNumber = match[2]; if (word !== contextWord) { setContextWord(word); } const updatedPrompt = currentExercise.questions[storeQuestionIndex].prompt.replace( `in line ${originalLineNumber}`, `in line ${contextWordLine || originalLineNumber}` ); currentExercise.questions[storeQuestionIndex].prompt = updatedPrompt; } else { setContextWord(undefined); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentExercise, storeQuestionIndex]); const nextExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]); } /*if (storeQuestionIndex > 0 || currentExercise?.type == "fillBlanks") { setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]); }*/ if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length && !hasExamEnded) { setExerciseIndex(exerciseIndex + 1); return; } if (partIndex + 1 < exam.parts.length && !hasExamEnded && (showQuestionsModal || showSolutions)) { if (!showSolutions && exam.parts[0].intro) { setShowPartDivider(true); setBgColor(levelBgColor); } setPartIndex(partIndex + 1); setExerciseIndex(!!exam.parts[partIndex + 1].context ? -1 : 0); setStoreQuestionIndex(0); setMultipleChoicesDone((prev) => [...prev.filter((x) => x.id !== currentExercise!.id), { id: currentExercise!.id, amount: currentExercise?.type == "fillBlanks" ? currentExercise.words.length - 1 : storeQuestionIndex }]); return; } if (partIndex + 1 < exam.parts.length && !hasExamEnded && !showQuestionsModal && !showSolutions) { setShowQuestionsModal(true); return; } if ( solution && ![...userSolutions.filter((x) => x.exercise !== solution?.exercise).map((x) => x.score.missing), solution?.score.missing].every( (x) => x === 0, ) && !showSolutions && !editing && !hasExamEnded ) { setShowQuestionsModal(true); return; } setHasExamEnded(false); if (solution) { let stat = { ...solution, module: "level" as Module, exam: exam.id } if (exam.shuffle) { stat.shuffleMaps = shuffleMaps } onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...stat }]); } else { onFinish(userSolutions); } }; const previousExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), { ...solution, module: "level", exam: exam.id }]); } setExerciseIndex(exerciseIndex - 1); if (exerciseIndex - 1 === -1) { setPartIndex(partIndex - 1); const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex]; if (previousExercise.type === "multipleChoice") { setStoreQuestionIndex(previousExercise.questions.length - 1) } const multipleChoiceQuestionsDone = []; for (let i = 0; i < exam.parts.length; i++) { if (i == (partIndex - 1)) break; for (let j = 0; j < exam.parts[i].exercises.length; j++) { const exercise = exam.parts[i].exercises[j]; if (exercise.type === "multipleChoice") { multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.questions.length - 1 }) } if (exercise.type === "fillBlanks") { multipleChoiceQuestionsDone.push({ id: exercise.id, amount: exercise.words.length - 1 }) } } } setMultipleChoicesDone(multipleChoiceQuestionsDone); } }; useEffect(() => { if (exerciseIndex === -1) { nextExercise() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [exerciseIndex]) const calculateExerciseIndex = () => { if (partIndex === 0) { return ( (exerciseIndex === -1 ? 0 : exerciseIndex + 1) + storeQuestionIndex //+ multipleChoicesDone.reduce((acc, curr) => acc + curr.amount, 0) ); } const exercisesPerPart = exam.parts.map((x) => x.exercises.length); const exercisesDone = exercisesPerPart.filter((_, index) => index < partIndex).reduce((acc, curr) => curr + acc, 0); return ( exercisesDone + (exerciseIndex === -1 ? 0 : exerciseIndex + 1) + storeQuestionIndex + multipleChoicesDone.reduce((acc, curr) => { return acc + curr.amount }, 0) ); }; const renderText = () => (
<>

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
); const partLabel = () => { if (currentExercise?.type === "fillBlanks" && typeCheckWordsMC(currentExercise.words)) return `Part ${partIndex + 1} (Questions ${currentExercise.words[0].id} - ${currentExercise.words[currentExercise.words.length - 1].id})\n\n${currentExercise.prompt}` if (currentExercise?.type === "multipleChoice") { return `Part ${partIndex + 1} (Questions ${currentExercise.questions[0].id} - ${currentExercise.questions[currentExercise.questions.length - 1].id})\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})\n\n${nextExercise.prompt}` } } const modalKwargs = () => { const allSolutionsCorrectLength = exam.parts[partIndex].exercises.every((exercise) => { const userSolution = userSolutions.find(x => x.exercise === exercise.id); if (exercise.type === "multipleChoice") { return userSolution?.solutions.length === exercise.questions.length; } if (exercise.type === "fillBlanks") { return userSolution?.solutions.length === exercise.words.length; } return false; }); return { blankQuestions: !allSolutionsCorrectLength, finishingWhat: "part", onClose: partIndex !== exam.parts.length - 1 ? ( function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } ) : function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); onFinish(userSolutions); } else { setShowQuestionsModal(false) } } } } return ( <>
{ !(partIndex === 0 && storeQuestionIndex === 0 && showPartDivider) && } {exam.parts[0].intro && showPartDivider ? { setShowPartDivider(false); setBgColor("bg-white") }} /> : ( <> x.exercises))} disableTimer={showSolutions || editing} showTimer={typeof exam.parts[0].intro === "undefined"} />
-1 && exerciseIndex > -1 && !!exam.parts[partIndex].context && "grid grid-cols-2 gap-4", )}> {partIndex > -1 && !!exam.parts[partIndex].context && renderText()} {exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && !showSolutions && !editing && currentExercise && renderExercise(currentExercise, exam.id, nextExercise, previousExercise)} {exerciseIndex > -1 && partIndex > -1 && exerciseIndex < exam.parts[partIndex].exercises.length && (showSolutions || editing) && currentExercise && renderSolution(currentExercise, nextExercise, previousExercise)}
{/*exerciseIndex === -1 && partIndex > 0 && (
)*/} {exerciseIndex === -1 && partIndex === 0 && ( )} )}
); }