diff --git a/src/components/Exercises/Speaking.tsx b/src/components/Exercises/Speaking.tsx
index cc7b2fdd..746f1d18 100644
--- a/src/components/Exercises/Speaking.tsx
+++ b/src/components/Exercises/Speaking.tsx
@@ -4,7 +4,6 @@ import {Fragment, useEffect, useState} from "react";
import {BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill} from "react-icons/bs";
import dynamic from "next/dynamic";
import Button from "../Low/Button";
-import axios from "axios";
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
@@ -29,31 +28,6 @@ export default function Speaking({id, title, text, type, prompts, onNext, onBack
};
}, [isRecording]);
- useEffect(() => {
- const uploadFile = () => {
- if (mediaBlob) {
- axios.get(mediaBlob, {responseType: "arraybuffer"}).then((response) => {
- const audioBlob = Buffer.from(response.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", `${text.replaceAll("\n", "")} You should talk about: ${prompts.join(", ")}`);
-
- const config = {
- headers: {
- "Content-Type": "audio/mp3",
- },
- };
-
- axios.post("/api/evaluate/speaking", formData, config);
- });
- }
- };
-
- if (mediaBlob) uploadFile();
- }, [mediaBlob, text, prompts]);
-
return (
diff --git a/src/components/Solutions/Speaking.tsx b/src/components/Solutions/Speaking.tsx
new file mode 100644
index 00000000..999e04bf
--- /dev/null
+++ b/src/components/Solutions/Speaking.tsx
@@ -0,0 +1,84 @@
+/* eslint-disable @next/next/no-img-element */
+import {SpeakingExercise} from "@/interfaces/exam";
+import {CommonProps} from ".";
+import {Fragment, useEffect, useState} from "react";
+import Button from "../Low/Button";
+import dynamic from "next/dynamic";
+import axios from "axios";
+
+const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
+
+export default function Speaking({title, text, prompts, userSolutions, onNext, onBack}: SpeakingExercise & CommonProps) {
+ const [solutionURL, setSolutionURL] = useState
();
+
+ useEffect(() => {
+ axios.post(`/api/speaking`, {path: userSolutions[0].solution}, {responseType: "arraybuffer"}).then(({data}) => {
+ const blob = new Blob([data], {type: "audio/wav"});
+ const url = URL.createObjectURL(blob);
+
+ setSolutionURL(url);
+ });
+ }, [userSolutions]);
+
+ return (
+ <>
+
+
+
+ {title}
+
+ {text.split("\\n").map((line, index) => (
+
+ {line}
+
+
+ ))}
+
+
+
+
You should talk about the following things:
+
+ {prompts.map((x, index) => (
+
+ {x}
+
+ ))}
+
+
+
+
+
+
+
+ {solutionURL && }
+
+
+ {userSolutions && userSolutions.length > 0 && (
+
+
+ {Object.keys(userSolutions[0].evaluation!.task_response).map((key) => (
+
+ {key}: Level {userSolutions[0].evaluation!.task_response[key]}
+
+ ))}
+
+
+ {userSolutions[0].evaluation!.comment}
+
+
+ )}
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/Solutions/index.tsx b/src/components/Solutions/index.tsx
index fdb3d2f3..f77e41f0 100644
--- a/src/components/Solutions/index.tsx
+++ b/src/components/Solutions/index.tsx
@@ -1,7 +1,16 @@
-import {Exercise, FillBlanksExercise, MatchSentencesExercise, MultipleChoiceExercise, WriteBlanksExercise, WritingExercise} from "@/interfaces/exam";
+import {
+ Exercise,
+ FillBlanksExercise,
+ MatchSentencesExercise,
+ MultipleChoiceExercise,
+ SpeakingExercise,
+ WriteBlanksExercise,
+ WritingExercise,
+} from "@/interfaces/exam";
import dynamic from "next/dynamic";
import FillBlanks from "./FillBlanks";
import MultipleChoice from "./MultipleChoice";
+import Speaking from "./Speaking";
import WriteBlanks from "./WriteBlanks";
import Writing from "./Writing";
@@ -24,5 +33,7 @@ export const renderSolution = (exercise: Exercise, onNext: () => void, onBack: (
return ;
case "writing":
return ;
+ case "speaking":
+ return ;
}
};
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index d9122cc7..43dc219d 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -15,7 +15,7 @@ export default function App({Component, pageProps}: AppProps) {
const router = useRouter();
useEffect(() => {
- reset();
+ if (router.pathname !== "/exercises") reset();
}, [router.pathname, reset]);
return ;
diff --git a/src/pages/api/evaluate/speaking.ts b/src/pages/api/evaluate/speaking.ts
index beef374a..1241aae9 100644
--- a/src/pages/api/evaluate/speaking.ts
+++ b/src/pages/api/evaluate/speaking.ts
@@ -20,26 +20,25 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const storage = getStorage(app);
const form = formidable({keepExtensions: true, uploadDir: "./"});
- form.parse(req, (err, fields, files) => {
- const audioFile = (files.audio as unknown as PersistentFile[])[0];
- const audioFileRef = ref(storage, `speaking_recordings/${(audioFile as any).newFilename}`);
+ const [fields, files] = await form.parse(req);
+ const audioFile = (files.audio as unknown as PersistentFile[])[0];
+ const audioFileRef = ref(storage, `speaking_recordings/${(audioFile as any).newFilename}`);
- const binary = fs.readFileSync((audioFile as any).filepath).buffer;
- uploadBytes(audioFileRef, binary).then(async (snapshot) => {
- const backendRequest = await axios.post(
- `${process.env.BACKEND_URL}/speaking_task_1`,
- {question: (fields.question as string[]).join(""), answer: snapshot.metadata.fullPath},
- {
- headers: {
- Authorization: `Bearer ${process.env.BACKEND_JWT}`,
- },
- },
- );
+ const binary = fs.readFileSync((audioFile as any).filepath).buffer;
+ const snapshot = await uploadBytes(audioFileRef, binary);
- fs.rmSync((audioFile as any).filepath);
- res.status(200).json({...backendRequest.data, fullPath: snapshot.metadata.fullPath});
- });
- });
+ const backendRequest = await axios.post(
+ `${process.env.BACKEND_URL}/speaking_task_1`,
+ {question: (fields.question as string[]).join(""), answer: snapshot.metadata.fullPath},
+ {
+ headers: {
+ Authorization: `Bearer ${process.env.BACKEND_JWT}`,
+ },
+ },
+ );
+
+ fs.rmSync((audioFile as any).filepath);
+ res.status(200).json({...backendRequest.data, fullPath: snapshot.metadata.fullPath});
}
export const config = {
diff --git a/src/pages/api/speaking.ts b/src/pages/api/speaking.ts
index 5678693c..d33e927f 100644
--- a/src/pages/api/speaking.ts
+++ b/src/pages/api/speaking.ts
@@ -4,19 +4,23 @@ import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import {getDownloadURL, getStorage, ref} from "firebase/storage";
import {app} from "@/firebase";
+import axios from "axios";
-// export default withIronSessionApiRoute(handler, sessionOptions);
-export default handler;
+export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
- // if (!req.session.user) {
- // res.status(401).json({ok: false});
- // return;
- // }
+ if (!req.session.user) {
+ res.status(401).json({ok: false});
+ return;
+ }
const storage = getStorage(app);
const {path} = req.body as {path: string};
const pathReference = ref(storage, path);
- getDownloadURL(pathReference).then((url) => res.status(200).json({url}));
+ const url = await getDownloadURL(pathReference);
+
+ const response = await axios.get(url, {responseType: "arraybuffer"});
+
+ res.status(200).send(response.data);
}
diff --git a/src/pages/exam.tsx b/src/pages/exam.tsx
index b7b5e5e7..0713b1db 100644
--- a/src/pages/exam.tsx
+++ b/src/pages/exam.tsx
@@ -133,7 +133,7 @@ export default function Page() {
const speakingExam = exams.find((x) => x.id === examId)!;
const exercise = speakingExam.exercises.find((x) => x.id === exerciseId)! as SpeakingExercise;
- const blobResponse = await axios.get(exercise.userSolutions[0].solution.trim());
+ 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"});
@@ -155,7 +155,7 @@ export default function Page() {
{
...solution,
score: {
- correct: speakingReverseMarking[response.data.overall],
+ correct: speakingReverseMarking[response.data.overall] || 0,
missing: 0,
total: 100,
},
@@ -180,7 +180,7 @@ export default function Page() {
{
...solution,
score: {
- correct: writingReverseMarking[response.data.overall],
+ correct: writingReverseMarking[response.data.overall] || 0,
missing: 0,
total: 100,
},
@@ -294,11 +294,6 @@ export default function Page() {
return ;
}
- if (exam && exam.module === "speaking" && showSolutions) {
- setModuleIndex((prev) => prev + 1);
- return <>>;
- }
-
if (exam && exam.module === "speaking") {
return ;
}
diff --git a/src/pages/exercises.tsx b/src/pages/exercises.tsx
index 1f3a008b..967c8d4d 100644
--- a/src/pages/exercises.tsx
+++ b/src/pages/exercises.tsx
@@ -6,7 +6,17 @@ import {Module} from "@/interfaces";
import Selection from "@/exams/Selection";
import Reading from "@/exams/Reading";
-import {Exam, ListeningExam, ReadingExam, SpeakingExam, UserSolution, Evaluation, WritingExam, WritingExercise} from "@/interfaces/exam";
+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";
@@ -22,7 +32,7 @@ import useExamStore from "@/stores/examStore";
import Sidebar from "@/components/Sidebar";
import Layout from "@/components/High/Layout";
import {sortByModule} from "@/utils/moduleUtils";
-import {writingReverseMarking} from "@/utils/score";
+import {speakingReverseMarking, writingReverseMarking} from "@/utils/score";
export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user;
@@ -122,6 +132,42 @@ 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;
@@ -137,7 +183,7 @@ export default function Page() {
{
...solution,
score: {
- correct: writingReverseMarking[response.data.overall],
+ correct: writingReverseMarking[response.data.overall] || 0,
missing: 0,
total: 100,
},
@@ -148,9 +194,7 @@ export default function Page() {
};
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),
- );
+ const exercises = exam.exercises.map((x) => Object.assign(x, {userSolutions: userSolutions.find((y) => x.id === y.exercise)?.solutions}));
return Object.assign(exam, exercises);
};
@@ -158,11 +202,17 @@ export default function Page() {
const onFinish = (solutions: UserSolution[]) => {
const solutionIds = solutions.map((x) => x.exercise);
- if (exam && exam.module === "writing" && solutions.length > 0 && !showSolutions) {
+ if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) {
setHasBeenUploaded(true);
setIsEvaluationLoading(true);
Promise.all(
- exam.exercises.map((exercise) => evaluateWritingAnswer(exam.id, exercise.id, solutions.find((x) => x.exercise === exercise.id)!)),
+ 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);
@@ -245,11 +295,6 @@ export default function Page() {
return ;
}
- if (exam && exam.module === "speaking" && showSolutions) {
- setModuleIndex((prev) => prev + 1);
- return <>>;
- }
-
if (exam && exam.module === "speaking") {
return ;
}
@@ -260,7 +305,7 @@ export default function Page() {
return (
<>
- Exam | IELTS GPT
+ Exercises | IELTS GPT
x!.module),
);
- router.push("/exam");
+ router.push("/exercises");
}
});
};
diff --git a/src/utils/score.ts b/src/utils/score.ts
index 2c9bb5da..d1276618 100644
--- a/src/utils/score.ts
+++ b/src/utils/score.ts
@@ -4,27 +4,45 @@ type Type = "academic" | "general";
export const writingReverseMarking: {[key: number]: number} = {
9: 90,
+ 8.5: 85,
8: 80,
+ 7.5: 75,
7: 70,
+ 6.5: 65,
6: 60,
+ 5.5: 55,
5: 50,
+ 4.5: 45,
4: 40,
+ 3.5: 35,
3: 30,
+ 2.5: 25,
2: 20,
+ 1.5: 15,
1: 10,
+ 0.5: 5,
0: 0,
};
export const speakingReverseMarking: {[key: number]: number} = {
9: 90,
+ 8.5: 85,
8: 80,
+ 7.5: 75,
7: 70,
+ 6.5: 65,
6: 60,
+ 5.5: 55,
5: 50,
+ 4.5: 45,
4: 40,
+ 3.5: 35,
3: 30,
+ 2.5: 25,
2: 20,
+ 1.5: 15,
1: 10,
+ 0.5: 5,
0: 0,
};