feat(exams,homework): add rich text exam editor and scan-based grading

- Add Tiptap-based rich text editor with custom extensions (dotted-mark,
  blank-node, image-node, group-block, question-block) for exam creation
- Add AI auto-marking action to convert pasted exam text to structured editor doc
- Add resizable split-panel layout for editor + live preview
- Add student scan upload (photo of paper answers) with drag-drop and reorder
- Add scan image viewer with zoom/rotate/fullscreen for teachers
- Add scan grading view with side-by-side questions and scan images
- Add /teacher/exams/new and /teacher/homework/submissions/[id]/scan-grading routes
- Fix getScansAction to support both teacher (HOMEWORK_GRADE) and student
  (HOMEWORK_SUBMIT) permission scopes
- Add i18n keys for rich editor, scan upload, and scan grading (zh-CN/en)
- Sync architecture diagrams (004/005) with new modules, routes, and deps
This commit is contained in:
SpecialX
2026-06-24 13:16:33 +08:00
parent 0c64219cb8
commit 6114607c1e
30 changed files with 3548 additions and 26 deletions

View File

@@ -1,7 +1,11 @@
import type { JSX } from "react"
import Link from "next/link"
import { notFound } from "next/navigation"
import { getTranslations } from "next-intl/server"
import { getHomeworkSubmissionDetails } from "@/modules/homework/data-access"
import { HomeworkGradingView } from "@/modules/homework/components/homework-grading-view"
import { Button } from "@/shared/components/ui/button"
import { ScanLine } from "lucide-react"
import { formatDate } from "@/shared/lib/utils"
import {
AiClientProvider,
@@ -37,6 +41,7 @@ function createAiClientService(): AiClientService {
export default async function HomeworkSubmissionGradingPage({ params }: { params: Promise<{ submissionId: string }> }): Promise<JSX.Element> {
const { submissionId } = await params
const t = await getTranslations("examHomework")
const submission = await getHomeworkSubmissionDetails(submissionId)
if (!submission) return notFound()
@@ -58,6 +63,12 @@ export default async function HomeworkSubmissionGradingPage({ params }: { params
<span className="capitalize">Status: {submission.status}</span>
</div>
</div>
<Button asChild variant="outline" size="sm">
<Link href={`/teacher/homework/submissions/${submissionId}/scan-grading`}>
<ScanLine className="mr-2 h-4 w-4" />
{t("homework.grade.scanGrading")}
</Link>
</Button>
</div>
<AiClientProvider service={aiClientService}>

View File

@@ -0,0 +1,34 @@
import type { JSX } from "react"
import { notFound } from "next/navigation"
import { getHomeworkSubmissionDetails } from "@/modules/homework/data-access"
import { HomeworkScanGradingView } from "@/modules/homework/components/homework-scan-grading-view"
export const dynamic = "force-dynamic"
export default async function HomeworkScanGradingPage({
params,
}: {
params: Promise<{ submissionId: string }>
}): Promise<JSX.Element> {
const { submissionId } = await params
const submission = await getHomeworkSubmissionDetails(submissionId)
if (!submission) return notFound()
return (
<div className="flex h-full flex-col space-y-4 p-6">
<HomeworkScanGradingView
submissionId={submission.id}
studentName={submission.studentName}
assignmentTitle={submission.assignmentTitle}
submittedAt={submission.submittedAt}
status={submission.status}
totalScore={submission.totalScore}
answers={submission.answers}
prevSubmissionId={submission.prevSubmissionId}
nextSubmissionId={submission.nextSubmissionId}
/>
</div>
)
}