Prepared the code to later handle the evaluation of the Interactive Speaking exercise
This commit is contained in:
@@ -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 {
|
export interface FillBlanksExercise {
|
||||||
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
prompt: string; // *EXAMPLE: "Complete the summary below. Click a blank to select the corresponding word for it."
|
||||||
type: "fillBlanks";
|
type: "fillBlanks";
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const ExamLoader = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
if (selectedModule && examId) {
|
if (selectedModule && examId) {
|
||||||
const exam = await getExamById(selectedModule, examId);
|
const exam = await getExamById(selectedModule, examId.trim());
|
||||||
if (!exam) {
|
if (!exam) {
|
||||||
toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", {
|
toast.error("Unknown Exam ID! Please make sure you selected the right module and entered the right exam ID", {
|
||||||
toastId: "invalid-exam-id",
|
toastId: "invalid-exam-id",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import useExamStore from "@/stores/examStore";
|
|||||||
import Layout from "@/components/High/Layout";
|
import Layout from "@/components/High/Layout";
|
||||||
import {speakingReverseMarking, writingReverseMarking} from "@/utils/score";
|
import {speakingReverseMarking, writingReverseMarking} from "@/utils/score";
|
||||||
import AbandonPopup from "@/components/AbandonPopup";
|
import AbandonPopup from "@/components/AbandonPopup";
|
||||||
|
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
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<Evaluation>("/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 updateExamWithUserSolutions = (exam: Exam): Exam => {
|
||||||
const exercises = exam.exercises.map((x) =>
|
const exercises = exam.exercises.map((x) =>
|
||||||
Object.assign(x, !x.userSolutions ? {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions} : x.userSolutions),
|
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 onFinish = (solutions: UserSolution[]) => {
|
||||||
const solutionIds = solutions.map((x) => x.exercise);
|
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) {
|
if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) {
|
||||||
setHasBeenUploaded(true);
|
setHasBeenUploaded(true);
|
||||||
setIsEvaluationLoading(true);
|
setIsEvaluationLoading(true);
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
exam.exercises.map((exercise) =>
|
exam.exercises.map(async (exercise) => {
|
||||||
(exam.module === "writing" ? evaluateWritingAnswer : evaluateSpeakingAnswer)(
|
return (exam.module === "writing" ? evaluateWritingAnswer : evaluateSpeakingAnswer)(
|
||||||
|
exams,
|
||||||
exam.id,
|
exam.id,
|
||||||
exercise.id,
|
exercise.id,
|
||||||
solutions.find((x) => x.exercise === exercise.id)!,
|
solutions.find((x) => x.exercise === exercise.id)!,
|
||||||
),
|
).then((response) => {
|
||||||
),
|
if (response) {
|
||||||
|
setUserSolutions([...userSolutions.filter((x) => x.exercise !== exercise.id), response]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
).finally(() => {
|
).finally(() => {
|
||||||
setIsEvaluationLoading(false);
|
setIsEvaluationLoading(false);
|
||||||
setHasBeenUploaded(false);
|
setHasBeenUploaded(false);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import Layout from "@/components/High/Layout";
|
|||||||
import {sortByModule} from "@/utils/moduleUtils";
|
import {sortByModule} from "@/utils/moduleUtils";
|
||||||
import {speakingReverseMarking, writingReverseMarking} from "@/utils/score";
|
import {speakingReverseMarking, writingReverseMarking} from "@/utils/score";
|
||||||
import AbandonPopup from "@/components/AbandonPopup";
|
import AbandonPopup from "@/components/AbandonPopup";
|
||||||
|
import {evaluateSpeakingAnswer, evaluateWritingAnswer} from "@/utils/evaluation";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
|
||||||
const user = req.session.user;
|
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<Evaluation>("/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 updateExamWithUserSolutions = (exam: Exam): Exam => {
|
||||||
const exercises = exam.exercises.map((x) => Object.assign(x, {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions}));
|
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) {
|
if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) {
|
||||||
setHasBeenUploaded(true);
|
setHasBeenUploaded(true);
|
||||||
setIsEvaluationLoading(true);
|
setIsEvaluationLoading(true);
|
||||||
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
exam.exercises.map((exercise) =>
|
exam.exercises.map(async (exercise) => {
|
||||||
(exam.module === "writing" ? evaluateWritingAnswer : evaluateSpeakingAnswer)(
|
return (exam.module === "writing" ? evaluateWritingAnswer : evaluateSpeakingAnswer)(
|
||||||
|
exams,
|
||||||
exam.id,
|
exam.id,
|
||||||
exercise.id,
|
exercise.id,
|
||||||
solutions.find((x) => x.exercise === exercise.id)!,
|
solutions.find((x) => x.exercise === exercise.id)!,
|
||||||
),
|
).then((response) => {
|
||||||
),
|
if (response) {
|
||||||
|
setUserSolutions([...userSolutions.filter((x) => x.exercise !== exercise.id), response]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
).finally(() => {
|
).finally(() => {
|
||||||
setIsEvaluationLoading(false);
|
setIsEvaluationLoading(false);
|
||||||
setHasBeenUploaded(false);
|
setHasBeenUploaded(false);
|
||||||
|
|||||||
70
src/utils/evaluation.ts
Normal file
70
src/utils/evaluation.ts
Normal file
@@ -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<Evaluation>("/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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user