Updated the eval calls to the backend, passed the navigation logic of level to useExamNavigation hook

This commit is contained in:
Carlos-Mesquita
2024-11-26 09:04:38 +00:00
parent bb5326a331
commit 2ed4e6509e
14 changed files with 452 additions and 493 deletions

View File

@@ -34,10 +34,8 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
const router = useRouter();
const [variant, setVariant] = useState<Variant>("full");
const [avoidRepeated, setAvoidRepeated] = useState(false);
const [hasBeenUploaded, setHasBeenUploaded] = useState(false);
const [showAbandonPopup, setShowAbandonPopup] = useState(false);
const [isEvaluationLoading, setIsEvaluationLoading] = useState(false);
const [statsAwaitingEvaluation, setStatsAwaitingEvaluation] = useState<string[]>([]);
const [pendingExercises, setPendingExercises] = useState<string[]>([]);
const {
exam, setExam,
@@ -59,11 +57,11 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
saveStats,
saveSession,
setFlags,
setShuffles
setShuffles,
evaluated,
setEvaluated,
} = useExamStore();
const { finalizeModule, finalizeExam } = flags;
const [isFetchingExams, setIsFetchingExams] = useState(false);
const [isExamLoaded, setIsExamLoaded] = useState(moduleIndex < selectedModules.length);
@@ -114,99 +112,115 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
resetStore();
setVariant("full");
setAvoidRepeated(false);
setHasBeenUploaded(false);
setShowAbandonPopup(false);
setIsEvaluationLoading(false);
setStatsAwaitingEvaluation([]);
};
useEffect(() => {
if (finalizeModule && !showSolutions) {
/*if (exam && (exam.module === "writing" || exam.module === "speaking") && userSolutions.length > 0 && !showSolutions) {
setIsEvaluationLoading(true);
if (flags.finalizeModule && !showSolutions && flags.pendingEvaluation) {
if (exam && (exam.module === "writing" || exam.module === "speaking") && userSolutions.length > 0 && !showSolutions) {
const exercisesToEvaluate = exam.exercises
.map(exercise => exercise.id);
setPendingExercises(exercisesToEvaluate);
(async () => {
const responses: UserSolution[] = (
await Promise.all(
exam.exercises.map(async (exercise, index) => {
const evaluationID = uuidv4();
if (exercise.type === "writing")
return await evaluateWritingAnswer(exercise, index + 1, userSolutions.find((x) => x.exercise === exercise.id)!, evaluationID);
await Promise.all(
exam.exercises.map(async (exercise, index) => {
if (exercise.type === "writing")
await evaluateWritingAnswer(user.id, sessionId, exercise, index + 1, userSolutions.find((x) => x.exercise === exercise.id)!);
if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking")
return await evaluateSpeakingAnswer(
exercise,
userSolutions.find((x) => x.exercise === exercise.id)!,
evaluationID,
index + 1,
);
}),
)
).filter((x) => !!x) as UserSolution[];
if (exercise.type === "interactiveSpeaking" || exercise.type === "speaking")
await evaluateSpeakingAnswer(
user.id,
sessionId,
exercise,
userSolutions.find((x) => x.exercise === exercise.id)!,
index + 1,
);
}),
)
})();
}*/
}
}
}, [exam, finalizeModule, showSolutions, userSolutions]);
/*useEffect(() => {
// poll backend and setIsEvaluationLoading to false
}, []);*/
}, [exam, showSolutions, userSolutions, sessionId, user?.id, flags]);
useEffect(() => {
if (finalizeExam && !isEvaluationLoading) {
if (!flags.pendingEvaluation || pendingExercises.length === 0) return;
const pollStatus = async () => {
try {
// Will fetch evaluations that either were completed or had an error
const { data } = await axios.get('/api/evaluate/status', {
params: {
sessionId,
userId: user.id,
exerciseIds: pendingExercises.join(',')
}
});
if (data.finishedExerciseIds.length > 0) {
const remainingExercises = pendingExercises.filter(id => !data.finishedExerciseIds.includes(id));
setPendingExercises(remainingExercises);
if (remainingExercises.length === 0) {
const evaluatedData = await axios.post('/api/evaluate/fetchSolutions', {
sessionId,
userId: user.id,
userSolutions
});
const newEvaluations = evaluatedData.data.filter(
(newEval: UserSolution) => !evaluated.some(
existingEval => existingEval.exercise === newEval.exercise
)
);
setEvaluated([...evaluated, ...newEvaluations]);
setFlags({ pendingEvaluation: false });
return;
}
}
if (pendingExercises.length > 0) {
setTimeout(pollStatus, 5000);
}
} catch (error) {
console.error(error);
setTimeout(pollStatus, 5000);
}
};
pollStatus();
}, [sessionId, user.id, userSolutions, setFlags, setEvaluated, evaluated, flags, pendingExercises]);
useEffect(() => {
if (flags.finalizeExam && moduleIndex !== -1) {
setModuleIndex(-1);
}
}, [flags.finalizeExam, moduleIndex, setModuleIndex]);
useEffect(() => {
if (flags.finalizeExam && !flags.pendingEvaluation && pendingExercises.length === 0) {
(async () => {
axios.get("/api/stats/update");
if (evaluated.length !== 0) {
setUserSolutions(
userSolutions.map(solution => {
const evaluatedSolution = evaluated.find(e => e.exercise === solution.exercise);
if (evaluatedSolution) {
return { ...solution, ...evaluatedSolution };
}
return solution;
})
);
}
await saveStats();
setModuleIndex(-1);
await axios.get("/api/stats/update");
setShowSolutions(true);
setFlags({ finalizeExam: false });
dispatch({type: "UPDATE_EXAMS"})
})();
}
}, [finalizeExam, saveStats, setFlags, setModuleIndex, isEvaluationLoading]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveStats, setFlags, setModuleIndex, evaluated, pendingExercises, setUserSolutions]);
const onFinish = async (solutions: UserSolution[]) => {
const solutionIds = solutions.map((x) => x.exercise);
const solutionExams = solutions.map((x) => x.exam);
let newSolutions = [...solutions];
if (exam && !solutionExams.includes(exam.id)) return;
if (exam && (exam.module === "writing" || exam.module === "speaking") && solutions.length > 0 && !showSolutions) {
setHasBeenUploaded(true);
setIsEvaluationLoading(true);
const responses: UserSolution[] = (
await Promise.all(
exam.exercises.map(async (exercise, index) => {
const evaluationID = uuidv4();
if (exercise.type === "writing")
return await evaluateWritingAnswer(exercise, index + 1, 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)!,
evaluationID,
index + 1,
);
}),
)
).filter((x) => !!x) as UserSolution[];
newSolutions = [...newSolutions.filter((x) => !responses.map((y) => y.exercise).includes(x.exercise)), ...responses];
setStatsAwaitingEvaluation((prev) => [...prev, ...responses.filter((x) => !!x).map((r) => (r as any).id)]);
setHasBeenUploaded(false);
}
axios.get("/api/stats/update");
setUserSolutions([...userSolutions.filter((x) => !solutionIds.includes(x.exercise)), ...newSolutions]);
setModuleIndex(moduleIndex + 1);
setPartIndex(0);
setExerciseIndex(0);
setQuestionIndex(0);
};
const aggregateScoresByModule = (): {
module: Module;
@@ -306,7 +320,7 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
)}
{(moduleIndex === -1 && selectedModules.length !== 0) &&
<Finish
isLoading={isEvaluationLoading}
isLoading={flags.pendingEvaluation}
user={user!}
modules={selectedModules}
solutions={userSolutions}
@@ -331,6 +345,7 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar =
setUserSolutions(userSolutions);
}
setShuffles([]);
console.log(exam);
if (index === undefined) {
setFlags({ reviewAll: true });
setModuleIndex(0);

View File

@@ -0,0 +1,80 @@
import type { NextApiRequest, NextApiResponse } from "next";
import client from "@/lib/mongodb";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import { UserSolution } from "@/interfaces/exam";
import { speakingReverseMarking, writingReverseMarking } from "@/utils/score";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
}
async function post(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ ok: false });
return;
}
const { sessionId, userId, userSolutions } = req.body;
const completedEvals = await db.collection("evaluation").find({
session_id: sessionId,
user: userId,
status: "completed"
}).toArray();
const evalsByExercise = new Map(
completedEvals.map(e => [e.exercise_id, e])
);
const solutionsWithEvals = userSolutions.filter((solution: UserSolution) =>
evalsByExercise.has(solution.exercise)
).map((solution: any) => {
const evaluation = evalsByExercise.get(solution.exercise)!;
if (solution.type === 'writing') {
return {
...solution,
solutions: [{
...solution.solutions[0],
evaluation: evaluation.result
}],
score: {
correct: writingReverseMarking[evaluation.result.overall],
total: 100,
missing: 0
},
isDisabled: false
};
}
if (solution.type === 'speaking' || solution.type === 'interactiveSpeaking') {
return {
...solution,
solutions: [{
...solution.solutions[0],
...(
solution.type === 'speaking'
? { fullPath: evaluation.result.fullPath }
: { answer: evaluation.result.answer }
),
evaluation: evaluation.result
}],
score: {
correct: speakingReverseMarking[evaluation.result.overall || 0] || 0,
total: 100,
missing: 0
},
isDisabled: false
};
}
return {
solution,
evaluation
};
});
res.status(200).json(solutionsWithEvals)
}

