@kiberon-labs/behave-graph-flow 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 (46) hide show
  1. package/README.md +3 -0
  2. package/package.json +42 -0
  3. package/postcss.config.ts +6 -0
  4. package/src/components/AutoSizeInput.tsx +65 -0
  5. package/src/components/Controls.tsx +87 -0
  6. package/src/components/Flow.tsx +101 -0
  7. package/src/components/InputSocket.tsx +142 -0
  8. package/src/components/Node.tsx +68 -0
  9. package/src/components/NodeContainer.tsx +46 -0
  10. package/src/components/NodePicker.tsx +77 -0
  11. package/src/components/OutputSocket.tsx +58 -0
  12. package/src/components/modals/ClearModal.tsx +40 -0
  13. package/src/components/modals/HelpModal.tsx +36 -0
  14. package/src/components/modals/LoadModal.tsx +96 -0
  15. package/src/components/modals/Modal.tsx +64 -0
  16. package/src/components/modals/SaveModal.tsx +60 -0
  17. package/src/hooks/useBehaveGraphFlow.ts +75 -0
  18. package/src/hooks/useChangeNodeData.ts +24 -0
  19. package/src/hooks/useCustomNodeTypes.tsx +31 -0
  20. package/src/hooks/useFlowHandlers.ts +180 -0
  21. package/src/hooks/useGraphRunner.ts +104 -0
  22. package/src/hooks/useMergeMap.ts +14 -0
  23. package/src/hooks/useNodeSpecJson.ts +20 -0
  24. package/src/hooks/useOnPressKey.ts +16 -0
  25. package/src/hooks/useQueriableDefinitions.ts +22 -0
  26. package/src/index.ts +37 -0
  27. package/src/styles.css +8 -0
  28. package/src/transformers/behaveToFlow.ts +57 -0
  29. package/src/transformers/flowToBehave.ts +93 -0
  30. package/src/types.d.ts +4 -0
  31. package/src/util/autoLayout.ts +9 -0
  32. package/src/util/calculateNewEdge.ts +49 -0
  33. package/src/util/colors.ts +41 -0
  34. package/src/util/getPickerFilters.ts +32 -0
  35. package/src/util/getSocketsByNodeTypeAndHandleType.ts +11 -0
  36. package/src/util/hasPositionMetaData.ts +10 -0
  37. package/src/util/isHandleConnected.ts +12 -0
  38. package/src/util/isValidConnection.ts +51 -0
  39. package/src/util/sleep.ts +6 -0
  40. package/tailwind.config.ts +19 -0
  41. package/tests/flowToBehave.test.ts +25 -0
  42. package/tests/tsconfig.json +10 -0
  43. package/tsconfig.json +60 -0
  44. package/tsdown.config.ts +15 -0
  45. package/typedoc.json +8 -0
  46. package/vitest.config.ts +15 -0
