From 1d7ebea2af392db7dfc399af0641b7716350b50e Mon Sep 17 00:00:00 2001 From: Yamen Ahmad Date: Mon, 13 Apr 2026 12:27:18 +0400 Subject: [PATCH] feat(generation): add Listening section selector, exercise wizard, and edit/preview support - Replace section list with checkbox-based section type selector (Social Conversation, Social Monologue, Academic Discussion, Academic Monologue) matching old platform - Add Set Up Exercises wizard for Listening sections with listening-specific exercise types - Add exercise display with edit/delete/clear-all for each listening section - Add Save/Discard/Edit buttons and read-only mode for listening context textarea - Generalize exercise wizard and edit dialog to work with both Reading and Listening modules Made-with: Cursor --- src/pages/GenerationPage.tsx | 1026 ++++++++++++++++++++++++++++++---- 1 file changed, 911 insertions(+), 115 deletions(-) diff --git a/src/pages/GenerationPage.tsx b/src/pages/GenerationPage.tsx index f49a81c..bfe4814 100644 --- a/src/pages/GenerationPage.tsx +++ b/src/pages/GenerationPage.tsx @@ -104,6 +104,31 @@ const LISTENING_EXERCISE_TYPES = [ { key: "write_blanks_form", label: "Write Blanks: Form" }, ]; +const WRITING_TYPES = [ + { key: "general", label: "General" }, + { key: "letter", label: "Letter" }, + { key: "essay", label: "Essay" }, + { key: "report", label: "Report" }, +]; + +const LEVEL_EXERCISE_TYPES = [ + { key: "mc_blank", label: "Multiple Choice: Blank Space" }, + { key: "mc_underline", label: "Multiple Choice: Underlined" }, + { key: "fill_blanks", label: "Fill Blanks" }, + { key: "fill_blanks_mc", label: "Fill Blanks: Multiple Choice" }, + { key: "verb_tense", label: "Verb Tense" }, +]; + +const INDUSTRY_EXERCISE_TYPES = [ + { key: "mc_blank_space", label: "Multiple Choice: Blank Space" }, + { key: "mc_normal", label: "Multiple Choice: Normal" }, + { key: "fill_blanks", label: "Fill Blanks" }, + { key: "true_false", label: "True/False" }, + { key: "technical_writing", label: "Technical Writing" }, +]; + +const INDUSTRY_DIFFICULTY_LEVELS = ["Beginner", "Intermediate", "Advanced"]; + const DEFAULT_AVATARS: Avatar[] = [ { id: "gia", name: "Gia", gender: "female" }, { id: "vadim", name: "Vadim", gender: "male" }, @@ -153,6 +178,8 @@ interface WritingTaskState { divider: string; wordLimit: number; marks: number; + graded: boolean; + images: string[]; editing: boolean; } @@ -168,6 +195,25 @@ interface SpeakingPartState { editing: boolean; } +interface LevelPartState { + category: string; + divider: string; + levelExerciseTypes: string[]; + levelExercises: Exercise[]; + readingPassages: PassageState[]; + listeningSections: ListeningSectionState[]; + writingTasks: WritingTaskState[]; + speakingParts: SpeakingPartState[]; +} + +interface IndustryPartState { + category: string; + divider: string; + exerciseTypes: string[]; + exercises: Exercise[]; + attachments: string[]; +} + interface ModuleState { timer: number; difficulty: string[]; @@ -179,16 +225,49 @@ interface ModuleState { totalMarks: number; gradingSystem: string; shuffling: boolean; + numberOfParts: number; passages: PassageState[]; listeningSections: ListeningSectionState[]; writingTasks: WritingTaskState[]; speakingParts: SpeakingPartState[]; + levelParts: LevelPartState[]; + industryParts: IndustryPartState[]; +} + +function defaultPassage(): PassageState { + return { text: "", category: "", type: "general", divider: "", exerciseTypes: [], exercises: [], editing: false }; +} +function defaultListeningSection(): ListeningSectionState { + return { type: "social_conversation", category: "", divider: "", context: "", audioUrl: "", exerciseTypes: [], exercises: [], editing: false }; +} +function defaultWritingTask(): WritingTaskState { + return { instructions: "", category: "", type: "general", divider: "", wordLimit: 150, marks: 0, graded: true, images: [], editing: false }; +} +function defaultSpeakingPart(type = "speaking_1"): SpeakingPartState { + return { type, category: "", divider: "", script: "", videoUrl: "", avatarId: "", marks: 0, topics: ["", ""], editing: false }; +} +function defaultLevelPart(): LevelPartState { + return { + category: "", divider: "", + levelExerciseTypes: [], levelExercises: [], + readingPassages: [defaultPassage(), defaultPassage(), defaultPassage()], + listeningSections: [defaultListeningSection(), defaultListeningSection(), defaultListeningSection(), defaultListeningSection()], + writingTasks: [defaultWritingTask(), { ...defaultWritingTask(), type: "essay", wordLimit: 250 }], + speakingParts: [defaultSpeakingPart("speaking_1"), defaultSpeakingPart("speaking_2"), defaultSpeakingPart("interactive")], + }; +} +function defaultIndustryPart(): IndustryPartState { + return { category: "", divider: "", exerciseTypes: [], exercises: [], attachments: [] }; } function defaultModuleState(mod: ModuleKey): ModuleState { + const diffMap: Record = { + reading: ["B2"], listening: ["A2"], writing: ["B2"], speaking: ["B1"], + level: ["A2"], industry: ["Intermediate"], + }; return { timer: 5, - difficulty: [mod === "reading" ? "B2" : mod === "listening" ? "A2" : mod === "writing" ? "A1" : "B1"], + difficulty: diffMap[mod] || ["B2"], accessType: "private", entity: "", approvalWorkflow: "", @@ -197,10 +276,13 @@ function defaultModuleState(mod: ModuleKey): ModuleState { 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 }], + numberOfParts: 1, + passages: [defaultPassage()], + listeningSections: [defaultListeningSection()], + writingTasks: [defaultWritingTask()], + speakingParts: [defaultSpeakingPart()], + levelParts: [defaultLevelPart()], + industryParts: [defaultIndustryPart()], }; } @@ -213,17 +295,19 @@ export default function GenerationPage() { const [activeModule, setActiveModule] = useState(null); const [moduleStates, setModuleStates] = useState>({}); const [previewOpen, setPreviewOpen] = useState(false); - const [editingExercise, setEditingExercise] = useState<{ passageIndex: number; exerciseIndex: number } | null>(null); + const [editingExercise, setEditingExercise] = useState<{ module: "reading" | "listening"; itemIndex: number; exerciseIndex: number } | null>(null); const [exerciseDraft, setExerciseDraft] = useState(null); const [exerciseWizard, setExerciseWizard] = useState<{ open: boolean; - passageIndex: number; - step: number; // 0 = configure, 1 = generating/done + module: "reading" | "listening"; + itemIndex: number; + step: number; types: Record; }>({ open: false, - passageIndex: 0, + module: "reading", + itemIndex: 0, step: 0, types: {}, }); @@ -234,13 +318,19 @@ export default function GenerationPage() { write_blanks: "Fill in each blank with a suitable word or phrase based on the passage.", true_false: "Read each statement and decide whether it is True, False, or Not Given based on the passage.", paragraph_match: "Match each heading or statement to the correct paragraph from the passage.", + write_blanks_questions: "Listen to the audio and write the correct answers to the questions.", + write_blanks_fill: "Listen to the audio and fill in the missing words.", + write_blanks_form: "Listen to the audio and complete the form with the correct information.", }; - const openExerciseWizard = (pi: number) => { - const st = getModuleState("reading"); - const existing = st.passages[pi]?.exerciseTypes ?? []; + const openExerciseWizard = (pi: number, mod: "reading" | "listening" = "reading") => { + const st = getModuleState(mod); + const exerciseTypeList = mod === "listening" ? LISTENING_EXERCISE_TYPES : READING_EXERCISE_TYPES; + const existing = mod === "listening" + ? st.listeningSections[pi]?.exerciseTypes ?? [] + : st.passages[pi]?.exerciseTypes ?? []; const types: Record = {}; - for (const et of READING_EXERCISE_TYPES) { + for (const et of exerciseTypeList) { types[et.key] = { enabled: existing.includes(et.key) || existing.length === 0, count: 5, @@ -248,7 +338,7 @@ export default function GenerationPage() { instructions: DEFAULT_INSTRUCTIONS[et.key] || "", }; } - setExerciseWizard({ open: true, passageIndex: pi, step: 0, types }); + setExerciseWizard({ open: true, module: mod, itemIndex: pi, step: 0, types }); }; const wizardTotalQuestions = Object.values(exerciseWizard.types).reduce( @@ -352,6 +442,14 @@ export default function GenerationPage() { exerciseTypes: vars.types, }; updateModuleState(vars.module, { passages }); + } else if (vars.module === "listening") { + const sections = [...st.listeningSections]; + sections[vars.passageIndex] = { + ...sections[vars.passageIndex], + exercises: [...sections[vars.passageIndex].exercises, ...items], + exerciseTypes: vars.types, + }; + updateModuleState(vars.module, { listeningSections: sections }); } setExerciseWizard((prev) => ({ ...prev, step: 1 })); toast({ title: `${items.length} exercises generated` }); @@ -675,7 +773,7 @@ export default function GenerationPage() { - )} + +
+ {LISTENING_SECTION_TYPES.map((sType) => ( +
+ { + if (checked) { + const newS = defaultListeningSection(); + newS.type = sType.key; + updateModuleState("listening", { listeningSections: [...st.listeningSections, newS] }); + } else { + const filtered = st.listeningSections.filter((s) => s.type !== sType.key); + if (filtered.length === 0) return; + updateModuleState("listening", { listeningSections: filtered }); + } + }} + /> +
))}
+
+ + +
+
{st.listeningSections.map((sec, si) => (

{LISTENING_SECTION_TYPES.find((t) => t.key === sec.type)?.label} Settings

+ + + Category + + + { + const sections = [...st.listeningSections]; sections[si] = { ...sections[si], category: e.target.value }; + updateModuleState("listening", { listeningSections: sections }); + }} /> + + + + + Divider + + + + + Audio Context @@ -883,26 +1030,6 @@ export default function GenerationPage() {
- - - Add Exercises - - - {LISTENING_EXERCISE_TYPES.map((et) => ( -
- { - const sections = [...st.listeningSections]; - const types = checked ? [...sections[si].exerciseTypes, et.key] : sections[si].exerciseTypes.filter((t) => t !== et.key); - sections[si] = { ...sections[si], exerciseTypes: types }; - updateModuleState("listening", { listeningSections: sections }); - }} /> - -
- ))} -
-
Generate Audio @@ -921,20 +1048,139 @@ export default function GenerationPage() { )} + -

{LISTENING_SECTION_TYPES.find((t) => t.key === sec.type)?.label}

+
+

{LISTENING_SECTION_TYPES.find((t) => t.key === sec.type)?.label}

+
+ + + +
+

Enter the section's conversation or import your own