View File

@@ -1,98 +1,62 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import axios, {AxiosResponse} from "axios";
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import formidable from "formidable-serverless";
import {ref, uploadBytes} from "firebase/storage";
import fs from "fs";
import {storage} from "@/firebase";
import client from "@/lib/mongodb";
import {Stat} from "@/interfaces/user";
import {speakingReverseMarking} from "@/utils/score";
import FormData from 'form-data';
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
res.status(401).json({ ok: false });
return;
}
const form = formidable({keepExtensions: true});
const form = formidable({ keepExtensions: true });
await form.parse(req, async (err: any, fields: any, files: any) => {
if (err) console.log(err);
if (err) {
console.log(err);
res.status(500).json({ ok: false });
return;
}
const uploadingAudios = await Promise.all(
Object.keys(files).map(async (fileID: string) => {
const audioFile = files[fileID];
const questionID = fileID.replace("answer_", "question_");
const formData = new FormData();
formData.append('userId', fields.userId);
formData.append('sessionId', fields.sessionId);
formData.append('exerciseId', fields.exerciseId);
const audioFileRef = ref(storage, `speaking_recordings/${(audioFile as any).path.replace("upload_", "")}`);
Object.keys(files).forEach(fileKey => {
const index = fileKey.split('_')[1];
const questionKey = `question_${index}`;
const binary = fs.readFileSync((audioFile as any).path).buffer;
const snapshot = await uploadBytes(audioFileRef, binary);
const audioFile = files[fileKey];
const binary = fs.readFileSync((audioFile as any).path);
formData.append(`audio_${index}`, binary, 'audio.wav');
formData.append(questionKey, fields[questionKey]);
fs.rmSync((audioFile as any).path);
fs.rmSync((audioFile as any).path);
});
return {question: fields[questionID], answer: snapshot.metadata.fullPath};
}),
);
res.status(200).json(null);
console.log("🌱 - Still processing");
const backendRequest = await evaluate({answers: uploadingAudios}, fields.variant);
console.log("🌱 - Process complete");
const correspondingStat = await getCorrespondingStat(fields.id, 1);
const solutions = correspondingStat.solutions.map((x) => ({...x, evaluation: backendRequest.data, solution: uploadingAudios}));
await db.collection("stats").updateOne(
{ id: fields.id },
await axios.post(
`${process.env.BACKEND_URL}/grade/speaking/${fields.task}`,
formData,
{
$set: {
id: fields.id,
solutions,
score: {
correct: speakingReverseMarking[backendRequest.data.overall || 0] || 0,
missing: 0,
total: 100,
headers: {
...formData.getHeaders(),
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
isDisabled: false
}
},
{ upsert: true }
}
);
console.log("🌱 - Updated the DB");
res.status(200).json({ ok: true });
});
}
async function getCorrespondingStat(id: string, index: number): Promise<Stat> {
console.log(`🌱 - Try number ${index} - ${id}`);
const correspondingStat = await db.collection("stats").findOne<Stat>({ id: id });
if (correspondingStat) return correspondingStat;
await delay(3 * 10000);
return getCorrespondingStat(id, index + 1);
}
async function evaluate(body: {answers: object[]}, variant?: "initial" | "final"): Promise<AxiosResponse> {
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/grade/speaking/${variant === "initial" ? "1" : "3"}`, body, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
});
if (backendRequest.status !== 200) return evaluate(body);
return backendRequest;
}
export const config = {
api: {

View File

@@ -1,67 +1,56 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type {NextApiRequest, NextApiResponse} from "next";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
import axios, {AxiosResponse} from "axios";
import type { NextApiRequest, NextApiResponse } from "next";
import { withIronSessionApiRoute } from "iron-session/next";
import { sessionOptions } from "@/lib/session";
import axios from "axios";
import formidable from "formidable-serverless";
import {getDownloadURL, ref, uploadBytes} from "firebase/storage";
import fs from "fs";
import {storage} from "@/firebase";
import {Stat} from "@/interfaces/user";
import FormData from 'form-data';
export default withIronSessionApiRoute(handler, sessionOptions);
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
res.status(401).json({ ok: false });
return;
}
const form = formidable({keepExtensions: true});
const form = formidable({ keepExtensions: true });
await form.parse(req, async (err: any, fields: any, files: any) => {
if (err) console.log(err);
if (err) {
console.log(err);
res.status(500).json({ ok: false });
return;
}
const formData = new FormData();
formData.append('userId', fields.userId);
formData.append('sessionId', fields.sessionId);
formData.append('exerciseId', fields.exerciseId);
formData.append('question_1', fields.question);
const audioFile = files.audio;
const audioFileRef = ref(storage, `speaking_recordings/${fields.id}.wav`);
const binary = fs.readFileSync((audioFile as any).path);
formData.append('audio_1', binary, 'audio.wav');
fs.rmSync((audioFile as any).path);
const binary = fs.readFileSync((audioFile as any).path).buffer;
await axios.post(
`${process.env.BACKEND_URL}/grade/speaking/2`,
formData,
{
headers: {
...formData.getHeaders(),
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
}
);
const snapshot = await uploadBytes(audioFileRef, binary);
const url = await getDownloadURL(snapshot.ref);
const path = snapshot.metadata.fullPath;
/*const solutions = correspondingStat.solutions.map((x) => ({
...x,
evaluation: backendRequest.data,
solution: url,
}));*/
await axios.post(`${process.env.BACKEND_URL}/grade/speaking/2`, {answer: path, question: fields.question}, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
});
res.status(200).json({ ok: true });
});
}
async function evaluate(body: {answer: string; question: string}, task: number): Promise<AxiosResponse> {
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/grade/speaking/2`, body, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
});
if (backendRequest.status !== 200) return evaluate(body, task);
return backendRequest;
}
export const config = {
api: {
bodyParser: false,

View File

@@ -0,0 +1,34 @@
import type {NextApiRequest, NextApiResponse} from "next";
import client from "@/lib/mongodb";
import {withIronSessionApiRoute} from "iron-session/next";
import {sessionOptions} from "@/lib/session";
const db = client.db(process.env.MONGODB_DB);
export default withIronSessionApiRoute(handler, sessionOptions);
async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "GET") return get(req, res);
}
async function get(req: NextApiRequest, res: NextApiResponse) {
if (!req.session.user) {
res.status(401).json({ok: false});
return;
}
const {sessionId, userId, exerciseIds} = req.query;
const exercises = (exerciseIds! as string).split(',');
const finishedEvaluations = await db.collection("evaluation").find({
session_id: sessionId,
user: userId,
$or: [
{ status: "completed" },
{ status: "error" }
],
exercise_id: { $in: exercises }
}).toArray();
const finishedExerciseIds = finishedEvaluations.map(evaluation => evaluation.exercise_id);
res.status(200).json({ finishedExerciseIds });
}

View File

@@ -5,10 +5,12 @@ import { sessionOptions } from "@/lib/session";
import axios from "axios";
interface Body {
userId: string;
sessionId: string;
question: string;
answer: string;
exerciseId: string;
task: 1 | 2;
id: string;
}
export default withIronSessionApiRoute(handler, sessionOptions);
@@ -20,13 +22,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return;
}
const body = req.body as Body;
const taskNumber = body.task.toString() !== "1" && body.task.toString() !== "2" ? "1" : body.task.toString();
const { task, ...body} = req.body as Body;
const taskNumber = task.toString() !== "1" && task.toString() !== "2" ? "1" : task.toString();
await axios.post(`${process.env.BACKEND_URL}/grade/writing/${taskNumber}`, body, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
},
});
res.status(200);
res.status(200).json({ok: true});
}