/* eslint-disable @next/next/no-img-element */ import { Module } from "@/interfaces"; import { useEffect, useState } from "react"; import AbandonPopup from "@/components/AbandonPopup"; import Layout 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 useUser from "@/hooks/useUser"; import { Exam, UserSolution, Variant } from "@/interfaces/exam"; import { Stat } from "@/interfaces/user"; import useExamStore from "@/stores/examStore"; 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 { v4 as uuidv4 } from "uuid"; interface Props { page: "exams" | "exercises"; } export default function ExamPage({ page }: Props) { const [hasBeenUploaded, setHasBeenUploaded] = useState(false); const [moduleIndex, setModuleIndex] = useState(0); const [sessionId, setSessionId] = useState(""); const [exam, setExam] = useState(); const [isEvaluationLoading, setIsEvaluationLoading] = useState(false); const [showAbandonPopup, setShowAbandonPopup] = useState(false); const [avoidRepeated, setAvoidRepeated] = useState(false); const [timeSpent, setTimeSpent] = useState(0); const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState< string[] >([]); const [variant, setVariant] = useState("full"); const [exams, setExams] = useExamStore((state) => [ state.exams, state.setExams, ]); const [userSolutions, setUserSolutions] = useExamStore((state) => [ state.userSolutions, state.setUserSolutions, ]); const [showSolutions, setShowSolutions] = useExamStore((state) => [ state.showSolutions, state.setShowSolutions, ]); const [selectedModules, setSelectedModules] = useExamStore((state) => [ state.selectedModules, state.setSelectedModules, ]); const assignment = useExamStore((state) => state.assignment); const { user } = useUser({ redirectTo: "/login" }); const router = useRouter(); useEffect(() => setSessionId(uuidv4()), []); useEffect(() => { if (user?.type === "developer") console.log(exam); }, [exam, user]); useEffect(() => { selectedModules.length > 0 && timeSpent === 0 && !showSolutions; if (selectedModules.length > 0 && timeSpent === 0 && !showSolutions) { const timerInterval = setInterval(() => { setTimeSpent((prev) => prev + 1); }, 1000); return () => { clearInterval(timerInterval); }; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedModules.length]); useEffect(() => { if (showSolutions) setModuleIndex(-1); }, [showSolutions]); useEffect(() => { (async () => { if ( selectedModules.length > 0 && exams.length > 0 && moduleIndex < selectedModules.length ) { const nextExam = exams[moduleIndex]; setExam(nextExam ? updateExamWithUserSolutions(nextExam) : undefined); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedModules, moduleIndex, exams]); useEffect(() => { (async () => { if (selectedModules.length > 0 && exams.length === 0) { const examPromises = selectedModules.map((module) => getExam(module, avoidRepeated, variant), ); Promise.all(examPromises).then((values) => { if (values.every((x) => !!x)) { setExams(values.map((x) => x!)); } else { toast.error("Something went wrong, please try again"); setTimeout(router.reload, 500); } }); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedModules, setExams, exams]); useEffect(() => { if ( selectedModules.length > 0 && exams.length !== 0 && moduleIndex >= selectedModules.length && !hasBeenUploaded && !showSolutions ) { const newStats: Stat[] = userSolutions.map((solution) => ({ ...solution, id: solution.id || uuidv4(), timeSpent, session: sessionId, exam: solution.exam!, module: solution.module!, user: user?.id || "", date: new Date().getTime(), ...(assignment ? { assignment: assignment.id } : {}), })); axios .post<{ ok: boolean }>("/api/stats", newStats) .then((response) => setHasBeenUploaded(response.data.ok)) .catch(() => setHasBeenUploaded(false)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedModules, moduleIndex, hasBeenUploaded]); useEffect(() => { setIsEvaluationLoading(statsAwaitingEvaluation.length !== 0); }, [statsAwaitingEvaluation]); useEffect(() => { if (statsAwaitingEvaluation.length > 0) { checkIfStatsHaveBeenEvaluated(statsAwaitingEvaluation); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [statsAwaitingEvaluation]); const checkIfStatsHaveBeenEvaluated = (ids: string[]) => { setTimeout(async () => { const awaitedStats = await Promise.all( ids.map(async (id) => (await axios.get(`/api/stats/${id}`)).data), ); const solutionsEvaluated = awaitedStats.every((stat) => stat.solutions.every((x) => x.evaluation !== null), ); if (solutionsEvaluated) { const statsUserSolutions: UserSolution[] = awaitedStats.map((stat) => ({ id: stat.id, exercise: stat.exercise, score: stat.score, solutions: stat.solutions, type: stat.type, exam: stat.exam, module: stat.module, })); const updatedUserSolutions = userSolutions.map((x) => { const respectiveSolution = statsUserSolutions.find( (y) => y.exercise === x.exercise, ); return respectiveSolution ? respectiveSolution : x; }); setUserSolutions(updatedUserSolutions); return setStatsAwaitingEvaluation((prev) => prev.filter((x) => !ids.includes(x)), ); } return checkIfStatsHaveBeenEvaluated(ids); }, 5 * 1000); }; const updateExamWithUserSolutions = (exam: Exam): Exam => { if (exam.module === "reading" || exam.module === "listening") { const parts = exam.parts.map((p) => Object.assign(p, { exercises: p.exercises.map((x) => Object.assign(x, { userSolutions: userSolutions.find((y) => x.id === y.exercise) ?.solutions, }), ), }), ); return Object.assign(exam, { parts }); } const exercises = exam.exercises.map((x) => Object.assign(x, { userSolutions: userSolutions.find((y) => x.id === y.exercise) ?.solutions, }), ); return Object.assign(exam, { exercises }); }; const onFinish = (solutions: UserSolution[]) => { const solutionIds = solutions.map((x) => x.exercise); const solutionExams = solutions.map((x) => x.exam); if (exam && !solutionExams.includes(exam.id)) return; if ( exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions ) { setHasBeenUploaded(true); setIsEvaluationLoading(true); Promise.all( exam.exercises.map(async (exercise) => { const evaluationID = uuidv4(); if (exercise.type === "writing") return await evaluateWritingAnswer( exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID, ); if ( exercise.type === "interactiveSpeaking" || exercise.type === "speaking" ) return await evaluateSpeakingAnswer( exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID, ); }), ) .then((responses) => { setStatsAwaitingEvaluation((prev) => [ ...prev, ...responses.filter((x) => !!x).map((r) => (r as any).id), ]); setUserSolutions([ ...userSolutions, ...responses.filter((x) => !!x), ] as any); }) .finally(() => { setHasBeenUploaded(false); }); } axios.get("/api/stats/update"); setUserSolutions([ ...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...solutions, ]); setModuleIndex((prev) => prev + 1); }; const aggregateScoresByModule = ( answers: UserSolution[], ): { 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, }, }; answers.forEach((x) => { scores[x.module!] = { total: scores[x.module!].total + x.score.total, correct: scores[x.module!].correct + x.score.correct, missing: scores[x.module!].missing + x.score.missing, }; }); return Object.keys(scores) .filter((x) => scores[x as Module].total > 0) .map((x) => ({ module: x as Module, ...scores[x as Module] })); }; const renderScreen = () => { if (selectedModules.length === 0) { return ( { setModuleIndex(0); setAvoidRepeated(avoid); setSelectedModules(modules); setVariant(variant); }} /> ); } if (moduleIndex >= selectedModules.length || moduleIndex === -1) { return ( { setShowSolutions(true); setModuleIndex(0); setExam(exams[0]); }} scores={aggregateScoresByModule(userSolutions)} /> ); } if (exam && exam.module === "reading") { return ( ); } if (exam && exam.module === "listening") { return ( ); } if (exam && exam.module === "writing") { return ( ); } if (exam && exam.module === "speaking") { return ( ); } if (exam && exam.module === "level") { return ( ); } return <>Loading...; }; return ( <> {user && ( setShowAbandonPopup(true)} > <> {renderScreen()} {!showSolutions && moduleIndex < selectedModules.length && ( router.reload()} onCancel={() => setShowAbandonPopup(false)} /> )} )} ); }