diff --git a/cypress/composables/modals/chat-modal.ts b/cypress/composables/modals/chat-modal.ts index 31e139c93e6bc..254d811a18405 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 421d2cf8016b3..24d73fb227d25 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 ca93cdb240d1c..043039753f87a 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 04b6d61b65034..b8b046c8982d5 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 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..997954b6c80c0 --- /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 d393ab90ed6f8..04153960825b6 100644 --- a/packages/@n8n/chat/src/components/Input.vue +++ b/packages/@n8n/chat/src/components/Input.vue @@ -1,31 +1,102 @@