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:
कारतोफ्फेलस्क्रिप्ट™
2023-10-02 17:33:43 +02:00
committed by GitHub
parent 04dfcd73be
commit 00a4b8b0c6
93 changed files with 6209 additions and 728 deletions

View File

@@ -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) => {},
});
};

View File

@@ -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;
},
};

View File

@@ -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);
},
});
};

View 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;
},
};

View 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);
},
};