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

@@ -131,6 +131,31 @@
"binaryDataDisplay.backToOverviewPage": "Back to overview page",
"binaryDataDisplay.noDataFoundToDisplay": "No data found to display",
"binaryDataDisplay.yourBrowserDoesNotSupport": "Your browser does not support the video element. Kindly update it to latest version.",
"chat.window.title": "Chat Window ({nodeName})",
"chat.window.logs": "Log (for last message)",
"chat.window.noChatNode": "No Chat Node",
"chat.window.noExecution": "Nothing got executed yet",
"chat.window.chat.placeholder": "Type in message",
"chat.window.chat.sendButtonText": "Send",
"chat.window.chat.chatMessageOptions.reuseMessage": "Reuse Message",
"chat.window.chat.chatMessageOptions.repostMessage": "Repost Message",
"chat.window.chat.chatMessageOptions.executionId": "Execution ID",
"chatEmbed.infoTip.description": "Add chat to external applications using the n8n chat package.",
"chatEmbed.infoTip.link": "More info",
"chatEmbed.title": "Embed Chat in your website",
"chatEmbed.close": "Close",
"chatEmbed.install": "First, install the n8n chat package:",
"chatEmbed.paste.cdn": "Paste the following code anywhere in the {code} tag of your HTML file.",
"chatEmbed.paste.cdn.file": "<body>",
"chatEmbed.paste.vue": "Next, paste the following code in your {code} file.",
"chatEmbed.paste.vue.file": "App.vue",
"chatEmbed.paste.react": "Next, paste the following code in your {code} file.",
"chatEmbed.paste.react.file": "App.ts",
"chatEmbed.paste.other": "Next, paste the following code in your {code} file.",
"chatEmbed.paste.other.file": "main.ts",
"chatEmbed.packageInfo.description": "The n8n Chat widget can be easily customized to fit your needs.",
"chatEmbed.packageInfo.link": "Read the full documentation",
"chatEmbed.url": "https://www.npmjs.com/package/{'@'}n8n/chat",
"codeEdit.edit": "Edit",
"codeNodeEditor.askAi": "✨ Ask AI",
"codeNodeEditor.completer.$()": "Output data of the {nodeName} node",
@@ -711,7 +736,10 @@
"ndv.input": "Input",
"ndv.input.nodeDistance": "({count} node back) | ({count} nodes back)",
"ndv.input.noNodesFound": "No nodes found",
"ndv.input.mapping": "Mapping",
"ndv.input.debugging": "Debugging",
"ndv.input.parentNodes": "Parent nodes",
"ndv.input.previousNode": "Previous node",
"ndv.input.tooMuchData.title": "Input data is huge",
"ndv.input.noOutputDataInBranch": "No input data in this branch",
"ndv.input.noOutputDataInNode": "Node did not output any data. n8n stops executing the workflow when a node has no output data.",
@@ -726,6 +754,9 @@
"ndv.input.disabled": "The '{nodeName}' node is disabled and wont execute.",
"ndv.input.disabled.cta": "Enable it",
"ndv.output": "Output",
"ndv.output.ai.empty": "👈 This is {node}s AI Logs. Click on a node to see the input it received and data it outputted.",
"ndv.output.outType.logs": "Logs",
"ndv.output.outType.regular": "Output",
"ndv.output.edit": "Edit Output",
"ndv.output.all": "all",
"ndv.output.branch": "Branch",
@@ -813,6 +844,18 @@
"nodeCreator.subcategoryDescriptions.flow": "IF, Switch, Wait, Compare and Merge data, etc.",
"nodeCreator.subcategoryDescriptions.helpers": "HTTP Requests (API Calls), date and time, scrape HTML, RSS, SSH, etc.",
"nodeCreator.subcategoryDescriptions.otherTriggerNodes": "Runs the flow on workflow errors, file changes, etc.",
"nodeCreator.subcategoryDescriptions.agents": "Autonomous entities that interact and make decisions.",
"nodeCreator.subcategoryDescriptions.chains": "Structured assemblies for specific tasks.",
"nodeCreator.subcategoryDescriptions.documentLoaders": "Handles loading of documents for processing.",
"nodeCreator.subcategoryDescriptions.embeddings": "Transforms text into vector representations.",
"nodeCreator.subcategoryDescriptions.languageModels": "AI models that understand and generate language.",
"nodeCreator.subcategoryDescriptions.memory": "Manages storage and retrieval of information during execution.",
"nodeCreator.subcategoryDescriptions.outputParsers": "Ensures the output adheres to a defined format.",
"nodeCreator.subcategoryDescriptions.retrievers": "Fetches relevant information from a source.",
"nodeCreator.subcategoryDescriptions.textSplitters": "Breaks down text into smaller parts.",
"nodeCreator.subcategoryDescriptions.tools": "Utility components providing various functionalities.",
"nodeCreator.subcategoryDescriptions.vectorStores": "Handles storage and retrieval of vector representations.",
"nodeCreator.subcategoryDescriptions.miscellaneous": "Other AI related nodes.",
"nodeCreator.subcategoryNames.appTriggerNodes": "On app event",
"nodeCreator.subcategoryNames.appRegularNodes": "Action in an app",
"nodeCreator.subcategoryNames.dataTransformation": "Data transformation",
@@ -820,6 +863,18 @@
"nodeCreator.subcategoryNames.flow": "Flow",
"nodeCreator.subcategoryNames.helpers": "Helpers",
"nodeCreator.subcategoryNames.otherTriggerNodes": "Other ways...",
"nodeCreator.subcategoryNames.agents": "Agents",
"nodeCreator.subcategoryNames.chains": "Chains",
"nodeCreator.subcategoryNames.documentLoaders": "Document Loaders",
"nodeCreator.subcategoryNames.embeddings": "Embeddings",
"nodeCreator.subcategoryNames.languageModels": "Language Models",
"nodeCreator.subcategoryNames.memory": "Memory",
"nodeCreator.subcategoryNames.outputParsers": "Output Parsers",
"nodeCreator.subcategoryNames.retrievers": "Retrievers",
"nodeCreator.subcategoryNames.textSplitters": "Text Splitters",
"nodeCreator.subcategoryNames.tools": "Tools",
"nodeCreator.subcategoryNames.vectorStores": "Vector Stores",
"nodeCreator.subcategoryNames.miscellaneous": "Miscellaneous",
"nodeCreator.triggerHelperPanel.addAnotherTrigger": "Add another trigger",
"nodeCreator.triggerHelperPanel.addAnotherTriggerDescription": "Triggers start your workflow. Workflows can have multiple triggers.",
"nodeCreator.triggerHelperPanel.title": "When should this workflow run?",
@@ -834,6 +889,27 @@
"nodeCreator.triggerHelperPanel.selectATriggerDescription": "A trigger is a step that starts your workflow",
"nodeCreator.triggerHelperPanel.workflowTriggerDisplayName": "When called by another workflow",
"nodeCreator.triggerHelperPanel.workflowTriggerDescription": "Runs the flow when called by the Execute Workflow node from a different workflow",
"nodeCreator.aiPanel.aiNodes": "AI Nodes",
"nodeCreator.aiPanel.aiOtherNodes": "Other AI Nodes",
"nodeCreator.aiPanel.aiOtherNodesDescription": "Embeddings, Vector Stores, LLMs and other AI nodes",
"nodeCreator.aiPanel.selectAiNode": "Select an Al Node to add to your workflow",
"nodeCreator.aiPanel.nodesForAi": "Build autonomous agents, summarize or interrogate documents, etc.",
"nodeCreator.aiPanel.langchainAiNodes": "Advanced AI",
"nodeCreator.aiPanel.title": "When should this workflow run?",
"nodeCreator.aiPanel.infoBox": "Check out our <a href=\"/collections/8\" target=\"_blank\">templates</a> for workflow examples and inspiration.",
"nodeCreator.aiPanel.scheduleTriggerDisplayName": "On a schedule",
"nodeCreator.aiPanel.scheduleTriggerDescription": "Runs the flow every day, hour, or custom interval",
"nodeCreator.aiPanel.webhookTriggerDisplayName": "On webhook call",
"nodeCreator.aiPanel.webhookTriggerDescription": "Runs the flow when another app sends a webhook",
"nodeCreator.aiPanel.manualTriggerDisplayName": "Manually",
"nodeCreator.aiPanel.manualTriggerDescription": "Runs the flow on clicking a button in n8n",
"nodeCreator.aiPanel.whatHappensNext": "What happens next?",
"nodeCreator.aiPanel.selectATrigger": "Select an AI Component",
"nodeCreator.aiPanel.selectATriggerDescription": "A trigger is a step that starts your workflow",
"nodeCreator.aiPanel.workflowTriggerDisplayName": "When called by another workflow",
"nodeCreator.aiPanel.workflowTriggerDescription": "Runs the flow when called by the Execute Workflow node from a different workflow",
"nodeCreator.nodeItem.triggerIconTitle": "Trigger Node",
"nodeCreator.nodeItem.aiIconTitle": "LangChain AI Node",
"nodeCredentials.createNew": "Create New Credential",
"nodeCredentials.credentialFor": "Credential for {credentialType}",
"nodeCredentials.credentialsLabel": "Credential to connect with",
@@ -935,6 +1011,8 @@
"nodeView.showError.openWorkflow.title": "Problem opening workflow",
"nodeView.showError.stopExecution.title": "Problem stopping execution",
"nodeView.showError.stopWaitingForWebhook.title": "Problem deleting test webhook",
"nodeView.showError.nodeNodeCompatible.title": "Connection not possible",
"nodeView.showError.nodeNodeCompatible.message": "The node \"{sourceNodeName}\" can't be connected to the node \"{targetNodeName}\" because they are not compatible.",
"nodeView.showMessage.addNodeButton.message": "'{nodeTypeName}' is an unknown node type",
"nodeView.showMessage.addNodeButton.title": "Could not insert node",
"nodeView.showMessage.keyDown.title": "Workflow created",
@@ -1196,6 +1274,10 @@
"runData.editor.save": "Save",
"runData.editor.cancel": "Cancel",
"runData.editor.copyDataInfo": "You can copy data from previous executions and paste it above.",
"runData.aiContentBlock.startedAt": "Started at {startTime}",
"runData.aiContentBlock.tokens": "{count} Tokens",
"runData.aiContentBlock.tokens.prompt": "Prompt:",
"runData.aiContentBlock.tokens.completion": "Completion:",
"saveButton.save": "@:_reusableBaseText.save",
"saveButton.saved": "Saved",
"saveButton.saving": "Saving",
@@ -1616,6 +1698,7 @@
"nodeIssues.credentials.doNotExist.hint": "You can create credentials with the exact name and then they get auto-selected on refresh..",
"nodeIssues.credentials.notIdentified": "Credentials with name {name} exist for {type}.",
"nodeIssues.credentials.notIdentified.hint": "Credentials are not clearly identified. Please select the correct credentials.",
"nodeIssues.input.missing": "No node connected to required input \"{inputName}\"",
"ndv.trigger.moreInfo": "More info",
"ndv.trigger.copiedTestUrl": "Test URL copied to clipboard",
"ndv.trigger.webhookBasedNode.executionsHelp.inactive": "<b>While building your workflow</b>, click the 'listen' button, then go to {service} and make an event happen. This will trigger an execution, which will show up in this editor.<br /> <br /> <b>Once you're happy with your workflow</b>, <a data-key=\"activate\">activate</a> it. Then every time there's a matching event in {service}, the workflow will execute. These executions will show up in the <a data-key=\"executions\">executions list</a>, but not in the editor.",

