@foresthubai/workflow-builder 0.3.0 → 0.4.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 (120) hide show
  1. package/LICENSE +661 -661
  2. package/NOTICE +16 -16
  3. package/README.md +110 -93
  4. package/dist/components/ui/command.d.ts +2 -2
  5. package/dist/components/ui/input.d.ts +1 -1
  6. package/dist/components/ui/resizable.d.ts +1 -1
  7. package/dist/components/ui/textarea.d.ts +1 -1
  8. package/dist/graph/BaseNode.js +10 -10
  9. package/dist/graph/reactFlowRegistry.d.ts.map +1 -1
  10. package/dist/lib/utils.d.ts +3 -0
  11. package/dist/lib/utils.d.ts.map +1 -0
  12. package/dist/lib/utils.js +6 -0
  13. package/dist/lib/utils.js.map +1 -0
  14. package/dist/toolbars/CanvasTabsToolbar.d.ts +11 -0
  15. package/dist/toolbars/CanvasTabsToolbar.d.ts.map +1 -0
  16. package/dist/toolbars/CanvasTabsToolbar.js +101 -0
  17. package/dist/toolbars/CanvasTabsToolbar.js.map +1 -0
  18. package/package.json +2 -2
  19. package/src/BuilderLayout.tsx +345 -345
  20. package/src/Canvas.tsx +261 -261
  21. package/src/CanvasEditor.tsx +142 -142
  22. package/src/CanvasTabsToolbar.tsx +176 -176
  23. package/src/RightConfigPanel.tsx +266 -266
  24. package/src/WorkflowBuilder.tsx +412 -412
  25. package/src/cn.ts +6 -6
  26. package/src/components/ui/add-button.tsx +39 -39
  27. package/src/components/ui/alert-dialog.tsx +141 -141
  28. package/src/components/ui/alert.tsx +59 -59
  29. package/src/components/ui/badge.tsx +36 -36
  30. package/src/components/ui/button.tsx +85 -85
  31. package/src/components/ui/card.tsx +79 -79
  32. package/src/components/ui/checkbox.tsx +28 -28
  33. package/src/components/ui/collapsible.tsx +9 -9
  34. package/src/components/ui/command.tsx +153 -153
  35. package/src/components/ui/delete-button.tsx +23 -23
  36. package/src/components/ui/dialog.tsx +125 -125
  37. package/src/components/ui/dropdown-menu.tsx +198 -198
  38. package/src/components/ui/input.tsx +55 -55
  39. package/src/components/ui/label.tsx +24 -24
  40. package/src/components/ui/readonly-banner.tsx +15 -15
  41. package/src/components/ui/resizable.tsx +43 -43
  42. package/src/components/ui/scroll-area.tsx +102 -102
  43. package/src/components/ui/select.tsx +160 -160
  44. package/src/components/ui/separator.tsx +29 -29
  45. package/src/components/ui/switch.tsx +27 -27
  46. package/src/components/ui/textarea.tsx +51 -51
  47. package/src/components/ui/toast.tsx +127 -127
  48. package/src/components/ui/toaster.tsx +33 -33
  49. package/src/components/ui/toggle-group.tsx +59 -59
  50. package/src/components/ui/toggle.tsx +43 -43
  51. package/src/components/ui/tooltip.tsx +32 -32
  52. package/src/dialogs/NodePickerDialog.tsx +84 -84
  53. package/src/dialogs/ValidationDialog.tsx +184 -184
  54. package/src/graph/BaseNode.tsx +557 -557
  55. package/src/graph/CustomEdge.tsx +185 -185
  56. package/src/graph/CustomNode.tsx +16 -16
  57. package/src/graph/FunctionCallNode.tsx +30 -30
  58. package/src/graph/PortHandle.tsx +189 -189
  59. package/src/graph/reactFlowRegistry.ts +26 -26
  60. package/src/hooks/use-toast.ts +125 -125
  61. package/src/hooks/useAvailableVariables.ts +20 -20
  62. package/src/hooks/useCanvasHistory.ts +22 -22
  63. package/src/hooks/useCanvasTabs.ts +168 -168
  64. package/src/hooks/useFunctionDiagnosticsSync.ts +40 -40
  65. package/src/hooks/useFunctionRegistry.ts +26 -26
  66. package/src/hooks/useFunctions.ts +44 -44
  67. package/src/hooks/useGraph.ts +161 -161
  68. package/src/hooks/useNodeDefinitions.ts +82 -82
  69. package/src/hooks/useParamErrors.ts +26 -26
  70. package/src/hooks/useResolvedTheme.ts +30 -30
  71. package/src/hooks/useResourceDiagnosticsSync.ts +58 -58
  72. package/src/hooks/useSuppressThemeTransition.ts +79 -79
  73. package/src/hooks/useWorkflowSerialization.ts +127 -127
  74. package/src/i18n/index.ts +53 -53
  75. package/src/i18n/locales/de.json +501 -501
  76. package/src/i18n/locales/en.json +557 -557
  77. package/src/index.ts +27 -27
  78. package/src/inputs/ExpressionInput.tsx +297 -297
  79. package/src/inputs/ParameterEditor.tsx +515 -515
  80. package/src/inputs/PortSection.tsx +144 -144
  81. package/src/panels/BuilderSidebar.tsx +301 -301
  82. package/src/panels/ChannelConfigPanel.tsx +49 -49
  83. package/src/panels/ChannelsPanel.tsx +28 -28
  84. package/src/panels/DebugConsolePanel.tsx +73 -73
  85. package/src/panels/DebugContextPanel.tsx +77 -77
  86. package/src/panels/DebugExternalIOPanel.tsx +180 -180
  87. package/src/panels/DiagnosticsPanel.tsx +170 -170
  88. package/src/panels/EdgeConfigPanel.tsx +104 -104
  89. package/src/panels/FunctionConfigPanel.tsx +179 -179
  90. package/src/panels/FunctionListPanel.tsx +45 -45
  91. package/src/panels/MemoryConfigPanel.tsx +55 -55
  92. package/src/panels/MemoryPanel.tsx +40 -40
  93. package/src/panels/ModelConfigPanel.tsx +41 -41
  94. package/src/panels/ModelsPanel.tsx +36 -36
  95. package/src/panels/NodeConfigPanel.tsx +630 -630
  96. package/src/panels/NodeLibrary.tsx +288 -288
  97. package/src/panels/ResourceConfigPanel.tsx +132 -132
  98. package/src/panels/ResourceListPanel.tsx +113 -113
  99. package/src/panels/VariableConfigPanel.tsx +161 -161
  100. package/src/panels/VariablesPanel.tsx +145 -145
  101. package/src/stores/canvasStore.test.ts +44 -44
  102. package/src/stores/canvasStore.ts +245 -245
  103. package/src/stores/debugStore.ts +74 -74
  104. package/src/stores/diagnosticsStore.ts +130 -130
  105. package/src/stores/editorStore.ts +202 -202
  106. package/src/styles/index.css +526 -526
  107. package/src/utils/categoryConstants.ts +26 -26
  108. package/src/utils/channelOperations.ts +86 -86
  109. package/src/utils/connectionRules.ts +137 -137
  110. package/src/utils/functionOperations.ts +179 -179
  111. package/src/utils/graphOperations.ts +550 -550
  112. package/src/utils/history.ts +207 -207
  113. package/src/utils/memoryOperations.ts +57 -57
  114. package/src/utils/migrateFunctionNodes.ts +107 -107
  115. package/src/utils/modelOperations.ts +55 -55
  116. package/src/utils/paramDisplay.ts +71 -71
  117. package/src/utils/resourceHelpers.ts +32 -32
  118. package/src/utils/translation.ts +28 -28
  119. package/src/utils/variableOperations.ts +75 -75
  120. package/tailwind-preset.ts +166 -166
