diff --git a/src/exams/Level.tsx b/src/exams/Level.tsx index b2a09dd2..b31d1ee0 100644 --- a/src/exams/Level.tsx +++ b/src/exams/Level.tsx @@ -38,7 +38,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { const nextExercise = (solution?: UserSolution) => { if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]); } setQuestionIndex((prev) => prev + currentQuestionIndex); @@ -52,17 +52,15 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) { setHasExamEnded(false); if (solution) { - onFinish( - [...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "level", exam: exam.id})), - ); + onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]); } else { - onFinish(userSolutions.map((x) => ({...x, module: "level", exam: exam.id}))); + onFinish(userSolutions); } }; const previousExercise = (solution?: UserSolution) => { if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "level", exam: exam.id}]); } if (exerciseIndex > 0) { diff --git a/src/exams/Listening.tsx b/src/exams/Listening.tsx index ef974ab3..856bfd9a 100644 --- a/src/exams/Listening.tsx +++ b/src/exams/Listening.tsx @@ -55,13 +55,13 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props return; } - onFinish(userSolutions.map((x) => ({...x, module: "listening", exam: exam.id}))); + onFinish(userSolutions); }; const nextExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "listening", exam: exam.id}]); } setQuestionIndex((prev) => prev + currentQuestionIndex); @@ -91,18 +91,16 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props setHasExamEnded(false); if (solution) { - onFinish( - [...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "listening", exam: exam.id})), - ); + onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "listening", exam: exam.id}]); } else { - onFinish(userSolutions.map((x) => ({...x, module: "listening", exam: exam.id}))); + onFinish(userSolutions); } }; const previousExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "listening", exam: exam.id}]); } setExerciseIndex(exerciseIndex - 1); diff --git a/src/exams/Reading.tsx b/src/exams/Reading.tsx index 58d3f728..b3575124 100644 --- a/src/exams/Reading.tsx +++ b/src/exams/Reading.tsx @@ -128,13 +128,13 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) return; } - onFinish(userSolutions.map((x) => ({...x, module: "reading", exam: exam.id}))); + onFinish(userSolutions); }; const nextExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "reading", exam: exam.id}]); } setQuestionIndex((prev) => prev + currentQuestionIndex); setStoreQuestionIndex(0); @@ -165,18 +165,16 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props) setHasExamEnded(false); if (solution) { - onFinish( - [...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "reading", exam: exam.id})), - ); + onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "reading", exam: exam.id}]); } else { - onFinish(userSolutions.map((x) => ({...x, module: "reading", exam: exam.id}))); + onFinish(userSolutions); } }; const previousExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "reading", exam: exam.id}]); } setStoreQuestionIndex(0); diff --git a/src/exams/Speaking.tsx b/src/exams/Speaking.tsx index d69c707f..b131bdcd 100644 --- a/src/exams/Speaking.tsx +++ b/src/exams/Speaking.tsx @@ -36,7 +36,7 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props) const nextExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "speaking", exam: exam.id}]); } setQuestionIndex((prev) => prev + currentQuestionIndex); @@ -50,18 +50,16 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props) setHasExamEnded(false); if (solution) { - onFinish( - [...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "speaking", exam: exam.id})), - ); + onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "speaking", exam: exam.id}]); } else { - onFinish(userSolutions.map((x) => ({...x, module: "speaking", exam: exam.id}))); + onFinish(userSolutions); } }; const previousExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "speaking", exam: exam.id}]); } if (exerciseIndex > 0) { diff --git a/src/exams/Writing.tsx b/src/exams/Writing.tsx index 2efa9244..f103bcab 100644 --- a/src/exams/Writing.tsx +++ b/src/exams/Writing.tsx @@ -28,7 +28,7 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props) const nextExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "writing", exam: exam.id}]); } if (exerciseIndex + 1 < exam.exercises.length) { @@ -41,18 +41,16 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props) setHasExamEnded(false); if (solution) { - onFinish( - [...userSolutions.filter((x) => x.exercise !== solution.exercise), solution].map((x) => ({...x, module: "writing", exam: exam.id})), - ); + onFinish([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "writing", exam: exam.id}]); } else { - onFinish(userSolutions.map((x) => ({...x, module: "writing", exam: exam.id}))); + onFinish(userSolutions); } }; const previousExercise = (solution?: UserSolution) => { scrollToTop(); if (solution) { - setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), solution]); + setUserSolutions([...userSolutions.filter((x) => x.exercise !== solution.exercise), {...solution, module: "writing", exam: exam.id}]); } if (exerciseIndex > 0) { diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index e52cc51d..46acd8af 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -41,6 +41,8 @@ export default function ExamPage({page}: Props) { const assignment = useExamStore((state) => state.assignment); const initialTimeSpent = useExamStore((state) => state.timeSpent); + const examStore = useExamStore; + const {exam, setExam} = useExamStore((state) => state); const {exams, setExams} = useExamStore((state) => state); const {sessionId, setSessionId} = useExamStore((state) => state); @@ -189,8 +191,8 @@ export default function ExamPage({page}: Props) { id: solution.id || uuidv4(), timeSpent, session: sessionId, - exam: exam!.id, - module: exam!.module, + exam: solution.exam!, + module: solution.module!, user: user?.id || "", date: new Date().getTime(), isDisabled: solution.isDisabled, @@ -218,29 +220,33 @@ export default function ExamPage({page}: Props) { 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, - })); + try { + 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; - }); + 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))); + setUserSolutions(updatedUserSolutions); + return setStatsAwaitingEvaluation((prev) => prev.filter((x) => !ids.includes(x))); + } + + return checkIfStatsHaveBeenEvaluated(ids); + } catch { + return checkIfStatsHaveBeenEvaluated(ids); } - - return checkIfStatsHaveBeenEvaluated(ids); }, 5 * 1000); }; @@ -276,7 +282,7 @@ export default function ExamPage({page}: Props) { setHasBeenUploaded(true); setIsEvaluationLoading(true); - await Promise.all( + Promise.all( exam.exercises.map(async (exercise) => { const evaluationID = uuidv4(); if (exercise.type === "writing") @@ -287,8 +293,21 @@ export default function ExamPage({page}: Props) { }), ) .then((responses) => { + examStore.setState((prev) => { + return { + userSolutions: [ + ...prev.userSolutions.filter( + (x) => + !responses + .filter((r) => !!r) + .map((r) => r!.id) + .includes(x.id), + ), + ...responses.filter((x) => !!x), + ] as any, + }; + }); setStatsAwaitingEvaluation((prev) => [...prev, ...responses.filter((x) => !!x).map((r) => (r as any).id)]); - setUserSolutions([...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...responses.filter((x) => !!x)] as any); }) .finally(() => { setHasBeenUploaded(false); @@ -297,8 +316,7 @@ export default function ExamPage({page}: Props) { axios.get("/api/stats/update"); - if (exam && exam.module !== "writing" && exam.module !== "speaking") - setUserSolutions([...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...solutions]); + setUserSolutions([...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...solutions]); setModuleIndex(moduleIndex + 1); setPartIndex(-1); @@ -306,7 +324,7 @@ export default function ExamPage({page}: Props) { setQuestionIndex(0); }; - const aggregateScoresByModule = (answers: UserSolution[]): {module: Module; total: number; missing: number; correct: number}[] => { + const aggregateScoresByModule = (): {module: Module; total: number; missing: number; correct: number}[] => { const scores: { [key in Module]: {total: number; missing: number; correct: number}; } = { @@ -337,7 +355,7 @@ export default function ExamPage({page}: Props) { }, }; - answers.forEach((x) => { + userSolutions.forEach((x) => { const examModule = x.module || (x.type === "writing" ? "writing" : x.type === "speaking" || x.type === "interactiveSpeaking" ? "speaking" : undefined); @@ -383,7 +401,7 @@ export default function ExamPage({page}: Props) { setPartIndex(exams[0].module === "listening" ? -1 : 0); setExam(exams[0]); }} - scores={aggregateScoresByModule(userSolutions)} + scores={aggregateScoresByModule()} /> ); } diff --git a/src/pages/api/stats/[id]/index.ts b/src/pages/api/stats/[id]/index.ts index 8ea9d243..d83e13a5 100644 --- a/src/pages/api/stats/[id]/index.ts +++ b/src/pages/api/stats/[id]/index.ts @@ -18,6 +18,7 @@ async function GET(req: NextApiRequest, res: NextApiResponse) { const {id} = req.query; const snapshot = await getDoc(doc(db, "stats", id as string)); + if (!snapshot.exists()) return res.status(404).json({id: snapshot.id}); res.status(200).json({...snapshot.data(), id: snapshot.id}); -} \ No newline at end of file +} diff --git a/src/utils/evaluation.ts b/src/utils/evaluation.ts index e92bf0d3..f9a8abc4 100644 --- a/src/utils/evaluation.ts +++ b/src/utils/evaluation.ts @@ -11,7 +11,7 @@ import { import axios from "axios"; import {speakingReverseMarking, writingReverseMarking} from "./score"; -export const evaluateWritingAnswer = async (exercise: WritingExercise, solution: UserSolution, id: string): Promise => { +export const evaluateWritingAnswer = async (exercise: WritingExercise, solution: UserSolution, id: string): Promise => { 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", " "), @@ -35,12 +35,16 @@ export const evaluateWritingAnswer = async (exercise: WritingExercise, solution: return undefined; }; -export const evaluateSpeakingAnswer = async (exercise: SpeakingExercise | InteractiveSpeakingExercise, solution: UserSolution, id: string) => { +export const evaluateSpeakingAnswer = async ( + exercise: SpeakingExercise | InteractiveSpeakingExercise, + solution: UserSolution, + id: string, +): Promise => { switch (exercise?.type) { case "speaking": - return {...(await evaluateSpeakingExercise(exercise, exercise.id, solution, id)), id}; + return {...(await evaluateSpeakingExercise(exercise, exercise.id, solution, id)), id} as UserSolution; case "interactiveSpeaking": - return {...(await evaluateInteractiveSpeakingExercise(exercise.id, solution, id)), id}; + return {...(await evaluateInteractiveSpeakingExercise(exercise.id, solution, id)), id} as UserSolution; default: return undefined; } @@ -51,7 +55,12 @@ export const downloadBlob = async (url: string): Promise => { return Buffer.from(blobResponse.data, "binary"); }; -const evaluateSpeakingExercise = async (exercise: SpeakingExercise, exerciseId: string, solution: UserSolution, id: string) => { +const evaluateSpeakingExercise = async ( + exercise: SpeakingExercise, + exerciseId: string, + solution: UserSolution, + id: string, +): Promise => { const formData = new FormData(); const url = solution.solutions[0].solution.trim() as string; @@ -92,7 +101,7 @@ const evaluateSpeakingExercise = async (exercise: SpeakingExercise, exerciseId: return undefined; }; -const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution: UserSolution, id: string) => { +const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution: UserSolution, id: string): Promise => { const promiseParts = solution.solutions.map(async (x: {prompt: string; blob: string}) => { const blob = await downloadBlob(x.blob); if (!x.blob.startsWith("blob")) await axios.post("/api/storage/delete", {path: x.blob});