- Update ai-feedback-dialog, attachment-picker, blocks/text-study-block - Update detail-panel (detail-panel, detail-props) - Update inline-question-editor, lesson-plan-card, lesson-plan-editor, lesson-plan-mobile-view, schedule-dialog, version-history-drawer - Update paper-editor (inline-qa-dialog, paper-context-menu) - Update structure-tree (structure-tree, tree-node-row) - Update config/block-registry, hooks (editor-slice, use-lesson-plan-persistence, use-node-ai-assist), lib (ai-node-assist, consistency-check, export, type-guards) - Update publish-service, services/default-question-service - Update data-access files (ai-evaluation, analytics, attachments, calendar, comments, formative, knowledge, review, schedules, templates, versions, main)
110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useState } from "react";
|
||
import { useTranslations } from "next-intl";
|
||
import { notify } from "@/shared/lib/notify";
|
||
import { useLessonPlanEditor } from "./use-lesson-plan-editor";
|
||
import {
|
||
generateNodeContentAction,
|
||
optimizeNodeExpressionAction,
|
||
suggestNodeDifferentiationAction,
|
||
generateLayeredQuestionsAction,
|
||
} from "../actions-ai";
|
||
import type { LessonPlanNode } from "../types";
|
||
import { isInteractionBlockData, mergeBlockDataPatch } from "../lib/type-guards";
|
||
|
||
export type NodeAiAction = "generate" | "optimize" | "differentiation" | "layered";
|
||
|
||
/**
|
||
* V4 节点级 AI 协助 hook。
|
||
*
|
||
* - 调用对应 Server Action 获取 AI 结果
|
||
* - 将结果合并到 node.data 或 node.differentiation
|
||
* - 通过 toast 反馈运行/成功/失败状态
|
||
*
|
||
* 用法:
|
||
* const { run, isRunning } = useNodeAiAssist();
|
||
* <button onClick={() => run("generate", nodeId)}>生成</button>
|
||
*/
|
||
export function useNodeAiAssist() {
|
||
const t = useTranslations("lessonPreparation");
|
||
const doc = useLessonPlanEditor((s) => s.doc);
|
||
const updateNode = useLessonPlanEditor((s) => s.updateNode);
|
||
const [isRunning, setIsRunning] = useState(false);
|
||
|
||
const run = useCallback(
|
||
async (action: NodeAiAction, nodeId: string) => {
|
||
const node = doc.nodes.find(
|
||
(n): n is LessonPlanNode => n.id === nodeId && n.type !== "textbook_content",
|
||
);
|
||
if (!node) return;
|
||
|
||
setIsRunning(true);
|
||
notify.info(t("v4.contextMenu.aiRunning"));
|
||
|
||
try {
|
||
if (action === "generate") {
|
||
const res = await generateNodeContentAction(node);
|
||
if (res.success && res.data) {
|
||
// AI patch 合并到 node.data,mergeBlockDataPatch 内部校验类型安全
|
||
const merged = mergeBlockDataPatch(node, res.data.data);
|
||
updateNode(nodeId, { data: merged });
|
||
notify.success(t("v4.contextMenu.aiSuccess"));
|
||
} else {
|
||
notify.error(t("v4.contextMenu.aiFailed"));
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (action === "optimize") {
|
||
const res = await optimizeNodeExpressionAction(node);
|
||
if (res.success && res.data) {
|
||
const merged = mergeBlockDataPatch(node, res.data.data);
|
||
updateNode(nodeId, { data: merged });
|
||
notify.success(t("v4.contextMenu.aiSuccess"));
|
||
} else {
|
||
notify.error(t("v4.contextMenu.aiFailed"));
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (action === "differentiation") {
|
||
const res = await suggestNodeDifferentiationAction(node);
|
||
if (res.success && res.data) {
|
||
updateNode(nodeId, { differentiation: res.data.level });
|
||
notify.success(`${t("v4.contextMenu.aiSuccess")}:${res.data.reason}`);
|
||
} else {
|
||
notify.error(t("v4.contextMenu.aiFailed"));
|
||
}
|
||
return;
|
||
}
|
||
|
||
// layered
|
||
if (node.type !== "interaction") {
|
||
notify.error(t("v4.contextMenu.aiFailed"));
|
||
return;
|
||
}
|
||
const res = await generateLayeredQuestionsAction(node);
|
||
if (res.success && res.data) {
|
||
// 类型守卫二次校验 node.data 为 InteractionBlockData,失败提示错误
|
||
if (!isInteractionBlockData(node.data)) {
|
||
notify.error(t("v4.contextMenu.aiFailed"));
|
||
return;
|
||
}
|
||
const newTurns = [...(node.data.turns ?? []), ...res.data.turns];
|
||
const merged = mergeBlockDataPatch(node, { turns: newTurns });
|
||
updateNode(nodeId, { data: merged });
|
||
notify.success(t("v4.contextMenu.aiSuccess"));
|
||
} else {
|
||
notify.error(t("v4.contextMenu.aiFailed"));
|
||
}
|
||
} finally {
|
||
setIsRunning(false);
|
||
}
|
||
},
|
||
[doc.nodes, updateNode, t],
|
||
);
|
||
|
||
return { run, isRunning };
|
||
}
|