feat(lesson-preparation): major update with AI features, schedules, and new components
- 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
This commit is contained in:
135
src/modules/lesson-preparation/lib/ai-feedback.ts
Normal file
135
src/modules/lesson-preparation/lib/ai-feedback.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 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: "" };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user