Files
NextEdu/src/modules/lesson-preparation/lib/ai-feedback.ts
SpecialX 25dca843be 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
2026-07-04 10:22:10 +08:00

136 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* V5-17 A1/A2AI 反馈闭环 + 解释性展示。
*
* 调用 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: "" };
}
}