/* eslint-disable @next/next/no-img-element */ import Head from "next/head"; import {useEffect, useState} from "react"; import {Module} from "@/interfaces"; import Selection from "@/exams/Selection"; import Reading from "@/exams/Reading"; import {Exam, InteractiveSpeakingExercise, SpeakingExercise, UserSolution, WritingExercise} from "@/interfaces/exam"; import Listening from "@/exams/Listening"; import Writing from "@/exams/Writing"; import {ToastContainer, toast} from "react-toastify"; import Finish from "@/exams/Finish"; import axios from "axios"; import {Stat} from "@/interfaces/user"; import Speaking from "@/exams/Speaking"; import {v4 as uuidv4} from "uuid"; import useUser from "@/hooks/useUser"; import useExamStore from "@/stores/examStore"; import Layout from "@/components/High/Layout"; import AbandonPopup from "@/components/AbandonPopup"; import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"; import {useRouter} from "next/router"; import {getExam} from "@/utils/exams"; import {capitalize} from "lodash"; import Level from "@/exams/Level"; 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([]); 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(() => { 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(() => { (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)); 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(() => { if (statsAwaitingEvaluation.length === 0) return setIsEvaluationLoading(false); return setIsEvaluationLoading(true); }, [statsAwaitingEvaluation]); useEffect(() => { if (statsAwaitingEvaluation.length > 0) { statsAwaitingEvaluation.forEach(checkIfStatHasBeenEvaluated); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [statsAwaitingEvaluation]); const checkIfStatHasBeenEvaluated = (id: string) => { setTimeout(async () => { const statRequest = await axios.get(`/api/stats/${id}`); const stat = statRequest.data; if (stat.solutions.every((x) => x.evaluation !== null)) { const userSolution: UserSolution = { id, exercise: stat.exercise, score: stat.score, solutions: stat.solutions, type: stat.type, exam: stat.exam, module: stat.module, }; setUserSolutions(userSolutions.map((x) => (x.exercise === userSolution.exercise ? userSolution : x))); return setStatsAwaitingEvaluation((prev) => prev.filter((x) => x !== id)); } return checkIfStatHasBeenEvaluated(id); }, 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 ( { setSelectedModules(modules); setAvoidRepeated(avoid); }} /> ); } if (moduleIndex >= selectedModules.length) { 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)} /> )} )} ); }