Module Update

This commit is contained in:
SpecialX
2025-12-30 14:42:30 +08:00
parent f1797265b2
commit e7c902e8e1
148 changed files with 19317 additions and 113 deletions

View File

@@ -0,0 +1,286 @@
"use client"
import { useState, useEffect } from "react"
import { useForm, type SubmitHandler } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { Plus, Trash2, GripVertical } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/components/ui/dialog"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/shared/components/ui/form"
import { Input } from "@/shared/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
import { Textarea } from "@/shared/components/ui/textarea"
import { BaseQuestionSchema } from "../schema"
import { createNestedQuestion } from "../actions"
import { toast } from "sonner"
import { Question } from "../types"
// Extend schema for form usage (e.g. handling options for choice questions)
const QuestionFormSchema = BaseQuestionSchema.extend({
difficulty: z.number().min(1).max(5),
content: z.string().min(1, "Question content is required"),
options: z.array(z.object({
label: z.string(),
value: z.string(),
isCorrect: z.boolean().default(false)
})).optional(),
})
type QuestionFormValues = z.input<typeof QuestionFormSchema>
interface CreateQuestionDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
initialData?: Question | null
}
export function CreateQuestionDialog({ open, onOpenChange, initialData }: CreateQuestionDialogProps) {
const [isPending, setIsPending] = useState(false)
const isEdit = !!initialData
const form = useForm<QuestionFormValues>({
resolver: zodResolver(QuestionFormSchema),
defaultValues: {
type: initialData?.type || "single_choice",
difficulty: initialData?.difficulty || 1,
content: (typeof initialData?.content === 'string' ? initialData.content : "") || "",
options: [
{ label: "Option A", value: "A", isCorrect: true },
{ label: "Option B", value: "B", isCorrect: false },
],
},
})
// Reset form when initialData changes
useEffect(() => {
if (initialData) {
form.reset({
type: initialData.type,
difficulty: initialData.difficulty,
content: typeof initialData.content === 'string' ? initialData.content : JSON.stringify(initialData.content),
options: [
{ label: "Option A", value: "A", isCorrect: true },
{ label: "Option B", value: "B", isCorrect: false },
]
})
} else {
form.reset({
type: "single_choice",
difficulty: 1,
content: "",
options: [
{ label: "Option A", value: "A", isCorrect: true },
{ label: "Option B", value: "B", isCorrect: false },
]
})
}
}, [initialData, form])
const questionType = form.watch("type")
const onSubmit: SubmitHandler<QuestionFormValues> = async (data) => {
setIsPending(true)
try {
const payload = {
type: data.type,
difficulty: data.difficulty,
content: data.content,
knowledgePointIds: [],
}
const fd = new FormData()
fd.set("json", JSON.stringify(payload))
const res = await createNestedQuestion(undefined, fd)
if (res.success) {
toast.success(isEdit ? "Updated question" : "Created question")
onOpenChange(false)
if (!isEdit) {
form.reset()
}
} else {
toast.error(res.message || "Operation failed")
}
} catch (error) {
toast.error("Unexpected error")
} finally {
setIsPending(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{isEdit ? "Edit Question" : "Create New Question"}</DialogTitle>
<DialogDescription>
{isEdit ? "Update question details." : "Add a new question to the bank. Fill in the details below."}
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Question Type</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="single_choice">Single Choice</SelectItem>
<SelectItem value="multiple_choice">Multiple Choice</SelectItem>
<SelectItem value="judgment">True/False</SelectItem>
<SelectItem value="text">Short Answer</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="difficulty"
render={({ field }) => (
<FormItem>
<FormLabel>Difficulty (1-5)</FormLabel>
<Select
onValueChange={(val) => field.onChange(parseInt(val))}
defaultValue={String(field.value)}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select difficulty" />
</SelectTrigger>
</FormControl>
<SelectContent>
{[1, 2, 3, 4, 5].map((level) => (
<SelectItem key={level} value={String(level)}>
{level} - {level === 1 ? "Easy" : level === 5 ? "Hard" : "Medium"}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>Question Content</FormLabel>
<FormControl>
<Textarea
placeholder="Enter the question text here..."
className="min-h-[100px]"
{...field}
/>
</FormControl>
<FormDescription>
Supports basic text. Rich text editor coming soon.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{(questionType === "single_choice" || questionType === "multiple_choice") && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<FormLabel>Options</FormLabel>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => {
const currentOptions = form.getValues("options") || [];
form.setValue("options", [
...currentOptions,
{ label: `Option ${String.fromCharCode(65 + currentOptions.length)}`, value: String.fromCharCode(65 + currentOptions.length), isCorrect: false }
]);
}}
>
<Plus className="mr-2 h-3 w-3" /> Add Option
</Button>
</div>
<div className="space-y-2">
{form.watch("options")?.map((option, index) => (
<div key={index} className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center text-muted-foreground">
<GripVertical className="h-4 w-4" />
</div>
<Input
value={option.label}
onChange={(e) => {
const newOptions = [...(form.getValues("options") || [])];
newOptions[index].label = e.target.value;
form.setValue("options", newOptions);
}}
placeholder={`Option ${index + 1}`}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="text-destructive hover:text-destructive/90"
onClick={() => {
const newOptions = [...(form.getValues("options") || [])];
newOptions.splice(index, 1);
form.setValue("options", newOptions);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
)}
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create Question"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}