- Add actions-schedules.ts and data-access-schedules.ts for schedule management - Add AI differentiation, AI feedback, consistency check dialogs - Add attachment-picker, curriculum-heatmap, print-view, version-diff-view - Add lesson-plan-mobile-view and schedule-dialog components - Add lib: ai-differentiation, ai-feedback, auto-layout, consistency-check, curriculum-coverage, export, version-diff - Add history-slice hook for version history - Update existing components, hooks, providers, services, types - Add teacher lesson-plans heatmap and library pages
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
/**
|
||
* V5-17 A1/A2:AI 反馈闭环 + 解释性展示。
|
||
*
|
||
* 调用 AI 对课案文档进行教学评一致性反馈,返回结构化建议。
|
||
* 反馈包含:
|
||
* - strengths:课案优点
|
||
* - improvements:改进建议
|
||
* - alignment:教学评一致性评估
|
||
* - differentiation:差异化教学建议
|
||
*
|
||
* 每条建议附带 reason(解释性展示),帮助教师理解 AI 判断依据。
|
||
*/
|
||
import "server-only";
|
||
import { env } from "@/env.mjs";
|
||
import { createAiChatCompletion } from "@/shared/lib/ai";
|
||
import { isRecord } from "@/shared/lib/type-guards";
|
||
import { z } from "zod";
|
||
import type { LessonPlanDocument, LessonPlanNode } from "../types";
|
||
|
||
/** AI 反馈单条建议 */
|
||
export interface AiFeedbackItem {
|
||
/** i18n 键后缀(feedback.* 命名空间下) */
|
||
category: "strengths" | "improvements" | "alignment" | "differentiation";
|
||
/** 建议标题 */
|
||
title: string;
|
||
/** 解释性理由(A2:解释性展示) */
|
||
reason: string;
|
||
/** 关联节点 ID(如适用) */
|
||
nodeId?: string;
|
||
}
|
||
|
||
/** AI 反馈结果 */
|
||
export interface AiFeedbackResult {
|
||
items: AiFeedbackItem[];
|
||
/** 整体评分(0-100) */
|
||
overallScore: number;
|
||
/** 摘要 */
|
||
summary: string;
|
||
}
|
||
|
||
const FeedbackItemSchema = z.object({
|
||
category: z.enum(["strengths", "improvements", "alignment", "differentiation"]),
|
||
title: z.string().min(1),
|
||
reason: z.string(),
|
||
nodeId: z.string().optional(),
|
||
});
|
||
|
||
const FeedbackResultSchema = z.object({
|
||
items: z.array(FeedbackItemSchema),
|
||
overallScore: z.number().min(0).max(100),
|
||
summary: z.string(),
|
||
});
|
||
|
||
const AI_FEEDBACK_PROMPT_TEMPLATE = `你是资深教学设计专家。请对以下课案文档进行教学评一致性评估,给出结构化反馈。
|
||
|
||
课案文档(JSON):
|
||
---
|
||
{doc}
|
||
---
|
||
|
||
请从四个维度评估:
|
||
1. strengths:课案优点
|
||
2. improvements:改进建议
|
||
3. alignment:教学评一致性(目标-教学-评价是否对齐)
|
||
4. differentiation:差异化教学建议
|
||
|
||
返回 JSON 对象,含:
|
||
- items:数组,每项含 category(维度)/title(建议标题)/reason(解释性理由,说明为何给出此建议)/nodeId(关联节点 ID,可选)
|
||
- overallScore:整体评分 0-100
|
||
- summary:一句话摘要
|
||
|
||
注意:reason 字段必须解释判断依据,帮助教师理解。`;
|
||
|
||
/** 安全提取节点文本用于 AI prompt */
|
||
function extractNodeText(node: LessonPlanNode): string {
|
||
const data = node.data as unknown;
|
||
if (!isRecord(data)) return "";
|
||
const html = typeof data.html === "string" ? data.html : "";
|
||
const sourceText = typeof data.sourceText === "string" ? data.sourceText : "";
|
||
return html || sourceText || "";
|
||
}
|
||
|
||
/**
|
||
* 调用 AI 对课案文档生成结构化反馈。
|
||
*
|
||
* @param doc 课案文档
|
||
* @returns AI 反馈结果;AI 不可用时返回空结果
|
||
*/
|
||
export async function generateLessonPlanFeedback(
|
||
doc: LessonPlanDocument,
|
||
): Promise<AiFeedbackResult> {
|
||
// 提取教学节点摘要(排除正文节点,控制 token 用量)
|
||
const teachingNodes = doc.nodes.filter(
|
||
(n): n is LessonPlanNode => n.type !== "textbook_content",
|
||
);
|
||
if (teachingNodes.length === 0) {
|
||
return { items: [], overallScore: 0, summary: "" };
|
||
}
|
||
|
||
const docSummary = teachingNodes.slice(0, 20).map((n) => ({
|
||
id: n.id,
|
||
type: n.type,
|
||
title: n.title,
|
||
stage: n.stage,
|
||
differentiation: n.differentiation,
|
||
text: extractNodeText(n).slice(0, 200),
|
||
}));
|
||
|
||
const prompt = AI_FEEDBACK_PROMPT_TEMPLATE.replace(
|
||
"{doc}",
|
||
JSON.stringify(docSummary),
|
||
);
|
||
|
||
try {
|
||
const { content } = await createAiChatCompletion({
|
||
messages: [{ role: "user", content: prompt }],
|
||
model: env.AI_MODEL ?? "gpt-4o-mini",
|
||
temperature: 0.4,
|
||
});
|
||
|
||
// 从返回内容中提取 JSON 对象
|
||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||
if (!jsonMatch) return { items: [], overallScore: 0, summary: "" };
|
||
|
||
const parsed: unknown = JSON.parse(jsonMatch[0]);
|
||
const validated = FeedbackResultSchema.safeParse(parsed);
|
||
if (!validated.success) {
|
||
return { items: [], overallScore: 0, summary: "" };
|
||
}
|
||
|
||
return validated.data;
|
||
} catch {
|
||
return { items: [], overallScore: 0, summary: "" };
|
||
}
|
||
}
|