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( 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(); 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: false, 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 ( ); }; export default SpeakingSettings;