import type { JSX } from "react" import { notFound } from "next/navigation" import { ExamAssembly } from "@/modules/exams/components/exam-assembly" import { getExamById } from "@/modules/exams/data-access" import { getQuestions } from "@/modules/questions/data-access" import { normalizeStructure } from "@/modules/exams/utils/normalize-structure" import type { Question } from "@/modules/questions/types" import type { ExamNode } from "@/modules/exams/components/assembly/selected-question-list" import { createId } from "@paralleldrive/cuid2" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { AiClientProvider, type AiClientService, } from "@/modules/ai/context/ai-client-provider" import { aiChatAction, suggestSimilarQuestionsAction, suggestGradingAction, generateLessonContentAction, generateQuestionVariantAction, analyzeWeaknessAction, } from "@/modules/ai/actions" export const dynamic = "force-dynamic" /** * 构建 AI 客户端服务(Server Action 引用集合) * * 通过 React Context 注入,客户端组件不直接 import actions, * 遵循依赖注入模式,便于测试时替换为 mock。 */ function createAiClientService(): AiClientService { return { chat: aiChatAction, suggestSimilarQuestions: suggestSimilarQuestionsAction, suggestGrading: suggestGradingAction, generateLessonContent: generateLessonContentAction, generateQuestionVariant: generateQuestionVariantAction, analyzeWeakness: analyzeWeaknessAction, } } export default async function BuildExamPage({ params }: { params: Promise<{ id: string }> }): Promise { const { id } = await params const ctx = await requirePermission(Permissions.EXAM_READ) const exam = await getExamById(id, ctx.dataScope) if (!exam) return notFound() // Fetch initial questions for the bank (pagination handled by client) // Run both queries in parallel since the second depends on exam.questions IDs const initialSelected = (exam.questions || []).map(q => ({ id: q.id, score: q.score || 0 })) const selectedQuestionIds = initialSelected.map((s) => s.id) const [bankResult, selectedResult] = await Promise.all([ getQuestions({ pageSize: 20 }), selectedQuestionIds.length ? getQuestions({ ids: selectedQuestionIds, pageSize: Math.max(10, selectedQuestionIds.length) }) : Promise.resolve({ data: [] as Awaited>["data"] }), ]) const questionsData = bankResult.data const selectedQuestionsData = selectedResult.data type RawQuestion = (typeof questionsData)[number] const toQuestionOption = (q: RawQuestion): Question => ({ id: q.id, content: q.content, type: q.type, difficulty: q.difficulty ?? 1, createdAt: new Date(q.createdAt), updatedAt: new Date(q.updatedAt), author: q.author ? { id: q.author.id, name: q.author.name || "Unknown", image: q.author.image || null, } : null, knowledgePoints: q.knowledgePoints ?? [], }) const questionOptionsById = new Map() for (const q of questionsData) questionOptionsById.set(q.id, toQuestionOption(q)) for (const q of selectedQuestionsData) questionOptionsById.set(q.id, toQuestionOption(q)) const questionOptions = Array.from(questionOptionsById.values()) let initialStructure: ExamNode[] = normalizeStructure(exam.structure) if (initialStructure.length === 0 && initialSelected.length > 0) { initialStructure = initialSelected.map((s) => ({ id: createId(), type: "question", questionId: s.id, score: s.score, })) } const aiClientService = createAiClientService() return (

Build Exam

Assemble questions for your exam.

) }