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:
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user