@arronqzy/react-blueprint 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.eslintrc.cjs +5 -0
  2. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/aix-ppc64.json +1 -0
  3. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm.json +1 -0
  4. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-arm64.json +1 -0
  5. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/android-x64.json +1 -0
  6. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-arm64.json +1 -0
  7. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/darwin-x64.json +1 -0
  8. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-arm64.json +1 -0
  9. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/freebsd-x64.json +1 -0
  10. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm.json +1 -0
  11. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-arm64.json +1 -0
  12. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ia32.json +1 -0
  13. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-loong64.json +1 -0
  14. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-mips64el.json +1 -0
  15. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-ppc64.json +1 -0
  16. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-riscv64.json +1 -0
  17. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-s390x.json +1 -0
  18. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/linux-x64.json +1 -0
  19. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/netbsd-x64.json +1 -0
  20. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/openbsd-x64.json +1 -0
  21. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/sunos-x64.json +1 -0
  22. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-arm64.json +1 -0
  23. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-ia32.json +1 -0
  24. package/.pnpm-cache/metadata/registry.npmjs.org/@esbuild/win32-x64.json +1 -0
  25. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm-eabi.json +1 -0
  26. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-android-arm64.json +1 -0
  27. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-arm64.json +1 -0
  28. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-darwin-x64.json +1 -0
  29. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-arm64.json +1 -0
  30. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-freebsd-x64.json +1 -0
  31. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf.json +1 -0
  32. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf.json +1 -0
  33. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-gnu.json +1 -0
  34. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-arm64-musl.json +1 -0
  35. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu.json +1 -0
  36. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu.json +1 -0
  37. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu.json +1 -0
  38. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-riscv64-musl.json +1 -0
  39. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-s390x-gnu.json +1 -0
  40. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-gnu.json +1 -0
  41. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-linux-x64-musl.json +1 -0
  42. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-arm64-msvc.json +1 -0
  43. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-ia32-msvc.json +1 -0
  44. package/.pnpm-cache/metadata/registry.npmjs.org/@rollup/rollup-win32-x64-msvc.json +1 -0
  45. package/.pnpm-cache/metadata/registry.npmjs.org/eslint.json +1 -0
  46. package/.pnpm-cache/metadata/registry.npmjs.org/fsevents.json +1 -0
  47. package/.pnpm-cache/metadata/registry.npmjs.org/react-dom.json +1 -0
  48. package/.pnpm-cache/metadata/registry.npmjs.org/react-is.json +1 -0
  49. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-64.json +1 -0
  50. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-darwin-arm64.json +1 -0
  51. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-64.json +1 -0
  52. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-linux-arm64.json +1 -0
  53. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-64.json +1 -0
  54. package/.pnpm-cache/metadata/registry.npmjs.org/turbo-windows-arm64.json +1 -0
  55. package/.pnpm-cache/metadata/registry.npmjs.org/typescript.json +1 -0
  56. package/.turbo/turbo-lint.log +4 -0
  57. package/dist/assets/index-CneNgzWb.css +1 -0
  58. package/dist/assets/index-DsVhHV0-.js +226 -0
  59. package/dist/index.html +14 -0
  60. package/index.html +13 -0
  61. package/index.ts +70 -0
  62. package/package.json +43 -0
  63. package/postcss.config.cjs +6 -0
  64. package/src/BlueprintCanvasContext.tsx +61 -0
  65. package/src/BlueprintContextMenu.tsx +108 -0
  66. package/src/BlueprintNodeConfigSidebar.tsx +358 -0
  67. package/src/blueprint.css +321 -0
  68. package/src/blueprintNodeTypes.ts +20 -0
  69. package/src/components/BlueprintExecutionLogPanel.tsx +427 -0
  70. package/src/components/BlueprintLibrarySelect.tsx +68 -0
  71. package/src/components/BlueprintMetaDialog.tsx +96 -0
  72. package/src/components/BlueprintNodeSwitchTaskDialog.tsx +58 -0
  73. package/src/components/BlueprintPanelToolbar.tsx +664 -0
  74. package/src/components/ClockNodeConfigPanel.tsx +139 -0
  75. package/src/components/FetchNodeConfigPanel.tsx +716 -0
  76. package/src/components/FetchUrlAutocomplete.tsx +190 -0
  77. package/src/components/JsonNodeConfigPanel.tsx +79 -0
  78. package/src/components/LogicNodeConfigPanel.tsx +80 -0
  79. package/src/components/ViewElementMultiSelect.tsx +140 -0
  80. package/src/createBlueprintEdgeTypes.tsx +10 -0
  81. package/src/dev-seed.ts +34 -0
  82. package/src/dev.css +38 -0
  83. package/src/edges/BlueprintSmoothEdge.tsx +35 -0
  84. package/src/fetch-config-task-store.ts +197 -0
  85. package/src/flowCoordinates.ts +19 -0
  86. package/src/flowDefaults.ts +9 -0
  87. package/src/graph/blueprint-graph.ts +261 -0
  88. package/src/graph/document.ts +384 -0
  89. package/src/graph/index.ts +7 -0
  90. package/src/graph/node-summary.ts +88 -0
  91. package/src/graph/node-types.ts +9 -0
  92. package/src/graph/react-flow-adapter.ts +127 -0
  93. package/src/graph/sync-edges.ts +69 -0
  94. package/src/graph/sync-nodes.ts +104 -0
  95. package/src/hooks/useBlueprintDebugSession.ts +515 -0
  96. package/src/hooks/useBlueprintFlowColorMode.ts +8 -0
  97. package/src/hooks/useBlueprintFlowState.ts +486 -0
  98. package/src/hooks/useBlueprintFlowViewport.ts +54 -0
  99. package/src/hooks/useBlueprintNodeSelectionGuard.ts +63 -0
  100. package/src/hooks/useBlueprintPageLifecycle.ts +245 -0
  101. package/src/index.tsx +271 -0
  102. package/src/library/blueprint-io.ts +108 -0
  103. package/src/library/blueprint-library-db.ts +112 -0
  104. package/src/library/execution-log-db.ts +171 -0
  105. package/src/library/execution-log-settings.ts +50 -0
  106. package/src/library/swagger-docs.ts +56 -0
  107. package/src/library/types.ts +35 -0
  108. package/src/main.tsx +43 -0
  109. package/src/nodes/AndFlowNode.tsx +59 -0
  110. package/src/nodes/BlueprintFlowNode.tsx +26 -0
  111. package/src/nodes/BlueprintNodeCard.tsx +169 -0
  112. package/src/nodes/BlueprintNodeShell.tsx +45 -0
  113. package/src/nodes/ClockFlowNode.tsx +67 -0
  114. package/src/nodes/FetchFlowNode.tsx +26 -0
  115. package/src/nodes/JsonFlowNode.tsx +26 -0
  116. package/src/nodes/LifecycleFlowNode.tsx +44 -0
  117. package/src/nodes/LogicFlowNode.tsx +26 -0
  118. package/src/runtime/document-to-runnable-graph.ts +48 -0
  119. package/src/runtime/execution-overlay.ts +169 -0
  120. package/src/types.ts +1 -0
  121. package/src/vite-env.d.ts +1 -0
  122. package/tailwind.config.ts +6 -0
  123. package/tsconfig.json +12 -0
  124. package/vite.config.ts +16 -0
