feat(core): Add support for building LLM applications (#7235)
This extracts all core and editor changes from #7246 and #7137, so that we can get these changes merged first. ADO-1120 [DB Tests](https://github.com/n8n-io/n8n/actions/runs/6379749011) [E2E Tests](https://github.com/n8n-io/n8n/actions/runs/6379751480) [Workflow Tests](https://github.com/n8n-io/n8n/actions/runs/6379752828) --------- Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com> Co-authored-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Alex Grozav <alex@grozav.com> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
committed by
GitHub
parent
04dfcd73be
commit
00a4b8b0c6
@@ -0,0 +1,80 @@
|
||||
import { registerEndpointRenderer, svg } from '@jsplumb/browser-ui';
|
||||
import { N8nAddInputEndpoint } from './N8nAddInputEndpointType';
|
||||
|
||||
export const register = () => {
|
||||
registerEndpointRenderer<N8nAddInputEndpoint>(N8nAddInputEndpoint.type, {
|
||||
makeNode: (endpointInstance: N8nAddInputEndpoint) => {
|
||||
const xOffset = 1;
|
||||
const lineYOffset = -2;
|
||||
const width = endpointInstance.params.width;
|
||||
const height = endpointInstance.params.height;
|
||||
const unconnectedDiamondSize = width / 2;
|
||||
const unconnectedDiamondWidth = unconnectedDiamondSize * Math.sqrt(2);
|
||||
const unconnectedPlusStroke = 2;
|
||||
const unconnectedPlusSize = width - 2 * unconnectedPlusStroke;
|
||||
|
||||
const sizeDifference = (unconnectedPlusSize - unconnectedDiamondWidth) / 2;
|
||||
|
||||
const container = svg.node('g', {
|
||||
style: `--svg-color: var(${endpointInstance.params.color})`,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
const unconnectedGroup = svg.node('g', { class: 'add-input-endpoint-unconnected' });
|
||||
const unconnectedLine = svg.node('rect', {
|
||||
x: xOffset / 2 + unconnectedDiamondWidth / 2 + sizeDifference,
|
||||
y: unconnectedDiamondWidth + lineYOffset,
|
||||
width: 2,
|
||||
height: height - unconnectedDiamondWidth - unconnectedPlusSize,
|
||||
'stroke-width': 0,
|
||||
class: 'add-input-endpoint-line',
|
||||
});
|
||||
const unconnectedPlusGroup = svg.node('g', {
|
||||
transform: `translate(${xOffset / 2}, ${height - unconnectedPlusSize + lineYOffset})`,
|
||||
});
|
||||
const plusRectangle = svg.node('rect', {
|
||||
x: 1,
|
||||
y: 1,
|
||||
rx: 3,
|
||||
'stroke-width': unconnectedPlusStroke,
|
||||
fillOpacity: 0,
|
||||
height: unconnectedPlusSize,
|
||||
width: unconnectedPlusSize,
|
||||
class: 'add-input-endpoint-plus-rectangle',
|
||||
});
|
||||
const plusIcon = svg.node('path', {
|
||||
transform: `scale(${width / 24})`,
|
||||
d: 'm15.40655,9.89837l-3.30491,0l0,-3.30491c0,-0.40555 -0.32889,-0.73443 -0.73443,-0.73443l-0.73443,0c-0.40554,0 -0.73442,0.32888 -0.73442,0.73443l0,3.30491l-3.30491,0c-0.40555,0 -0.73443,0.32888 -0.73443,0.73442l0,0.73443c0,0.40554 0.32888,0.73443 0.73443,0.73443l3.30491,0l0,3.30491c0,0.40554 0.32888,0.73442 0.73442,0.73442l0.73443,0c0.40554,0 0.73443,-0.32888 0.73443,-0.73442l0,-3.30491l3.30491,0c0.40554,0 0.73442,-0.32889 0.73442,-0.73443l0,-0.73443c0,-0.40554 -0.32888,-0.73442 -0.73442,-0.73442z',
|
||||
class: 'add-input-endpoint-plus-icon',
|
||||
});
|
||||
|
||||
unconnectedPlusGroup.appendChild(plusRectangle);
|
||||
unconnectedPlusGroup.appendChild(plusIcon);
|
||||
unconnectedGroup.appendChild(unconnectedLine);
|
||||
unconnectedGroup.appendChild(unconnectedPlusGroup);
|
||||
|
||||
const defaultGroup = svg.node('g', { class: 'add-input-endpoint-default' });
|
||||
const defaultDiamond = svg.node('rect', {
|
||||
x: xOffset + sizeDifference + unconnectedPlusStroke,
|
||||
y: 0,
|
||||
'stroke-width': 0,
|
||||
width: unconnectedDiamondSize,
|
||||
height: unconnectedDiamondSize,
|
||||
transform: `translate(${unconnectedDiamondWidth / 2}, 0) rotate(45)`,
|
||||
class: 'add-input-endpoint-diamond',
|
||||
});
|
||||
|
||||
defaultGroup.appendChild(defaultDiamond);
|
||||
|
||||
container.appendChild(unconnectedGroup);
|
||||
container.appendChild(defaultGroup);
|
||||
|
||||
endpointInstance.setupOverlays();
|
||||
endpointInstance.setVisible(false);
|
||||
|
||||
return container;
|
||||
},
|
||||
updateNode: (endpointInstance: N8nAddInputEndpoint) => {},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { EndpointHandler, Endpoint } from '@jsplumb/core';
|
||||
import { EndpointRepresentation } from '@jsplumb/core';
|
||||
import type { AnchorPlacement, EndpointRepresentationParams } from '@jsplumb/common';
|
||||
import { EVENT_ENDPOINT_CLICK } from '@jsplumb/browser-ui';
|
||||
|
||||
export type ComputedN8nAddInputEndpoint = [number, number, number, number, number];
|
||||
interface N8nAddInputEndpointParams extends EndpointRepresentationParams {
|
||||
endpoint: Endpoint;
|
||||
width: number;
|
||||
height: number;
|
||||
color: string;
|
||||
multiple: boolean;
|
||||
}
|
||||
export const N8nAddInputEndpointType = 'N8nAddInput';
|
||||
export const EVENT_ADD_INPUT_ENDPOINT_CLICK = 'eventAddInputEndpointClick';
|
||||
export class N8nAddInputEndpoint extends EndpointRepresentation<ComputedN8nAddInputEndpoint> {
|
||||
params: N8nAddInputEndpointParams;
|
||||
|
||||
constructor(endpoint: Endpoint, params: N8nAddInputEndpointParams) {
|
||||
super(endpoint, params);
|
||||
|
||||
this.params = params;
|
||||
this.params.width = params.width || 18;
|
||||
this.params.height = params.height || 48;
|
||||
this.params.color = params.color || '--color-foreground-xdark';
|
||||
this.params.multiple = params.multiple || false;
|
||||
|
||||
this.unbindEvents();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
static type = N8nAddInputEndpointType;
|
||||
type = N8nAddInputEndpoint.type;
|
||||
|
||||
setupOverlays() {
|
||||
this.endpoint.instance.setSuspendDrawing(true);
|
||||
this.endpoint.instance.setSuspendDrawing(false);
|
||||
}
|
||||
bindEvents() {
|
||||
this.instance.bind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||
}
|
||||
unbindEvents() {
|
||||
this.instance.unbind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||
}
|
||||
fireClickEvent = (endpoint: Endpoint) => {
|
||||
if (endpoint === this.endpoint) {
|
||||
this.instance.fire(EVENT_ADD_INPUT_ENDPOINT_CLICK, this.endpoint);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const N8nAddInputEndpointHandler: EndpointHandler<
|
||||
N8nAddInputEndpoint,
|
||||
ComputedN8nAddInputEndpoint
|
||||
> = {
|
||||
type: N8nAddInputEndpoint.type,
|
||||
cls: N8nAddInputEndpoint,
|
||||
compute: (ep: N8nAddInputEndpoint, anchorPoint: AnchorPlacement): ComputedN8nAddInputEndpoint => {
|
||||
const x = anchorPoint.curX - ep.params.width / 2;
|
||||
const y = anchorPoint.curY - ep.params.width / 2;
|
||||
const w = ep.params.width;
|
||||
const h = ep.params.height;
|
||||
|
||||
ep.x = x;
|
||||
ep.y = y;
|
||||
ep.w = w;
|
||||
ep.h = h;
|
||||
|
||||
ep.addClass('add-input-endpoint');
|
||||
if (ep.params.multiple) {
|
||||
ep.addClass('add-input-endpoint-multiple');
|
||||
}
|
||||
return [x, y, w, h, ep.params.width];
|
||||
},
|
||||
|
||||
getParams: (ep: N8nAddInputEndpoint): N8nAddInputEndpointParams => {
|
||||
return ep.params;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { registerEndpointRenderer, svg } from '@jsplumb/browser-ui';
|
||||
import { N8nPlusEndpoint } from './N8nPlusEndpointType';
|
||||
|
||||
export const register = () => {
|
||||
registerEndpointRenderer<N8nPlusEndpoint>(N8nPlusEndpoint.type, {
|
||||
makeNode: (ep: N8nPlusEndpoint) => {
|
||||
const group = svg.node('g');
|
||||
const containerBorder = svg.node('rect', {
|
||||
rx: 3,
|
||||
'stroke-width': 2,
|
||||
fillOpacity: 0,
|
||||
height: ep.params.dimensions - 2,
|
||||
width: ep.params.dimensions - 2,
|
||||
y: 1,
|
||||
x: 1,
|
||||
});
|
||||
const plusPath = svg.node('path', {
|
||||
d: 'm16.40655,10.89837l-3.30491,0l0,-3.30491c0,-0.40555 -0.32889,-0.73443 -0.73443,-0.73443l-0.73443,0c-0.40554,0 -0.73442,0.32888 -0.73442,0.73443l0,3.30491l-3.30491,0c-0.40555,0 -0.73443,0.32888 -0.73443,0.73442l0,0.73443c0,0.40554 0.32888,0.73443 0.73443,0.73443l3.30491,0l0,3.30491c0,0.40554 0.32888,0.73442 0.73442,0.73442l0.73443,0c0.40554,0 0.73443,-0.32888 0.73443,-0.73442l0,-3.30491l3.30491,0c0.40554,0 0.73442,-0.32889 0.73442,-0.73443l0,-0.73443c0,-0.40554 -0.32888,-0.73442 -0.73442,-0.73442z',
|
||||
});
|
||||
if (ep.params.size !== 'medium') {
|
||||
ep.addClass(ep.params.size);
|
||||
}
|
||||
group.appendChild(containerBorder);
|
||||
group.appendChild(plusPath);
|
||||
|
||||
ep.setupOverlays();
|
||||
ep.setVisible(false);
|
||||
return group;
|
||||
},
|
||||
|
||||
updateNode: (ep: N8nPlusEndpoint) => {
|
||||
const ifNoConnections = ep.getConnections().length === 0;
|
||||
|
||||
ep.setIsVisible(ifNoConnections);
|
||||
},
|
||||
});
|
||||
};
|
||||
189
packages/editor-ui/src/plugins/jsplumb/N8nPlusEndpointType.ts
Normal file
189
packages/editor-ui/src/plugins/jsplumb/N8nPlusEndpointType.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import type { EndpointHandler, Endpoint, Overlay } from '@jsplumb/core';
|
||||
import { EndpointRepresentation } from '@jsplumb/core';
|
||||
import type { AnchorPlacement, EndpointRepresentationParams } from '@jsplumb/common';
|
||||
import {
|
||||
createElement,
|
||||
EVENT_ENDPOINT_MOUSEOVER,
|
||||
EVENT_ENDPOINT_MOUSEOUT,
|
||||
EVENT_ENDPOINT_CLICK,
|
||||
EVENT_CONNECTION_ABORT,
|
||||
} from '@jsplumb/browser-ui';
|
||||
|
||||
export type ComputedN8nPlusEndpoint = [number, number, number, number, number];
|
||||
interface N8nPlusEndpointParams extends EndpointRepresentationParams {
|
||||
dimensions: number;
|
||||
connectedEndpoint: Endpoint;
|
||||
hoverMessage: string;
|
||||
size: 'small' | 'medium';
|
||||
showOutputLabel: boolean;
|
||||
}
|
||||
export const PlusStalkOverlay = 'plus-stalk';
|
||||
export const HoverMessageOverlay = 'hover-message';
|
||||
export const N8nPlusEndpointType = 'N8nPlus';
|
||||
export const EVENT_PLUS_ENDPOINT_CLICK = 'eventPlusEndpointClick';
|
||||
export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpoint> {
|
||||
params: N8nPlusEndpointParams;
|
||||
label: string;
|
||||
stalkOverlay: Overlay | null;
|
||||
messageOverlay: Overlay | null;
|
||||
|
||||
constructor(endpoint: Endpoint, params: N8nPlusEndpointParams) {
|
||||
super(endpoint, params);
|
||||
|
||||
this.params = params;
|
||||
|
||||
this.label = '';
|
||||
this.stalkOverlay = null;
|
||||
this.messageOverlay = null;
|
||||
|
||||
this.unbindEvents();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
static type = N8nPlusEndpointType;
|
||||
type = N8nPlusEndpoint.type;
|
||||
|
||||
setupOverlays() {
|
||||
this.clearOverlays();
|
||||
this.endpoint.instance.setSuspendDrawing(true);
|
||||
this.stalkOverlay = this.endpoint.addOverlay({
|
||||
type: 'Custom',
|
||||
options: {
|
||||
id: PlusStalkOverlay,
|
||||
create: () => {
|
||||
const stalk = createElement('div', {}, `${PlusStalkOverlay} ${this.params.size}`);
|
||||
return stalk;
|
||||
},
|
||||
},
|
||||
});
|
||||
this.messageOverlay = this.endpoint.addOverlay({
|
||||
type: 'Custom',
|
||||
options: {
|
||||
id: HoverMessageOverlay,
|
||||
location: 0.5,
|
||||
create: () => {
|
||||
const hoverMessage = createElement('p', {}, `${HoverMessageOverlay} ${this.params.size}`);
|
||||
hoverMessage.innerHTML = this.params.hoverMessage;
|
||||
return hoverMessage;
|
||||
},
|
||||
},
|
||||
});
|
||||
this.endpoint.instance.setSuspendDrawing(false);
|
||||
}
|
||||
bindEvents() {
|
||||
this.instance.bind(EVENT_ENDPOINT_MOUSEOVER, this.setHoverMessageVisible);
|
||||
this.instance.bind(EVENT_ENDPOINT_MOUSEOUT, this.unsetHoverMessageVisible);
|
||||
this.instance.bind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||
this.instance.bind(EVENT_CONNECTION_ABORT, this.setStalkLabels);
|
||||
}
|
||||
unbindEvents() {
|
||||
this.instance.unbind(EVENT_ENDPOINT_MOUSEOVER, this.setHoverMessageVisible);
|
||||
this.instance.unbind(EVENT_ENDPOINT_MOUSEOUT, this.unsetHoverMessageVisible);
|
||||
this.instance.unbind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||
this.instance.unbind(EVENT_CONNECTION_ABORT, this.setStalkLabels);
|
||||
}
|
||||
setStalkLabels = () => {
|
||||
if (!this.endpoint) return;
|
||||
|
||||
const stalkOverlay = this.endpoint.getOverlay(PlusStalkOverlay);
|
||||
const messageOverlay = this.endpoint.getOverlay(HoverMessageOverlay);
|
||||
if (stalkOverlay && messageOverlay) {
|
||||
// Increase the size of the stalk overlay if the label is too long
|
||||
const fnKey = this.label.length > 10 ? 'add' : 'remove';
|
||||
this.instance[`${fnKey}OverlayClass`](stalkOverlay, 'long-stalk');
|
||||
this.instance[`${fnKey}OverlayClass`](messageOverlay, 'long-stalk');
|
||||
this[`${fnKey}Class`]('long-stalk');
|
||||
|
||||
if (this.label) {
|
||||
// @ts-expect-error: Overlay interface is missing the `canvas` property
|
||||
stalkOverlay.canvas.setAttribute('data-label', this.label);
|
||||
}
|
||||
}
|
||||
};
|
||||
fireClickEvent = (endpoint: Endpoint) => {
|
||||
if (endpoint === this.endpoint) {
|
||||
this.instance.fire(EVENT_PLUS_ENDPOINT_CLICK, this.endpoint);
|
||||
}
|
||||
};
|
||||
setHoverMessageVisible = (endpoint: Endpoint) => {
|
||||
if (endpoint === this.endpoint && this.messageOverlay) {
|
||||
this.instance.addOverlayClass(this.messageOverlay, 'visible');
|
||||
}
|
||||
};
|
||||
unsetHoverMessageVisible = (endpoint: Endpoint) => {
|
||||
if (endpoint === this.endpoint && this.messageOverlay) {
|
||||
this.instance.removeOverlayClass(this.messageOverlay, 'visible');
|
||||
}
|
||||
};
|
||||
clearOverlays() {
|
||||
Object.keys(this.endpoint.getOverlays()).forEach((key) => {
|
||||
this.endpoint.removeOverlay(key);
|
||||
});
|
||||
this.stalkOverlay = null;
|
||||
this.messageOverlay = null;
|
||||
}
|
||||
getConnections() {
|
||||
const connections = [
|
||||
...this.endpoint.connections,
|
||||
...this.params.connectedEndpoint.connections,
|
||||
];
|
||||
|
||||
return connections;
|
||||
}
|
||||
setIsVisible(visible: boolean) {
|
||||
this.instance.setSuspendDrawing(true);
|
||||
Object.keys(this.endpoint.getOverlays()).forEach((overlay) => {
|
||||
this.endpoint.getOverlays()[overlay].setVisible(visible);
|
||||
});
|
||||
|
||||
this.setVisible(visible);
|
||||
|
||||
// Re-trigger the success state if label is set
|
||||
if (visible && this.label) {
|
||||
this.setSuccessOutput(this.label);
|
||||
}
|
||||
this.instance.setSuspendDrawing(false);
|
||||
}
|
||||
|
||||
setSuccessOutput(label: string) {
|
||||
this.endpoint.addClass('ep-success');
|
||||
if (this.params.showOutputLabel) {
|
||||
this.label = label;
|
||||
this.setStalkLabels();
|
||||
return;
|
||||
}
|
||||
|
||||
this.endpoint.addClass('ep-success--without-label');
|
||||
}
|
||||
|
||||
clearSuccessOutput() {
|
||||
this.endpoint.removeOverlay('successOutputOverlay');
|
||||
this.endpoint.removeClass('ep-success');
|
||||
this.endpoint.removeClass('ep-success--without-label');
|
||||
this.label = '';
|
||||
this.setStalkLabels();
|
||||
}
|
||||
}
|
||||
|
||||
export const N8nPlusEndpointHandler: EndpointHandler<N8nPlusEndpoint, ComputedN8nPlusEndpoint> = {
|
||||
type: N8nPlusEndpoint.type,
|
||||
cls: N8nPlusEndpoint,
|
||||
compute: (ep: N8nPlusEndpoint, anchorPoint: AnchorPlacement): ComputedN8nPlusEndpoint => {
|
||||
const x = anchorPoint.curX - ep.params.dimensions / 2;
|
||||
const y = anchorPoint.curY - ep.params.dimensions / 2;
|
||||
const w = ep.params.dimensions;
|
||||
const h = ep.params.dimensions;
|
||||
|
||||
ep.x = x;
|
||||
ep.y = y;
|
||||
ep.w = w;
|
||||
ep.h = h;
|
||||
|
||||
ep.addClass('plus-endpoint');
|
||||
return [x, y, w, h, ep.params.dimensions];
|
||||
},
|
||||
|
||||
getParams: (ep: N8nPlusEndpoint): N8nPlusEndpointParams => {
|
||||
return ep.params;
|
||||
},
|
||||
};
|
||||
19
packages/editor-ui/src/plugins/jsplumb/index.ts
Normal file
19
packages/editor-ui/src/plugins/jsplumb/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Plugin } from 'vue';
|
||||
import { N8nPlusEndpointHandler } from '@/plugins/jsplumb/N8nPlusEndpointType';
|
||||
import * as N8nPlusEndpointRenderer from '@/plugins/jsplumb/N8nPlusEndpointRenderer';
|
||||
import { N8nConnector } from '@/plugins/connectors/N8nCustomConnector';
|
||||
import * as N8nAddInputEndpointRenderer from '@/plugins/jsplumb/N8nAddInputEndpointRenderer';
|
||||
import { N8nAddInputEndpointHandler } from '@/plugins/jsplumb/N8nAddInputEndpointType';
|
||||
import { Connectors, EndpointFactory } from '@jsplumb/core';
|
||||
|
||||
export const JsPlumbPlugin: Plugin<{}> = {
|
||||
install: () => {
|
||||
Connectors.register(N8nConnector.type, N8nConnector);
|
||||
|
||||
N8nPlusEndpointRenderer.register();
|
||||
EndpointFactory.registerHandler(N8nPlusEndpointHandler);
|
||||
|
||||
N8nAddInputEndpointRenderer.register();
|
||||
EndpointFactory.registerHandler(N8nAddInputEndpointHandler);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user