import { Exercise, InteractiveSpeakingExercise, LevelExam, LevelPart, SpeakingExercise } from "@/interfaces/exam"; import SettingsEditor from "."; import Option from "@/interfaces/option"; import Dropdown from "@/components/Dropdown"; import clsx from "clsx"; import ExercisePicker from "../ExercisePicker"; import useExamEditorStore from "@/stores/examEditor"; import useSettingsState from "../Hooks/useSettingsState"; import { LevelSectionSettings } from "@/stores/examEditor/types"; import { toast } from "react-toastify"; import axios from "axios"; import { playSound } from "@/utils/sound"; import { useRouter } from "next/router"; import { usePersistentExamStore } from "@/stores/exam"; import openDetachedTab from "@/utils/popout"; import ListeningComponents from "./listening/components"; import ReadingComponents from "./reading/components"; import SpeakingComponents from "./speaking/components"; import SectionPicker from "./Shared/SectionPicker"; const LevelSettings: React.FC = () => { const router = useRouter(); const { setExam, setExerciseIndex, setPartIndex, setQuestionIndex, setBgColor, } = usePersistentExamStore(); const { currentModule, title } = useExamEditorStore(); const { focusedSection, difficulty, sections, minTimer, isPrivate, } = useExamEditorStore(state => state.modules[currentModule]); const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState( currentModule, focusedSection ); const section = sections.find((section) => section.sectionId == focusedSection); const focusedExercise = section?.focusedExercise; if (section === undefined) return <>; const currentSection = section.state as LevelPart; const readingSection = section.readingSection; const listeningSection = section.listeningSection; const canPreviewOrSubmit = sections.length > 0 && sections.some(s => { const part = s.state as LevelPart; return part.exercises.length > 0 && part.exercises.every((exercise) => { if (exercise.type === 'speaking') { return exercise.title !== '' && exercise.text !== '' && exercise.video_url !== '' && exercise.prompts.every(prompt => prompt !== ''); } else if (exercise.type === 'interactiveSpeaking') { if ('first_title' in exercise && 'second_title' in exercise) { return exercise.first_title !== '' && exercise.second_title !== '' && exercise.prompts.every(prompt => prompt.video_url !== '') && exercise.prompts.length > 2; } return exercise.title !== '' && exercise.prompts.every(prompt => prompt.video_url !== ''); } return true; }); }); const submitLevel = async () => { if (title === "") { toast.error("Enter a title for the exam!"); return; } const partsWithMissingAudio = sections.some(s => { const part = s.state as LevelPart; return part.audio && !part.audio.source; }); if (partsWithMissingAudio) { toast.error("There are parts with missing audio recordings. Either generate them or remove the listening sections."); return; } try { const audioFormData = new FormData(); const videoFormData = new FormData(); const audioMap = new Map(); const videoMap = new Map(); const partsWithAudio = sections.filter(s => (s.state as LevelPart).audio?.source); await Promise.all( partsWithAudio.map(async (section) => { const levelPart = section.state as LevelPart; const blobUrl = levelPart.audio!.source; const response = await fetch(blobUrl); const blob = await response.blob(); audioFormData.append('file', blob, 'audio.mp3'); audioMap.set(section.sectionId, blobUrl); }) ); await Promise.all( sections.flatMap(async (section) => { const levelPart = section.state as LevelPart; return Promise.all( levelPart.exercises.map(async (exercise, exerciseIndex) => { if (exercise.type === "speaking") { if (exercise.video_url) { const response = await fetch(exercise.video_url); const blob = await response.blob(); videoFormData.append('file', blob, 'video.mp4'); videoMap.set(`${section.sectionId}-${exerciseIndex}`, exercise.video_url); } } else if (exercise.type === "interactiveSpeaking") { 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(); videoFormData.append('file', blob, 'video.mp4'); videoMap.set(`${section.sectionId}-${exerciseIndex}-${promptIndex}`, prompt.video_url); } }) ); } }) ); }) ); const [audioUrls, videoUrls] = await Promise.all([ audioMap.size > 0 ? axios.post('/api/storage', audioFormData, { params: { directory: 'listening_recordings' }, headers: { 'Content-Type': 'multipart/form-data' } }).then(response => response.data.urls) : [], videoMap.size > 0 ? axios.post('/api/storage', videoFormData, { params: { directory: 'speaking_videos' }, headers: { 'Content-Type': 'multipart/form-data' } }).then(response => response.data.urls) : [] ]); const exam: LevelExam = { parts: sections.map((s) => { const part = s.state as LevelPart; const audioIndex = Array.from(audioMap.entries()) .findIndex(([id]) => id === s.sectionId); const updatedExercises = part.exercises.map((exercise, exerciseIndex) => { if (exercise.type === "speaking") { const videoIndex = Array.from(videoMap.entries()) .findIndex(([key]) => key === `${s.sectionId}-${exerciseIndex}`); return { ...exercise, video_url: videoIndex !== -1 ? videoUrls[videoIndex] : exercise.video_url }; } else if (exercise.type === "interactiveSpeaking") { const updatedPrompts = exercise.prompts.map((prompt, promptIndex) => { const videoIndex = Array.from(videoMap.entries()) .findIndex(([key]) => key === `${s.sectionId}-${exerciseIndex}-${promptIndex}`); return { ...prompt, video_url: videoIndex !== -1 ? videoUrls[videoIndex] : prompt.video_url }; }); return { ...exercise, prompts: updatedPrompts }; } return exercise; }); return { ...part, audio: part.audio ? { ...part.audio, source: audioIndex !== -1 ? audioUrls[audioIndex] : part.audio.source } : undefined, exercises: updatedExercises, intro: s.settings.currentIntro, category: s.settings.category }; }).filter(part => part.exercises.length > 0), isDiagnostic: false, minTimer, module: "level", id: title, difficulty, private: isPrivate, }; const result = await axios.post('/api/exam/level', exam); playSound("sent"); toast.success(`Submitted Exam ID: ${result.data.id}`); Array.from(audioMap.values()).forEach(url => { URL.revokeObjectURL(url); }); Array.from(videoMap.values()).forEach(url => { URL.revokeObjectURL(url); }); } catch (error: any) { console.error('Error submitting exam:', error); toast.error( "Something went wrong while submitting, please try again later." ); } }; const preview = () => { setExam({ parts: sections.map((s) => { const part = s.state as LevelPart; return { ...part, intro: s.settings.currentIntro, category: s.settings.category }; }), minTimer, module: "level", id: title, isDiagnostic: false, variant: undefined, difficulty, private: isPrivate, } as LevelExam); setExerciseIndex(0); setQuestionIndex(0); setPartIndex(0); openDetachedTab("popout?type=Exam&module=level", router) } const speakingExercise = focusedExercise === undefined ? undefined : currentSection.exercises.find((ex) => ex.id === focusedExercise.id) as SpeakingExercise | InteractiveSpeakingExercise; return (
updateLocalAndScheduleGlobal({ isLevelDropdownOpen: isOpen }, false)} >
updateLocalAndScheduleGlobal({ isReadingDropdownOpen: isOpen }, false)} >
updateLocalAndScheduleGlobal({ isListeningDropdownOpen: isOpen }, false)} >
updateLocalAndScheduleGlobal({ isWritingDropdownOpen: isOpen }, false)} >
updateLocalAndScheduleGlobal({ isSpeakingDropdownOpen: isOpen }, false)} >
updateLocalAndScheduleGlobal({ isSpeakingExercisesOpen: isOpen }, false)} > {speakingExercise !== undefined && updateLocalAndScheduleGlobal({ isConfigureExercisesOpen: isOpen }, false)} >
}
); }; export default LevelSettings;