Files
encoach_frontend/src/components/ExamEditor/SettingsEditor/speaking/index.tsx
2025-02-09 17:37:19 +00:00

269 lines
9.9 KiB
TypeScript

import useExamEditorStore from "@/stores/examEditor";
import useSettingsState from "../../Hooks/useSettingsState";
import { SpeakingSectionSettings } from "@/stores/examEditor/types";
import Option from "@/interfaces/option";
import SettingsEditor from "..";
import { InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise } from "@/interfaces/exam";
import { toast } from "react-toastify";
import { usePersistentExamStore } from "@/stores/exam";
import { useRouter } from "next/router";
import openDetachedTab from "@/utils/popout";
import axios from "axios";
import { playSound } from "@/utils/sound";
import SpeakingComponents from "./components";
import { getExamById } from "@/utils/exams";
export interface Avatar {
name: string;
gender: string;
}
const SpeakingSettings: React.FC = () => {
const router = useRouter();
const {
setExam,
setExerciseIndex,
setQuestionIndex,
setBgColor,
} = usePersistentExamStore();
const { title, currentModule } = useExamEditorStore();
const { focusedSection, difficulty, sections, minTimer, access } = useExamEditorStore((store) => store.modules[currentModule])
const section = sections.find((section) => section.sectionId == focusedSection)?.state;
const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState<SpeakingSectionSettings>(
currentModule,
focusedSection,
);
if (section === undefined) return <></>;
const currentSection = section as SpeakingExercise | InteractiveSpeakingExercise;
const defaultPresets: Option[] = [
{
label: "Preset: Speaking Part 1",
value: "Welcome to {part} of the {label}. You will engage in a conversation about yourself and familiar topics such as your home, family, work, studies, and interests. General questions will be asked."
},
{
label: "Preset: Speaking Part 2",
value: "Welcome to {part} of the {label}. You will be given a topic card describing a particular person, object, event, or experience."
},
{
label: "Preset: Speaking Part 3",
value: "Welcome to {part} of the {label}. You will engage in an in-depth discussion about abstract ideas and issues. The examiner will ask questions that require you to explain, analyze, and speculate about various aspects of the topic."
}
];
const canPreviewOrSubmit = (() => {
return sections.every((s) => {
const section = s.state as SpeakingExercise | InteractiveSpeakingExercise;
switch (section.type) {
case 'speaking':
return section.title !== '' &&
section.text !== '' &&
section.video_url !== '' &&
section.prompts.every(prompt => prompt !== '');
case 'interactiveSpeaking':
if ('first_title' in section && 'second_title' in section) {
return section.first_title !== '' &&
section.second_title !== '' &&
section.prompts.every(prompt => prompt.video_url !== '') &&
section.prompts.length > 2;
}
return section.title !== '' &&
section.prompts.every(prompt => prompt.video_url !== '');
default:
return false;
}
});
})();
const submitSpeaking = async (requiresApproval: boolean) => {
if (title === "") {
toast.error("Enter a title for the exam!");
return;
}
try {
const formData = new FormData();
const urlMap = new Map<string, string>();
const sectionsWithVideos = sections.filter(s => {
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
if (exercise.type === "speaking") {
return exercise.video_url !== "";
}
if (exercise.type === "interactiveSpeaking") {
return exercise.prompts?.some(prompt => prompt.video_url !== "");
}
return false;
});
if (sectionsWithVideos.length === 0) {
toast.error('No video sections found in the exam! Please record or import videos.');
return;
}
await Promise.all(
sectionsWithVideos.map(async (section) => {
const exercise = section.state as SpeakingExercise | InteractiveSpeakingExercise;
if (exercise.type === "speaking") {
const response = await fetch(exercise.video_url);
const blob = await response.blob();
formData.append('file', blob, 'video.mp4');
urlMap.set(`${section.sectionId}`, exercise.video_url);
} else {
await Promise.all(
exercise.prompts.map(async (prompt, promptIndex) => {
if (prompt.video_url) {
const response = await fetch(prompt.video_url);
const blob = await response.blob();
formData.append('file', blob, 'video.mp4');
urlMap.set(`${section.sectionId}-${promptIndex}`, prompt.video_url);
}
})
);
}
})
);
const response = await axios.post('/api/storage', formData, {
params: {
directory: 'speaking_videos'
},
headers: {
'Content-Type': 'multipart/form-data'
}
});
const { urls } = response.data;
const exam: SpeakingExam = {
exercises: sections.map((s) => {
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
if (exercise.type === "speaking") {
const videoIndex = Array.from(urlMap.entries())
.findIndex(([key]) => key === `${s.sectionId}`);
return {
...exercise,
video_url: videoIndex !== -1 ? urls[videoIndex] : exercise.video_url,
intro: s.settings.currentIntro,
category: s.settings.category
};
} else {
const updatedPrompts = exercise.prompts.map((prompt, promptIndex) => {
const videoIndex = Array.from(urlMap.entries())
.findIndex(([key]) => key === `${s.sectionId}-${promptIndex}`);
return {
...prompt,
video_url: videoIndex !== -1 ? urls[videoIndex] : prompt.video_url
};
});
return {
...exercise,
prompts: updatedPrompts,
intro: s.settings.currentIntro,
category: s.settings.category
};
}
}),
minTimer,
module: "speaking",
id: title,
requiresApproval: requiresApproval,
isDiagnostic: requiresApproval ? true : false, // using isDiagnostic to keep exam hidden until the respective approval workflow is completed.
variant: undefined,
difficulty,
instructorGender: "varied",
access,
};
const result = await axios.post('/api/exam/speaking', exam);
playSound("sent");
// Successfully submitted exam
if (result.status === 200) {
toast.success(result.data.message);
} else if (result.status === 207) {
toast.warning(result.data.message);
}
Array.from(urlMap.values()).forEach(url => {
URL.revokeObjectURL(url);
});
} catch (error: any) {
toast.error(
"Something went wrong while submitting, please try again later."
);
}
};
const preview = () => {
setExam({
exercises: sections
.filter((s) => {
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
if (exercise.type === "speaking") {
return exercise.video_url !== "";
}
if (exercise.type === "interactiveSpeaking") {
return exercise.prompts?.every(prompt => prompt.video_url !== "");
}
return false;
})
.map((s) => {
const exercise = s.state as SpeakingExercise | InteractiveSpeakingExercise;
return {
...exercise,
intro: s.settings.currentIntro,
category: s.settings.category
};
}),
minTimer,
module: "speaking",
id: title,
isDiagnostic: false,
variant: undefined,
difficulty,
access,
} as SpeakingExam);
setExerciseIndex(0);
setQuestionIndex(0);
setBgColor("bg-white");
openDetachedTab("popout?type=Exam&module=speaking", router)
}
return (
<SettingsEditor
sectionLabel={`Speaking ${focusedSection}`}
sectionId={focusedSection}
module="speaking"
introPresets={[defaultPresets[focusedSection - 1]]}
preview={preview}
canPreview={canPreviewOrSubmit}
canSubmit={canPreviewOrSubmit}
submitModule={submitSpeaking}
>
<SpeakingComponents
{...{ localSettings, updateLocalAndScheduleGlobal, section: currentSection }}
/>
</SettingsEditor>
);
};
export default SpeakingSettings;