diff --git a/cypress/constants.ts b/cypress/constants.ts index 1f5793e0e3..d37dd37574 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -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'; diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 08d3b04f46..90ea78c1fb 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -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 diff --git a/packages/@n8n/chat/README.md b/packages/@n8n/chat/README.md index 65ef9b196e..2273d7d5c8 100644 --- a/packages/@n8n/chat/README.md +++ b/packages/@n8n/chat/README.md @@ -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'` diff --git a/packages/@n8n/chat/build.config.js b/packages/@n8n/chat/build.config.js new file mode 100644 index 0000000000..9d5910dbe7 --- /dev/null +++ b/packages/@n8n/chat/build.config.js @@ -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, +}); diff --git a/packages/@n8n/chat/package.json b/packages/@n8n/chat/package.json index 0c324030c8..9408ae572d 100644 --- a/packages/@n8n/chat/package.json +++ b/packages/@n8n/chat/package.json @@ -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" }, diff --git a/packages/@n8n/chat/resources/images/fullscreen.png b/packages/@n8n/chat/resources/images/fullscreen.png new file mode 100644 index 0000000000..4c3f5aed4e Binary files /dev/null and b/packages/@n8n/chat/resources/images/fullscreen.png differ diff --git a/packages/@n8n/chat/resources/images/windowed.png b/packages/@n8n/chat/resources/images/windowed.png new file mode 100644 index 0000000000..1a598c823e Binary files /dev/null and b/packages/@n8n/chat/resources/images/windowed.png differ diff --git a/packages/@n8n/chat/resources/workflow-manual.json b/packages/@n8n/chat/resources/workflow-manual.json new file mode 100644 index 0000000000..a21028b9fb --- /dev/null +++ b/packages/@n8n/chat/resources/workflow-manual.json @@ -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": [] +} \ No newline at end of file diff --git a/packages/@n8n/chat/resources/workflow.json b/packages/@n8n/chat/resources/workflow.json index f063f4d992..1bf4be681b 100644 --- a/packages/@n8n/chat/resources/workflow.json +++ b/packages/@n8n/chat/resources/workflow.json @@ -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" }, diff --git a/packages/@n8n/chat/scripts/postbuild.js b/packages/@n8n/chat/scripts/postbuild.js index f60ef525d0..2ce6a4e8c2 100644 --- a/packages/@n8n/chat/scripts/postbuild.js +++ b/packages/@n8n/chat/scripts/postbuild.js @@ -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); diff --git a/packages/@n8n/chat/src/App.vue b/packages/@n8n/chat/src/App.vue index 6a00331316..d2afb274cd 100644 --- a/packages/@n8n/chat/src/App.vue +++ b/packages/@n8n/chat/src/App.vue @@ -1,10 +1,10 @@