From 677ba85c77b8c933a1e27156d4568a571b4e8bf8 Mon Sep 17 00:00:00 2001 From: Yamen Ahmad Date: Sun, 12 Apr 2026 11:00:47 +0400 Subject: [PATCH] feat: implement Preview module dialog and improve submit toast feedback - Added Preview module dialog showing all configured modules with their content - Preview shows passages, listening sections, writing tasks, speaking parts - Displays module badges (timer, difficulty, access type) - Improved submit toast: shows exam ID, status, and duration (8s) - Preview button now enabled when a module is active - Added .vite to .gitignore, added .dockerignore Made-with: Cursor --- .dockerignore | 10 ++++ .gitignore | 3 + src/pages/GenerationPage.tsx | 112 ++++++++++++++++++++++++++++++++++- 3 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..b5b764aa9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +node_modules +dist +.git +.env +.env.local +.env.production +*.md +docs/ +.vscode/ +coverage/ diff --git a/.gitignore b/.gitignore index 4899f06f6..2b259f7ef 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ Thumbs.db npm-debug.log* yarn-debug.log* yarn-error.log* + +# Vite cache +.vite/ diff --git a/src/pages/GenerationPage.tsx b/src/pages/GenerationPage.tsx index da4709cb1..af366210f 100644 --- a/src/pages/GenerationPage.tsx +++ b/src/pages/GenerationPage.tsx @@ -8,6 +8,13 @@ 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 { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Select, SelectContent, @@ -188,6 +195,7 @@ export default function GenerationPage() { const [selectedModules, setSelectedModules] = useState>(new Set()); const [activeModule, setActiveModule] = useState(null); const [moduleStates, setModuleStates] = useState>({}); + const [previewOpen, setPreviewOpen] = useState(false); const getModuleState = useCallback((mod: ModuleKey): ModuleState => { return moduleStates[mod] ?? defaultModuleState(mod); @@ -384,7 +392,11 @@ export default function GenerationPage() { } 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})` }), + onSuccess: (res) => toast({ + title: "Exam submitted successfully", + description: `Exam #${res.exam_id} created with status "${res.status}". ${res.status === "published" ? "The exam is now live." : "Pending approval."}`, + duration: 8000, + }), onError: (err: Error) => toast({ variant: "destructive", title: "Submit failed", description: err.message }), }); @@ -1056,11 +1068,107 @@ export default function GenerationPage() { - )} + + + + + {title || "Untitled Exam"} — Preview + + Read-only preview of all configured modules and their content. + + +
+ {[...selectedModules].map((mod) => { + const st = getModuleState(mod); + return ( +
+
+

{mod}

+
+ {st.timer} min + {st.difficulty.map((d) => {d})} + {st.accessType} +
+
+ + {mod === "reading" && st.passages.map((p, i) => ( +
+

Passage {i + 1} {p.category && `(${p.category})`}

+ {p.text ? ( +

{p.text.slice(0, 500)}{p.text.length > 500 ? "…" : ""}

+ ) : ( +

No passage text yet

+ )} + {p.exercises.length > 0 && ( +

{p.exercises.length} exercise(s) generated

+ )} +
+ ))} + + {mod === "listening" && st.listeningSections.map((s, i) => ( +
+

Section {i + 1}: {s.type.replace(/_/g, " ")}

+ {s.context ? ( +

{s.context.slice(0, 500)}{s.context.length > 500 ? "…" : ""}

+ ) : ( +

No context yet

+ )} + {s.audioUrl &&

Audio generated

} + {s.exercises.length > 0 && ( +

{s.exercises.length} exercise(s) generated

+ )} +
+ ))} + + {mod === "writing" && st.writingTasks.map((t, i) => ( +
+
+

Task {i + 1}

+ {t.wordLimit} words · {t.marks} marks +
+ {t.instructions ? ( +

{t.instructions}

+ ) : ( +

No instructions yet

+ )} +
+ ))} + + {mod === "speaking" && st.speakingParts.map((p, i) => ( +
+
+

Part {i + 1}: {p.type.replace(/_/g, " ")}

+ {p.marks} marks +
+ {p.script ? ( +

{p.script.slice(0, 600)}{p.script.length > 600 ? "…" : ""}

+ ) : ( +

No script yet

+ )} + {p.videoUrl && !p.videoUrl.startsWith("pending:") && ( +

Video ready

+ )} + {p.videoUrl && p.videoUrl.startsWith("pending:") && ( +

Video processing…

+ )} +
+ ))} + + {st.shuffling &&

Shuffling enabled

} +
+ ); + })} + {selectedModules.size === 0 && ( +

No modules selected yet.

+ )} +
+
+
); }