/* 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, ListeningExam, ReadingExam, SpeakingExam, UserSolution, Evaluation, WritingExam, WritingExercise, SpeakingExercise, } 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 {withIronSessionSsr} from "iron-session/next"; import {sessionOptions} from "@/lib/session"; 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 {speakingReverseMarking, writingReverseMarking} from "@/utils/score"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; if (!user) { res.setHeader("location", "/login"); res.statusCode = 302; res.end(); return { props: { user: null, }, }; } return { props: {user: req.session.user}, }; }, sessionOptions); export default function Page() { const [hasBeenUploaded, setHasBeenUploaded] = useState(false); const [moduleIndex, setModuleIndex] = useState(0); const [sessionId, setSessionId] = useState(""); const [exam, setExam] = useState(); const [isEvaluationLoading, setIsEvaluationLoading] = useState(false); 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 setHasExamEnded = useExamStore((state) => state.setHasExamEnded); const {user} = useUser({redirectTo: "/login"}); useEffect(() => setSessionId(uuidv4()), []); 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(getExam); Promise.all(examPromises).then((values) => { if (values.every((x) => !!x)) { setExams(values.map((x) => x!)); } }); } })(); }, [selectedModules, setExams, exams]); useEffect(() => { if (selectedModules.length > 0 && exams.length !== 0 && moduleIndex >= selectedModules.length && !hasBeenUploaded) { const newStats: Stat[] = userSolutions.map((solution) => ({ ...solution, session: sessionId, exam: solution.exam!, module: solution.module!, user: user?.id || "", date: new Date().getTime(), })); 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]); const getExam = async (module: Module): Promise => { const examRequest = await axios(`/api/exam/${module}`); if (examRequest.status !== 200) { toast.error("Something went wrong!"); return undefined; } const newExam = examRequest.data; switch (module) { case "reading": return newExam.shift() as ReadingExam; case "listening": return newExam.shift() as ListeningExam; case "writing": return newExam.shift() as WritingExam; case "speaking": return newExam.shift() as SpeakingExam; } }; const evaluateSpeakingAnswer = async (examId: string, exerciseId: string, solution: UserSolution) => { const speakingExam = exams.find((x) => x.id === examId)!; const exercise = speakingExam.exercises.find((x) => x.id === exerciseId)! as SpeakingExercise; const blobResponse = await axios.get(solution.solutions[0].solution.trim(), {responseType: "arraybuffer"}); const audioBlob = Buffer.from(blobResponse.data, "binary"); const audioFile = new File([audioBlob], "audio.wav", {type: "audio/wav"}); const formData = new FormData(); formData.append("audio", audioFile, "audio.wav"); formData.append("question", `${exercise.text.replaceAll("\n", "")} You should talk about: ${exercise.prompts.join(", ")}`); const config = { headers: { "Content-Type": "audio/mp3", }, }; const response = await axios.post("/api/evaluate/speaking", formData, config); if (response.status === 200) { setUserSolutions([ ...userSolutions.filter((x) => x.exercise !== exerciseId), { ...solution, score: { correct: speakingReverseMarking[response.data.overall] || 0, missing: 0, total: 100, }, solutions: [{id: exerciseId, solution: response.data.fullPath, evaluation: response.data}], }, ]); } }; const evaluateWritingAnswer = async (examId: string, exerciseId: string, solution: UserSolution) => { const writingExam = exams.find((x) => x.id === examId)!; const exercise = writingExam.exercises.find((x) => x.id === exerciseId)! as WritingExercise; const response = await axios.post("/api/evaluate/writing", { question: `${exercise.prompt} ${exercise.attachment ? exercise.attachment.description : ""}`.replaceAll("\n", ""), answer: solution.solutions[0].solution.trim().replaceAll("\n", " "), }); if (response.status === 200) { setUserSolutions([ ...userSolutions.filter((x) => x.exercise !== exerciseId), { ...solution, score: { correct: writingReverseMarking[response.data.overall] || 0, missing: 0, total: 100, }, solutions: [{id: exerciseId, solution: solution.solutions[0].solution, evaluation: response.data}], }, ]); } }; const updateExamWithUserSolutions = (exam: Exam): Exam => { const exercises = exam.exercises.map((x) => Object.assign(x, !x.userSolutions ? {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions} : x.userSolutions), ); return Object.assign(exam, exercises); }; const onFinish = (solutions: UserSolution[]) => { const solutionIds = solutions.map((x) => x.exercise); if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) { setHasBeenUploaded(true); setIsEvaluationLoading(true); Promise.all( exam.exercises.map((exercise) => (exam.module === "writing" ? evaluateWritingAnswer : evaluateSpeakingAnswer)( exam.id, exercise.id, solutions.find((x) => x.exercise === exercise.id)!, ), ), ).finally(() => { setIsEvaluationLoading(false); 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, }, }; 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 ; } 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 ; } return <>Loading...; }; return ( <> Exam | IELTS GPT {user && ( {renderScreen()} )} ); }