import useExamEditorStore from "@/stores/examEditor"; import { LevelSectionSettings, SpeakingSectionSettings } from "@/stores/examEditor/types"; import { useCallback, useEffect, useState } from "react"; import { generate } from "../Shared/Generate"; import Dropdown from "../Shared/SettingsDropdown"; import Input from "@/components/Low/Input"; import GenerateBtn from "../Shared/GenerateBtn"; import clsx from "clsx"; import { FaFemale, FaMale } from "react-icons/fa"; import { Difficulty, InteractiveSpeakingExercise, LevelPart, SpeakingExercise } from "@/interfaces/exam"; import { toast } from "react-toastify"; import { generateVideos } from "../Shared/generateVideos"; import { Module } from "@/interfaces"; import useCanGenerate from "./useCanGenerate"; import ReactSelect, { components } from "react-select"; import { capitalize } from "lodash"; import Option from "@/interfaces/option"; import { MdSignalCellularAlt } from "react-icons/md"; export interface Avatar { name: string; gender: string; } interface Props { localSettings: SpeakingSectionSettings | LevelSectionSettings; updateLocalAndScheduleGlobal: (updates: Partial, schedule?: boolean) => void; section: SpeakingExercise | InteractiveSpeakingExercise | LevelPart; level?: boolean; module?: Module; id?: string; sectionId?: number; } const SpeakingComponents: React.FC = ({ localSettings, updateLocalAndScheduleGlobal, section, level = false, module = "speaking", id, sectionId }) => { const { currentModule, speakingAvatars, dispatch, modules } = useExamEditorStore(); const { focusedSection, difficulty, sections } = useExamEditorStore((store) => store.modules[level ? "level" : currentModule]) const state = sections.find((s) => s.sectionId === sectionId); const [selectedAvatar, setSelectedAvatar] = useState(null); const randomDiff = difficulty.length === 1 ? capitalize(difficulty[0]) : `Random (${difficulty.map(dif => capitalize(dif)).join(", ")})` as Difficulty; const DIFFICULTIES = difficulty.length === 1 ? ["A1", "A2", "B1", "B2", "C1", "C2"] : ["A1", "A2", "B1", "B2", "C1", "C2", randomDiff]; const difficultyOptions: Option[] = DIFFICULTIES.map(level => ({ label: level, value: level })); const [specificDiff, setSpecificDiff] = useState(randomDiff); const generateScript = useCallback((scriptSectionId: number) => { const queryParams: { difficulty: string[]; first_topic?: string; second_topic?: string; topic?: string; } = { difficulty }; if (scriptSectionId === 1) { if (localSettings.speakingTopic) { queryParams['first_topic'] = localSettings.speakingTopic; } if (localSettings.speakingSecondTopic) { queryParams['second_topic'] = localSettings.speakingSecondTopic; } } else { if (localSettings.speakingTopic) { queryParams['topic'] = localSettings.speakingTopic; } } generate( level ? section.sectionId! : focusedSection, "speaking", `${id ? `${id}-` : ''}speakingScript`, { method: 'GET', queryParams }, (data: any) => { switch (level ? section.sectionId! : focusedSection) { case 1: return [{ prompts: data.questions, first_topic: data.first_topic, second_topic: data.second_topic, difficulty: specificDiff.length == 2 ? specificDiff : difficulty, }]; case 2: return [{ topic: data.topic, question: data.question, prompts: data.prompts, suffix: data.suffix, difficulty: specificDiff.length == 2 ? specificDiff : difficulty, }]; case 3: return [{ title: data.topic, prompts: data.questions, difficulty: specificDiff.length == 2 ? specificDiff : difficulty, }]; default: return [data]; } }, sectionId, level ); }, [difficulty, level, section.sectionId, focusedSection, id, sectionId, localSettings.speakingTopic, localSettings.speakingSecondTopic, specificDiff]); const onTopicChange = useCallback((speakingTopic: string) => { updateLocalAndScheduleGlobal({ speakingTopic }); }, [updateLocalAndScheduleGlobal]); const onSecondTopicChange = useCallback((speakingSecondTopic: string) => { updateLocalAndScheduleGlobal({ speakingSecondTopic }); }, [updateLocalAndScheduleGlobal]); const canGenerate = useCanGenerate({ section, sections, id, focusedSection }); useEffect(() => { if (!canGenerate) { updateLocalAndScheduleGlobal({ isGenerateVideoOpen: false }, false); } }, [canGenerate, updateLocalAndScheduleGlobal]); const generateVideoCallback = useCallback((sectionId: number) => { if (level) { dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, module: "level", field: "levelGenerating", value: [...state!.levelGenerating, `${id ? `${id}-` : ''}video`] } }) } else { dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId: focusedSection, module: "speaking", field: "generating", value: "video" } }) } generateVideos( section as InteractiveSpeakingExercise | SpeakingExercise, level ? section.sectionId! : focusedSection, selectedAvatar, speakingAvatars ).then((results) => { switch (level ? section.sectionId! : focusedSection) { case 1: case 3: { const interactiveSection = section as InteractiveSpeakingExercise; const updatedPrompts = interactiveSection.prompts.map((prompt, index) => ({ ...prompt, video_url: results[index].url || '' })); if (level) { dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, field: "levelGenResults", value: [...state!.levelGenResults, { generating: `${id ? `${id}-` : ''}video`, result: [{ prompts: updatedPrompts }] }], module: "level" } }) } else { dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId: focusedSection, module: "speaking", field: "genResult", value: { generating: "video", result: [{ prompts: updatedPrompts }], module: module } } }) } break; } case 2: { if (results[0]?.url) { if (level) { dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId, field: "levelGenResults", value: [...state!.levelGenResults, { generating: `${id ? `${id}-` : ''}video`, result: [{ video_url: results[0].url }] }], module: "level" } }) } else { dispatch({ type: "UPDATE_SECTION_SINGLE_FIELD", payload: { sectionId: focusedSection, module, field: "genResult", value: { generating: 'video', result: [{ video_url: results[0].url }], module: "speaking" } } }) } } break; } } }).catch((error) => { toast.error("Failed to generate the video, try again later!") }); }, [level, section, focusedSection, selectedAvatar, speakingAvatars, dispatch, module, state, id]); const secId = level ? section.sectionId! : focusedSection; return ( <> updateLocalAndScheduleGlobal({ isSpeakingTopicOpen: isOpen }, false)} contentWrapperClassName={level ? `border border-ielts-speaking` : ''} >
{secId === 1 &&
}
opt.value === specificDiff)} onChange={(value) => setSpecificDiff(value!.value as Difficulty)} menuPortalTarget={document?.body} components={{ IndicatorSeparator: null, ValueContainer: ({ children, ...props }) => (
{children}
) }} styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }), control: (styles) => ({ ...styles, minHeight: '50px', border: '1px solid #e5e7eb', borderRadius: '0.5rem', boxShadow: 'none', backgroundColor: 'white', cursor: 'pointer', '&:hover': { border: '1px solid #e5e7eb', } }), valueContainer: (styles) => ({ ...styles, padding: '0 8px', display: 'flex', alignItems: 'center' }), input: (styles) => ({ ...styles, margin: '0', padding: '0' }), dropdownIndicator: (styles) => ({ ...styles, padding: '8px' }), option: (styles, state) => ({ ...styles, backgroundColor: state.isFocused ? "#D5D9F0" : state.isSelected ? "#7872BF" : "white", color: state.isFocused ? "black" : styles.color, }), }} className="text-sm" />
updateLocalAndScheduleGlobal({ isGenerateVideoOpen: isOpen }, false)} contentWrapperClassName={level ? `border border-ielts-speaking` : ''} >
{selectedAvatar && ( selectedAvatar.gender === 'male' ? ( ) : ( ) )}
); }; export default SpeakingComponents;