Merge branch 'develop' of https://bitbucket.org/ecropdev/ielts-ui into ENCOA-316-ENCOA-317
This commit is contained in:
@@ -1,95 +1,114 @@
|
|||||||
import { UserSolution } from '@/interfaces/exam';
|
import { UserSolution } from "@/interfaces/exam";
|
||||||
import useExamStore from '@/stores/exam';
|
import useExamStore from "@/stores/exam";
|
||||||
import { StateFlags } from '@/stores/exam/types';
|
import axios from "axios";
|
||||||
import axios from 'axios';
|
import { useEffect, useRef } from "react";
|
||||||
import { SetStateAction, useEffect, useRef } from 'react';
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
type UseEvaluationPolling = (props: {
|
const useEvaluationPolling = (sessionIds: string[], mode: "exam" | "records", userId: string) => {
|
||||||
pendingExercises: string[],
|
const { setUserSolutions, userSolutions } = useExamStore();
|
||||||
setPendingExercises: React.Dispatch<SetStateAction<string[]>>,
|
const pollingTimeoutsRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
||||||
}) => void;
|
const router = useRouter();
|
||||||
|
|
||||||
const useEvaluationPolling: UseEvaluationPolling = ({
|
const poll = async (sessionId: string) => {
|
||||||
pendingExercises,
|
try {
|
||||||
setPendingExercises,
|
const { data: statusData } = await axios.get('/api/evaluate/status', {
|
||||||
}) => {
|
params: { op: 'pending', userId, sessionId }
|
||||||
const {
|
});
|
||||||
flags, sessionId, user,
|
|
||||||
userSolutions, evaluated,
|
|
||||||
setEvaluated, setFlags
|
|
||||||
} = useExamStore();
|
|
||||||
|
|
||||||
const pollingTimeoutRef = useRef<NodeJS.Timeout>();
|
if (!statusData.hasPendingEvaluation) {
|
||||||
|
|
||||||
|
let solutionsOrStats = userSolutions;
|
||||||
|
|
||||||
|
if (mode === "records") {
|
||||||
|
const res = await axios.get(`/api/stats/session/${sessionId}`)
|
||||||
|
solutionsOrStats = res.data;
|
||||||
|
}
|
||||||
|
const { data: completedSolutions } = await axios.post('/api/evaluate/fetchSolutions?op=session', {
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
stats: solutionsOrStats,
|
||||||
|
});
|
||||||
|
|
||||||
|
await axios.post('/api/stats/disabled', {
|
||||||
|
sessionId,
|
||||||
|
userId,
|
||||||
|
solutions: completedSolutions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeout = pollingTimeoutsRef.current.get(sessionId);
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
|
pollingTimeoutsRef.current.delete(sessionId);
|
||||||
|
|
||||||
|
if (mode === "exam") {
|
||||||
|
const updatedSolutions = userSolutions.map(solution => {
|
||||||
|
const completed = completedSolutions.find(
|
||||||
|
(c: UserSolution) => c.exercise === solution.exercise
|
||||||
|
);
|
||||||
|
return completed || solution;
|
||||||
|
});
|
||||||
|
|
||||||
|
setUserSolutions(updatedSolutions);
|
||||||
|
} else {
|
||||||
|
router.reload();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pollingTimeoutsRef.current.has(sessionId)) {
|
||||||
|
clearTimeout(pollingTimeoutsRef.current.get(sessionId));
|
||||||
|
}
|
||||||
|
pollingTimeoutsRef.current.set(
|
||||||
|
sessionId,
|
||||||
|
setTimeout(() => poll(sessionId), 5000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (pollingTimeoutsRef.current.has(sessionId)) {
|
||||||
|
clearTimeout(pollingTimeoutsRef.current.get(sessionId));
|
||||||
|
}
|
||||||
|
pollingTimeoutsRef.current.set(
|
||||||
|
sessionId,
|
||||||
|
setTimeout(() => poll(sessionId), 5000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
if (mode === "exam") {
|
||||||
if (pollingTimeoutRef.current) {
|
const hasDisabledSolutions = userSolutions.some(s => s.isDisabled);
|
||||||
clearTimeout(pollingTimeoutRef.current);
|
|
||||||
|
if (hasDisabledSolutions && sessionIds.length > 0) {
|
||||||
|
poll(sessionIds[0]);
|
||||||
|
} else {
|
||||||
|
pollingTimeoutsRef.current.forEach((timeout) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
pollingTimeoutsRef.current.clear();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [mode, sessionIds, userSolutions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === "records" && sessionIds.length > 0) {
|
||||||
|
sessionIds.forEach(sessionId => {
|
||||||
|
poll(sessionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [mode, sessionIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeouts = pollingTimeoutsRef.current;
|
||||||
|
return () => {
|
||||||
|
timeouts.forEach((timeout) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
});
|
||||||
|
timeouts.clear();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
return {
|
||||||
if (!flags.pendingEvaluation || pendingExercises.length === 0) {
|
isPolling: pollingTimeoutsRef.current.size > 0
|
||||||
|
|
||||||
if (pollingTimeoutRef.current) {
|
|
||||||
clearTimeout(pollingTimeoutRef.current);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pollStatus = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await axios.get('/api/evaluate/status', {
|
|
||||||
params: {
|
|
||||||
sessionId,
|
|
||||||
userId: user,
|
|
||||||
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,
|
|
||||||
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) {
|
|
||||||
pollingTimeoutRef.current = setTimeout(pollStatus, 5000);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Evaluation polling error:', error);
|
|
||||||
pollingTimeoutRef.current = setTimeout(pollStatus, 5000);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pollStatus();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (pollingTimeoutRef.current) {
|
|
||||||
clearTimeout(pollingTimeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useEvaluationPolling;
|
export default useEvaluationPolling;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function ExamPage({
|
|||||||
const [variant, setVariant] = useState<Variant>("full");
|
const [variant, setVariant] = useState<Variant>("full");
|
||||||
const [avoidRepeated, setAvoidRepeated] = useState(false);
|
const [avoidRepeated, setAvoidRepeated] = useState(false);
|
||||||
const [showAbandonPopup, setShowAbandonPopup] = useState(false);
|
const [showAbandonPopup, setShowAbandonPopup] = useState(false);
|
||||||
const [pendingExercises, setPendingExercises] = useState<string[]>([]);
|
const [moduleLock, setModuleLock] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
exam,
|
exam,
|
||||||
@@ -74,7 +74,6 @@ export default function ExamPage({
|
|||||||
saveSession,
|
saveSession,
|
||||||
setFlags,
|
setFlags,
|
||||||
setShuffles,
|
setShuffles,
|
||||||
evaluated,
|
|
||||||
} = useExamStore();
|
} = useExamStore();
|
||||||
|
|
||||||
const [isFetchingExams, setIsFetchingExams] = useState(false);
|
const [isFetchingExams, setIsFetchingExams] = useState(false);
|
||||||
@@ -139,24 +138,25 @@ export default function ExamPage({
|
|||||||
setShowAbandonPopup(false);
|
setShowAbandonPopup(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEvaluationPolling(sessionId ? [sessionId] : [], "exam", user?.id);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flags.finalizeModule && !showSolutions && flags.pendingEvaluation) {
|
setModuleLock(true);
|
||||||
|
}, [flags.finalizeModule]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (flags.finalizeModule && !showSolutions) {
|
||||||
if (
|
if (
|
||||||
exam &&
|
exam &&
|
||||||
(exam.module === "writing" || exam.module === "speaking") &&
|
(exam.module === "writing" || exam.module === "speaking") &&
|
||||||
userSolutions.length > 0 &&
|
userSolutions.length > 0
|
||||||
!showSolutions
|
|
||||||
) {
|
) {
|
||||||
const exercisesToEvaluate = exam.exercises.map(
|
|
||||||
(exercise) => exercise.id
|
|
||||||
);
|
|
||||||
|
|
||||||
setPendingExercises(exercisesToEvaluate);
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await Promise.all(
|
try {
|
||||||
|
const results = await Promise.all(
|
||||||
exam.exercises.map(async (exercise, index) => {
|
exam.exercises.map(async (exercise, index) => {
|
||||||
if (exercise.type === "writing")
|
if (exercise.type === "writing") {
|
||||||
await evaluateWritingAnswer(
|
const sol = await evaluateWritingAnswer(
|
||||||
user.id,
|
user.id,
|
||||||
sessionId,
|
sessionId,
|
||||||
exercise,
|
exercise,
|
||||||
@@ -164,71 +164,81 @@ export default function ExamPage({
|
|||||||
userSolutions.find((x) => x.exercise === exercise.id)!,
|
userSolutions.find((x) => x.exercise === exercise.id)!,
|
||||||
exercise.attachment?.url
|
exercise.attachment?.url
|
||||||
);
|
);
|
||||||
|
return sol;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
exercise.type === "interactiveSpeaking" ||
|
exercise.type === "interactiveSpeaking" ||
|
||||||
exercise.type === "speaking"
|
exercise.type === "speaking"
|
||||||
) {
|
) {
|
||||||
await evaluateSpeakingAnswer(
|
const sol = await evaluateSpeakingAnswer(
|
||||||
user.id,
|
user.id,
|
||||||
sessionId,
|
sessionId,
|
||||||
exercise,
|
exercise,
|
||||||
userSolutions.find((x) => x.exercise === exercise.id)!,
|
userSolutions.find((x) => x.exercise === exercise.id)!,
|
||||||
index + 1
|
index + 1
|
||||||
);
|
);
|
||||||
|
return sol;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
const updatedSolutions = userSolutions.map((solution) => {
|
||||||
|
const completed = results
|
||||||
|
.filter((r) => r !== null)
|
||||||
|
.find((c: any) => c.exercise === solution.exercise);
|
||||||
|
return completed || solution;
|
||||||
|
});
|
||||||
|
setUserSolutions(updatedSolutions);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during module evaluation:", error);
|
||||||
|
} finally {
|
||||||
|
setModuleLock(false);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
} else {
|
||||||
|
setModuleLock(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [exam, showSolutions, userSolutions, sessionId, user?.id, flags]);
|
}, [
|
||||||
|
exam,
|
||||||
useEvaluationPolling({ pendingExercises, setPendingExercises });
|
showSolutions,
|
||||||
|
userSolutions,
|
||||||
|
sessionId,
|
||||||
|
user.id,
|
||||||
|
flags.finalizeModule,
|
||||||
|
setUserSolutions,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flags.finalizeExam && moduleIndex !== -1) {
|
if (flags.finalizeExam && moduleIndex !== -1 && !moduleLock) {
|
||||||
|
(async () => {
|
||||||
setModuleIndex(-1);
|
setModuleIndex(-1);
|
||||||
|
await saveStats();
|
||||||
|
await axios.get("/api/stats/update");
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}, [flags.finalizeExam, moduleIndex, setModuleIndex]);
|
}, [
|
||||||
|
flags.finalizeExam,
|
||||||
|
moduleIndex,
|
||||||
|
saveStats,
|
||||||
|
setModuleIndex,
|
||||||
|
userSolutions,
|
||||||
|
moduleLock,
|
||||||
|
flags.finalizeModule,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
flags.finalizeExam &&
|
flags.finalizeExam &&
|
||||||
!flags.pendingEvaluation &&
|
!userSolutions.some((s) => s.isDisabled) &&
|
||||||
pendingExercises.length === 0
|
!moduleLock
|
||||||
) {
|
) {
|
||||||
(async () => {
|
|
||||||
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();
|
|
||||||
await axios.get("/api/stats/update");
|
|
||||||
setShowSolutions(true);
|
setShowSolutions(true);
|
||||||
setFlags({ finalizeExam: false });
|
setFlags({ finalizeExam: false });
|
||||||
dispatch({ type: "UPDATE_EXAMS" });
|
dispatch({ type: "UPDATE_EXAMS" });
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [
|
}, [flags.finalizeExam, userSolutions, showSolutions, moduleLock]);
|
||||||
saveStats,
|
|
||||||
setFlags,
|
|
||||||
setModuleIndex,
|
|
||||||
evaluated,
|
|
||||||
pendingExercises,
|
|
||||||
setUserSolutions,
|
|
||||||
flags,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const aggregateScoresByModule = (
|
const aggregateScoresByModule = (
|
||||||
isPractice?: boolean
|
isPractice?: boolean
|
||||||
@@ -269,7 +279,7 @@ export default function ExamPage({
|
|||||||
};
|
};
|
||||||
|
|
||||||
userSolutions.forEach((x) => {
|
userSolutions.forEach((x) => {
|
||||||
if ((isPractice && x.isPractice) || (!isPractice && !x.isPractice)) {
|
if (x.isPractice === isPractice) {
|
||||||
const examModule =
|
const examModule =
|
||||||
x.module ||
|
x.module ||
|
||||||
(x.type === "writing"
|
(x.type === "writing"
|
||||||
@@ -286,12 +296,13 @@ export default function ExamPage({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.keys(scores).reduce((acc, x) => {
|
return Object.keys(scores).reduce<
|
||||||
if (scores[x as Module].total > 0) {
|
{ module: Module; total: number; missing: number; correct: number }[]
|
||||||
acc.push({ module: x as Module, ...scores[x as Module] });
|
>((accm, x) => {
|
||||||
}
|
if (scores[x as Module].total > 0)
|
||||||
return acc;
|
accm.push({ module: x as Module, ...scores[x as Module] });
|
||||||
}, [] as any[]);
|
return accm;
|
||||||
|
}, []);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ModuleExamMap: Record<Module, React.ComponentType<ExamProps<Exam>>> = {
|
const ModuleExamMap: Record<Module, React.ComponentType<ExamProps<Exam>>> = {
|
||||||
@@ -318,8 +329,7 @@ export default function ExamPage({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setOnFocusLayerMouseEnter(() => () => setShowAbandonPopup(true));
|
setOnFocusLayerMouseEnter(() => () => setShowAbandonPopup(true));
|
||||||
}, [
|
}, []);
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setBgColor(bgColor);
|
setBgColor(bgColor);
|
||||||
@@ -339,6 +349,7 @@ export default function ExamPage({
|
|||||||
setHideSidebar,
|
setHideSidebar,
|
||||||
showSolutions,
|
showSolutions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
@@ -375,7 +386,7 @@ export default function ExamPage({
|
|||||||
)}
|
)}
|
||||||
{moduleIndex === -1 && selectedModules.length !== 0 && (
|
{moduleIndex === -1 && selectedModules.length !== 0 && (
|
||||||
<Finish
|
<Finish
|
||||||
isLoading={flags.pendingEvaluation}
|
isLoading={userSolutions.some((s) => s.isDisabled)}
|
||||||
user={user!}
|
user={user!}
|
||||||
modules={selectedModules}
|
modules={selectedModules}
|
||||||
solutions={userSolutions}
|
solutions={userSolutions}
|
||||||
|
|||||||
@@ -4,41 +4,31 @@ import { withIronSessionApiRoute } from "iron-session/next";
|
|||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { UserSolution } from "@/interfaces/exam";
|
import { UserSolution } from "@/interfaces/exam";
|
||||||
import { speakingReverseMarking, writingReverseMarking } from "@/utils/score";
|
import { speakingReverseMarking, writingReverseMarking } from "@/utils/score";
|
||||||
|
import { Stat } from "@/interfaces/user";
|
||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
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) {
|
if (!req.session.user) {
|
||||||
res.status(401).json({ ok: false });
|
res.status(401).json({ ok: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { sessionId, userId, userSolutions } = req.body;
|
try {
|
||||||
const completedEvals = await db.collection("evaluation").find({
|
return await getSessionEvals(req, res);
|
||||||
session_id: sessionId,
|
} catch (error) {
|
||||||
user: userId,
|
console.error(error);
|
||||||
status: "completed"
|
res.status(500).json({ ok: false });
|
||||||
}).toArray();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const evalsByExercise = new Map(
|
function formatSolutionWithEval(userSolution: UserSolution | Stat, evaluation: any) {
|
||||||
completedEvals.map(e => [e.exercise_id, e])
|
if (userSolution.type === 'writing') {
|
||||||
);
|
|
||||||
|
|
||||||
const solutionsWithEvals = userSolutions.filter((solution: UserSolution) =>
|
|
||||||
evalsByExercise.has(solution.exercise)
|
|
||||||
).map((solution: any) => {
|
|
||||||
const evaluation = evalsByExercise.get(solution.exercise)!;
|
|
||||||
|
|
||||||
if (solution.type === 'writing') {
|
|
||||||
return {
|
return {
|
||||||
...solution,
|
...userSolution,
|
||||||
solutions: [{
|
solutions: [{
|
||||||
...solution.solutions[0],
|
...userSolution.solutions[0],
|
||||||
evaluation: evaluation.result
|
evaluation: evaluation.result
|
||||||
}],
|
}],
|
||||||
score: {
|
score: {
|
||||||
@@ -50,13 +40,13 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (solution.type === 'speaking' || solution.type === 'interactiveSpeaking') {
|
if (userSolution.type === 'speaking' || userSolution.type === 'interactiveSpeaking') {
|
||||||
return {
|
return {
|
||||||
...solution,
|
...userSolution,
|
||||||
solutions: [{
|
solutions: [{
|
||||||
...solution.solutions[0],
|
...userSolution.solutions[0],
|
||||||
...(
|
...(
|
||||||
solution.type === 'speaking'
|
userSolution.type === 'speaking'
|
||||||
? { fullPath: evaluation.result.fullPath }
|
? { fullPath: evaluation.result.fullPath }
|
||||||
: { answer: evaluation.result.answer }
|
: { answer: evaluation.result.answer }
|
||||||
),
|
),
|
||||||
@@ -70,11 +60,30 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
isDisabled: false
|
isDisabled: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
solution,
|
solution: userSolution,
|
||||||
evaluation
|
evaluation
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
res.status(200).json(solutionsWithEvals)
|
async function getSessionEvals(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { sessionId, userId, stats } = 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 statsWithEvals = stats
|
||||||
|
.filter((solution: UserSolution | Stat) => evalsByExercise.has(solution.exercise))
|
||||||
|
.map((solution: UserSolution | Stat) =>
|
||||||
|
formatSolutionWithEval(solution, evalsByExercise.get(solution.exercise)!)
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json(statsWithEvals);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,19 +11,100 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (req.method === "GET") return get(req, res);
|
if (req.method === "GET") return get(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Query = {
|
||||||
|
op: string;
|
||||||
|
sessionId: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
async function get(req: NextApiRequest, res: NextApiResponse) {
|
async function get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!req.session.user) {
|
if (!req.session.user) {
|
||||||
res.status(401).json({ ok: false });
|
return res.status(401).json({ ok: false });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sessionId, userId } = req.query;
|
const { sessionId, userId, op } = req.query as Query;
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case 'pending':
|
||||||
|
return getPendingEvaluation(userId, sessionId, res);
|
||||||
|
case 'disabled':
|
||||||
|
return getSessionsWIthDisabledWithPending(userId, res);
|
||||||
|
default:
|
||||||
|
return res.status(400).json({
|
||||||
|
ok: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPendingEvaluation(
|
||||||
|
userId: string,
|
||||||
|
sessionId: string,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
const singleEval = await db.collection("evaluation").findOne({
|
const singleEval = await db.collection("evaluation").findOne({
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
user: userId,
|
user: userId,
|
||||||
status: "pending",
|
status: "pending",
|
||||||
});
|
});
|
||||||
|
return res.status(200).json({ hasPendingEvaluation: singleEval !== null });
|
||||||
res.status(200).json({ hasPendingEvaluation: singleEval !== null});
|
}
|
||||||
|
|
||||||
|
async function getSessionsWIthDisabledWithPending(
|
||||||
|
userId: string,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
const sessions = await db.collection("stats")
|
||||||
|
.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
user: userId,
|
||||||
|
disabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 0,
|
||||||
|
session: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: "evaluation",
|
||||||
|
let: { sessionId: "$session" },
|
||||||
|
pipeline: [
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
$expr: {
|
||||||
|
$and: [
|
||||||
|
{ $eq: ["$session", "$$sessionId"] },
|
||||||
|
{ $eq: ["$user", userId] },
|
||||||
|
{ $eq: ["$status", "pending"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
as: "pendingEvals"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
"pendingEvals.0": { $exists: true }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
id: "$session"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]).toArray();
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
sessions: sessions.map(s => s.id)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,37 +3,41 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import client from "@/lib/mongodb";
|
import client from "@/lib/mongodb";
|
||||||
import { withIronSessionApiRoute } from "iron-session/next";
|
import { withIronSessionApiRoute } from "iron-session/next";
|
||||||
import { sessionOptions } from "@/lib/session";
|
import { sessionOptions } from "@/lib/session";
|
||||||
import { Stat } from "@/interfaces/user";
|
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { UserSolution } from "@/interfaces/exam";
|
import { UserSolution } from "@/interfaces/exam";
|
||||||
|
import { WithId } from "mongodb";
|
||||||
|
|
||||||
const db = client.db(process.env.MONGODB_DB);
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
export default withIronSessionApiRoute(handler, sessionOptions);
|
export default withIronSessionApiRoute(handler, sessionOptions);
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
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)
|
const user = await requestUser(req, res)
|
||||||
if (!user) return res.status(401).json({ ok: false });
|
if (!user) return res.status(401).json({ ok: false });
|
||||||
|
|
||||||
const { solutions, sessionID } = req.body as Body;
|
if (req.method === "POST") return post(req, res);
|
||||||
|
}
|
||||||
|
|
||||||
const disabledStats = await db.collection("stats").find({ user: user.id, session: sessionID, disabled: true }).toArray();
|
|
||||||
|
interface Body {
|
||||||
|
solutions: UserSolution[];
|
||||||
|
sessionId: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { userId, solutions, sessionId } = req.body as Body;
|
||||||
|
const disabledStats = await db.collection("stats").find(
|
||||||
|
{ user: userId, session: sessionId, isDisabled: true }
|
||||||
|
).toArray();
|
||||||
|
|
||||||
await Promise.all(disabledStats.map(async (stat) => {
|
await Promise.all(disabledStats.map(async (stat) => {
|
||||||
const matchingSolution = solutions.find(s => s.exercise === stat.exercise);
|
const matchingSolution = solutions.find(s => s.exercise === stat.exercise);
|
||||||
if (matchingSolution) {
|
if (matchingSolution) {
|
||||||
|
const { _id, ...updateFields } = matchingSolution as WithId<UserSolution>;
|
||||||
await db.collection("stats").updateOne(
|
await db.collection("stats").updateOne(
|
||||||
{ id: stat.id },
|
{ id: stat.id },
|
||||||
{ $set: { ...matchingSolution } }
|
{ $set: { ...updateFields } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
21
src/pages/api/stats/session/[session].ts
Normal file
21
src/pages/api/stats/session/[session].ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// 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";
|
||||||
|
|
||||||
|
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 {session} = req.query;
|
||||||
|
const snapshot = await db.collection("stats").find({ user: req.session.user.id, session }).toArray();
|
||||||
|
|
||||||
|
res.status(200).json(snapshot);
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ import { EntityWithRoles } from "@/interfaces/entity";
|
|||||||
import CardList from "@/components/High/CardList";
|
import CardList from "@/components/High/CardList";
|
||||||
import { requestUser } from "@/utils/api";
|
import { requestUser } from "@/utils/api";
|
||||||
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
import { useAllowedEntities } from "@/hooks/useEntityPermissions";
|
||||||
|
import getPendingEvals from "@/utils/disabled.be";
|
||||||
|
import useEvaluationPolling from "@/hooks/useEvaluationPolling";
|
||||||
|
|
||||||
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
||||||
const user = await requestUser(req, res)
|
const user = await requestUser(req, res)
|
||||||
@@ -44,9 +46,10 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => {
|
|||||||
const users = await (isAdmin ? getUsers() : getEntitiesUsers(entitiesIds))
|
const users = await (isAdmin ? getUsers() : getEntitiesUsers(entitiesIds))
|
||||||
const assignments = await (isAdmin ? getAssignments() : getEntitiesAssignments(entitiesIds))
|
const assignments = await (isAdmin ? getAssignments() : getEntitiesAssignments(entitiesIds))
|
||||||
const gradingSystems = await getGradingSystemByEntities(entitiesIds)
|
const gradingSystems = await getGradingSystemByEntities(entitiesIds)
|
||||||
|
const pendingSessionIds = await getPendingEvals(user.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: serialize({ user, users, assignments, entities, gradingSystems,isAdmin }),
|
props: serialize({ user, users, assignments, entities, gradingSystems, isAdmin, pendingSessionIds }),
|
||||||
};
|
};
|
||||||
}, sessionOptions);
|
}, sessionOptions);
|
||||||
|
|
||||||
@@ -58,12 +61,13 @@ interface Props {
|
|||||||
assignments: Assignment[];
|
assignments: Assignment[];
|
||||||
entities: EntityWithRoles[]
|
entities: EntityWithRoles[]
|
||||||
gradingSystems: Grading[]
|
gradingSystems: Grading[]
|
||||||
|
pendingSessionIds: string[];
|
||||||
isAdmin:boolean
|
isAdmin:boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TRAINING_EXAMS = 10;
|
const MAX_TRAINING_EXAMS = 10;
|
||||||
|
|
||||||
export default function History({ user, users, assignments, entities, gradingSystems,isAdmin }: Props) {
|
export default function History({ user, users, assignments, entities, gradingSystems, isAdmin, pendingSessionIds }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
const [statsUserId, setStatsUserId, training, setTraining] = useRecordStore((state) => [
|
||||||
state.selectedUser,
|
state.selectedUser,
|
||||||
@@ -85,9 +89,11 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
const groupedStats = useMemo(() => groupByDate(
|
const groupedStats = useMemo(() => groupByDate(
|
||||||
stats.filter((x) => {
|
stats.filter((x) => {
|
||||||
if (
|
if (
|
||||||
(x.module === "writing" || x.module === "speaking") &&
|
(
|
||||||
!x.isDisabled &&
|
x.module === "writing" || x.module === "speaking") &&
|
||||||
!x.solutions.every((y) => Object.keys(y).includes("evaluation"))
|
!x.isDisabled && Array.isArray(x.solutions) &&
|
||||||
|
!x.solutions.every((y) => Object.keys(y).includes("evaluation")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return false;
|
return false;
|
||||||
return true;
|
return true;
|
||||||
@@ -179,6 +185,8 @@ export default function History({ user, users, assignments, entities, gradingSys
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEvaluationPolling(pendingSessionIds ? pendingSessionIds : [], "records", user.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const initialState: ExamState = {
|
|||||||
inactivity: 0,
|
inactivity: 0,
|
||||||
shuffles: [],
|
shuffles: [],
|
||||||
bgColor: "bg-white",
|
bgColor: "bg-white",
|
||||||
evaluated: [],
|
|
||||||
user: undefined,
|
user: undefined,
|
||||||
navigation: {
|
navigation: {
|
||||||
previousDisabled: false,
|
previousDisabled: false,
|
||||||
@@ -39,7 +38,6 @@ export const initialState: ExamState = {
|
|||||||
reviewAll: false,
|
reviewAll: false,
|
||||||
finalizeModule: false,
|
finalizeModule: false,
|
||||||
finalizeExam: false,
|
finalizeExam: false,
|
||||||
pendingEvaluation: false,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,8 +60,6 @@ const useExamStore = create<ExamState & ExamFunctions>((set, get) => ({
|
|||||||
setQuestionIndex: (questionIndex: number) => set(() => ({ questionIndex })),
|
setQuestionIndex: (questionIndex: number) => set(() => ({ questionIndex })),
|
||||||
setBgColor: (bgColor: string) => set(() => ({ bgColor })),
|
setBgColor: (bgColor: string) => set(() => ({ bgColor })),
|
||||||
|
|
||||||
setEvaluated: (evaluated: UserSolution[]) => set(() => ({ evaluated })),
|
|
||||||
|
|
||||||
setNavigation: (updates: Partial<Navigation>) => set((state) => ({
|
setNavigation: (updates: Partial<Navigation>) => set((state) => ({
|
||||||
navigation: {
|
navigation: {
|
||||||
...state.navigation,
|
...state.navigation,
|
||||||
@@ -166,7 +162,7 @@ export const usePersistentExamStore = create<ExamState & ExamFunctions>()(
|
|||||||
|
|
||||||
saveStats: async () => { },
|
saveStats: async () => { },
|
||||||
saveSession: async () => { },
|
saveSession: async () => { },
|
||||||
setEvaluated: (evaluated: UserSolution[]) => {},
|
setEvalSolutions: (evaluated: UserSolution[]) => {},
|
||||||
reset: () => set(() => initialState),
|
reset: () => set(() => initialState),
|
||||||
dispatch: (action) => set((state) => rootReducer(state, action))
|
dispatch: (action) => set((state) => rootReducer(state, action))
|
||||||
|
|
||||||
|
|||||||
@@ -93,20 +93,11 @@ export const rootReducer = (
|
|||||||
};
|
};
|
||||||
case 'FINALIZE_MODULE': {
|
case 'FINALIZE_MODULE': {
|
||||||
const { updateTimers } = action.payload;
|
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
|
// To finalize a module first flag the timers to be updated
|
||||||
if (updateTimers) {
|
if (updateTimers) {
|
||||||
return {
|
return {
|
||||||
flags: { ...state.flags, finalizeModule: true, pendingEvaluation: hasUnevaluatedSolutions }
|
flags: { ...state.flags, finalizeModule: true }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// then check whether there are more modules in the exam, if there are
|
// then check whether there are more modules in the exam, if there are
|
||||||
@@ -118,7 +109,6 @@ export const rootReducer = (
|
|||||||
...state.flags,
|
...state.flags,
|
||||||
finalizeModule: false,
|
finalizeModule: false,
|
||||||
finalizeExam: true,
|
finalizeExam: true,
|
||||||
pendingEvaluation: hasUnevaluatedSolutions,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (state.moduleIndex < state.selectedModules.length - 1) {
|
} else if (state.moduleIndex < state.selectedModules.length - 1) {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export interface StateFlags {
|
|||||||
reviewAll: boolean;
|
reviewAll: boolean;
|
||||||
finalizeModule: boolean;
|
finalizeModule: boolean;
|
||||||
finalizeExam: boolean;
|
finalizeExam: boolean;
|
||||||
pendingEvaluation: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExamState {
|
export interface ExamState {
|
||||||
@@ -39,8 +38,7 @@ export interface ExamState {
|
|||||||
user: undefined | string;
|
user: undefined | string;
|
||||||
currentSolution?: UserSolution | undefined;
|
currentSolution?: UserSolution | undefined;
|
||||||
navigation: Navigation;
|
navigation: Navigation;
|
||||||
flags: StateFlags,
|
flags: StateFlags;
|
||||||
evaluated: UserSolution[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -65,8 +63,6 @@ export interface ExamFunctions {
|
|||||||
|
|
||||||
setTimeIsUp: (timeIsUp: boolean) => void;
|
setTimeIsUp: (timeIsUp: boolean) => void;
|
||||||
|
|
||||||
setEvaluated: (evaluated: UserSolution[]) => void,
|
|
||||||
|
|
||||||
saveSession: () => Promise<void>;
|
saveSession: () => Promise<void>;
|
||||||
|
|
||||||
saveStats: () => Promise<void>;
|
saveStats: () => Promise<void>;
|
||||||
|
|||||||
27
src/utils/disabled.be.ts
Normal file
27
src/utils/disabled.be.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import client from "@/lib/mongodb";
|
||||||
|
|
||||||
|
const db = client.db(process.env.MONGODB_DB);
|
||||||
|
|
||||||
|
async function getPendingEvals(userId: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const disabledStatsSessions = await db.collection("stats")
|
||||||
|
.distinct("session", {
|
||||||
|
"isDisabled": true,
|
||||||
|
"user": userId
|
||||||
|
});
|
||||||
|
|
||||||
|
const sessionsWithEvals = await db.collection("evaluation")
|
||||||
|
.distinct("session_id", {
|
||||||
|
"session_id": { $in: disabledStatsSessions },
|
||||||
|
"user": userId
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return sessionsWithEvals;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching session IDs:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getPendingEvals;
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
WritingExercise,
|
WritingExercise,
|
||||||
} from "@/interfaces/exam";
|
} from "@/interfaces/exam";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { v4 } from "uuid";
|
||||||
|
|
||||||
export const evaluateWritingAnswer = async (
|
export const evaluateWritingAnswer = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -13,7 +14,7 @@ export const evaluateWritingAnswer = async (
|
|||||||
task: number,
|
task: number,
|
||||||
solution: UserSolution,
|
solution: UserSolution,
|
||||||
attachment?: string,
|
attachment?: string,
|
||||||
): Promise<void> => {
|
): Promise<UserSolution> => {
|
||||||
await axios.post("/api/evaluate/writing", {
|
await axios.post("/api/evaluate/writing", {
|
||||||
question: `${exercise.prompt}`.replaceAll("\n", ""),
|
question: `${exercise.prompt}`.replaceAll("\n", ""),
|
||||||
answer: solution.solutions[0].solution.trim().replaceAll("\n", " "),
|
answer: solution.solutions[0].solution.trim().replaceAll("\n", " "),
|
||||||
@@ -23,6 +24,18 @@ export const evaluateWritingAnswer = async (
|
|||||||
exerciseId: exercise.id,
|
exerciseId: exercise.id,
|
||||||
attachment,
|
attachment,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...solution,
|
||||||
|
id: v4(),
|
||||||
|
score: {
|
||||||
|
correct: 0,
|
||||||
|
missing: 0,
|
||||||
|
total: 100,
|
||||||
|
},
|
||||||
|
solutions: [{id: exercise.id, solution: solution.solutions[0].solution}],
|
||||||
|
isDisabled: true,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const evaluateSpeakingAnswer = async (
|
export const evaluateSpeakingAnswer = async (
|
||||||
@@ -31,12 +44,12 @@ export const evaluateSpeakingAnswer = async (
|
|||||||
exercise: SpeakingExercise | InteractiveSpeakingExercise,
|
exercise: SpeakingExercise | InteractiveSpeakingExercise,
|
||||||
solution: UserSolution,
|
solution: UserSolution,
|
||||||
task: number,
|
task: number,
|
||||||
): Promise<void> => {
|
): Promise<UserSolution> => {
|
||||||
switch (exercise?.type) {
|
switch (exercise?.type) {
|
||||||
case "speaking":
|
case "speaking":
|
||||||
await evaluateSpeakingExercise(userId, sessionId, exercise, solution);
|
return await evaluateSpeakingExercise(userId, sessionId, exercise, solution);
|
||||||
case "interactiveSpeaking":
|
case "interactiveSpeaking":
|
||||||
await evaluateInteractiveSpeakingExercise(userId, sessionId, exercise.id, solution, task);
|
return await evaluateInteractiveSpeakingExercise(userId, sessionId, exercise.id, solution, task);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,7 +63,7 @@ const evaluateSpeakingExercise = async (
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
exercise: SpeakingExercise,
|
exercise: SpeakingExercise,
|
||||||
solution: UserSolution,
|
solution: UserSolution,
|
||||||
): Promise<void> => {
|
): Promise<UserSolution> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
const url = solution.solutions[0].solution.trim() as string;
|
const url = solution.solutions[0].solution.trim() as string;
|
||||||
@@ -76,6 +89,17 @@ const evaluateSpeakingExercise = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
await axios.post(`/api/evaluate/speaking`, formData, config);
|
await axios.post(`/api/evaluate/speaking`, formData, config);
|
||||||
|
return {
|
||||||
|
...solution,
|
||||||
|
id: v4(),
|
||||||
|
score: {
|
||||||
|
correct: 0,
|
||||||
|
missing: 0,
|
||||||
|
total: 100,
|
||||||
|
},
|
||||||
|
solutions: [{id: exercise.id, solution: null}],
|
||||||
|
isDisabled: true,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const evaluateInteractiveSpeakingExercise = async (
|
const evaluateInteractiveSpeakingExercise = async (
|
||||||
@@ -84,7 +108,7 @@ const evaluateInteractiveSpeakingExercise = async (
|
|||||||
exerciseId: string,
|
exerciseId: string,
|
||||||
solution: UserSolution,
|
solution: UserSolution,
|
||||||
task: number,
|
task: number,
|
||||||
): Promise<void> => {
|
): Promise<UserSolution> => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("userId", userId);
|
formData.append("userId", userId);
|
||||||
formData.append("sessionId", sessionId);
|
formData.append("sessionId", sessionId);
|
||||||
@@ -111,4 +135,15 @@ const evaluateInteractiveSpeakingExercise = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
await axios.post(`/api/evaluate/interactiveSpeaking`, formData, config);
|
await axios.post(`/api/evaluate/interactiveSpeaking`, formData, config);
|
||||||
|
return {
|
||||||
|
...solution,
|
||||||
|
id: v4(),
|
||||||
|
score: {
|
||||||
|
correct: 0,
|
||||||
|
missing: 0,
|
||||||
|
total: 100,
|
||||||
|
},
|
||||||
|
solutions: [{id: exerciseId, solution: null}],
|
||||||
|
isDisabled: true,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user