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:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
*.md
|
||||
docs/
|
||||
.vscode/
|
||||
coverage/
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -31,3 +31,6 @@ Thumbs.db
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Vite cache
|
||||
.vite/
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user