import { useState, useId } from "react"; import { useForm, useFieldArray, useWatch, type Control, type UseFormReturn } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { useNavigate } from "react-router-dom"; import { ChevronDown, ChevronUp, Plus, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Slider } from "@/components/ui/slider"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useCreateCustomExam, useSaveAsTemplate } from "@/hooks/queries/useExamTemplates"; import { useSubjects } from "@/hooks/queries/useAdaptive"; import type { CustomExamCreateRequest, CustomScoringMethod } from "@/types"; import { toast } from "sonner"; const sectionSchema = z.object({ title: z.string().min(1), skill: z.string().optional(), question_count: z.coerce.number().min(1), time_limit_min: z.coerce.number().optional(), scoring_method: z.enum(["auto", "rubric", "mixed"]), sequence: z.number(), question_ids: z.array(z.number()), }); const formSchema = z.object({ title: z.string().min(2), subject_id: z.coerce.number().min(1), description: z.string().optional(), total_time_min: z.coerce.number().min(1), pass_threshold: z.coerce.number().min(0).max(100), results_release_mode: z.enum(["auto", "manual_approval"]), randomize: z.boolean(), sections: z.array(sectionSchema).min(1), save_as_template: z.boolean(), template_name: z.string().optional(), }); type FormValues = z.infer; const SKILL_OPTIONS = ["Reading", "Listening", "Writing", "Speaking", "Mixed"]; export default function CustomExamCreate() { const navigate = useNavigate(); const [step, setStep] = useState(1); const subjectsQ = useSubjects(); const createExam = useCreateCustomExam(); const saveTemplate = useSaveAsTemplate(); const subjects = subjectsQ.data ?? []; const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { title: "", subject_id: 0, description: "", total_time_min: 60, pass_threshold: 50, results_release_mode: "auto", randomize: false, sections: [ { title: "Section A", skill: "Reading", question_count: 10, time_limit_min: 20, scoring_method: "auto", sequence: 0, question_ids: [], }, ], save_as_template: false, template_name: "", }, }); const { fields, append, remove, move } = useFieldArray({ control: form.control, name: "sections" }); const validateStep = async (s: number) => { if (s === 1) { return form.trigger(["title", "subject_id", "total_time_min", "pass_threshold", "results_release_mode", "randomize"]); } if (s === 2) { return form.trigger("sections"); } if (s === 3) { return form.trigger("sections"); } return true; }; const next = async () => { const ok = await validateStep(step); if (ok) setStep((x) => Math.min(4, x + 1)); }; const back = () => setStep((x) => Math.max(1, x - 1)); const buildPayload = (): CustomExamCreateRequest => { const v = form.getValues(); return { title: v.title, subject_id: v.subject_id, description: v.description, total_time_min: v.total_time_min, pass_threshold: v.pass_threshold, results_release_mode: v.results_release_mode, randomize: v.randomize, sections: v.sections.map((sec, i) => ({ title: sec.title, skill: sec.skill, question_count: sec.question_count, time_limit_min: sec.time_limit_min, scoring_method: sec.scoring_method as CustomScoringMethod, sequence: i, question_ids: sec.question_ids, })), }; }; const onSaveDraft = async () => { try { const payload = buildPayload(); await createExam.mutateAsync(payload); toast.success("Draft saved."); } catch { toast.error("Could not save."); } }; const onPublish = async () => { try { const payload = buildPayload(); const res = await createExam.mutateAsync(payload); const raw = res as { exam_id?: number; data?: { exam_id?: number } }; const id = raw.exam_id ?? raw.data?.exam_id; if (form.getValues("save_as_template")) { const name = form.getValues("template_name")?.trim() || form.getValues("title"); await saveTemplate.mutateAsync({ name, structure: { sections: buildPayload().sections }, editable: true, type: "custom", }); } toast.success("Exam published."); if (id != null) navigate(`/admin/exam`); } catch { toast.error("Publish failed."); } }; return (

Custom exam

Build sections, attach questions, then review.

{[1, 2, 3, 4].map((n) => ( ))}
{step === 1 ? ( Properties Basics and scoring policy. ( Title )} /> ( Subject )} /> ( Description