@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.
- package/README.md +3 -0
- package/package.json +42 -0
- package/postcss.config.ts +6 -0
- package/src/components/AutoSizeInput.tsx +65 -0
- package/src/components/Controls.tsx +87 -0
- package/src/components/Flow.tsx +101 -0
- package/src/components/InputSocket.tsx +142 -0
- package/src/components/Node.tsx +68 -0
- package/src/components/NodeContainer.tsx +46 -0
- package/src/components/NodePicker.tsx +77 -0
- package/src/components/OutputSocket.tsx +58 -0
- package/src/components/modals/ClearModal.tsx +40 -0
- package/src/components/modals/HelpModal.tsx +36 -0
- package/src/components/modals/LoadModal.tsx +96 -0
- package/src/components/modals/Modal.tsx +64 -0
- package/src/components/modals/SaveModal.tsx +60 -0
- package/src/hooks/useBehaveGraphFlow.ts +75 -0
- package/src/hooks/useChangeNodeData.ts +24 -0
- package/src/hooks/useCustomNodeTypes.tsx +31 -0
- package/src/hooks/useFlowHandlers.ts +180 -0
- package/src/hooks/useGraphRunner.ts +104 -0
- package/src/hooks/useMergeMap.ts +14 -0
- package/src/hooks/useNodeSpecJson.ts +20 -0
- package/src/hooks/useOnPressKey.ts +16 -0
- package/src/hooks/useQueriableDefinitions.ts +22 -0
- package/src/index.ts +37 -0
- package/src/styles.css +8 -0
- package/src/transformers/behaveToFlow.ts +57 -0
- package/src/transformers/flowToBehave.ts +93 -0
- package/src/types.d.ts +4 -0
- package/src/util/autoLayout.ts +9 -0
- package/src/util/calculateNewEdge.ts +49 -0
- package/src/util/colors.ts +41 -0
- package/src/util/getPickerFilters.ts +32 -0
- package/src/util/getSocketsByNodeTypeAndHandleType.ts +11 -0
- package/src/util/hasPositionMetaData.ts +10 -0
- package/src/util/isHandleConnected.ts +12 -0
- package/src/util/isValidConnection.ts +51 -0
- package/src/util/sleep.ts +6 -0
- package/tailwind.config.ts +19 -0
- package/tests/flowToBehave.test.ts +25 -0
- package/tests/tsconfig.json +10 -0
- package/tsconfig.json +60 -0
- package/tsdown.config.ts +15 -0
- package/typedoc.json +8 -0
- 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
|
+
};
|