/* eslint-disable @next/next/no-img-element */ import { Module } from "@/interfaces"; import React, { useContext, useEffect, useState } from "react"; import AbandonPopup from "@/components/AbandonPopup"; import { LayoutContext } from "@/components/High/Layout"; import Finish from "@/exams/Finish"; import Level from "@/exams/Level"; import Listening from "@/exams/Listening"; import Reading from "@/exams/Reading"; import Selection from "@/exams/Selection"; import Speaking from "@/exams/Speaking"; import Writing from "@/exams/Writing"; import { Exam, LevelExam, Variant } from "@/interfaces/exam"; import { User } from "@/interfaces/user"; import { evaluateSpeakingAnswer, evaluateWritingAnswer, } from "@/utils/evaluation"; import { getExam } from "@/utils/exams"; import axios from "axios"; import { useRouter } from "next/router"; import { toast, ToastContainer } from "react-toastify"; import ShortUniqueId from "short-unique-id"; import { ExamProps } from "@/exams/types"; import useExamStore from "@/stores/exam"; import useEvaluationPolling from "@/hooks/useEvaluationPolling"; interface Props { page: "exams" | "exercises"; user: User; destination?: string; hideSidebar?: boolean; } export default function ExamPage({ page, user, destination = "/", hideSidebar = false, }: Props) { const router = useRouter(); const [variant, setVariant] = useState("full"); const [avoidRepeated, setAvoidRepeated] = useState(false); const [showAbandonPopup, setShowAbandonPopup] = useState(false); const [moduleLock, setModuleLock] = useState(false); const { exam, setExam, exams, sessionId, setSessionId, setPartIndex, moduleIndex, setModuleIndex, setQuestionIndex, setExerciseIndex, userSolutions, setUserSolutions, showSolutions, setShowSolutions, selectedModules, setSelectedModules, setUser, inactivity, timeSpent, assignment, bgColor, flags, dispatch, reset: resetStore, saveStats, saveSession, setFlags, setShuffles, } = useExamStore(); const [isFetchingExams, setIsFetchingExams] = useState(false); const [isExamLoaded, setIsExamLoaded] = useState( moduleIndex < selectedModules.length ); useEffect(() => { setIsExamLoaded(moduleIndex < selectedModules.length); }, [showSolutions, moduleIndex, selectedModules]); useEffect(() => { if (!showSolutions && sessionId.length === 0 && user?.id) { const shortUID = new ShortUniqueId(); setUser(user.id); setSessionId(shortUID.randomUUID(8)); } }, [setSessionId, isExamLoaded, sessionId, showSolutions, setUser, user?.id]); useEffect(() => { if (user?.type === "developer") console.log(exam); }, [exam, user]); useEffect(() => { (async () => { if (selectedModules.length > 0 && exams.length === 0) { setIsFetchingExams(true); const examPromises = selectedModules.map((module) => getExam( module, avoidRepeated, variant, user?.type === "student" || user?.type === "developer" ? user.preferredGender : undefined ) ); Promise.all(examPromises).then((values) => { setIsFetchingExams(false); if (values.every((x) => !!x)) { dispatch({ type: "INIT_EXAM", payload: { exams: values.map((x) => x!), modules: selectedModules, }, }); } else { toast.error("Something went wrong, please try again"); setTimeout(router.reload, 500); } }); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedModules, exams]); const reset = () => { resetStore(); setVariant("full"); setAvoidRepeated(false); setShowAbandonPopup(false); }; useEvaluationPolling(sessionId ? [sessionId] : [], "exam", user?.id); useEffect(() => { setModuleLock(true); }, [flags.finalizeModule]); useEffect(() => { if (flags.finalizeModule && !showSolutions) { if ( exam && (exam.module === "writing" || exam.module === "speaking") && userSolutions.length > 0 ) { (async () => { try { const results = await Promise.all( exam.exercises.map(async (exercise, index) => { if (exercise.type === "writing") { const sol = await evaluateWritingAnswer( user.id, sessionId, exercise, index + 1, userSolutions.find((x) => x.exercise === exercise.id)!, exercise.attachment?.url ); return sol; } if ( exercise.type === "interactiveSpeaking" || exercise.type === "speaking" ) { const sol = await evaluateSpeakingAnswer( user.id, sessionId, exercise, userSolutions.find((x) => x.exercise === exercise.id)!, index + 1 ); return sol; } return null; }) ); const updatedSolutions = userSolutions.map((solution) => { const completed = results .filter((r) => r !== null) .find((c: any) => c.exercise === solution.exercise); return completed || solution; }); setUserSolutions(updatedSolutions); } catch (error) { console.error("Error during module evaluation:", error); } finally { setModuleLock(false); } })(); } else { setModuleLock(false); } } }, [ exam, showSolutions, userSolutions, sessionId, user.id, flags.finalizeModule, setUserSolutions, ]); useEffect(() => { if (flags.finalizeExam && moduleIndex !== -1 && !moduleLock) { (async () => { setModuleIndex(-1); await saveStats(); await axios.get("/api/stats/update"); })(); } }, [ flags.finalizeExam, moduleIndex, saveStats, setModuleIndex, userSolutions, moduleLock, flags.finalizeModule, ]); useEffect(() => { if ( flags.finalizeExam && !userSolutions.some((s) => s.isDisabled) && !moduleLock ) { setShowSolutions(true); setFlags({ finalizeExam: false }); dispatch({ type: "UPDATE_EXAMS" }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [flags.finalizeExam, userSolutions, showSolutions, moduleLock]); const aggregateScoresByModule = ( isPractice?: boolean ): { module: Module; total: number; missing: number; correct: number; }[] => { const scores: { [key in Module]: { total: number; missing: number; correct: number }; } = { reading: { total: 0, correct: 0, missing: 0, }, listening: { total: 0, correct: 0, missing: 0, }, writing: { total: 0, correct: 0, missing: 0, }, speaking: { total: 0, correct: 0, missing: 0, }, level: { total: 0, correct: 0, missing: 0, }, }; userSolutions.forEach((x) => { if (x.isPractice === isPractice) { const examModule = x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined); scores[examModule!] = { total: scores[examModule!].total + x.score.total, correct: scores[examModule!].correct + x.score.correct, missing: scores[examModule!].missing + x.score.missing, }; } }); return Object.keys(scores).reduce< { module: Module; total: number; missing: number; correct: number }[] >((accm, x) => { if (scores[x as Module].total > 0) accm.push({ module: x as Module, ...scores[x as Module] }); return accm; }, []); }; const ModuleExamMap: Record>> = { reading: Reading as React.ComponentType>, listening: Listening as React.ComponentType>, writing: Writing as React.ComponentType>, speaking: Speaking as React.ComponentType>, level: Level as React.ComponentType>, }; const CurrentExam = exam?.module ? ModuleExamMap[exam.module] : undefined; const onAbandon = async () => { await saveSession(); reset(); }; const { setBgColor, setHideSidebar, setFocusMode, setOnFocusLayerMouseEnter, } = React.useContext(LayoutContext); useEffect(() => { setOnFocusLayerMouseEnter(() => () => setShowAbandonPopup(true)); }, []); useEffect(() => { setBgColor(bgColor); setHideSidebar(hideSidebar); setFocusMode( selectedModules.length !== 0 && !showSolutions && moduleIndex < selectedModules.length ); }, [ bgColor, hideSidebar, moduleIndex, selectedModules.length, setBgColor, setFocusMode, setHideSidebar, showSolutions, ]); return ( <> {user && ( <> {/* Modules weren't yet set by an INIT_EXAM or INIT_SOLUTIONS dispatch, show Selection component*/} {selectedModules.length === 0 && ( { setModuleIndex(0); setAvoidRepeated(avoid); setSelectedModules(modules); setVariant(variant); }} /> )} {isFetchingExams && (
Loading Exam ...
)} {moduleIndex === -1 && selectedModules.length !== 0 && ( s.isDisabled)} user={user!} modules={selectedModules} solutions={userSolutions} assignment={assignment} information={{ timeSpent, inactivity, }} destination={destination} onViewResults={(index?: number) => { if (exams[0].module === "level") { const levelExam = exams[0] as LevelExam; const allExercises = levelExam.parts.flatMap( (part) => part.exercises ); const exerciseOrderMap = new Map( allExercises.map((ex, index) => [ex.id, index]) ); const orderedSolutions = userSolutions .slice() .sort((a, b) => { const indexA = exerciseOrderMap.get(a.exercise) ?? Infinity; const indexB = exerciseOrderMap.get(b.exercise) ?? Infinity; return indexA - indexB; }); setUserSolutions(orderedSolutions); } else { setUserSolutions(userSolutions); } setShuffles([]); if (index === undefined) { setFlags({ reviewAll: true }); setModuleIndex(0); setExam(exams[0]); } else { setModuleIndex(index); setExam(exams[index]); } setShowSolutions(true); setQuestionIndex(0); setExerciseIndex(0); setPartIndex(0); }} scores={aggregateScoresByModule()} practiceScores={aggregateScoresByModule(true)} /> )} {/* Exam is on going, display it and the abandon modal */} {isExamLoaded && moduleIndex !== -1 && ( <> {exam && CurrentExam && ( )} {!showSolutions && ( setShowAbandonPopup(false)} /> )} )} )} ); }