import useExamEditorStore from "@/stores/examEditor"; import useSettingsState from "../Hooks/useSettingsState"; import { SpeakingSectionSettings } from "@/stores/examEditor/types"; import Option from "@/interfaces/option"; import { useCallback, useState } from "react"; import { generate } from "./Shared/Generate"; import SettingsEditor from "."; import Dropdown from "./Shared/SettingsDropdown"; import Input from "@/components/Low/Input"; import GenerateBtn from "./Shared/GenerateBtn"; import clsx from "clsx"; import { FaFemale, FaMale, FaChevronDown } from "react-icons/fa"; import { InteractiveSpeakingExercise, SpeakingExam, SpeakingExercise } from "@/interfaces/exam"; import { toast } from "react-toastify"; import { generateVideos } from "./Shared/generateVideos"; import { usePersistentExamStore } from "@/stores/examStore"; import { useRouter } from "next/router"; import openDetachedTab from "@/utils/popout"; import axios from "axios"; import { playSound } from "@/utils/sound"; export interface Avatar { name: string; gender: string; } const SpeakingSettings: React.FC = () => { const router = useRouter(); const { setExam, setExerciseIndex, setQuestionIndex, setBgColor, } = usePersistentExamStore(); const { title, currentModule, speakingAvatars, dispatch } = useExamEditorStore(); const { focusedSection, difficulty, sections, minTimer, isPrivate } = useExamEditorStore((store) => store.modules[currentModule]) const section = sections.find((section) => section.sectionId == focusedSection)?.state; const { localSettings, updateLocalAndScheduleGlobal } = useSettingsState( currentModule, focusedSection, ); const [selectedAvatar, setSelectedAvatar] = useState(null); 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 generateScript = useCallback((sectionId: number) => { const queryParams: { difficulty: string; first_topic?: string; second_topic?: string; topic?: string; } = { difficulty }; if (sectionId === 1) { if (localSettings.topic) { queryParams['first_topic'] = localSettings.topic; } if (localSettings.secondTopic) { queryParams['second_topic'] = localSettings.secondTopic; } } else { if (localSettings.topic) { queryParams['topic'] = localSettings.topic; } } generate( sectionId, currentModule, "context", // <- not really context but exercises is reserved for reading, listening and level { method: 'GET', queryParams }, (data: any) => { switch (sectionId) { case 1: return [{ prompts: data.questions, first_topic: data.first_topic, second_topic: data.second_topic }]; case 2: return [{ topic: data.topic, question: data.question, prompts: data.prompts, suffix: data.suffix }]; case 3: return [{ title: data.topic, prompts: data.questions }]; default: return [data]; } } ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [localSettings, difficulty]); const onTopicChange = useCallback((topic: string) => { updateLocalAndScheduleGlobal({ topic }); }, [updateLocalAndScheduleGlobal]); const onSecondTopicChange = useCallback((topic: string) => { updateLocalAndScheduleGlobal({ secondTopic: topic }); }, [updateLocalAndScheduleGlobal]); 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 canGenerate = section && (() => { switch (focusedSection) { case 1: { const currentSection = section as InteractiveSpeakingExercise; return currentSection.first_title !== "" && currentSection.second_title !== "" && currentSection.prompts.every(prompt => prompt.text !== "") && currentSection.prompts.length > 2; } case 2: { const currentSection = section as SpeakingExercise; return currentSection.title !== "" && currentSection.text !== "" && currentSection.prompts.every(prompt => prompt !== ""); } case 3: { const currentSection = section as InteractiveSpeakingExercise; return currentSection.title !== "" && currentSection.prompts.every(prompt => prompt.text !== ""); } default: return false; } })(); const generateVideoCallback = useCallback((sectionId: number) => { dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "generating", value: "media" } }) generateVideos( section as InteractiveSpeakingExercise | SpeakingExercise, sectionId, selectedAvatar, speakingAvatars ).then((results) => { switch (sectionId) { case 1: case 3: { const interactiveSection = section as InteractiveSpeakingExercise; const updatedPrompts = interactiveSection.prompts.map((prompt, index) => ({ ...prompt, video_url: results[index].url || '' })); dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: [{ prompts: updatedPrompts }] } }) break; } case 2: { if (results[0]?.url) { dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: currentModule, field: "genResult", value: [{ video_url: results[0].url }] } }) } break; } } }).catch((error) => { toast.error("Failed to generate the video, try again later!") }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedAvatar, section]); const submitSpeaking = async () => { 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, isDiagnostic: false, variant: undefined, difficulty, instructorGender: "varied", private: isPrivate, }; const result = await axios.post('/api/exam/speaking', exam); playSound("sent"); toast.success(`Submitted Exam ID: ${result.data.id}`); 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, private: isPrivate, } as SpeakingExam); setExerciseIndex(0); setQuestionIndex(0); setBgColor("bg-white"); openDetachedTab("popout?type=Exam&module=speaking", router) } return ( updateLocalAndScheduleGlobal({ isExerciseDropdownOpen: isOpen }, false)} >
{focusedSection === 1 &&
}
updateLocalAndScheduleGlobal({ isGenerateAudioOpen: isOpen }, false)} >
{selectedAvatar && ( selectedAvatar.gender === 'male' ? ( ) : ( ) )}
); }; export default SpeakingSettings;