View File

@@ -13,10 +13,12 @@ import {
faArrowDown,
faAt,
faBan,
faBars,
faBolt,
faBook,
faBoxOpen,
faBug,
faBrain,
faCalculator,
faCalendar,
faChartBar,
@@ -31,6 +33,8 @@ import {
faCodeBranch,
faCog,
faCogs,
faComment,
faComments,
faClipboardList,
faClock,
faClone,
@@ -39,6 +43,7 @@ import {
faCopy,
faCube,
faCut,
faDatabase,
faDotCircle,
faEdit,
faEllipsisH,
@@ -67,7 +72,9 @@ import {
faGift,
faGlobe,
faGraduationCap,
faGripLinesVertical,
faGripVertical,
faHandScissors,
faHandPointLeft,
faHashtag,
faHdd,
@@ -78,6 +85,7 @@ import {
faInfo,
faInfoCircle,
faKey,
faLanguage,
faLink,
faList,
faLightbulb,
@@ -98,6 +106,7 @@ import {
faQuestion,
faQuestionCircle,
faRedo,
faRobot,
faRss,
faSave,
faSatelliteDish,
@@ -105,6 +114,7 @@ import {
faSearchMinus,
faSearchPlus,
faServer,
faScrewdriver,
faSignInAlt,
faSignOutAlt,
faSlidersH,
@@ -128,12 +138,17 @@ import {
faUserCircle,
faUserFriends,
faUsers,
faVectorSquare,
faVideo,
faTree,
faStickyNote as faSolidStickyNote,
faUserLock,
faGem,
faDownload,
faRemoveFormat,
faTools,
faProjectDiagram,
faStream,
} from '@fortawesome/free-solid-svg-icons';
import { faVariable, faXmark, faVault } from './custom';
import { faStickyNote } from '@fortawesome/free-regular-svg-icons';
@@ -156,10 +171,12 @@ export const FontAwesomePlugin: Plugin<{}> = {
addIcon(faArrowDown);
addIcon(faAt);
addIcon(faBan);
addIcon(faBars);
addIcon(faBolt);
addIcon(faBook);
addIcon(faBoxOpen);
addIcon(faBug);
addIcon(faBrain);
addIcon(faCalculator);
addIcon(faCalendar);
addIcon(faChartBar);
@@ -174,6 +191,8 @@ export const FontAwesomePlugin: Plugin<{}> = {
addIcon(faCodeBranch);
addIcon(faCog);
addIcon(faCogs);
addIcon(faComment);
addIcon(faComments);
addIcon(faClipboardList);
addIcon(faClock);
addIcon(faClone);
@@ -182,7 +201,9 @@ export const FontAwesomePlugin: Plugin<{}> = {
addIcon(faCopy);
addIcon(faCube);
addIcon(faCut);
addIcon(faDatabase);
addIcon(faDotCircle);
addIcon(faGripLinesVertical);
addIcon(faGripVertical);
addIcon(faEdit);
addIcon(faEllipsisH);
@@ -211,6 +232,7 @@ export const FontAwesomePlugin: Plugin<{}> = {
addIcon(faGlobe);
addIcon(faGlobeAmericas);
addIcon(faGraduationCap);
addIcon(faHandScissors);
addIcon(faHandPointLeft);
addIcon(faHashtag);
addIcon(faHdd);
@@ -221,6 +243,7 @@ export const FontAwesomePlugin: Plugin<{}> = {
addIcon(faInfo);
addIcon(faInfoCircle);
addIcon(faKey);
addIcon(faLanguage);
addIcon(faLink);
addIcon(faList);
addIcon(faLightbulb);
@@ -238,9 +261,12 @@ export const FontAwesomePlugin: Plugin<{}> = {
addIcon(faPlus);
addIcon(faPlusCircle);
addIcon(faPlusSquare);
addIcon(faProjectDiagram);
addIcon(faQuestion);
addIcon(faQuestionCircle);
addIcon(faRedo);
addIcon(faRemoveFormat);
addIcon(faRobot);
addIcon(faRss);
addIcon(faSave);
addIcon(faSatelliteDish);
@@ -248,6 +274,7 @@ export const FontAwesomePlugin: Plugin<{}> = {
addIcon(faSearchMinus);
addIcon(faSearchPlus);
addIcon(faServer);
addIcon(faScrewdriver);
addIcon(faSignInAlt);
addIcon(faSignOutAlt);
addIcon(faSlidersH);
@@ -255,6 +282,7 @@ export const FontAwesomePlugin: Plugin<{}> = {
addIcon(faSolidStickyNote);
addIcon(faStickyNote as IconDefinition);
addIcon(faStop);
addIcon(faStream);
addIcon(faSun);
addIcon(faSync);
addIcon(faSyncAlt);
@@ -266,6 +294,7 @@ export const FontAwesomePlugin: Plugin<{}> = {
addIcon(faTimes);
addIcon(faTimesCircle);
addIcon(faToolbox);
addIcon(faTools);
addIcon(faTrash);
addIcon(faUndo);
addIcon(faUnlink);
@@ -275,6 +304,7 @@ export const FontAwesomePlugin: Plugin<{}> = {
addIcon(faUsers);
addIcon(faVariable);
addIcon(faVault);
addIcon(faVectorSquare);
addIcon(faVideo);
addIcon(faTree);
addIcon(faUserLock);

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