import { useCallback, useState } from "react"; import { useMutation } from "@tanstack/react-query"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Wand2, BookOpen, Headphones, PenTool, Mic, Layers, Briefcase, ChevronDown, Plus, X, Loader2, RotateCcw, Eye, Send, SkipForward, Upload, FileText, Play, Video, Sparkles, } from "lucide-react"; import AiTipBanner from "@/components/ai/AiTipBanner"; import { generationService } from "@/services/generation.service"; import { mediaService, type Avatar } from "@/services/media.service"; import { useToast } from "@/hooks/use-toast"; type ModuleKey = "reading" | "listening" | "writing" | "speaking" | "level" | "industry"; interface ModuleInfo { key: ModuleKey; label: string; icon: React.ReactNode; color: string; bgColor: string; } const MODULES: ModuleInfo[] = [ { key: "reading", label: "Reading", icon: , color: "text-blue-600", bgColor: "bg-blue-50 border-blue-200" }, { key: "listening", label: "Listening", icon: , color: "text-orange-600", bgColor: "bg-orange-50 border-orange-200" }, { key: "writing", label: "Writing", icon: , color: "text-green-600", bgColor: "bg-green-50 border-green-200" }, { key: "speaking", label: "Speaking", icon: , color: "text-pink-600", bgColor: "bg-pink-50 border-pink-200" }, { key: "level", label: "Level", icon: , color: "text-purple-600", bgColor: "bg-purple-50 border-purple-200" }, { key: "industry", label: "Industry", icon: , color: "text-amber-700", bgColor: "bg-amber-50 border-amber-200" }, ]; const CEFR_LEVELS = ["A1", "A2", "B1", "B2", "C1", "C2"]; const READING_EXERCISE_TYPES = [ { key: "mcq", label: "Multiple Choice" }, { key: "fill_blanks", label: "Fill Blanks" }, { key: "write_blanks", label: "Write Blanks" }, { key: "true_false", label: "True False" }, { key: "paragraph_match", label: "Paragraph Match" }, ]; const LISTENING_SECTION_TYPES = [ { key: "social_conversation", label: "Social Conversation" }, { key: "social_monologue", label: "Social Monologue" }, { key: "academic_discussion", label: "Academic Discussion" }, { key: "academic_monologue", label: "Academic Monologue" }, ]; const LISTENING_EXERCISE_TYPES = [ { key: "mcq", label: "Multiple Choice" }, { key: "write_blanks_questions", label: "Write Blanks: Questions" }, { key: "true_false", label: "True False" }, { key: "write_blanks_fill", label: "Write Blanks: Fill" }, { key: "write_blanks_form", label: "Write Blanks: Form" }, ]; const DEFAULT_AVATARS: Avatar[] = [ { id: "gia", name: "Gia", gender: "female" }, { id: "vadim", name: "Vadim", gender: "male" }, { id: "orhan", name: "Orhan", gender: "male" }, { id: "flora", name: "Flora", gender: "female" }, { id: "scarlett", name: "Scarlett", gender: "female" }, { id: "parker", name: "Parker", gender: "male" }, { id: "ethan", name: "Ethan", gender: "male" }, ]; interface PassageState { text: string; category: string; type: string; divider: string; exerciseTypes: string[]; exercises: unknown[]; editing: boolean; } interface ListeningSectionState { type: string; category: string; divider: string; context: string; audioUrl: string; exerciseTypes: string[]; exercises: unknown[]; editing: boolean; } interface WritingTaskState { instructions: string; category: string; type: string; divider: string; wordLimit: number; marks: number; editing: boolean; } interface SpeakingPartState { type: string; category: string; divider: string; script: string; videoUrl: string; avatarId: string; marks: number; topics: string[]; editing: boolean; } interface ModuleState { timer: number; difficulty: string[]; accessType: string; entity: string; approvalWorkflow: string; rubricGroup: string; rubricCriteria: string; totalMarks: number; gradingSystem: string; shuffling: boolean; passages: PassageState[]; listeningSections: ListeningSectionState[]; writingTasks: WritingTaskState[]; speakingParts: SpeakingPartState[]; } function defaultModuleState(mod: ModuleKey): ModuleState { return { timer: 5, difficulty: [mod === "reading" ? "B2" : mod === "listening" ? "A2" : mod === "writing" ? "A1" : "B1"], accessType: "private", entity: "", approvalWorkflow: "", rubricGroup: "", rubricCriteria: "", totalMarks: 0, gradingSystem: "", shuffling: false, passages: [{ text: "", category: "", type: "general", divider: "", exerciseTypes: [], exercises: [], editing: false }], listeningSections: [{ type: "social_conversation", category: "", divider: "", context: "", audioUrl: "", exerciseTypes: [], exercises: [], editing: false }], writingTasks: [{ instructions: "", category: "", type: "", divider: "", wordLimit: 150, marks: 0, editing: false }], speakingParts: [{ type: "speaking_1", category: "", divider: "", script: "", videoUrl: "", avatarId: "", marks: 0, topics: ["", ""], editing: false }], }; } export default function GenerationPage() { const { toast } = useToast(); const [title, setTitle] = useState(""); const [examLabel, setExamLabel] = useState(""); const [examStructure, setExamStructure] = useState(""); const [selectedModules, setSelectedModules] = useState>(new Set()); const [activeModule, setActiveModule] = useState(null); const [moduleStates, setModuleStates] = useState>({}); const getModuleState = useCallback((mod: ModuleKey): ModuleState => { return moduleStates[mod] ?? defaultModuleState(mod); }, [moduleStates]); const updateModuleState = useCallback((mod: ModuleKey, patch: Partial) => { setModuleStates((prev) => ({ ...prev, [mod]: { ...(prev[mod] ?? defaultModuleState(mod)), ...patch }, })); }, []); const toggleModule = (mod: ModuleKey) => { setSelectedModules((prev) => { const next = new Set(prev); if (next.has(mod)) { next.delete(mod); if (activeModule === mod) setActiveModule(null); } else { next.add(mod); setActiveModule(mod); } return next; }); }; const currentState = activeModule ? getModuleState(activeModule) : null; const generatePassageMut = useMutation({ mutationFn: (params: { index: number; topic: string; difficulty: string; wordCount: number }) => generationService.generatePassage({ topic: params.topic, difficulty: params.difficulty, word_count: params.wordCount, }), onSuccess: (res, vars) => { if (!activeModule) return; const st = getModuleState(activeModule); const passages = [...st.passages]; const r = res as Record; const passageText = (r.passage as string) ?? (r.text as string) ?? JSON.stringify(res); passages[vars.index] = { ...passages[vars.index], text: passageText }; updateModuleState(activeModule, { passages }); toast({ title: "Passage generated", description: `${passageText.length} characters` }); }, onError: (err: Error) => toast({ variant: "destructive", title: "Generation failed", description: err.message }), }); const generateExercisesMut = useMutation({ mutationFn: (params: { module: ModuleKey; passageIndex: number; types: string[]; passageText: string }) => generationService.generate(params.module === "level" || params.module === "industry" ? "reading" : params.module, { topic: params.passageText.slice(0, 200), difficulty: getModuleState(params.module).difficulty[0] ?? "B2", question_count: 5, }), onSuccess: (res, vars) => { const st = getModuleState(vars.module); const items = Array.isArray((res as Record).questions) ? (res as Record).questions as unknown[] : []; if (vars.module === "reading") { const passages = [...st.passages]; passages[vars.passageIndex] = { ...passages[vars.passageIndex], exercises: items }; updateModuleState(vars.module, { passages }); } toast({ title: `${items.length} exercises generated` }); }, onError: (err: Error) => toast({ variant: "destructive", title: "Generation failed", description: err.message }), }); const generateAudioMut = useMutation({ mutationFn: (params: { text: string; sectionIndex: number }) => mediaService.generateListeningAudio({ text: params.text }), onSuccess: (res, vars) => { if (activeModule !== "listening") return; const st = getModuleState("listening"); const sections = [...st.listeningSections]; sections[vars.sectionIndex] = { ...sections[vars.sectionIndex], audioUrl: res.audio_url || "" }; updateModuleState("listening", { listeningSections: sections }); toast({ title: "Audio generated" }); }, onError: (err: Error) => toast({ variant: "destructive", title: "Audio generation failed", description: err.message }), }); const generateWritingMut = useMutation({ mutationFn: (params: { topic: string; difficulty: string; taskIndex: number }) => generationService.generateWritingInstructions({ topic: params.topic, difficulty: params.difficulty, task_type: "letter" }), onSuccess: (res, vars) => { const st = getModuleState("writing"); const tasks = [...st.writingTasks]; const r = res as Record; const instructions = (r.instructions as string) ?? JSON.stringify(r.questions ?? res); tasks[vars.taskIndex] = { ...tasks[vars.taskIndex], instructions }; updateModuleState("writing", { writingTasks: tasks }); toast({ title: "Instructions generated" }); }, onError: (err: Error) => toast({ variant: "destructive", title: "Generation failed", description: err.message }), }); const generateSpeakingMut = useMutation({ mutationFn: (params: { topics: string[]; difficulty: string; partIndex: number }) => generationService.generateSpeakingScript({ topics: params.topics.filter(Boolean), difficulty: params.difficulty, part: "speaking_1" }), onSuccess: (res, vars) => { const st = getModuleState("speaking"); const parts = [...st.speakingParts]; const r = res as Record; const script = (r.script as string) ?? JSON.stringify(r.questions ?? res); parts[vars.partIndex] = { ...parts[vars.partIndex], script }; updateModuleState("speaking", { speakingParts: parts }); toast({ title: "Script generated" }); }, onError: (err: Error) => toast({ variant: "destructive", title: "Generation failed", description: err.message }), }); const generateVideoMut = useMutation({ mutationFn: (params: { script: string; avatarId: string; partIndex: number }) => mediaService.createAvatarVideo({ script: params.script, avatar_id: params.avatarId, title: title || "Speaking Video" }), onSuccess: (res, vars) => { const st = getModuleState("speaking"); const parts = [...st.speakingParts]; parts[vars.partIndex] = { ...parts[vars.partIndex], videoUrl: `pending:${res.video_id}` }; updateModuleState("speaking", { speakingParts: parts }); toast({ title: "Video generation started", description: `Job ID: ${res.video_id}` }); }, onError: (err: Error) => toast({ variant: "destructive", title: "Video generation failed", description: err.message }), }); const submitMut = useMutation({ mutationFn: (skipApproval: boolean) => { const modulesPayload: Record = {}; for (const mod of selectedModules) { const st = getModuleState(mod); modulesPayload[mod] = { timer: st.timer, difficulty: st.difficulty, accessType: st.accessType, shuffling: st.shuffling, gradingSystem: st.gradingSystem, passages: mod === "reading" ? st.passages.map((p) => ({ text: p.text, category: p.category, type: p.type, exercises: p.exercises })) : undefined, sections: mod === "listening" ? st.listeningSections.map((s) => ({ type: s.type, context: s.context, audioUrl: s.audioUrl })) : undefined, tasks: mod === "writing" ? st.writingTasks.map((t) => ({ instructions: t.instructions, wordLimit: t.wordLimit, marks: t.marks })) : undefined, parts: mod === "speaking" ? st.speakingParts.map((p) => ({ type: p.type, script: p.script, videoUrl: p.videoUrl, marks: p.marks })) : undefined, }; } return generationService.submitExam({ title, label: examLabel, modules: modulesPayload, skip_approval: skipApproval }); }, onSuccess: (res) => toast({ title: "Exam submitted", description: `Exam #${res.exam_id} created (${res.status})` }), onError: (err: Error) => toast({ variant: "destructive", title: "Submit failed", description: err.message }), }); const anyGenerating = generatePassageMut.isPending || generateExercisesMut.isPending || generateAudioMut.isPending || generateWritingMut.isPending || generateSpeakingMut.isPending || generateVideoMut.isPending || submitMut.isPending; const renderDifficultyTags = (mod: ModuleKey) => { const st = getModuleState(mod); return (
{st.difficulty.map((d) => ( {d} ))}
); }; const renderCommonConfig = (mod: ModuleKey) => { const st = getModuleState(mod); return (
updateModuleState(mod, { timer: Number(e.target.value) || 1 })} className="h-8 text-xs" />
{renderDifficultyTags(mod)}
{st.totalMarks}
updateModuleState(mod, { shuffling: v })} />
); }; const renderReadingModule = () => { if (!activeModule || activeModule !== "reading") return null; const st = getModuleState("reading"); return (
{renderCommonConfig("reading")}
{st.passages.map((_, i) => (
))}
{st.passages.map((passage, pi) => (

Passage {pi + 1} Settings

Category { const p = [...st.passages]; p[pi] = { ...p[pi], category: e.target.value }; updateModuleState("reading", { passages: p }); }} /> Type Generate Passage { const p = [...st.passages]; p[pi] = { ...p[pi], category: e.target.value }; updateModuleState("reading", { passages: p }); }} /> { const p = [...st.passages]; p[pi] = { ...p[pi], divider: e.target.value }; updateModuleState("reading", { passages: p }); }} /> Add Exercises {READING_EXERCISE_TYPES.map((et) => (
{ const p = [...st.passages]; const types = checked ? [...p[pi].exerciseTypes, et.key] : p[pi].exerciseTypes.filter((t) => t !== et.key); p[pi] = { ...p[pi], exerciseTypes: types }; updateModuleState("reading", { passages: p }); }} />
))}

Reading Passage

The reading passage that the exercises will refer to.