Updated the speaking and interactive speaking to the new format
This commit is contained in:
@@ -16,8 +16,9 @@ const ReactMediaRecorder = dynamic(() => import("react-media-recorder").then((mo
|
|||||||
export default function InteractiveSpeaking({
|
export default function InteractiveSpeaking({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
first_title,
|
||||||
|
second_title,
|
||||||
examID,
|
examID,
|
||||||
text,
|
|
||||||
type,
|
type,
|
||||||
prompts,
|
prompts,
|
||||||
userSolutions,
|
userSolutions,
|
||||||
@@ -35,31 +36,6 @@ export default function InteractiveSpeaking({
|
|||||||
|
|
||||||
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 () => {
|
const back = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
@@ -177,7 +153,7 @@ export default function InteractiveSpeaking({
|
|||||||
<div className="flex flex-col h-full w-full gap-9">
|
<div className="flex flex-col h-full w-full gap-9">
|
||||||
<div className="flex flex-col w-full gap-8 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
<div className="flex flex-col w-full gap-8 bg-mti-gray-smoke rounded-xl py-8 pb-12 px-16">
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<span className="font-semibold">{title}</span>
|
<span className="font-semibold">{!!first_title && !!second_title ? `${first_title} & ${second_title}` : title}</span>
|
||||||
</div>
|
</div>
|
||||||
{prompts && prompts.length > 0 && (
|
{prompts && prompts.length > 0 && (
|
||||||
<div className="flex flex-col gap-4 w-full items-center">
|
<div className="flex flex-col gap-4 w-full items-center">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {renderExercise} from "@/components/Exercises";
|
|||||||
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
import ModuleTitle from "@/components/Medium/ModuleTitle";
|
||||||
import {renderSolution} from "@/components/Solutions";
|
import {renderSolution} from "@/components/Solutions";
|
||||||
import {infoButtonStyle} from "@/constants/buttonStyles";
|
import {infoButtonStyle} from "@/constants/buttonStyles";
|
||||||
import {UserSolution, SpeakingExam} from "@/interfaces/exam";
|
import {UserSolution, SpeakingExam, SpeakingExercise, InteractiveSpeakingExercise} from "@/interfaces/exam";
|
||||||
import useExamStore from "@/stores/examStore";
|
import useExamStore from "@/stores/examStore";
|
||||||
import {defaultUserSolutions} from "@/utils/exams";
|
import {defaultUserSolutions} from "@/utils/exams";
|
||||||
import {countExercises} from "@/utils/moduleUtils";
|
import {countExercises} from "@/utils/moduleUtils";
|
||||||
@@ -64,8 +64,9 @@ export default function Speaking({exam, showSolutions = false, onFinish}: Props)
|
|||||||
const exercise = exam.exercises[exerciseIndex];
|
const exercise = exam.exercises[exerciseIndex];
|
||||||
return {
|
return {
|
||||||
...exercise,
|
...exercise,
|
||||||
|
variant: exerciseIndex < 2 && exercise.type === "interactiveSpeaking" ? "initial" : undefined,
|
||||||
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
userSolutions: userSolutions.find((x) => x.exercise === exercise.id)?.solutions || [],
|
||||||
};
|
} as SpeakingExercise | InteractiveSpeakingExercise;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ export interface InteractiveSpeakingExercise {
|
|||||||
id: string;
|
id: string;
|
||||||
type: "interactiveSpeaking";
|
type: "interactiveSpeaking";
|
||||||
title: string;
|
title: string;
|
||||||
|
first_title?: string;
|
||||||
|
second_title?: string;
|
||||||
text: string;
|
text: string;
|
||||||
prompts: {text: string; video_url: string}[];
|
prompts: {text: string; video_url: string}[];
|
||||||
userSolutions: {
|
userSolutions: {
|
||||||
@@ -185,6 +187,9 @@ export interface InteractiveSpeakingExercise {
|
|||||||
evaluation?: InteractiveSpeakingEvaluation;
|
evaluation?: InteractiveSpeakingEvaluation;
|
||||||
}[];
|
}[];
|
||||||
topic?: string;
|
topic?: string;
|
||||||
|
first_topic?: string;
|
||||||
|
second_topic?: string;
|
||||||
|
variant?: "initial" | "final";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FillBlanksExercise {
|
export interface FillBlanksExercise {
|
||||||
|
|||||||
@@ -133,14 +133,22 @@ export default function ExamPage({page}: Props) {
|
|||||||
!!exam &&
|
!!exam &&
|
||||||
timeSpent > 0 &&
|
timeSpent > 0 &&
|
||||||
!showSolutions &&
|
!showSolutions &&
|
||||||
moduleIndex < selectedModules.length
|
moduleIndex < selectedModules.length &&
|
||||||
|
selectedModules[moduleIndex] !== "speaking"
|
||||||
)
|
)
|
||||||
saveSession();
|
saveSession();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [assignment, exam, exams, moduleIndex, selectedModules, sessionId, userSolutions, user, exerciseIndex, partIndex, questionIndex]);
|
}, [assignment, exam, exams, moduleIndex, selectedModules, sessionId, userSolutions, user, exerciseIndex, partIndex, questionIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (timeSpent % 20 === 0 && timeSpent > 0 && moduleIndex < selectedModules.length && !showSolutions) saveSession();
|
if (
|
||||||
|
timeSpent % 20 === 0 &&
|
||||||
|
timeSpent > 0 &&
|
||||||
|
moduleIndex < selectedModules.length &&
|
||||||
|
selectedModules[moduleIndex] !== "speaking" &&
|
||||||
|
!showSolutions
|
||||||
|
)
|
||||||
|
saveSession();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [timeSpent]);
|
}, [timeSpent]);
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const PartTab = ({
|
|||||||
.then((result) => {
|
.then((result) => {
|
||||||
playSound(typeof result.data === "string" ? "error" : "check");
|
playSound(typeof result.data === "string" ? "error" : "check");
|
||||||
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again.");
|
if (typeof result.data === "string") return toast.error("Something went wrong, please try to generate again.");
|
||||||
|
console.log(result.data);
|
||||||
setPart(result.data);
|
setPart(result.data);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -54,7 +55,7 @@ const PartTab = ({
|
|||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateVideo = () => {
|
const generateVideo = async () => {
|
||||||
if (!part) return toast.error("Please generate the first part before generating the video!");
|
if (!part) return toast.error("Please generate the first part before generating the video!");
|
||||||
toast.info("This will take quite a while, please do not leave this page or close the tab/window.");
|
toast.info("This will take quite a while, please do not leave this page or close the tab/window.");
|
||||||
|
|
||||||
@@ -64,11 +65,12 @@ const PartTab = ({
|
|||||||
const initialTime = moment();
|
const initialTime = moment();
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.post(`/api/exam/speaking/generate/speaking/generate_${index === 3 ? "interactive" : "speaking"}_video`, {...part, avatar: avatar?.id})
|
.post(`/api/exam/speaking/generate/speaking/generate_video_${index}`, {...part, avatar: avatar?.id})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const isError = typeof result.data === "string" || moment().diff(initialTime, "seconds") < 60;
|
const isError = typeof result.data === "string" || moment().diff(initialTime, "seconds") < 60;
|
||||||
|
|
||||||
playSound(isError ? "error" : "check");
|
playSound(isError ? "error" : "check");
|
||||||
|
console.log(result.data);
|
||||||
if (isError) return toast.error("Something went wrong, please try to generate the video again.");
|
if (isError) return toast.error("Something went wrong, please try to generate the video again.");
|
||||||
setPart({...part, result: {...result.data, topic: part?.topic}, gender, avatar});
|
setPart({...part, result: {...result.data, topic: part?.topic}, gender, avatar});
|
||||||
})
|
})
|
||||||
@@ -139,7 +141,9 @@ const PartTab = ({
|
|||||||
)}
|
)}
|
||||||
{part && !isLoading && (
|
{part && !isLoading && (
|
||||||
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-96">
|
<div className="flex flex-col gap-2 w-full overflow-y-scroll scrollbar-hide h-96">
|
||||||
<h3 className="text-xl font-semibold">{part.topic}</h3>
|
<h3 className="text-xl font-semibold">
|
||||||
|
{!!part.first_topic && !!part.second_topic ? `${part.first_topic} & ${part.second_topic}` : part.topic}
|
||||||
|
</h3>
|
||||||
{part.question && <span className="w-full">{part.question}</span>}
|
{part.question && <span className="w-full">{part.question}</span>}
|
||||||
{part.questions && (
|
{part.questions && (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -177,6 +181,8 @@ interface SpeakingPart {
|
|||||||
question?: string;
|
question?: string;
|
||||||
questions?: string[];
|
questions?: string[];
|
||||||
topic: string;
|
topic: string;
|
||||||
|
first_topic?: string;
|
||||||
|
second_topic?: string;
|
||||||
result?: SpeakingExercise | InteractiveSpeakingExercise;
|
result?: SpeakingExercise | InteractiveSpeakingExercise;
|
||||||
gender?: "male" | "female";
|
gender?: "male" | "female";
|
||||||
avatar?: (typeof AVATARS)[number];
|
avatar?: (typeof AVATARS)[number];
|
||||||
@@ -208,10 +214,18 @@ const SpeakingGeneration = () => {
|
|||||||
|
|
||||||
const genders = [part1?.gender, part2?.gender, part3?.gender].filter((x) => !!x);
|
const genders = [part1?.gender, part2?.gender, part3?.gender].filter((x) => !!x);
|
||||||
|
|
||||||
|
const exercises = [part1?.result, part2?.result, part3?.result]
|
||||||
|
.filter((x) => !!x)
|
||||||
|
.map((x) => ({
|
||||||
|
...x,
|
||||||
|
first_title: x?.type === "interactiveSpeaking" ? x.first_topic : undefined,
|
||||||
|
second_title: x?.type === "interactiveSpeaking" ? x.second_topic : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
const exam: SpeakingExam = {
|
const exam: SpeakingExam = {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
isDiagnostic: false,
|
isDiagnostic: false,
|
||||||
exercises: [part1?.result, part2?.result, part3?.result].filter((x) => !!x) as (SpeakingExercise | InteractiveSpeakingExercise)[],
|
exercises: exercises as (SpeakingExercise | InteractiveSpeakingExercise)[],
|
||||||
minTimer,
|
minTimer,
|
||||||
variant: minTimer >= 14 ? "full" : "partial",
|
variant: minTimer >= 14 ? "full" : "partial",
|
||||||
module: "speaking",
|
module: "speaking",
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
res.status(200).json(null);
|
res.status(200).json(null);
|
||||||
|
|
||||||
console.log("🌱 - Still processing");
|
console.log("🌱 - Still processing");
|
||||||
const backendRequest = await evaluate({answers: uploadingAudios});
|
const backendRequest = await evaluate({answers: uploadingAudios}, fields.variant);
|
||||||
console.log("🌱 - Process complete");
|
console.log("🌱 - Process complete");
|
||||||
|
|
||||||
const correspondingStat = await getCorrespondingStat(fields.id, 1);
|
const correspondingStat = await getCorrespondingStat(fields.id, 1);
|
||||||
@@ -79,8 +79,8 @@ async function getCorrespondingStat(id: string, index: number): Promise<Stat> {
|
|||||||
return getCorrespondingStat(id, index + 1);
|
return getCorrespondingStat(id, index + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function evaluate(body: {answers: object[]}): Promise<AxiosResponse> {
|
async function evaluate(body: {answers: object[]}, variant?: "initial" | "final"): Promise<AxiosResponse> {
|
||||||
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_3`, body, {
|
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_${variant === "initial" ? "1" : "3"}`, body, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ async function getCorrespondingStat(id: string, index: number): Promise<Stat> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function evaluate(body: {answer: string; question: string}, task: number): Promise<AxiosResponse> {
|
async function evaluate(body: {answer: string; question: string}, task: number): Promise<AxiosResponse> {
|
||||||
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_${task}`, body, {
|
const backendRequest = await axios.post(`${process.env.BACKEND_URL}/speaking_task_2`, body, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
Authorization: `Bearer ${process.env.BACKEND_JWT}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const evaluateSpeakingAnswer = async (
|
|||||||
case "speaking":
|
case "speaking":
|
||||||
return {...(await evaluateSpeakingExercise(exercise, exercise.id, solution, id, task)), id} as UserSolution;
|
return {...(await evaluateSpeakingExercise(exercise, exercise.id, solution, id, task)), id} as UserSolution;
|
||||||
case "interactiveSpeaking":
|
case "interactiveSpeaking":
|
||||||
return {...(await evaluateInteractiveSpeakingExercise(exercise.id, solution, id)), id} as UserSolution;
|
return {...(await evaluateInteractiveSpeakingExercise(exercise.id, solution, id, exercise.variant)), id} as UserSolution;
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,12 @@ const evaluateSpeakingExercise = async (
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution: UserSolution, id: string): Promise<UserSolution | undefined> => {
|
const evaluateInteractiveSpeakingExercise = async (
|
||||||
|
exerciseId: string,
|
||||||
|
solution: UserSolution,
|
||||||
|
id: string,
|
||||||
|
variant?: "initial" | "final",
|
||||||
|
): Promise<UserSolution | undefined> => {
|
||||||
const promiseParts = solution.solutions.map(async (x: {prompt: string; blob: string}) => {
|
const promiseParts = solution.solutions.map(async (x: {prompt: string; blob: string}) => {
|
||||||
const blob = await downloadBlob(x.blob);
|
const blob = await downloadBlob(x.blob);
|
||||||
if (!x.blob.startsWith("blob")) await axios.post("/api/storage/delete", {path: x.blob});
|
if (!x.blob.startsWith("blob")) await axios.post("/api/storage/delete", {path: x.blob});
|
||||||
@@ -132,6 +137,7 @@ const evaluateInteractiveSpeakingExercise = async (exerciseId: string, solution:
|
|||||||
formData.append(`answer_${seed}`, audioFile, `${seed}.wav`);
|
formData.append(`answer_${seed}`, audioFile, `${seed}.wav`);
|
||||||
});
|
});
|
||||||
formData.append("id", id);
|
formData.append("id", id);
|
||||||
|
formData.append("variant", variant || "final");
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
Reference in New Issue
Block a user