diff --git a/src/app/(dashboard)/teacher/exams/create/page.tsx b/src/app/(dashboard)/teacher/exams/create/page.tsx index d417bb5..061cfb5 100644 --- a/src/app/(dashboard)/teacher/exams/create/page.tsx +++ b/src/app/(dashboard)/teacher/exams/create/page.tsx @@ -1,18 +1,18 @@ import type { JSX } from "react" +import { getTranslations } from "next-intl/server" import { ExamForm } from "@/modules/exams/components/exam-form" export const dynamic = "force-dynamic" -export default function CreateExamPage(): JSX.Element { +export default async function CreateExamPage(): Promise { + const t = await getTranslations("examHomework") return ( -
-
-
-

Create Exam

-

Configure a new exam for your classes.

-
- +
+
+

{t("exam.form.createTitle")}

+

{t("exam.form.createDescription")}

+
) } diff --git a/src/modules/exams/components/exam-basic-info-form.tsx b/src/modules/exams/components/exam-basic-info-form.tsx index f8249cb..3745f57 100644 --- a/src/modules/exams/components/exam-basic-info-form.tsx +++ b/src/modules/exams/components/exam-basic-info-form.tsx @@ -1,5 +1,6 @@ "use client" +import { useTranslations } from "next-intl" import type { Control } from "react-hook-form" import { Card, @@ -20,14 +21,6 @@ type ExamBasicInfoFormProps = { loadingGrades: boolean } -const DIFFICULTY_OPTIONS = [ - { value: "1", label: "Level 1 (Easy)" }, - { value: "2", label: "Level 2" }, - { value: "3", label: "Level 3 (Medium)" }, - { value: "4", label: "Level 4" }, - { value: "5", label: "Level 5 (Hard)" }, -] - export function ExamBasicInfoForm({ control, subjects, @@ -35,36 +28,46 @@ export function ExamBasicInfoForm({ loadingSubjects, loadingGrades, }: ExamBasicInfoFormProps) { + const t = useTranslations("examHomework") + + const DIFFICULTY_OPTIONS = [ + { value: "1", label: t("exam.form.difficultyLevel1") }, + { value: "2", label: t("exam.form.difficultyLevel2") }, + { value: "3", label: t("exam.form.difficultyLevel3") }, + { value: "4", label: t("exam.form.difficultyLevel4") }, + { value: "5", label: t("exam.form.difficultyLevel5") }, + ] + return ( - Exam Details + {t("exam.form.detailsTitle")} - Define the core information for your exam. + {t("exam.form.detailsDesc")}
({ value: s.id, label: s.name }))} disabled={loadingSubjects} /> ({ value: g.id, label: g.name }))} disabled={loadingGrades} /> @@ -74,20 +77,20 @@ export function ExamBasicInfoForm({
@@ -95,9 +98,9 @@ export function ExamBasicInfoForm({
diff --git a/src/modules/exams/components/exam-form-types.ts b/src/modules/exams/components/exam-form-types.ts index 46ebe4e..127ecfd 100644 --- a/src/modules/exams/components/exam-form-types.ts +++ b/src/modules/exams/components/exam-form-types.ts @@ -11,7 +11,7 @@ export const formSchema = z.object({ totalScore: z.coerce.number().min(1, "Total score must be at least 1.").optional(), durationMin: z.coerce.number().min(10, "Duration must be at least 10 minutes.").optional(), scheduledAt: z.string().optional(), - mode: z.enum(["manual", "ai"]), + mode: z.enum(["manual", "ai", "rich"]), aiSourceText: z.string().optional(), aiQuestionCount: z.coerce.number().min(1).max(200).optional(), aiProviderId: z.string().optional(), @@ -23,6 +23,9 @@ export const formSchema = z.object({ lateStartGraceMinutes: z.coerce.number().int().min(0).default(0), antiCheatEnabled: z.boolean().default(false), }).superRefine((data, ctx) => { + // 富文本模式不需要表单校验,直接跳转到 /teacher/exams/new + if (data.mode === "rich") return + // 监考模式必须设置考试时长 if (data.examMode === "proctored" && (data.durationMinutes === null || data.durationMinutes === undefined || data.durationMinutes < 1)) { ctx.addIssue({ diff --git a/src/modules/exams/components/exam-form.tsx b/src/modules/exams/components/exam-form.tsx index 5cd13fd..212ed63 100644 --- a/src/modules/exams/components/exam-form.tsx +++ b/src/modules/exams/components/exam-form.tsx @@ -2,6 +2,7 @@ import { useTransition, useEffect, useState, type FormEvent } from "react" import { useRouter } from "next/navigation" +import { useTranslations } from "next-intl" import { useForm, type Resolver } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { toast } from "sonner" @@ -24,6 +25,7 @@ export type { ExamFormValues } from "./exam-form-types" export function ExamForm() { const router = useRouter() + const t = useTranslations("examHomework") const [isPending, startTransition] = useTransition() const [subjects, setSubjects] = useState<{ id: string; name: string }[]>([]) const [loadingSubjects, setLoadingSubjects] = useState(true) @@ -56,7 +58,7 @@ export function ExamForm() { form.setValue("subject", subjectsResult.data[0].id) } } else { - toast.error("Failed to load subjects") + toast.error(t("exam.form.loadSubjectsFailed")) } if (gradesResult.success && gradesResult.data) { @@ -65,7 +67,7 @@ export function ExamForm() { form.setValue("grade", gradesResult.data[0].id) } } else { - toast.error("Failed to load grades") + toast.error(t("exam.form.loadGradesFailed")) } if (aiProvidersResult.success && aiProvidersResult.data) { setAiProviders(aiProvidersResult.data) @@ -79,7 +81,7 @@ export function ExamForm() { } } catch (error) { console.error(error) - toast.error("Failed to load form data") + toast.error(t("exam.form.loadFormFailed")) } finally { setLoadingSubjects(false) setLoadingGrades(false) @@ -87,10 +89,11 @@ export function ExamForm() { } } void fetchMetadata() - }, [form]) + }, [form, t]) const mode = form.watch("mode") const isAiMode = mode === "ai" + const isRichMode = mode === "rich" const previewSubject = form.watch("subject") const previewGrade = form.watch("grade") const previewDuration = form.watch("durationMin") ?? 90 @@ -98,6 +101,12 @@ export function ExamForm() { const previewTitleValue = form.watch("title") function onSubmit(data: ExamFormValues) { + // 富文本模式不通过表单提交,直接跳转 + if (data.mode === "rich") { + router.push("/teacher/exams/new") + return + } + const resolvedTitle = data.mode === "ai" && !data.title?.trim() ? "AI Exam" : data.title?.trim() ?? "" const resolvedSubject = data.subject?.trim() || subjects[0]?.id || "" const resolvedGrade = data.grade?.trim() || grades[0]?.id || "" @@ -106,13 +115,13 @@ export function ExamForm() { const resolvedDurationMin = typeof data.durationMin === "number" && data.durationMin > 0 ? data.durationMin : 90 if (data.mode === "ai" && (!resolvedSubject || !resolvedGrade)) { - toast.error("Missing subject or grade configuration") + toast.error(t("exam.form.missingSubjectOrGrade")) return } if (data.mode === "ai") { const signature = preview.buildPreviewSignature(data) if (!preview.previewSignature || signature !== preview.previewSignature || preview.previewNodes.length === 0) { - toast.error("Please preview and confirm before creating") + toast.error(t("exam.form.previewBeforeCreate")) return } } @@ -153,17 +162,21 @@ export function ExamForm() { : await createExamAction(null, formData) if (result.success && result.data) { - toast.success("Exam draft created", { - description: "Redirecting to exam builder...", + toast.success(t("exam.form.createSuccess"), { + description: t("exam.form.redirecting"), }) router.push(`/teacher/exams/${result.data}/build`) } else { - toast.error(result.message || "Failed to create exam") + toast.error(result.message || t("exam.form.createFailed")) } }) } const handleCreateClick = () => { + if (isRichMode) { + router.push("/teacher/exams/new") + return + } if (isAiMode) { preview.handleBackgroundPreview() return @@ -198,10 +211,20 @@ export function ExamForm() { } return ( -
-
- - {!isAiMode && ( +
+ {/* 顶部: 创建方式选择 */} + form.setValue("mode", value)} + isPending={isPending} + handleCreateClick={handleCreateClick} + /> + + {/* 富文本模式: 不显示下方表单 */} + {isRichMode ? null : ( + + + {/* 基本信息(始终可见) */} - )} - {isAiMode && ( - - )} - - - + + {/* AI 模式: 显示 AI 生成器 */} + {isAiMode && ( + + )} + + {/* 考试模式配置(始终可见) */} + + + + )} - -
- form.setValue("mode", value)} - isPending={isPending} - handleCreateClick={handleCreateClick} - /> -
) } diff --git a/src/modules/exams/components/exam-mode-selector.tsx b/src/modules/exams/components/exam-mode-selector.tsx index f58b125..df02562 100644 --- a/src/modules/exams/components/exam-mode-selector.tsx +++ b/src/modules/exams/components/exam-mode-selector.tsx @@ -1,20 +1,16 @@ "use client" -import { Loader2, Sparkles, BookOpen } from "lucide-react" +import { useRouter } from "next/navigation" +import { useTranslations } from "next-intl" +import { Loader2, Sparkles, BookOpen, FileText, ArrowRight } from "lucide-react" import { cn } from "@/shared/lib/utils" import { Button } from "@/shared/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/shared/components/ui/card" + +export type ExamCreationMode = "manual" | "ai" | "rich" type ExamModeSelectorProps = { - mode: "manual" | "ai" - setMode: (mode: "manual" | "ai") => void + mode: ExamCreationMode + setMode: (mode: ExamCreationMode) => void isPending: boolean handleCreateClick: () => void } @@ -25,61 +21,112 @@ export function ExamModeSelector({ isPending, handleCreateClick, }: ExamModeSelectorProps) { - return ( - - - Assembly Mode - - Choose how to build the exam structure. - - - -
- + const router = useRouter() + const t = useTranslations("examHomework") - + ) + })} +
+ + {/* 富文本模式: 显示跳转按钮 */} + {mode === "rich" && ( +
+

+ {t("exam.form.modeRichHint")} +

+ + + {t("exam.form.modeRichAction")} + +
-
- - - -
+ )} + + {/* 手动/AI 模式: 显示创建按钮 */} + {mode !== "rich" && ( +
+ +
+ )} +
) } diff --git a/src/shared/i18n/messages/en/exam-homework.json b/src/shared/i18n/messages/en/exam-homework.json index d651beb..9ab4b84 100644 --- a/src/shared/i18n/messages/en/exam-homework.json +++ b/src/shared/i18n/messages/en/exam-homework.json @@ -32,7 +32,32 @@ "createFailed": "Failed to create exam", "loadFormFailed": "Failed to load form data", "loadSubjectsFailed": "Failed to load subjects", - "loadGradesFailed": "Failed to load grades" + "loadGradesFailed": "Failed to load grades", + "creating": "Creating...", + "detailsTitle": "Exam Details", + "detailsDesc": "Define the core information for your exam.", + "titlePlaceholder": "e.g. Midterm Mathematics Exam", + "subjectPlaceholder": "Select subject", + "gradePlaceholder": "Select grade level", + "difficultyPlaceholder": "Select level", + "difficultyLevel1": "Level 1 (Easy)", + "difficultyLevel2": "Level 2", + "difficultyLevel3": "Level 3 (Medium)", + "difficultyLevel4": "Level 4", + "difficultyLevel5": "Level 5 (Hard)", + "loadingSubjects": "Loading subjects...", + "loadingGrades": "Loading grades...", + "scheduledAtDesc": "If set, this exam will be scheduled for a specific time.", + "modeManual": "Manual Assembly", + "modeManualDesc": "Manually select questions from the bank and organize structure.", + "modeManualAction": "Create & Start Building", + "modeAi": "AI Generation", + "modeAiDesc": "Paste exam text and AI will auto-parse questions into structured preview.", + "modeAiAction": "Generate in Background", + "modeRich": "Rich Text Editor", + "modeRichDesc": "Use rich text editor to paste exam text, manually or AI-assisted marking of questions, groups, blanks, etc.", + "modeRichHint": "The rich text editor supports selecting text to mark as questions/groups/dotted chars/blanks, with live preview on the right.", + "modeRichAction": "Open Rich Text Editor" }, "status": { "draft": "Draft", diff --git a/src/shared/i18n/messages/zh-CN/exam-homework.json b/src/shared/i18n/messages/zh-CN/exam-homework.json index 2257e9c..2a67df0 100644 --- a/src/shared/i18n/messages/zh-CN/exam-homework.json +++ b/src/shared/i18n/messages/zh-CN/exam-homework.json @@ -32,7 +32,32 @@ "createFailed": "创建考试失败", "loadFormFailed": "加载表单数据失败", "loadSubjectsFailed": "加载科目失败", - "loadGradesFailed": "加载年级失败" + "loadGradesFailed": "加载年级失败", + "creating": "创建中...", + "detailsTitle": "基本信息", + "detailsDesc": "设置考试的核心信息。", + "titlePlaceholder": "例如:期中数学考试", + "subjectPlaceholder": "选择科目", + "gradePlaceholder": "选择年级", + "difficultyPlaceholder": "选择难度", + "difficultyLevel1": "1级 (简单)", + "difficultyLevel2": "2级", + "difficultyLevel3": "3级 (中等)", + "difficultyLevel4": "4级", + "difficultyLevel5": "5级 (困难)", + "loadingSubjects": "加载科目中...", + "loadingGrades": "加载年级中...", + "scheduledAtDesc": "设置后,考试将在指定时间开放。", + "modeManual": "手动组卷", + "modeManualDesc": "从题库手动选择题目并组织试卷结构。", + "modeManualAction": "创建并开始组卷", + "modeAi": "AI 生成", + "modeAiDesc": "粘贴试卷文本,AI 自动解析题目并生成结构化预览。", + "modeAiAction": "后台生成试卷", + "modeRich": "富文本编辑器", + "modeRichDesc": "使用富文本编辑器粘贴试卷文本,手动或 AI 辅助标记题目、分组、填空等。", + "modeRichHint": "富文本编辑器支持选中文本标记题目/分组/加点字/填空,右侧实时预览试卷效果。", + "modeRichAction": "打开富文本编辑器" }, "status": { "draft": "草稿",