@@ -1,40 +1,40 @@
1
- import { useEffect } from "react";
2
- import { validateFunction } from "@foresthubai/workflow-core/diagnostics";
3
- import { computeAvailableVariables } from "@foresthubai/workflow-core/variable";
4
- import { useEditorStore } from "../stores/editorStore";
5
- import { useDiagnosticsStore } from "../stores/diagnosticsStore";
6
- import { getCanvasStore } from "../stores/canvasStore";
7
-
8
- /**
9
- * Keeps the `byFunctionId` diagnostics slot in sync with the function declarations —
10
- * the single source for the sidebar tab badge, the function list ring, AND the config
11
- * panel's per-output rings (they all read this slot). Mounted once at the
12
- * workflow-builder root so it survives tab open/close.
13
- *
14
- * Scope-aware: each function is validated against its own body canvas's variables, so
15
- * invalid/typed return expressions surface here too — not just missing ones. (Reacts
16
- * to declaration/expression edits via `functions`; a body-only edit that changes the
17
- * available variables without touching a declaration refreshes on the next
18
- * declaration change or a full `validate`.)
19
- */
20
- export function useFunctionDiagnosticsSync(): void {
21
- const functions = useEditorStore((s) => s.functions);
22
-
23
- useEffect(() => {
24
- const ds = useDiagnosticsStore.getState();
25
-
26
- const seen = new Set<string>();
27
- for (const [id, def] of Object.entries(functions)) {
28
- seen.add(id);
29
- const store = getCanvasStore(id);
30
- const lookup = store
31
- ? computeAvailableVariables(store.getState().variables, store.getState().edges).lookup
32
- : undefined;
33
- ds.setFunctionDiagnostics(id, validateFunction(def, lookup));
34
- }
35
-
36
- for (const id of Object.keys(ds.byFunctionId)) {
37
- if (!seen.has(id)) ds.clearFunctionDiagnostics(id);
38
- }
39
- }, [functions]);
40
- }
1
+ import { useEffect } from "react";
2
+ import { validateFunction } from "@foresthubai/workflow-core/diagnostics";
3
+ import { computeAvailableVariables } from "@foresthubai/workflow-core/variable";
4
+ import { useEditorStore } from "../stores/editorStore";
5
+ import { useDiagnosticsStore } from "../stores/diagnosticsStore";
6
+ import { getCanvasStore } from "../stores/canvasStore";
7
+
8
+ /**
9
+ * Keeps the `byFunctionId` diagnostics slot in sync with the function declarations —
10
+ * the single source for the sidebar tab badge, the function list ring, AND the config
11
+ * panel's per-output rings (they all read this slot). Mounted once at the
12
+ * workflow-builder root so it survives tab open/close.
13
+ *
14
+ * Scope-aware: each function is validated against its own body canvas's variables, so
15
+ * invalid/typed return expressions surface here too — not just missing ones. (Reacts
16
+ * to declaration/expression edits via `functions`; a body-only edit that changes the
17
+ * available variables without touching a declaration refreshes on the next
18
+ * declaration change or a full `validate`.)
19
+ */
20
+ export function useFunctionDiagnosticsSync(): void {
21
+ const functions = useEditorStore((s) => s.functions);
22
+
23
+ useEffect(() => {
24
+ const ds = useDiagnosticsStore.getState();
25
+
26
+ const seen = new Set<string>();
27
+ for (const [id, def] of Object.entries(functions)) {
28
+ seen.add(id);
29
+ const store = getCanvasStore(id);
30
+ const lookup = store
31
+ ? computeAvailableVariables(store.getState().variables, store.getState().edges).lookup
32
+ : undefined;
33
+ ds.setFunctionDiagnostics(id, validateFunction(def, lookup));
34
+ }
35
+
36
+ for (const id of Object.keys(ds.byFunctionId)) {
37
+ if (!seen.has(id)) ds.clearFunctionDiagnostics(id);
38
+ }
39
+ }, [functions]);
40
+ }
@@ -1,26 +1,26 @@
1
- import { useCallback, useMemo } from "react";
2
- import { useEditorStore } from "../stores/editorStore";
3
- import type { FunctionDeclaration } from "@foresthubai/workflow-core/function";
4
-
5
- // Functions are a project-scoped resource: the registry IS editorStore.functions —
6
- // the domain FunctionDeclaration, no conversion. (Crossing to the flat api FunctionInfo
7
- // is done only when stamping a call-site snapshot; see useNodeDefinitions/migration.)
8
-
9
- /**
10
- * Access to all function declarations by id. Reactive over editorStore.functions.
11
- * - functions: Record of FunctionDeclaration by id
12
- * - functionsList: array of FunctionDeclaration
13
- * - getFunction(id): one declaration by id
14
- */
15
- export function useFunctionRegistry() {
16
- const functions = useEditorStore((s) => s.functions);
17
- const functionsList = useMemo(() => Object.values(functions), [functions]);
18
- const getFunction = useCallback((id: string): FunctionDeclaration | undefined => functions[id], [functions]);
19
-
20
- return { functions, functionsList, getFunction };
21
- }
22
-
23
- /** All function declarations without React subscription (for non-component code). */
24
- export function getAllFunctions(): Record<string, FunctionDeclaration> {
25
- return useEditorStore.getState().functions;
26
- }
1
+ import { useCallback, useMemo } from "react";
2
+ import { useEditorStore } from "../stores/editorStore";
3
+ import type { FunctionDeclaration } from "@foresthubai/workflow-core/function";
4
+
5
+ // Functions are a project-scoped resource: the registry IS editorStore.functions —
6
+ // the domain FunctionDeclaration, no conversion. (Crossing to the flat api FunctionInfo
7
+ // is done only when stamping a call-site snapshot; see useNodeDefinitions/migration.)
8
+
9
+ /**
10
+ * Access to all function declarations by id. Reactive over editorStore.functions.
11
+ * - functions: Record of FunctionDeclaration by id
12
+ * - functionsList: array of FunctionDeclaration
13
+ * - getFunction(id): one declaration by id
14
+ */
15
+ export function useFunctionRegistry() {
16
+ const functions = useEditorStore((s) => s.functions);
17
+ const functionsList = useMemo(() => Object.values(functions), [functions]);
18
+ const getFunction = useCallback((id: string): FunctionDeclaration | undefined => functions[id], [functions]);
19
+
20
+ return { functions, functionsList, getFunction };
21
+ }
22
+
23
+ /** All function declarations without React subscription (for non-component code). */
24
+ export function getAllFunctions(): Record<string, FunctionDeclaration> {
25
+ return useEditorStore.getState().functions;
26
+ }
@@ -1,44 +1,44 @@
1
- import { useCallback } from "react";
2
- import { useEditorStore } from "../stores/editorStore";
3
- import { getOrCreateCanvasStore } from "../stores/canvasStore";
4
- import { useFunctionRegistry } from "./useFunctionRegistry";
5
- import { addFunction } from "../utils/functionOperations";
6
-
7
- export interface UseFunctionsOptions {
8
- /** Open (or focus) a tab for a function canvas. */
9
- onOpenTab: (id: string, label: string) => void;
10
- }
11
-
12
- /**
13
- * Coordinates the canvas-tab UI with function declarations (editorStore). The
14
- * declarations themselves are CRUD'd in utils/functionOperations; this hook only
15
- * handles the open/create flows that must also touch the tab strip and selection.
16
- */
17
- export const useFunctions = ({ onOpenTab }: UseFunctionsOptions) => {
18
- const { functionsList: functions, getFunction } = useFunctionRegistry();
19
- const selectFunction = useEditorStore((s) => s.selectFunction);
20
-
21
- // Open an existing function: ensure its body canvas exists, open its tab, and
22
- // select it so the right panel shows its definition. onOpenTab switches the active
23
- // canvas first; selectFunction sets the selection last so it isn't cleared.
24
- const openFunction = useCallback(
25
- (functionId: string) => {
26
- const fn = getFunction(functionId);
27
- if (!fn) return;
28
- getOrCreateCanvasStore(functionId);
29
- onOpenTab(functionId, fn.name);
30
- selectFunction(functionId);
31
- },
32
- [getFunction, onOpenTab, selectFunction],
33
- );
34
-
35
- // Create a new function and open it (the canvas body is created by addFunction).
36
- const createFunction = useCallback(() => {
37
- const fn = addFunction();
38
- onOpenTab(fn.id, fn.name);
39
- selectFunction(fn.id);
40
- return fn.id;
41
- }, [onOpenTab, selectFunction]);
42
-
43
- return { functions, openFunction, createFunction };
44
- };
1
+ import { useCallback } from "react";
2
+ import { useEditorStore } from "../stores/editorStore";
3
+ import { getOrCreateCanvasStore } from "../stores/canvasStore";
4
+ import { useFunctionRegistry } from "./useFunctionRegistry";
5
+ import { addFunction } from "../utils/functionOperations";
6
+
7
+ export interface UseFunctionsOptions {
8
+ /** Open (or focus) a tab for a function canvas. */
9
+ onOpenTab: (id: string, label: string) => void;
10
+ }
11
+
12
+ /**
13
+ * Coordinates the canvas-tab UI with function declarations (editorStore). The
14
+ * declarations themselves are CRUD'd in utils/functionOperations; this hook only
15
+ * handles the open/create flows that must also touch the tab strip and selection.
16
+ */
17
+ export const useFunctions = ({ onOpenTab }: UseFunctionsOptions) => {
18
+ const { functionsList: functions, getFunction } = useFunctionRegistry();
19
+ const selectFunction = useEditorStore((s) => s.selectFunction);
20
+
21
+ // Open an existing function: ensure its body canvas exists, open its tab, and
22
+ // select it so the right panel shows its definition. onOpenTab switches the active
23
+ // canvas first; selectFunction sets the selection last so it isn't cleared.
24
+ const openFunction = useCallback(
25
+ (functionId: string) => {
26
+ const fn = getFunction(functionId);
27
+ if (!fn) return;
28
+ getOrCreateCanvasStore(functionId);
29
+ onOpenTab(functionId, fn.name);
30
+ selectFunction(functionId);
31
+ },
32
+ [getFunction, onOpenTab, selectFunction],
33
+ );
34
+
35
+ // Create a new function and open it (the canvas body is created by addFunction).
36
+ const createFunction = useCallback(() => {
37
+ const fn = addFunction();
38
+ onOpenTab(fn.id, fn.name);
39
+ selectFunction(fn.id);
40
+ return fn.id;
41
+ }, [onOpenTab, selectFunction]);
42
+
43
+ return { functions, openFunction, createFunction };
44
+ };
@@ -1,161 +1,161 @@
1
- import { NodeDefinition, NodeData } from "@foresthubai/workflow-core/node";
2
- import { Connection, Edge, Node } from "@xyflow/react";
3
- import type { EdgeData } from "@foresthubai/workflow-core/edge";
4
- import { useCallback } from "react";
5
- import { getOrCreateCanvasStore, MAIN_CANVAS_ID } from "../stores/canvasStore";
6
- import {
7
- addNodeToStore,
8
- type Clipboard,
9
- connectNodesInStore,
10
- deleteEdgesFromStore,
11
- deleteNodeFromStore,
12
- pasteToStore,
13
- updateEdgeInStore,
14
- updateNodeInStore,
15
- } from "../utils/graphOperations";
16
- import type { EdgeType } from "@foresthubai/workflow-core/edge";
17
- import { useNodeDefinitions } from "./useNodeDefinitions";
18
-
19
- // Shared across all useGraph instances (one per canvas) so copy/paste works
20
- // across canvas switches within a single builder session.
21
- const clipboardRef: { current: Clipboard | null } = { current: null };
22
-
23
- /**
24
- * Hook that provides graph management actions for a specific canvas.
25
- *
26
- * All mutations are:
27
- * - Gated by readOnly (no-op when true)
28
- * - Automatically wrapped in a history checkpoint (undo/redo)
29
- *
30
- * Callers don't need to worry about either concern.
31
- */
32
- export const useGraph = (canvasId: string = MAIN_CANVAS_ID, readOnly: boolean = false) => {
33
- const canvasStore = getOrCreateCanvasStore(canvasId);
34
- const nodes = canvasStore((s) => s.nodes);
35
- const edges = canvasStore((s) => s.edges);
36
- const { withCheckpoint } = canvasStore;
37
-
38
- const { getNodeDefinition } = useNodeDefinitions();
39
-
40
- // Guarded checkpoint: skips when readOnly, otherwise wraps in undo history entry
41
- const guarded = useCallback(
42
- <R>(operation: () => R): R | undefined => {
43
- if (readOnly) return undefined;
44
- return withCheckpoint(operation);
45
- },
46
- [readOnly, withCheckpoint],
47
- );
48
-
49
- const addNode = useCallback(
50
- (nodeDef: NodeDefinition, position?: { x: number; y: number }) => {
51
- return guarded(() => addNodeToStore(canvasStore, nodeDef, position)) ?? null;
52
- },
53
- [canvasStore, guarded],
54
- );
55
-
56
- const updateNode = useCallback(
57
- (nodeId: string, updates: { arguments?: Record<string, unknown>; label?: string }) => {
58
- guarded(() => updateNodeInStore(canvasStore, nodeId, updates));
59
- },
60
- [canvasStore, guarded],
61
- );
62
-
63
- const deleteNode = useCallback(
64
- (nodeId: string) => {
65
- guarded(() => deleteNodeFromStore(canvasStore, nodeId, getNodeDefinition));
66
- },
67
- [canvasStore, getNodeDefinition, guarded],
68
- );
69
-
70
- const onConnect = useCallback(
71
- (connection: Connection): EdgeType | false => {
72
- return guarded(() => connectNodesInStore(canvasStore, connection)) ?? false;
73
- },
74
- [canvasStore, guarded],
75
- );
76
-
77
- const updateEdge = useCallback(
78
- (edgeId: string, updates: Record<string, unknown>) => {
79
- guarded(() => updateEdgeInStore(canvasStore, edgeId, updates));
80
- },
81
- [canvasStore, guarded],
82
- );
83
-
84
- const deleteEdges = useCallback(
85
- (edgeIds: string[]) => {
86
- guarded(() => deleteEdgesFromStore(canvasStore, edgeIds));
87
- },
88
- [canvasStore, guarded],
89
- );
90
-
91
- // Batch delete nodes and edges as a single undo entry
92
- const deleteSelected = useCallback(
93
- (nodeIds: string[], edgeIds: string[]) => {
94
- if (nodeIds.length === 0 && edgeIds.length === 0) return;
95
- guarded(() => {
96
- for (const nodeId of nodeIds) deleteNodeFromStore(canvasStore, nodeId, getNodeDefinition);
97
- if (edgeIds.length > 0) deleteEdgesFromStore(canvasStore, edgeIds);
98
- });
99
- },
100
- [canvasStore, getNodeDefinition, guarded],
101
- );
102
-
103
- // Copy is read-only — no checkpoint, no readOnly gate
104
- const copySelection = useCallback(
105
- (nodeIds: string[]) => {
106
- if (nodeIds.length === 0) return;
107
-
108
- const nodeIdSet = new Set(nodeIds);
109
-
110
- const copiedNodes = nodes
111
- .filter((node) => nodeIdSet.has(node.id))
112
- .map((node) => JSON.parse(JSON.stringify(node)) as Node<NodeData>);
113
-
114
- const copiedEdges = edges
115
- .filter((edge) => nodeIdSet.has(edge.source) && nodeIdSet.has(edge.target))
116
- .map((edge) => JSON.parse(JSON.stringify(edge)) as Edge<EdgeData>);
117
-
118
- clipboardRef.current = { nodes: copiedNodes, edges: copiedEdges };
119
- },
120
- [nodes, edges],
121
- );
122
-
123
- const pasteSelection = useCallback(
124
- (offset?: { x: number; y: number }) => {
125
- const clipboard = clipboardRef.current;
126
- if (!clipboard) return undefined;
127
- return guarded(() => pasteToStore(canvasStore, clipboard, offset, getNodeDefinition));
128
- },
129
- [canvasStore, getNodeDefinition, guarded],
130
- );
131
-
132
- // Batch add node + connect as a single undo entry (used by contextual node picker)
133
- const addNodeAndConnect = useCallback(
134
- (nodeDef: NodeDefinition, position: { x: number; y: number }, connection: Connection): string | null => {
135
- return (
136
- guarded(() => {
137
- const nodeId = addNodeToStore(canvasStore, nodeDef, position);
138
- if (nodeId == null) return null;
139
- connectNodesInStore(canvasStore, { ...connection, target: nodeId });
140
- return nodeId;
141
- }) ?? null
142
- );
143
- },
144
- [canvasStore, guarded],
145
- );
146
-
147
- return {
148
- nodes,
149
- edges,
150
- addNode,
151
- updateNode,
152
- updateEdge,
153
- deleteNode,
154
- deleteEdges,
155
- deleteSelected,
156
- onConnect,
157
- addNodeAndConnect,
158
- copySelection,
159
- pasteSelection,
160
- };
161
- };
1
+ import { NodeDefinition, NodeData } from "@foresthubai/workflow-core/node";
2
+ import { Connection, Edge, Node } from "@xyflow/react";
3
+ import type { EdgeData } from "@foresthubai/workflow-core/edge";
4
+ import { useCallback } from "react";
5
+ import { getOrCreateCanvasStore, MAIN_CANVAS_ID } from "../stores/canvasStore";
6
+ import {
7
+ addNodeToStore,
8
+ type Clipboard,
9
+ connectNodesInStore,
10
+ deleteEdgesFromStore,
11
+ deleteNodeFromStore,
12
+ pasteToStore,
13
+ updateEdgeInStore,
14
+ updateNodeInStore,
15
+ } from "../utils/graphOperations";
16
+ import type { EdgeType } from "@foresthubai/workflow-core/edge";
17
+ import { useNodeDefinitions } from "./useNodeDefinitions";
18
+
19
+ // Shared across all useGraph instances (one per canvas) so copy/paste works
20
+ // across canvas switches within a single builder session.
21
+ const clipboardRef: { current: Clipboard | null } = { current: null };
22
+
23
+ /**
24
+ * Hook that provides graph management actions for a specific canvas.
25
+ *
26
+ * All mutations are:
27
+ * - Gated by readOnly (no-op when true)
28
+ * - Automatically wrapped in a history checkpoint (undo/redo)
29
+ *
30
+ * Callers don't need to worry about either concern.
31
+ */
32
+ export const useGraph = (canvasId: string = MAIN_CANVAS_ID, readOnly: boolean = false) => {
33
+ const canvasStore = getOrCreateCanvasStore(canvasId);
34
+ const nodes = canvasStore((s) => s.nodes);
35
+ const edges = canvasStore((s) => s.edges);
36
+ const { withCheckpoint } = canvasStore;
37
+
38
+ const { getNodeDefinition } = useNodeDefinitions();
39
+
40
+ // Guarded checkpoint: skips when readOnly, otherwise wraps in undo history entry
41
+ const guarded = useCallback(
42
+ <R>(operation: () => R): R | undefined => {
43
+ if (readOnly) return undefined;
44
+ return withCheckpoint(operation);
45
+ },
46
+ [readOnly, withCheckpoint],
47
+ );
48
+
49
+ const addNode = useCallback(
50
+ (nodeDef: NodeDefinition, position?: { x: number; y: number }) => {
51
+ return guarded(() => addNodeToStore(canvasStore, nodeDef, position)) ?? null;
52
+ },
53
+ [canvasStore, guarded],
54
+ );
55
+
56
+ const updateNode = useCallback(
57
+ (nodeId: string, updates: { arguments?: Record<string, unknown>; label?: string }) => {
58
+ guarded(() => updateNodeInStore(canvasStore, nodeId, updates));
59
+ },
60
+ [canvasStore, guarded],
61
+ );
62
+
63
+ const deleteNode = useCallback(
64
+ (nodeId: string) => {
65
+ guarded(() => deleteNodeFromStore(canvasStore, nodeId, getNodeDefinition));
66
+ },
67
+ [canvasStore, getNodeDefinition, guarded],
68
+ );
69
+
70
+ const onConnect = useCallback(
71
+ (connection: Connection): EdgeType | false => {
72
+ return guarded(() => connectNodesInStore(canvasStore, connection)) ?? false;
73
+ },
74
+ [canvasStore, guarded],
75
+ );
76
+
77
+ const updateEdge = useCallback(
78
+ (edgeId: string, updates: Record<string, unknown>) => {
79
+ guarded(() => updateEdgeInStore(canvasStore, edgeId, updates));
80
+ },
81
+ [canvasStore, guarded],
82
+ );
83
+
84
+ const deleteEdges = useCallback(
85
+ (edgeIds: string[]) => {
86
+ guarded(() => deleteEdgesFromStore(canvasStore, edgeIds));
87
+ },
88
+ [canvasStore, guarded],
89
+ );
90
+
91
+ // Batch delete nodes and edges as a single undo entry
92
+ const deleteSelected = useCallback(
93
+ (nodeIds: string[], edgeIds: string[]) => {
94
+ if (nodeIds.length === 0 && edgeIds.length === 0) return;
95
+ guarded(() => {
96
+ for (const nodeId of nodeIds) deleteNodeFromStore(canvasStore, nodeId, getNodeDefinition);
97
+ if (edgeIds.length > 0) deleteEdgesFromStore(canvasStore, edgeIds);
98
+ });
99
+ },
100
+ [canvasStore, getNodeDefinition, guarded],
101
+ );
102
+
103
+ // Copy is read-only — no checkpoint, no readOnly gate
104
+ const copySelection = useCallback(
105
+ (nodeIds: string[]) => {
106
+ if (nodeIds.length === 0) return;
107
+
108
+ const nodeIdSet = new Set(nodeIds);
109
+
110
+ const copiedNodes = nodes
111
+ .filter((node) => nodeIdSet.has(node.id))
112
+ .map((node) => JSON.parse(JSON.stringify(node)) as Node<NodeData>);
113
+
114
+ const copiedEdges = edges
115
+ .filter((edge) => nodeIdSet.has(edge.source) && nodeIdSet.has(edge.target))
116
+ .map((edge) => JSON.parse(JSON.stringify(edge)) as Edge<EdgeData>);
117
+
118
+ clipboardRef.current = { nodes: copiedNodes, edges: copiedEdges };
119
+ },
120
+ [nodes, edges],
121
+ );
122
+
123
+ const pasteSelection = useCallback(
124
+ (offset?: { x: number; y: number }) => {
125
+ const clipboard = clipboardRef.current;
126
+ if (!clipboard) return undefined;
127
+ return guarded(() => pasteToStore(canvasStore, clipboard, offset, getNodeDefinition));
128
+ },
129
+ [canvasStore, getNodeDefinition, guarded],
130
+ );
131
+
132
+ // Batch add node + connect as a single undo entry (used by contextual node picker)
133
+ const addNodeAndConnect = useCallback(
134
+ (nodeDef: NodeDefinition, position: { x: number; y: number }, connection: Connection): string | null => {
135
+ return (
136
+ guarded(() => {
137
+ const nodeId = addNodeToStore(canvasStore, nodeDef, position);
138
+ if (nodeId == null) return null;
139
+ connectNodesInStore(canvasStore, { ...connection, target: nodeId });
140
+ return nodeId;
141
+ }) ?? null
142
+ );
143
+ },
144
+ [canvasStore, guarded],
145
+ );
146
+
147
+ return {
148
+ nodes,
149
+ edges,
150
+ addNode,
151
+ updateNode,
152
+ updateEdge,
153
+ deleteNode,
154
+ deleteEdges,
155
+ deleteSelected,
156
+ onConnect,
157
+ addNodeAndConnect,
158
+ copySelection,
159
+ pasteSelection,
160
+ };
161
+ };