From efb341355dcb8e0d3c10950622963c36a2520202 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Sun, 17 Sep 2023 08:56:00 +0100 Subject: [PATCH] Prepared the code to later handle the evaluation of the Interactive Speaking exercise --- src/interfaces/exam.ts | 13 +++++++ src/pages/admin.tsx | 2 +- src/pages/exam.tsx | 79 ++++++++--------------------------------- src/pages/exercises.tsx | 76 ++++++--------------------------------- src/utils/evaluation.ts | 70 ++++++++++++++++++++++++++++++++++++ 5 files changed, 109 insertions(+), 131 deletions(-) create mode 100644 src/utils/evaluation.ts diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts index fa606959..76bcad67 100644 --- a/src/interfaces/exam.ts +++ b/src/interfaces/exam.ts @@ -106,6 +106,19 @@ export interface SpeakingExercise { }[]; } +export interface InteractiveSpeakingExercise { + id: string; + type: "speaking"; + title: string; + text: string; + prompts: {text: string; video_url: string}[]; + userSolutions: { + id: string; + solution: string; + evaluation?: Evaluation; + }[]; +} + export interface FillBlanksExercise { prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it." type: "fillBlanks"; diff --git a/src/pages/admin.tsx b/src/pages/admin.tsx index 14a3385a..8d576576 100644 --- a/src/pages/admin.tsx +++ b/src/pages/admin.tsx @@ -62,7 +62,7 @@ const ExamLoader = () => { setIsLoading(true); if (selectedModule && examId) { - const exam = await getExamById(selectedModule, examId); + const exam = await getExamById(selectedModule, examId.trim()); if (!exam) { toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", { toastId: "invalid-exam-id", diff --git a/src/pages/exam.tsx b/src/pages/exam.tsx index 8cc69f1d..35f5084d 100644 --- a/src/pages/exam.tsx +++ b/src/pages/exam.tsx @@ -31,6 +31,7 @@ import useExamStore from "@/stores/examStore"; import Layout from "@/components/High/Layout"; import {speakingReverseMarking, writingReverseMarking} from "@/utils/score"; import AbandonPopup from "@/components/AbandonPopup"; +import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -132,67 +133,6 @@ export default function Page() { } }; - 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), @@ -203,18 +143,27 @@ export default function Page() { 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((exercise) => - (exam.module === "writing" ? evaluateWritingAnswer : evaluateSpeakingAnswer)( + exam.exercises.map(async (exercise) => { + return (exam.module === "writing" ? evaluateWritingAnswer : evaluateSpeakingAnswer)( + exams, exam.id, exercise.id, solutions.find((x) => x.exercise === exercise.id)!, - ), - ), + ).then((response) => { + if (response) { + setUserSolutions([...userSolutions.filter((x) => x.exercise !== exercise.id), response]); + } + }); + }), ).finally(() => { setIsEvaluationLoading(false); setHasBeenUploaded(false); diff --git a/src/pages/exercises.tsx b/src/pages/exercises.tsx index 7afaa7a4..4543aaac 100644 --- a/src/pages/exercises.tsx +++ b/src/pages/exercises.tsx @@ -34,6 +34,7 @@ import Layout from "@/components/High/Layout"; import {sortByModule} from "@/utils/moduleUtils"; import {speakingReverseMarking, writingReverseMarking} from "@/utils/score"; import AbandonPopup from "@/components/AbandonPopup"; +import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation"; export const getServerSideProps = withIronSessionSsr(({req, res}) => { const user = req.session.user; @@ -136,67 +137,6 @@ export default function Page() { } }; - 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, {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions})); @@ -212,14 +152,20 @@ export default function Page() { 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.exercises.map(async (exercise) => { + return (exam.module === "writing" ? evaluateWritingAnswer : evaluateSpeakingAnswer)( + exams, exam.id, exercise.id, solutions.find((x) => x.exercise === exercise.id)!, - ), - ), + ).then((response) => { + if (response) { + setUserSolutions([...userSolutions.filter((x) => x.exercise !== exercise.id), response]); + } + }); + }), ).finally(() => { setIsEvaluationLoading(false); setHasBeenUploaded(false); diff --git a/src/utils/evaluation.ts b/src/utils/evaluation.ts new file mode 100644 index 00000000..b29a10c4 --- /dev/null +++ b/src/utils/evaluation.ts @@ -0,0 +1,70 @@ +import {Evaluation, Exam, SpeakingExercise, UserSolution, WritingExercise} from "@/interfaces/exam"; +import axios from "axios"; +import {speakingReverseMarking, writingReverseMarking} from "./score"; + +export const evaluateWritingAnswer = async (exams: Exam[], 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) { + return { + ...solution, + score: { + correct: writingReverseMarking[response.data.overall] || 0, + missing: 0, + total: 100, + }, + solutions: [{id: exerciseId, solution: solution.solutions[0].solution, evaluation: response.data}], + }; + } + + return undefined; +}; + +export const evaluateSpeakingAnswer = async (exams: Exam[], examId: string, exerciseId: string, solution: UserSolution) => { + const speakingExam = exams.find((x) => x.id === examId)!; + const exercise = speakingExam.exercises.find((x) => x.id === exerciseId); + + if (exercise?.type === "speaking") { + return await evaluateSpeakingExercise(exercise, exerciseId, solution); + } + + return undefined; +}; + +const evaluateSpeakingExercise = async (exercise: SpeakingExercise, exerciseId: string, solution: UserSolution) => { + 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) { + return { + ...solution, + score: { + correct: speakingReverseMarking[response.data.overall] || 0, + missing: 0, + total: 100, + }, + solutions: [{id: exerciseId, solution: response.data.fullPath, evaluation: response.data}], + }; + } + + return undefined; +};