Skip to content

Commit 089f936

Browse files
committed
vector distance node-type with support for 3 distance calculations
1 parent c970e53 commit 089f936

File tree

2 files changed

+273
-0
lines changed

2 files changed

+273
-0
lines changed

libs/web-flow-executor/src/node-task-registry/canvas-node-task-registry.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ import { loadJSONFile, loadJSONFileNodeName } from '../nodes/load-json-file';
213213
import { getReduce, reduceNodeName } from '../nodes/reduce';
214214
import { getRawJsonNode, jsonNodeName } from '../nodes/raw-json';
215215

216+
import {
217+
getVectorDistanceNode,
218+
vectorDistanceNodeName,
219+
} from '../nodes/vector-distance';
220+
216221
export const canvasNodeTaskRegistry: NodeTypeRegistry<NodeInfo> = {};
217222
export const canvasNodeTaskRegistryLabels: Record<string, string> = {};
218223
export const registerNodeFactory = (
@@ -453,6 +458,8 @@ export const setupCanvasNodeTaskRegistry = (
453458
);
454459

455460
registerNodeFactory(jsonNodeName, getRawJsonNode);
461+
462+
registerNodeFactory(vectorDistanceNodeName, getVectorDistanceNode);
456463
}
457464

458465
registerExternalNodes?.(registerNodeFactory);
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import {
2+
IFlowCanvasBase,
3+
FormFieldType,
4+
IRectNodeComponent,
5+
IThumb,
6+
InitialValues,
7+
NodeTask,
8+
NodeTaskFactory,
9+
ThumbConnectionType,
10+
ThumbType,
11+
createJSXElement,
12+
thumbConstraints,
13+
} from '@devhelpr/visual-programming-system';
14+
import { NodeInfo } from '../types/node-info';
15+
16+
export const vectorDistanceNodeName = 'vector-distance';
17+
const thumbs = [
18+
{
19+
thumbType: ThumbType.StartConnectorCenter,
20+
thumbIndex: 0,
21+
connectionType: ThumbConnectionType.start,
22+
color: 'white',
23+
label: ' ',
24+
maxConnections: -1,
25+
},
26+
{
27+
thumbType: ThumbType.EndConnectorLeft,
28+
thumbIndex: 0,
29+
connectionType: ThumbConnectionType.end,
30+
color: 'white',
31+
label: '[]',
32+
name: 'a',
33+
maxConnections: 1,
34+
prefixLabel: '',
35+
thumbConstraints: thumbConstraints.array,
36+
},
37+
{
38+
thumbType: ThumbType.EndConnectorLeft,
39+
thumbIndex: 1,
40+
connectionType: ThumbConnectionType.end,
41+
color: 'white',
42+
label: '[]',
43+
name: 'b',
44+
maxConnections: 1,
45+
prefixLabel: '',
46+
thumbConstraints: thumbConstraints.array,
47+
},
48+
];
49+
50+
export const getVectorDistanceNode: NodeTaskFactory<NodeInfo> = (
51+
updated: () => void
52+
): NodeTask<any> => {
53+
//let contextInstance: CanvasAppInstance<NodeInfo> | undefined = undefined;
54+
let node: IRectNodeComponent<NodeInfo>;
55+
const initializeCompute = () => {
56+
values = { global: { value1: undefined, value2: undefined } };
57+
return;
58+
};
59+
60+
let values = {
61+
global: {
62+
value1: undefined,
63+
value2: undefined,
64+
},
65+
} as Record<string, Record<string, number[] | undefined>>;
66+
67+
const compute = (
68+
input: string,
69+
_loopIndex?: number,
70+
_payload?: any,
71+
thumbName?: string,
72+
scopeId?: string
73+
) => {
74+
if (scopeId && !values[scopeId]) {
75+
values[scopeId] = {
76+
value1: undefined,
77+
value2: undefined,
78+
};
79+
}
80+
const localValues = values[scopeId ?? 'global'];
81+
if (thumbName === 'a') {
82+
if (!Array.isArray(input)) {
83+
return {
84+
result: undefined,
85+
output: undefined,
86+
stop: true,
87+
followPath: undefined,
88+
};
89+
}
90+
localValues['value1'] = input as unknown as number[];
91+
} else {
92+
if (thumbName === 'b') {
93+
if (!Array.isArray(input)) {
94+
return {
95+
result: undefined,
96+
output: undefined,
97+
stop: true,
98+
followPath: undefined,
99+
};
100+
}
101+
localValues['value2'] = input as unknown as number[];
102+
}
103+
}
104+
105+
if (
106+
localValues['value1'] === undefined ||
107+
localValues['value2'] === undefined
108+
) {
109+
return {
110+
result: undefined,
111+
output: undefined,
112+
stop: true,
113+
followPath: undefined,
114+
};
115+
}
116+
const value1 = localValues['value1'];
117+
const value2 = localValues['value2'];
118+
localValues['value1'] = undefined;
119+
localValues['value2'] = undefined;
120+
const method =
121+
node.nodeInfo?.formValues?.['distance-method'] ?? 'euclidean';
122+
123+
if (value1.length !== value2.length) {
124+
return {
125+
result: undefined,
126+
output: undefined,
127+
stop: true,
128+
followPath: undefined,
129+
};
130+
}
131+
132+
if (method === 'cosine-similarity') {
133+
const dotProduct = value1.reduce(
134+
(acc: number, value: number, index: number) => {
135+
return acc + value * value2[index];
136+
},
137+
0
138+
);
139+
const magnitude1 = Math.sqrt(
140+
value1.reduce((acc: number, value: number) => {
141+
return acc + value * value;
142+
}, 0)
143+
);
144+
const magnitude2 = Math.sqrt(
145+
value2.reduce((acc: number, value: number) => {
146+
return acc + value * value;
147+
}, 0)
148+
);
149+
return {
150+
result: dotProduct / (magnitude1 * magnitude2),
151+
output: dotProduct / (magnitude1 * magnitude2),
152+
followPath: undefined,
153+
};
154+
} else if (method === 'manhattan') {
155+
return {
156+
result: value1.reduce((acc: number, value: number, index: number) => {
157+
return acc + Math.abs(value - value2[index]);
158+
}, 0),
159+
output: value1.reduce((acc: number, value: number, index: number) => {
160+
return acc + Math.abs(value - value2[index]);
161+
}, 0),
162+
followPath: undefined,
163+
};
164+
}
165+
const distance = Math.sqrt(
166+
value1.reduce((acc: number, value: number, index: number) => {
167+
return acc + Math.pow(value - value2[index], 2);
168+
}, 0)
169+
);
170+
return {
171+
result: distance,
172+
output: distance,
173+
followPath: undefined,
174+
};
175+
};
176+
177+
return {
178+
name: vectorDistanceNodeName,
179+
family: 'flow-canvas',
180+
category: 'flow-control',
181+
isContainer: false,
182+
thumbs,
183+
createVisualNode: (
184+
canvasApp: IFlowCanvasBase<NodeInfo>,
185+
x: number,
186+
y: number,
187+
id?: string,
188+
initalValues?: InitialValues,
189+
containerNode?: IRectNodeComponent<NodeInfo>
190+
) => {
191+
const Component = () => (
192+
<div class="inner-node bg-white text-black p-4 rounded flex flex-col justify-center items-center min-w-[150px] clip-merge">
193+
<div>Distance</div>
194+
</div>
195+
);
196+
const nodeThumbs: IThumb[] = [...thumbs];
197+
198+
const rect = canvasApp.createRect(
199+
x,
200+
y,
201+
150,
202+
110,
203+
undefined,
204+
nodeThumbs,
205+
Component() as unknown as HTMLElement,
206+
{
207+
classNames: `bg-slate-500 p-4 rounded`,
208+
},
209+
false,
210+
undefined,
211+
undefined,
212+
id,
213+
{
214+
type: vectorDistanceNodeName,
215+
formValues: {
216+
'distance-method': initalValues?.['distance-method'] ?? 'euclidean',
217+
},
218+
},
219+
containerNode
220+
);
221+
if (!rect.nodeComponent) {
222+
throw new Error('rect.nodeComponent is undefined');
223+
}
224+
rect.resize();
225+
226+
node = rect.nodeComponent;
227+
if (node.nodeInfo) {
228+
node.nodeInfo.formElements = [
229+
{
230+
fieldType: FormFieldType.Select,
231+
fieldName: 'distance-method',
232+
label: 'Distance method',
233+
hideDeleteButton: true,
234+
value: initalValues?.['distance-method'] ?? 'euclidean',
235+
options: [
236+
{ value: 'euclidean', label: 'Euclidean' },
237+
{ value: 'manhattan', label: 'Manhattan' },
238+
{ value: 'cosine-similarity', label: 'Cosine similarity' },
239+
],
240+
onChange: (value: unknown[]) => {
241+
if (!node.nodeInfo) {
242+
return;
243+
}
244+
245+
node.nodeInfo.formValues = {
246+
...node.nodeInfo.formValues,
247+
['distance-method']: value,
248+
};
249+
250+
if (updated) {
251+
updated();
252+
}
253+
},
254+
},
255+
];
256+
node.nodeInfo.isSettingsPopup = true;
257+
node.nodeInfo.compute = compute;
258+
node.nodeInfo.initializeCompute = initializeCompute;
259+
node.nodeInfo.formValues = {
260+
'distance-method': initalValues?.['distance-method'] ?? 'euclidean',
261+
};
262+
}
263+
return node;
264+
},
265+
};
266+
};

0 commit comments

Comments
 (0)