@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,104 @@
|
|
|
1
|
+
import { Engine, readGraphFromJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import type {
|
|
3
|
+
GraphJSON,
|
|
4
|
+
GraphNodes,
|
|
5
|
+
ILifecycleEventEmitter,
|
|
6
|
+
IRegistry
|
|
7
|
+
} from '@kiberon-labs/behave-graph';
|
|
8
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
/** Runs the behavior graph by building the execution
|
|
11
|
+
* engine and triggering start on the lifecycle event emitter.
|
|
12
|
+
*/
|
|
13
|
+
export const useGraphRunner = ({
|
|
14
|
+
graphJson,
|
|
15
|
+
autoRun = false,
|
|
16
|
+
registry
|
|
17
|
+
}: {
|
|
18
|
+
graphJson: GraphJSON | undefined;
|
|
19
|
+
autoRun?: boolean;
|
|
20
|
+
registry: IRegistry;
|
|
21
|
+
}) => {
|
|
22
|
+
const [engine, setEngine] = useState<Engine>();
|
|
23
|
+
|
|
24
|
+
const [run, setRun] = useState(autoRun);
|
|
25
|
+
|
|
26
|
+
const play = useCallback(() => {
|
|
27
|
+
setRun(true);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const pause = useCallback(() => {
|
|
31
|
+
setRun(false);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const togglePlay = useCallback(() => {
|
|
35
|
+
setRun((existing) => !existing);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!graphJson || !registry.values || !run || !registry.dependencies)
|
|
40
|
+
return;
|
|
41
|
+
|
|
42
|
+
let graphNodes: GraphNodes;
|
|
43
|
+
try {
|
|
44
|
+
graphNodes = readGraphFromJSON({
|
|
45
|
+
graphJson,
|
|
46
|
+
registry
|
|
47
|
+
}).nodes;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
console.error(e);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const engine = new Engine(graphNodes);
|
|
53
|
+
|
|
54
|
+
setEngine(engine);
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
engine.dispose();
|
|
58
|
+
setEngine(undefined);
|
|
59
|
+
};
|
|
60
|
+
}, [graphJson, registry.values, registry.nodes, run, registry.dependencies]);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!engine || !run) return;
|
|
64
|
+
|
|
65
|
+
engine.executeAllSync();
|
|
66
|
+
|
|
67
|
+
let timeout: number;
|
|
68
|
+
|
|
69
|
+
const eventEmitter = registry.dependencies
|
|
70
|
+
?.ILifecycleEventEmitter as ILifecycleEventEmitter;
|
|
71
|
+
|
|
72
|
+
const onTick = async () => {
|
|
73
|
+
eventEmitter.tickEvent.emit();
|
|
74
|
+
|
|
75
|
+
// eslint-disable-next-line no-await-in-loop
|
|
76
|
+
await engine.executeAllAsync(500);
|
|
77
|
+
|
|
78
|
+
timeout = window.setTimeout(onTick, 50);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
(async () => {
|
|
82
|
+
if (eventEmitter.startEvent.listenerCount > 0) {
|
|
83
|
+
eventEmitter.startEvent.emit();
|
|
84
|
+
|
|
85
|
+
await engine.executeAllAsync(5);
|
|
86
|
+
} else {
|
|
87
|
+
console.log('has no listener count');
|
|
88
|
+
}
|
|
89
|
+
onTick();
|
|
90
|
+
})();
|
|
91
|
+
|
|
92
|
+
return () => {
|
|
93
|
+
window.clearTimeout(timeout);
|
|
94
|
+
};
|
|
95
|
+
}, [engine, registry.dependencies?.ILifecycleEventEmitter, run]);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
engine,
|
|
99
|
+
playing: run,
|
|
100
|
+
play,
|
|
101
|
+
togglePlay,
|
|
102
|
+
pause
|
|
103
|
+
};
|
|
104
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useMergeMap<T, TMap extends Record<string, T>>(
|
|
4
|
+
mapA: TMap,
|
|
5
|
+
mapB: TMap
|
|
6
|
+
): TMap {
|
|
7
|
+
const [result, setResult] = useState<TMap>(() => ({ ...mapA, ...mapB }));
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
setResult({ ...mapA, ...mapB });
|
|
11
|
+
}, [mapA, mapB]);
|
|
12
|
+
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type IRegistry,
|
|
3
|
+
type NodeSpecJSON,
|
|
4
|
+
writeNodeSpecsToJSON
|
|
5
|
+
} from '@kiberon-labs/behave-graph';
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
export const useNodeSpecJson = (registry: IRegistry) => {
|
|
9
|
+
const [specJson, setSpecJson] = useState<NodeSpecJSON[]>();
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!registry.nodes || !registry.values || !registry.dependencies) {
|
|
13
|
+
setSpecJson(undefined);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
setSpecJson(writeNodeSpecsToJSON(registry));
|
|
17
|
+
}, [registry.nodes, registry.values, registry.dependencies]);
|
|
18
|
+
|
|
19
|
+
return specJson;
|
|
20
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export const useOnPressKey = (
|
|
4
|
+
key: string,
|
|
5
|
+
callback: (e: KeyboardEvent) => void
|
|
6
|
+
) => {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
9
|
+
if (e.code === key) {
|
|
10
|
+
callback(e);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
14
|
+
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
15
|
+
}, [key, callback]);
|
|
16
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IQueryableRegistry } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
export const toQueryableDefinitions = <T>(definitionsMap: {
|
|
5
|
+
[id: string]: T;
|
|
6
|
+
}): IQueryableRegistry<T> => ({
|
|
7
|
+
get: (id: string) => definitionsMap[id],
|
|
8
|
+
getAll: () => Object.values(definitionsMap),
|
|
9
|
+
getAllNames: () => Object.keys(definitionsMap),
|
|
10
|
+
contains: (id: string) => definitionsMap[id] !== undefined
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const useQueryableDefinitions = <T>(definitionsMap: {
|
|
14
|
+
[id: string]: T;
|
|
15
|
+
}): IQueryableRegistry<T> => {
|
|
16
|
+
const queriableDefinitions = useMemo(
|
|
17
|
+
() => toQueryableDefinitions(definitionsMap),
|
|
18
|
+
[definitionsMap]
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
return queriableDefinitions;
|
|
22
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export * from './components/modals/ClearModal.js';
|
|
2
|
+
export * from './components/modals/HelpModal.js';
|
|
3
|
+
export * from './components/modals/LoadModal.js';
|
|
4
|
+
export * from './components/modals/Modal.js';
|
|
5
|
+
export * from './components/modals/SaveModal.js';
|
|
6
|
+
export * from './components/Controls.js';
|
|
7
|
+
|
|
8
|
+
export * from './components/AutoSizeInput.js';
|
|
9
|
+
export * from './components/Controls.js';
|
|
10
|
+
export * from './components/InputSocket.js';
|
|
11
|
+
export * from './components/Node.js';
|
|
12
|
+
export * from './components/Flow.js';
|
|
13
|
+
export * from './components/NodeContainer.js';
|
|
14
|
+
export * from './components/NodePicker.js';
|
|
15
|
+
export * from './components/OutputSocket.js';
|
|
16
|
+
|
|
17
|
+
export * from './hooks/useChangeNodeData.js';
|
|
18
|
+
export * from './hooks/useOnPressKey.js';
|
|
19
|
+
export * from './hooks/useFlowHandlers.js';
|
|
20
|
+
export * from './hooks/useGraphRunner.js';
|
|
21
|
+
export * from './hooks/useBehaveGraphFlow.js';
|
|
22
|
+
export * from './hooks/useNodeSpecJson.js';
|
|
23
|
+
export * from './hooks/useCustomNodeTypes.js';
|
|
24
|
+
export * from './hooks/useMergeMap.js';
|
|
25
|
+
|
|
26
|
+
export * from './transformers/behaveToFlow.js';
|
|
27
|
+
export * from './transformers/flowToBehave.js';
|
|
28
|
+
|
|
29
|
+
export * from './util/autoLayout.js';
|
|
30
|
+
export * from './util/calculateNewEdge.js';
|
|
31
|
+
export * from './util/colors.js';
|
|
32
|
+
export * from './util/getPickerFilters.js';
|
|
33
|
+
export * from './util/getSocketsByNodeTypeAndHandleType.js';
|
|
34
|
+
export * from './util/hasPositionMetaData.js';
|
|
35
|
+
export * from './util/isHandleConnected.js';
|
|
36
|
+
export * from './util/isValidConnection.js';
|
|
37
|
+
export * from './util/sleep.js';
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { GraphJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import type { Edge, Node } from 'reactflow';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
|
|
5
|
+
export const behaveToFlow = (graph: GraphJSON): [Node[], Edge[]] => {
|
|
6
|
+
const nodes: Node[] = [];
|
|
7
|
+
const edges: Edge[] = [];
|
|
8
|
+
|
|
9
|
+
graph.nodes?.forEach((nodeJSON) => {
|
|
10
|
+
const node: Node = {
|
|
11
|
+
id: nodeJSON.id,
|
|
12
|
+
type: nodeJSON.type,
|
|
13
|
+
position: {
|
|
14
|
+
x: nodeJSON.metadata?.positionX
|
|
15
|
+
? Number(nodeJSON.metadata?.positionX)
|
|
16
|
+
: 0,
|
|
17
|
+
y: nodeJSON.metadata?.positionY
|
|
18
|
+
? Number(nodeJSON.metadata?.positionY)
|
|
19
|
+
: 0
|
|
20
|
+
},
|
|
21
|
+
data: {} as { [key: string]: any }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
nodes.push(node);
|
|
25
|
+
|
|
26
|
+
if (nodeJSON.parameters) {
|
|
27
|
+
for (const [inputKey, input] of Object.entries(nodeJSON.parameters)) {
|
|
28
|
+
if ('link' in input && input.link !== undefined) {
|
|
29
|
+
edges.push({
|
|
30
|
+
id: uuidv4(),
|
|
31
|
+
source: input.link.nodeId,
|
|
32
|
+
sourceHandle: input.link.socket,
|
|
33
|
+
target: nodeJSON.id,
|
|
34
|
+
targetHandle: inputKey
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if ('value' in input) {
|
|
38
|
+
node.data[inputKey] = input.value;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (nodeJSON.flows) {
|
|
44
|
+
for (const [inputKey, link] of Object.entries(nodeJSON.flows)) {
|
|
45
|
+
edges.push({
|
|
46
|
+
id: uuidv4(),
|
|
47
|
+
source: nodeJSON.id,
|
|
48
|
+
sourceHandle: inputKey,
|
|
49
|
+
target: link.nodeId,
|
|
50
|
+
targetHandle: link.socket
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return [nodes, edges];
|
|
57
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GraphJSON,
|
|
3
|
+
NodeJSON,
|
|
4
|
+
NodeSpecJSON
|
|
5
|
+
} from '@kiberon-labs/behave-graph';
|
|
6
|
+
import type { Edge, Node } from 'reactflow';
|
|
7
|
+
|
|
8
|
+
const isNullish = (value: any): value is null | undefined =>
|
|
9
|
+
value === undefined || value === null;
|
|
10
|
+
|
|
11
|
+
export const flowToBehave = (
|
|
12
|
+
nodes: Node[],
|
|
13
|
+
edges: Edge[],
|
|
14
|
+
nodeSpecJSON: NodeSpecJSON[]
|
|
15
|
+
): GraphJSON => {
|
|
16
|
+
const graph: GraphJSON = { nodes: [], variables: [], customEvents: [] };
|
|
17
|
+
|
|
18
|
+
nodes.forEach((node) => {
|
|
19
|
+
if (node.type === undefined) return;
|
|
20
|
+
|
|
21
|
+
const nodeSpec = nodeSpecJSON.find(
|
|
22
|
+
(nodeSpec) => nodeSpec.type === node.type
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (nodeSpec === undefined) return;
|
|
26
|
+
|
|
27
|
+
const behaveNode: NodeJSON = {
|
|
28
|
+
id: node.id,
|
|
29
|
+
type: node.type,
|
|
30
|
+
metadata: {
|
|
31
|
+
positionX: String(node.position.x),
|
|
32
|
+
positionY: String(node.position.y)
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
Object.entries(node.data).forEach(([key, value]) => {
|
|
37
|
+
if (behaveNode.parameters === undefined) {
|
|
38
|
+
behaveNode.parameters = {};
|
|
39
|
+
}
|
|
40
|
+
behaveNode.parameters[key] = { value: value as string };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
edges
|
|
44
|
+
.filter((edge) => edge.target === node.id)
|
|
45
|
+
.forEach((edge) => {
|
|
46
|
+
const inputSpec = nodeSpec.inputs.find(
|
|
47
|
+
(input) => input.name === edge.targetHandle
|
|
48
|
+
);
|
|
49
|
+
if (inputSpec && inputSpec.valueType === 'flow') {
|
|
50
|
+
// skip flows
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (behaveNode.parameters === undefined) {
|
|
54
|
+
behaveNode.parameters = {};
|
|
55
|
+
}
|
|
56
|
+
if (isNullish(edge.targetHandle)) return;
|
|
57
|
+
if (isNullish(edge.sourceHandle)) return;
|
|
58
|
+
|
|
59
|
+
// TODO: some of these are flow outputs, and should be saved differently. -Ben, Oct 11, 2022
|
|
60
|
+
behaveNode.parameters[edge.targetHandle] = {
|
|
61
|
+
link: { nodeId: edge.source, socket: edge.sourceHandle }
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
edges
|
|
66
|
+
.filter((edge) => edge.source === node.id)
|
|
67
|
+
.forEach((edge) => {
|
|
68
|
+
const outputSpec = nodeSpec.outputs.find(
|
|
69
|
+
(output) => output.name === edge.sourceHandle
|
|
70
|
+
);
|
|
71
|
+
if (outputSpec && outputSpec.valueType !== 'flow') {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (behaveNode.flows === undefined) {
|
|
75
|
+
behaveNode.flows = {};
|
|
76
|
+
}
|
|
77
|
+
if (isNullish(edge.targetHandle)) return;
|
|
78
|
+
if (isNullish(edge.sourceHandle)) return;
|
|
79
|
+
|
|
80
|
+
// TODO: some of these are flow outputs, and should be saved differently. -Ben, Oct 11, 2022
|
|
81
|
+
behaveNode.flows[edge.sourceHandle] = {
|
|
82
|
+
nodeId: edge.target,
|
|
83
|
+
socket: edge.targetHandle
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// TODO filter out any orphan nodes at this point, to avoid errors further down inside behave-graph
|
|
88
|
+
|
|
89
|
+
graph.nodes?.push(behaveNode);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return graph;
|
|
93
|
+
};
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import type { Node, OnConnectStartParams } from 'reactflow';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
|
|
5
|
+
import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType.js';
|
|
6
|
+
|
|
7
|
+
export const calculateNewEdge = (
|
|
8
|
+
originNode: Node,
|
|
9
|
+
destinationNodeType: string,
|
|
10
|
+
destinationNodeId: string,
|
|
11
|
+
connection: OnConnectStartParams,
|
|
12
|
+
specJSON: NodeSpecJSON[]
|
|
13
|
+
) => {
|
|
14
|
+
const sockets = getSocketsByNodeTypeAndHandleType(
|
|
15
|
+
specJSON,
|
|
16
|
+
originNode.type,
|
|
17
|
+
connection.handleType
|
|
18
|
+
);
|
|
19
|
+
const originSocket = sockets?.find(
|
|
20
|
+
(socket) => socket.name === connection.handleId
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const newSockets = getSocketsByNodeTypeAndHandleType(
|
|
24
|
+
specJSON,
|
|
25
|
+
destinationNodeType,
|
|
26
|
+
connection.handleType === 'source' ? 'target' : 'source'
|
|
27
|
+
);
|
|
28
|
+
const newSocket = newSockets?.find(
|
|
29
|
+
(socket) => socket.valueType === originSocket?.valueType
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (connection.handleType === 'source') {
|
|
33
|
+
return {
|
|
34
|
+
id: uuidv4(),
|
|
35
|
+
source: connection.nodeId ?? '',
|
|
36
|
+
sourceHandle: connection.handleId,
|
|
37
|
+
target: destinationNodeId,
|
|
38
|
+
targetHandle: newSocket?.name
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
id: uuidv4(),
|
|
44
|
+
target: connection.nodeId ?? '',
|
|
45
|
+
targetHandle: connection.handleId,
|
|
46
|
+
source: destinationNodeId,
|
|
47
|
+
sourceHandle: newSocket?.name
|
|
48
|
+
};
|
|
49
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
|
|
3
|
+
export type color =
|
|
4
|
+
| 'red'
|
|
5
|
+
| 'green'
|
|
6
|
+
| 'lime'
|
|
7
|
+
| 'purple'
|
|
8
|
+
| 'blue'
|
|
9
|
+
| 'gray'
|
|
10
|
+
| 'white';
|
|
11
|
+
|
|
12
|
+
export const colors: Record<color, [string, string, string]> = {
|
|
13
|
+
red: ['bg-orange-700', 'border-orange-700', 'text-white'],
|
|
14
|
+
green: ['bg-green-600', 'border-green-600', 'text-white'],
|
|
15
|
+
lime: ['bg-lime-500', 'border-lime-500', 'text-gray-900'],
|
|
16
|
+
purple: ['bg-purple-500', 'border-purple-500', 'text-white'],
|
|
17
|
+
blue: ['bg-cyan-600', 'border-cyan-600', 'text-white'],
|
|
18
|
+
gray: ['bg-gray-500', 'border-gray-500', 'text-white'],
|
|
19
|
+
white: ['bg-white', 'border-white', 'text-gray-700']
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const valueTypeColorMap: Record<string, string> = {
|
|
23
|
+
flow: 'white',
|
|
24
|
+
number: 'green',
|
|
25
|
+
float: 'green',
|
|
26
|
+
integer: 'lime',
|
|
27
|
+
boolean: 'red',
|
|
28
|
+
string: 'purple'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const categoryColorMap: Record<NodeSpecJSON['category'], color> = {
|
|
32
|
+
Event: 'red',
|
|
33
|
+
Logic: 'green',
|
|
34
|
+
Variable: 'purple',
|
|
35
|
+
Query: 'purple',
|
|
36
|
+
Action: 'blue',
|
|
37
|
+
Flow: 'gray',
|
|
38
|
+
Effect: 'lime',
|
|
39
|
+
Time: 'gray',
|
|
40
|
+
None: 'gray'
|
|
41
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import type { Node, OnConnectStartParams } from 'reactflow';
|
|
3
|
+
import type { NodePickerFilters } from '../components/NodePicker.js';
|
|
4
|
+
import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType.js';
|
|
5
|
+
|
|
6
|
+
export const getNodePickerFilters = (
|
|
7
|
+
nodes: Node[],
|
|
8
|
+
params: OnConnectStartParams | undefined,
|
|
9
|
+
specJSON: NodeSpecJSON[] | undefined
|
|
10
|
+
): NodePickerFilters | undefined => {
|
|
11
|
+
if (params === undefined) return;
|
|
12
|
+
|
|
13
|
+
const originNode = nodes.find((node) => node.id === params.nodeId);
|
|
14
|
+
if (originNode === undefined) return;
|
|
15
|
+
|
|
16
|
+
const sockets = specJSON
|
|
17
|
+
? getSocketsByNodeTypeAndHandleType(
|
|
18
|
+
specJSON,
|
|
19
|
+
originNode.type,
|
|
20
|
+
params.handleType
|
|
21
|
+
)
|
|
22
|
+
: undefined;
|
|
23
|
+
|
|
24
|
+
const socket = sockets?.find((socket) => socket.name === params.handleId);
|
|
25
|
+
|
|
26
|
+
if (socket === undefined) return;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
handleType: params.handleType === 'source' ? 'target' : 'source',
|
|
30
|
+
valueType: socket.valueType
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
|
|
3
|
+
export const getSocketsByNodeTypeAndHandleType = (
|
|
4
|
+
nodes: NodeSpecJSON[],
|
|
5
|
+
nodeType: string | undefined,
|
|
6
|
+
handleType: 'source' | 'target' | null
|
|
7
|
+
) => {
|
|
8
|
+
const nodeSpec = nodes.find((node) => node.type === nodeType);
|
|
9
|
+
if (nodeSpec === undefined) return;
|
|
10
|
+
return handleType === 'source' ? nodeSpec.outputs : nodeSpec.inputs;
|
|
11
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { GraphJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
|
|
3
|
+
export const hasPositionMetaData = (graph: GraphJSON): boolean => {
|
|
4
|
+
if (graph.nodes === undefined) return false;
|
|
5
|
+
return graph.nodes.some(
|
|
6
|
+
(node) =>
|
|
7
|
+
node.metadata?.positionX !== undefined ||
|
|
8
|
+
node.metadata?.positionY !== undefined
|
|
9
|
+
);
|
|
10
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Edge } from 'reactflow';
|
|
2
|
+
|
|
3
|
+
export const isHandleConnected = (
|
|
4
|
+
edges: Edge[],
|
|
5
|
+
nodeId: string,
|
|
6
|
+
handleId: string,
|
|
7
|
+
type: 'source' | 'target'
|
|
8
|
+
) => {
|
|
9
|
+
return edges.some(
|
|
10
|
+
(edge) => edge[type] === nodeId && edge[`${type}Handle`] === handleId
|
|
11
|
+
);
|
|
12
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
|
|
2
|
+
import type { Connection, ReactFlowInstance } from 'reactflow';
|
|
3
|
+
|
|
4
|
+
import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType.js';
|
|
5
|
+
import { isHandleConnected } from './isHandleConnected.js';
|
|
6
|
+
|
|
7
|
+
export const isValidConnection = (
|
|
8
|
+
connection: Connection,
|
|
9
|
+
instance: ReactFlowInstance,
|
|
10
|
+
specJSON: NodeSpecJSON[]
|
|
11
|
+
) => {
|
|
12
|
+
if (connection.source === null || connection.target === null) return false;
|
|
13
|
+
|
|
14
|
+
const sourceNode = instance.getNode(connection.source);
|
|
15
|
+
const targetNode = instance.getNode(connection.target);
|
|
16
|
+
const edges = instance.getEdges();
|
|
17
|
+
|
|
18
|
+
if (sourceNode === undefined || targetNode === undefined) return false;
|
|
19
|
+
|
|
20
|
+
const sourceSockets = getSocketsByNodeTypeAndHandleType(
|
|
21
|
+
specJSON,
|
|
22
|
+
sourceNode.type,
|
|
23
|
+
'source'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const sourceSocket = sourceSockets?.find(
|
|
27
|
+
(socket) => socket.name === connection.sourceHandle
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const targetSockets = getSocketsByNodeTypeAndHandleType(
|
|
31
|
+
specJSON,
|
|
32
|
+
targetNode.type,
|
|
33
|
+
'target'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const targetSocket = targetSockets?.find(
|
|
37
|
+
(socket) => socket.name === connection.targetHandle
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (sourceSocket === undefined || targetSocket === undefined) return false;
|
|
41
|
+
|
|
42
|
+
// only flow sockets can have two inputs
|
|
43
|
+
if (
|
|
44
|
+
targetSocket.valueType !== 'flow' &&
|
|
45
|
+
isHandleConnected(edges, targetNode.id, targetSocket.name, 'target')
|
|
46
|
+
) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return sourceSocket.valueType === targetSocket.valueType;
|
|
51
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @type {import('tailwindcss').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
content: [
|
|
4
|
+
'./public/index.html',
|
|
5
|
+
'./src/**/*.{js,ts,jsx,tsx,css}',
|
|
6
|
+
'../../packages/flow/dist/**/*.{js,ts,jsx,tsx,css}'
|
|
7
|
+
],
|
|
8
|
+
theme: {
|
|
9
|
+
extend: {}
|
|
10
|
+
},
|
|
11
|
+
plugins: [
|
|
12
|
+
{
|
|
13
|
+
tailwindcss: {}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
autoprefixer: {}
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerCoreProfile,
|
|
3
|
+
type GraphJSON,
|
|
4
|
+
writeNodeSpecsToJSON
|
|
5
|
+
} from '@kiberon-labs/behave-graph';
|
|
6
|
+
|
|
7
|
+
import rawFlowGraph from '../../../graphs/react-flow/graph.json';
|
|
8
|
+
import { behaveToFlow } from '../src/transformers/behaveToFlow.js';
|
|
9
|
+
import { flowToBehave } from '../src/transformers/flowToBehave.js';
|
|
10
|
+
import { it, expect } from 'vitest';
|
|
11
|
+
|
|
12
|
+
const flowGraph = rawFlowGraph as GraphJSON;
|
|
13
|
+
|
|
14
|
+
const [nodes, edges] = behaveToFlow(flowGraph);
|
|
15
|
+
|
|
16
|
+
it('transforms from flow to behave', () => {
|
|
17
|
+
const registry = registerCoreProfile({
|
|
18
|
+
nodes: {},
|
|
19
|
+
values: {},
|
|
20
|
+
dependencies: {}
|
|
21
|
+
});
|
|
22
|
+
const specJSON = writeNodeSpecsToJSON(registry);
|
|
23
|
+
const output = flowToBehave(nodes, edges, specJSON);
|
|
24
|
+
expect(output).toEqual(flowGraph);
|
|
25
|
+
});
|