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
This commit is contained in:
@@ -14,6 +14,7 @@ import type {
|
||||
TextbookContentNodeData,
|
||||
} from "../types";
|
||||
import { defaultDataForType } from "../lib/document-migration";
|
||||
import { computeAutoLayout } from "../lib/auto-layout";
|
||||
import type { EditorState } from "./use-lesson-plan-editor";
|
||||
|
||||
export interface EditorSlice {
|
||||
@@ -40,6 +41,8 @@ export interface EditorSlice {
|
||||
connect: (source: string, target: string) => void;
|
||||
disconnect: (edgeId: string) => void;
|
||||
setEdges: (edges: AnyLessonPlanEdge[]) => void;
|
||||
/** V5-8:自动布局,使用 dagre 计算并应用新位置 */
|
||||
autoLayout: (direction?: "TB" | "LR") => void;
|
||||
}
|
||||
|
||||
function reindex(nodes: LessonPlanNode[]): LessonPlanNode[] {
|
||||
@@ -62,12 +65,16 @@ export const createEditorSlice: StateCreator<
|
||||
anchors: [],
|
||||
},
|
||||
|
||||
setTitle: (title) => set({ title, isDirty: true }),
|
||||
setTitle: (title) => {
|
||||
get().pushHistory();
|
||||
set({ title, isDirty: true });
|
||||
},
|
||||
setPlanId: (planId) => set({ planId }),
|
||||
|
||||
addNode: (type, position, title) => {
|
||||
const id = createId();
|
||||
const state = get();
|
||||
state.pushHistory(); // V5-2:撤销/重做
|
||||
const teachingNodes = state.doc.nodes.filter(
|
||||
(n): n is LessonPlanNode => n.type !== "textbook_content",
|
||||
);
|
||||
@@ -91,7 +98,8 @@ export const createEditorSlice: StateCreator<
|
||||
return id;
|
||||
},
|
||||
|
||||
updateNode: (id, patch) =>
|
||||
updateNode: (id, patch) => {
|
||||
get().pushHistory(); // V5-2:撤销/重做
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
@@ -104,9 +112,12 @@ export const createEditorSlice: StateCreator<
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
updateNodePosition: (id, position) =>
|
||||
updateNodePosition: (id, position) => {
|
||||
// V5-2 注:拖拽过程中会产生大量 position 更新,仅在拖拽开始时推一次 history。
|
||||
// 调用方应在 onDragStart 时调用 pushHistory,此处不重复推入。
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
@@ -119,9 +130,11 @@ export const createEditorSlice: StateCreator<
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
removeNode: (id) =>
|
||||
removeNode: (id) => {
|
||||
get().pushHistory(); // V5-2:撤销/重做
|
||||
set((s) => {
|
||||
const remainingTeachingNodes = reindex(
|
||||
s.doc.nodes.filter(
|
||||
@@ -146,9 +159,11 @@ export const createEditorSlice: StateCreator<
|
||||
isDirty: true,
|
||||
selectedNodeId: s.selectedNodeId === id ? null : s.selectedNodeId,
|
||||
};
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
updateTextbookContent: (data) =>
|
||||
updateTextbookContent: (data) => {
|
||||
get().pushHistory(); // V5-2:撤销/重做
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
@@ -159,7 +174,8 @@ export const createEditorSlice: StateCreator<
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
getTextbookContentNode: () => {
|
||||
const state = get();
|
||||
@@ -171,6 +187,7 @@ export const createEditorSlice: StateCreator<
|
||||
addAnchor: ({ nodeId, type, start, end, textPreview }) => {
|
||||
const anchorId = createId();
|
||||
const state = get();
|
||||
state.pushHistory(); // V5-2:撤销/重做
|
||||
const textbookNodeId = state.doc.textbookContentNodeId;
|
||||
|
||||
const anchor: NodeAnchor = {
|
||||
@@ -202,7 +219,8 @@ export const createEditorSlice: StateCreator<
|
||||
return anchorId;
|
||||
},
|
||||
|
||||
removeAnchor: (anchorId) =>
|
||||
removeAnchor: (anchorId) => {
|
||||
get().pushHistory(); // V5-2:撤销/重做
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
@@ -212,9 +230,11 @@ export const createEditorSlice: StateCreator<
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
updateAnchor: (anchorId, patch) =>
|
||||
updateAnchor: (anchorId, patch) => {
|
||||
get().pushHistory(); // V5-2:撤销/重做
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
@@ -223,9 +243,11 @@ export const createEditorSlice: StateCreator<
|
||||
),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
connect: (source, target) =>
|
||||
connect: (source, target) => {
|
||||
get().pushHistory(); // V5-2:撤销/重做
|
||||
set((s) => {
|
||||
if (
|
||||
s.doc.edges.some((e) => e.source === source && e.target === target)
|
||||
@@ -241,17 +263,43 @@ export const createEditorSlice: StateCreator<
|
||||
doc: { ...s.doc, edges: [...s.doc.edges, edge] },
|
||||
isDirty: true,
|
||||
};
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
disconnect: (edgeId) =>
|
||||
disconnect: (edgeId) => {
|
||||
get().pushHistory(); // V5-2:撤销/重做
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
edges: s.doc.edges.filter((e) => e.id !== edgeId),
|
||||
},
|
||||
isDirty: true,
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
setEdges: (edges) =>
|
||||
set((s) => ({ doc: { ...s.doc, edges }, isDirty: true })),
|
||||
setEdges: (edges) => {
|
||||
get().pushHistory(); // V5-2:撤销/重做
|
||||
set((s) => ({ doc: { ...s.doc, edges }, isDirty: true }));
|
||||
},
|
||||
|
||||
autoLayout: (direction = "TB") => {
|
||||
const state = get();
|
||||
const positions = computeAutoLayout(state.doc.nodes, state.doc.edges, { direction });
|
||||
if (positions.size === 0) return;
|
||||
state.pushHistory(); // V5-2:撤销/重做
|
||||
set((s) => ({
|
||||
doc: {
|
||||
...s.doc,
|
||||
nodes: s.doc.nodes.map((n) => {
|
||||
const p = positions.get(n.id);
|
||||
return p
|
||||
? n.type === "textbook_content"
|
||||
? ({ ...n, position: p } as TextbookContentNode)
|
||||
: ({ ...n, position: p } as LessonPlanNode)
|
||||
: n;
|
||||
}),
|
||||
},
|
||||
isDirty: true,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user