@foresthubai/workflow-builder 0.4.3 → 0.4.4

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 -110
  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/package.json +2 -2
  11. package/src/BuilderLayout.tsx +345 -345
  12. package/src/Canvas.tsx +261 -261
  13. package/src/CanvasEditor.tsx +142 -142
  14. package/src/CanvasTabsToolbar.tsx +176 -176
  15. package/src/RightConfigPanel.tsx +266 -266
  16. package/src/WorkflowBuilder.tsx +408 -408
  17. package/src/cn.ts +6 -6
  18. package/src/components/ui/add-button.tsx +39 -39
  19. package/src/components/ui/alert-dialog.tsx +141 -141
  20. package/src/components/ui/alert.tsx +59 -59
  21. package/src/components/ui/badge.tsx +36 -36
  22. package/src/components/ui/button.tsx +85 -85
  23. package/src/components/ui/card.tsx +79 -79
  24. package/src/components/ui/checkbox.tsx +28 -28
  25. package/src/components/ui/collapsible.tsx +9 -9
  26. package/src/components/ui/command.tsx +153 -153
  27. package/src/components/ui/delete-button.tsx +23 -23
  28. package/src/components/ui/dialog.tsx +125 -125
  29. package/src/components/ui/dropdown-menu.tsx +198 -198
  30. package/src/components/ui/input.tsx +55 -55
  31. package/src/components/ui/label.tsx +24 -24
  32. package/src/components/ui/readonly-banner.tsx +15 -15
  33. package/src/components/ui/resizable.tsx +43 -43
  34. package/src/components/ui/scroll-area.tsx +102 -102
  35. package/src/components/ui/select.tsx +160 -160
  36. package/src/components/ui/separator.tsx +29 -29
  37. package/src/components/ui/switch.tsx +27 -27
  38. package/src/components/ui/textarea.tsx +51 -51
  39. package/src/components/ui/toast.tsx +127 -127
  40. package/src/components/ui/toaster.tsx +33 -33
  41. package/src/components/ui/toggle-group.tsx +59 -59
  42. package/src/components/ui/toggle.tsx +43 -43
  43. package/src/components/ui/tooltip.tsx +32 -32
  44. package/src/dialogs/NodePickerDialog.tsx +84 -84
  45. package/src/dialogs/ValidationDialog.tsx +184 -184
  46. package/src/graph/BaseNode.tsx +557 -557
  47. package/src/graph/CustomEdge.tsx +185 -185
  48. package/src/graph/CustomNode.tsx +16 -16
  49. package/src/graph/FunctionCallNode.tsx +30 -30
  50. package/src/graph/PortHandle.tsx +189 -189
  51. package/src/graph/reactFlowRegistry.ts +26 -26
  52. package/src/hooks/use-toast.ts +125 -125
  53. package/src/hooks/useAvailableVariables.ts +20 -20
  54. package/src/hooks/useCanvasHistory.ts +22 -22
  55. package/src/hooks/useCanvasTabs.ts +168 -168
  56. package/src/hooks/useFunctionDiagnosticsSync.ts +40 -40
  57. package/src/hooks/useFunctionRegistry.ts +26 -26
  58. package/src/hooks/useFunctions.ts +44 -44
  59. package/src/hooks/useGraph.ts +161 -161
  60. package/src/hooks/useNodeDefinitions.ts +82 -82
  61. package/src/hooks/useParamErrors.ts +26 -26
  62. package/src/hooks/useResolvedTheme.ts +30 -30
  63. package/src/hooks/useResourceDiagnosticsSync.ts +58 -58
  64. package/src/hooks/useSuppressThemeTransition.ts +79 -79
  65. package/src/hooks/useWorkflowSerialization.ts +141 -141
  66. package/src/i18n/index.ts +53 -53
  67. package/src/i18n/locales/de.json +501 -501
  68. package/src/i18n/locales/en.json +557 -557
  69. package/src/index.ts +27 -27
  70. package/src/inputs/ExpressionInput.tsx +297 -297
  71. package/src/inputs/ParameterEditor.tsx +515 -515
  72. package/src/inputs/PortSection.tsx +144 -144
  73. package/src/panels/BuilderSidebar.tsx +301 -301
  74. package/src/panels/ChannelConfigPanel.tsx +49 -49
  75. package/src/panels/ChannelsPanel.tsx +28 -28
  76. package/src/panels/DebugConsolePanel.tsx +73 -73
  77. package/src/panels/DebugContextPanel.tsx +77 -77
  78. package/src/panels/DebugExternalIOPanel.tsx +180 -180
  79. package/src/panels/DiagnosticsPanel.tsx +170 -170
  80. package/src/panels/EdgeConfigPanel.tsx +104 -104
  81. package/src/panels/FunctionConfigPanel.tsx +179 -179
  82. package/src/panels/FunctionListPanel.tsx +45 -45
  83. package/src/panels/MemoryConfigPanel.tsx +55 -55
  84. package/src/panels/MemoryPanel.tsx +40 -40
  85. package/src/panels/ModelConfigPanel.tsx +41 -41
  86. package/src/panels/ModelsPanel.tsx +36 -36
  87. package/src/panels/NodeConfigPanel.tsx +630 -630
  88. package/src/panels/NodeLibrary.tsx +288 -288
  89. package/src/panels/ResourceConfigPanel.tsx +132 -132
  90. package/src/panels/ResourceListPanel.tsx +113 -113
  91. package/src/panels/VariableConfigPanel.tsx +161 -161
  92. package/src/panels/VariablesPanel.tsx +145 -145
  93. package/src/stores/canvasStore.test.ts +44 -44
  94. package/src/stores/canvasStore.ts +245 -245
  95. package/src/stores/debugStore.ts +74 -74
  96. package/src/stores/diagnosticsStore.ts +130 -130
  97. package/src/stores/editorStore.ts +190 -190
  98. package/src/styles/index.css +526 -526
  99. package/src/utils/categoryConstants.ts +26 -26
  100. package/src/utils/channelOperations.ts +86 -86
  101. package/src/utils/connectionRules.ts +137 -137
  102. package/src/utils/functionOperations.ts +179 -179
  103. package/src/utils/graphOperations.ts +550 -550
  104. package/src/utils/history.ts +207 -207
  105. package/src/utils/memoryOperations.ts +57 -57
  106. package/src/utils/migrateFunctionNodes.ts +107 -107
  107. package/src/utils/modelOperations.ts +55 -55
  108. package/src/utils/paramDisplay.ts +71 -71
  109. package/src/utils/resourceHelpers.ts +32 -32
  110. package/src/utils/translation.ts +28 -28
  111. package/src/utils/variableOperations.ts +75 -75
  112. package/tailwind-preset.ts +166 -166
  113. package/dist/lib/utils.d.ts +0 -3
  114. package/dist/lib/utils.d.ts.map +0 -1
  115. package/dist/lib/utils.js +0 -6
  116. package/dist/lib/utils.js.map +0 -1
  117. package/dist/toolbars/CanvasTabsToolbar.d.ts +0 -11
  118. package/dist/toolbars/CanvasTabsToolbar.d.ts.map +0 -1
  119. package/dist/toolbars/CanvasTabsToolbar.js +0 -101
  120. package/dist/toolbars/CanvasTabsToolbar.js.map +0 -1
@@ -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
+ };