From bd9e24970433f6cf044bfc40b6d965aca4a21b5d Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Sat, 28 Dec 2024 03:30:15 +0000 Subject: [PATCH] Unfinished grading attempt at solving it --- src/pages/(exam)/ExamPage.tsx | 7 +- src/pages/api/evaluate/interactiveSpeaking.ts | 194 ++++++++++-------- src/pages/api/evaluate/speaking.ts | 31 +++ src/pages/api/evaluate/status.ts | 33 ++- src/pages/api/evaluate/writing.ts | 37 +++- src/pages/api/stats/updateDisabled.ts | 42 ++++ 6 files changed, 237 insertions(+), 107 deletions(-) create mode 100644 src/pages/api/stats/updateDisabled.ts diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index 9a041a35..e3ce8305 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -22,7 +22,6 @@ import ShortUniqueId from "short-unique-id"; import { ExamProps } from "@/exams/types"; import useExamStore from "@/stores/exam"; import useEvaluationPolling from "@/hooks/useEvaluationPolling"; -import PracticeModal from "@/components/PracticeModal"; interface Props { page: "exams" | "exercises"; @@ -37,6 +36,7 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar = const [avoidRepeated, setAvoidRepeated] = useState(false); const [showAbandonPopup, setShowAbandonPopup] = useState(false); const [pendingExercises, setPendingExercises] = useState([]); + const [shouldPoll, setShouldPoll] = useState(false); const { exam, setExam, @@ -60,7 +60,6 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar = setFlags, setShuffles, evaluated, - setEvaluated, } = useExamStore(); const [isFetchingExams, setIsFetchingExams] = useState(false); @@ -150,8 +149,10 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar = useEffect(() => { if (flags.finalizeExam && moduleIndex !== -1) { setModuleIndex(-1); + + } - }, [flags, moduleIndex, setModuleIndex]); + }, [flags.finalizeExam, moduleIndex, setModuleIndex]); useEffect(() => { if (flags.finalizeExam && !flags.pendingEvaluation && pendingExercises.length === 0) { diff --git a/src/pages/api/evaluate/interactiveSpeaking.ts b/src/pages/api/evaluate/interactiveSpeaking.ts index e217c7e5..4623c483 100644 --- a/src/pages/api/evaluate/interactiveSpeaking.ts +++ b/src/pages/api/evaluate/interactiveSpeaking.ts @@ -6,98 +6,128 @@ import axios from "axios"; import formidable from "formidable-serverless"; import fs from "fs"; import FormData from 'form-data'; +import client from "@/lib/mongodb"; +const db = client.db(process.env.MONGODB_DB); export default withIronSessionApiRoute(handler, sessionOptions); async function handler(req: NextApiRequest, res: NextApiResponse) { if (!req.session.user) { - res.status(401).json({ ok: false }); - return; - } - - const form = formidable({ keepExtensions: true }); - - await form.parse(req, async (err: any, fields: any, files: any) => { - if (err) { - console.error('Error parsing form:', err); - res.status(500).json({ ok: false, error: 'Failed to parse form data' }); + res.status(401).json({ ok: false }); return; - } - - try { - const formData = new FormData(); - - if (!fields.userId || !fields.sessionId || !fields.exerciseId || !fields.task) { - throw new Error('Missing required fields'); + } + + const form = formidable({ keepExtensions: true }); + + await form.parse(req, async (err: any, fields: any, files: any) => { + if (err) { + console.error('Error parsing form:', err); + res.status(500).json({ ok: false, error: 'Failed to parse form data' }); + return; } - - formData.append('userId', fields.userId); - formData.append('sessionId', fields.sessionId); - formData.append('exerciseId', fields.exerciseId); - - for (const fileKey of Object.keys(files)) { - const indexMatch = fileKey.match(/^audio_(\d+)$/); - if (!indexMatch) { - console.warn(`Skipping invalid file key: ${fileKey}`); - continue; - } - - const index = indexMatch[1]; - const questionKey = `question_${index}`; - const audioFile = files[fileKey]; - - if (!audioFile || !audioFile.path) { - throw new Error(`Invalid audio file for ${fileKey}`); - } - - if (!fields[questionKey]) { - throw new Error(`Missing question for audio ${index}`); - } - - try { - const buffer = fs.readFileSync(audioFile.path); - formData.append(`audio_${index}`, buffer, `audio_${index}.wav`); - formData.append(questionKey, fields[questionKey]); - fs.rmSync(audioFile.path); - } catch (fileError) { - console.error(`Error processing file ${fileKey}:`, fileError); - throw new Error(`Failed to process audio file ${index}`); - } - } - - await axios.post( - `${process.env.BACKEND_URL}/grade/speaking/${fields.task}`, - formData, - { - headers: { - ...formData.getHeaders(), - Authorization: `Bearer ${process.env.BACKEND_JWT}`, - }, - } - ); - - res.status(200).json({ ok: true }); - } catch (error) { - console.error('Error processing request:', error); - res.status(500).json({ - ok: false, - error: 'Internal server error' - }); - - Object.keys(files).forEach(fileKey => { - const audioFile = files[fileKey]; - if (audioFile && audioFile.path && fs.existsSync(audioFile.path)) { - try { - fs.rmSync(audioFile.path); - } catch (cleanupError) { - console.error(`Failed to clean up temp file ${audioFile.path}:`, cleanupError); + + try { + const formData = new FormData(); + + if (!fields.userId || !fields.sessionId || !fields.exerciseId || !fields.task) { + throw new Error('Missing required fields'); } - } - }); - } + + formData.append('userId', fields.userId); + formData.append('sessionId', fields.sessionId); + formData.append('exerciseId', fields.exerciseId); + + for (const fileKey of Object.keys(files)) { + const indexMatch = fileKey.match(/^audio_(\d+)$/); + if (!indexMatch) { + console.warn(`Skipping invalid file key: ${fileKey}`); + continue; + } + + const index = indexMatch[1]; + const questionKey = `question_${index}`; + const audioFile = files[fileKey]; + + if (!audioFile || !audioFile.path) { + throw new Error(`Invalid audio file for ${fileKey}`); + } + + if (!fields[questionKey]) { + throw new Error(`Missing question for audio ${index}`); + } + + try { + const buffer = fs.readFileSync(audioFile.path); + formData.append(`audio_${index}`, buffer, `audio_${index}.wav`); + formData.append(questionKey, fields[questionKey]); + fs.rmSync(audioFile.path); + } catch (fileError) { + console.error(`Error processing file ${fileKey}:`, fileError); + throw new Error(`Failed to process audio file ${index}`); + } + } + + // Check if there is one eval for the current exercise + const previousEval = await db.collection("evaluation").findOne({ + user: fields.userId, + session_id: fields.sessionId, + exercise_id: fields.exerciseId, + }) + + // If there is delete it + if (previousEval) { + await db.collection("evaluation").deleteOne({ + user: fields.userId, + session_id: fields.sessionId, + exercise_id: fields.exerciseId, + }) + } + + // Insert the new eval for the backend to place it's result + await db.collection("evaluation").insertOne( + { + user: fields.userId, + session_id: fields.sessionId, + exercise_id: fields.exerciseId, + type: "speaking_interactive", + task: fields.task, + status: "pending" + } + ); + + await axios.post( + `${process.env.BACKEND_URL}/grade/speaking/${fields.task}`, + formData, + { + headers: { + ...formData.getHeaders(), + Authorization: `Bearer ${process.env.BACKEND_JWT}`, + }, + } + ); + + res.status(200).json({ ok: true }); + } catch (error) { + console.error('Error processing request:', error); + res.status(500).json({ + ok: false, + error: 'Internal server error' + }); + + Object.keys(files).forEach(fileKey => { + const audioFile = files[fileKey]; + if (audioFile && audioFile.path && fs.existsSync(audioFile.path)) { + try { + fs.rmSync(audioFile.path); + } catch (cleanupError) { + console.error(`Failed to clean up temp file ${audioFile.path}:`, cleanupError); + } + } + }); + } }); - } +} export const config = { api: { diff --git a/src/pages/api/evaluate/speaking.ts b/src/pages/api/evaluate/speaking.ts index faebf428..89203d2a 100644 --- a/src/pages/api/evaluate/speaking.ts +++ b/src/pages/api/evaluate/speaking.ts @@ -6,6 +6,9 @@ import axios from "axios"; import formidable from "formidable-serverless"; import fs from "fs"; import FormData from 'form-data'; +import client from "@/lib/mongodb"; + +const db = client.db(process.env.MONGODB_DB); export default withIronSessionApiRoute(handler, sessionOptions); @@ -41,6 +44,34 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { formData.append('audio_1', buffer, 'audio_1.wav'); fs.rmSync(audioFile.path); + // Check if there is one eval for the current exercise + const previousEval = await db.collection("evaluation").findOne({ + user: fields.userId, + session_id: fields.sessionId, + exercise_id: fields.exerciseId, + }) + + // If there is delete it + if (previousEval) { + await db.collection("evaluation").deleteOne({ + user: fields.userId, + session_id: fields.sessionId, + exercise_id: fields.exerciseId, + }) + } + + // Insert the new eval for the backend to place it's result + await db.collection("evaluation").insertOne( + { + user: fields.userId, + session_id: fields.sessionId, + exercise_id: fields.exerciseId, + type: "speaking", + task: 2, + status: "pending" + } + ); + await axios.post( `${process.env.BACKEND_URL}/grade/speaking/2`, formData, diff --git a/src/pages/api/evaluate/status.ts b/src/pages/api/evaluate/status.ts index edec09d0..9679a545 100644 --- a/src/pages/api/evaluate/status.ts +++ b/src/pages/api/evaluate/status.ts @@ -1,34 +1,29 @@ -import type {NextApiRequest, NextApiResponse} from "next"; +import type { NextApiRequest, NextApiResponse } from "next"; import client from "@/lib/mongodb"; -import {withIronSessionApiRoute} from "iron-session/next"; -import {sessionOptions} from "@/lib/session"; +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); + 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}); + 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 { sessionId, userId } = req.query; - const finishedExerciseIds = finishedEvaluations.map(evaluation => evaluation.exercise_id); - res.status(200).json({ finishedExerciseIds }); -} \ No newline at end of file + const singleEval = await db.collection("evaluation").findOne({ + session_id: sessionId, + user: userId, + status: "pending", + }); + + res.status(200).json({ hasPendingEvaluation: singleEval !== null}); +} diff --git a/src/pages/api/evaluate/writing.ts b/src/pages/api/evaluate/writing.ts index 8f7b289b..9b4be221 100644 --- a/src/pages/api/evaluate/writing.ts +++ b/src/pages/api/evaluate/writing.ts @@ -3,6 +3,9 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { withIronSessionApiRoute } from "iron-session/next"; import { sessionOptions } from "@/lib/session"; import axios from "axios"; +import client from "@/lib/mongodb"; + +const db = client.db(process.env.MONGODB_DB); interface Body { userId: string; @@ -22,13 +25,41 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { return; } - const { task, ...body} = req.body as Body; - const taskNumber = task.toString() !== "1" && task.toString() !== "2" ? "1" : task.toString(); + const body = req.body as Body; + const taskNumber = body.task.toString() !== "1" && body.task.toString() !== "2" ? "1" : body.task.toString(); + + // Check if there is one eval for the current exercise + const previousEval = await db.collection("evaluation").findOne({ + user: body.userId, + session_id: body.sessionId, + exercise_id: body.exerciseId, + }) + + // If there is delete it + if (previousEval) { + await db.collection("evaluation").deleteOne({ + user: body.userId, + session_id: body.sessionId, + exercise_id: body.exerciseId, + }) + } + + // Insert the new eval for the backend to place it's result + await db.collection("evaluation").insertOne( + { + user: body.userId, + session_id: body.sessionId, + exercise_id: body.exerciseId, + type: "writing", + task: body.task, + status: "pending" + } + ); await axios.post(`${process.env.BACKEND_URL}/grade/writing/${taskNumber}`, body, { headers: { Authorization: `Bearer ${process.env.BACKEND_JWT}`, }, }); - res.status(200).json({ok: true}); + res.status(200).json({ ok: true }); } diff --git a/src/pages/api/stats/updateDisabled.ts b/src/pages/api/stats/updateDisabled.ts new file mode 100644 index 00000000..ba7e688c --- /dev/null +++ b/src/pages/api/stats/updateDisabled.ts @@ -0,0 +1,42 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import client from "@/lib/mongodb"; +import { withIronSessionApiRoute } from "iron-session/next"; +import { sessionOptions } from "@/lib/session"; +import { Stat } from "@/interfaces/user"; +import { requestUser } from "@/utils/api"; +import { UserSolution } from "@/interfaces/exam"; + +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); +} + +interface Body { + solutions: UserSolution[]; + sessionID: string; +} + +async function post(req: NextApiRequest, res: NextApiResponse) { + const user = await requestUser(req, res) + if (!user) return res.status(401).json({ ok: false }); + + const { solutions, sessionID } = req.body as Body; + + const disabledStats = await db.collection("stats").find({ user: user.id, session: sessionID, disabled: true }).toArray(); + + await Promise.all(disabledStats.map(async (stat) => { + const matchingSolution = solutions.find(s => s.exercise === stat.exercise); + if (matchingSolution) { + await db.collection("stats").updateOne( + { id: stat.id }, + { $set: { ...matchingSolution } } + ); + } + })); + + return res.status(200).json({ ok: true }); +}