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:
SpecialX
2026-07-04 10:22:10 +08:00
parent 41fe8d8903
commit 25dca843be
45 changed files with 5295 additions and 210 deletions

View File

@@ -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,
}));
},
});