mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat: Add Chat Trigger node (#7409)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com> Co-authored-by: Jesper Bylund <mail@jesperbylund.com> Co-authored-by: OlegIvaniv <me@olegivaniv.com> Co-authored-by: Deborah <deborah@starfallprojects.co.uk> Co-authored-by: Jan Oberhauser <janober@users.noreply.github.com> Co-authored-by: Jon <jonathan.bennetts@gmail.com> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: Mason Geloso <Mason.geloso@gmail.com> Co-authored-by: Mason Geloso <hone@Masons-Mac-mini.local> Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
This commit is contained in:
parent
1387541e33
commit
af49e95cc7
|
@ -36,7 +36,7 @@ export const INSTANCE_MEMBERS = [
|
|||
|
||||
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
|
||||
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Test Workflow"';
|
||||
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Manual Chat Trigger';
|
||||
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
|
||||
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
||||
export const CODE_NODE_NAME = 'Code';
|
||||
export const SET_NODE_NAME = 'Set';
|
||||
|
|
|
@ -35,7 +35,7 @@ describe('Node Creator', () => {
|
|||
nodeCreatorFeature.actions.openNodeCreator();
|
||||
|
||||
nodeCreatorFeature.getters.searchBar().find('input').type('manual');
|
||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 3);
|
||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 2);
|
||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('manual123');
|
||||
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
|
||||
nodeCreatorFeature.getters
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
This is an embeddable Chat widget for n8n. It allows the execution of AI-Powered Workflows through a Chat window.
|
||||
|
||||
## Prerequisites
|
||||
Create a n8n workflow which you want to execute via chat. The workflow has to be triggered using a **Webhook** node and return data using the **Respond to Webhook** node.
|
||||
Create a n8n workflow which you want to execute via chat. The workflow has to be triggered using a **Chat Trigger** node.
|
||||
|
||||
Open the **Webhook** node and add your domain to the **Domain Allowlist** field. This makes sure that only requests from your domain are accepted.
|
||||
Open the **Chat Trigger** node and add your domain to the **Allowed Origins (CORS)** field. This makes sure that only requests from your domain are accepted.
|
||||
|
||||
[See example workflow](https://github.com/n8n-io/n8n/blob/master/packages/%40n8n/chat/resources/workflow.json)
|
||||
|
||||
|
@ -17,8 +17,6 @@ Each request is accompanied by an `action` query parameter, where `action` can b
|
|||
- `loadPreviousSession` - When the user opens the Chatbot again and the previous chat session should be loaded
|
||||
- `sendMessage` - When the user sends a message
|
||||
|
||||
We use the `Switch` node to handle the different actions.
|
||||
|
||||
## Installation
|
||||
|
||||
Open the **Webhook** node and replace `YOUR_PRODUCTION_WEBHOOK_URL` with your production URL. This is the URL that the Chat widget will use to send requests to.
|
||||
|
@ -106,6 +104,10 @@ createChat({
|
|||
},
|
||||
target: '#n8n-chat',
|
||||
mode: 'window',
|
||||
chatInputKey: 'chatInput',
|
||||
chatSessionKey: 'sessionId',
|
||||
metadata: {},
|
||||
showWelcomeScreen: false,
|
||||
defaultLanguage: 'en',
|
||||
initialMessages: [
|
||||
'Hi there! 👋',
|
||||
|
@ -148,6 +150,21 @@ createChat({
|
|||
- In `window` mode, the Chat window will be embedded in the target element as a chat toggle button and a fixed size chat window.
|
||||
- In `fullscreen` mode, the Chat will take up the entire width and height of its target container.
|
||||
|
||||
### `showWelcomeScreen`
|
||||
- **Type**: `boolean`
|
||||
- **Default**: `false`
|
||||
- **Description**: Whether to show the welcome screen when the Chat window is opened.
|
||||
|
||||
### `chatSessionKey`
|
||||
- **Type**: `string`
|
||||
- **Default**: `'sessionId'`
|
||||
- **Description**: The key to use for sending the chat history session ID for the AI Memory node.
|
||||
|
||||
### `chatInputKey`
|
||||
- **Type**: `string`
|
||||
- **Default**: `'chatInput'`
|
||||
- **Description**: The key to use for sending the chat input for the AI Agent node.
|
||||
|
||||
### `defaultLanguage`
|
||||
- **Type**: `string`
|
||||
- **Default**: `'en'`
|
||||
|
|
21
packages/@n8n/chat/build.config.js
Normal file
21
packages/@n8n/chat/build.config.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { defineBuildConfig } from 'unbuild';
|
||||
|
||||
export default defineBuildConfig({
|
||||
entries: [
|
||||
{
|
||||
builder: 'mkdist',
|
||||
format: 'esm',
|
||||
input: './src',
|
||||
outDir: './tmp/lib',
|
||||
},
|
||||
{
|
||||
builder: 'mkdist',
|
||||
format: 'cjs',
|
||||
input: './src',
|
||||
outDir: './tmp/cjs',
|
||||
},
|
||||
],
|
||||
clean: true,
|
||||
declaration: true,
|
||||
failOnWarn: false,
|
||||
});
|
|
@ -3,9 +3,11 @@
|
|||
"version": "0.6.0",
|
||||
"scripts": {
|
||||
"dev": "pnpm run storybook",
|
||||
"build": "pnpm type-check && pnpm build:vite && pnpm build:prepare",
|
||||
"build:vite": "vite build && npm run build:vite:full",
|
||||
"build": "pnpm type-check && pnpm build:vite && pnpm run build:individual && npm run build:prepare",
|
||||
"build:full": "pnpm type-check && pnpm build:vite && pnpm build:vite:full && pnpm run build:individual && npm run build:prepare",
|
||||
"build:vite": "vite build",
|
||||
"build:vite:full": "INCLUDE_VUE=true vite build",
|
||||
"build:individual": "unbuild",
|
||||
"build:prepare": "node scripts/postbuild.js",
|
||||
"build:pack": "node scripts/pack.js",
|
||||
"preview": "vite preview",
|
||||
|
@ -16,7 +18,7 @@
|
|||
"format": "prettier --write src/",
|
||||
"storybook": "storybook dev -p 6006 --no-open",
|
||||
"build:storybook": "storybook build",
|
||||
"release": "pnpm run build && cd dist && pnpm publish"
|
||||
"release": "pnpm run build:full && cd dist && pnpm publish"
|
||||
},
|
||||
"main": "./chat.umd.cjs",
|
||||
"module": "./chat.es.js",
|
||||
|
@ -29,6 +31,10 @@
|
|||
"./style.css": {
|
||||
"import": "./style.css",
|
||||
"require": "./style.css"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./*",
|
||||
"require": "./*"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -39,8 +45,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/mdi": "^1.1.54",
|
||||
"n8n-design-system": "workspace:*",
|
||||
"shelljs": "^0.8.5",
|
||||
"unbuild": "^2.0.0",
|
||||
"unplugin-icons": "^0.17.0",
|
||||
"vite-plugin-dts": "^3.6.4"
|
||||
},
|
||||
|
|
BIN
packages/@n8n/chat/resources/images/fullscreen.png
Normal file
BIN
packages/@n8n/chat/resources/images/fullscreen.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
packages/@n8n/chat/resources/images/windowed.png
Normal file
BIN
packages/@n8n/chat/resources/images/windowed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 115 KiB |
238
packages/@n8n/chat/resources/workflow-manual.json
Normal file
238
packages/@n8n/chat/resources/workflow-manual.json
Normal file
|
@ -0,0 +1,238 @@
|
|||
{
|
||||
"name": "Hosted n8n AI Chat Manual",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "e6043748-44fc-4019-9301-5690fe26c614",
|
||||
"name": "OpenAI Chat Model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
860,
|
||||
540
|
||||
],
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "cIIkOhl7tUX1KsL6",
|
||||
"name": "OpenAi account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"sessionKey": "={{ $json.sessionId }}"
|
||||
},
|
||||
"id": "0a68a59a-8ab6-4fa5-a1ea-b7f99a93109b",
|
||||
"name": "Window Buffer Memory",
|
||||
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
640,
|
||||
540
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"text": "={{ $json.chatInput }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "3d4e0fbf-d761-4569-b02e-f5c1eeb830c8",
|
||||
"name": "AI Agent",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 1.1,
|
||||
"position": [
|
||||
840,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"dataType": "string",
|
||||
"value1": "={{ $json.action }}",
|
||||
"rules": {
|
||||
"rules": [
|
||||
{
|
||||
"value2": "loadPreviousSession",
|
||||
"outputKey": "loadPreviousSession"
|
||||
},
|
||||
{
|
||||
"value2": "sendMessage",
|
||||
"outputKey": "sendMessage"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "84213c7b-abc7-4f40-9567-cd3484a4ae6b",
|
||||
"name": "Switch",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
300,
|
||||
280
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"simplifyOutput": false
|
||||
},
|
||||
"id": "3be7f076-98ed-472a-80b6-bf8d9538ac87",
|
||||
"name": "Chat Messages Retriever",
|
||||
"type": "@n8n/n8n-nodes-langchain.memoryChatRetriever",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
620,
|
||||
140
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "3417c644-8a91-4524-974a-45b4a46d0e2e",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1240,
|
||||
140
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"public": true,
|
||||
"authentication": "n8nUserAuth",
|
||||
"options": {
|
||||
"loadPreviousSession": "manually",
|
||||
"responseMode": "responseNode"
|
||||
}
|
||||
},
|
||||
"id": "1b30c239-a819-45b4-b0ae-bdd5b92a5424",
|
||||
"name": "Chat Trigger",
|
||||
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
80,
|
||||
280
|
||||
],
|
||||
"webhookId": "ed3dea26-7d68-42b3-9032-98fe967d441d"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"aggregate": "aggregateAllItemData",
|
||||
"options": {}
|
||||
},
|
||||
"id": "79672cf0-686b-41eb-90ae-fd31b6da837d",
|
||||
"name": "Aggregate",
|
||||
"type": "n8n-nodes-base.aggregate",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1000,
|
||||
140
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"OpenAI Chat Model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Window Buffer Memory": {
|
||||
"ai_memory": [
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "ai_memory",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Chat Messages Retriever",
|
||||
"type": "ai_memory",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Switch": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Chat Messages Retriever",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "AI Agent",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Chat Messages Retriever": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Aggregate",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"AI Agent": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Chat Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Switch",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Aggregate": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": true,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "425c0efe-3aa0-4e0e-8c06-abe12234b1fd",
|
||||
"id": "1569HF92Y02EUtsU",
|
||||
"meta": {
|
||||
"instanceId": "374b43d8b8d6299cc777811a4ad220fc688ee2d54a308cfb0de4450a5233ca9e"
|
||||
},
|
||||
"tags": []
|
||||
}
|
|
@ -1,245 +1,77 @@
|
|||
{
|
||||
"name": "AI Webhook Chat",
|
||||
"name": "Hosted n8n AI Chat",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "513107b3-6f3a-4a1e-af21-659f0ed14183",
|
||||
"responseMode": "responseNode",
|
||||
"options": {
|
||||
"domainAllowlist": "*.localhost"
|
||||
}
|
||||
},
|
||||
"id": "51ab2689-647d-4cff-9d6f-0ba4df45e904",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
900,
|
||||
200
|
||||
],
|
||||
"webhookId": "513107b3-6f3a-4a1e-af21-659f0ed14183"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "3c7fd563-f610-41fa-b198-7fcf100e2815",
|
||||
"name": "Chat OpenAI",
|
||||
"id": "4c109d13-62a2-4e23-9979-e50201db743d",
|
||||
"name": "OpenAI Chat Model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1720,
|
||||
620
|
||||
640,
|
||||
540
|
||||
],
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "B5Fiv70Adfg6htxn",
|
||||
"name": "Alex's OpenAI Account"
|
||||
"id": "cIIkOhl7tUX1KsL6",
|
||||
"name": "OpenAi account"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"sessionKey": "={{ $json.body.sessionId }}"
|
||||
"sessionKey": "={{ $json.sessionId }}"
|
||||
},
|
||||
"id": "ebc23ffa-3bcf-494f-bcb8-51a5fff91885",
|
||||
"id": "b416df7b-4802-462f-8f74-f0a71dc4c0be",
|
||||
"name": "Window Buffer Memory",
|
||||
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1920,
|
||||
620
|
||||
340,
|
||||
540
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"simplifyOutput": false
|
||||
},
|
||||
"id": "d6721a60-159b-4a93-ac6b-b81e16d9f16f",
|
||||
"name": "Memory Chat Retriever",
|
||||
"type": "@n8n/n8n-nodes-langchain.memoryChatRetriever",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1780,
|
||||
-40
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"sessionKey": "={{ $json.body.sessionId }}"
|
||||
},
|
||||
"id": "347edc3a-1dda-4996-b778-dcdc447ecfd8",
|
||||
"name": "Memory Chat Retriever Window Buffer Memory",
|
||||
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1800,
|
||||
160
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {
|
||||
"responseCode": 200,
|
||||
"responseHeaders": {
|
||||
"entries": [
|
||||
{
|
||||
"name": "sessionId",
|
||||
"value": "={{ $json.body.sessionId }}"
|
||||
},
|
||||
{
|
||||
"name": "Access-Control-Allow-Headers",
|
||||
"value": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "d229963e-e2f1-4381-87d2-47043bd6ccc7",
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
2460,
|
||||
220
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"dataType": "string",
|
||||
"value1": "={{ $json.body.action }}",
|
||||
"rules": {
|
||||
"rules": [
|
||||
{
|
||||
"value2": "loadPreviousSession"
|
||||
},
|
||||
{
|
||||
"value2": "sendMessage",
|
||||
"output": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "fc4ad994-5f38-4dce-b1e5-397acc512687",
|
||||
"name": "Chatbot Action",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1320,
|
||||
200
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const response = { data: [] };\n\nfor (const item of $input.all()) {\n response.data.push(item.json);\n}\n\nreturn {\n json: response,\n pairedItem: 0\n};"
|
||||
},
|
||||
"id": "e1a80bdc-411a-42df-88dd-36915b1ae8f4",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
2160,
|
||||
-40
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"text": "={{ $json.body.message }}",
|
||||
"text": "={{ $json.chatInput }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "f28f5c00-c742-41d5-8ddb-f0f59ab111a3",
|
||||
"name": "Agent",
|
||||
"id": "4de25807-a2ef-4453-900e-e00e0021ecdc",
|
||||
"name": "AI Agent",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 1,
|
||||
"typeVersion": 1.1,
|
||||
"position": [
|
||||
1780,
|
||||
340
|
||||
620,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.body = JSON.parse(item.json.body);\n}\n\nreturn $input.all();"
|
||||
"public": true,
|
||||
"options": {
|
||||
"loadPreviousSession": "memory"
|
||||
}
|
||||
},
|
||||
"id": "415c071b-18b2-4ac5-8634-e3d939bf36ac",
|
||||
"name": "Transform request body",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"id": "5a9612ae-51c1-4be2-bd8b-8556872d1149",
|
||||
"name": "Chat Trigger",
|
||||
"type": "@n8n/n8n-nodes-langchain.chatTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1120,
|
||||
200
|
||||
]
|
||||
340,
|
||||
300
|
||||
],
|
||||
"webhookId": "f406671e-c954-4691-b39a-66c90aa2f103"
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Transform request body",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Memory Chat Retriever": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Memory Chat Retriever Window Buffer Memory": {
|
||||
"ai_memory": [
|
||||
[
|
||||
{
|
||||
"node": "Memory Chat Retriever",
|
||||
"type": "ai_memory",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Chatbot Action": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Memory Chat Retriever",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Agent",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Chat OpenAI": {
|
||||
"OpenAI Chat Model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Agent",
|
||||
"node": "AI Agent",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
|
@ -250,29 +82,23 @@
|
|||
"ai_memory": [
|
||||
[
|
||||
{
|
||||
"node": "Agent",
|
||||
"node": "AI Agent",
|
||||
"type": "ai_memory",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Chat Trigger",
|
||||
"type": "ai_memory",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Agent": {
|
||||
"Chat Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Transform request body": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Chatbot Action",
|
||||
"node": "AI Agent",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
|
@ -284,8 +110,8 @@
|
|||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "12c145a2-74bf-48b5-a87a-ba707949eaed",
|
||||
"id": "L3FlJuFOxZcHtoFT",
|
||||
"versionId": "6076136f-fdb4-48d9-b483-d1c24c95ef9e",
|
||||
"id": "zaBHnDtj22BzEQ6K",
|
||||
"meta": {
|
||||
"instanceId": "374b43d8b8d6299cc777811a4ad220fc688ee2d54a308cfb0de4450a5233ca9e"
|
||||
},
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
const path = require('path');
|
||||
const shelljs = require('shelljs');
|
||||
const glob = require('fast-glob');
|
||||
|
||||
const rootDirPath = path.resolve(__dirname, '..');
|
||||
const n8nRootDirPath = path.resolve(rootDirPath, '..', '..', '..');
|
||||
const distDirPath = path.resolve(rootDirPath, 'dist');
|
||||
const srcDirPath = path.resolve(rootDirPath, 'src');
|
||||
const libDirPath = path.resolve(rootDirPath, 'tmp', 'lib');
|
||||
const cjsDirPath = path.resolve(rootDirPath, 'tmp', 'cjs');
|
||||
|
||||
const packageJsonFilePath = path.resolve(rootDirPath, 'package.json');
|
||||
const readmeFilePath = path.resolve(rootDirPath, 'README.md');
|
||||
|
@ -14,3 +18,19 @@ shelljs.cp(readmeFilePath, distDirPath);
|
|||
shelljs.cp(licenseFilePath, distDirPath);
|
||||
|
||||
shelljs.mv(path.resolve(distDirPath, 'src'), path.resolve(distDirPath, 'types'));
|
||||
|
||||
function moveFiles(files, from, to) {
|
||||
files.forEach((file) => {
|
||||
const toFile = file.replace(from, to);
|
||||
shelljs.mkdir('-p', path.dirname(toFile));
|
||||
shelljs.mv(file, toFile);
|
||||
});
|
||||
}
|
||||
|
||||
const cjsFiles = glob.sync(path.resolve(cjsDirPath, '**', '*'));
|
||||
moveFiles(cjsFiles, 'tmp/cjs', 'dist');
|
||||
shelljs.rm('-rf', cjsDirPath);
|
||||
|
||||
const libFiles = glob.sync(path.resolve(libDirPath, '**/*'));
|
||||
moveFiles(libFiles, 'tmp/lib', 'dist');
|
||||
shelljs.rm('-rf', libDirPath);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script lang="ts" setup>
|
||||
import { Chat, ChatWindow } from '@/components';
|
||||
import { Chat, ChatWindow } from '@n8n/chat/components';
|
||||
import { computed, onMounted } from 'vue';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import hljsXML from 'highlight.js/lib/languages/xml';
|
||||
import hljsJavascript from 'highlight.js/lib/languages/javascript';
|
||||
import { useOptions } from '@/composables';
|
||||
import { useOptions } from '@n8n/chat/composables';
|
||||
|
||||
defineProps({});
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import type { ChatOptions } from '@/types';
|
||||
import { createChat } from '@/index';
|
||||
import type { ChatOptions } from '@n8n/chat/types';
|
||||
import { createChat } from '@n8n/chat/index';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const webhookUrl = 'http://localhost:5678/webhook/513107b3-6f3a-4a1e-af21-659f0ed14183';
|
||||
const webhookUrl = 'http://localhost:5678/webhook/f406671e-c954-4691-b39a-66c90aa2f103/chat';
|
||||
|
||||
const meta = {
|
||||
title: 'Chat',
|
||||
|
|
|
@ -14,9 +14,8 @@ import {
|
|||
getChatWrapper,
|
||||
getGetStartedButton,
|
||||
getMountingTarget,
|
||||
} from '@/__tests__/utils';
|
||||
import { createChat } from '@/index';
|
||||
import { useChat } from '@/composables';
|
||||
} from '@n8n/chat/__tests__/utils';
|
||||
import { createChat } from '@n8n/chat/index';
|
||||
|
||||
describe('createChat()', () => {
|
||||
let app: ReturnType<typeof createChat>;
|
||||
|
@ -77,6 +76,7 @@ describe('createChat()', () => {
|
|||
|
||||
app = createChat({
|
||||
mode: 'fullscreen',
|
||||
showWelcomeScreen: true,
|
||||
});
|
||||
|
||||
const getStartedButton = getGetStartedButton();
|
||||
|
@ -85,7 +85,9 @@ describe('createChat()', () => {
|
|||
expect(fetchSpy.mock.calls[0][1]).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: {},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: expect.stringContaining('"action":"loadPreviousSession"') as unknown,
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
|
@ -112,9 +114,6 @@ describe('createChat()', () => {
|
|||
await fireEvent.click(trigger as HTMLElement);
|
||||
}
|
||||
|
||||
const getStartedButton = getGetStartedButton();
|
||||
await fireEvent.click(getStartedButton as HTMLElement);
|
||||
|
||||
expect(getChatMessages().length).toBe(initialMessages.length);
|
||||
expect(getChatMessageByText(initialMessages[0])).toBeInTheDocument();
|
||||
expect(getChatMessageByText(initialMessages[1])).toBeInTheDocument();
|
||||
|
@ -144,12 +143,10 @@ describe('createChat()', () => {
|
|||
}
|
||||
|
||||
expect(getChatMessageTyping()).not.toBeInTheDocument();
|
||||
|
||||
const getStartedButton = getGetStartedButton();
|
||||
await fireEvent.click(getStartedButton as HTMLElement);
|
||||
|
||||
expect(getChatMessages().length).toBe(2);
|
||||
|
||||
await waitFor(() => expect(getChatInputTextarea()).toBeInTheDocument());
|
||||
|
||||
const textarea = getChatInputTextarea();
|
||||
const sendButton = getChatInputSendButton();
|
||||
await fireEvent.update(textarea as HTMLElement, input);
|
||||
|
@ -159,7 +156,9 @@ describe('createChat()', () => {
|
|||
expect(fetchSpy.mock.calls[1][1]).toEqual(
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
headers: {},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: expect.stringMatching(/"action":"sendMessage"/) as unknown,
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
|
@ -182,9 +181,6 @@ describe('createChat()', () => {
|
|||
const input = 'Teach me javascript!';
|
||||
const output = '# Code\n```js\nconsole.log("Hello World!");\n```';
|
||||
|
||||
const chatStore = useChat();
|
||||
console.log(chatStore);
|
||||
|
||||
const fetchSpy = vi.spyOn(window, 'fetch');
|
||||
fetchSpy
|
||||
.mockImplementationOnce(createFetchResponse(createGetLatestMessagesResponse))
|
||||
|
@ -199,8 +195,7 @@ describe('createChat()', () => {
|
|||
await fireEvent.click(trigger as HTMLElement);
|
||||
}
|
||||
|
||||
const getStartedButton = getGetStartedButton();
|
||||
await fireEvent.click(getStartedButton as HTMLElement);
|
||||
await waitFor(() => expect(getChatInputTextarea()).toBeInTheDocument());
|
||||
|
||||
const textarea = getChatInputTextarea();
|
||||
const sendButton = getChatInputSendButton();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { createChat } from '@/index';
|
||||
import { createChat } from '@n8n/chat/index';
|
||||
|
||||
export function createTestChat(options: Parameters<typeof createChat>[0] = {}): {
|
||||
unmount: () => void;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { LoadPreviousSessionResponse, SendMessageResponse } from '@/types';
|
||||
import type { LoadPreviousSessionResponse, SendMessageResponse } from '@n8n/chat/types';
|
||||
|
||||
export function createFetchResponse<T>(data: T) {
|
||||
return async () =>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { screen } from '@testing-library/vue';
|
||||
import { defaultMountingTarget } from '@/constants';
|
||||
import { defaultMountingTarget } from '@n8n/chat/constants';
|
||||
|
||||
export function getMountingTarget(target = defaultMountingTarget) {
|
||||
return document.querySelector(target);
|
||||
|
|
|
@ -10,6 +10,7 @@ export async function authenticatedFetch<T>(...args: Parameters<typeof fetch>):
|
|||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
|
||||
...args[1]?.headers,
|
||||
},
|
||||
|
@ -18,14 +19,12 @@ export async function authenticatedFetch<T>(...args: Parameters<typeof fetch>):
|
|||
return (await response.json()) as Promise<T>;
|
||||
}
|
||||
|
||||
export async function get<T>(
|
||||
url: string,
|
||||
query: Record<string, string> = {},
|
||||
options: RequestInit = {},
|
||||
) {
|
||||
export async function get<T>(url: string, query: object = {}, options: RequestInit = {}) {
|
||||
let resolvedUrl = url;
|
||||
if (Object.keys(query).length > 0) {
|
||||
resolvedUrl = `${resolvedUrl}?${new URLSearchParams(query).toString()}`;
|
||||
resolvedUrl = `${resolvedUrl}?${new URLSearchParams(
|
||||
query as Record<string, string>,
|
||||
).toString()}`;
|
||||
}
|
||||
|
||||
return authenticatedFetch<T>(resolvedUrl, { ...options, method: 'GET' });
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { get, post } from '@/api/generic';
|
||||
import type { ChatOptions, LoadPreviousSessionResponse, SendMessageResponse } from '@/types';
|
||||
import { get, post } from '@n8n/chat/api/generic';
|
||||
import type {
|
||||
ChatOptions,
|
||||
LoadPreviousSessionResponse,
|
||||
SendMessageResponse,
|
||||
} from '@n8n/chat/types';
|
||||
|
||||
export async function loadPreviousSession(sessionId: string, options: ChatOptions) {
|
||||
const method = options.webhookConfig?.method === 'POST' ? post : get;
|
||||
|
@ -7,7 +11,8 @@ export async function loadPreviousSession(sessionId: string, options: ChatOption
|
|||
`${options.webhookUrl}`,
|
||||
{
|
||||
action: 'loadPreviousSession',
|
||||
sessionId,
|
||||
[options.chatSessionKey as string]: sessionId,
|
||||
...(options.metadata ? { metadata: options.metadata } : {}),
|
||||
},
|
||||
{
|
||||
headers: options.webhookConfig?.headers,
|
||||
|
@ -21,8 +26,9 @@ export async function sendMessage(message: string, sessionId: string, options: C
|
|||
`${options.webhookUrl}`,
|
||||
{
|
||||
action: 'sendMessage',
|
||||
sessionId,
|
||||
message,
|
||||
[options.chatSessionKey as string]: sessionId,
|
||||
[options.chatInputKey as string]: message,
|
||||
...(options.metadata ? { metadata: options.metadata } : {}),
|
||||
},
|
||||
{
|
||||
headers: options.webhookConfig?.headers,
|
||||
|
|
|
@ -1,24 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import Layout from '@/components/Layout.vue';
|
||||
import GetStarted from '@/components/GetStarted.vue';
|
||||
import GetStartedFooter from '@/components/GetStartedFooter.vue';
|
||||
import MessagesList from '@/components/MessagesList.vue';
|
||||
import Input from '@/components/Input.vue';
|
||||
import Layout from '@n8n/chat/components/Layout.vue';
|
||||
import GetStarted from '@n8n/chat/components/GetStarted.vue';
|
||||
import GetStartedFooter from '@n8n/chat/components/GetStartedFooter.vue';
|
||||
import MessagesList from '@n8n/chat/components/MessagesList.vue';
|
||||
import Input from '@n8n/chat/components/Input.vue';
|
||||
import { nextTick, onMounted } from 'vue';
|
||||
import { useI18n, useChat } from '@/composables';
|
||||
import { chatEventBus } from '@/event-buses';
|
||||
import { useI18n, useChat, useOptions } from '@n8n/chat/composables';
|
||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
|
||||
const { t } = useI18n();
|
||||
const chatStore = useChat();
|
||||
|
||||
const { messages, currentSessionId } = chatStore;
|
||||
|
||||
async function initialize() {
|
||||
await chatStore.loadPreviousSession();
|
||||
void nextTick(() => {
|
||||
chatEventBus.emit('scrollToBottom');
|
||||
});
|
||||
}
|
||||
const { options } = useOptions();
|
||||
|
||||
async function getStarted() {
|
||||
void chatStore.startNewSession();
|
||||
|
@ -27,18 +21,28 @@ async function getStarted() {
|
|||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void initialize();
|
||||
async function initialize() {
|
||||
await chatStore.loadPreviousSession();
|
||||
void nextTick(() => {
|
||||
chatEventBus.emit('scrollToBottom');
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initialize();
|
||||
if (!options.showWelcomeScreen && !currentSessionId.value) {
|
||||
await getStarted();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout class="chat-wrapper">
|
||||
<template #header v-if="!currentSessionId">
|
||||
<template #header>
|
||||
<h1>{{ t('title') }}</h1>
|
||||
<p>{{ t('subtitle') }}</p>
|
||||
</template>
|
||||
<GetStarted v-if="!currentSessionId" @click:button="getStarted" />
|
||||
<GetStarted v-if="!currentSessionId && options.showWelcomeScreen" @click:button="getStarted" />
|
||||
<MessagesList v-else :messages="messages" />
|
||||
<template #footer>
|
||||
<Input v-if="currentSessionId" />
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
import IconChat from 'virtual:icons/mdi/chat';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import IconChevronDown from 'virtual:icons/mdi/chevron-down';
|
||||
import Chat from '@/components/Chat.vue';
|
||||
import Chat from '@n8n/chat/components/Chat.vue';
|
||||
import { nextTick, ref } from 'vue';
|
||||
import { chatEventBus } from '@/event-buses';
|
||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
|
||||
const isOpen = ref(false);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import Button from '@/components/Button.vue';
|
||||
import { useI18n } from '@/composables';
|
||||
import Button from '@n8n/chat/components/Button.vue';
|
||||
import { useI18n } from '@n8n/chat/composables';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables';
|
||||
import PoweredBy from '@/components/PoweredBy.vue';
|
||||
import { useI18n } from '@n8n/chat/composables';
|
||||
import PoweredBy from '@n8n/chat/components/PoweredBy.vue';
|
||||
|
||||
const { t, te } = useI18n();
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import IconSend from 'virtual:icons/mdi/send';
|
||||
import { useI18n, useChat } from '@/composables';
|
||||
import { useI18n, useChat } from '@n8n/chat/composables';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const chatStore = useChat();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { chatEventBus } from '@/event-buses';
|
||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
|
||||
const chatBodyRef = ref<HTMLElement | null>(null);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import type { ChatMessage } from '@/types';
|
||||
import type { ChatMessage } from '@n8n/chat/types';
|
||||
import type { PropType } from 'vue';
|
||||
import { computed, toRefs } from 'vue';
|
||||
import VueMarkdown from 'vue-markdown-render';
|
||||
|
@ -15,6 +15,10 @@ const props = defineProps({
|
|||
|
||||
const { message } = toRefs(props);
|
||||
|
||||
const messageText = computed(() => {
|
||||
return message.value.text || '<Empty response>';
|
||||
});
|
||||
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
'chat-message-from-user': message.value.sender === 'user',
|
||||
|
@ -39,7 +43,7 @@ const markdownOptions = {
|
|||
<slot>
|
||||
<vue-markdown
|
||||
class="chat-message-markdown"
|
||||
:source="message.text"
|
||||
:source="messageText"
|
||||
:options="markdownOptions"
|
||||
/>
|
||||
</slot>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import type { ChatMessage } from '@/types';
|
||||
import type { ChatMessage } from '@n8n/chat/types';
|
||||
import { Message } from './index';
|
||||
import type { PropType } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue';
|
||||
import Message from '@/components/Message.vue';
|
||||
import type { ChatMessage } from '@/types';
|
||||
import MessageTyping from '@/components/MessageTyping.vue';
|
||||
import { useChat } from '@/composables';
|
||||
import Message from '@n8n/chat/components/Message.vue';
|
||||
import type { ChatMessage } from '@n8n/chat/types';
|
||||
import MessageTyping from '@n8n/chat/components/MessageTyping.vue';
|
||||
import { useChat } from '@n8n/chat/composables';
|
||||
|
||||
defineProps({
|
||||
messages: {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { inject } from 'vue';
|
||||
import { ChatSymbol } from '@/constants';
|
||||
import type { Chat } from '@/types';
|
||||
import { ChatSymbol } from '@n8n/chat/constants';
|
||||
import type { Chat } from '@n8n/chat/types';
|
||||
|
||||
export function useChat() {
|
||||
return inject(ChatSymbol) as Chat;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useOptions } from '@/composables/useOptions';
|
||||
import { useOptions } from '@n8n/chat/composables/useOptions';
|
||||
|
||||
export function useI18n() {
|
||||
const { options } = useOptions();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { inject } from 'vue';
|
||||
import { ChatOptionsSymbol } from '@/constants';
|
||||
import type { ChatOptions } from '@/types';
|
||||
import { ChatOptionsSymbol } from '@n8n/chat/constants';
|
||||
import type { ChatOptions } from '@n8n/chat/types';
|
||||
|
||||
export function useOptions() {
|
||||
const options = inject(ChatOptionsSymbol) as ChatOptions;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { ChatOptions } from '@/types';
|
||||
import type { ChatOptions } from '@n8n/chat/types';
|
||||
|
||||
export const defaultOptions: ChatOptions = {
|
||||
webhookUrl: 'http://localhost:5678',
|
||||
|
@ -8,7 +8,11 @@ export const defaultOptions: ChatOptions = {
|
|||
},
|
||||
target: '#n8n-chat',
|
||||
mode: 'window',
|
||||
loadPreviousSession: true,
|
||||
chatInputKey: 'chatInput',
|
||||
chatSessionKey: 'sessionId',
|
||||
defaultLanguage: 'en',
|
||||
showWelcomeScreen: false,
|
||||
initialMessages: ['Hi there! 👋', 'My name is Nathan. How can I assist you today?'],
|
||||
i18n: {
|
||||
en: {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { InjectionKey } from 'vue';
|
||||
import type { Chat, ChatOptions } from '@/types';
|
||||
import type { Chat, ChatOptions } from '@n8n/chat/types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const ChatSymbol = 'Chat' as unknown as InjectionKey<Chat>;
|
||||
|
|
36
packages/@n8n/chat/src/css/_tokens.scss
Normal file
36
packages/@n8n/chat/src/css/_tokens.scss
Normal file
|
@ -0,0 +1,36 @@
|
|||
:root {
|
||||
--chat--color-primary: #e74266;
|
||||
--chat--color-primary-shade-50: #db4061;
|
||||
--chat--color-primary-shade-100: #cf3c5c;
|
||||
--chat--color-secondary: #20b69e;
|
||||
--chat--color-secondary-shade-50: #1ca08a;
|
||||
--chat--color-white: #ffffff;
|
||||
--chat--color-light: #f2f4f8;
|
||||
--chat--color-light-shade-50: #e6e9f1;
|
||||
--chat--color-light-shade-100: #c2c5cc;
|
||||
--chat--color-medium: #d2d4d9;
|
||||
--chat--color-dark: #101330;
|
||||
--chat--color-disabled: #777980;
|
||||
--chat--color-typing: #404040;
|
||||
|
||||
--chat--spacing: 1rem;
|
||||
--chat--border-radius: 0.25rem;
|
||||
--chat--transition-duration: 0.15s;
|
||||
|
||||
--chat--window--width: 400px;
|
||||
--chat--window--height: 600px;
|
||||
|
||||
--chat--textarea--height: 50px;
|
||||
|
||||
--chat--message--bot--background: var(--chat--color-white);
|
||||
--chat--message--bot--color: var(--chat--color-dark);
|
||||
--chat--message--user--background: var(--chat--color-secondary);
|
||||
--chat--message--user--color: var(--chat--color-white);
|
||||
--chat--message--pre--background: rgba(0, 0, 0, 0.05);
|
||||
|
||||
--chat--toggle--background: var(--chat--color-primary);
|
||||
--chat--toggle--hover--background: var(--chat--color-primary-shade-50);
|
||||
--chat--toggle--active--background: var(--chat--color-primary-shade-100);
|
||||
--chat--toggle--color: var(--chat--color-white);
|
||||
--chat--toggle--size: 64px;
|
||||
}
|
1
packages/@n8n/chat/src/css/index.scss
Normal file
1
packages/@n8n/chat/src/css/index.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@import 'tokens';
|
|
@ -1,3 +1,3 @@
|
|||
import { createEventBus } from '@/utils';
|
||||
import { createEventBus } from '@n8n/chat/utils';
|
||||
|
||||
export const chatEventBus = createEventBus();
|
||||
|
|
|
@ -2,10 +2,10 @@ import './main.scss';
|
|||
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import type { ChatOptions } from '@/types';
|
||||
import { defaultMountingTarget, defaultOptions } from '@/constants';
|
||||
import { createDefaultMountingTarget } from '@/utils';
|
||||
import { ChatPlugin } from '@/plugins';
|
||||
import type { ChatOptions } from '@n8n/chat/types';
|
||||
import { defaultMountingTarget, defaultOptions } from '@n8n/chat/constants';
|
||||
import { createDefaultMountingTarget } from '@n8n/chat/utils';
|
||||
import { ChatPlugin } from '@n8n/chat/plugins';
|
||||
|
||||
export function createChat(options?: Partial<ChatOptions>) {
|
||||
const resolvedOptions: ChatOptions = {
|
||||
|
|
|
@ -2,39 +2,4 @@
|
|||
@import 'highlight.js/styles/github';
|
||||
}
|
||||
|
||||
:root {
|
||||
--chat--color-primary: #e74266;
|
||||
--chat--color-primary-shade-50: #db4061;
|
||||
--chat--color-primary-shade-100: #cf3c5c;
|
||||
--chat--color-secondary: #20b69e;
|
||||
--chat--color-secondary-shade-50: #1ca08a;
|
||||
--chat--color-white: #ffffff;
|
||||
--chat--color-light: #f2f4f8;
|
||||
--chat--color-light-shade-50: #e6e9f1;
|
||||
--chat--color-light-shade-100: #c2c5cc;
|
||||
--chat--color-medium: #d2d4d9;
|
||||
--chat--color-dark: #101330;
|
||||
--chat--color-disabled: #777980;
|
||||
--chat--color-typing: #404040;
|
||||
|
||||
--chat--spacing: 1rem;
|
||||
--chat--border-radius: 0.25rem;
|
||||
--chat--transition-duration: 0.15s;
|
||||
|
||||
--chat--window--width: 400px;
|
||||
--chat--window--height: 600px;
|
||||
|
||||
--chat--textarea--height: 50px;
|
||||
|
||||
--chat--message--bot--background: var(--chat--color-white);
|
||||
--chat--message--bot--color: var(--chat--color-dark);
|
||||
--chat--message--user--background: var(--chat--color-secondary);
|
||||
--chat--message--user--color: var(--chat--color-white);
|
||||
--chat--message--pre--background: rgba(0, 0, 0, 0.05);
|
||||
|
||||
--chat--toggle--background: var(--chat--color-primary);
|
||||
--chat--toggle--hover--background: var(--chat--color-primary-shade-50);
|
||||
--chat--toggle--active--background: var(--chat--color-primary-shade-100);
|
||||
--chat--toggle--color: var(--chat--color-white);
|
||||
--chat--toggle--size: 64px;
|
||||
}
|
||||
@import 'css';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import type { Plugin } from 'vue';
|
||||
import { computed, nextTick, ref } from 'vue';
|
||||
import type { ChatMessage, ChatOptions } from '@/types';
|
||||
import type { ChatMessage, ChatOptions } from '@n8n/chat/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { chatEventBus } from '@/event-buses';
|
||||
import * as api from '@/api';
|
||||
import { ChatOptionsSymbol, ChatSymbol, localStorageSessionIdKey } from '@/constants';
|
||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||
import * as api from '@n8n/chat/api';
|
||||
import { ChatOptionsSymbol, ChatSymbol, localStorageSessionIdKey } from '@n8n/chat/constants';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const ChatPlugin: Plugin<ChatOptions> = {
|
||||
|
@ -61,6 +61,10 @@ export const ChatPlugin: Plugin<ChatOptions> = {
|
|||
}
|
||||
|
||||
async function loadPreviousSession() {
|
||||
if (!options.loadPreviousSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = localStorage.getItem(localStorageSessionIdKey) ?? uuidv4();
|
||||
const previousMessagesResponse = await api.loadPreviousSession(sessionId, options);
|
||||
const timestamp = new Date().toISOString();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { ChatMessage } from '@/types/messages';
|
||||
import type { ChatMessage } from '@n8n/chat/types/messages';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
export interface Chat {
|
||||
|
@ -6,7 +6,7 @@ export interface Chat {
|
|||
messages: Ref<ChatMessage[]>;
|
||||
currentSessionId: Ref<string | null>;
|
||||
waitingForResponse: Ref<boolean>;
|
||||
loadPreviousSession: () => Promise<string>;
|
||||
loadPreviousSession: () => Promise<string | undefined>;
|
||||
startNewSession: () => Promise<void>;
|
||||
sendMessage: (text: string) => Promise<void>;
|
||||
}
|
||||
|
|
|
@ -6,8 +6,13 @@ export interface ChatOptions {
|
|||
};
|
||||
target?: string | Element;
|
||||
mode?: 'window' | 'fullscreen';
|
||||
showWelcomeScreen?: boolean;
|
||||
loadPreviousSession?: boolean;
|
||||
chatInputKey?: string;
|
||||
chatSessionKey?: string;
|
||||
defaultLanguage?: 'en';
|
||||
initialMessages?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
i18n: Record<
|
||||
string,
|
||||
{
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
"types": ["vitest/globals", "unplugin-icons/types/vue"],
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"n8n-design-system/*": ["../design-system/src/*"]
|
||||
"@n8n/chat/*": ["src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
|
||||
// TODO: remove all options below this line
|
||||
|
|
|
@ -7,6 +7,7 @@ import icons from 'unplugin-icons/vite';
|
|||
import dts from 'vite-plugin-dts';
|
||||
|
||||
const includeVue = process.env.INCLUDE_VUE === 'true';
|
||||
const srcPath = fileURLToPath(new URL('./src', import.meta.url));
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
@ -19,7 +20,8 @@ export default defineConfig({
|
|||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@': srcPath,
|
||||
'@n8n/chat': srcPath,
|
||||
},
|
||||
},
|
||||
define: {
|
||||
|
|
|
@ -144,7 +144,7 @@ export class Agent implements INodeType {
|
|||
name: 'agent',
|
||||
icon: 'fa:robot',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1],
|
||||
version: [1, 1.1, 1.2],
|
||||
description: 'Generates an action plan and executes it. Can use external tools.',
|
||||
subtitle:
|
||||
"={{ { conversationalAgent: 'Conversational Agent', openAiFunctionsAgent: 'OpenAI Functions Agent', reactAgent: 'ReAct Agent', sqlAgent: 'SQL Agent' }[$parameter.agent] }}",
|
||||
|
|
|
@ -28,6 +28,19 @@ export const conversationalAgentProperties: INodeProperties[] = [
|
|||
},
|
||||
default: '={{ $json.chat_input }}',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: ['conversationalAgent'],
|
||||
'@version': [1.2],
|
||||
},
|
||||
},
|
||||
default: '={{ $json.chatInput }}',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
|
|
|
@ -28,6 +28,19 @@ export const openAiFunctionsAgentProperties: INodeProperties[] = [
|
|||
},
|
||||
default: '={{ $json.chat_input }}',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: ['openAiFunctionsAgent'],
|
||||
'@version': [1.2],
|
||||
},
|
||||
},
|
||||
default: '={{ $json.chatInput }}',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
|
|
|
@ -28,6 +28,19 @@ export const planAndExecuteAgentProperties: INodeProperties[] = [
|
|||
},
|
||||
default: '={{ $json.chat_input }}',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: ['planAndExecuteAgent'],
|
||||
'@version': [1.2],
|
||||
},
|
||||
},
|
||||
default: '={{ $json.chatInput } }',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
|
|
|
@ -28,6 +28,19 @@ export const reActAgentAgentProperties: INodeProperties[] = [
|
|||
},
|
||||
default: '={{ $json.chat_input }}',
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
agent: ['reActAgent'],
|
||||
'@version': [1.2],
|
||||
},
|
||||
},
|
||||
default: '={{ $json.chatInput }}',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
|
|
|
@ -224,6 +224,23 @@ export class OpenAiAssistant implements INodeType {
|
|||
type: 'string',
|
||||
required: true,
|
||||
default: '={{ $json.chat_input }}',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Text',
|
||||
name: 'text',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '={{ $json.chatInput }}',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'OpenAI Tools',
|
||||
|
|
|
@ -217,7 +217,7 @@ export class ChainLlm implements INodeType {
|
|||
name: 'chainLlm',
|
||||
icon: 'fa:link',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2],
|
||||
version: [1, 1.1, 1.2, 1.3],
|
||||
description: 'A simple chain to prompt a large language model',
|
||||
defaults: {
|
||||
name: 'Basic LLM Chain',
|
||||
|
@ -266,6 +266,18 @@ export class ChainLlm implements INodeType {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
name: 'prompt',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '={{ $json.chatInput }}',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.3],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Chat Messages (if Using a Chat Model)',
|
||||
name: 'messages',
|
||||
|
|
|
@ -18,7 +18,7 @@ export class ChainRetrievalQa implements INodeType {
|
|||
name: 'chainRetrievalQa',
|
||||
icon: 'fa:link',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1],
|
||||
version: [1, 1.1, 1.2],
|
||||
description: 'Answer questions about retrieved documents',
|
||||
defaults: {
|
||||
name: 'Question and Answer Chain',
|
||||
|
@ -82,6 +82,18 @@ export class ChainRetrievalQa implements INodeType {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '={{ $json.chatInput }}',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.2],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ export class MemoryBufferWindow implements INodeType {
|
|||
name: 'memoryBufferWindow',
|
||||
icon: 'fa:database',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
version: [1, 1.1],
|
||||
description: 'Stores in n8n memory, so no credentials required',
|
||||
defaults: {
|
||||
name: 'Window Buffer Memory',
|
||||
|
@ -101,6 +101,23 @@ export class MemoryBufferWindow implements INodeType {
|
|||
type: 'string',
|
||||
default: 'chat_history',
|
||||
description: 'The key to use to store the memory in the workflow data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionKey',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
description: 'The key to use to store the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Context Window Length',
|
||||
|
|
|
@ -17,7 +17,7 @@ export class MemoryMotorhead implements INodeType {
|
|||
name: 'memoryMotorhead',
|
||||
icon: 'fa:file-export',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
version: [1, 1.1],
|
||||
description: 'Use Motorhead Memory',
|
||||
defaults: {
|
||||
name: 'Motorhead',
|
||||
|
@ -54,6 +54,23 @@ export class MemoryMotorhead implements INodeType {
|
|||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionId',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
description: 'The key to use to store the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ export class MemoryRedisChat implements INodeType {
|
|||
name: 'memoryRedisChat',
|
||||
icon: 'file:redis.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
version: [1, 1.1],
|
||||
description: 'Stores the chat history in Redis.',
|
||||
defaults: {
|
||||
name: 'Redis Chat Memory',
|
||||
|
@ -58,6 +58,23 @@ export class MemoryRedisChat implements INodeType {
|
|||
type: 'string',
|
||||
default: 'chat_history',
|
||||
description: 'The key to use to store the memory in the workflow data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionKey',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
description: 'The key to use to store the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session Time To Live',
|
||||
|
|
|
@ -12,7 +12,7 @@ export class MemoryXata implements INodeType {
|
|||
name: 'memoryXata',
|
||||
icon: 'file:xata.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
version: [1, 1.1],
|
||||
description: 'Use Xata Memory',
|
||||
defaults: {
|
||||
name: 'Xata',
|
||||
|
@ -51,6 +51,23 @@ export class MemoryXata implements INodeType {
|
|||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionId',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
description: 'The key to use to store the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ export class MemoryZep implements INodeType {
|
|||
// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
|
||||
icon: 'file:zep.png',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
version: [1, 1.1],
|
||||
description: 'Use Zep Memory',
|
||||
defaults: {
|
||||
name: 'Zep',
|
||||
|
@ -54,6 +54,23 @@ export class MemoryZep implements INodeType {
|
|||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Session ID',
|
||||
name: 'sessionId',
|
||||
type: 'string',
|
||||
default: '={{ $json.sessionId }}',
|
||||
description: 'The key to use to store the memory',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [1.1],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -0,0 +1,428 @@
|
|||
import {
|
||||
type IDataObject,
|
||||
type IWebhookFunctions,
|
||||
type IWebhookResponseData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
NodeConnectionType,
|
||||
} from 'n8n-workflow';
|
||||
import { pick } from 'lodash';
|
||||
import type { BaseChatMemory } from 'langchain/memory';
|
||||
import { createPage } from './templates';
|
||||
import { validateAuth } from './GenericFunctions';
|
||||
import type { LoadPreviousSessionChatOption } from './types';
|
||||
|
||||
const CHAT_TRIGGER_PATH_IDENTIFIER = 'chat';
|
||||
|
||||
export class ChatTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Chat Trigger',
|
||||
name: 'chatTrigger',
|
||||
icon: 'fa:comments',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Runs the workflow when an n8n generated webchat is submitted',
|
||||
defaults: {
|
||||
name: 'Chat Trigger',
|
||||
},
|
||||
codex: {
|
||||
categories: ['Core Nodes'],
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
url: 'https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-langchain.chattrigger/',
|
||||
},
|
||||
],
|
||||
},
|
||||
subcategories: {
|
||||
'Core Nodes': ['Other Trigger Nodes'],
|
||||
},
|
||||
},
|
||||
supportsCORS: true,
|
||||
maxNodes: 1,
|
||||
inputs: `={{ (() => {
|
||||
if (!['hostedChat', 'webhook'].includes($parameter.mode)) {
|
||||
return [];
|
||||
}
|
||||
if ($parameter.options?.loadPreviousSession !== 'memory') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
displayName: 'Memory',
|
||||
maxConnections: 1,
|
||||
type: '${NodeConnectionType.AiMemory}',
|
||||
required: true,
|
||||
}
|
||||
];
|
||||
})() }}`,
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-credentials-name-unsuffixed
|
||||
name: 'httpBasicAuth',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['basicAuth'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'setup',
|
||||
httpMethod: 'GET',
|
||||
responseMode: 'onReceived',
|
||||
path: CHAT_TRIGGER_PATH_IDENTIFIER,
|
||||
ndvHideUrl: true,
|
||||
},
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: '={{$parameter.options?.["responseMode"] || "lastNode" }}',
|
||||
path: CHAT_TRIGGER_PATH_IDENTIFIER,
|
||||
ndvHideMethod: true,
|
||||
ndvHideUrl: '={{ !$parameter.public }}',
|
||||
},
|
||||
],
|
||||
eventTriggerDescription: 'Waiting for you to submit the chat',
|
||||
activationMessage: 'You can now make calls to your production chat URL.',
|
||||
triggerPanel: false,
|
||||
properties: [
|
||||
/**
|
||||
* @note If we change this property, also update it in ChatEmbedModal.vue
|
||||
*/
|
||||
{
|
||||
displayName: 'Make Chat Publicly Available',
|
||||
name: 'public',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether the chat should be publicly available or only accessible through the manual chat interface',
|
||||
},
|
||||
{
|
||||
displayName: 'Mode',
|
||||
name: 'mode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Hosted Chat',
|
||||
value: 'hostedChat',
|
||||
description: 'Chat on a page served by n8n',
|
||||
},
|
||||
{
|
||||
name: 'Embedded Chat',
|
||||
value: 'webhook',
|
||||
description: 'Chat through a widget embedded in another page, or by calling a webhook',
|
||||
},
|
||||
],
|
||||
default: 'hostedChat',
|
||||
displayOptions: {
|
||||
show: {
|
||||
public: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Chat will be live at the URL above once you activate this workflow. Live executions will show up in the ‘executions’ tab',
|
||||
name: 'hostedChatNotice',
|
||||
type: 'notice',
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: ['hostedChat'],
|
||||
public: [true],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'Follow the instructions <a href="https://www.npmjs.com/package/@n8n/chat" target="_blank">here</a> to embed chat in a webpage (or just call the webhook URL at the top of this section). Chat will be live once you activate this workflow',
|
||||
name: 'embeddedChatNotice',
|
||||
type: 'notice',
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: ['webhook'],
|
||||
public: [true],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
public: [true],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Basic Auth',
|
||||
value: 'basicAuth',
|
||||
description: 'Simple username and password (the same one for all users)',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
name: 'n8n User Auth',
|
||||
value: 'n8nUserAuth',
|
||||
description: 'Require user to be logged in with their n8n account',
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none',
|
||||
},
|
||||
],
|
||||
default: 'none',
|
||||
description: 'The way to authenticate',
|
||||
},
|
||||
{
|
||||
displayName: 'Initial Message(s)',
|
||||
name: 'initialMessages',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: ['hostedChat'],
|
||||
public: [true],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
rows: 3,
|
||||
},
|
||||
default: 'Hi there! 👋\nMy name is Nathan. How can I assist you today?',
|
||||
description: 'Default messages shown at the start of the chat, one per line',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
mode: ['hostedChat', 'webhook'],
|
||||
public: [true],
|
||||
},
|
||||
},
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Input Placeholder',
|
||||
name: 'inputPlaceholder',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/mode': ['hostedChat'],
|
||||
},
|
||||
},
|
||||
default: 'Type your question..',
|
||||
placeholder: 'e.g. Type your message here',
|
||||
description: 'Shown as placeholder text in the chat input field',
|
||||
},
|
||||
{
|
||||
displayName: 'Load Previous Session',
|
||||
name: 'loadPreviousSession',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Off',
|
||||
value: 'notSupported',
|
||||
description: 'Loading messages of previous session is turned off',
|
||||
},
|
||||
{
|
||||
name: 'From Memory',
|
||||
value: 'memory',
|
||||
description: 'Load session messages from memory',
|
||||
},
|
||||
{
|
||||
name: 'Manually',
|
||||
value: 'manually',
|
||||
description: 'Manually return messages of session',
|
||||
},
|
||||
],
|
||||
default: 'notSupported',
|
||||
description: 'If loading messages of a previous session should be enabled',
|
||||
},
|
||||
{
|
||||
displayName: 'Response Mode',
|
||||
name: 'responseMode',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'When Last Node Finishes',
|
||||
value: 'lastNode',
|
||||
description: 'Returns data of the last-executed node',
|
||||
},
|
||||
{
|
||||
name: "Using 'Respond to Webhook' Node",
|
||||
value: 'responseNode',
|
||||
description: 'Response defined in that node',
|
||||
},
|
||||
],
|
||||
default: 'lastNode',
|
||||
description: 'When and how to respond to the webhook',
|
||||
},
|
||||
{
|
||||
displayName: 'Require Button Click to Start Chat',
|
||||
name: 'showWelcomeScreen',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/mode': ['hostedChat'],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'Whether to show the welcome screen at the start of the chat',
|
||||
},
|
||||
{
|
||||
displayName: 'Start Conversation Button Text',
|
||||
name: 'getStarted',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
showWelcomeScreen: [true],
|
||||
'/mode': ['hostedChat'],
|
||||
},
|
||||
},
|
||||
default: 'New Conversation',
|
||||
placeholder: 'e.g. New Conversation',
|
||||
description: 'Shown as part of the welcome screen, in the middle of the chat window',
|
||||
},
|
||||
{
|
||||
displayName: 'Subtitle',
|
||||
name: 'subtitle',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/mode': ['hostedChat'],
|
||||
},
|
||||
},
|
||||
default: "Start a chat. We're here to help you 24/7.",
|
||||
placeholder: "e.g. We're here for you",
|
||||
description: 'Shown at the top of the chat, under the title',
|
||||
},
|
||||
{
|
||||
displayName: 'Title',
|
||||
name: 'title',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/mode': ['hostedChat'],
|
||||
},
|
||||
},
|
||||
default: 'Hi there! 👋',
|
||||
placeholder: 'e.g. Welcome',
|
||||
description: 'Shown at the top of the chat',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const res = this.getResponseObject();
|
||||
|
||||
const isPublic = this.getNodeParameter('public', false) as boolean;
|
||||
const nodeMode = this.getNodeParameter('mode', 'hostedChat') as string;
|
||||
if (!isPublic) {
|
||||
res.status(404).end();
|
||||
return {
|
||||
noWebhookResponse: true,
|
||||
};
|
||||
}
|
||||
|
||||
const webhookName = this.getWebhookName();
|
||||
const mode = this.getMode() === 'manual' ? 'test' : 'production';
|
||||
const bodyData = this.getBodyData() ?? {};
|
||||
|
||||
const options = this.getNodeParameter('options', {}) as {
|
||||
getStarted?: string;
|
||||
inputPlaceholder?: string;
|
||||
loadPreviousSession?: LoadPreviousSessionChatOption;
|
||||
showWelcomeScreen?: boolean;
|
||||
subtitle?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
if (nodeMode === 'hostedChat') {
|
||||
try {
|
||||
await validateAuth(this);
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
res.writeHead(error.responseCode as number, {
|
||||
'www-authenticate': 'Basic realm="Webhook"',
|
||||
});
|
||||
res.end(error.message as string);
|
||||
return { noWebhookResponse: true };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Show the chat on GET request
|
||||
if (webhookName === 'setup') {
|
||||
const webhookUrlRaw = this.getNodeWebhookUrl('default') as string;
|
||||
const webhookUrl =
|
||||
mode === 'test' ? webhookUrlRaw.replace('/webhook', '/webhook-test') : webhookUrlRaw;
|
||||
const authentication = this.getNodeParameter('authentication') as
|
||||
| 'none'
|
||||
| 'basicAuth'
|
||||
| 'n8nUserAuth';
|
||||
const initialMessagesRaw = this.getNodeParameter('initialMessages', '') as string;
|
||||
const initialMessages = initialMessagesRaw
|
||||
.split('\n')
|
||||
.filter((line) => line)
|
||||
.map((line) => line.trim());
|
||||
const instanceId = this.getInstanceId();
|
||||
|
||||
const i18nConfig = pick(options, ['getStarted', 'inputPlaceholder', 'subtitle', 'title']);
|
||||
|
||||
const page = createPage({
|
||||
i18n: {
|
||||
en: i18nConfig,
|
||||
},
|
||||
showWelcomeScreen: options.showWelcomeScreen,
|
||||
loadPreviousSession: options.loadPreviousSession,
|
||||
initialMessages,
|
||||
webhookUrl,
|
||||
mode,
|
||||
instanceId,
|
||||
authentication,
|
||||
});
|
||||
|
||||
res.status(200).send(page).end();
|
||||
return {
|
||||
noWebhookResponse: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (bodyData.action === 'loadPreviousSession') {
|
||||
if (options?.loadPreviousSession === 'memory') {
|
||||
const memory = (await this.getInputConnectionData(NodeConnectionType.AiMemory, 0)) as
|
||||
| BaseChatMemory
|
||||
| undefined;
|
||||
const messages = ((await memory?.chatHistory.getMessages()) ?? []).map(
|
||||
(message) => message?.toJSON(),
|
||||
);
|
||||
return {
|
||||
webhookResponse: { data: messages },
|
||||
};
|
||||
} else if (options?.loadPreviousSession === 'notSupported') {
|
||||
// If messages of a previous session should not be loaded, simply return an empty array
|
||||
return {
|
||||
webhookResponse: { data: [] },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const returnData: IDataObject = { ...bodyData };
|
||||
const webhookResponse: IDataObject = { status: 200 };
|
||||
return {
|
||||
webhookResponse,
|
||||
workflowData: [this.helpers.returnJsonArray(returnData)],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
import type { ICredentialDataDecryptedObject, IWebhookFunctions } from 'n8n-workflow';
|
||||
import basicAuth from 'basic-auth';
|
||||
import { ChatTriggerAuthorizationError } from './error';
|
||||
import type { AuthenticationChatOption } from './types';
|
||||
|
||||
export async function validateAuth(context: IWebhookFunctions) {
|
||||
const authentication = context.getNodeParameter('authentication') as AuthenticationChatOption;
|
||||
const req = context.getRequestObject();
|
||||
const headers = context.getHeaderData();
|
||||
|
||||
if (authentication === 'none') {
|
||||
return;
|
||||
} else if (authentication === 'basicAuth') {
|
||||
// Basic authorization is needed to call webhook
|
||||
let expectedAuth: ICredentialDataDecryptedObject | undefined;
|
||||
try {
|
||||
expectedAuth = await context.getCredentials('httpBasicAuth');
|
||||
} catch {}
|
||||
|
||||
if (expectedAuth === undefined || !expectedAuth.user || !expectedAuth.password) {
|
||||
// Data is not defined on node so can not authenticate
|
||||
throw new ChatTriggerAuthorizationError(500, 'No authentication data defined on node!');
|
||||
}
|
||||
|
||||
const providedAuth = basicAuth(req);
|
||||
// Authorization data is missing
|
||||
if (!providedAuth) throw new ChatTriggerAuthorizationError(401);
|
||||
|
||||
if (providedAuth.name !== expectedAuth.user || providedAuth.pass !== expectedAuth.password) {
|
||||
// Provided authentication data is wrong
|
||||
throw new ChatTriggerAuthorizationError(403);
|
||||
}
|
||||
} else if (authentication === 'n8nUserAuth') {
|
||||
const webhookName = context.getWebhookName();
|
||||
|
||||
function getCookie(name: string) {
|
||||
const value = `; ${headers.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
|
||||
if (parts.length === 2) {
|
||||
return parts.pop()?.split(';').shift();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
const authCookie = getCookie('n8n-auth');
|
||||
if (!authCookie && webhookName !== 'setup') {
|
||||
// Data is not defined on node so can not authenticate
|
||||
throw new ChatTriggerAuthorizationError(500, 'User not authenticated!');
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
export class ChatTriggerAuthorizationError extends Error {
|
||||
constructor(
|
||||
readonly responseCode: number,
|
||||
message?: string,
|
||||
) {
|
||||
if (message === undefined) {
|
||||
message = 'Authorization problem!';
|
||||
if (responseCode === 401) {
|
||||
message = 'Authorization is required!';
|
||||
} else if (responseCode === 403) {
|
||||
message = 'Authorization data is wrong!';
|
||||
}
|
||||
}
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import type { AuthenticationChatOption, LoadPreviousSessionChatOption } from './types';
|
||||
|
||||
export function createPage({
|
||||
instanceId,
|
||||
webhookUrl,
|
||||
showWelcomeScreen,
|
||||
loadPreviousSession,
|
||||
i18n: { en },
|
||||
initialMessages,
|
||||
authentication,
|
||||
}: {
|
||||
instanceId: string;
|
||||
webhookUrl?: string;
|
||||
showWelcomeScreen?: boolean;
|
||||
loadPreviousSession?: LoadPreviousSessionChatOption;
|
||||
i18n: {
|
||||
en: Record<string, string>;
|
||||
};
|
||||
initialMessages: string[];
|
||||
mode: 'test' | 'production';
|
||||
authentication: AuthenticationChatOption;
|
||||
}) {
|
||||
const validAuthenticationOptions: AuthenticationChatOption[] = [
|
||||
'none',
|
||||
'basicAuth',
|
||||
'n8nUserAuth',
|
||||
];
|
||||
const validLoadPreviousSessionOptions: LoadPreviousSessionChatOption[] = [
|
||||
'manually',
|
||||
'memory',
|
||||
'notSupported',
|
||||
];
|
||||
|
||||
const sanitizedAuthentication = validAuthenticationOptions.includes(authentication)
|
||||
? authentication
|
||||
: 'none';
|
||||
const sanitizedShowWelcomeScreen = !!showWelcomeScreen;
|
||||
const sanitizedLoadPreviousSession = validLoadPreviousSessionOptions.includes(
|
||||
loadPreviousSession as LoadPreviousSessionChatOption,
|
||||
)
|
||||
? loadPreviousSession
|
||||
: 'notSupported';
|
||||
|
||||
return `<doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Chat</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/@n8n/chat/style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<script type="module">
|
||||
import { createChat } from 'https://cdn.jsdelivr.net/npm/@n8n/chat@latest/chat.bundle.es.js';
|
||||
|
||||
(async function () {
|
||||
const authentication = '${sanitizedAuthentication}';
|
||||
let metadata;
|
||||
if (authentication === 'n8nUserAuth') {
|
||||
try {
|
||||
const response = await fetch('/rest/login', {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error('Not logged in');
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
metadata = {
|
||||
user: {
|
||||
id: responseData.data.id,
|
||||
firstName: responseData.data.firstName,
|
||||
lastName: responseData.data.lastName,
|
||||
email: responseData.data.email,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
window.location.href = '/signin?redirect=' + window.location.href;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
createChat({
|
||||
mode: 'fullscreen',
|
||||
webhookUrl: '${webhookUrl}',
|
||||
showWelcomeScreen: ${sanitizedShowWelcomeScreen},
|
||||
loadPreviousSession: ${sanitizedLoadPreviousSession !== 'notSupported'},
|
||||
metadata: metadata,
|
||||
webhookConfig: {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Instance-Id': '${instanceId}',
|
||||
}
|
||||
},
|
||||
i18n: {
|
||||
${en ? `en: ${JSON.stringify(en)},` : ''}
|
||||
},
|
||||
${initialMessages.length ? `initialMessages: ${JSON.stringify(initialMessages)},` : ''}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export type AuthenticationChatOption = 'none' | 'basicAuth' | 'n8nUserAuth';
|
||||
export type LoadPreviousSessionChatOption = 'manually' | 'memory' | 'notSupported';
|
|
@ -16,6 +16,7 @@ export class ManualChatTrigger implements INodeType {
|
|||
description: 'Runs the flow on new manual chat message',
|
||||
eventTriggerDescription: '',
|
||||
maxNodes: 1,
|
||||
hidden: true,
|
||||
defaults: {
|
||||
name: 'On new manual Chat Message',
|
||||
color: '#909298',
|
||||
|
|
|
@ -91,6 +91,7 @@
|
|||
"dist/nodes/tools/ToolWolframAlpha/ToolWolframAlpha.node.js",
|
||||
"dist/nodes/tools/ToolWorkflow/ToolWorkflow.node.js",
|
||||
"dist/nodes/trigger/ManualChatTrigger/ManualChatTrigger.node.js",
|
||||
"dist/nodes/trigger/ChatTrigger/ChatTrigger.node.js",
|
||||
"dist/nodes/vector_store/VectorStoreInMemory/VectorStoreInMemory.node.js",
|
||||
"dist/nodes/vector_store/VectorStoreInMemoryInsert/VectorStoreInMemoryInsert.node.js",
|
||||
"dist/nodes/vector_store/VectorStoreInMemoryLoad/VectorStoreInMemoryLoad.node.js",
|
||||
|
@ -107,6 +108,7 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/basic-auth": "^1.1.3",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/html-to-text": "^9.0.1",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
|
@ -128,6 +130,7 @@
|
|||
"@qdrant/js-client-rest": "1.7.0",
|
||||
"@supabase/supabase-js": "2.38.5",
|
||||
"@xata.io/client": "0.25.3",
|
||||
"basic-auth": "2.0.1",
|
||||
"cohere-ai": "6.2.2",
|
||||
"d3-dsv": "2.0.0",
|
||||
"epub2": "3.0.1",
|
||||
|
|
|
@ -106,7 +106,7 @@ export class N8nBinaryLoader {
|
|||
}
|
||||
|
||||
let loader: PDFLoader | CSVLoader | EPubLoader | DocxLoader | TextLoader | JSONLoader;
|
||||
let cleanupTmpFile: DirectoryResult["cleanup"] | undefined = undefined;
|
||||
let cleanupTmpFile: DirectoryResult['cleanup'] | undefined = undefined;
|
||||
|
||||
switch (mimeType) {
|
||||
case 'application/pdf':
|
||||
|
@ -144,12 +144,9 @@ export class N8nBinaryLoader {
|
|||
cleanupTmpFile = tmpFileData.cleanup;
|
||||
try {
|
||||
const bufferData = await filePathOrBlob.arrayBuffer();
|
||||
await pipeline(
|
||||
[new Uint8Array(bufferData)],
|
||||
createWriteStream(tmpFileData.path),
|
||||
);
|
||||
await pipeline([new Uint8Array(bufferData)], createWriteStream(tmpFileData.path));
|
||||
loader = new EPubLoader(tmpFileData.path);
|
||||
break
|
||||
break;
|
||||
} catch (error) {
|
||||
await cleanupTmpFile();
|
||||
throw new NodeOperationError(this.context.getNode(), error as Error);
|
||||
|
|
|
@ -2514,6 +2514,197 @@ const addExecutionDataFunctions = async (
|
|||
}
|
||||
};
|
||||
|
||||
async function getInputConnectionData(
|
||||
this: IAllExecuteFunctions,
|
||||
workflow: Workflow,
|
||||
runExecutionData: IRunExecutionData,
|
||||
runIndex: number,
|
||||
connectionInputData: INodeExecutionData[],
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
executeData: IExecuteData | undefined,
|
||||
mode: WorkflowExecuteMode,
|
||||
closeFunctions: CloseFunction[],
|
||||
inputName: ConnectionTypes,
|
||||
itemIndex: number,
|
||||
// TODO: Not implemented yet, and maybe also not needed
|
||||
inputIndex?: number,
|
||||
): Promise<unknown> {
|
||||
const node = this.getNode();
|
||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
|
||||
const inputs = NodeHelpers.getNodeInputs(workflow, node, nodeType.description);
|
||||
|
||||
let inputConfiguration = inputs.find((input) => {
|
||||
if (typeof input === 'string') {
|
||||
return input === inputName;
|
||||
}
|
||||
return input.type === inputName;
|
||||
});
|
||||
|
||||
if (inputConfiguration === undefined) {
|
||||
throw new ApplicationError('Node does not have input of type', {
|
||||
extra: { nodeName: node.name, inputName },
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof inputConfiguration === 'string') {
|
||||
inputConfiguration = {
|
||||
type: inputConfiguration,
|
||||
} as INodeInputConfiguration;
|
||||
}
|
||||
|
||||
const parentNodes = workflow.getParentNodes(node.name, inputName, 1);
|
||||
if (parentNodes.length === 0) {
|
||||
return inputConfiguration.maxConnections === 1 ? undefined : [];
|
||||
}
|
||||
|
||||
const constParentNodes = parentNodes
|
||||
.map((nodeName) => {
|
||||
return workflow.getNode(nodeName) as INode;
|
||||
})
|
||||
.filter((connectedNode) => connectedNode.disabled !== true)
|
||||
.map(async (connectedNode) => {
|
||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(
|
||||
connectedNode.type,
|
||||
connectedNode.typeVersion,
|
||||
);
|
||||
|
||||
if (!nodeType.supplyData) {
|
||||
throw new ApplicationError('Node does not have a `supplyData` method defined', {
|
||||
extra: { nodeName: connectedNode.name },
|
||||
});
|
||||
}
|
||||
|
||||
const context = Object.assign({}, this);
|
||||
|
||||
context.getNodeParameter = (
|
||||
parameterName: string,
|
||||
itemIndex: number,
|
||||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
) => {
|
||||
return getNodeParameter(
|
||||
workflow,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
connectedNode,
|
||||
parameterName,
|
||||
itemIndex,
|
||||
mode,
|
||||
getAdditionalKeys(additionalData, mode, runExecutionData),
|
||||
executeData,
|
||||
fallbackValue,
|
||||
{ ...(options || {}), contextNode: node },
|
||||
) as any;
|
||||
};
|
||||
|
||||
// TODO: Check what else should be overwritten
|
||||
context.getNode = () => {
|
||||
return deepCopy(connectedNode);
|
||||
};
|
||||
|
||||
context.getCredentials = async (key: string) => {
|
||||
try {
|
||||
return await getCredentials(
|
||||
workflow,
|
||||
connectedNode,
|
||||
key,
|
||||
additionalData,
|
||||
mode,
|
||||
executeData,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
itemIndex,
|
||||
);
|
||||
} catch (error) {
|
||||
// Display the error on the node which is causing it
|
||||
|
||||
let currentNodeRunIndex = 0;
|
||||
if (runExecutionData.resultData.runData.hasOwnProperty(node.name)) {
|
||||
currentNodeRunIndex = runExecutionData.resultData.runData[node.name].length;
|
||||
}
|
||||
|
||||
await addExecutionDataFunctions(
|
||||
'input',
|
||||
connectedNode.name,
|
||||
error,
|
||||
runExecutionData,
|
||||
inputName,
|
||||
additionalData,
|
||||
node.name,
|
||||
runIndex,
|
||||
currentNodeRunIndex,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await nodeType.supplyData.call(context, itemIndex);
|
||||
if (response.closeFunction) {
|
||||
closeFunctions.push(response.closeFunction);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Propagate errors from sub-nodes
|
||||
if (error.functionality === 'configuration-node') throw error;
|
||||
if (!(error instanceof ExecutionBaseError)) {
|
||||
error = new NodeOperationError(connectedNode, error, {
|
||||
itemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
let currentNodeRunIndex = 0;
|
||||
if (runExecutionData.resultData.runData.hasOwnProperty(node.name)) {
|
||||
currentNodeRunIndex = runExecutionData.resultData.runData[node.name].length;
|
||||
}
|
||||
|
||||
// Display the error on the node which is causing it
|
||||
await addExecutionDataFunctions(
|
||||
'input',
|
||||
connectedNode.name,
|
||||
error,
|
||||
runExecutionData,
|
||||
inputName,
|
||||
additionalData,
|
||||
node.name,
|
||||
runIndex,
|
||||
currentNodeRunIndex,
|
||||
);
|
||||
|
||||
// Display on the calling node which node has the error
|
||||
throw new NodeOperationError(connectedNode, `Error in sub-node ${connectedNode.name}`, {
|
||||
itemIndex,
|
||||
functionality: 'configuration-node',
|
||||
description: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Validate the inputs
|
||||
const nodes = await Promise.all(constParentNodes);
|
||||
|
||||
if (inputConfiguration.required && nodes.length === 0) {
|
||||
throw new NodeOperationError(node, `A ${inputName} processor node must be connected!`);
|
||||
}
|
||||
if (
|
||||
inputConfiguration.maxConnections !== undefined &&
|
||||
nodes.length > inputConfiguration.maxConnections
|
||||
) {
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
`Only ${inputConfiguration.maxConnections} ${inputName} processor nodes are/is allowed to be connected!`,
|
||||
);
|
||||
}
|
||||
|
||||
return inputConfiguration.maxConnections === 1
|
||||
? (nodes || [])[0]?.response
|
||||
: nodes.map((node) => node.response);
|
||||
}
|
||||
|
||||
const getCommonWorkflowFunctions = (
|
||||
workflow: Workflow,
|
||||
node: INode,
|
||||
|
@ -3197,191 +3388,29 @@ export function getExecuteFunctions(
|
|||
getContext(type: ContextType): IContextObject {
|
||||
return NodeHelpers.getContext(runExecutionData, type, node);
|
||||
},
|
||||
|
||||
async getInputConnectionData(
|
||||
inputName: ConnectionTypes,
|
||||
itemIndex: number,
|
||||
// TODO: Not implemented yet, and maybe also not needed
|
||||
inputIndex?: number,
|
||||
): Promise<unknown> {
|
||||
const node = this.getNode();
|
||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
|
||||
const inputs = NodeHelpers.getNodeInputs(workflow, node, nodeType.description);
|
||||
|
||||
let inputConfiguration = inputs.find((input) => {
|
||||
if (typeof input === 'string') {
|
||||
return input === inputName;
|
||||
}
|
||||
return input.type === inputName;
|
||||
});
|
||||
|
||||
if (inputConfiguration === undefined) {
|
||||
throw new ApplicationError('Node does not have input of type', {
|
||||
extra: { nodeName: node.name, inputName },
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof inputConfiguration === 'string') {
|
||||
inputConfiguration = {
|
||||
type: inputConfiguration,
|
||||
} as INodeInputConfiguration;
|
||||
}
|
||||
|
||||
const parentNodes = workflow.getParentNodes(node.name, inputName, 1);
|
||||
if (parentNodes.length === 0) {
|
||||
return inputConfiguration.maxConnections === 1 ? undefined : [];
|
||||
}
|
||||
|
||||
const constParentNodes = parentNodes
|
||||
.map((nodeName) => {
|
||||
return workflow.getNode(nodeName) as INode;
|
||||
})
|
||||
.filter((connectedNode) => connectedNode.disabled !== true)
|
||||
.map(async (connectedNode) => {
|
||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(
|
||||
connectedNode.type,
|
||||
connectedNode.typeVersion,
|
||||
);
|
||||
|
||||
if (!nodeType.supplyData) {
|
||||
throw new ApplicationError('Node does not have a `supplyData` method defined', {
|
||||
extra: { nodeName: connectedNode.name },
|
||||
});
|
||||
}
|
||||
|
||||
const context = Object.assign({}, this);
|
||||
|
||||
context.getNodeParameter = (
|
||||
parameterName: string,
|
||||
itemIndex: number,
|
||||
fallbackValue?: any,
|
||||
options?: IGetNodeParameterOptions,
|
||||
) => {
|
||||
return getNodeParameter(
|
||||
workflow,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
connectedNode,
|
||||
parameterName,
|
||||
itemIndex,
|
||||
mode,
|
||||
getAdditionalKeys(additionalData, mode, runExecutionData),
|
||||
executeData,
|
||||
fallbackValue,
|
||||
{ ...(options || {}), contextNode: node },
|
||||
) as any;
|
||||
};
|
||||
|
||||
// TODO: Check what else should be overwritten
|
||||
context.getNode = () => {
|
||||
return deepCopy(connectedNode);
|
||||
};
|
||||
|
||||
context.getCredentials = async (key: string) => {
|
||||
try {
|
||||
return await getCredentials(
|
||||
workflow,
|
||||
connectedNode,
|
||||
key,
|
||||
additionalData,
|
||||
mode,
|
||||
executeData,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
itemIndex,
|
||||
);
|
||||
} catch (error) {
|
||||
// Display the error on the node which is causing it
|
||||
|
||||
let currentNodeRunIndex = 0;
|
||||
if (runExecutionData.resultData.runData.hasOwnProperty(node.name)) {
|
||||
currentNodeRunIndex = runExecutionData.resultData.runData[node.name].length;
|
||||
}
|
||||
|
||||
await addExecutionDataFunctions(
|
||||
'input',
|
||||
connectedNode.name,
|
||||
error,
|
||||
runExecutionData,
|
||||
inputName,
|
||||
additionalData,
|
||||
node.name,
|
||||
runIndex,
|
||||
currentNodeRunIndex,
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await nodeType.supplyData.call(context, itemIndex);
|
||||
if (response.closeFunction) {
|
||||
closeFunctions.push(response.closeFunction);
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Propagate errors from sub-nodes
|
||||
if (error.functionality === 'configuration-node') throw error;
|
||||
if (!(error instanceof ExecutionBaseError)) {
|
||||
error = new NodeOperationError(connectedNode, error, {
|
||||
itemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
let currentNodeRunIndex = 0;
|
||||
if (runExecutionData.resultData.runData.hasOwnProperty(node.name)) {
|
||||
currentNodeRunIndex = runExecutionData.resultData.runData[node.name].length;
|
||||
}
|
||||
|
||||
// Display the error on the node which is causing it
|
||||
await addExecutionDataFunctions(
|
||||
'input',
|
||||
connectedNode.name,
|
||||
error,
|
||||
runExecutionData,
|
||||
inputName,
|
||||
additionalData,
|
||||
node.name,
|
||||
runIndex,
|
||||
currentNodeRunIndex,
|
||||
);
|
||||
|
||||
// Display on the calling node which node has the error
|
||||
throw new NodeOperationError(
|
||||
connectedNode,
|
||||
`Error in sub-node ${connectedNode.name}`,
|
||||
{
|
||||
itemIndex,
|
||||
functionality: 'configuration-node',
|
||||
description: error.message,
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Validate the inputs
|
||||
const nodes = await Promise.all(constParentNodes);
|
||||
|
||||
if (inputConfiguration.required && nodes.length === 0) {
|
||||
throw new NodeOperationError(node, `A ${inputName} processor node must be connected!`);
|
||||
}
|
||||
if (
|
||||
inputConfiguration.maxConnections !== undefined &&
|
||||
nodes.length > inputConfiguration.maxConnections
|
||||
) {
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
`Only ${inputConfiguration.maxConnections} ${inputName} processor nodes are/is allowed to be connected!`,
|
||||
);
|
||||
}
|
||||
|
||||
return inputConfiguration.maxConnections === 1
|
||||
? (nodes || [])[0]?.response
|
||||
: nodes.map((node) => node.response);
|
||||
return getInputConnectionData.call(
|
||||
this,
|
||||
workflow,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
additionalData,
|
||||
executeData,
|
||||
mode,
|
||||
closeFunctions,
|
||||
inputName,
|
||||
itemIndex,
|
||||
inputIndex,
|
||||
);
|
||||
},
|
||||
|
||||
getNodeOutputs(): INodeOutputConfiguration[] {
|
||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
return NodeHelpers.getNodeOutputs(workflow, node, nodeType.description).map((output) => {
|
||||
|
@ -3862,12 +3891,14 @@ export function getExecuteHookFunctions(
|
|||
/**
|
||||
* Returns the execute functions regular nodes have access to when webhook-function is defined.
|
||||
*/
|
||||
// TODO: check where it is used and make sure close functions are called
|
||||
export function getExecuteWebhookFunctions(
|
||||
workflow: Workflow,
|
||||
node: INode,
|
||||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
mode: WorkflowExecuteMode,
|
||||
webhookData: IWebhookData,
|
||||
closeFunctions: CloseFunction[],
|
||||
): IWebhookFunctions {
|
||||
return ((workflow: Workflow, node: INode) => {
|
||||
return {
|
||||
|
@ -3885,6 +3916,47 @@ export function getExecuteWebhookFunctions(
|
|||
}
|
||||
return additionalData.httpRequest.headers;
|
||||
},
|
||||
async getInputConnectionData(
|
||||
inputName: ConnectionTypes,
|
||||
itemIndex: number,
|
||||
// TODO: Not implemented yet, and maybe also not needed
|
||||
inputIndex?: number,
|
||||
): Promise<unknown> {
|
||||
// To be able to use expressions like "$json.sessionId" set the
|
||||
// body data the webhook received to what is normally used for
|
||||
// incoming node data.
|
||||
const connectionInputData: INodeExecutionData[] = [
|
||||
{ json: additionalData.httpRequest?.body || {} },
|
||||
];
|
||||
const runExecutionData: IRunExecutionData = {
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
};
|
||||
const executeData: IExecuteData = {
|
||||
data: {
|
||||
main: [connectionInputData],
|
||||
},
|
||||
node,
|
||||
source: null,
|
||||
};
|
||||
const runIndex = 0;
|
||||
|
||||
return getInputConnectionData.call(
|
||||
this,
|
||||
workflow,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
connectionInputData,
|
||||
additionalData,
|
||||
executeData,
|
||||
mode,
|
||||
closeFunctions,
|
||||
inputName,
|
||||
itemIndex,
|
||||
inputIndex,
|
||||
);
|
||||
},
|
||||
getMode: () => mode,
|
||||
getNodeParameter: (
|
||||
parameterName: string,
|
||||
|
|
|
@ -66,10 +66,10 @@
|
|||
|
||||
// LangChain
|
||||
--color-lm-chat-messages-background: var(--color-background-base);
|
||||
--color-lm-chat-bot-background: var(--prim-gray-120);
|
||||
--color-lm-chat-bot-border: var(--prim-gray-200);
|
||||
--color-lm-chat-user-background: var(--prim-color-alt-a-tint-400);
|
||||
--color-lm-chat-user-border: var(--prim-color-alt-a-tint-300);
|
||||
--color-lm-chat-bot-background: var(--prim-gray-0);
|
||||
--color-lm-chat-bot-color: var(--color-text-dark);
|
||||
--color-lm-chat-user-background: var(--prim-color-alt-a);
|
||||
--color-lm-chat-user-color: var(--color-text-xlight);
|
||||
|
||||
// Canvas
|
||||
--color-canvas-background: var(--prim-gray-10);
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
"@jsplumb/core": "^5.13.2",
|
||||
"@jsplumb/util": "^5.13.2",
|
||||
"@lezer/common": "^1.0.4",
|
||||
"@n8n/chat": "workspace:*",
|
||||
"@n8n/codemirror-lang-sql": "^1.0.2",
|
||||
"@n8n/permissions": "workspace:*",
|
||||
"@vueuse/components": "^10.5.0",
|
||||
|
@ -89,7 +90,8 @@
|
|||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/luxon": "^3.2.0",
|
||||
"@types/uuid": "^8.3.2",
|
||||
"miragejs": "^0.1.47"
|
||||
"miragejs": "^0.1.47",
|
||||
"unplugin-icons": "^0.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "*",
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import type { INodeTypeData, INodeTypeDescription } from 'n8n-workflow';
|
||||
import {
|
||||
AGENT_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import { AGENT_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
|
||||
import nodeTypesJson from '../../../nodes-base/dist/types/nodes.json';
|
||||
import aiNodeTypesJson from '../../../@n8n/nodes-langchain/dist/types/nodes.json';
|
||||
|
||||
|
@ -20,10 +16,10 @@ export const testingNodeTypes: INodeTypeData = {
|
|||
description: findNodeWithName(MANUAL_TRIGGER_NODE_TYPE),
|
||||
},
|
||||
},
|
||||
[MANUAL_CHAT_TRIGGER_NODE_TYPE]: {
|
||||
[CHAT_TRIGGER_NODE_TYPE]: {
|
||||
sourcePath: '',
|
||||
type: {
|
||||
description: findNodeWithName(MANUAL_CHAT_TRIGGER_NODE_TYPE),
|
||||
description: findNodeWithName(CHAT_TRIGGER_NODE_TYPE),
|
||||
},
|
||||
},
|
||||
[AGENT_NODE_TYPE]: {
|
||||
|
|
|
@ -4,7 +4,7 @@ import { computed, ref } from 'vue';
|
|||
import type { EventBus } from 'n8n-design-system/utils';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import Modal from './Modal.vue';
|
||||
import { CHAT_EMBED_MODAL_KEY, WEBHOOK_NODE_TYPE } from '../constants';
|
||||
import { CHAT_EMBED_MODAL_KEY, CHAT_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE } from '../constants';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
|
||||
|
@ -43,11 +43,30 @@ const tabs = ref([
|
|||
const currentTab = ref('cdn');
|
||||
|
||||
const webhookNode = computed(() => {
|
||||
return workflowsStore.workflow.nodes.find((node) => node.type === WEBHOOK_NODE_TYPE);
|
||||
for (const type of [CHAT_TRIGGER_NODE_TYPE, WEBHOOK_NODE_TYPE]) {
|
||||
const node = workflowsStore.workflow.nodes.find((node) => node.type === type);
|
||||
if (node) {
|
||||
// This has to be kept up-to-date with the mode in the Chat-Trigger node
|
||||
if (type === CHAT_TRIGGER_NODE_TYPE && !node.parameters.public) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
node,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const webhookUrl = computed(() => {
|
||||
return `${rootStore.getWebhookUrl}${webhookNode.value ? `/${webhookNode.value.webhookId}` : ''}`;
|
||||
const url = `${rootStore.getWebhookUrl}${
|
||||
webhookNode.value ? `/${webhookNode.value.node.webhookId}` : ''
|
||||
}`;
|
||||
|
||||
return webhookNode.value?.type === CHAT_TRIGGER_NODE_TYPE ? `${url}/chat` : url;
|
||||
});
|
||||
|
||||
function indentLines(code: string, indent: string = ' ') {
|
||||
|
@ -57,7 +76,7 @@ function indentLines(code: string, indent: string = ' ') {
|
|||
.join('\n');
|
||||
}
|
||||
|
||||
const importCode = 'import';
|
||||
const importCode = 'import'; // To avoid vite from parsing the import statement
|
||||
const commonCode = computed(() => ({
|
||||
import: `${importCode} '@n8n/chat/style.css';
|
||||
${importCode} { createChat } from '@n8n/chat';`,
|
||||
|
@ -126,29 +145,38 @@ function closeDialog() {
|
|||
<n8n-tabs v-model="currentTab" :options="tabs" />
|
||||
|
||||
<div v-if="currentTab !== 'cdn'">
|
||||
<n8n-text>
|
||||
{{ i18n.baseText('chatEmbed.install') }}
|
||||
</n8n-text>
|
||||
<div class="mb-s">
|
||||
<n8n-text>
|
||||
{{ i18n.baseText('chatEmbed.install') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<CodeNodeEditor :model-value="commonCode.install" is-read-only />
|
||||
</div>
|
||||
|
||||
<n8n-text>
|
||||
<i18n-t :keypath="`chatEmbed.paste.${currentTab}`">
|
||||
<template #code>
|
||||
<code>{{ i18n.baseText(`chatEmbed.paste.${currentTab}.file`) }}</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</n8n-text>
|
||||
<div class="mb-s">
|
||||
<n8n-text>
|
||||
<i18n-t :keypath="`chatEmbed.paste.${currentTab}`">
|
||||
<template #code>
|
||||
<code>{{ i18n.baseText(`chatEmbed.paste.${currentTab}.file`) }}</code>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
||||
<HtmlEditor v-if="currentTab === 'cdn'" :model-value="cdnCode" is-read-only />
|
||||
<HtmlEditor v-if="currentTab === 'vue'" :model-value="vueCode" is-read-only />
|
||||
<CodeNodeEditor v-if="currentTab === 'react'" :model-value="reactCode" is-read-only />
|
||||
<CodeNodeEditor v-if="currentTab === 'other'" :model-value="otherCode" is-read-only />
|
||||
|
||||
<n8n-info-tip>
|
||||
<n8n-text>
|
||||
{{ i18n.baseText('chatEmbed.packageInfo.description') }}
|
||||
<n8n-link :href="i18n.baseText('chatEmbed.url')" new-window size="small" bold>
|
||||
<n8n-link :href="i18n.baseText('chatEmbed.url')" new-window bold>
|
||||
{{ i18n.baseText('chatEmbed.packageInfo.link') }}
|
||||
</n8n-link>
|
||||
</n8n-text>
|
||||
|
||||
<n8n-info-tip class="mt-s">
|
||||
{{ i18n.baseText('chatEmbed.chatTriggerNode') }}
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
:style="nodeWrapperStyles"
|
||||
data-test-id="canvas-node"
|
||||
:data-name="data.name"
|
||||
:data-node-type="nodeType?.name"
|
||||
@contextmenu="(e: MouseEvent) => openContextMenu(e, 'node-right-click')"
|
||||
>
|
||||
<div v-show="isSelected" class="select-background"></div>
|
||||
|
@ -1026,6 +1027,11 @@ export default defineComponent({
|
|||
left: -67px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-node-type='@n8n/n8n-nodes-langchain.chatTrigger'] {
|
||||
--configurable-node-min-input-count: 1;
|
||||
--configurable-node-input-width: 176px;
|
||||
}
|
||||
}
|
||||
|
||||
&--trigger .node-default .node-box {
|
||||
|
|
|
@ -12,6 +12,7 @@ import type {
|
|||
import {
|
||||
AGENT_NODE_TYPE,
|
||||
BASIC_CHAIN_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
NODE_CREATOR_OPEN_SOURCES,
|
||||
|
@ -190,7 +191,9 @@ export const useActions = () => {
|
|||
];
|
||||
|
||||
const isChatTriggerMissing =
|
||||
allNodes.find((node) => node.type === MANUAL_CHAT_TRIGGER_NODE_TYPE) === undefined;
|
||||
allNodes.find((node) =>
|
||||
[MANUAL_CHAT_TRIGGER_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE].includes(node.type),
|
||||
) === undefined;
|
||||
const isCompatibleNode = addedNodes.some((node) => COMPATIBLE_CHAT_NODES.includes(node.type));
|
||||
|
||||
return isCompatibleNode && isChatTriggerMissing;
|
||||
|
@ -211,7 +214,7 @@ export const useActions = () => {
|
|||
}
|
||||
|
||||
if (shouldPrependChatTrigger(addedNodes)) {
|
||||
addedNodes.unshift({ type: MANUAL_CHAT_TRIGGER_NODE_TYPE, isAutoAdd: true });
|
||||
addedNodes.unshift({ type: CHAT_TRIGGER_NODE_TYPE, isAutoAdd: true });
|
||||
connections.push({
|
||||
from: { nodeIndex: 0 },
|
||||
to: { nodeIndex: 1 },
|
||||
|
|
|
@ -276,9 +276,14 @@ export default defineComponent({
|
|||
return null;
|
||||
},
|
||||
showTriggerPanel(): boolean {
|
||||
const override = !!this.activeNodeType?.triggerPanel;
|
||||
if (typeof this.activeNodeType?.triggerPanel === 'boolean') {
|
||||
return override;
|
||||
}
|
||||
|
||||
const isWebhookBasedNode = !!this.activeNodeType?.webhooks?.length;
|
||||
const isPollingNode = this.activeNodeType?.polling;
|
||||
const override = !!this.activeNodeType?.triggerPanel;
|
||||
|
||||
return (
|
||||
!this.readOnly && this.isTriggerNode && (isWebhookBasedNode || isPollingNode || override)
|
||||
);
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
MODAL_CONFIRM,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
@ -40,6 +41,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|||
import { useMessage } from '@/composables/useMessage';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { nodeViewEventBus } from '@/event-bus';
|
||||
import { usePinnedData } from '@/composables/usePinnedData';
|
||||
|
||||
export default defineComponent({
|
||||
|
@ -116,6 +118,9 @@ export default defineComponent({
|
|||
isManualTriggerNode(): boolean {
|
||||
return Boolean(this.nodeType && this.nodeType.name === MANUAL_TRIGGER_NODE_TYPE);
|
||||
},
|
||||
isChatNode(): boolean {
|
||||
return Boolean(this.nodeType && this.nodeType.name === CHAT_TRIGGER_NODE_TYPE);
|
||||
},
|
||||
isFormTriggerNode(): boolean {
|
||||
return Boolean(this.nodeType && this.nodeType.name === FORM_TRIGGER_NODE_TYPE);
|
||||
},
|
||||
|
@ -186,6 +191,10 @@ export default defineComponent({
|
|||
return this.label;
|
||||
}
|
||||
|
||||
if (this.isChatNode) {
|
||||
return this.$locale.baseText('ndv.execute.testChat');
|
||||
}
|
||||
|
||||
if (this.isWebhookNode) {
|
||||
return this.$locale.baseText('ndv.execute.listenForTestEvent');
|
||||
}
|
||||
|
@ -212,7 +221,10 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
async onClick() {
|
||||
if (this.isListeningForEvents) {
|
||||
if (this.isChatNode) {
|
||||
this.ndvStore.setActiveNodeName(null);
|
||||
nodeViewEventBus.emit('openChat');
|
||||
} else if (this.isListeningForEvents) {
|
||||
await this.stopWaitingForWebhook();
|
||||
} else if (this.isListeningForWorkflowEvents) {
|
||||
this.$emit('stopExecution');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div v-if="webhooksNode.length" class="webhooks">
|
||||
<div v-if="webhooksNode.length && visibleWebhookUrls.length > 0" class="webhooks">
|
||||
<div
|
||||
class="clickable headline"
|
||||
:class="{ expanded: !isMinimized }"
|
||||
|
@ -11,31 +11,22 @@
|
|||
</div>
|
||||
<el-collapse-transition>
|
||||
<div v-if="!isMinimized" class="node-webhooks">
|
||||
<div class="url-selection">
|
||||
<div v-if="!isProductionOnly" class="url-selection">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<n8n-radio-buttons
|
||||
v-model="showUrlFor"
|
||||
:options="[
|
||||
{ label: baseText.testUrl, value: 'test' },
|
||||
{
|
||||
label: baseText.productionUrl,
|
||||
value: 'production',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<n8n-radio-buttons v-model="showUrlFor" :options="urlOptions" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<n8n-tooltip
|
||||
v-for="(webhook, index) in webhooksNode.filter((webhook) => !webhook.ndvHideUrl)"
|
||||
v-for="(webhook, index) in visibleWebhookUrls"
|
||||
:key="index"
|
||||
class="item"
|
||||
:content="baseText.clickToCopy"
|
||||
placement="left"
|
||||
>
|
||||
<div v-if="!webhook.ndvHideMethod" class="webhook-wrapper">
|
||||
<div v-if="isWebhookMethodVisible(webhook)" class="webhook-wrapper">
|
||||
<div class="http-field">
|
||||
<div class="http-method">
|
||||
{{ getWebhookExpressionValue(webhook, 'httpMethod') }}<br />
|
||||
|
@ -65,7 +56,12 @@ import type { INodeTypeDescription, IWebhookDescription } from 'n8n-workflow';
|
|||
import { defineComponent } from 'vue';
|
||||
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { FORM_TRIGGER_NODE_TYPE, OPEN_URL_PANEL_TRIGGER_NODE_TYPES } from '@/constants';
|
||||
import {
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
OPEN_URL_PANEL_TRIGGER_NODE_TYPES,
|
||||
PRODUCTION_ONLY_TRIGGER_NODE_TYPES,
|
||||
} from '@/constants';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
|
||||
|
@ -91,6 +87,27 @@ export default defineComponent({
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
isProductionOnly(): boolean {
|
||||
return this.nodeType && PRODUCTION_ONLY_TRIGGER_NODE_TYPES.includes(this.nodeType.name);
|
||||
},
|
||||
urlOptions(): Array<{ label: string; value: string }> {
|
||||
return [
|
||||
...(this.isProductionOnly ? [] : [{ label: this.baseText.testUrl, value: 'test' }]),
|
||||
{
|
||||
label: this.baseText.productionUrl,
|
||||
value: 'production',
|
||||
},
|
||||
];
|
||||
},
|
||||
visibleWebhookUrls(): IWebhookDescription[] {
|
||||
return this.webhooksNode.filter((webhook) => {
|
||||
if (typeof webhook.ndvHideUrl === 'string') {
|
||||
return !this.getWebhookExpressionValue(webhook, 'ndvHideUrl');
|
||||
}
|
||||
|
||||
return !webhook.ndvHideUrl;
|
||||
});
|
||||
},
|
||||
webhooksNode(): IWebhookDescription[] {
|
||||
if (this.nodeType === null || this.nodeType.webhooks === undefined) {
|
||||
return [];
|
||||
|
@ -103,6 +120,20 @@ export default defineComponent({
|
|||
baseText() {
|
||||
const nodeType = this.nodeType.name;
|
||||
switch (nodeType) {
|
||||
case CHAT_TRIGGER_NODE_TYPE:
|
||||
return {
|
||||
toggleTitle: this.$locale.baseText('nodeWebhooks.webhookUrls.chatTrigger'),
|
||||
clickToDisplay: this.$locale.baseText(
|
||||
'nodeWebhooks.clickToDisplayWebhookUrls.formTrigger',
|
||||
),
|
||||
clickToHide: this.$locale.baseText('nodeWebhooks.clickToHideWebhookUrls.chatTrigger'),
|
||||
clickToCopy: this.$locale.baseText('nodeWebhooks.clickToCopyWebhookUrls.chatTrigger'),
|
||||
testUrl: this.$locale.baseText('nodeWebhooks.testUrl'),
|
||||
productionUrl: this.$locale.baseText('nodeWebhooks.productionUrl'),
|
||||
copyTitle: this.$locale.baseText('nodeWebhooks.showMessage.title.chatTrigger'),
|
||||
copyMessage: this.$locale.baseText('nodeWebhooks.showMessage.message.chatTrigger'),
|
||||
};
|
||||
|
||||
case FORM_TRIGGER_NODE_TYPE:
|
||||
return {
|
||||
toggleTitle: this.$locale.baseText('nodeWebhooks.webhookUrls.formTrigger'),
|
||||
|
@ -153,10 +184,21 @@ export default defineComponent({
|
|||
},
|
||||
getWebhookUrlDisplay(webhookData: IWebhookDescription): string {
|
||||
if (this.node) {
|
||||
return this.getWebhookUrl(webhookData, this.node, this.showUrlFor);
|
||||
return this.getWebhookUrl(
|
||||
webhookData,
|
||||
this.node,
|
||||
this.isProductionOnly ? 'production' : this.showUrlFor,
|
||||
);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isWebhookMethodVisible(webhook: IWebhookDescription): boolean {
|
||||
if (typeof webhook.ndvHideMethod === 'string') {
|
||||
return !this.getWebhookExpressionValue(webhook, 'ndvHideMethod');
|
||||
}
|
||||
|
||||
return !webhook.ndvHideMethod;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -168,7 +168,6 @@ import {
|
|||
isAuthRelatedParameter,
|
||||
} from '@/utils/nodeTypesUtils';
|
||||
import { get, set } from 'lodash-es';
|
||||
import { nodeViewEventBus } from '@/event-bus';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
|
||||
const FixedCollectionParameter = defineAsyncComponent(
|
||||
|
@ -476,14 +475,16 @@ export default defineComponent({
|
|||
this.$emit('activate');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Handles default node button parameter type actions
|
||||
* @param parameter
|
||||
*/
|
||||
onButtonAction(parameter: INodeProperties) {
|
||||
const action: string | undefined = parameter.typeOptions?.action;
|
||||
|
||||
switch (action) {
|
||||
case 'openChat':
|
||||
this.ndvStore.setActiveNodeName(null);
|
||||
nodeViewEventBus.emit('openChat');
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
isNodeAuthField(name: string): boolean {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div :class="$style.container">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="hasIssues" key="empty"></div>
|
||||
<div v-if="hasIssues || hideContent" key="empty"></div>
|
||||
<div v-else-if="isListeningForEvents" key="listening">
|
||||
<n8n-pulse>
|
||||
<NodeIcon :node-type="nodeType" :size="40"></NodeIcon>
|
||||
|
@ -45,6 +45,12 @@
|
|||
{{ listeningHint }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<div v-if="displayChatButton">
|
||||
<n8n-button @click="openWebhookUrl()" class="mb-xl">
|
||||
{{ $locale.baseText('ndv.trigger.chatTrigger.openChat') }}
|
||||
</n8n-button>
|
||||
</div>
|
||||
|
||||
<NodeExecuteButton
|
||||
data-test-id="trigger-execute-button"
|
||||
:node-name="nodeName"
|
||||
|
@ -105,6 +111,7 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import {
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
VIEWS,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
|
@ -156,6 +163,36 @@ export default defineComponent({
|
|||
|
||||
return null;
|
||||
},
|
||||
hideContent(): boolean {
|
||||
if (!this.nodeType?.triggerPanel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.nodeType?.triggerPanel &&
|
||||
this.nodeType?.triggerPanel.hasOwnProperty('hideContent')
|
||||
) {
|
||||
const hideContent = this.nodeType?.triggerPanel.hideContent;
|
||||
if (typeof hideContent === 'boolean') {
|
||||
return hideContent;
|
||||
}
|
||||
|
||||
if (this.node) {
|
||||
const hideContentValue = this.getCurrentWorkflow().expression.getSimpleParameterValue(
|
||||
this.node,
|
||||
hideContent,
|
||||
'internal',
|
||||
{},
|
||||
);
|
||||
|
||||
if (typeof hideContentValue === 'boolean') {
|
||||
return hideContentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
hasIssues(): boolean {
|
||||
return Boolean(
|
||||
this.node?.issues && (this.node.issues.parameters || this.node.issues.credentials),
|
||||
|
@ -168,6 +205,13 @@ export default defineComponent({
|
|||
|
||||
return '';
|
||||
},
|
||||
displayChatButton(): boolean {
|
||||
return Boolean(
|
||||
this.node &&
|
||||
this.node.type === CHAT_TRIGGER_NODE_TYPE &&
|
||||
this.node.parameters.mode !== 'webhook',
|
||||
);
|
||||
},
|
||||
isWebhookNode(): boolean {
|
||||
return Boolean(this.node && this.node.type === WEBHOOK_NODE_TYPE);
|
||||
},
|
||||
|
@ -219,11 +263,16 @@ export default defineComponent({
|
|||
: this.$locale.baseText('ndv.trigger.webhookNode.listening');
|
||||
},
|
||||
listeningHint(): string {
|
||||
return this.nodeType?.name === FORM_TRIGGER_NODE_TYPE
|
||||
? this.$locale.baseText('ndv.trigger.webhookBasedNode.formTrigger.serviceHint')
|
||||
: this.$locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
|
||||
switch (this.nodeType?.name) {
|
||||
case CHAT_TRIGGER_NODE_TYPE:
|
||||
return this.$locale.baseText('ndv.trigger.webhookBasedNode.chatTrigger.serviceHint');
|
||||
case FORM_TRIGGER_NODE_TYPE:
|
||||
return this.$locale.baseText('ndv.trigger.webhookBasedNode.formTrigger.serviceHint');
|
||||
default:
|
||||
return this.$locale.baseText('ndv.trigger.webhookBasedNode.serviceHint', {
|
||||
interpolate: { service: this.serviceName },
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
header(): string {
|
||||
const serviceName = this.nodeType ? getTriggerNodeServiceName(this.nodeType) : '';
|
||||
|
@ -349,6 +398,15 @@ export default defineComponent({
|
|||
this.executionsHelpEventBus.emit('expand');
|
||||
}
|
||||
},
|
||||
openWebhookUrl() {
|
||||
this.$telemetry.track('User clicked ndv link', {
|
||||
workflow_id: this.workflowsStore.workflowId,
|
||||
session_id: this.sessionId,
|
||||
pane: 'input',
|
||||
type: 'open-chat',
|
||||
});
|
||||
window.open(this.webhookTestUrl, '_blank', 'noreferrer');
|
||||
},
|
||||
onLinkClick(e: MouseEvent) {
|
||||
if (!e.target) {
|
||||
return;
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
@keydown.stop
|
||||
>
|
||||
<template #content>
|
||||
<div v-loading="isLoading" class="workflow-lm-chat" data-test-id="workflow-lm-chat-dialog">
|
||||
<div class="workflow-lm-chat" data-test-id="workflow-lm-chat-dialog">
|
||||
<div class="messages ignore-key-press">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
|
@ -64,6 +64,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MessageTyping ref="messageContainer" v-if="isLoading" />
|
||||
</div>
|
||||
<div v-if="node" class="logs-wrapper" data-test-id="lm-chat-logs">
|
||||
<n8n-text class="logs-title" tag="p" size="large">{{
|
||||
|
@ -128,6 +129,7 @@ import {
|
|||
AI_CODE_NODE_TYPE,
|
||||
AI_SUBCATEGORY,
|
||||
CHAT_EMBED_MODAL_KEY,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
VIEWS,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
|
@ -137,13 +139,17 @@ import { workflowRun } from '@/mixins/workflowRun';
|
|||
import { get, last } from 'lodash-es';
|
||||
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import type { IDataObject, INodeType, INode, ITaskData } from 'n8n-workflow';
|
||||
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { INodeUi, IUser } from '@/Interface';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import MessageTyping from '@n8n/chat/components/MessageTyping.vue';
|
||||
|
||||
const RunDataAi = defineAsyncComponent(async () => import('@/components/RunDataAi/RunDataAi.vue'));
|
||||
|
||||
interface ChatMessage {
|
||||
|
@ -167,6 +173,7 @@ export default defineComponent({
|
|||
name: 'WorkflowLMChat',
|
||||
components: {
|
||||
Modal,
|
||||
MessageTyping,
|
||||
RunDataAi,
|
||||
},
|
||||
mixins: [workflowRun],
|
||||
|
@ -246,18 +253,17 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
setConnectedNode() {
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
const triggerNode = workflow.queryNodes(
|
||||
(nodeType: INodeType) => nodeType.description.name === MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
);
|
||||
const triggerNode = this.getTriggerNode();
|
||||
|
||||
if (!triggerNode.length) {
|
||||
if (!triggerNode) {
|
||||
this.showError(
|
||||
new Error('Chat Trigger Node could not be found!'),
|
||||
'Trigger Node not found',
|
||||
);
|
||||
return;
|
||||
}
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
|
||||
const chatNode = this.workflowsStore.getNodes().find((node: INodeUi): boolean => {
|
||||
const nodeType = this.nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
if (!nodeType) return false;
|
||||
|
@ -288,7 +294,7 @@ export default defineComponent({
|
|||
|
||||
const parentNodes = workflow.getParentNodes(node.name);
|
||||
const isChatChild = parentNodes.some(
|
||||
(parentNodeName) => parentNodeName === triggerNode[0].name,
|
||||
(parentNodeName) => parentNodeName === triggerNode.name,
|
||||
);
|
||||
|
||||
return Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
||||
|
@ -311,7 +317,7 @@ export default defineComponent({
|
|||
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
const connectedMemoryInputs =
|
||||
workflow.connectionsByDestinationNode[this.connectedNode.name]?.memory;
|
||||
workflow.connectionsByDestinationNode[this.connectedNode.name][NodeConnectionType.AiMemory];
|
||||
if (!connectedMemoryInputs) return [];
|
||||
|
||||
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
|
||||
|
@ -330,9 +336,9 @@ export default defineComponent({
|
|||
action: string;
|
||||
chatHistory?: unknown[];
|
||||
response?: {
|
||||
chat_history?: unknown[];
|
||||
sessionId?: unknown[];
|
||||
};
|
||||
} => get(data, 'data.memory.0.0.json')!,
|
||||
} => get(data, ['data', NodeConnectionType.AiMemory, 0, 0, 'json'])!,
|
||||
)
|
||||
?.find((data) =>
|
||||
['chatHistory', 'loadMemoryVariables'].includes(data?.action) ? data : undefined,
|
||||
|
@ -342,12 +348,12 @@ export default defineComponent({
|
|||
if (memoryOutputData?.chatHistory) {
|
||||
chatHistory = memoryOutputData?.chatHistory as LangChainMessage[];
|
||||
} else if (memoryOutputData?.response) {
|
||||
chatHistory = memoryOutputData?.response.chat_history as LangChainMessage[];
|
||||
chatHistory = memoryOutputData?.response.sessionId as LangChainMessage[];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
return chatHistory.map((message) => {
|
||||
return (chatHistory || []).map((message) => {
|
||||
return {
|
||||
text: message.kwargs.content,
|
||||
sender: last(message.id) === 'HumanMessage' ? 'user' : 'bot',
|
||||
|
@ -382,8 +388,8 @@ export default defineComponent({
|
|||
|
||||
getTriggerNode(): INode | null {
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
const triggerNode = workflow.queryNodes(
|
||||
(nodeType: INodeType) => nodeType.description.name === MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
const triggerNode = workflow.queryNodes((nodeType: INodeType) =>
|
||||
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
|
||||
);
|
||||
|
||||
if (!triggerNode.length) {
|
||||
|
@ -403,7 +409,16 @@ export default defineComponent({
|
|||
return;
|
||||
}
|
||||
|
||||
const inputKey = triggerNode.typeVersion < 1.1 ? 'input' : 'chat_input';
|
||||
let inputKey = 'chatInput';
|
||||
if (triggerNode.type === MANUAL_CHAT_TRIGGER_NODE_TYPE && triggerNode.typeVersion < 1.1) {
|
||||
inputKey = 'input';
|
||||
}
|
||||
if (triggerNode.type === CHAT_TRIGGER_NODE_TYPE) {
|
||||
inputKey = 'chatInput';
|
||||
}
|
||||
|
||||
const usersStore = useUsersStore();
|
||||
const currentUser = usersStore.currentUser ?? ({} as IUser);
|
||||
|
||||
const nodeData: ITaskData = {
|
||||
startTime: new Date().getTime(),
|
||||
|
@ -414,6 +429,8 @@ export default defineComponent({
|
|||
[
|
||||
{
|
||||
json: {
|
||||
sessionId: `test-${currentUser.id || 'unknown'}`,
|
||||
action: 'sendMessage',
|
||||
[inputKey]: message,
|
||||
},
|
||||
},
|
||||
|
@ -549,13 +566,18 @@ export default defineComponent({
|
|||
padding-top: 1.5em;
|
||||
margin-right: 1em;
|
||||
|
||||
.chat-message {
|
||||
float: left;
|
||||
margin: var(--spacing-2xs) var(--spacing-s);
|
||||
}
|
||||
|
||||
.message {
|
||||
float: left;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.content {
|
||||
border-radius: var(--border-radius-large);
|
||||
border-radius: var(--border-radius-base);
|
||||
line-height: 1.5;
|
||||
margin: var(--spacing-2xs) var(--spacing-s);
|
||||
max-width: 75%;
|
||||
|
@ -565,8 +587,9 @@ export default defineComponent({
|
|||
|
||||
&.bot {
|
||||
background-color: var(--color-lm-chat-bot-background);
|
||||
border: 1px solid var(--color-lm-chat-bot-border);
|
||||
color: var(--color-lm-chat-bot-color);
|
||||
float: left;
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
.message-options {
|
||||
left: 1.5em;
|
||||
|
@ -575,9 +598,10 @@ export default defineComponent({
|
|||
|
||||
&.user {
|
||||
background-color: var(--color-lm-chat-user-background);
|
||||
border: 1px solid var(--color-lm-chat-user-border);
|
||||
color: var(--color-lm-chat-user-color);
|
||||
float: right;
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
.message-options {
|
||||
right: 1.5em;
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import WorkflowLMChatModal from '@/components/WorkflowLMChat.vue';
|
||||
import {
|
||||
AGENT_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
import { AGENT_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE, WORKFLOW_LM_CHAT_MODAL_KEY } from '@/constants';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { fireEvent, waitFor } from '@testing-library/vue';
|
||||
import { uuid } from '@jsplumb/util';
|
||||
|
@ -32,7 +28,7 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
|
|||
name: 'Test Workflow',
|
||||
connections: withConnections
|
||||
? {
|
||||
'On new manual Chat Message': {
|
||||
'Chat Trigger': {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
|
@ -48,8 +44,8 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
|
|||
active: true,
|
||||
nodes: [
|
||||
createTestNode({
|
||||
name: 'On new manual Chat Message',
|
||||
type: MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
name: 'Chat Trigger',
|
||||
type: CHAT_TRIGGER_NODE_TYPE,
|
||||
}),
|
||||
...(withAgentNode
|
||||
? [
|
||||
|
@ -71,7 +67,7 @@ async function createPiniaWithAINodes(options = { withConnections: true, withAge
|
|||
|
||||
nodeTypesStore.setNodeTypes(
|
||||
mockNodeTypesToArray({
|
||||
[MANUAL_CHAT_TRIGGER_NODE_TYPE]: testingNodeTypes[MANUAL_CHAT_TRIGGER_NODE_TYPE],
|
||||
[CHAT_TRIGGER_NODE_TYPE]: testingNodeTypes[CHAT_TRIGGER_NODE_TYPE],
|
||||
[AGENT_NODE_TYPE]: testingNodeTypes[AGENT_NODE_TYPE],
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -128,6 +128,7 @@ export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
|||
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
||||
export const MANUAL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.manualTrigger';
|
||||
export const MANUAL_CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.manualChatTrigger';
|
||||
export const CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.chatTrigger';
|
||||
export const AGENT_NODE_TYPE = '@n8n/n8n-nodes-langchain.agent';
|
||||
export const OPEN_AI_ASSISTANT_NODE_TYPE = '@n8n/n8n-nodes-langchain.openAiAssistant';
|
||||
export const BASIC_CHAIN_NODE_TYPE = '@n8n/n8n-nodes-langchain.chainLlm';
|
||||
|
@ -200,7 +201,13 @@ export const NODES_USING_CODE_NODE_EDITOR = [CODE_NODE_TYPE, AI_CODE_NODE_TYPE];
|
|||
|
||||
export const PIN_DATA_NODE_TYPES_DENYLIST = [SPLIT_IN_BATCHES_NODE_TYPE, STICKY_NODE_TYPE];
|
||||
|
||||
export const OPEN_URL_PANEL_TRIGGER_NODE_TYPES = [WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE];
|
||||
export const OPEN_URL_PANEL_TRIGGER_NODE_TYPES = [
|
||||
WEBHOOK_NODE_TYPE,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
];
|
||||
|
||||
export const PRODUCTION_ONLY_TRIGGER_NODE_TYPES = [CHAT_TRIGGER_NODE_TYPE];
|
||||
|
||||
// Node creator
|
||||
export const NODE_CREATOR_OPEN_SOURCES: Record<
|
||||
|
@ -614,6 +621,7 @@ export const KEEP_AUTH_IN_NDV_FOR_NODES = [
|
|||
WEBHOOK_NODE_TYPE,
|
||||
WAIT_NODE_TYPE,
|
||||
DISCORD_NODE_TYPE,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
];
|
||||
export const MAIN_AUTH_FIELD_NAME = 'authentication';
|
||||
export const NODE_RESOURCE_FIELD_NAME = 'resource';
|
||||
|
|
|
@ -168,6 +168,13 @@ export function resolveParameter(
|
|||
nodeConnection,
|
||||
);
|
||||
|
||||
if (_connectionInputData === null && contextNode && activeNode?.name !== contextNode.name) {
|
||||
// For Sub-Nodes connected to Trigger-Nodes use the data of the root-node
|
||||
// (Gets for example used by the Memory connected to the Chat-Trigger-Node)
|
||||
const _executeData = executeData([contextNode.name], contextNode.name, inputName, 0);
|
||||
_connectionInputData = get(_executeData, ['data', inputName, 0], null);
|
||||
}
|
||||
|
||||
let runExecutionData: IRunExecutionData;
|
||||
if (!executionData?.data) {
|
||||
runExecutionData = {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@import '@n8n/chat/css';
|
||||
@import 'styles/plugins';
|
||||
|
||||
:root {
|
||||
|
@ -170,6 +171,8 @@
|
|||
var(--node-type-background-l)
|
||||
);
|
||||
--node-error-output-color: #991818;
|
||||
|
||||
--chat--spacing: var(--spacing-s);
|
||||
}
|
||||
|
||||
.clickable {
|
||||
|
|
|
@ -161,6 +161,7 @@
|
|||
"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.chatTriggerNode": "You can use a Chat Trigger Node to embed the chat widget directly into n8n.",
|
||||
"chatEmbed.url": "https://www.npmjs.com/package/{'@'}n8n/chat",
|
||||
"codeEdit.edit": "Edit",
|
||||
"codeNodeEditor.askAi": "✨ Ask AI",
|
||||
|
@ -759,6 +760,7 @@
|
|||
"ndv.execute.fetchEvent": "Fetch Test Event",
|
||||
"ndv.execute.fixPrevious": "Fix previous node first",
|
||||
"ndv.execute.listenForTestEvent": "Listen For Test Event",
|
||||
"ndv.execute.testChat": "Test Chat",
|
||||
"ndv.execute.testStep": "Test Step",
|
||||
"ndv.execute.stopListening": "Stop Listening",
|
||||
"ndv.execute.nodeIsDisabled": "Enable node to execute",
|
||||
|
@ -1116,18 +1118,24 @@
|
|||
"contextMenu.changeColor": "Change color",
|
||||
"nodeWebhooks.clickToCopyWebhookUrls": "Click to copy webhook URLs",
|
||||
"nodeWebhooks.clickToCopyWebhookUrls.formTrigger": "Click to copy Form URL",
|
||||
"nodeWebhooks.clickToCopyWebhookUrls.chatTrigger": "Click to copy Chat URL",
|
||||
"nodeWebhooks.clickToDisplayWebhookUrls": "Click to display webhook URLs",
|
||||
"nodeWebhooks.clickToDisplayWebhookUrls.formTrigger": "Click to display Form URL",
|
||||
"nodeWebhooks.clickToDisplayWebhookUrls.chatTrigger": "Click to display Chat URL",
|
||||
"nodeWebhooks.clickToHideWebhookUrls": "Click to hide webhook URLs",
|
||||
"nodeWebhooks.clickToHideWebhookUrls.formTrigger": "Click to hide Form URL",
|
||||
"nodeWebhooks.clickToHideWebhookUrls.chatTrigger": "Click to hide Chat URL",
|
||||
"nodeWebhooks.invalidExpression": "[INVALID EXPRESSION]",
|
||||
"nodeWebhooks.productionUrl": "Production URL",
|
||||
"nodeWebhooks.showMessage.title": "URL copied",
|
||||
"nodeWebhooks.showMessage.title.formTrigger": "Form URL copied",
|
||||
"nodeWebhooks.showMessage.title.chatTrigger": "Chat URL copied",
|
||||
"nodeWebhooks.showMessage.message.formTrigger": "Form submissions made via this URL will trigger the workflow when it's activated",
|
||||
"nodeWebhooks.showMessage.message.chatTrigger": "Chat submissions made via this URL will trigger the workflow when it's activated",
|
||||
"nodeWebhooks.testUrl": "Test URL",
|
||||
"nodeWebhooks.webhookUrls": "Webhook URLs",
|
||||
"nodeWebhooks.webhookUrls.formTrigger": "Form URLs",
|
||||
"nodeWebhooks.webhookUrls.chatTrigger": "Chat URL",
|
||||
"onboardingCallSignupModal.title": "Your onboarding session",
|
||||
"onboardingCallSignupModal.description": "Pop in your email and we'll send you some scheduling options",
|
||||
"onboardingCallSignupModal.emailInput.placeholder": "Your work email",
|
||||
|
@ -1803,10 +1811,12 @@
|
|||
"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.",
|
||||
"ndv.trigger.webhookBasedNode.executionsHelp.active": "<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>Your workflow will also execute automatically</b>, since it's activated. Every time there’s a matching event in {service}, this node will trigger an execution. These executions will show up in the <a data-key=\"executions\">executions list</a>, but not in the editor. ",
|
||||
"ndv.trigger.webhookNode.listening": "Listening for test event",
|
||||
"ndv.trigger.chatTrigger.openChat": "Open Chat Window",
|
||||
"ndv.trigger.webhookNode.formTrigger.listening": "Listening for a test form submission",
|
||||
"ndv.trigger.webhookBasedNode.listening": "Listening for your trigger event",
|
||||
"ndv.trigger.webhookNode.requestHint": "Make a {type} request to:",
|
||||
"ndv.trigger.webhookBasedNode.serviceHint": "Go to {service} and create an event",
|
||||
"ndv.trigger.webhookBasedNode.chatTrigger.serviceHint": "Send a message in the chat",
|
||||
"ndv.trigger.webhookBasedNode.formTrigger.serviceHint": "Submit the test form that just opened in a new tab",
|
||||
"ndv.trigger.webhookBasedNode.activationHint.inactive": "Once you’ve finished building your workflow, <a data-key=\"activate\">activate it</a> to have it also listen continuously (you just won’t see those executions here).",
|
||||
"ndv.trigger.webhookBasedNode.activationHint.active": "This node will also trigger automatically on new {service} events (but those executions won’t show up here).",
|
||||
|
|
|
@ -7,7 +7,7 @@ export const guestMiddleware: RouterMiddleware<GuestPermissionOptions> = async (
|
|||
const valid = isGuest();
|
||||
if (!valid) {
|
||||
const redirect = to.query.redirect as string;
|
||||
if (redirect && redirect.startsWith('/')) {
|
||||
if (redirect && (redirect.startsWith('/') || redirect.startsWith(window.location.origin))) {
|
||||
return next(redirect);
|
||||
}
|
||||
|
||||
|
|
|
@ -237,6 +237,7 @@ import {
|
|||
EnterpriseEditionFeature,
|
||||
REGULAR_NODE_CREATOR_VIEW,
|
||||
NODE_CREATOR_OPEN_SOURCES,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
AI_NODE_CREATOR_VIEW,
|
||||
|
@ -692,12 +693,14 @@ export default defineComponent({
|
|||
containsTrigger(): boolean {
|
||||
return this.triggerNodes.length > 0;
|
||||
},
|
||||
isManualChatOnly(): boolean {
|
||||
return this.containsChatNodes && this.triggerNodes.length === 1;
|
||||
},
|
||||
containsChatNodes(): boolean {
|
||||
return !!this.nodes.find(
|
||||
(node) => node.type === MANUAL_CHAT_TRIGGER_NODE_TYPE && node.disabled !== true,
|
||||
return (
|
||||
!this.executionWaitingForWebhook &&
|
||||
!!this.nodes.find(
|
||||
(node) =>
|
||||
[MANUAL_CHAT_TRIGGER_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE].includes(node.type) &&
|
||||
node.disabled !== true,
|
||||
)
|
||||
);
|
||||
},
|
||||
isExecutionDisabled(): boolean {
|
||||
|
|
|
@ -116,7 +116,7 @@ export default defineComponent({
|
|||
},
|
||||
isRedirectSafe() {
|
||||
const redirect = this.getRedirectQueryParameter();
|
||||
return redirect.startsWith('/');
|
||||
return redirect.startsWith('/') || redirect.startsWith(window.location.origin);
|
||||
},
|
||||
getRedirectQueryParameter() {
|
||||
let redirect = '';
|
||||
|
@ -152,6 +152,11 @@ export default defineComponent({
|
|||
|
||||
if (this.isRedirectSafe()) {
|
||||
const redirect = this.getRedirectQueryParameter();
|
||||
if (redirect.startsWith('http')) {
|
||||
window.location.href = redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
void this.$router.push(redirect);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
"types": ["vitest/globals"],
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"n8n-design-system/*": ["../design-system/src/*"]
|
||||
"n8n-design-system/*": ["../design-system/src/*"],
|
||||
"@n8n/chat/*": ["../@n8n/chat/src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
|
||||
// TODO: remove all options below this line
|
||||
|
|
|
@ -5,14 +5,16 @@ import { sentryVitePlugin } from '@sentry/vite-plugin';
|
|||
|
||||
import packageJSON from './package.json';
|
||||
import { vitestConfig } from '../design-system/vite.config.mts';
|
||||
import icons from 'unplugin-icons/vite';
|
||||
|
||||
const vendorChunks = ['vue', 'vue-router'];
|
||||
const n8nChunks = ['n8n-workflow', 'n8n-design-system'];
|
||||
const n8nChunks = ['n8n-workflow', 'n8n-design-system', '@n8n/chat'];
|
||||
const ignoreChunks = [
|
||||
'@fontsource/open-sans',
|
||||
'@vueuse/components',
|
||||
// TODO: remove this. It's currently required by xml2js in NodeErrors
|
||||
'stream-browserify',
|
||||
'vue-markdown-render',
|
||||
];
|
||||
|
||||
const isScopedPackageToIgnore = (str: string) => /@codemirror\//.test(str);
|
||||
|
@ -49,6 +51,14 @@ const alias = [
|
|||
find: /^n8n-design-system\//,
|
||||
replacement: resolve(__dirname, '..', 'design-system', 'src') + '/',
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/chat$/,
|
||||
replacement: resolve(__dirname, '..', '@n8n', 'chat', 'src', 'index.ts'),
|
||||
},
|
||||
{
|
||||
find: /^@n8n\/chat\//,
|
||||
replacement: resolve(__dirname, '..', '@n8n', 'chat', 'src') + '/',
|
||||
},
|
||||
...['orderBy', 'camelCase', 'cloneDeep', 'startCase'].map((name) => ({
|
||||
find: new RegExp(`^lodash.${name}$`, 'i'),
|
||||
replacement: `lodash-es/${name}`,
|
||||
|
@ -59,7 +69,12 @@ const alias = [
|
|||
},
|
||||
];
|
||||
|
||||
const plugins = [vue()];
|
||||
const plugins = [
|
||||
icons({
|
||||
compiler: 'vue3',
|
||||
}),
|
||||
vue()
|
||||
];
|
||||
|
||||
const { SENTRY_AUTH_TOKEN: authToken, RELEASE: release } = process.env;
|
||||
if (release && authToken) {
|
||||
|
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="101" height="22" fill="none"><path fill="#292929" d="M10.058 20.817C4.321 20.817 0 16.277 0 10.67 0 5.046 4.1.468 10.058.468c3.163 0 5.351.971 7.061 3.195l-2.758 2.299c-1.158-1.234-2.556-1.85-4.303-1.85-3.88 0-6.013 2.97-6.013 6.558s2.336 6.503 6.013 6.503c1.729 0 3.2-.616 4.358-1.85l2.721 2.392c-1.636 2.13-3.88 3.102-7.079 3.102M29.016 5.886h3.714v14.575h-3.714v-2.13c-.772 1.514-2.06 2.523-4.523 2.523-3.935 0-7.08-3.42-7.08-7.624 0-4.205 3.145-7.624 7.08-7.624 2.445 0 3.75 1.009 4.523 2.522zm.11 7.344c0-2.28-1.563-4.167-4.027-4.167-2.372 0-3.916 1.906-3.916 4.167 0 2.205 1.544 4.167 3.916 4.167 2.446 0 4.027-1.906 4.027-4.167M35.36 0h3.714v20.443H35.36zm5.37 18.518c0-1.196.955-2.205 2.26-2.205a2.18 2.18 0 0 1 2.226 2.205c0 1.233-.938 2.242-2.225 2.242a2.23 2.23 0 0 1-2.262-2.242m18.701-.411c-1.38 1.681-3.476 2.747-5.958 2.747-4.432 0-7.686-3.42-7.686-7.624 0-4.205 3.254-7.624 7.686-7.624 2.39 0 4.468 1.009 5.847 2.597l-2.868 2.41c-.717-.896-1.655-1.569-2.98-1.569-2.371 0-3.916 1.906-3.916 4.167s1.545 4.167 3.917 4.167c1.434 0 2.427-.747 3.163-1.757zm.312-4.877c0-4.205 3.255-7.624 7.686-7.624 4.432 0 7.686 3.42 7.686 7.624s-3.254 7.624-7.686 7.624c-4.431-.02-7.686-3.42-7.686-7.624m11.603 0c0-2.28-1.545-4.167-3.917-4.167-2.372-.019-3.916 1.887-3.916 4.167 0 2.26 1.544 4.167 3.916 4.167s3.917-1.906 3.917-4.167m28.887-1.682v8.895h-3.714v-7.98c0-2.522-1.177-3.606-2.942-3.606-1.655 0-2.832.823-2.832 3.607v7.979H87.03v-7.98c0-2.522-1.195-3.606-2.942-3.606-1.655 0-3.108.823-3.108 3.607v7.979h-3.714V5.868h3.714v2.018c.772-1.57 2.17-2.355 4.321-2.355 2.041 0 3.751 1.01 4.69 2.71.937-1.738 2.316-2.71 4.817-2.71 3.052.019 5.424 2.336 5.424 6.017"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="101" height="22" fill="none"><path fill="#292929" d="M10.058 20.817C4.321 20.817 0 16.277 0 10.67 0 5.046 4.1.468 10.058.468c3.163 0 5.351.971 7.061 3.195l-2.758 2.299c-1.158-1.234-2.556-1.85-4.303-1.85-3.88 0-6.013 2.97-6.013 6.558s2.336 6.503 6.013 6.503c1.729 0 3.2-.616 4.358-1.85l2.721 2.392c-1.636 2.13-3.88 3.102-7.079 3.102M29.016 5.886h3.714v14.575h-3.714v-2.13c-.772 1.514-2.06 2.523-4.523 2.523-3.935 0-7.08-3.42-7.08-7.624 0-4.205 3.145-7.624 7.08-7.624 2.445 0 3.75 1.009 4.523 2.522zm.11 7.344c0-2.28-1.563-4.167-4.027-4.167-2.372 0-3.916 1.906-3.916 4.167 0 2.205 1.544 4.167 3.916 4.167 2.446 0 4.027-1.906 4.027-4.167M35.36 0h3.714v20.443H35.36zm5.37 18.518c0-1.196.955-2.205 2.26-2.205a2.18 2.18 0 0 1 2.226 2.205c0 1.233-.938 2.242-2.225 2.242a2.23 2.23 0 0 1-2.262-2.242m18.701-.411c-1.38 1.681-3.476 2.747-5.958 2.747-4.432 0-7.686-3.42-7.686-7.624 0-4.205 3.254-7.624 7.686-7.624 2.39 0 4.468 1.009 5.847 2.597l-2.868 2.41c-.717-.896-1.655-1.569-2.98-1.569-2.371 0-3.916 1.906-3.916 4.167s1.545 4.167 3.917 4.167c1.434 0 2.427-.747 3.163-1.757zm.312-4.877c0-4.205 3.255-7.624 7.686-7.624 4.432 0 7.686 3.42 7.686 7.624s-3.254 7.624-7.686 7.624c-4.431-.02-7.686-3.42-7.686-7.624m11.603 0c0-2.28-1.545-4.167-3.917-4.167-2.372-.019-3.916 1.887-3.916 4.167 0 2.26 1.544 4.167 3.916 4.167s3.917-1.906 3.917-4.167m28.887-1.682v8.895h-3.714v-7.98c0-2.522-1.177-3.606-2.942-3.606-1.655 0-2.832.823-2.832 3.607v7.979H87.03v-7.98c0-2.522-1.195-3.606-2.942-3.606-1.655 0-3.108.823-3.108 3.607v7.979h-3.714V5.868h3.714v2.018c.772-1.57 2.17-2.355 4.321-2.355 2.041 0 3.751 1.01 4.69 2.71.937-1.738 2.316-2.71 4.817-2.71 3.052.019 5.424 2.336 5.424 6.017"/></svg>
|
||||
|
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
@ -465,6 +465,7 @@ export interface IGetExecuteWebhookFunctions {
|
|||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
mode: WorkflowExecuteMode,
|
||||
webhookData: IWebhookData,
|
||||
closeFunctions: CloseFunction[],
|
||||
): IWebhookFunctions;
|
||||
}
|
||||
|
||||
|
@ -926,6 +927,11 @@ export interface IHookFunctions
|
|||
export interface IWebhookFunctions extends FunctionsBaseWithRequiredKeys<'getMode'> {
|
||||
getBodyData(): IDataObject;
|
||||
getHeaderData(): IncomingHttpHeaders;
|
||||
getInputConnectionData(
|
||||
inputName: ConnectionTypes,
|
||||
itemIndex: number,
|
||||
inputIndex?: number,
|
||||
): Promise<unknown>;
|
||||
getNodeParameter(
|
||||
parameterName: string,
|
||||
fallbackValue?: any,
|
||||
|
@ -1333,7 +1339,7 @@ export interface SupplyData {
|
|||
|
||||
export interface INodeType {
|
||||
description: INodeTypeDescription;
|
||||
supplyData?(this: IExecuteFunctions, itemIndex: number): Promise<SupplyData>;
|
||||
supplyData?(this: IAllExecuteFunctions, itemIndex: number): Promise<SupplyData>;
|
||||
execute?(
|
||||
this: IExecuteFunctions,
|
||||
): Promise<INodeExecutionData[][] | NodeExecutionWithMetadata[][] | null>;
|
||||
|
@ -1633,21 +1639,24 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
|
|||
webhooks?: IWebhookDescription[];
|
||||
translation?: { [key: string]: object };
|
||||
mockManualExecution?: true;
|
||||
triggerPanel?: {
|
||||
header?: string;
|
||||
executionsHelp?:
|
||||
| string
|
||||
| {
|
||||
active: string;
|
||||
inactive: string;
|
||||
};
|
||||
activationHint?:
|
||||
| string
|
||||
| {
|
||||
active: string;
|
||||
inactive: string;
|
||||
};
|
||||
};
|
||||
triggerPanel?:
|
||||
| {
|
||||
hideContent?: boolean | string;
|
||||
header?: string;
|
||||
executionsHelp?:
|
||||
| string
|
||||
| {
|
||||
active: string;
|
||||
inactive: string;
|
||||
};
|
||||
activationHint?:
|
||||
| string
|
||||
| {
|
||||
active: string;
|
||||
inactive: string;
|
||||
};
|
||||
}
|
||||
| boolean;
|
||||
extendsCredential?: string;
|
||||
__loadOptionsMethods?: string[]; // only for validation during build
|
||||
}
|
||||
|
@ -1681,8 +1690,8 @@ export interface IWebhookDescription {
|
|||
restartWebhook?: boolean;
|
||||
isForm?: boolean;
|
||||
hasLifecycleMethods?: boolean; // set automatically by generate-ui-types
|
||||
ndvHideUrl?: boolean; // If true the webhook will not be displayed in the editor
|
||||
ndvHideMethod?: boolean; // If true the method will not be displayed in the editor
|
||||
ndvHideUrl?: string | boolean; // If true the webhook will not be displayed in the editor
|
||||
ndvHideMethod?: string | boolean; // If true the method will not be displayed in the editor
|
||||
}
|
||||
|
||||
export interface ProxyInput {
|
||||
|
|
|
@ -1198,12 +1198,15 @@ export class Workflow {
|
|||
});
|
||||
}
|
||||
|
||||
const closeFunctions: CloseFunction[] = [];
|
||||
|
||||
const context = nodeExecuteFunctions.getExecuteWebhookFunctions(
|
||||
this,
|
||||
node,
|
||||
additionalData,
|
||||
mode,
|
||||
webhookData,
|
||||
closeFunctions,
|
||||
);
|
||||
return nodeType instanceof Node ? nodeType.webhook(context) : nodeType.webhook.call(context);
|
||||
}
|
||||
|
|
776
pnpm-lock.yaml
776
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue