"use client"; import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { X, Layers, Loader2, AlertCircle, Users, CheckCircle2, XCircle, BookCheck, ClipboardList, GraduationCap, } from "lucide-react"; import { FocusTrap } from "@/shared/components/a11y/focus-trap"; import { Button } from "@/shared/components/ui/button"; import { generateDifferentiationSuggestionsAction, checkCurriculumAlignmentAction, generateExplainableAssessmentAction, getKnowledgePointsForAlignmentAction, } from "../actions-ai"; import type { LessonPlanDocument, DifferentiationLevel } from "../types"; import type { DifferentiationSuggestion, CurriculumCheckItem, ExplainableAssessment, } from "../lib/ai-differentiation"; interface Props { doc: LessonPlanDocument; /** 课案所属教材 ID(用于 A4 课标核对按需加载知识点) */ textbookId?: string; onClose: () => void; } type TabKey = "differentiation" | "curriculum" | "assessment"; const TABS: TabKey[] = ["differentiation", "curriculum", "assessment"]; /** * V5-21 A3/A4/A5:AI 差异化对话框。 * * 三个 Tab 分别承载: * - A3 差异化教学建议(基础/提高/拓展) * - A4 课标实时核对(按 textbookId 按需加载知识点) * - A5 可解释评估(练习节点的评分依据) */ export function AiDifferentiationDialog({ doc, textbookId, onClose }: Props) { const t = useTranslations("lessonPreparation"); const [activeTab, setActiveTab] = useState("differentiation"); // ESC 关闭 useEffect(() => { function handleEsc(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } document.addEventListener("keydown", handleEsc); return () => document.removeEventListener("keydown", handleEsc); }, [onClose]); return (
{/* arbitrary-value: dialog fixed width */}

{/* Tab 切换 */}
{TABS.map((tab) => ( ))}
{activeTab === "differentiation" && ( )} {activeTab === "curriculum" && ( )} {activeTab === "assessment" && }
); } /** A3:差异化教学建议 Tab */ function DifferentiationTab({ doc, t, }: { doc: LessonPlanDocument; t: ReturnType; }) { const [loading, setLoading] = useState(true); const [items, setItems] = useState([]); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; (async () => { setLoading(true); setError(null); try { const res = await generateDifferentiationSuggestionsAction(doc); if (cancelled) return; if (res.success && res.data) { setItems(res.data.items); } else { setError(res.message ?? t("aiDifferentiation.loadFailed")); } } catch { if (!cancelled) setError(t("aiDifferentiation.loadFailed")); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [doc, t]); if (loading) return ; if (error) return ; if (items.length === 0) return ; // 按层次排序:basic → intermediate → advanced const order: DifferentiationLevel[] = ["basic", "intermediate", "advanced"]; const sorted = [...items].sort( (a, b) => order.indexOf(a.level) - order.indexOf(b.level), ); return (
{sorted.map((item) => (

    {item.suggestions.map((s, idx) => (
  • · {s}
  • ))}
))}
); } /** A4:课标实时核对 Tab(按 textbookId 按需加载知识点) */ function CurriculumTab({ doc, textbookId, t, }: { doc: LessonPlanDocument; textbookId?: string; t: ReturnType; }) { const [loading, setLoading] = useState(true); const [items, setItems] = useState([]); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; (async () => { // 无教材 ID 时无法获取知识点 if (!textbookId) { setLoading(false); setError(t("aiDifferentiation.noKnowledgePoints")); return; } setLoading(true); setError(null); try { // 先按需加载知识点列表 const kpRes = await getKnowledgePointsForAlignmentAction(textbookId); if (cancelled) return; if (!kpRes.success || !kpRes.data || kpRes.data.items.length === 0) { setError(t("aiDifferentiation.noKnowledgePoints")); return; } // 再调用课标核对 const res = await checkCurriculumAlignmentAction(doc, kpRes.data.items); if (cancelled) return; if (res.success && res.data) { setItems(res.data.items); } else { setError(res.message ?? t("aiDifferentiation.loadFailed")); } } catch { if (!cancelled) setError(t("aiDifferentiation.loadFailed")); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [doc, textbookId, t]); if (loading) return ; if (error) return ; if (items.length === 0) return ; const covered = items.filter((i) => i.covered).length; const missed = items.length - covered; return (
    {items.map((item, idx) => (
  • {item.covered ? (
    {item.explanation && (

    {item.explanation}

    )}
  • ))}
); } /** A5:可解释评估 Tab */ function AssessmentTab({ doc, t, }: { doc: LessonPlanDocument; t: ReturnType; }) { const [loading, setLoading] = useState(true); const [items, setItems] = useState([]); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; (async () => { setLoading(true); setError(null); try { const res = await generateExplainableAssessmentAction(doc); if (cancelled) return; if (res.success && res.data) { setItems(res.data.items); } else { setError(res.message ?? t("aiDifferentiation.loadFailed")); } } catch { if (!cancelled) setError(t("aiDifferentiation.loadFailed")); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [doc, t]); if (loading) return ; if (error) return ; if (items.length === 0) return ; return (
    {items.map((item, idx) => (
  • {item.rationale && (

    )} {item.suggestion && (

    )}
  • ))}
); } function LoadingBlock({ label }: { label: string }) { return (
); } function ErrorBlock({ message }: { message: string }) { return (
); } function EmptyBlock({ label }: { label: string }) { return
{label}
; }