sync-docs-and-fixes

This commit is contained in:
SpecialX
2026-03-03 17:32:26 +08:00
parent 538805bad0
commit eb08c0ab68
73 changed files with 2218 additions and 422 deletions

View File

@@ -27,6 +27,7 @@ import {
FormMessage,
} from "@/shared/components/ui/form"
import { Input } from "@/shared/components/ui/input"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import {
Select,
SelectContent,
@@ -36,9 +37,9 @@ import {
} from "@/shared/components/ui/select"
import { Textarea } from "@/shared/components/ui/textarea"
import { BaseQuestionSchema } from "../schema"
import { createNestedQuestion, updateQuestionAction } from "../actions"
import { createNestedQuestion, getKnowledgePointOptionsAction, updateQuestionAction } from "../actions"
import { toast } from "sonner"
import { Question } from "../types"
import { KnowledgePointOption, Question } from "../types"
const QuestionFormSchema = BaseQuestionSchema.extend({
difficulty: z.number().min(1).max(5),
@@ -111,6 +112,10 @@ export function CreateQuestionDialog({
const router = useRouter()
const [isPending, setIsPending] = useState(false)
const isEdit = !!initialData
const [knowledgePointOptions, setKnowledgePointOptions] = useState<KnowledgePointOption[]>([])
const [knowledgePointQuery, setKnowledgePointQuery] = useState("")
const [selectedKnowledgePointIds, setSelectedKnowledgePointIds] = useState<string[]>([])
const [isLoadingKnowledgePoints, setIsLoadingKnowledgePoints] = useState(false)
const form = useForm<QuestionFormValues>({
resolver: zodResolver(QuestionFormSchema),
@@ -151,7 +156,60 @@ export function CreateQuestionDialog({
}
}, [initialData, form, open, defaultContent, defaultType])
useEffect(() => {
if (!open) return
setIsLoadingKnowledgePoints(true)
getKnowledgePointOptionsAction()
.then((rows) => {
setKnowledgePointOptions(rows)
})
.catch(() => {
toast.error("Failed to load knowledge points")
})
.finally(() => {
setIsLoadingKnowledgePoints(false)
})
}, [open])
useEffect(() => {
if (!open) return
if (initialData) {
const nextIds = initialData.knowledgePoints.map((kp) => kp.id)
setSelectedKnowledgePointIds((prev) => {
if (prev.length === nextIds.length && prev.every((id, idx) => id === nextIds[idx])) {
return prev
}
return nextIds
})
return
}
setSelectedKnowledgePointIds((prev) => {
if (
prev.length === defaultKnowledgePointIds.length &&
prev.every((id, idx) => id === defaultKnowledgePointIds[idx])
) {
return prev
}
return defaultKnowledgePointIds
})
}, [open, initialData, defaultKnowledgePointIds])
const questionType = form.watch("type")
const filteredKnowledgePoints = knowledgePointOptions.filter((kp) => {
const query = knowledgePointQuery.trim().toLowerCase()
if (!query) return true
const fullLabel = [
kp.textbookTitle,
kp.chapterTitle,
kp.name,
kp.subject,
kp.grade,
]
.filter(Boolean)
.join(" ")
.toLowerCase()
return fullLabel.includes(query)
})
const buildContent = (data: QuestionFormValues) => {
const text = data.content.trim()
@@ -194,7 +252,7 @@ export function CreateQuestionDialog({
type: data.type,
difficulty: data.difficulty,
content: buildContent(data),
knowledgePointIds: isEdit ? [] : defaultKnowledgePointIds,
knowledgePointIds: selectedKnowledgePointIds,
}
const fd = new FormData()
fd.set("json", JSON.stringify(payload))
@@ -306,6 +364,58 @@ export function CreateQuestionDialog({
)}
/>
<div className="space-y-3">
<div className="flex items-center justify-between">
<FormLabel>Knowledge Points</FormLabel>
<span className="text-xs text-muted-foreground">
{selectedKnowledgePointIds.length > 0 ? `${selectedKnowledgePointIds.length} selected` : "Optional"}
</span>
</div>
<Input
placeholder="Search knowledge points..."
value={knowledgePointQuery}
onChange={(e) => setKnowledgePointQuery(e.target.value)}
/>
<div className="rounded-md border">
<ScrollArea className="h-48">
{isLoadingKnowledgePoints ? (
<div className="p-3 text-sm text-muted-foreground">Loading...</div>
) : filteredKnowledgePoints.length === 0 ? (
<div className="p-3 text-sm text-muted-foreground">No knowledge points found.</div>
) : (
<div className="space-y-1 p-2">
{filteredKnowledgePoints.map((kp) => {
const labelParts = [
kp.textbookTitle,
kp.chapterTitle,
kp.name,
].filter(Boolean)
const label = labelParts.join(" · ")
return (
<label key={kp.id} className="flex items-center gap-2 rounded-md px-2 py-1 hover:bg-muted/50">
<Checkbox
checked={selectedKnowledgePointIds.includes(kp.id)}
onCheckedChange={(checked) => {
const isChecked = checked === true
setSelectedKnowledgePointIds((prev) => {
if (isChecked) {
if (prev.includes(kp.id)) return prev
return [...prev, kp.id]
}
return prev.filter((id) => id !== kp.id)
})
}}
/>
<span className="text-sm">{label}</span>
</label>
)
})}
</div>
)}
</ScrollArea>
</div>
</div>
{(questionType === "single_choice" || questionType === "multiple_choice") && (
<div className="space-y-4">
<div className="flex items-center justify-between">