@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
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Behave Flow
2
+
3
+ This is a set of UI components for making a UI for the behave flow graphs
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,6 @@
1
+ import autoprefixer from 'autoprefixer';
2
+ import tailwindcss from 'tailwindcss';
3
+
4
+ export default {
5
+ plugins: [tailwindcss, autoprefixer]
6
+ };
@@ -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
+ };