From e27efb6282624db300223e131e179bb2c01181b8 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:37:10 +0800 Subject: [PATCH] feat(exams): update question bank list, rich form, and selection toolbar - Update question-bank-list component for exam assembly - Update exam-rich-form for rich text exam editing - Update selection-toolbar for editor selection actions --- .../assembly/question-bank-list.tsx | 57 ++++++++++- .../exams/components/exam-rich-form.tsx | 18 ---- .../exams/editor/selection-toolbar.tsx | 98 +++++++++++++------ 3 files changed, 121 insertions(+), 52 deletions(-) diff --git a/src/modules/exams/components/assembly/question-bank-list.tsx b/src/modules/exams/components/assembly/question-bank-list.tsx index c5866f8..7694ab0 100644 --- a/src/modules/exams/components/assembly/question-bank-list.tsx +++ b/src/modules/exams/components/assembly/question-bank-list.tsx @@ -12,9 +12,50 @@ import { DialogTrigger, } from "@/shared/components/ui/dialog" import { Plus, Sparkles } from "lucide-react" -import type { Question } from "@/modules/questions/types" +import { createId } from "@paralleldrive/cuid2" +import type { Question, QuestionType } from "@/modules/questions/types" +import { QuestionTypeEnum } from "@/modules/questions/schema" import { AiQuestionVariantGenerator } from "@/modules/ai/components/ai-question-variant-generator" import { useAiClientOptional } from "@/modules/ai/context/ai-client-provider" +import type { QuestionVariantResult } from "@/modules/ai/types" + +/** 合法题型集合,用于运行时校验 AI 返回的 type 字符串 */ +const QUESTION_TYPES = new Set(QuestionTypeEnum.options) + +/** + * 类型守卫:校验字符串是否为合法 QuestionType + * + * AI 返回的 type 是 string,需收窄为 QuestionType 联合类型, + * 避免直接使用 as 断言。 + */ +function isQuestionType(value: string): value is QuestionType { + return QUESTION_TYPES.has(value) +} + +/** + * 将 AI 生成的题目变体转换为 Question 对象 + * + * 变体在客户端生成临时 ID(cuid2),加入试卷结构后随试卷保存时持久化。 + * content 结构与题库一致:{ text, options, answer, explanation }。 + */ +function variantToQuestion(variant: QuestionVariantResult): Question { + const type: QuestionType = isQuestionType(variant.type) ? variant.type : "single_choice" + return { + id: createId(), + content: { + text: variant.text, + options: variant.options, + answer: variant.answer, + explanation: variant.explanation, + }, + type, + difficulty: variant.difficulty, + createdAt: new Date(), + updatedAt: new Date(), + author: null, + knowledgePoints: [], + } +} type QuestionBankListProps = { questions: Question[] @@ -46,7 +87,7 @@ function extractQuestionText(content: unknown): string { } } -export function QuestionBankList({ questions, onAdd, isAdded, onLoadMore, hasMore, isLoading, subject }: QuestionBankListProps) { +export function QuestionBankList({ questions, onAdd, isAdded, onLoadMore, hasMore, isLoading, subject }: QuestionBankListProps): React.ReactNode { const aiClient = useAiClientOptional() if (questions.length === 0 && !isLoading) { @@ -104,6 +145,7 @@ export function QuestionBankList({ questions, onAdd, isAdded, onLoadMore, hasMor questionType={q.type} difficulty={q.difficulty} subject={subject} + onAdd={onAdd} /> ) : null}