@@ -0,0 +1,58 @@
1
+ import type {
2
+ NodeSpecJSON,
3
+ OutputSocketSpecJSON
4
+ } from '@kiberon-labs/behave-graph';
5
+ import { faCaretRight } from '@fortawesome/free-solid-svg-icons';
6
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
7
+ import cx from 'classnames';
8
+ import React from 'react';
9
+ import { type Connection, Handle, Position, useReactFlow } from 'reactflow';
10
+
11
+ import { colors, valueTypeColorMap } from '../util/colors.js';
12
+ import { isValidConnection } from '../util/isValidConnection.js';
13
+
14
+ export type OutputSocketProps = {
15
+ connected: boolean;
16
+ specJSON: NodeSpecJSON[];
17
+ } & OutputSocketSpecJSON;
18
+
19
+ export default function OutputSocket({
20
+ specJSON,
21
+ connected,
22
+ valueType,
23
+ name
24
+ }: OutputSocketProps) {
25
+ const instance = useReactFlow();
26
+ const isFlowSocket = valueType === 'flow';
27
+ let colorName = valueTypeColorMap[valueType];
28
+ if (colorName === undefined) {
29
+ colorName = 'red';
30
+ }
31
+ // @ts-ignore
32
+ const [backgroundColor, borderColor] = colors[colorName];
33
+ const showName = isFlowSocket === false || name !== 'flow';
34
+
35
+ return (
36
+ <div className="flex grow items-center justify-end h-7">
37
+ {showName && <div className="capitalize">{name}</div>}
38
+ {isFlowSocket && (
39
+ <FontAwesomeIcon
40
+ icon={faCaretRight}
41
+ color="#ffffff"
42
+ size="lg"
43
+ className="ml-1"
44
+ />
45
+ )}
46
+
47
+ <Handle
48
+ id={name}
49
+ type="source"
50
+ position={Position.Right}
51
+ className={cx(borderColor, connected ? backgroundColor : 'bg-gray-800')}
52
+ isValidConnection={(connection: Connection) =>
53
+ isValidConnection(connection, instance, specJSON)
54
+ }
55
+ />
56
+ </div>
57
+ );
58
+ }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { useReactFlow } from 'reactflow';
3
+
4
+ import { Modal } from './Modal.js';
5
+
6
+ export type ClearModalProps = {
7
+ open?: boolean;
8
+ onClose: () => void;
9
+ };
10
+
11
+ export const ClearModal: React.FC<ClearModalProps> = ({
12
+ open = false,
13
+ onClose
14
+ }) => {
15
+ const instance = useReactFlow();
16
+
17
+ const handleClear = () => {
18
+ instance.setNodes([]);
19
+ instance.setEdges([]);
20
+ // TODO better way to call fit vew after edges render
21
+ setTimeout(() => {
22
+ instance.fitView();
23
+ }, 100);
24
+ onClose();
25
+ };
26
+
27
+ return (
28
+ <Modal
29
+ title="Clear Graph"
30
+ actions={[
31
+ { label: 'Cancel', onClick: onClose },
32
+ { label: 'Clear', onClick: handleClear }
33
+ ]}
34
+ open={open}
35
+ onClose={onClose}
36
+ >
37
+ <p>Are you sure?</p>
38
+ </Modal>
39
+ );
40
+ };
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+
3
+ import { Modal } from './Modal.js';
4
+
5
+ export type HelpModalProps = {
6
+ open?: boolean;
7
+ onClose: () => void;
8
+ };
9
+
10
+ export const HelpModal: React.FC<HelpModalProps> = ({
11
+ open = false,
12
+ onClose
13
+ }) => {
14
+ return (
15
+ <Modal
16
+ title="Help"
17
+ actions={[{ label: 'Close', onClick: onClose }]}
18
+ open={open}
19
+ onClose={onClose}
20
+ >
21
+ <p className="mb-2">Right click anywhere to add a new node.</p>
22
+ <p className="mb-2">
23
+ Drag a connection into empty space to add a new node and connect it to
24
+ the source.
25
+ </p>
26
+ <p className="mb-2">
27
+ Click and drag on a socket to connect to another socket of the same
28
+ type.
29
+ </p>
30
+ <p>
31
+ Left click to select nodes or connections, backspace to delete selected
32
+ nodes or connections.
33
+ </p>
34
+ </Modal>
35
+ );
36
+ };
@@ -0,0 +1,96 @@
1
+ import type { GraphJSON } from '@kiberon-labs/behave-graph';
2
+ import React from 'react';
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { useReactFlow } from 'reactflow';
5
+
6
+ import { Modal } from './Modal.js';
7
+
8
+ export type Examples = {
9
+ [key: string]: GraphJSON;
10
+ };
11
+
12
+ export type LoadModalProps = {
13
+ open?: boolean;
14
+ onClose: () => void;
15
+ setBehaviorGraph: (value: GraphJSON) => void;
16
+ examples: Examples;
17
+ };
18
+
19
+ export const LoadModal: React.FC<LoadModalProps> = ({
20
+ open = false,
21
+ onClose,
22
+ setBehaviorGraph,
23
+ examples
24
+ }) => {
25
+ const [value, setValue] = useState<string>();
26
+ const [selected, setSelected] = useState('');
27
+
28
+ const instance = useReactFlow();
29
+
30
+ useEffect(() => {
31
+ if (selected) {
32
+ setValue(JSON.stringify(examples[selected], null, 2));
33
+ }
34
+ }, [selected, examples]);
35
+
36
+ const handleLoad = useCallback(() => {
37
+ let graph;
38
+ if (value !== undefined) {
39
+ graph = JSON.parse(value) as GraphJSON;
40
+ } else if (selected !== '') {
41
+ graph = examples[selected];
42
+ }
43
+
44
+ if (graph === undefined) return;
45
+
46
+ setBehaviorGraph(graph);
47
+
48
+ // TODO better way to call fit vew after edges render
49
+ setTimeout(() => {
50
+ instance.fitView();
51
+ }, 100);
52
+
53
+ handleClose();
54
+ }, [setBehaviorGraph, value, instance]);
55
+
56
+ const handleClose = () => {
57
+ setValue(undefined);
58
+ setSelected('');
59
+ onClose();
60
+ };
61
+
62
+ return (
63
+ <Modal
64
+ title="Load Graph"
65
+ actions={[
66
+ { label: 'Cancel', onClick: handleClose },
67
+ { label: 'Load', onClick: handleLoad }
68
+ ]}
69
+ open={open}
70
+ onClose={onClose}
71
+ >
72
+ <textarea
73
+ autoFocus
74
+ className="border border-gray-300 w-full p-2 h-32 align-top"
75
+ placeholder="Paste JSON here"
76
+ value={value}
77
+ onChange={(e) => setValue(e.currentTarget.value)}
78
+ ></textarea>
79
+ <div className="p-4 text-center text-gray-800">or</div>
80
+ <select
81
+ className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded block w-full p-3"
82
+ onChange={(e) => setSelected(e.target.value)}
83
+ value={selected}
84
+ >
85
+ <option disabled value="">
86
+ Select an example
87
+ </option>
88
+ {Object.keys(examples).map((key) => (
89
+ <option key={key} value={key}>
90
+ {key}
91
+ </option>
92
+ ))}
93
+ </select>
94
+ </Modal>
95
+ );
96
+ };
@@ -0,0 +1,64 @@
1
+ import React from 'react';
2
+ import type { PropsWithChildren } from 'react';
3
+
4
+ import { useOnPressKey } from '../../hooks/useOnPressKey.js';
5
+
6
+ export type ModalAction = {
7
+ label: string;
8
+ onClick: () => void;
9
+ };
10
+
11
+ export type ModalProps = {
12
+ open?: boolean;
13
+ onClose: () => void;
14
+ title: string;
15
+ actions: ModalAction[];
16
+ };
17
+
18
+ export const Modal: React.FC<PropsWithChildren<ModalProps>> = ({
19
+ open = false,
20
+ onClose,
21
+ title,
22
+ children,
23
+ actions
24
+ }) => {
25
+ useOnPressKey('Escape', onClose);
26
+
27
+ if (open === false) return null;
28
+
29
+ const actionColors = {
30
+ primary: 'bg-teal-400 hover:bg-teal-500',
31
+ secondary: 'bg-gray-400 hover:bg-gray-500'
32
+ };
33
+
34
+ return (
35
+ <>
36
+ <div
37
+ className="z-[19] fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full"
38
+ onClick={onClose}
39
+ ></div>
40
+ <div className="z-20 relative top-20 mx-auto border w-96 shadow-lg bg-white text-sm rounded-md">
41
+ <div className="p-3 border-b">
42
+ <h2 className="text-lg text-center font-bold">{title}</h2>
43
+ </div>
44
+ <div className="p-3">{children}</div>
45
+ <div className="flex gap-3 p-3 border-t">
46
+ {actions.map((action, ix) => (
47
+ <button
48
+ key={ix}
49
+ className={
50
+ 'text-white p-2 w-full cursor-pointer ' +
51
+ (ix === actions.length - 1
52
+ ? actionColors.primary
53
+ : actionColors.secondary)
54
+ }
55
+ onClick={action.onClick}
56
+ >
57
+ {action.label}
58
+ </button>
59
+ ))}
60
+ </div>
61
+ </div>
62
+ </>
63
+ );
64
+ };
@@ -0,0 +1,60 @@
1
+ import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
2
+ import React from 'react';
3
+ import { useMemo, useRef, useState } from 'react';
4
+ import { useEdges, useNodes } from 'reactflow';
5
+
6
+ import { flowToBehave } from '../../transformers/flowToBehave.js';
7
+ import { Modal } from './Modal.js';
8
+
9
+ export type SaveModalProps = {
10
+ open?: boolean;
11
+ onClose: () => void;
12
+ specJson: NodeSpecJSON[];
13
+ };
14
+
15
+ export const SaveModal: React.FC<SaveModalProps> = ({
16
+ open = false,
17
+ onClose,
18
+ specJson
19
+ }) => {
20
+ const ref = useRef<HTMLTextAreaElement>(null);
21
+ const [copied, setCopied] = useState(false);
22
+
23
+ const edges = useEdges();
24
+ const nodes = useNodes();
25
+
26
+ const flow = useMemo(
27
+ () => flowToBehave(nodes, edges, specJson),
28
+ [nodes, edges, specJson]
29
+ );
30
+
31
+ const jsonString = JSON.stringify(flow, null, 2);
32
+
33
+ const handleCopy = () => {
34
+ ref.current?.select();
35
+ document.execCommand('copy');
36
+ ref.current?.blur();
37
+ setCopied(true);
38
+ setInterval(() => {
39
+ setCopied(false);
40
+ }, 1000);
41
+ };
42
+
43
+ return (
44
+ <Modal
45
+ title="Save Graph"
46
+ actions={[
47
+ { label: 'Cancel', onClick: onClose },
48
+ { label: copied ? 'Copied' : 'Copy', onClick: handleCopy }
49
+ ]}
50
+ open={open}
51
+ onClose={onClose}
52
+ >
53
+ <textarea
54
+ ref={ref}
55
+ className="border border-gray-300 w-full p-2 h-32"
56
+ defaultValue={jsonString}
57
+ ></textarea>
58
+ </Modal>
59
+ );
60
+ };
@@ -0,0 +1,75 @@
1
+ import type { GraphJSON, NodeSpecJSON } from '@kiberon-labs/behave-graph';
2
+ import { useCallback, useEffect, useState } from 'react';
3
+ import { useEdgesState, useNodesState } from 'reactflow';
4
+
5
+ import { behaveToFlow } from '../transformers/behaveToFlow.js';
6
+ import { flowToBehave } from '../transformers/flowToBehave.js';
7
+ import { autoLayout } from '../util/autoLayout.js';
8
+ import { hasPositionMetaData } from '../util/hasPositionMetaData.js';
9
+ import { useCustomNodeTypes } from './useCustomNodeTypes.js';
10
+
11
+ export const fetchBehaviorGraphJson = async (url: string) =>
12
+ // eslint-disable-next-line unicorn/no-await-expression-member
13
+ (await (await fetch(url)).json()) as GraphJSON;
14
+
15
+ /**
16
+ * Hook that returns the nodes and edges for react-flow, and the graphJson for the behave-graph.
17
+ * If nodes or edges are changes, the graph json is updated automatically.
18
+ * The graph json can be set manually, in which case the nodes and edges are updated to match the graph json.
19
+ * @param param0
20
+ * @returns
21
+ */
22
+ export const useBehaveGraphFlow = ({
23
+ initialGraphJson,
24
+ specJson
25
+ }: {
26
+ initialGraphJson: GraphJSON;
27
+ specJson: NodeSpecJSON[] | undefined;
28
+ }) => {
29
+ const [graphJson, setStoredGraphJson] = useState<GraphJSON | undefined>();
30
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
31
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
32
+
33
+ const setGraphJson = useCallback(
34
+ (graphJson: GraphJSON) => {
35
+ if (!graphJson) return;
36
+
37
+ const [nodes, edges] = behaveToFlow(graphJson);
38
+
39
+ if (hasPositionMetaData(graphJson) === false) {
40
+ autoLayout(nodes, edges);
41
+ }
42
+
43
+ setNodes(nodes);
44
+ setEdges(edges);
45
+ setStoredGraphJson(graphJson);
46
+ },
47
+ [setEdges, setNodes]
48
+ );
49
+
50
+ useEffect(() => {
51
+ if (!initialGraphJson) return;
52
+ setGraphJson(initialGraphJson);
53
+ }, [initialGraphJson, setGraphJson]);
54
+
55
+ useEffect(() => {
56
+ if (!specJson) return;
57
+ // when nodes and edges are updated, update the graph json with the flow to behave behavior
58
+ const graphJson = flowToBehave(nodes, edges, specJson);
59
+ setStoredGraphJson(graphJson);
60
+ }, [nodes, edges, specJson]);
61
+
62
+ const nodeTypes = useCustomNodeTypes({
63
+ specJson
64
+ });
65
+
66
+ return {
67
+ nodes,
68
+ edges,
69
+ onEdgesChange,
70
+ onNodesChange,
71
+ setGraphJson,
72
+ graphJson,
73
+ nodeTypes
74
+ };
75
+ };
@@ -0,0 +1,24 @@
1
+ import { useCallback } from 'react';
2
+ import { useReactFlow } from 'reactflow';
3
+
4
+ export const useChangeNodeData = (id: string) => {
5
+ const instance = useReactFlow();
6
+
7
+ return useCallback(
8
+ (key: string, value: any) => {
9
+ instance.setNodes((nodes) =>
10
+ nodes.map((n) => {
11
+ if (n.id !== id) return n;
12
+ return {
13
+ ...n,
14
+ data: {
15
+ ...n.data,
16
+ [key]: value
17
+ }
18
+ };
19
+ })
20
+ );
21
+ },
22
+ [instance, id]
23
+ );
24
+ };
@@ -0,0 +1,31 @@
1
+ import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
2
+ import React from 'react';
3
+ import { useEffect, useState } from 'react';
4
+ import type { NodeTypes } from 'reactflow';
5
+
6
+ import { Node } from '../components/Node.js';
7
+
8
+ const getCustomNodeTypes = (allSpecs: NodeSpecJSON[]) => {
9
+ return allSpecs.reduce((nodes: NodeTypes, node) => {
10
+ nodes[node.type] = (props) => (
11
+ <Node spec={node} allSpecs={allSpecs} {...props} />
12
+ );
13
+ return nodes;
14
+ }, {});
15
+ };
16
+
17
+ export const useCustomNodeTypes = ({
18
+ specJson
19
+ }: {
20
+ specJson: NodeSpecJSON[] | undefined;
21
+ }) => {
22
+ const [customNodeTypes, setCustomNodeTypes] = useState<NodeTypes>();
23
+ useEffect(() => {
24
+ if (!specJson) return;
25
+ const customNodeTypes = getCustomNodeTypes(specJson);
26
+
27
+ setCustomNodeTypes(customNodeTypes);
28
+ }, [specJson]);
29
+
30
+ return customNodeTypes;
31
+ };
@@ -0,0 +1,180 @@
1
+ import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
2
+ import {
3
+ type MouseEvent as ReactMouseEvent,
4
+ useCallback,
5
+ useEffect,
6
+ useState
7
+ } from 'react';
8
+ import type {
9
+ Connection,
10
+ Node,
11
+ OnConnectStartParams,
12
+ XYPosition
13
+ } from 'reactflow';
14
+ import { v4 as uuidv4 } from 'uuid';
15
+
16
+ import { calculateNewEdge } from '../util/calculateNewEdge.js';
17
+ import { getNodePickerFilters } from '../util/getPickerFilters.js';
18
+ import { useBehaveGraphFlow } from './useBehaveGraphFlow.js';
19
+
20
+ type BehaveGraphFlow = ReturnType<typeof useBehaveGraphFlow>;
21
+
22
+ const useNodePickFilters = ({
23
+ nodes,
24
+ lastConnectStart,
25
+ specJSON
26
+ }: {
27
+ nodes: Node[];
28
+ lastConnectStart: OnConnectStartParams | undefined;
29
+ specJSON: NodeSpecJSON[] | undefined;
30
+ }) => {
31
+ const [nodePickFilters, setNodePickFilters] = useState(
32
+ getNodePickerFilters(nodes, lastConnectStart, specJSON)
33
+ );
34
+
35
+ useEffect(() => {
36
+ setNodePickFilters(getNodePickerFilters(nodes, lastConnectStart, specJSON));
37
+ }, [nodes, lastConnectStart, specJSON]);
38
+
39
+ return nodePickFilters;
40
+ };
41
+
42
+ export const useFlowHandlers = ({
43
+ onEdgesChange,
44
+ onNodesChange,
45
+ nodes,
46
+ specJSON
47
+ }: Pick<BehaveGraphFlow, 'onEdgesChange' | 'onNodesChange'> & {
48
+ nodes: Node[];
49
+ specJSON: NodeSpecJSON[] | undefined;
50
+ }) => {
51
+ const [lastConnectStart, setLastConnectStart] =
52
+ useState<OnConnectStartParams>();
53
+ const [nodePickerVisibility, setNodePickerVisibility] =
54
+ useState<XYPosition>();
55
+
56
+ const onConnect = useCallback(
57
+ (connection: Connection) => {
58
+ if (connection.source === null) return;
59
+ if (connection.target === null) return;
60
+
61
+ const newEdge = {
62
+ id: uuidv4(),
63
+ source: connection.source,
64
+ target: connection.target,
65
+ sourceHandle: connection.sourceHandle,
66
+ targetHandle: connection.targetHandle
67
+ };
68
+ onEdgesChange([
69
+ {
70
+ type: 'add',
71
+ item: newEdge
72
+ }
73
+ ]);
74
+ },
75
+ [onEdgesChange]
76
+ );
77
+
78
+ const closeNodePicker = useCallback(() => {
79
+ setLastConnectStart(undefined);
80
+ setNodePickerVisibility(undefined);
81
+ }, []);
82
+
83
+ const handleAddNode = useCallback(
84
+ (nodeType: string, position: XYPosition) => {
85
+ closeNodePicker();
86
+ const newNode = {
87
+ id: uuidv4(),
88
+ type: nodeType,
89
+ position,
90
+ data: {}
91
+ };
92
+ onNodesChange([
93
+ {
94
+ type: 'add',
95
+ item: newNode
96
+ }
97
+ ]);
98
+
99
+ if (lastConnectStart === undefined) return;
100
+
101
+ // add an edge if we started on a socket
102
+ const originNode = nodes.find(
103
+ (node) => node.id === lastConnectStart.nodeId
104
+ );
105
+ if (originNode === undefined) return;
106
+ if (!specJSON) return;
107
+ onEdgesChange([
108
+ {
109
+ type: 'add',
110
+ item: calculateNewEdge(
111
+ originNode,
112
+ nodeType,
113
+ newNode.id,
114
+ lastConnectStart,
115
+ specJSON
116
+ )
117
+ }
118
+ ]);
119
+ },
120
+ [
121
+ closeNodePicker,
122
+ lastConnectStart,
123
+ nodes,
124
+ onEdgesChange,
125
+ onNodesChange,
126
+ specJSON
127
+ ]
128
+ );
129
+
130
+ const handleStartConnect = useCallback(
131
+ (e: ReactMouseEvent, params: OnConnectStartParams) => {
132
+ setLastConnectStart(params);
133
+ },
134
+ []
135
+ );
136
+
137
+ const handleStopConnect = useCallback((e: MouseEvent) => {
138
+ const element = e.target as HTMLElement;
139
+ console.log(
140
+ 'here',
141
+ element.classList,
142
+ element.classList.contains('react-flow__pane')
143
+ );
144
+ if (element.classList.contains('react-flow__pane')) {
145
+ console.log('setting node picker');
146
+ setNodePickerVisibility({ x: e.clientX, y: e.clientY });
147
+ } else {
148
+ setLastConnectStart(undefined);
149
+ }
150
+ }, []);
151
+
152
+ const handlePaneClick = useCallback(
153
+ () => closeNodePicker(),
154
+ [closeNodePicker]
155
+ );
156
+
157
+ const handlePaneContextMenu = useCallback((e: ReactMouseEvent) => {
158
+ e.preventDefault();
159
+ setNodePickerVisibility({ x: e.clientX, y: e.clientY });
160
+ }, []);
161
+
162
+ const nodePickFilters = useNodePickFilters({
163
+ nodes,
164
+ lastConnectStart,
165
+ specJSON
166
+ });
167
+
168
+ return {
169
+ onConnect,
170
+ handleStartConnect,
171
+ handleStopConnect,
172
+ handlePaneClick,
173
+ handlePaneContextMenu,
174
+ lastConnectStart,
175
+ nodePickerVisibility,
176
+ handleAddNode,
177
+ closeNodePicker,
178
+ nodePickFilters
179
+ };
180
+ };