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
This commit is contained in:
Yamen Ahmad
2026-04-12 11:00:47 +04:00
parent 38f0464c81
commit 677ba85c77
3 changed files with 123 additions and 2 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
node_modules
dist
.git
.env
.env.local
.env.production
*.md
docs/
.vscode/
coverage/

3
.gitignore vendored
View File

@@ -31,3 +31,6 @@ Thumbs.db
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Vite cache
.vite/

View File

@@ -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<Set<ModuleKey>>(new Set());
const [activeModule, setActiveModule] = useState<ModuleKey | null>(null);
const [moduleStates, setModuleStates] = useState<Record<string, ModuleState>>({});
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() {
<Button variant="outline" disabled={anyGenerating || !title} onClick={() => submitMut.mutate(true)}>
<SkipForward className="h-4 w-4 mr-2" /> Submit module as exam and skip approval
</Button>
<Button variant="outline" disabled>
<Button variant="outline" disabled={!activeModule} onClick={() => setPreviewOpen(true)}>
<Eye className="h-4 w-4 mr-2" /> Preview module
</Button>
</div>
)}
<Dialog open={previewOpen} onOpenChange={setPreviewOpen}>
<DialogContent className="max-w-3xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{title || "Untitled Exam"} Preview</DialogTitle>
<DialogDescription>
Read-only preview of all configured modules and their content.
</DialogDescription>
</DialogHeader>
<div className="space-y-6 pt-2">
{[...selectedModules].map((mod) => {
const st = getModuleState(mod);
return (
<div key={mod} className="border rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-lg capitalize">{mod}</h3>
<div className="flex gap-2">
<Badge variant="outline">{st.timer} min</Badge>
{st.difficulty.map((d) => <Badge key={d} variant="secondary">{d}</Badge>)}
<Badge variant={st.accessType === "public" ? "default" : "outline"}>{st.accessType}</Badge>
</div>
</div>
{mod === "reading" && st.passages.map((p, i) => (
<div key={i} className="bg-muted/50 rounded p-3 space-y-2">
<h4 className="font-medium text-sm">Passage {i + 1} {p.category && `(${p.category})`}</h4>
{p.text ? (
<p className="text-sm whitespace-pre-wrap leading-relaxed">{p.text.slice(0, 500)}{p.text.length > 500 ? "…" : ""}</p>
) : (
<p className="text-sm text-muted-foreground italic">No passage text yet</p>
)}
{p.exercises.length > 0 && (
<p className="text-xs text-muted-foreground">{p.exercises.length} exercise(s) generated</p>
)}
</div>
))}
{mod === "listening" && st.listeningSections.map((s, i) => (
<div key={i} className="bg-muted/50 rounded p-3 space-y-2">
<h4 className="font-medium text-sm">Section {i + 1}: {s.type.replace(/_/g, " ")}</h4>
{s.context ? (
<p className="text-sm whitespace-pre-wrap leading-relaxed">{s.context.slice(0, 500)}{s.context.length > 500 ? "…" : ""}</p>
) : (
<p className="text-sm text-muted-foreground italic">No context yet</p>
)}
{s.audioUrl && <p className="text-xs text-green-600">Audio generated</p>}
{s.exercises.length > 0 && (
<p className="text-xs text-muted-foreground">{s.exercises.length} exercise(s) generated</p>
)}
</div>
))}
{mod === "writing" && st.writingTasks.map((t, i) => (
<div key={i} className="bg-muted/50 rounded p-3 space-y-2">
<div className="flex items-center justify-between">
<h4 className="font-medium text-sm">Task {i + 1}</h4>
<span className="text-xs text-muted-foreground">{t.wordLimit} words · {t.marks} marks</span>
</div>
{t.instructions ? (
<p className="text-sm whitespace-pre-wrap leading-relaxed">{t.instructions}</p>
) : (
<p className="text-sm text-muted-foreground italic">No instructions yet</p>
)}
</div>
))}
{mod === "speaking" && st.speakingParts.map((p, i) => (
<div key={i} className="bg-muted/50 rounded p-3 space-y-2">
<div className="flex items-center justify-between">
<h4 className="font-medium text-sm">Part {i + 1}: {p.type.replace(/_/g, " ")}</h4>
<span className="text-xs text-muted-foreground">{p.marks} marks</span>
</div>
{p.script ? (
<p className="text-sm whitespace-pre-wrap leading-relaxed">{p.script.slice(0, 600)}{p.script.length > 600 ? "…" : ""}</p>
) : (
<p className="text-sm text-muted-foreground italic">No script yet</p>
)}
{p.videoUrl && !p.videoUrl.startsWith("pending:") && (
<p className="text-xs text-green-600">Video ready</p>
)}
{p.videoUrl && p.videoUrl.startsWith("pending:") && (
<p className="text-xs text-orange-600">Video processing</p>
)}
</div>
))}
{st.shuffling && <p className="text-xs text-muted-foreground">Shuffling enabled</p>}
</div>
);
})}
{selectedModules.size === 0 && (
<p className="text-muted-foreground text-center py-8">No modules selected yet.</p>
)}
</div>
</DialogContent>
</Dialog>
</div>
);
}