Files
NextEdu/src/modules/lesson-preparation/hooks/use-node-ai-assist.ts
SpecialX 524ecade19 feat(lesson-preparation): major update with data-access splits and new components
- 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)
2026-07-07 16:20:34 +08:00

110 lines
3.8 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.
"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.datamergeBlockDataPatch 内部校验类型安全
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 };
}