From 214ccb80340c0eeaa5c16ecda2bddd863209fb2c Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Wed, 12 Jun 2024 08:48:33 +0200 Subject: [PATCH 01/40] WIP: Chat file uploads Signed-off-by: Oleg Ivaniv --- packages/@n8n/chat/package.json | 3 + .../@n8n/chat/src/__stories__/App.stories.ts | 12 ++ packages/@n8n/chat/src/api/generic.ts | 27 ++- packages/@n8n/chat/src/api/message.ts | 24 ++- .../@n8n/chat/src/components/ChatFile.vue | 77 ++++++++ packages/@n8n/chat/src/components/Input.vue | 148 ++++++++++---- packages/@n8n/chat/src/css/index.scss | 1 + packages/@n8n/chat/src/css/tooltips.scss | 30 +++ packages/@n8n/chat/src/index.ts | 18 ++ packages/@n8n/chat/src/main.scss | 1 + packages/@n8n/chat/src/plugins/chat.ts | 3 +- packages/@n8n/chat/src/types/chat.ts | 2 +- packages/@n8n/chat/src/types/options.ts | 2 + .../agents/ConversationalAgent/execute.ts | 42 ++-- .../agents/Agent/agents/ToolsAgent/execute.ts | 25 +++ .../trigger/ChatTrigger/ChatTrigger.node.ts | 185 +++++++++++++++--- .../nodes/trigger/ChatTrigger/templates.ts | 8 + pnpm-lock.yaml | 79 ++++++-- 18 files changed, 598 insertions(+), 89 deletions(-) create mode 100644 packages/@n8n/chat/src/components/ChatFile.vue create mode 100644 packages/@n8n/chat/src/css/tooltips.scss diff --git a/packages/@n8n/chat/package.json b/packages/@n8n/chat/package.json index b161ccd947102..a2b85c7fc080d 100644 --- a/packages/@n8n/chat/package.json +++ b/packages/@n8n/chat/package.json @@ -39,8 +39,11 @@ } }, "dependencies": { + "@vueuse/core": "^10.5.0", + "floating-vue": "^5.2.2", "highlight.js": "^11.8.0", "markdown-it-link-attributes": "^4.0.1", + "pretty-bytes": "5.6.0", "uuid": "^8.3.2", "vue": "^3.4.21", "vue-markdown-render": "^2.1.1" diff --git a/packages/@n8n/chat/src/__stories__/App.stories.ts b/packages/@n8n/chat/src/__stories__/App.stories.ts index ca93cdb240d1c..d211be4b73bff 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/d984fd87-fb24-478f-87c1-a2511f013c99/chat', + mode: 'fullscreen', + allowedFilesMimeTypes: ['image/*'], + 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 04b6d61b65034..9d0be888ea02f 100644 --- a/packages/@n8n/chat/src/api/generic.ts +++ b/packages/@n8n/chat/src/api/generic.ts @@ -10,7 +10,7 @@ export async function authenticatedFetch(...args: Parameters): mode: 'cors', cache: 'no-cache', headers: { - 'Content-Type': 'application/json', + // 'Content-Type': 'application/json', ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}), ...args[1]?.headers, }, @@ -37,6 +37,31 @@ 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, + headers: { + // 'Content-Type': 'multipart/form-data', + }, + }); +} 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 72f8e2fb2741c..b479dc51c7905 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 0000000000000..7c6932c3393f6 --- /dev/null +++ b/packages/@n8n/chat/src/components/ChatFile.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/packages/@n8n/chat/src/components/Input.vue b/packages/@n8n/chat/src/components/Input.vue index d393ab90ed6f8..4f4cc82927e35 100644 --- a/packages/@n8n/chat/src/components/Input.vue +++ b/packages/@n8n/chat/src/components/Input.vue @@ -1,22 +1,54 @@