feat(lesson-preparation): update paper-editor, detail-panel, AI node assist, i18n

- Update actions-ai.ts and curriculum-map-view.tsx

- Update detail-panel: detail-panel, detail-props, qa-editor

- Update paper-editor: inline-node, inline-qa-dialog, paper-context-menu,

  paper-editor, textbook-tiptap-editor

- Update hooks/editor-slice.ts and lib/i18n-errors.ts

- Add hooks/use-node-ai-assist.ts and lib/ai-node-assist.ts

- Update en/zh-CN lesson-preparation i18n messages
This commit is contained in:
SpecialX
2026-07-04 23:02:04 +08:00
parent 0780524c08
commit e962b67050
16 changed files with 695 additions and 327 deletions

View File

@@ -3,7 +3,9 @@ import { createId } from "@paralleldrive/cuid2";
import type {
AnchorType,
Block,
BlockData,
BlockType,
InteractionBlockData,
LessonPlanDocument,
LessonPlanNode,
NodeAnchor,
@@ -13,6 +15,23 @@ import type {
import { defaultDataForType } from "../lib/document-migration";
import type { EditorState } from "./use-lesson-plan-editor";
/**
* V4深拷贝 BlockData为 interaction 节点的 turns 重新生成 id避免 id 冲突)。
* 其他类型直接结构化克隆JSON 序列化足够,数据全是 POJO
*/
function cloneBlockData(data: BlockData, type: BlockType): BlockData {
if (type === "interaction") {
const d = data as InteractionBlockData;
return {
designIntent: d.designIntent,
knowledgePointIds: [...d.knowledgePointIds],
turns: d.turns.map((t) => ({ ...t, id: createId() })),
};
}
// 其他类型JSON 深拷贝(数据全是 POJO无函数/Date/Symbol
return JSON.parse(JSON.stringify(data)) as BlockData;
}
export interface EditorSlice {
planId: string;
title: string;
@@ -22,6 +41,8 @@ export interface EditorSlice {
addNode: (type: BlockType, position?: { x: number; y: number }, title?: string) => string;
updateNode: (id: string, patch: Omit<Partial<Block>, "type">) => void;
removeNode: (id: string) => void;
/** V4复制节点深拷贝数据 + 新 id + 标题加"副本" + order 置末) */
duplicateNode: (id: string) => string | null;
updateTextbookContent: (data: Partial<TextbookContentNodeData>) => void;
getTextbookContentNode: () => TextbookContentNode | undefined;
/**
@@ -141,6 +162,37 @@ export const createEditorSlice: StateCreator<
});
},
duplicateNode: (id) => {
const state = get();
const src = state.doc.nodes.find(
(n): n is LessonPlanNode => n.id === id && n.type !== "textbook_content",
);
if (!src) return null;
state.pushHistory();
const newId = createId();
const teachingNodes = state.doc.nodes.filter(
(n): n is LessonPlanNode => n.type !== "textbook_content",
);
// 深拷贝 data避免引用共享interaction 节点的 turns 内 id 重新生成
const clonedData = cloneBlockData(src.data, src.type);
const newNode: LessonPlanNode = {
id: newId,
type: src.type,
title: `${src.title}(副本)`,
data: clonedData,
order: teachingNodes.length,
position: { x: 0, y: 0 },
stage: src.stage,
differentiation: src.differentiation,
};
set((s) => ({
doc: { ...s.doc, nodes: [...s.doc.nodes, newNode] },
isDirty: true,
selectedNodeId: newId,
}));
return newId;
},
updateTextbookContent: (data) => {
get().pushHistory(); // V5-2撤销/重做
set((s) => ({