@@ -0,0 +1,245 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import {
3
+ BlueprintGraphRunner,
4
+ detectBlueprintReferenceCycle,
5
+ type LibraryBlueprintResolver,
6
+ type PageLifecyclePhase,
7
+ } from "@arronqzy/blueprint-dsl";
8
+
9
+ import type { BlueprintGraph } from "../graph/blueprint-graph";
10
+ import { documentToRunnableGraph } from "../runtime/document-to-runnable-graph";
11
+
12
+ const PAGE_BOOT_PHASES: PageLifecyclePhase[] = [
13
+ "created",
14
+ "beforeMount",
15
+ "mounted",
16
+ ];
17
+
18
+ const PAGE_TEARDOWN_PHASES: PageLifecyclePhase[] = [
19
+ "beforeDestroy",
20
+ "destroy",
21
+ ];
22
+
23
+ async function waitForPagePaint() {
24
+ if (document.readyState !== "complete") {
25
+ await new Promise<void>((resolve) => {
26
+ window.addEventListener("load", () => resolve(), { once: true });
27
+ });
28
+ }
29
+ await new Promise<void>((resolve) => {
30
+ requestAnimationFrame(() => {
31
+ requestAnimationFrame(() => resolve());
32
+ });
33
+ });
34
+ }
35
+
36
+ export type UseBlueprintPageLifecycleOptions = {
37
+ graph: BlueprintGraph;
38
+ /** 页面(工作区)是否处于激活状态,用于 activated / deactivated */
39
+ active?: boolean;
40
+ /** 为 false 时不触发任何生命周期阶段 */
41
+ enabled?: boolean;
42
+ /** 挂载时要执行的阶段;默认 created → beforeMount → mounted */
43
+ bootPhases?: PageLifecyclePhase[];
44
+ /**
45
+ * 挂载流程的触发键。提供后会在键变化时重新执行 bootPhases;
46
+ * 未提供时仅在首次满足 enabled 时执行一次。
47
+ */
48
+ bootKey?: unknown;
49
+ /** 为 true 时在执行 bootPhases 前等待 window load 与双 rAF(页面绘制完成) */
50
+ waitForPageReady?: boolean;
51
+ /** 元素或图层变更时是否触发 updated */
52
+ onUpdated?: unknown;
53
+ /** 解析蓝图库记录为可执行子图 */
54
+ resolveLibraryBlueprint?: LibraryBlueprintResolver;
55
+ /** 蓝图库 id -> 名称,用于 signal 中的 blueprintName */
56
+ libraryNameById?: ReadonlyMap<string, string>;
57
+ /** 当前蓝图在蓝图库中的 id;未入库时为 null */
58
+ rootLibraryBlueprintId?: string | null;
59
+ /** 检测到蓝图相互引用死循环时回调 */
60
+ onExecutionBlocked?: (message: string) => void;
61
+ /** 视图绑定节点收到真信号时,为关联视图元素写入 scope */
62
+ onViewScopeUpdate?: (
63
+ viewElementIds: string[],
64
+ scope: unknown
65
+ ) => void;
66
+ };
67
+
68
+ type RunnerOptions = Pick<
69
+ UseBlueprintPageLifecycleOptions,
70
+ | "resolveLibraryBlueprint"
71
+ | "libraryNameById"
72
+ | "rootLibraryBlueprintId"
73
+ | "onExecutionBlocked"
74
+ | "onViewScopeUpdate"
75
+ >;
76
+
77
+ function resolveBlueprintName(
78
+ libraryNameById: ReadonlyMap<string, string> | undefined,
79
+ id: string
80
+ ) {
81
+ return libraryNameById?.get(id) ?? id;
82
+ }
83
+
84
+ function createRunner(graph: BlueprintGraph, options: RunnerOptions) {
85
+ return new BlueprintGraphRunner(
86
+ documentToRunnableGraph(graph.document, {
87
+ libraryNameById: options.libraryNameById,
88
+ }),
89
+ {
90
+ resolveLibraryBlueprint: options.resolveLibraryBlueprint,
91
+ rootLibraryBlueprintId: options.rootLibraryBlueprintId,
92
+ resolveBlueprintName: (id) =>
93
+ resolveBlueprintName(options.libraryNameById, id),
94
+ onViewScopeUpdate: options.onViewScopeUpdate,
95
+ }
96
+ );
97
+ }
98
+
99
+ async function emitPhases(
100
+ graph: BlueprintGraph,
101
+ phases: PageLifecyclePhase[],
102
+ options: RunnerOptions
103
+ ) {
104
+ if (!phases.length) return;
105
+
106
+ if (options.resolveLibraryBlueprint) {
107
+ const result = await detectBlueprintReferenceCycle({
108
+ rootGraph: documentToRunnableGraph(graph.document, {
109
+ libraryNameById: options.libraryNameById,
110
+ }),
111
+ rootLibraryBlueprintId: options.rootLibraryBlueprintId,
112
+ resolveLibraryBlueprint: options.resolveLibraryBlueprint,
113
+ resolveBlueprintName: (id) =>
114
+ resolveBlueprintName(options.libraryNameById, id),
115
+ });
116
+ if (!result.ok) {
117
+ options.onExecutionBlocked?.(result.message);
118
+ return;
119
+ }
120
+ }
121
+
122
+ const runner = createRunner(graph, options);
123
+ for (const phase of phases) {
124
+ await runner.emitLifecycle(phase);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * 将蓝图中的生命周期节点与当前页面生命周期对齐。
130
+ * 在编辑器工作区挂载/卸载、图层切换时自动触发对应钩子。
131
+ */
132
+ export function useBlueprintPageLifecycle({
133
+ graph,
134
+ active = true,
135
+ enabled = true,
136
+ bootPhases = PAGE_BOOT_PHASES,
137
+ bootKey,
138
+ waitForPageReady = false,
139
+ onUpdated,
140
+ resolveLibraryBlueprint,
141
+ libraryNameById,
142
+ rootLibraryBlueprintId = null,
143
+ onExecutionBlocked,
144
+ onViewScopeUpdate,
145
+ }: UseBlueprintPageLifecycleOptions) {
146
+ const graphRef = useRef(graph);
147
+ graphRef.current = graph;
148
+
149
+ const runnerOptionsRef = useRef<RunnerOptions>({
150
+ resolveLibraryBlueprint,
151
+ libraryNameById,
152
+ rootLibraryBlueprintId,
153
+ onExecutionBlocked,
154
+ onViewScopeUpdate,
155
+ });
156
+ runnerOptionsRef.current = {
157
+ resolveLibraryBlueprint,
158
+ libraryNameById,
159
+ rootLibraryBlueprintId,
160
+ onExecutionBlocked,
161
+ onViewScopeUpdate,
162
+ };
163
+
164
+ const bootPhasesKey = bootPhases.join("|");
165
+ const bootTrigger = bootKey ?? "__initial__";
166
+ const [bootCompleted, setBootCompleted] = useState(false);
167
+ const hadBootRef = useRef(false);
168
+ const activeRef = useRef(active);
169
+ activeRef.current = active;
170
+ const prevActiveRef = useRef<boolean | undefined>(undefined);
171
+
172
+ useEffect(() => {
173
+ if (!enabled) {
174
+ setBootCompleted(false);
175
+ return;
176
+ }
177
+
178
+ let cancelled = false;
179
+
180
+ const runBoot = async () => {
181
+ if (waitForPageReady) {
182
+ await waitForPagePaint();
183
+ }
184
+ if (cancelled) return;
185
+
186
+ await emitPhases(
187
+ graphRef.current,
188
+ bootPhases,
189
+ runnerOptionsRef.current
190
+ );
191
+ if (cancelled) return;
192
+
193
+ if (activeRef.current) {
194
+ await emitPhases(
195
+ graphRef.current,
196
+ ["activated"],
197
+ runnerOptionsRef.current
198
+ );
199
+ }
200
+ if (cancelled) return;
201
+
202
+ hadBootRef.current = true;
203
+ prevActiveRef.current = activeRef.current;
204
+ setBootCompleted(true);
205
+ };
206
+
207
+ void runBoot();
208
+
209
+ return () => {
210
+ cancelled = true;
211
+ setBootCompleted(false);
212
+ prevActiveRef.current = undefined;
213
+ if (hadBootRef.current) {
214
+ hadBootRef.current = false;
215
+ void emitPhases(
216
+ graphRef.current,
217
+ PAGE_TEARDOWN_PHASES,
218
+ runnerOptionsRef.current
219
+ );
220
+ }
221
+ };
222
+ }, [bootPhasesKey, bootTrigger, enabled, waitForPageReady]);
223
+
224
+ useEffect(() => {
225
+ if (!enabled || !bootCompleted) return;
226
+
227
+ const prev = prevActiveRef.current;
228
+ if (prev === undefined || prev === active) {
229
+ prevActiveRef.current = active;
230
+ return;
231
+ }
232
+ prevActiveRef.current = active;
233
+
234
+ void emitPhases(
235
+ graphRef.current,
236
+ [active ? "activated" : "deactivated"],
237
+ runnerOptionsRef.current
238
+ );
239
+ }, [active, bootCompleted, enabled]);
240
+
241
+ useEffect(() => {
242
+ if (!enabled || !bootCompleted) return;
243
+ void emitPhases(graphRef.current, ["updated"], runnerOptionsRef.current);
244
+ }, [bootCompleted, enabled, onUpdated]);
245
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,271 @@
1
+ import {
2
+ FC,
3
+ useCallback,
4
+ useRef,
5
+ useState,
6
+ type Dispatch,
7
+ type SetStateAction,
8
+ } from "react";
9
+ import {
10
+ ReactFlow,
11
+ ReactFlowProvider,
12
+ ConnectionMode,
13
+ useReactFlow,
14
+ type Node,
15
+ type Edge,
16
+ } from "@xyflow/react";
17
+ import "@xyflow/react/dist/style.css";
18
+ import "./blueprint.css";
19
+
20
+ import { BlueprintGraph, type BlueprintFlowNodeData } from "./graph";
21
+ import {
22
+ BlueprintContextMenu,
23
+ type BlueprintContextMenuState,
24
+ } from "./BlueprintContextMenu";
25
+ import { BlueprintCanvasProvider } from "./BlueprintCanvasContext";
26
+ import { blueprintNodeTypes } from "./blueprintNodeTypes";
27
+ import { createBlueprintEdgeTypes } from "./createBlueprintEdgeTypes";
28
+ import { BLUEPRINT_DEFAULT_EDGE_OPTIONS } from "./flowDefaults";
29
+ import { useBlueprintFlowColorMode } from "./hooks/useBlueprintFlowColorMode";
30
+ import { useBlueprintFlowState } from "./hooks/useBlueprintFlowState";
31
+ import { useBlueprintFlowViewport } from "./hooks/useBlueprintFlowViewport";
32
+ import { clientToFlowNodePosition } from "./flowCoordinates";
33
+ import type { BlueprintExecutionOverlay } from "./runtime/execution-overlay";
34
+
35
+ export type { BlueprintExecutionOverlay } from "./runtime/execution-overlay";
36
+
37
+ export type { BlueprintNodeConfigSidebarProps, BlueprintViewElementOption, BlueprintLibraryOption } from "./BlueprintNodeConfigSidebar";
38
+ export { BlueprintNodeConfigSidebar } from "./BlueprintNodeConfigSidebar";
39
+
40
+ export type BluePrintReactRootProps = {
41
+ style?: React.CSSProperties;
42
+ graph: BlueprintGraph;
43
+ onGraphChange: Dispatch<SetStateAction<BlueprintGraph>>;
44
+ selectedNodeId?: string | null;
45
+ onSelectNode?: (nodeId: string | null) => void;
46
+ /** 中止正在运行的时钟节点 */
47
+ onAbortClock?: (nodeId: string) => void;
48
+ /** 调试执行时的画布高亮(节点 + 边线信号色) */
49
+ executionOverlay?: BlueprintExecutionOverlay | null;
50
+ /** 蓝图库 id -> 名称,用于画布节点展示引用蓝图名 */
51
+ libraryNameById?: ReadonlyMap<string, string>;
52
+ };
53
+
54
+ type BlueprintCanvasProps = Omit<BluePrintReactRootProps, "style"> & {
55
+ containerRef: React.RefObject<HTMLDivElement | null>;
56
+ };
57
+
58
+ const blueprintEdgeTypes = createBlueprintEdgeTypes();
59
+
60
+ function BlueprintCanvas({
61
+ graph,
62
+ onGraphChange,
63
+ selectedNodeId = null,
64
+ executionOverlay = null,
65
+ onSelectNode,
66
+ onAbortClock,
67
+ libraryNameById,
68
+ containerRef,
69
+ }: BlueprintCanvasProps) {
70
+ const { screenToFlowPosition } = useReactFlow();
71
+ const [menu, setMenu] = useState<BlueprintContextMenuState | null>(null);
72
+
73
+ const applyGraphChange = useCallback(
74
+ (updater: (prev: BlueprintGraph) => BlueprintGraph) => {
75
+ onGraphChange((prev) => updater(prev));
76
+ },
77
+ [onGraphChange]
78
+ );
79
+
80
+ const {
81
+ nodes,
82
+ edges,
83
+ onNodesChange,
84
+ onEdgesChange,
85
+ onNodeDragStop,
86
+ onConnect,
87
+ isValidConnection,
88
+ } = useBlueprintFlowState({
89
+ graph,
90
+ selectedNodeId,
91
+ executionOverlay,
92
+ libraryNameById,
93
+ onGraphChange: applyGraphChange,
94
+ onSelectNode,
95
+ });
96
+
97
+ const flowColorMode = useBlueprintFlowColorMode();
98
+
99
+ useBlueprintFlowViewport(containerRef, true);
100
+
101
+ const onPaneContextMenu = useCallback(
102
+ (event: MouseEvent | React.MouseEvent) => {
103
+ event.preventDefault();
104
+ setMenu({
105
+ kind: "pane",
106
+ clientX: event.clientX,
107
+ clientY: event.clientY,
108
+ });
109
+ },
110
+ []
111
+ );
112
+
113
+ const onNodeContextMenu = useCallback(
114
+ (
115
+ event: MouseEvent | React.MouseEvent,
116
+ node: Node<BlueprintFlowNodeData>
117
+ ) => {
118
+ event.preventDefault();
119
+ onSelectNode?.(node.id);
120
+ setMenu({
121
+ kind: "node",
122
+ clientX: event.clientX,
123
+ clientY: event.clientY,
124
+ nodeId: node.id,
125
+ role: node.data.role,
126
+ });
127
+ },
128
+ [onSelectNode]
129
+ );
130
+
131
+ const onEdgeContextMenu = useCallback(
132
+ (event: MouseEvent | React.MouseEvent, edge: Edge) => {
133
+ event.preventDefault();
134
+ setMenu({
135
+ kind: "edge",
136
+ clientX: event.clientX,
137
+ clientY: event.clientY,
138
+ edgeId: edge.id,
139
+ });
140
+ },
141
+ []
142
+ );
143
+
144
+ const onEdgeClick = useCallback(() => {
145
+ setMenu(null);
146
+ }, []);
147
+
148
+ const onPaneClick = useCallback(() => {
149
+ setMenu(null);
150
+ onSelectNode?.(null);
151
+ }, [onSelectNode]);
152
+
153
+ const setGraph = useCallback(
154
+ (updater: (prev: BlueprintGraph) => BlueprintGraph) => {
155
+ applyGraphChange(updater);
156
+ },
157
+ [applyGraphChange]
158
+ );
159
+
160
+ const handleAddBlueprintNode = useCallback(
161
+ (clientX: number, clientY: number) => {
162
+ const position = clientToFlowNodePosition(
163
+ (point) => screenToFlowPosition(point, { snapToGrid: false }),
164
+ clientX,
165
+ clientY
166
+ );
167
+ setGraph((prev) => prev.addBlueprintNode(position));
168
+ },
169
+ [screenToFlowPosition, setGraph]
170
+ );
171
+
172
+ const handleDeleteNode = useCallback(
173
+ (nodeId: string) => {
174
+ if (nodeId === selectedNodeId) {
175
+ onSelectNode?.(null);
176
+ }
177
+ setGraph((prev) => prev.removeNode(nodeId));
178
+ },
179
+ [onSelectNode, selectedNodeId, setGraph]
180
+ );
181
+
182
+ const handleDeleteEdge = useCallback(
183
+ (edgeId: string) => {
184
+ setGraph((prev) => prev.removeEdge(edgeId));
185
+ },
186
+ [setGraph]
187
+ );
188
+
189
+ return (
190
+ <>
191
+ <ReactFlow
192
+ className="bp-flow h-full w-full"
193
+ colorMode={flowColorMode}
194
+ nodes={nodes}
195
+ edges={edges}
196
+ nodeTypes={blueprintNodeTypes}
197
+ edgeTypes={blueprintEdgeTypes}
198
+ onNodesChange={onNodesChange}
199
+ onEdgesChange={onEdgesChange}
200
+ onConnect={onConnect}
201
+ isValidConnection={isValidConnection}
202
+ onPaneContextMenu={onPaneContextMenu}
203
+ onNodeContextMenu={onNodeContextMenu}
204
+ onEdgeContextMenu={onEdgeContextMenu}
205
+ onEdgeClick={onEdgeClick}
206
+ onNodeDragStop={onNodeDragStop}
207
+ onPaneClick={onPaneClick}
208
+ nodesConnectable
209
+ elementsSelectable
210
+ edgesFocusable
211
+ connectionMode={ConnectionMode.Strict}
212
+ defaultEdgeOptions={BLUEPRINT_DEFAULT_EDGE_OPTIONS}
213
+ selectNodesOnDrag={false}
214
+ elevateNodesOnSelect={false}
215
+ nodeClickDistance={8}
216
+ proOptions={{ hideAttribution: true }}
217
+ />
218
+ <BlueprintContextMenu
219
+ menu={menu}
220
+ onClose={() => setMenu(null)}
221
+ onAddBlueprintNode={handleAddBlueprintNode}
222
+ onDeleteNode={handleDeleteNode}
223
+ onDeleteEdge={handleDeleteEdge}
224
+ />
225
+ </>
226
+ );
227
+ }
228
+
229
+ export const BluePrintReactRoot: FC<BluePrintReactRootProps> = ({
230
+ style,
231
+ graph,
232
+ onGraphChange,
233
+ selectedNodeId,
234
+ executionOverlay,
235
+ onSelectNode,
236
+ onAbortClock,
237
+ libraryNameById,
238
+ }) => {
239
+ const containerRef = useRef<HTMLDivElement>(null);
240
+
241
+ return (
242
+ <div
243
+ ref={containerRef}
244
+ data-workspace-region="blueprint"
245
+ className="bp-canvas h-full w-full bg-background text-foreground"
246
+ style={{
247
+ width: "100%",
248
+ height: "100%",
249
+ minHeight: 0,
250
+ ...style,
251
+ }}
252
+ >
253
+ <BlueprintCanvasProvider
254
+ onSelectNode={onSelectNode}
255
+ onAbortClock={onAbortClock}
256
+ >
257
+ <ReactFlowProvider>
258
+ <BlueprintCanvas
259
+ containerRef={containerRef}
260
+ graph={graph}
261
+ onGraphChange={onGraphChange}
262
+ selectedNodeId={selectedNodeId}
263
+ executionOverlay={executionOverlay}
264
+ onSelectNode={onSelectNode}
265
+ libraryNameById={libraryNameById}
266
+ />
267
+ </ReactFlowProvider>
268
+ </BlueprintCanvasProvider>
269
+ </div>
270
+ );
271
+ };
@@ -0,0 +1,108 @@
1
+ import type { BlueprintDocument } from "../graph/document";
2
+ import { createNodeId } from "../graph/document";
3
+ import {
4
+ BLUEPRINT_EXPORT_KIND,
5
+ BLUEPRINT_EXPORT_VERSION,
6
+ type BlueprintExportPayload,
7
+ type BlueprintLibraryRecord,
8
+ type BlueprintLibrarySource,
9
+ type BlueprintMetaDraft,
10
+ } from "./types";
11
+
12
+ export function createLibraryBlueprintId() {
13
+ return createNodeId("lib_bp");
14
+ }
15
+
16
+ export function blueprintDocumentsEqual(
17
+ a: BlueprintDocument,
18
+ b: BlueprintDocument
19
+ ): boolean {
20
+ return JSON.stringify(a) === JSON.stringify(b);
21
+ }
22
+
23
+ export function buildBlueprintExportPayload(
24
+ document: BlueprintDocument,
25
+ meta: BlueprintMetaDraft
26
+ ): BlueprintExportPayload {
27
+ return {
28
+ kind: BLUEPRINT_EXPORT_KIND,
29
+ version: BLUEPRINT_EXPORT_VERSION,
30
+ name: meta.name.trim() || "未命名蓝图",
31
+ remark: meta.remark.trim() || undefined,
32
+ exportedAt: Date.now(),
33
+ document: {
34
+ ...document,
35
+ name: meta.name.trim() || document.name,
36
+ },
37
+ };
38
+ }
39
+
40
+ export function parseBlueprintImportFile(raw: unknown): BlueprintExportPayload {
41
+ if (!raw || typeof raw !== "object") {
42
+ throw new Error("invalid-format");
43
+ }
44
+ const data = raw as Partial<BlueprintExportPayload>;
45
+ if (data.kind !== BLUEPRINT_EXPORT_KIND || data.version !== BLUEPRINT_EXPORT_VERSION) {
46
+ throw new Error("unsupported-version");
47
+ }
48
+ if (!data.document || !Array.isArray(data.document.nodes) || !Array.isArray(data.document.edges)) {
49
+ throw new Error("invalid-document");
50
+ }
51
+ return {
52
+ kind: BLUEPRINT_EXPORT_KIND,
53
+ version: BLUEPRINT_EXPORT_VERSION,
54
+ name: typeof data.name === "string" ? data.name : "导入的蓝图",
55
+ remark: typeof data.remark === "string" ? data.remark : undefined,
56
+ exportedAt: typeof data.exportedAt === "number" ? data.exportedAt : Date.now(),
57
+ document: data.document,
58
+ };
59
+ }
60
+
61
+ export function downloadBlueprintExport(payload: BlueprintExportPayload) {
62
+ const blob = new Blob([JSON.stringify(payload, null, 2)], {
63
+ type: "application/json",
64
+ });
65
+ const url = URL.createObjectURL(blob);
66
+ const anchor = document.createElement("a");
67
+ anchor.href = url;
68
+ const safeName = payload.name
69
+ .replace(/[\\/:*?"<>|]/g, "-")
70
+ .replace(/\s+/g, "-");
71
+ anchor.download = `${safeName || "blueprint"}-${Date.now()}.json`;
72
+ anchor.click();
73
+ URL.revokeObjectURL(url);
74
+ }
75
+
76
+ export function buildLibraryRecord(args: {
77
+ document: BlueprintDocument;
78
+ meta: BlueprintMetaDraft;
79
+ source: BlueprintLibrarySource;
80
+ id?: string;
81
+ createdAt?: number;
82
+ }): BlueprintLibraryRecord {
83
+ const now = Date.now();
84
+ const name = args.meta.name.trim() || "未命名蓝图";
85
+ return {
86
+ id: args.id ?? createLibraryBlueprintId(),
87
+ name,
88
+ remark: args.meta.remark.trim() || undefined,
89
+ source: args.source,
90
+ createdAt: args.createdAt ?? now,
91
+ updatedAt: now,
92
+ document: {
93
+ ...args.document,
94
+ name,
95
+ },
96
+ };
97
+ }
98
+
99
+ export function libraryRecordFromImport(payload: BlueprintExportPayload): BlueprintLibraryRecord {
100
+ return buildLibraryRecord({
101
+ document: payload.document,
102
+ meta: {
103
+ name: payload.name,
104
+ remark: payload.remark ?? "",
105
+ },
106
+ source: "imported",
107
+ });
108
+ }