From 2ed4e6509e263e4c4e5da8fca722f449d699e5fc Mon Sep 17 00:00:00 2001 From: Carlos-Mesquita Date: Tue, 26 Nov 2024 09:04:38 +0000 Subject: [PATCH] Updated the eval calls to the backend, passed the navigation logic of level to useExamNavigation hook --- src/exams/Level/index.tsx | 204 +++++------------- src/exams/Navigation/useExamNavigation.tsx | 43 ++-- src/exams/Speaking.tsx | 9 +- src/exams/Writing.tsx | 2 +- src/pages/(exam)/ExamPage.tsx | 183 ++++++++-------- src/pages/api/evaluate/fetchSolutions.ts | 80 +++++++ src/pages/api/evaluate/interactiveSpeaking.ts | 106 +++------ src/pages/api/evaluate/speaking.ts | 77 +++---- src/pages/api/evaluate/status.ts | 34 +++ src/pages/api/evaluate/writing.ts | 10 +- src/stores/exam/index.ts | 14 +- src/stores/exam/reducers/index.ts | 32 ++- src/stores/exam/types.ts | 4 + src/utils/evaluation.ts | 147 ++++--------- 14 files changed, 452 insertions(+), 493 deletions(-) create mode 100644 src/pages/api/evaluate/fetchSolutions.ts create mode 100644 src/pages/api/evaluate/status.ts diff --git a/src/exams/Level/index.tsx b/src/exams/Level/index.tsx index e110276c..9b2b661f 100644 --- a/src/exams/Level/index.tsx +++ b/src/exams/Level/index.tsx @@ -19,18 +19,18 @@ import { typeCheckWordsMC } from "@/utils/type.check"; import SectionNavbar from "../Navigation/SectionNavbar"; import AudioPlayer from "@/components/Low/AudioPlayer"; import { ExamProps } from "../types"; -import {answeredEveryQuestionInPart} from "../utils/answeredEveryQuestion"; +import { answeredEveryQuestionInPart } from "../utils/answeredEveryQuestion"; import useExamTimer from "@/hooks/useExamTimer"; import ProgressButtons from "../components/ProgressButtons"; +import useExamNavigation from "../Navigation/useExamNavigation"; +import { calculateExerciseIndex } from "../utils/calculateExerciseIndex"; const Level: React.FC> = ({ exam, showSolutions = false, preview = false }) => { - const levelBgColor = "bg-ielts-level-light"; - const updateTimers = useExamTimer(exam.module, preview || showSolutions); const userSolutionRef = useRef<(() => UserSolution) | null>(null); const [solutionWasUpdated, setSolutionWasUpdated] = useState(false); - + const examState = useExamStore((state) => state); const persistentExamState = usePersistentExamStore((state) => state); @@ -55,6 +55,7 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr const { finalizeModule, timeIsUp } = flags; const timer = useRef(exam.minTimer - timeSpentCurrentModule / 60); + const [isFirstTimeRender, setIsFirstTimeRender] = useState(partIndex === 0 && exerciseIndex == 0 && !showSolutions); // In case client want to switch back const textRenderDisabled = true; @@ -66,7 +67,6 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr const [textRender, setTextRender] = useState(false); const [changedPrompt, setChangedPrompt] = useState(false); - const [seenParts, setSeenParts] = useState>(new Set(showSolutions ? exam.parts.map((_, index) => index) : [0])); const [questionModalKwargs, setQuestionModalKwargs] = useState<{ type?: "module" | "blankQuestions" | "submit"; unanswered?: boolean | undefined; onClose: (next?: boolean) => void | undefined; @@ -74,25 +74,39 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr type: "blankQuestions", onClose: function (x: boolean | undefined) { if (x) { setShowQuestionsModal(false); nextExercise(); } else { setShowQuestionsModal(false) } } }); - const [showPartDivider, setShowPartDivider] = useState(typeof exam.parts[0].intro === "string" && !showSolutions); - const [startNow, setStartNow] = useState(!showSolutions); + const modalKwargs = () => { + const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = { + type: "blankQuestions", + unanswered: false, + onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } } + }; - useEffect(() => { - if (!showSolutions && exam.parts[partIndex]?.intro !== undefined && exam.parts[partIndex]?.intro !== "" && !seenParts.has(partIndex)) { - setShowPartDivider(true); - setBgColor(levelBgColor); + if (partIndex === exam.parts.length - 1) { + kwargs.type = "submit" + kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestionInPart(exam, partIndex, userSolutions)); + kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [exerciseIndex]); + setQuestionModalKwargs(kwargs); + } + + const { + nextExercise, previousExercise, + showPartDivider, setShowPartDivider, + seenParts, setSeenParts, + } = useExamNavigation( + { + exam, module: "level", showBlankModal: showQuestionsModal, + setShowBlankModal: setShowQuestionsModal, showSolutions, + preview, disableBetweenParts: true, modalKwargs + }); + const registerSolution = useCallback((updateSolution: () => UserSolution) => { userSolutionRef.current = updateSolution; setSolutionWasUpdated(true); }, []); - const scrollToTop = () => Array.from(document.getElementsByTagName("body")).forEach((body) => body.scrollTo(0, 0)); - const [contextWords, setContextWords] = useState<{ match: string, originalLine: string }[] | undefined>(undefined); const [contextWordLines, setContextWordLines] = useState(undefined); const [totalLines, setTotalLines] = useState(0); @@ -139,104 +153,6 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr // eslint-disable-next-line react-hooks/exhaustive-deps }, [finalizeModule, timeIsUp]) - const nextExercise = () => { - scrollToTop(); - - if (exerciseIndex + 1 < exam.parts[partIndex].exercises.length) { - setExerciseIndex(exerciseIndex + 1); - return; - } - - if (partIndex + 1 === exam.parts.length && !showQuestionsModal && !showSolutions && !continueAnyways) { - modalKwargs(); - setShowQuestionsModal(true); - return; - } - - if (partIndex + 1 < exam.parts.length) { - if (!answeredEveryQuestionInPart(exam, partIndex, userSolutions) && !continueAnyways && !showSolutions && !seenParts.has(partIndex + 1)) { - modalKwargs(); - setShowQuestionsModal(true); - return; - } - - if (!showSolutions && exam.parts[0].intro && !seenParts.has(partIndex + 1)) { - setShowPartDivider(true); - setBgColor(levelBgColor); - } - - if (partIndex < exam.parts.length - 1 && exam.parts[partIndex + 1].context && !textRenderDisabled) { - setTextRender(true); - } - - setTimesListened(0); - setPartIndex(partIndex + 1); - setExerciseIndex(0); - setQuestionIndex(0); - return; - } - - if (partIndex + 1 === exam.parts.length && exerciseIndex === exam.parts[partIndex].exercises.length - 1 && !continueAnyways && !showSolutions) { - modalKwargs(); - setShowQuestionsModal(true); - } - - if (!showSolutions) { - dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: true } }) - } else { - dispatch({ type: "FINALIZE_MODULE_SOLUTIONS"}) - } - } - - const previousExercise = () => { - scrollToTop(); - - if (exam.parts[partIndex].context && questionIndex === 0 && !textRender && !textRenderDisabled) { - setTextRender(true); - return; - } - - if (questionIndex == 0) { - setPartIndex(partIndex - 1); - if (!seenParts.has(partIndex - 1)) { - setBgColor(levelBgColor); - setShowPartDivider(true); - setQuestionIndex(0); - return; - } - - const lastExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; - const lastExercise = exam.parts[partIndex - 1].exercises[lastExerciseIndex]; - setExerciseIndex(lastExerciseIndex); - - if (lastExercise.type === "multipleChoice") { - setQuestionIndex(lastExercise.questions.length - 1) - } else { - setQuestionIndex(0) - } - return; - } - - setExerciseIndex(exerciseIndex - 1); - if (exerciseIndex - 1 === -1) { - setPartIndex(partIndex - 1); - const lastPartExerciseIndex = exam.parts[partIndex - 1].exercises.length - 1; - const previousExercise = exam.parts[partIndex - 1].exercises[lastPartExerciseIndex]; - if (previousExercise.type === "multipleChoice") { - setQuestionIndex(previousExercise.questions.length - 1) - } - } - - }; - - const calculateExerciseIndex = () => { - return exam.parts.reduce((acc, curr, index) => { - if (index < partIndex) { - return acc + countExercises(curr.exercises) - } - return acc; - }, 0) + (questionIndex + 1); - }; const renderAudioPlayer = () => (
@@ -393,26 +309,11 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr useEffect(() => { if (continueAnyways) { setContinueAnyways(false); - nextExercise(); + nextExercise(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [continueAnyways]); - const modalKwargs = () => { - const kwargs: { type: "module" | "blankQuestions" | "submit", unanswered: boolean, onClose: (next?: boolean) => void; } = { - type: "blankQuestions", - unanswered: false, - onClose: function (x: boolean | undefined) { if (x) { setContinueAnyways(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } } - }; - - if (partIndex === exam.parts.length - 1) { - kwargs.type = "submit" - kwargs.unanswered = !exam.parts.every((_, partIndex) => answeredEveryQuestionInPart(exam, partIndex, userSolutions)); - kwargs.onClose = function (x: boolean | undefined) { if (x) { setShowSubmissionModal(true); setShowQuestionsModal(false); } else { setShowQuestionsModal(false) } }; - } - setQuestionModalKwargs(kwargs); - } - const mcNavKwargs = { userSolutions: userSolutions, exam: exam, @@ -423,27 +324,10 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr runOnClick: setQuestionIndex } - const progressButtons = ; - - const memoizedRender = useMemo(() => { - setChangedPrompt(false); - return ( - <> - {textRender && !textRenderDisabled ? - renderText() : - <> - {exam.parts[partIndex]?.context && renderText()} - {exam.parts[partIndex]?.audio && renderAudioPlayer()} - {(showSolutions) ? - currentExercise && renderSolution(currentExercise, progressButtons, progressButtons) : - currentExercise && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons) - } - - } - - ) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [textRender, currentExercise, changedPrompt]); + const progressButtons = useMemo(() => + // Do not remove the ()=> in handle next + nextExercise()} /> + , [nextExercise, previousExercise]); return ( <> @@ -469,17 +353,17 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr { - !(partIndex === 0 && questionIndex === 0 && (showPartDivider || startNow)) && + !(partIndex === 0 && questionIndex === 0 && (showPartDivider || isFirstTimeRender)) && } - {(showPartDivider || startNow) ? + {(showPartDivider || isFirstTimeRender) ? { setShowPartDivider(false); setStartNow(false); setBgColor("bg-white"); setSeenParts(prev => new Set(prev).add(partIndex)); }} + onNext={() => { setShowPartDivider(false); setIsFirstTimeRender(false); setBgColor("bg-white"); setSeenParts(prev => new Set(prev).add(partIndex)); }} /> : ( <> > = ({ exam, showSolutions = false, pr examLabel={exam.label} partLabel={partLabel()} minTimer={timer.current} - exerciseIndex={calculateExerciseIndex()} + exerciseIndex={calculateExerciseIndex(exam, partIndex, exerciseIndex, questionIndex)} module="level" totalExercises={countExercises(exam.parts.flatMap((x) => x.exercises))} disableTimer={showSolutions} @@ -507,7 +391,17 @@ const Level: React.FC> = ({ exam, showSolutions = false, pr "mb-20 w-full", !!exam.parts[partIndex].context && !textRender && "grid grid-cols-2 gap-4", )}> - {memoizedRender} + {textRender && !textRenderDisabled ? + renderText() : + <> + {exam.parts[partIndex]?.context && renderText()} + {exam.parts[partIndex]?.audio && renderAudioPlayer()} + {(showSolutions) ? + currentExercise && renderSolution(currentExercise, progressButtons, progressButtons) : + currentExercise && renderExercise(currentExercise, exam.id, registerSolution, preview, progressButtons, progressButtons) + } + + }
)} diff --git a/src/exams/Navigation/useExamNavigation.tsx b/src/exams/Navigation/useExamNavigation.tsx index e8c65ed5..31f6892c 100644 --- a/src/exams/Navigation/useExamNavigation.tsx +++ b/src/exams/Navigation/useExamNavigation.tsx @@ -11,6 +11,7 @@ const MC_PER_PAGE = 2; type UseExamNavigation = (props: { exam: ModuleExam; module: Module; + modalKwargs?: () => void; showBlankModal?: boolean; setShowBlankModal?: React.Dispatch>; showSolutions: boolean; @@ -31,14 +32,15 @@ const useExamNavigation: UseExamNavigation = ({ exam, module, setShowBlankModal, + modalKwargs, showSolutions, preview, disableBetweenParts = false, }) => { const examState = useExamStore((state) => state); - const persistentExamState = usePersistentExamStore((state) => state); - + const persistentExamState = usePersistentExamStore((state) => state); + const { exerciseIndex, setExerciseIndex, partIndex, setPartIndex, @@ -101,11 +103,13 @@ const useExamNavigation: UseExamNavigation = ({ return; } - if (currentExercise.type === "multipleChoice" && questionIndex < currentExercise.questions.length - 1) { - setQuestionIndex(questionIndex + MC_PER_PAGE); - return; + if (currentExercise.type === "multipleChoice") { + const nextQuestionIndex = questionIndex + MC_PER_PAGE; + if (nextQuestionIndex < currentExercise.questions!.length) { + setQuestionIndex(nextQuestionIndex); + return; + } } - if (!reachedFinalExercise) { setExerciseIndex(exerciseIndex + 1); setQuestionIndex(0); @@ -121,6 +125,7 @@ const useExamNavigation: UseExamNavigation = ({ } if (!answeredEveryQuestion(exam as PartExam, userSolutions) && !keepGoing && setShowBlankModal && !showSolutions && !preview) { + if (modalKwargs) modalKwargs() setShowBlankModal(true); return; } @@ -134,20 +139,17 @@ const useExamNavigation: UseExamNavigation = ({ if (!showSolutions) { dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: true } }); } else { - dispatch({ type: "FINALIZE_MODULE_SOLUTIONS"}); + dispatch({ type: "FINALIZE_MODULE_SOLUTIONS" }); } } const previousPartExam = () => { - if (partIndex !== 0) { - setPartIndex(partIndex - 1); - setExerciseIndex((exam as PartExam).parts[partIndex].exercises.length - 1); - if (isBetweenParts) setIsBetweenParts(false); + const currentExercise = (exam as PartExam).parts[partIndex].exercises[exerciseIndex]; + if (currentExercise.type === "multipleChoice" && questionIndex > 0) { + setQuestionIndex(Math.max(0, questionIndex - MC_PER_PAGE)); return; } - setQuestionIndex(0); - if (exerciseIndex === 0 && !disableBetweenParts) { setIsBetweenParts(true); return; @@ -155,7 +157,16 @@ const useExamNavigation: UseExamNavigation = ({ if (exerciseIndex !== 0) { setExerciseIndex(exerciseIndex - 1); + setQuestionIndex(0); } + + if (partIndex !== 0) { + setPartIndex(partIndex - 1); + setExerciseIndex((exam as PartExam).parts[partIndex].exercises.length - 1); + if (isBetweenParts) setIsBetweenParts(false); + return; + } + }; const nextExerciseOnlyExam = () => { @@ -166,7 +177,7 @@ const useExamNavigation: UseExamNavigation = ({ if (currentExercise.type === "interactiveSpeaking" && questionIndex < currentExercise.prompts.length - 1) { setQuestionIndex(questionIndex + 1) return; - } + } if (!reachedFinalExercise) { setQuestionIndex(0); @@ -183,7 +194,7 @@ const useExamNavigation: UseExamNavigation = ({ if (!showSolutions) { dispatch({ type: "FINALIZE_MODULE", payload: { updateTimers: true } }); } else { - dispatch({ type: "FINALIZE_MODULE_SOLUTIONS"}); + dispatch({ type: "FINALIZE_MODULE_SOLUTIONS" }); } } @@ -194,7 +205,7 @@ const useExamNavigation: UseExamNavigation = ({ if (currentExercise.type === "interactiveSpeaking" && questionIndex !== 0) { setQuestionIndex(questionIndex - 1); return; - } + } if (exerciseIndex !== 0) { setExerciseIndex(exerciseIndex - 1); diff --git a/src/exams/Speaking.tsx b/src/exams/Speaking.tsx index 5e87e018..bcd2be29 100644 --- a/src/exams/Speaking.tsx +++ b/src/exams/Speaking.tsx @@ -15,7 +15,7 @@ import { calculateExerciseIndexSpeaking } from "./utils/calculateExerciseIndex"; const Speaking: React.FC> = ({ exam, showSolutions = false, preview = false }) => { - const updateTimers = useExamTimer(exam.module, preview); + const updateTimers = useExamTimer(exam.module, preview || showSolutions); const userSolutionRef = useRef<(() => UserSolution) | null>(null); const [solutionWasUpdated, setSolutionWasUpdated] = useState(false); @@ -90,11 +90,8 @@ const Speaking: React.FC> = ({ exam, showSolutions = fal setSeenParts((prev) => new Set(prev).add(exerciseIndex)); } - const memoizedExerciseIndex = useMemo(() => { - const bruh = calculateExerciseIndexSpeaking(exam, exerciseIndex, questionIndex) - console.log(bruh); - return bruh; - } + const memoizedExerciseIndex = useMemo(() => + calculateExerciseIndexSpeaking(exam, exerciseIndex, questionIndex) // eslint-disable-next-line react-hooks/exhaustive-deps , [exerciseIndex, questionIndex] ); diff --git a/src/exams/Writing.tsx b/src/exams/Writing.tsx index b4dc588a..75bba4cd 100644 --- a/src/exams/Writing.tsx +++ b/src/exams/Writing.tsx @@ -13,7 +13,7 @@ import SectionNavbar from "./Navigation/SectionNavbar"; import ProgressButtons from "./components/ProgressButtons"; const Writing: React.FC> = ({ exam, showSolutions = false, preview = false }) => { - const updateTimers = useExamTimer(exam.module, preview); + const updateTimers = useExamTimer(exam.module, preview || showSolutions); const userSolutionRef = useRef<(() => UserSolution) | null>(null); const [solutionWasUpdated, setSolutionWasUpdated] = useState(false); diff --git a/src/pages/(exam)/ExamPage.tsx b/src/pages/(exam)/ExamPage.tsx index 26a0e60f..0cb6dbc7 100644 --- a/src/pages/(exam)/ExamPage.tsx +++ b/src/pages/(exam)/ExamPage.tsx @@ -34,10 +34,8 @@ export default function ExamPage({ page, user, destination = "/", hideSidebar = const router = useRouter(); const [variant, setVariant] = useState("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([]); + const [pendingExercises, setPendingExercises] = useState([]); 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) && [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) +} diff --git a/src/pages/api/evaluate/interactiveSpeaking.ts b/src/pages/api/evaluate/interactiveSpeaking.ts index 960dacfb..6b76a78d 100644 --- a/src/pages/api/evaluate/interactiveSpeaking.ts +++ b/src/pages/api/evaluate/interactiveSpeaking.ts @@ -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 { - console.log(`🌱 - Try number ${index} - ${id}`); - const correspondingStat = await db.collection("stats").findOne({ 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 { - 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: { diff --git a/src/pages/api/evaluate/speaking.ts b/src/pages/api/evaluate/speaking.ts index 93b0fa34..ca303ed9 100644 --- a/src/pages/api/evaluate/speaking.ts +++ b/src/pages/api/evaluate/speaking.ts @@ -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 { - 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, diff --git a/src/pages/api/evaluate/status.ts b/src/pages/api/evaluate/status.ts new file mode 100644 index 00000000..edec09d0 --- /dev/null +++ b/src/pages/api/evaluate/status.ts @@ -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 }); +} \ No newline at end of file diff --git a/src/pages/api/evaluate/writing.ts b/src/pages/api/evaluate/writing.ts index d255e1b6..8f7b289b 100644 --- a/src/pages/api/evaluate/writing.ts +++ b/src/pages/api/evaluate/writing.ts @@ -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}); } diff --git a/src/stores/exam/index.ts b/src/stores/exam/index.ts index e39d165e..24292a89 100644 --- a/src/stores/exam/index.ts +++ b/src/stores/exam/index.ts @@ -26,7 +26,7 @@ export const initialState: ExamState = { inactivity: 0, shuffles: [], bgColor: "bg-white", - currentSolution: undefined, + evaluated: [], user: undefined, navigation: { previousDisabled: false, @@ -39,6 +39,7 @@ export const initialState: ExamState = { reviewAll: false, finalizeModule: false, finalizeExam: false, + pendingEvaluation: false, }, }; @@ -61,6 +62,8 @@ const useExamStore = create((set, get) => ({ setQuestionIndex: (questionIndex: number) => set(() => ({ questionIndex })), setBgColor: (bgColor: string) => set(() => ({ bgColor })), + setEvaluated: (evaluated: UserSolution[]) => set(() => ({ evaluated })), + setNavigation: (updates: Partial) => set((state) => ({ navigation: { ...state.navigation, @@ -75,7 +78,6 @@ const useExamStore = create((set, get) => ({ } })), - setTimeIsUp: (timeIsUp: boolean) => set((state) => ({ flags: { ...state.flags, timeIsUp } })), reset: () => set(() => initialState), @@ -162,12 +164,12 @@ export const usePersistentExamStore = create()( setTimeIsUp: (timeIsUp: boolean) => set((state) => ({ flags: { ...state.flags, timeIsUp } })), - saveStats: async () => {}, - saveSession: async () => {}, - + saveStats: async () => { }, + saveSession: async () => { }, + setEvaluated: (evaluated: UserSolution[]) => {}, reset: () => set(() => initialState), dispatch: (action) => set((state) => rootReducer(state, action)) - + })), { name: 'persistent-exam-store', diff --git a/src/stores/exam/reducers/index.ts b/src/stores/exam/reducers/index.ts index 6705db94..ac368610 100644 --- a/src/stores/exam/reducers/index.ts +++ b/src/stores/exam/reducers/index.ts @@ -11,9 +11,10 @@ import { convertToUserSolutions } from "@/utils/stats"; export type RootActions = { type: 'INIT_EXAM'; payload: { exams: Exam[], modules: Module[], assignment?: Assignment } } | { type: 'INIT_SOLUTIONS'; payload: { exams: Exam[], modules: Module[], stats: Stat[], timeSpent?: number, inactivity?: number } } | - { type: 'UPDATE_TIMERS'; payload: { timeSpent: number; inactivity: number; timeSpentCurrentModule: number;} } | + { type: 'UPDATE_TIMERS'; payload: { timeSpent: number; inactivity: number; timeSpentCurrentModule: number; } } | { type: 'FINALIZE_MODULE'; payload: { updateTimers: boolean } } | - { type: 'FINALIZE_MODULE_SOLUTIONS' } + { type: 'FINALIZE_MODULE_SOLUTIONS' } | + { type: 'UPDATE_EXAMS'} export type Action = RootActions | SessionActions; @@ -84,19 +85,28 @@ export const rootReducer = ( case 'UPDATE_TIMERS': { // Just assigning the timers at once instead of two different calls const { timeSpent, inactivity, timeSpentCurrentModule } = action.payload; - return { + return { timeSpentCurrentModule, - timeSpent, - inactivity + timeSpent, + inactivity } }; case 'FINALIZE_MODULE': { const { updateTimers } = action.payload; + const solutions = state.userSolutions; + const evaluated = state.evaluated; + + const hasUnevaluatedSolutions = solutions.some(solution => + (solution.type === 'speaking' || + solution.type === 'writing' || + solution.type === 'interactiveSpeaking') && + !evaluated.some(evaluation => evaluation.exercise === solution.exercise) + ); // To finalize a module first flag the timers to be updated if (updateTimers) { return { - flags: { ...state.flags, finalizeModule: true } + flags: { ...state.flags, finalizeModule: true, pendingEvaluation: hasUnevaluatedSolutions } } } else { // then check whether there are more modules in the exam, if there are @@ -119,10 +129,12 @@ export const rootReducer = ( // and the Finish view is set there, no need to // dispatch another init return { + showSolutions: true, flags: { ...state.flags, finalizeModule: false, finalizeExam: true, + pendingEvaluation: hasUnevaluatedSolutions, } } } @@ -155,6 +167,14 @@ export const rootReducer = ( } } } + case 'UPDATE_EXAMS': { + const exams = state.exams.map((e) => updateExamWithUserSolutions(e, state.userSolutions)); + const exam = updateExamWithUserSolutions(state.exam!, state.userSolutions); + return { + exams, + exam + } + } default: return {}; } diff --git a/src/stores/exam/types.ts b/src/stores/exam/types.ts index 9bb8a93e..eadf8b0d 100644 --- a/src/stores/exam/types.ts +++ b/src/stores/exam/types.ts @@ -16,6 +16,7 @@ export interface StateFlags { reviewAll: boolean; finalizeModule: boolean; finalizeExam: boolean; + pendingEvaluation: boolean; } export interface ExamState { @@ -39,6 +40,7 @@ export interface ExamState { currentSolution?: UserSolution | undefined; navigation: Navigation; flags: StateFlags, + evaluated: UserSolution[]; } @@ -63,6 +65,8 @@ export interface ExamFunctions { setTimeIsUp: (timeIsUp: boolean) => void; + setEvaluated: (evaluated: UserSolution[]) => void, + saveSession: () => Promise; saveStats: () => Promise; diff --git a/src/utils/evaluation.ts b/src/utils/evaluation.ts index ca746887..43e4efa2 100644 --- a/src/utils/evaluation.ts +++ b/src/utils/evaluation.ts @@ -1,165 +1,112 @@ import { - Evaluation, - Exam, InteractiveSpeakingExercise, - SpeakingExam, SpeakingExercise, UserSolution, - WritingExam, WritingExercise, } from "@/interfaces/exam"; import axios from "axios"; -import {speakingReverseMarking, writingReverseMarking} from "./score"; export const evaluateWritingAnswer = async ( + userId: string, + sessionId: string, exercise: WritingExercise, task: number, solution: UserSolution, - id: string, -): Promise => { - const response = await axios.post("/api/evaluate/writing", { +): Promise => { + await axios.post("/api/evaluate/writing", { question: `${exercise.prompt} ${exercise.attachment ? exercise.attachment.description : ""}`.replaceAll("\n", ""), answer: solution.solutions[0].solution.trim().replaceAll("\n", " "), task, - id, + userId, + sessionId, + exerciseId: exercise.id }); - - if (response.status === 200) { - return { - ...solution, - id, - score: { - correct: response.data ? writingReverseMarking[response.data.overall] : 0, - missing: 0, - total: 100, - }, - solutions: [{id: exercise.id, solution: solution.solutions[0].solution, evaluation: response.data}], - isDisabled: true, - }; - } - - return undefined; }; export const evaluateSpeakingAnswer = async ( + userId: string, + sessionId: string, exercise: SpeakingExercise | InteractiveSpeakingExercise, solution: UserSolution, - id: string, task: number, -): Promise => { +): Promise => { switch (exercise?.type) { case "speaking": - return {...(await evaluateSpeakingExercise(exercise, exercise.id, solution, id, task)), id} as UserSolution; + await evaluateSpeakingExercise(userId, sessionId, exercise, solution); case "interactiveSpeaking": - return {...(await evaluateInteractiveSpeakingExercise(exercise.id, solution, id, task === 3 ? "final" : "initial")), id} as UserSolution; - default: - return undefined; + await evaluateInteractiveSpeakingExercise(userId, sessionId, exercise.id, solution, task); } }; export const downloadBlob = async (url: string): Promise => { - const blobResponse = await axios.get(url, {responseType: "arraybuffer"}); + const blobResponse = await axios.get(url, { responseType: "arraybuffer" }); return Buffer.from(blobResponse.data, "binary"); }; const evaluateSpeakingExercise = async ( + userId: string, + sessionId: string, exercise: SpeakingExercise, - exerciseId: string, solution: UserSolution, - id: string, - task: number, -): Promise => { +): Promise => { const formData = new FormData(); const url = solution.solutions[0].solution.trim() as string; const audioBlob = await downloadBlob(url); - const audioFile = new File([audioBlob], "audio.wav", {type: "audio/wav"}); + const audioFile = new File([audioBlob], "audio.wav", { type: "audio/wav" }); - if (url && !url.startsWith("blob")) await axios.post("/api/storage/delete", {path: url}); + if (url && !url.startsWith("blob")) { + await axios.post("/api/storage/delete", { path: url }); + } - formData.append("audio", audioFile, "audio.wav"); + formData.append("userId", userId); + formData.append("sessionId", sessionId); + formData.append("exerciseId", exercise.id); const evaluationQuestion = `${exercise.text.replaceAll("\n", "")}` + (exercise.prompts.length > 0 ? `You should talk about: ${exercise.prompts.join(", ")}` : ""); - formData.append("question", evaluationQuestion); - formData.append("id", id); - formData.append("task", task.toString()); + formData.append("question_1", evaluationQuestion); + formData.append("audio_1", audioFile, "audio.wav"); const config = { headers: { - "Content-Type": "audio/wav", + "Content-Type": "multipart/form-data", }, }; - const response = await axios.post("/api/evaluate/speaking", formData, config); - - if (response.status === 200) { - return { - ...solution, - id, - score: { - correct: 0, - missing: 0, - total: 100, - }, - solutions: [{id: exerciseId, solution: response.data ? response.data.fullPath : null, evaluation: response.data}], - isDisabled: true, - }; - } - - return undefined; + await axios.post(`/api/evaluate/speaking`, formData, config); }; const evaluateInteractiveSpeakingExercise = async ( + userId: string, + sessionId: string, exerciseId: string, solution: UserSolution, - id: string, - variant?: "initial" | "final", -): Promise => { - const promiseParts = solution.solutions.map(async (x: {prompt: string; blob: string}) => { - const blob = await downloadBlob(x.blob); - if (!x.blob.startsWith("blob")) await axios.post("/api/storage/delete", {path: x.blob}); - - return { - question: x.prompt, - answer: blob, - }; - }); - const body = await Promise.all(promiseParts); - + task: number, +): Promise => { const formData = new FormData(); - body.forEach(({question, answer}) => { - const seed = Math.random().toString().replace("0.", ""); + formData.append("userId", userId); + formData.append("sessionId", sessionId); + formData.append("exerciseId", exerciseId); + formData.append("task", task.toString()); - const audioFile = new File([answer], `${seed}.wav`, {type: "audio/wav"}); + const promiseParts = solution.solutions.map(async (x: { prompt: string; blob: string }, index: number) => { + const audioBlob = await downloadBlob(x.blob); + if (!x.blob.startsWith("blob")) { + await axios.post("/api/storage/delete", { path: x.blob }); + } + const audioFile = new File([audioBlob], "audio.wav", { type: "audio/wav" }); - formData.append(`question_${seed}`, question); - formData.append(`answer_${seed}`, audioFile, `${seed}.wav`); + formData.append(`question_${index + 1}`, x.prompt); + formData.append(`audio_${index + 1}`, audioFile, "audio.wav"); }); - formData.append("id", id); - formData.append("variant", variant || "final"); + + await Promise.all(promiseParts); const config = { headers: { - "Content-Type": "audio/mp3", + "Content-Type": "multipart/form-data", }, }; - const response = await axios.post("/api/evaluate/interactiveSpeaking", formData, config); - - if (response.status === 200) { - return { - ...solution, - id, - score: { - correct: 0, - missing: 0, - total: 100, - }, - module: "speaking", - solutions: [{id: exerciseId, solution: response.data ? response.data.answer : null, evaluation: response.data}], - isDisabled: true, - }; - } - - return undefined; + await axios.post(`/api/evaluate/interactiveSpeaking`, formData, config); };