diff --git a/src/components/Exercises/Writing.tsx b/src/components/Exercises/Writing.tsx
index 1ba75275..5560c2b7 100644
--- a/src/components/Exercises/Writing.tsx
+++ b/src/components/Exercises/Writing.tsx
@@ -26,6 +26,8 @@ export default function Writing({
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
useEffect(() => {
+ if (localStorage.getItem("enable_paste")) return;
+
const listener = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
e.preventDefault();
diff --git a/src/exams/Finish.tsx b/src/exams/Finish.tsx
index 47bc559f..d3c76218 100644
--- a/src/exams/Finish.tsx
+++ b/src/exams/Finish.tsx
@@ -10,8 +10,8 @@ import Link from "next/link";
import {useRouter} from "next/router";
import {Fragment, useEffect, useState} from "react";
import {BsArrowCounterclockwise, BsBook, BsClipboard, BsEyeFill, BsHeadphones, BsMegaphone, BsPen, BsShareFill} from "react-icons/bs";
-import { LevelScore } from "@/constants/ielts";
-import { getLevelScore } from "@/utils/score";
+import {LevelScore} from "@/constants/ielts";
+import {getLevelScore} from "@/utils/score";
interface Score {
module: Module;
@@ -71,20 +71,18 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
const bandScore: number = calculateBandScore(selectedScore.correct, selectedScore.total, selectedModule, user.focus);
const showLevel = (level: number) => {
- if(selectedModule === "level") {
+ if (selectedModule === "level") {
const [levelStr, grade] = getLevelScore(level);
return (
{levelStr}
{grade}
- )
- }
-
-
- return {level};
+ );
+ }
- }
+ return {level};
+ };
return (
<>
@@ -156,14 +154,16 @@ export default function Finish({user, scores, modules, isLoading, onViewResults}
{isLoading && (
- Evaluating your answers...
+
+ Evaluating your answers, please be patient...
+
+ You can also check it later on your records page!
+
)}
{!isLoading && (
-
- {moduleResultText(selectedModule, bandScore)}
-
+
{moduleResultText(selectedModule, bandScore)}
{
setIsLoading(true);
axios
- .get
(!id ? "/api/stats" : `/api/stats/${id}`)
+ .get(!id ? "/api/stats" : `/api/stats/user/${id}`)
.then((response) => setStats(response.data))
.finally(() => setIsLoading(false));
}, [id]);
diff --git a/src/interfaces/exam.ts b/src/interfaces/exam.ts
index f25cdfbb..bd4f26c1 100644
--- a/src/interfaces/exam.ts
+++ b/src/interfaces/exam.ts
@@ -44,6 +44,7 @@ export interface ListeningPart {
}
export interface UserSolution {
+ id?: string;
solutions: any[];
module?: Module;
exam?: string;
diff --git a/src/interfaces/user.ts b/src/interfaces/user.ts
index cd337ac5..c7b0efff 100644
--- a/src/interfaces/user.ts
+++ b/src/interfaces/user.ts
@@ -98,6 +98,7 @@ export const EMPLOYMENT_STATUS: {status: EmploymentStatus; label: string}[] = [
];
export interface Stat {
+ id: string;
user: string;
exam: string;
exercise: string;
diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx
index f092ffcb..67cc275a 100644
--- a/src/pages/(exam)/ExamPage.tsx
+++ b/src/pages/(exam)/ExamPage.tsx
@@ -37,6 +37,7 @@ export default function ExamPage({page}: Props) {
const [showAbandonPopup, setShowAbandonPopup] = useState(false);
const [avoidRepeated, setAvoidRepeated] = useState(false);
const [timeSpent, setTimeSpent] = useState(0);
+ const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState([]);
const [exams, setExams] = useExamStore((state) => [state.exams, state.setExams]);
const [userSolutions, setUserSolutions] = useExamStore((state) => [state.userSolutions, state.setUserSolutions]);
@@ -94,6 +95,7 @@ export default function ExamPage({page}: Props) {
if (selectedModules.length > 0 && exams.length !== 0 && moduleIndex >= selectedModules.length && !hasBeenUploaded && !showSolutions) {
const newStats: Stat[] = userSolutions.map((solution) => ({
...solution,
+ id: solution.id || uuidv4(),
timeSpent,
session: sessionId,
exam: solution.exam!,
@@ -111,6 +113,41 @@ export default function ExamPage({page}: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedModules, moduleIndex, hasBeenUploaded]);
+ useEffect(() => {
+ if (statsAwaitingEvaluation.length === 0) return setIsEvaluationLoading(false);
+ return setIsEvaluationLoading(true);
+ }, [statsAwaitingEvaluation]);
+
+ useEffect(() => {
+ if (statsAwaitingEvaluation.length > 0) {
+ statsAwaitingEvaluation.forEach(checkIfStatHasBeenEvaluated);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [statsAwaitingEvaluation]);
+
+ const checkIfStatHasBeenEvaluated = (id: string) => {
+ setTimeout(async () => {
+ const statRequest = await axios.get(`/api/stats/${id}`);
+ const stat = statRequest.data;
+ if (stat.solutions.every((x) => x.evaluation !== null)) {
+ const userSolution: UserSolution = {
+ id,
+ exercise: stat.exercise,
+ score: stat.score,
+ solutions: stat.solutions,
+ type: stat.type,
+ exam: stat.exam,
+ module: stat.module,
+ };
+
+ setUserSolutions(userSolutions.map((x) => (x.exercise === userSolution.exercise ? userSolution : x)));
+ return setStatsAwaitingEvaluation((prev) => prev.filter((x) => x !== id));
+ }
+
+ return checkIfStatHasBeenEvaluated(id);
+ }, 5 * 1000);
+ };
+
const updateExamWithUserSolutions = (exam: Exam): Exam => {
if (exam.module === "reading" || exam.module === "listening") {
const parts = exam.parts.map((p) =>
@@ -137,20 +174,19 @@ export default function ExamPage({page}: Props) {
Promise.all(
exam.exercises.map(async (exercise) => {
- if (exercise.type === "writing") {
- return await evaluateWritingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!);
- }
+ const evaluationID = uuidv4();
+ if (exercise.type === "writing")
+ return await evaluateWritingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID);
- if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking") {
- return await evaluateSpeakingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!);
- }
+ if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking")
+ return await evaluateSpeakingAnswer(exercise, solutions.find((x) => x.exercise === exercise.id)!, evaluationID);
}),
)
.then((responses) => {
+ setStatsAwaitingEvaluation((prev) => [...prev, ...responses.filter((x) => !!x).map((r) => (r as any).id)]);
setUserSolutions([...userSolutions, ...responses.filter((x) => !!x)] as any);
})
.finally(() => {
- setIsEvaluationLoading(false);
setHasBeenUploaded(false);
});
}
diff --git a/src/pages/api/evaluate/interactiveSpeaking.ts b/src/pages/api/evaluate/interactiveSpeaking.ts
index aa761ff1..62e9f53e 100644
--- a/src/pages/api/evaluate/interactiveSpeaking.ts
+++ b/src/pages/api/evaluate/interactiveSpeaking.ts
@@ -2,12 +2,16 @@
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
-import axios from "axios";
+import axios, {AxiosResponse} from "axios";
import formidable from "formidable-serverless";
import {ref, uploadBytes} from "firebase/storage";
import fs from "fs";
-import {storage} from "@/firebase";
+import {app, storage} from "@/firebase";
+import {doc, getDoc, getFirestore, setDoc} from "firebase/firestore";
+import {Stat} from "@/interfaces/user";
+import {speakingReverseMarking} from "@/utils/score";
+const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -36,20 +40,41 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}),
);
- const backendRequest = await axios.post(
- `${process.env.BACKEND_URL}/speaking_task_3`,
- {answers: uploadingAudios},
+ res.status(200).json(null);
+
+ console.log("🌱 - Still processing");
+ const backendRequest = await evaluate({answers: uploadingAudios});
+ console.log("🌱 - Process complete");
+
+ const correspondingStat = (await getDoc(doc(db, "stats", fields.id))).data() as Stat;
+ const solutions = correspondingStat.solutions.map((x) => ({...x, evaluation: backendRequest.data, solution: uploadingAudios}));
+ await setDoc(
+ doc(db, "stats", fields.id),
{
- headers: {
- Authorization: `Bearer ${process.env.BACKEND_JWT}`,
+ solutions,
+ score: {
+ correct: speakingReverseMarking[backendRequest.data.overall],
+ missing: 0,
+ total: 100,
},
},
+ {merge: true},
);
-
- res.status(200).json({...backendRequest.data, answer: uploadingAudios});
+ console.log("🌱 - Updated the DB");
});
}
+async function evaluate(body: {answers: object[]}): Promise {
+ const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_3`, body, {
+ headers: {
+ Authorization: `Bearer ${process.env.BACKEND_JWT}`,
+ },
+ });
+
+ if (typeof backendRequest.data === "string") return evaluate(body);
+ return backendRequest;
+}
+
export const config = {
api: {
bodyParser: false,
diff --git a/src/pages/api/evaluate/speaking.ts b/src/pages/api/evaluate/speaking.ts
index 2706f9d9..a299df9e 100644
--- a/src/pages/api/evaluate/speaking.ts
+++ b/src/pages/api/evaluate/speaking.ts
@@ -6,8 +6,12 @@ import axios, {AxiosResponse} from "axios";
import formidable from "formidable-serverless";
import {ref, uploadBytes} from "firebase/storage";
import fs from "fs";
-import {storage} from "@/firebase";
+import {app, storage} from "@/firebase";
+import {doc, getDoc, getFirestore, setDoc} from "firebase/firestore";
+import {Stat} from "@/interfaces/user";
+import {speakingReverseMarking} from "@/utils/score";
+const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -26,10 +30,32 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const binary = fs.readFileSync((audioFile as any).path).buffer;
const snapshot = await uploadBytes(audioFileRef, binary);
- const backendRequest = await evaluate({answers: [{question: fields.question, answer: snapshot.metadata.fullPath}]});
+ res.status(200).json(null);
+ console.log("🌱 - Still processing");
+ const backendRequest = await evaluate({answers: [{question: fields.question, answer: snapshot.metadata.fullPath}]});
fs.rmSync((audioFile as any).path);
- res.status(200).json({...backendRequest.data, fullPath: snapshot.metadata.fullPath});
+ console.log("🌱 - Process complete");
+
+ const correspondingStat = (await getDoc(doc(db, "stats", fields.id))).data() as Stat;
+ const solutions = correspondingStat.solutions.map((x) => ({
+ ...x,
+ evaluation: backendRequest.data,
+ solution: snapshot.metadata.fullPath,
+ }));
+ await setDoc(
+ doc(db, "stats", fields.id),
+ {
+ solutions,
+ score: {
+ correct: speakingReverseMarking[backendRequest.data.overall],
+ total: 100,
+ missing: 0,
+ },
+ },
+ {merge: true},
+ );
+ console.log("🌱 - Updated the DB");
});
}
diff --git a/src/pages/api/evaluate/writing.ts b/src/pages/api/evaluate/writing.ts
index 40c84958..841b4c92 100644
--- a/src/pages/api/evaluate/writing.ts
+++ b/src/pages/api/evaluate/writing.ts
@@ -1,15 +1,20 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
-import {getFirestore, doc, getDoc} from "firebase/firestore";
+import {getFirestore, doc, getDoc, setDoc} from "firebase/firestore";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import axios, {AxiosResponse} from "axios";
+import {app} from "@/firebase";
+import {Stat} from "@/interfaces/user";
+import {writingReverseMarking} from "@/utils/score";
interface Body {
question: string;
answer: string;
+ id: string;
}
+const db = getFirestore(app);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -18,9 +23,27 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return;
}
- const backendRequest = await evaluate(req.body as Body);
+ res.status(200).json(null);
- res.status(backendRequest.status).json(backendRequest.data);
+ console.log("🌱 - Still processing");
+ const backendRequest = await evaluate(req.body as Body);
+ console.log("🌱 - Process complete");
+
+ const correspondingStat = (await getDoc(doc(db, "stats", req.body.id))).data() as Stat;
+ const solutions = correspondingStat.solutions.map((x) => ({...x, evaluation: backendRequest.data}));
+ await setDoc(
+ doc(db, "stats", (req.body as Body).id),
+ {
+ solutions,
+ score: {
+ correct: writingReverseMarking[backendRequest.data.overall],
+ total: 100,
+ missing: 0,
+ },
+ },
+ {merge: true},
+ );
+ console.log("🌱 - Updated the DB");
}
async function evaluate(body: Body): Promise {
diff --git a/src/pages/api/stats/[id].ts b/src/pages/api/stats/[id].ts
new file mode 100644
index 00000000..0597f529
--- /dev/null
+++ b/src/pages/api/stats/[id].ts
@@ -0,0 +1,23 @@
+// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
+import type {NextApiRequest, NextApiResponse} from "next";
+import {app} from "@/firebase";
+import {getFirestore, collection, getDocs, query, where, setDoc, doc, getDoc, deleteDoc} from "firebase/firestore";
+import {withIronSessionApiRoute} from "iron-session/next";
+import {sessionOptions} from "@/lib/session";
+import {uuidv4} from "@firebase/util";
+
+const db = getFirestore(app);
+
+export default async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method === "GET") return GET(req, res);
+
+ res.status(404).json({ok: false});
+}
+
+async function GET(req: NextApiRequest, res: NextApiResponse) {
+ const {id} = req.query;
+
+ const snapshot = await getDoc(doc(db, "stats", id as string));
+
+ res.status(200).json({...snapshot.data(), id: snapshot.id});
+}
diff --git a/src/pages/api/stats/index.ts b/src/pages/api/stats/index.ts
index 7444b50d..779427ba 100644
--- a/src/pages/api/stats/index.ts
+++ b/src/pages/api/stats/index.ts
@@ -42,7 +42,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
}
const stats = req.body as Stat[];
- await stats.forEach(async (stat) => await addDoc(collection(db, "stats"), stat));
+ await stats.forEach(async (stat) => await setDoc(doc(db, "stats", stat.id), stat));
const groupedStatsByAssignment = groupBy(
stats.filter((x) => !!x.assignment),
diff --git a/src/pages/api/stats/[user].ts b/src/pages/api/stats/user/[user].ts
similarity index 100%
rename from src/pages/api/stats/[user].ts
rename to src/pages/api/stats/user/[user].ts
diff --git a/src/utils/evaluation.ts b/src/utils/evaluation.ts
index 55bec84f..d9c53b19 100644
--- a/src/utils/evaluation.ts
+++ b/src/utils/evaluation.ts
@@ -11,17 +11,19 @@ import {
import axios from "axios";
import {speakingReverseMarking, writingReverseMarking} from "./score";
-export const evaluateWritingAnswer = async (exercise: WritingExercise, solution: UserSolution): Promise