@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
package/README.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kiberon-labs/behave-graph-flow",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"source": "./src/index.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "tsdown --watch src",
|
|
10
|
+
"build": "tsdown",
|
|
11
|
+
"test": "vitest",
|
|
12
|
+
"check": "oxfmt --check",
|
|
13
|
+
"format": "oxfmt",
|
|
14
|
+
"lint": "oxlint",
|
|
15
|
+
"typecheck": "tsc"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@testing-library/react": "^13.3.0",
|
|
19
|
+
"@testing-library/user-event": "^13.5.0",
|
|
20
|
+
"@types/react": "^19.2.6",
|
|
21
|
+
"@types/uuid": "^8.3.4",
|
|
22
|
+
"autoprefixer": "^10.4.22",
|
|
23
|
+
"oxlint": "^1.29.0",
|
|
24
|
+
"react": "^19.2.0",
|
|
25
|
+
"tailwindcss": "^4.1.17",
|
|
26
|
+
"tsdown": "^0.16.5",
|
|
27
|
+
"vitest": "^4.0.10",
|
|
28
|
+
"oxfmt": "^0.14.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@fortawesome/fontawesome-svg-core": "^6.1.2",
|
|
32
|
+
"@fortawesome/free-solid-svg-icons": "^6.1.2",
|
|
33
|
+
"@fortawesome/react-fontawesome": "^0.2.0",
|
|
34
|
+
"classnames": "^2.3.1",
|
|
35
|
+
"downshift": "^6.1.7",
|
|
36
|
+
"uuid": "^8.3.2"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"reactflow": "^11.1.1",
|
|
40
|
+
"@kiberon-labs/behave-graph": "*"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type CSSProperties,
|
|
4
|
+
type HTMLProps,
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState
|
|
9
|
+
} from 'react';
|
|
10
|
+
|
|
11
|
+
export type AutoSizeInputProps = HTMLProps<HTMLInputElement> & {
|
|
12
|
+
minWidth?: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const baseStyles: CSSProperties = {
|
|
16
|
+
position: 'absolute',
|
|
17
|
+
top: 0,
|
|
18
|
+
left: 0,
|
|
19
|
+
visibility: 'hidden',
|
|
20
|
+
height: 0,
|
|
21
|
+
width: 'auto',
|
|
22
|
+
whiteSpace: 'pre'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const AutoSizeInput: React.FC<AutoSizeInputProps> = ({
|
|
26
|
+
minWidth = 30,
|
|
27
|
+
...props
|
|
28
|
+
}) => {
|
|
29
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
30
|
+
const measureRef = useRef<HTMLSpanElement | null>(null);
|
|
31
|
+
const [styles, setStyles] = useState<CSSProperties>({});
|
|
32
|
+
|
|
33
|
+
// grab the font size of the input on ref mount
|
|
34
|
+
const setRef = useCallback((input: HTMLInputElement | null) => {
|
|
35
|
+
if (input) {
|
|
36
|
+
const styles = window.getComputedStyle(input);
|
|
37
|
+
setStyles({
|
|
38
|
+
fontSize: styles.getPropertyValue('font-size'),
|
|
39
|
+
paddingLeft: styles.getPropertyValue('padding-left'),
|
|
40
|
+
paddingRight: styles.getPropertyValue('padding-right')
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
inputRef.current = input;
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
// measure the text on change and update input
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (measureRef.current === null) return;
|
|
49
|
+
if (inputRef.current === null) return;
|
|
50
|
+
|
|
51
|
+
const padding = props.type === 'number' || props.type === 'float' ? 20 : 0;
|
|
52
|
+
|
|
53
|
+
const width = measureRef.current.clientWidth + padding;
|
|
54
|
+
inputRef.current.style.width = Math.max(minWidth, width) + 'px';
|
|
55
|
+
}, [props.value, minWidth, styles, props.type]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<input ref={setRef} {...props} />
|
|
60
|
+
<span ref={measureRef} style={{ ...baseStyles, ...styles }}>
|
|
61
|
+
{props.value}
|
|
62
|
+
</span>
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { GraphJSON, NodeSpecJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import {
|
|
3
|
+
faDownload,
|
|
4
|
+
faPause,
|
|
5
|
+
faPlay,
|
|
6
|
+
faQuestion,
|
|
7
|
+
faTrash,
|
|
8
|
+
faUpload
|
|
9
|
+
} from '@fortawesome/free-solid-svg-icons';
|
|
10
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { ControlButton, Controls } from 'reactflow';
|
|
14
|
+
|
|
15
|
+
import { ClearModal } from './modals/ClearModal.js';
|
|
16
|
+
import { HelpModal } from './modals/HelpModal.js';
|
|
17
|
+
import { type Examples, LoadModal } from './modals/LoadModal.js';
|
|
18
|
+
import { SaveModal } from './modals/SaveModal.js';
|
|
19
|
+
|
|
20
|
+
export type CustomControlsProps = {
|
|
21
|
+
playing: boolean;
|
|
22
|
+
togglePlay: () => void;
|
|
23
|
+
setBehaviorGraph: (value: GraphJSON) => void;
|
|
24
|
+
examples: Examples;
|
|
25
|
+
specJson: NodeSpecJSON[] | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const CustomControls: React.FC<CustomControlsProps> = ({
|
|
29
|
+
playing,
|
|
30
|
+
togglePlay,
|
|
31
|
+
setBehaviorGraph,
|
|
32
|
+
examples,
|
|
33
|
+
specJson
|
|
34
|
+
}: {
|
|
35
|
+
playing: boolean;
|
|
36
|
+
togglePlay: () => void;
|
|
37
|
+
setBehaviorGraph: (value: GraphJSON) => void;
|
|
38
|
+
examples: Examples;
|
|
39
|
+
specJson: NodeSpecJSON[] | undefined;
|
|
40
|
+
}) => {
|
|
41
|
+
const [loadModalOpen, setLoadModalOpen] = useState(false);
|
|
42
|
+
const [saveModalOpen, setSaveModalOpen] = useState(false);
|
|
43
|
+
const [helpModalOpen, setHelpModalOpen] = useState(false);
|
|
44
|
+
const [clearModalOpen, setClearModalOpen] = useState(false);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
<Controls>
|
|
49
|
+
<ControlButton title="Help" onClick={() => setHelpModalOpen(true)}>
|
|
50
|
+
<FontAwesomeIcon icon={faQuestion} />
|
|
51
|
+
</ControlButton>
|
|
52
|
+
<ControlButton title="Load" onClick={() => setLoadModalOpen(true)}>
|
|
53
|
+
<FontAwesomeIcon icon={faUpload} />
|
|
54
|
+
</ControlButton>
|
|
55
|
+
<ControlButton title="Save" onClick={() => setSaveModalOpen(true)}>
|
|
56
|
+
<FontAwesomeIcon icon={faDownload} />
|
|
57
|
+
</ControlButton>
|
|
58
|
+
<ControlButton title="Clear" onClick={() => setClearModalOpen(true)}>
|
|
59
|
+
<FontAwesomeIcon icon={faTrash} />
|
|
60
|
+
</ControlButton>
|
|
61
|
+
<ControlButton title="Run" onClick={togglePlay}>
|
|
62
|
+
<FontAwesomeIcon icon={playing ? faPause : faPlay} />
|
|
63
|
+
</ControlButton>
|
|
64
|
+
</Controls>
|
|
65
|
+
<LoadModal
|
|
66
|
+
open={loadModalOpen}
|
|
67
|
+
onClose={() => setLoadModalOpen(false)}
|
|
68
|
+
setBehaviorGraph={setBehaviorGraph}
|
|
69
|
+
examples={examples}
|
|
70
|
+
/>
|
|
71
|
+
{specJson && (
|
|
72
|
+
<SaveModal
|
|
73
|
+
open={saveModalOpen}
|
|
74
|
+
specJson={specJson}
|
|
75
|
+
onClose={() => setSaveModalOpen(false)}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
<HelpModal open={helpModalOpen} onClose={() => setHelpModalOpen(false)} />
|
|
79
|
+
<ClearModal
|
|
80
|
+
open={clearModalOpen}
|
|
81
|
+
onClose={() => setClearModalOpen(false)}
|
|
82
|
+
/>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default CustomControls;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { GraphJSON, IRegistry } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Background, BackgroundVariant, ReactFlow } from 'reactflow';
|
|
4
|
+
|
|
5
|
+
import { useBehaveGraphFlow } from '../hooks/useBehaveGraphFlow.js';
|
|
6
|
+
import { useFlowHandlers } from '../hooks/useFlowHandlers.js';
|
|
7
|
+
import { useGraphRunner } from '../hooks/useGraphRunner.js';
|
|
8
|
+
import { useNodeSpecJson } from '../hooks/useNodeSpecJson.js';
|
|
9
|
+
import CustomControls from './Controls.js';
|
|
10
|
+
import { type Examples } from './modals/LoadModal.js';
|
|
11
|
+
import { NodePicker } from './NodePicker.js';
|
|
12
|
+
|
|
13
|
+
type FlowProps = {
|
|
14
|
+
initialGraph: GraphJSON;
|
|
15
|
+
registry: IRegistry;
|
|
16
|
+
examples: Examples;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const Flow: React.FC<FlowProps> = ({
|
|
20
|
+
initialGraph: graph,
|
|
21
|
+
registry,
|
|
22
|
+
examples
|
|
23
|
+
}) => {
|
|
24
|
+
const specJson = useNodeSpecJson(registry);
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
nodes,
|
|
28
|
+
edges,
|
|
29
|
+
onNodesChange,
|
|
30
|
+
onEdgesChange,
|
|
31
|
+
graphJson,
|
|
32
|
+
setGraphJson,
|
|
33
|
+
nodeTypes
|
|
34
|
+
} = useBehaveGraphFlow({
|
|
35
|
+
initialGraphJson: graph,
|
|
36
|
+
specJson
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
onConnect,
|
|
41
|
+
handleStartConnect,
|
|
42
|
+
handleStopConnect,
|
|
43
|
+
handlePaneClick,
|
|
44
|
+
handlePaneContextMenu,
|
|
45
|
+
nodePickerVisibility,
|
|
46
|
+
handleAddNode,
|
|
47
|
+
closeNodePicker,
|
|
48
|
+
nodePickFilters
|
|
49
|
+
} = useFlowHandlers({
|
|
50
|
+
nodes,
|
|
51
|
+
onEdgesChange,
|
|
52
|
+
onNodesChange,
|
|
53
|
+
specJSON: specJson
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const { togglePlay, playing } = useGraphRunner({
|
|
57
|
+
graphJson,
|
|
58
|
+
registry
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ReactFlow
|
|
63
|
+
nodeTypes={nodeTypes}
|
|
64
|
+
nodes={nodes}
|
|
65
|
+
edges={edges}
|
|
66
|
+
onNodesChange={onNodesChange}
|
|
67
|
+
onEdgesChange={onEdgesChange}
|
|
68
|
+
onConnect={onConnect}
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
onConnectStart={handleStartConnect}
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
onConnectEnd={handleStopConnect}
|
|
73
|
+
fitView
|
|
74
|
+
fitViewOptions={{ maxZoom: 1 }}
|
|
75
|
+
onPaneClick={handlePaneClick}
|
|
76
|
+
onPaneContextMenu={handlePaneContextMenu}
|
|
77
|
+
>
|
|
78
|
+
<CustomControls
|
|
79
|
+
playing={playing}
|
|
80
|
+
togglePlay={togglePlay}
|
|
81
|
+
setBehaviorGraph={setGraphJson}
|
|
82
|
+
examples={examples}
|
|
83
|
+
specJson={specJson}
|
|
84
|
+
/>
|
|
85
|
+
<Background
|
|
86
|
+
variant={BackgroundVariant.Lines}
|
|
87
|
+
color="#2a2b2d"
|
|
88
|
+
style={{ backgroundColor: '#1E1F22' }}
|
|
89
|
+
/>
|
|
90
|
+
{nodePickerVisibility && (
|
|
91
|
+
<NodePicker
|
|
92
|
+
position={nodePickerVisibility}
|
|
93
|
+
filters={nodePickFilters}
|
|
94
|
+
onPickNode={handleAddNode}
|
|
95
|
+
onClose={closeNodePicker}
|
|
96
|
+
specJSON={specJson}
|
|
97
|
+
/>
|
|
98
|
+
)}
|
|
99
|
+
</ReactFlow>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InputSocketSpecJSON,
|
|
3
|
+
NodeSpecJSON
|
|
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
|
+
import { AutoSizeInput } from './AutoSizeInput.js';
|
|
14
|
+
|
|
15
|
+
export type InputSocketProps = {
|
|
16
|
+
connected: boolean;
|
|
17
|
+
value: any | undefined;
|
|
18
|
+
onChange: (key: string, value: any) => void;
|
|
19
|
+
specJSON: NodeSpecJSON[];
|
|
20
|
+
} & InputSocketSpecJSON;
|
|
21
|
+
|
|
22
|
+
const InputFieldForValue = ({
|
|
23
|
+
choices,
|
|
24
|
+
value,
|
|
25
|
+
defaultValue,
|
|
26
|
+
onChange,
|
|
27
|
+
name,
|
|
28
|
+
valueType
|
|
29
|
+
}: Pick<
|
|
30
|
+
InputSocketProps,
|
|
31
|
+
'choices' | 'value' | 'defaultValue' | 'name' | 'onChange' | 'valueType'
|
|
32
|
+
>) => {
|
|
33
|
+
const showChoices = choices?.length;
|
|
34
|
+
//Stops 'undefined'
|
|
35
|
+
const defaultSafeValue =
|
|
36
|
+
defaultValue !== undefined ? String(defaultValue) : '';
|
|
37
|
+
const inputVal = value !== undefined ? String(value) : defaultSafeValue;
|
|
38
|
+
|
|
39
|
+
if (showChoices)
|
|
40
|
+
return (
|
|
41
|
+
<select
|
|
42
|
+
className="bg-gray-600 disabled:bg-gray-700 py-1 px-2 nodrag"
|
|
43
|
+
value={value ?? defaultValue ?? ''}
|
|
44
|
+
onChange={(e) => onChange(name, e.currentTarget.value)}
|
|
45
|
+
>
|
|
46
|
+
<>
|
|
47
|
+
{choices.map((choice) => (
|
|
48
|
+
<option key={choice.text} value={choice.value}>
|
|
49
|
+
{choice.text}
|
|
50
|
+
</option>
|
|
51
|
+
))}
|
|
52
|
+
</>
|
|
53
|
+
</select>
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
{valueType === 'string' && (
|
|
59
|
+
<AutoSizeInput
|
|
60
|
+
type="text"
|
|
61
|
+
className="bg-gray-600 disabled:bg-gray-700 py-1 px-2 nodrag"
|
|
62
|
+
value={inputVal}
|
|
63
|
+
onChange={(e) => onChange(name, e.currentTarget.value)}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
{valueType === 'number' && (
|
|
67
|
+
<AutoSizeInput
|
|
68
|
+
type="number"
|
|
69
|
+
className=" bg-gray-600 disabled:bg-gray-700 py-1 px-2 nodrag"
|
|
70
|
+
value={inputVal}
|
|
71
|
+
onChange={(e) => onChange(name, e.currentTarget.value)}
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
{valueType === 'float' && (
|
|
75
|
+
<AutoSizeInput
|
|
76
|
+
type="number"
|
|
77
|
+
className=" bg-gray-600 disabled:bg-gray-700 py-1 px-2 nodrag"
|
|
78
|
+
value={inputVal}
|
|
79
|
+
onChange={(e) => onChange(name, e.currentTarget.value)}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
{valueType === 'integer' && (
|
|
83
|
+
<AutoSizeInput
|
|
84
|
+
type="number"
|
|
85
|
+
className=" bg-gray-600 disabled:bg-gray-700 py-1 px-2 nodrag"
|
|
86
|
+
value={inputVal}
|
|
87
|
+
onChange={(e) => onChange(name, e.currentTarget.value)}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
{valueType === 'boolean' && (
|
|
91
|
+
<input
|
|
92
|
+
type="checkbox"
|
|
93
|
+
className=" bg-gray-600 disabled:bg-gray-700 py-1 px-2 nodrag"
|
|
94
|
+
value={inputVal}
|
|
95
|
+
onChange={(e) => onChange(name, e.currentTarget.checked)}
|
|
96
|
+
/>
|
|
97
|
+
)}
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const InputSocket: React.FC<InputSocketProps> = ({
|
|
103
|
+
connected,
|
|
104
|
+
specJSON,
|
|
105
|
+
...rest
|
|
106
|
+
}) => {
|
|
107
|
+
const { name, valueType } = rest;
|
|
108
|
+
const instance = useReactFlow();
|
|
109
|
+
|
|
110
|
+
const isFlowSocket = valueType === 'flow';
|
|
111
|
+
|
|
112
|
+
let colorName = valueTypeColorMap[valueType];
|
|
113
|
+
if (colorName === undefined) {
|
|
114
|
+
colorName = 'red';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// @ts-ignore
|
|
118
|
+
const [backgroundColor, borderColor] = colors[colorName];
|
|
119
|
+
const showName = isFlowSocket === false || name !== 'flow';
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className="flex grow items-center justify-start h-7">
|
|
123
|
+
{isFlowSocket && (
|
|
124
|
+
<FontAwesomeIcon icon={faCaretRight} color="#ffffff" size="lg" />
|
|
125
|
+
)}
|
|
126
|
+
{showName && <div className="capitalize mr-2">{name}</div>}
|
|
127
|
+
|
|
128
|
+
{!isFlowSocket && !connected && <InputFieldForValue {...rest} />}
|
|
129
|
+
<Handle
|
|
130
|
+
id={name}
|
|
131
|
+
type="target"
|
|
132
|
+
position={Position.Left}
|
|
133
|
+
className={cx(borderColor, connected ? backgroundColor : 'bg-gray-800')}
|
|
134
|
+
isValidConnection={(connection: Connection) =>
|
|
135
|
+
isValidConnection(connection, instance, specJSON)
|
|
136
|
+
}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export default InputSocket;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useEdges, type NodeProps as FlowNodeProps } from 'reactflow';
|
|
4
|
+
|
|
5
|
+
import { useChangeNodeData } from '../hooks/useChangeNodeData.js';
|
|
6
|
+
import { isHandleConnected } from '../util/isHandleConnected.js';
|
|
7
|
+
import InputSocket from './InputSocket.js';
|
|
8
|
+
import NodeContainer from './NodeContainer.js';
|
|
9
|
+
import OutputSocket from './OutputSocket.js';
|
|
10
|
+
|
|
11
|
+
type NodeProps = FlowNodeProps & {
|
|
12
|
+
spec: NodeSpecJSON;
|
|
13
|
+
allSpecs: NodeSpecJSON[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const getPairs = <T, U>(arr1: T[], arr2: U[]) => {
|
|
17
|
+
const max = Math.max(arr1.length, arr2.length);
|
|
18
|
+
const pairs = [];
|
|
19
|
+
for (let i = 0; i < max; i++) {
|
|
20
|
+
const pair: [T | undefined, U | undefined] = [arr1[i], arr2[i]];
|
|
21
|
+
pairs.push(pair);
|
|
22
|
+
}
|
|
23
|
+
return pairs;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Node: React.FC<NodeProps> = ({
|
|
27
|
+
id,
|
|
28
|
+
data,
|
|
29
|
+
spec,
|
|
30
|
+
selected,
|
|
31
|
+
allSpecs
|
|
32
|
+
}: NodeProps) => {
|
|
33
|
+
const edges = useEdges();
|
|
34
|
+
const handleChange = useChangeNodeData(id);
|
|
35
|
+
const pairs = getPairs(spec.inputs, spec.outputs);
|
|
36
|
+
return (
|
|
37
|
+
<NodeContainer
|
|
38
|
+
title={spec.label}
|
|
39
|
+
category={spec.category}
|
|
40
|
+
selected={selected}
|
|
41
|
+
>
|
|
42
|
+
{pairs.map(([input, output], ix) => (
|
|
43
|
+
<div
|
|
44
|
+
key={ix}
|
|
45
|
+
className="flex flex-row justify-between gap-8 relative px-2"
|
|
46
|
+
// className={styles.container}
|
|
47
|
+
>
|
|
48
|
+
{input && (
|
|
49
|
+
<InputSocket
|
|
50
|
+
{...input}
|
|
51
|
+
specJSON={allSpecs}
|
|
52
|
+
value={data[input.name] ?? input.defaultValue}
|
|
53
|
+
onChange={handleChange}
|
|
54
|
+
connected={isHandleConnected(edges, id, input.name, 'target')}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
{output && (
|
|
58
|
+
<OutputSocket
|
|
59
|
+
{...output}
|
|
60
|
+
specJSON={allSpecs}
|
|
61
|
+
connected={isHandleConnected(edges, id, output.name, 'source')}
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
))}
|
|
66
|
+
</NodeContainer>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { NodeCategory, type NodeSpecJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import React, { type PropsWithChildren } from 'react';
|
|
4
|
+
|
|
5
|
+
import { categoryColorMap, colors } from '../util/colors.js';
|
|
6
|
+
|
|
7
|
+
type NodeProps = {
|
|
8
|
+
title: string;
|
|
9
|
+
category?: NodeSpecJSON['category'];
|
|
10
|
+
selected: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const NodeContainer: React.FC<PropsWithChildren<NodeProps>> = ({
|
|
14
|
+
title,
|
|
15
|
+
category = NodeCategory.None,
|
|
16
|
+
selected,
|
|
17
|
+
children
|
|
18
|
+
}) => {
|
|
19
|
+
let colorName = categoryColorMap[category];
|
|
20
|
+
if (colorName === undefined) {
|
|
21
|
+
colorName = 'red';
|
|
22
|
+
}
|
|
23
|
+
let [backgroundColor, borderColor, textColor] = colors[colorName];
|
|
24
|
+
if (selected) {
|
|
25
|
+
borderColor = 'border-gray-800';
|
|
26
|
+
}
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={cx(
|
|
30
|
+
'rounded text-white text-sm bg-gray-800 min-w-[120px]',
|
|
31
|
+
selected && 'outline outline-1'
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
<div className={`${backgroundColor} ${textColor} px-2 py-1 rounded-t`}>
|
|
35
|
+
{title}
|
|
36
|
+
</div>
|
|
37
|
+
<div
|
|
38
|
+
className={`flex flex-col gap-2 py-2 border-l border-r border-b ${borderColor} `}
|
|
39
|
+
>
|
|
40
|
+
{children}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default NodeContainer;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type NodeSpecJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { useReactFlow, type XYPosition } from 'reactflow';
|
|
4
|
+
|
|
5
|
+
import { useOnPressKey } from '../hooks/useOnPressKey.js';
|
|
6
|
+
|
|
7
|
+
export type NodePickerFilters = {
|
|
8
|
+
handleType: 'source' | 'target';
|
|
9
|
+
valueType: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type NodePickerProps = {
|
|
13
|
+
position: XYPosition;
|
|
14
|
+
filters?: NodePickerFilters;
|
|
15
|
+
onPickNode: (type: string, position: XYPosition) => void;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
specJSON: NodeSpecJSON[] | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const NodePicker: React.FC<NodePickerProps> = ({
|
|
21
|
+
position,
|
|
22
|
+
onPickNode,
|
|
23
|
+
onClose,
|
|
24
|
+
filters,
|
|
25
|
+
specJSON
|
|
26
|
+
}: NodePickerProps) => {
|
|
27
|
+
const [search, setSearch] = useState('');
|
|
28
|
+
const instance = useReactFlow();
|
|
29
|
+
|
|
30
|
+
useOnPressKey('Escape', onClose);
|
|
31
|
+
|
|
32
|
+
if (!specJSON) return null;
|
|
33
|
+
let filtered = specJSON;
|
|
34
|
+
if (filters !== undefined) {
|
|
35
|
+
filtered = filtered?.filter((node) => {
|
|
36
|
+
const sockets =
|
|
37
|
+
filters?.handleType === 'source' ? node.outputs : node.inputs;
|
|
38
|
+
return sockets.some((socket) => socket.valueType === filters?.valueType);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
filtered =
|
|
43
|
+
filtered?.filter((node) => {
|
|
44
|
+
const term = search.toLowerCase();
|
|
45
|
+
return node.type.toLowerCase().includes(term);
|
|
46
|
+
}) || [];
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
className="node-picker absolute z-10 text-sm text-white bg-gray-800 border rounded border-gray-500"
|
|
51
|
+
style={{ top: position.y, left: position.x }}
|
|
52
|
+
>
|
|
53
|
+
<div className="bg-gray-500 p-2">Add Node</div>
|
|
54
|
+
<div className="p-2">
|
|
55
|
+
<input
|
|
56
|
+
type="text"
|
|
57
|
+
autoFocus
|
|
58
|
+
placeholder="Type to filter"
|
|
59
|
+
className=" bg-gray-600 disabled:bg-gray-700 w-full py-1 px-2"
|
|
60
|
+
value={search}
|
|
61
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="max-h-48 overflow-y-scroll">
|
|
65
|
+
{filtered.map(({ type }) => (
|
|
66
|
+
<div
|
|
67
|
+
key={type}
|
|
68
|
+
className="p-2 cursor-pointer border-b border-gray-600"
|
|
69
|
+
onClick={() => onPickNode(type, instance.project(position))}
|
|
70
|
+
>
|
|
71
|
+
{type}
|
|
72
|
+
</div>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|