import {FillBlanksExercise, FillBlanksMCOption} from "@/interfaces/exam"; import useExamStore from "@/stores/examStore"; import clsx from "clsx"; import {Fragment, useCallback, useEffect, useMemo, useState} from "react"; import reactStringReplace from "react-string-replace"; import {CommonProps} from ".."; import Button from "../../Low/Button"; import {v4} from "uuid"; const FillBlanks: React.FC = ({ id, type, prompt, solutions, text, words, userSolutions, variant, onNext, onBack, }) => { const {shuffles, exam, partIndex, questionIndex, exerciseIndex} = useExamStore((state) => state); const [answers, setAnswers] = useState<{id: string; solution: string}[]>(userSolutions); const hasExamEnded = useExamStore((state) => state.hasExamEnded); const setCurrentSolution = useExamStore((state) => state.setCurrentSolution); const shuffleMaps = shuffles.find((x) => x.exerciseID == id)?.shuffles; const [currentMCSelection, setCurrentMCSelection] = useState<{id: string; selection: FillBlanksMCOption}>(); const typeCheckWordsMC = (words: any[]): words is FillBlanksMCOption[] => { return Array.isArray(words) && words.every((word) => word && typeof word === "object" && "id" in word && "options" in word); }; const excludeWordMCType = (x: any) => { return typeof x === "string" ? x : (x as {letter: string; word: string}); }; useEffect(() => { if (hasExamEnded) onNext({exercise: id, solutions: answers, score: calculateScore(), type}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasExamEnded]); let correctWords: any; if (exam && (exam.module === "level" || exam.module === "reading") && exam.parts[partIndex].exercises[exerciseIndex].type === "fillBlanks") { correctWords = (exam.parts[partIndex].exercises[exerciseIndex] as FillBlanksExercise).words; } const calculateScore = () => { const total = text.match(/({{\d+}})/g)?.length || 0; const correct = answers!.filter((x) => { const solution = solutions.find((y) => x.id.toString() === y.id.toString())?.solution; if (!solution) return false; const option = correctWords!.find((w: any) => { if (typeof w === "string") { return w.toLowerCase() === x.solution.toLowerCase(); } else if ("letter" in w) { return w.letter.toLowerCase() === x.solution.toLowerCase(); } else { return w.id.toString() === x.id.toString(); } }); if (!option) return false; if (typeof option === "string") { return solution.toLowerCase() === option.toLowerCase(); } else if ("letter" in option) { return solution.toLowerCase() === option.word.toLowerCase(); } else if ("options" in option) { return option.options[solution as keyof typeof option.options] == x.solution; } return false; }).length; const missing = total - answers!.filter((x) => solutions.find((y) => x.id.toString() === y.id.toString())).length; return {total, correct, missing}; }; const renderLines = useCallback( (line: string) => { return (
{reactStringReplace(line, /({{\d+}})/g, (match) => { const id = match.replaceAll(/[\{\}]/g, ""); const userSolution = answers.find((x) => x.id === id); const styles = clsx( "rounded-full hover:text-white transition duration-300 ease-in-out my-1 px-5 py-2 text-center", currentMCSelection?.id == id && "!bg-mti-purple !text-white !outline-none !ring-0", !userSolution && "text-center text-mti-purple-light bg-mti-purple-ultralight", userSolution && "text-center text-mti-purple-dark bg-mti-purple-ultralight", ); return variant === "mc" ? ( <> {/*{`(${id})`}*/} ) : ( setAnswers((prev) => [...prev.filter((x) => x.id !== id), {id, solution: e.target.value}])} value={userSolution?.solution} /> ); })}
); }, [variant, words, setCurrentMCSelection, answers, currentMCSelection], ); const memoizedLines = useMemo(() => { return text.split("\\n").map((line, index) => (

{renderLines(line)}

)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [text, variant, renderLines, currentMCSelection]); const onSelection = (questionID: string, value: string) => { setAnswers((prev) => [...prev.filter((x) => x.id !== questionID), {id: questionID, solution: value}]); }; useEffect(() => { if (variant === "mc") { setCurrentSolution({exercise: id, solutions: answers, score: calculateScore(), type, shuffleMaps: shuffleMaps}); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [answers]); return (
{variant !== "mc" && ( {prompt.split("\\n").map((line, index) => ( {line}
))}
)} {memoizedLines} {variant === "mc" && typeCheckWordsMC(words) ? ( <> {currentMCSelection && (
{`${currentMCSelection.id} - Select the appropriate word.`}
{currentMCSelection.selection?.options && Object.entries(currentMCSelection.selection.options) .sort((a, b) => a[0].localeCompare(b[0])) .map(([key, value]) => { return (
onSelection(currentMCSelection.id, value)} className={clsx( "flex border p-4 rounded-xl gap-2 cursor-pointer bg-white text-base", !!answers.find( (x) => x.solution.toLocaleLowerCase() === value.toLocaleLowerCase() && x.id === currentMCSelection.id, ) && "!bg-mti-purple-light !text-white", )}> {key}. {value}
); })}
)} ) : (
Options
{words.map((v) => { v = excludeWordMCType(v); const text = typeof v === "string" ? v : `${v.letter} - ${v.word}`; return ( x.solution.toLowerCase() === (typeof v === "string" ? v : "letter" in v ? v.letter : "").toLowerCase(), ) && "bg-mti-purple-dark text-white", )} key={v4()}> {text} ); })}
)}
); }; export default FillBlanks;