From df783151b86e2db3e325d3b9d85f4abb71d3d246 Mon Sep 17 00:00:00 2001 From: oleg Date: Tue, 9 Jul 2024 13:45:41 +0200 Subject: [PATCH] feat(Chat Trigger Node): Add support for file uploads & harmonize public and development chat (#9802) Signed-off-by: Oleg Ivaniv --- cypress/composables/modals/chat-modal.ts | 6 +- packages/@n8n/chat/package.json | 1 + .../@n8n/chat/src/__stories__/App.stories.ts | 12 + packages/@n8n/chat/src/api/generic.ts | 40 +- packages/@n8n/chat/src/api/message.ts | 24 +- .../@n8n/chat/src/components/ChatFile.vue | 92 +++ packages/@n8n/chat/src/components/Input.vue | 261 +++++-- packages/@n8n/chat/src/components/Message.vue | 95 ++- .../chat/src/components/MessageTyping.vue | 10 +- .../@n8n/chat/src/components/MessagesList.vue | 26 +- packages/@n8n/chat/src/css/index.scss | 1 + packages/@n8n/chat/src/css/markdown.scss | 627 ++++++++++++++++ packages/@n8n/chat/src/plugins/chat.ts | 4 +- packages/@n8n/chat/src/types/chat.ts | 2 +- packages/@n8n/chat/src/types/messages.ts | 1 + packages/@n8n/chat/src/types/options.ts | 3 + .../Agent/agents/ToolsAgent/description.ts | 8 + .../agents/Agent/agents/ToolsAgent/execute.ts | 50 +- .../DocumentDefaultDataLoader.node.ts | 27 + .../trigger/ChatTrigger/ChatTrigger.node.ts | 179 ++++- .../nodes/trigger/ChatTrigger/templates.ts | 8 + .../nodes-langchain/utils/N8nBinaryLoader.ts | 150 ++-- .../@n8n/nodes-langchain/utils/helpers.ts | 19 +- packages/editor-ui/package.json | 4 +- packages/editor-ui/src/components/Modals.vue | 2 +- .../src/components/WorkflowLMChat.vue | 695 ------------------ .../WorkflowLMChat/MessageOptionAction.vue | 31 + .../WorkflowLMChat/MessageOptionTooltip.vue | 15 + .../WorkflowLMChat/WorkflowLMChat.vue | 688 +++++++++++++++++ .../__tests__/WorkflowLMChatModal.test.ts | 47 +- .../__tests__/useClipboard.test.ts | 19 +- pnpm-lock.yaml | 102 +-- 32 files changed, 2309 insertions(+), 940 deletions(-) create mode 100644 packages/@n8n/chat/src/components/ChatFile.vue create mode 100644 packages/@n8n/chat/src/css/markdown.scss delete mode 100644 packages/editor-ui/src/components/WorkflowLMChat.vue create mode 100644 packages/editor-ui/src/components/WorkflowLMChat/MessageOptionAction.vue create mode 100644 packages/editor-ui/src/components/WorkflowLMChat/MessageOptionTooltip.vue create mode 100644 packages/editor-ui/src/components/WorkflowLMChat/WorkflowLMChat.vue diff --git a/cypress/composables/modals/chat-modal.ts b/cypress/composables/modals/chat-modal.ts index 31e139c93e..254d811a18 100644 --- a/cypress/composables/modals/chat-modal.ts +++ b/cypress/composables/modals/chat-modal.ts @@ -7,15 +7,15 @@ export function getManualChatModal() { } export function getManualChatInput() { - return cy.getByTestId('workflow-chat-input'); + return getManualChatModal().get('.chat-inputs textarea'); } export function getManualChatSendButton() { - return getManualChatModal().getByTestId('workflow-chat-send-button'); + return getManualChatModal().get('.chat-input-send-button'); } export function getManualChatMessages() { - return getManualChatModal().get('.messages .message'); + return getManualChatModal().get('.chat-messages-list .chat-message'); } export function getManualChatModalCloseButton() { diff --git a/packages/@n8n/chat/package.json b/packages/@n8n/chat/package.json index 421d2cf801..24d73fb227 100644 --- a/packages/@n8n/chat/package.json +++ b/packages/@n8n/chat/package.json @@ -36,6 +36,7 @@ } }, "dependencies": { + "@vueuse/core": "^10.11.0", "highlight.js": "^11.8.0", "markdown-it-link-attributes": "^4.0.1", "uuid": "^8.3.2", diff --git a/packages/@n8n/chat/src/__stories__/App.stories.ts b/packages/@n8n/chat/src/__stories__/App.stories.ts index ca93cdb240..043039753f 100644 --- a/packages/@n8n/chat/src/__stories__/App.stories.ts +++ b/packages/@n8n/chat/src/__stories__/App.stories.ts @@ -41,3 +41,15 @@ export const Windowed: Story = { mode: 'window', } satisfies Partial, }; + +export const WorkflowChat: Story = { + name: 'Workflow Chat', + args: { + webhookUrl: 'http://localhost:5678/webhook/ad324b56-3e40-4b27-874f-58d150504edc/chat', + mode: 'fullscreen', + allowedFilesMimeTypes: 'image/*,text/*,audio/*, application/pdf', + allowFileUploads: true, + showWelcomeScreen: false, + initialMessages: [], + } satisfies Partial, +}; diff --git a/packages/@n8n/chat/src/api/generic.ts b/packages/@n8n/chat/src/api/generic.ts index 04b6d61b65..b8b046c898 100644 --- a/packages/@n8n/chat/src/api/generic.ts +++ b/packages/@n8n/chat/src/api/generic.ts @@ -5,15 +5,23 @@ async function getAccessToken() { export async function authenticatedFetch(...args: Parameters): Promise { const accessToken = await getAccessToken(); + const body = args[1]?.body; + const headers: RequestInit['headers'] & { 'Content-Type'?: string } = { + ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}), + ...args[1]?.headers, + }; + + // Automatically set content type to application/json if body is FormData + if (body instanceof FormData) { + delete headers['Content-Type']; + } else { + headers['Content-Type'] = 'application/json'; + } const response = await fetch(args[0], { ...args[1], mode: 'cors', cache: 'no-cache', - headers: { - 'Content-Type': 'application/json', - ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}), - ...args[1]?.headers, - }, + headers, }); return (await response.json()) as T; @@ -37,6 +45,28 @@ export async function post(url: string, body: object = {}, options: RequestIn body: JSON.stringify(body), }); } +export async function postWithFiles( + url: string, + body: Record = {}, + files: File[] = [], + options: RequestInit = {}, +) { + const formData = new FormData(); + + for (const key in body) { + formData.append(key, body[key] as string); + } + + for (const file of files) { + formData.append('files', file); + } + + return await authenticatedFetch(url, { + ...options, + method: 'POST', + body: formData, + }); +} export async function put(url: string, body: object = {}, options: RequestInit = {}) { return await authenticatedFetch(url, { diff --git a/packages/@n8n/chat/src/api/message.ts b/packages/@n8n/chat/src/api/message.ts index 72f8e2fb27..b479dc51c7 100644 --- a/packages/@n8n/chat/src/api/message.ts +++ b/packages/@n8n/chat/src/api/message.ts @@ -1,4 +1,4 @@ -import { get, post } from '@n8n/chat/api/generic'; +import { get, post, postWithFiles } from '@n8n/chat/api/generic'; import type { ChatOptions, LoadPreviousSessionResponse, @@ -20,7 +20,27 @@ export async function loadPreviousSession(sessionId: string, options: ChatOption ); } -export async function sendMessage(message: string, sessionId: string, options: ChatOptions) { +export async function sendMessage( + message: string, + files: File[], + sessionId: string, + options: ChatOptions, +) { + if (files.length > 0) { + return await postWithFiles( + `${options.webhookUrl}`, + { + action: 'sendMessage', + [options.chatSessionKey as string]: sessionId, + [options.chatInputKey as string]: message, + ...(options.metadata ? { metadata: options.metadata } : {}), + }, + files, + { + headers: options.webhookConfig?.headers, + }, + ); + } const method = options.webhookConfig?.method === 'POST' ? post : get; return await method( `${options.webhookUrl}`, diff --git a/packages/@n8n/chat/src/components/ChatFile.vue b/packages/@n8n/chat/src/components/ChatFile.vue new file mode 100644 index 0000000000..997954b6c8 --- /dev/null +++ b/packages/@n8n/chat/src/components/ChatFile.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/packages/@n8n/chat/src/components/Input.vue b/packages/@n8n/chat/src/components/Input.vue index d393ab90ed..0415396082 100644 --- a/packages/@n8n/chat/src/components/Input.vue +++ b/packages/@n8n/chat/src/components/Input.vue @@ -1,31 +1,102 @@