Updated the speaking and interactive speaking to the new format

This commit is contained in:
Tiago Ribeiro
2024-06-18 10:02:03 +01:00
parent 0eddded560
commit cb49e15cb0
8 changed files with 51 additions and 41 deletions

View File

@@ -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">

View File

@@ -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 (

View File

@@ -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 {

View File

@@ -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]);

View File

@@ -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",

View File

@@ -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}`,
}, },

View File

@@ -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}`,
}, },

View File

@@ -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: {