Updated the InteractiveSpeaking to also work with the session persistence
This commit is contained in:
@@ -5,6 +5,8 @@ import {BsCheckCircleFill, BsMicFill, BsPauseCircle, BsPlayCircle, BsTrashFill}
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Button from "../Low/Button";
|
import Button from "../Low/Button";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
|
import {downloadBlob} from "@/utils/evaluation";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
const Waveform = dynamic(() => import("../Waveform"), {ssr: false});
|
||||||
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
|
const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mod) => mod.ReactMediaRecorder), {
|
||||||
@@ -14,9 +16,11 @@ const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mo
|
|||||||
export default function InteractiveSpeaking({
|
export default function InteractiveSpeaking({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
examID,
|
||||||
text,
|
text,
|
||||||
type,
|
type,
|
||||||
prompts,
|
prompts,
|
||||||
|
userSolutions,
|
||||||
updateIndex,
|
updateIndex,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
@@ -24,21 +28,109 @@ export default function InteractiveSpeaking({
|
|||||||
const [recordingDuration, setRecordingDuration] = useState(0);
|
const [recordingDuration, setRecordingDuration] = useState(0);
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [mediaBlob, setMediaBlob] = useState<string>();
|
const [mediaBlob, setMediaBlob] = useState<string>();
|
||||||
const [answers, setAnswers] = useState<{prompt: string; blob: string}[]>([]);
|
const [answers, setAnswers] = useState<{prompt: string; blob: string; questionIndex: number}[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
|
const {questionIndex, setQuestionIndex} = useExamStore((state) => state);
|
||||||
|
const {userSolutions: storeUserSolutions, setUserSolutions} = useExamStore((state) => state);
|
||||||
|
|
||||||
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
const hasExamEnded = useExamStore((state) => state.hasExamEnded);
|
||||||
|
|
||||||
|
const saveToStorage = async (previousURL?: string) => {
|
||||||
|
if (mediaBlob && mediaBlob.startsWith("blob")) {
|
||||||
|
const blobBuffer = await downloadBlob(mediaBlob);
|
||||||
|
const audioFile = new File([blobBuffer], "audio.wav", {type: "audio/wav"});
|
||||||
|
|
||||||
|
const seed = Math.random().toString().replace("0.", "");
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("audio", audioFile, `${seed}.wav`);
|
||||||
|
formData.append("root", "speaking_recordings");
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "audio/wav",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.post<{path: string}>("/api/storage/insert", formData, config);
|
||||||
|
if (previousURL && !previousURL.startsWith("blob")) await axios.post("/api/storage/delete", {path: previousURL});
|
||||||
|
return response.data.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const back = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const answer = await saveAnswer(questionIndex);
|
||||||
|
if (questionIndex - 1 >= 0) {
|
||||||
|
setQuestionIndex(questionIndex - 1);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
onBack({
|
||||||
|
exercise: id,
|
||||||
|
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
||||||
|
score: {correct: 1, total: 1, missing: 0},
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const answer = await saveAnswer(questionIndex);
|
||||||
|
if (questionIndex + 1 < prompts.length) {
|
||||||
|
setQuestionIndex(questionIndex + 1);
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
onNext({
|
||||||
|
exercise: id,
|
||||||
|
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
||||||
|
score: {correct: 1, total: 1, missing: 0},
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userSolutions.length > 0 && answers.length === 0) {
|
||||||
|
console.log(userSolutions);
|
||||||
|
const solutions = userSolutions as unknown as typeof answers;
|
||||||
|
setAnswers(solutions);
|
||||||
|
|
||||||
|
if (!mediaBlob) setMediaBlob(solutions.find((x) => x.questionIndex === questionIndex)?.blob);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [userSolutions, mediaBlob, answers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log({answers});
|
||||||
|
}, [answers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (updateIndex) updateIndex(questionIndex);
|
if (updateIndex) updateIndex(questionIndex);
|
||||||
}, [questionIndex, updateIndex]);
|
}, [questionIndex, updateIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasExamEnded) {
|
if (hasExamEnded) {
|
||||||
|
const answer = {
|
||||||
|
questionIndex,
|
||||||
|
prompt: prompts[questionIndex].text,
|
||||||
|
blob: mediaBlob!,
|
||||||
|
};
|
||||||
|
|
||||||
onNext({
|
onNext({
|
||||||
exercise: id,
|
exercise: id,
|
||||||
solutions: mediaBlob ? [{id, solution: mediaBlob}] : [],
|
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
||||||
score: {correct: 1, total: 1, missing: 0},
|
score: {correct: 1, total: 1, missing: 0},
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
@@ -60,19 +152,38 @@ export default function InteractiveSpeaking({
|
|||||||
}, [isRecording]);
|
}, [isRecording]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (questionIndex === answers.length - 1) {
|
if (questionIndex <= answers.length - 1) {
|
||||||
setMediaBlob(answers[questionIndex].blob);
|
const blob = answers.find((x) => x.questionIndex === questionIndex)?.blob;
|
||||||
|
setMediaBlob(blob);
|
||||||
}
|
}
|
||||||
}, [answers, questionIndex]);
|
}, [answers, questionIndex]);
|
||||||
|
|
||||||
const saveAnswer = () => {
|
const saveAnswer = async (index: number) => {
|
||||||
|
const previousURL = answers.find((x) => x.questionIndex === questionIndex)?.blob;
|
||||||
|
const audioPath = await saveToStorage(previousURL);
|
||||||
|
|
||||||
const answer = {
|
const answer = {
|
||||||
|
questionIndex,
|
||||||
prompt: prompts[questionIndex].text,
|
prompt: prompts[questionIndex].text,
|
||||||
blob: mediaBlob!,
|
blob: audioPath ? audioPath : mediaBlob!,
|
||||||
};
|
};
|
||||||
|
|
||||||
setAnswers((prev) => [...prev, answer]);
|
setAnswers((prev) => [...prev.filter((x) => x.questionIndex !== index), answer]);
|
||||||
setMediaBlob(undefined);
|
setMediaBlob(undefined);
|
||||||
|
|
||||||
|
setUserSolutions([
|
||||||
|
...storeUserSolutions.filter((x) => x.exercise !== id),
|
||||||
|
{
|
||||||
|
exercise: id,
|
||||||
|
solutions: [...answers.filter((x) => x.questionIndex !== questionIndex), answer],
|
||||||
|
score: {correct: 1, total: 1, missing: 0},
|
||||||
|
module: "speaking",
|
||||||
|
exam: examID,
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return answer;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -98,7 +209,7 @@ export default function InteractiveSpeaking({
|
|||||||
<div className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
<div className="w-full p-4 px-8 bg-transparent border-2 border-mti-gray-platinum rounded-2xl flex-col gap-8 items-center">
|
||||||
<p className="text-base font-normal">Record your answer:</p>
|
<p className="text-base font-normal">Record your answer:</p>
|
||||||
<div className="flex gap-8 items-center justify-center py-8">
|
<div className="flex gap-8 items-center justify-center py-8">
|
||||||
{status === "idle" && (
|
{status === "idle" && !mediaBlob && (
|
||||||
<>
|
<>
|
||||||
<div className="w-full h-2 max-w-4xl bg-mti-gray-smoke rounded-full" />
|
<div className="w-full h-2 max-w-4xl bg-mti-gray-smoke rounded-full" />
|
||||||
{status === "idle" && (
|
{status === "idle" && (
|
||||||
@@ -177,9 +288,9 @@ export default function InteractiveSpeaking({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{status === "stopped" && mediaBlobUrl && (
|
{((status === "stopped" && mediaBlobUrl) || (status === "idle" && mediaBlob)) && (
|
||||||
<>
|
<>
|
||||||
<Waveform audio={mediaBlobUrl} waveColor="#FCDDEC" progressColor="#EF5DA8" />
|
<Waveform audio={mediaBlobUrl ? mediaBlobUrl : mediaBlob!} waveColor="#FCDDEC" progressColor="#EF5DA8" />
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
<BsTrashFill
|
<BsTrashFill
|
||||||
className="text-mti-gray-cool cursor-pointer w-5 h-5"
|
className="text-mti-gray-cool cursor-pointer w-5 h-5"
|
||||||
@@ -209,43 +320,10 @@ export default function InteractiveSpeaking({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="self-end flex justify-between w-full gap-8">
|
<div className="self-end flex justify-between w-full gap-8">
|
||||||
<Button
|
<Button color="purple" variant="outline" isLoading={isLoading} onClick={back} className="max-w-[200px] self-end w-full">
|
||||||
color="purple"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
onBack({
|
|
||||||
exercise: id,
|
|
||||||
solutions: answers,
|
|
||||||
score: {correct: 1, total: 1, missing: 0},
|
|
||||||
type,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button color="purple" disabled={!mediaBlob} isLoading={isLoading} onClick={next} className="max-w-[200px] self-end w-full">
|
||||||
color="purple"
|
|
||||||
disabled={!mediaBlob}
|
|
||||||
onClick={() => {
|
|
||||||
saveAnswer();
|
|
||||||
if (questionIndex + 1 < prompts.length) {
|
|
||||||
setQuestionIndex(questionIndex + 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onNext({
|
|
||||||
exercise: id,
|
|
||||||
solutions: [
|
|
||||||
...answers,
|
|
||||||
{
|
|
||||||
prompt: prompts[questionIndex].text,
|
|
||||||
blob: mediaBlob!,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
score: {correct: 1, total: 1, missing: 0},
|
|
||||||
type,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="max-w-[200px] self-end w-full">
|
|
||||||
{questionIndex + 1 < prompts.length ? "Next Prompt" : "Submit"}
|
{questionIndex + 1 < prompts.length ? "Next Prompt" : "Submit"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import InteractiveSpeaking from "./InteractiveSpeaking";
|
|||||||
const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false});
|
const MatchSentences = dynamic(() => import("@/components/Exercises/MatchSentences"), {ssr: false});
|
||||||
|
|
||||||
export interface CommonProps {
|
export interface CommonProps {
|
||||||
|
examID?: string;
|
||||||
updateIndex?: (internalIndex: number) => void;
|
updateIndex?: (internalIndex: number) => void;
|
||||||
onNext: (userSolutions: UserSolution) => void;
|
onNext: (userSolutions: UserSolution) => void;
|
||||||
onBack: (userSolutions: UserSolution) => void;
|
onBack: (userSolutions: UserSolution) => void;
|
||||||
@@ -29,17 +30,18 @@ export interface CommonProps {
|
|||||||
|
|
||||||
export const renderExercise = (
|
export const renderExercise = (
|
||||||
exercise: Exercise,
|
exercise: Exercise,
|
||||||
|
examID: string,
|
||||||
onNext: (userSolutions: UserSolution) => void,
|
onNext: (userSolutions: UserSolution) => void,
|
||||||
onBack: (userSolutions: UserSolution) => void,
|
onBack: (userSolutions: UserSolution) => void,
|
||||||
updateIndex?: (internalIndex: number) => void,
|
updateIndex?: (internalIndex: number) => void,
|
||||||
) => {
|
) => {
|
||||||
switch (exercise.type) {
|
switch (exercise.type) {
|
||||||
case "fillBlanks":
|
case "fillBlanks":
|
||||||
return <FillBlanks key={exercise.id} {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
return <FillBlanks key={exercise.id} {...(exercise as FillBlanksExercise)} onNext={onNext} onBack={onBack} examID={examID} />;
|
||||||
case "trueFalse":
|
case "trueFalse":
|
||||||
return <TrueFalse key={exercise.id} {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} />;
|
return <TrueFalse key={exercise.id} {...(exercise as TrueFalseExercise)} onNext={onNext} onBack={onBack} examID={examID} />;
|
||||||
case "matchSentences":
|
case "matchSentences":
|
||||||
return <MatchSentences key={exercise.id} {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} />;
|
return <MatchSentences key={exercise.id} {...(exercise as MatchSentencesExercise)} onNext={onNext} onBack={onBack} examID={examID} />;
|
||||||
case "multipleChoice":
|
case "multipleChoice":
|
||||||
return (
|
return (
|
||||||
<MultipleChoice
|
<MultipleChoice
|
||||||
@@ -48,19 +50,21 @@ export const renderExercise = (
|
|||||||
updateIndex={updateIndex}
|
updateIndex={updateIndex}
|
||||||
onNext={onNext}
|
onNext={onNext}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
|
examID={examID}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "writeBlanks":
|
case "writeBlanks":
|
||||||
return <WriteBlanks key={exercise.id} {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} />;
|
return <WriteBlanks key={exercise.id} {...(exercise as WriteBlanksExercise)} onNext={onNext} onBack={onBack} examID={examID} />;
|
||||||
case "writing":
|
case "writing":
|
||||||
return <Writing key={exercise.id} {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} />;
|
return <Writing key={exercise.id} {...(exercise as WritingExercise)} onNext={onNext} onBack={onBack} examID={examID} />;
|
||||||
case "speaking":
|
case "speaking":
|
||||||
return <Speaking key={exercise.id} {...(exercise as SpeakingExercise)} onNext={onNext} onBack={onBack} />;
|
return <Speaking key={exercise.id} {...(exercise as SpeakingExercise)} onNext={onNext} onBack={onBack} examID={examID} />;
|
||||||
case "interactiveSpeaking":
|
case "interactiveSpeaking":
|
||||||
return (
|
return (
|
||||||
<InteractiveSpeaking
|
<InteractiveSpeaking
|
||||||
key={exercise.id}
|
key={exercise.id}
|
||||||
{...(exercise as InteractiveSpeakingExercise)}
|
{...(exercise as InteractiveSpeakingExercise)}
|
||||||
|
examID={examID}
|
||||||
updateIndex={updateIndex}
|
updateIndex={updateIndex}
|
||||||
onNext={onNext}
|
onNext={onNext}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export default function Level({exam, showSolutions = false, onFinish}: Props) {
|
|||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
exerciseIndex < exam.exercises.length &&
|
exerciseIndex < exam.exercises.length &&
|
||||||
!showSolutions &&
|
!showSolutions &&
|
||||||
renderExercise(getExercise(), nextExercise, previousExercise, setCurrentQuestionIndex)}
|
renderExercise(getExercise(), exam.id, nextExercise, previousExercise, setCurrentQuestionIndex)}
|
||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
exerciseIndex < exam.exercises.length &&
|
exerciseIndex < exam.exercises.length &&
|
||||||
showSolutions &&
|
showSolutions &&
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ export default function Listening({exam, showSolutions = false, onFinish}: Props
|
|||||||
partIndex > -1 &&
|
partIndex > -1 &&
|
||||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
||||||
!showSolutions &&
|
!showSolutions &&
|
||||||
renderExercise(getExercise(), nextExercise, previousExercise, setCurrentQuestionIndex)}
|
renderExercise(getExercise(), exam.id, nextExercise, previousExercise, setCurrentQuestionIndex)}
|
||||||
|
|
||||||
{/* Solution renderer */}
|
{/* Solution renderer */}
|
||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ export default function Reading({exam, showSolutions = false, onFinish}: Props)
|
|||||||
partIndex > -1 &&
|
partIndex > -1 &&
|
||||||
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
exerciseIndex < exam.parts[partIndex].exercises.length &&
|
||||||
!showSolutions &&
|
!showSolutions &&
|
||||||
renderExercise(getExercise(), nextExercise, previousExercise, setCurrentQuestionIndex)}
|
renderExercise(getExercise(), exam.id, nextExercise, previousExercise, setCurrentQuestionIndex)}
|
||||||
|
|
||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
partIndex > -1 &&
|
partIndex > -1 &&
|
||||||
|
|||||||
@@ -80,18 +80,18 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props)
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col h-full w-full gap-8 items-center">
|
<div className="flex flex-col h-full w-full gap-8 items-center">
|
||||||
{/* <ModuleTitle
|
<ModuleTitle
|
||||||
label={convertCamelCaseToReadable(exam.exercises[exerciseIndex].type)}
|
label={convertCamelCaseToReadable(exam.exercises[exerciseIndex].type)}
|
||||||
minTimer={exam.minTimer}
|
minTimer={exam.minTimer}
|
||||||
exerciseIndex={exerciseIndex + 1 + questionIndex + currentQuestionIndex}
|
exerciseIndex={exerciseIndex + 1 + questionIndex + currentQuestionIndex}
|
||||||
module="speaking"
|
module="speaking"
|
||||||
totalExercises={countExercises(exam.exercises)}
|
totalExercises={countExercises(exam.exercises)}
|
||||||
disableTimer={showSolutions}
|
disableTimer={showSolutions}
|
||||||
/> */}
|
/>
|
||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
exerciseIndex < exam.exercises.length &&
|
exerciseIndex < exam.exercises.length &&
|
||||||
!showSolutions &&
|
!showSolutions &&
|
||||||
renderExercise(getExercise(), nextExercise, previousExercise, setCurrentQuestionIndex)}
|
renderExercise(getExercise(), exam.id, nextExercise, previousExercise, setCurrentQuestionIndex)}
|
||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
exerciseIndex < exam.exercises.length &&
|
exerciseIndex < exam.exercises.length &&
|
||||||
showSolutions &&
|
showSolutions &&
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export default function Writing({exam, showSolutions = false, onFinish}: Props)
|
|||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
exerciseIndex < exam.exercises.length &&
|
exerciseIndex < exam.exercises.length &&
|
||||||
!showSolutions &&
|
!showSolutions &&
|
||||||
renderExercise(getExercise(), nextExercise, previousExercise)}
|
renderExercise(getExercise(), exam.id, nextExercise, previousExercise)}
|
||||||
{exerciseIndex > -1 &&
|
{exerciseIndex > -1 &&
|
||||||
exerciseIndex < exam.exercises.length &&
|
exerciseIndex < exam.exercises.length &&
|
||||||
showSolutions &&
|
showSolutions &&
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export interface InteractiveSpeakingExercise {
|
|||||||
prompts: {text: string; video_url: string}[];
|
prompts: {text: string; video_url: string}[];
|
||||||
userSolutions: {
|
userSolutions: {
|
||||||
id: string;
|
id: string;
|
||||||
solution: {question: string; answer: string}[];
|
solution: {questionIndex: number; question: string; answer: string}[];
|
||||||
evaluation?: InteractiveSpeakingEvaluation;
|
evaluation?: InteractiveSpeakingEvaluation;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,8 +181,8 @@ export default function ExamPage({page}: Props) {
|
|||||||
id: solution.id || uuidv4(),
|
id: solution.id || uuidv4(),
|
||||||
timeSpent,
|
timeSpent,
|
||||||
session: sessionId,
|
session: sessionId,
|
||||||
exam: solution.exam!,
|
exam: exam!.id,
|
||||||
module: solution.module!,
|
module: exam!.module,
|
||||||
user: user?.id || "",
|
user: user?.id || "",
|
||||||
date: new Date().getTime(),
|
date: new Date().getTime(),
|
||||||
...(assignment ? {assignment: assignment.id} : {}),
|
...(assignment ? {assignment: assignment.id} : {}),
|
||||||
@@ -328,6 +328,8 @@ export default function ExamPage({page}: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
answers.forEach((x) => {
|
answers.forEach((x) => {
|
||||||
|
console.log({x});
|
||||||
|
|
||||||
scores[x.module!] = {
|
scores[x.module!] = {
|
||||||
total: scores[x.module!].total + x.score.total,
|
total: scores[x.module!].total + x.score.total,
|
||||||
correct: scores[x.module!].correct + x.score.correct,
|
correct: scores[x.module!].correct + x.score.correct,
|
||||||
@@ -366,6 +368,8 @@ export default function ExamPage({page}: Props) {
|
|||||||
onViewResults={() => {
|
onViewResults={() => {
|
||||||
setShowSolutions(true);
|
setShowSolutions(true);
|
||||||
setModuleIndex(0);
|
setModuleIndex(0);
|
||||||
|
setExerciseIndex(["reading", "listening"].includes(exams[0].module) ? -1 : 0);
|
||||||
|
setPartIndex(exams[0].module === "listening" ? -1 : 0);
|
||||||
setExam(exams[0]);
|
setExam(exams[0]);
|
||||||
}}
|
}}
|
||||||
scores={aggregateScoresByModule(userSolutions)}
|
scores={aggregateScoresByModule(userSolutions)}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const audioFile = files.audio;
|
const audioFile = files.audio;
|
||||||
console.log({fields}, (audioFile as any).path);
|
|
||||||
const audioFileRef = ref(storage, `${fields.root}/${(audioFile as any).path.split("/").pop()!.replace("upload_", "")}`);
|
const audioFileRef = ref(storage, `${fields.root}/${(audioFile as any).path.split("/").pop()!.replace("upload_", "")}`);
|
||||||
|
|
||||||
const binary = fs.readFileSync((audioFile as any).path).buffer;
|
const binary = fs.readFileSync((audioFile as any).path).buffer;
|
||||||
|
|||||||
@@ -178,7 +178,10 @@ export default function History({user}: {user: User}) {
|
|||||||
const {timeSpent, session} = dateStats[0];
|
const {timeSpent, session} = dateStats[0];
|
||||||
|
|
||||||
const selectExam = () => {
|
const selectExam = () => {
|
||||||
const examPromises = uniqBy(dateStats, "exam").map((stat) => getExamById(stat.module, stat.exam));
|
const examPromises = uniqBy(dateStats, "exam").map((stat) => {
|
||||||
|
console.log({stat});
|
||||||
|
return getExamById(stat.module, stat.exam);
|
||||||
|
});
|
||||||
|
|
||||||
Promise.all(examPromises).then((exams) => {
|
Promise.all(examPromises).then((exams) => {
|
||||||
if (exams.every((x) => !!x)) {
|
if (exams.every((x) => !!x)) {
|
||||||
|
|||||||
@@ -53,9 +53,12 @@ export const downloadBlob = async (url: string): Promise<Buffer> => {
|
|||||||
const evaluateSpeakingExercise = async (exercise: SpeakingExercise, exerciseId: string, solution: UserSolution, id: string) => {
|
const evaluateSpeakingExercise = async (exercise: SpeakingExercise, exerciseId: string, solution: UserSolution, id: string) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
const audioBlob = await downloadBlob(solution.solutions[0].solution.trim());
|
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});
|
||||||
|
|
||||||
formData.append("audio", audioFile, "audio.wav");
|
formData.append("audio", audioFile, "audio.wav");
|
||||||
|
|
||||||
const evaluationQuestion =
|
const evaluationQuestion =
|
||||||
@@ -87,10 +90,15 @@ const evaluateSpeakingExercise = async (exercise: SpeakingExercise, exerciseId:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution: UserSolution, id: string) => {
|
const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution: UserSolution, id: string) => {
|
||||||
const promiseParts = solution.solutions.map(async (x: {prompt: string; blob: string}) => ({
|
const promiseParts = solution.solutions.map(async (x: {prompt: string; blob: string}) => {
|
||||||
question: x.prompt,
|
const blob = await downloadBlob(x.blob);
|
||||||
answer: 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);
|
const body = await Promise.all(promiseParts);
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -111,6 +119,7 @@ const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const response = await axios.post("/api/evaluate/interactiveSpeaking", formData, config);
|
const response = await axios.post("/api/evaluate/interactiveSpeaking", formData, config);
|
||||||
|
console.log({data: response.data, status: response.status});
|
||||||
|
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
return {
|
return {
|
||||||
@@ -120,6 +129,7 @@ const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution:
|
|||||||
missing: 0,
|
missing: 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
},
|
},
|
||||||
|
module: "speaking",
|
||||||
solutions: [{id: exerciseId, solution: response.data ? response.data.answer : null, evaluation: response.data}],
|
solutions: [{id: exerciseId, solution: response.data ? response.data.answer : null, evaluation: response.data}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user