Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Chat Trigger Node): Add support for file uploads & harmonize public and development chat #9802

Merged
merged 52 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
214ccb8
WIP: Chat file uploads
OlegIvaniv Jun 12, 2024
aa7f15e
File file reset
OlegIvaniv Jun 12, 2024
d520aed
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jun 12, 2024
a204a18
Implement allInputData binary mode
OlegIvaniv Jun 13, 2024
5a5d0e8
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jun 17, 2024
7946100
WIP: Implement file upload for Workflow LM Chat
OlegIvaniv Jun 18, 2024
8d293d3
Fix memory auto sessionId, markdown rendering, improve dark mode corr…
OlegIvaniv Jun 19, 2024
96f9f4a
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jun 19, 2024
1969984
Remove debugging console.logs
OlegIvaniv Jun 19, 2024
a9d5abb
Fix TS error
OlegIvaniv Jun 19, 2024
dfc66ba
Cleanup
OlegIvaniv Jun 19, 2024
9317b64
Improve theme
OlegIvaniv Jun 19, 2024
390246b
Update public chat package version import
OlegIvaniv Jun 19, 2024
be568e8
Improve styles
OlegIvaniv Jun 19, 2024
6e3b2d7
Clean-up and refactor message actions
OlegIvaniv Jun 20, 2024
3bcf8b6
Remove unused component
OlegIvaniv Jun 20, 2024
f4ffda4
Bump chat sdk version
OlegIvaniv Jun 20, 2024
264225e
Fix binary data in filesystem mode
OlegIvaniv Jun 20, 2024
6ca7945
Fix unit tests
OlegIvaniv Jun 25, 2024
484ba19
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jun 25, 2024
bb3ee1b
fix: Improve scrolling and prevent files resetting when selecting
OlegIvaniv Jul 1, 2024
c325763
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jul 1, 2024
64a1254
Fix linting issues
OlegIvaniv Jul 1, 2024
b908416
Design improvements
OlegIvaniv Jul 2, 2024
661dd93
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jul 2, 2024
66e18c0
Fix past messages cycling if there’s only a single message
OlegIvaniv Jul 2, 2024
35a757b
Reset previousMessageIndex sooner
OlegIvaniv Jul 2, 2024
84dfd9d
Add option to passthrough binary images in tools agent, fix hover CTA
OlegIvaniv Jul 3, 2024
01462e3
Remove unused method
OlegIvaniv Jul 3, 2024
3875831
Linting fixes
OlegIvaniv Jul 3, 2024
26f8084
Reset conversational agent changes
OlegIvaniv Jul 3, 2024
ce788fe
Remove unused modules
OlegIvaniv Jul 3, 2024
4a41639
Linting fixes
OlegIvaniv Jul 3, 2024
10fa054
Bump chat sdk version
OlegIvaniv Jul 3, 2024
48c4224
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jul 3, 2024
a041cb9
Remove non-supported node test
OlegIvaniv Jul 4, 2024
2cea41c
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jul 4, 2024
a96b863
PR review
OlegIvaniv Jul 4, 2024
edcb8f5
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jul 4, 2024
6711dd9
Fix chat colors
OlegIvaniv Jul 4, 2024
5ee7712
Fix e2e and replace debugging package path
OlegIvaniv Jul 5, 2024
6897172
Improve vertical scrolling
OlegIvaniv Jul 5, 2024
14c22c9
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jul 8, 2024
3953056
Fix chat file name & use method for file handler
OlegIvaniv Jul 8, 2024
e38400e
Fix linting issue
OlegIvaniv Jul 8, 2024
e320b3e
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jul 8, 2024
d06b6ed
Remove eslint 9.4 from pnpm-lock
OlegIvaniv Jul 8, 2024
ce68a06
debugging linting
OlegIvaniv Jul 8, 2024
571f18f
Cleanup pnpm-lock
OlegIvaniv Jul 8, 2024
46c5623
Merge branch 'master' into ai-190-add-file-upload-to-chat-trigger
OlegIvaniv Jul 9, 2024
0f7ee05
Update `@vueuse` to 10.11.0 to fix issues with onClickOutside directive
OlegIvaniv Jul 9, 2024
43c88f1
Fix useClipboard spec
OlegIvaniv Jul 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions cypress/composables/modals/chat-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions packages/@n8n/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
}
},
"dependencies": {
"@vueuse/core": "^10.5.0",
"highlight.js": "^11.8.0",
"markdown-it-link-attributes": "^4.0.1",
"uuid": "^8.3.2",
Expand Down
12 changes: 12 additions & 0 deletions packages/@n8n/chat/src/__stories__/App.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,15 @@ export const Windowed: Story = {
mode: 'window',
} satisfies Partial<ChatOptions>,
};

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<ChatOptions>,
};
40 changes: 35 additions & 5 deletions packages/@n8n/chat/src/api/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,23 @@ async function getAccessToken() {
export async function authenticatedFetch<T>(...args: Parameters<typeof fetch>): Promise<T> {
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;
Expand All @@ -37,6 +45,28 @@ export async function post<T>(url: string, body: object = {}, options: RequestIn
body: JSON.stringify(body),
});
}
export async function postWithFiles<T>(
url: string,
body: Record<string, unknown> = {},
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<T>(url, {
...options,
method: 'POST',
body: formData,
});
}

export async function put<T>(url: string, body: object = {}, options: RequestInit = {}) {
return await authenticatedFetch<T>(url, {
Expand Down
24 changes: 22 additions & 2 deletions packages/@n8n/chat/src/api/message.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<SendMessageResponse>(
`${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<SendMessageResponse>(
`${options.webhookUrl}`,
Expand Down
92 changes: 92 additions & 0 deletions packages/@n8n/chat/src/components/ChatFile.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<script setup lang="ts">
import IconFileText from 'virtual:icons/mdi/fileText';
import IconFileMusic from 'virtual:icons/mdi/fileMusic';
import IconFileImage from 'virtual:icons/mdi/fileImage';
import IconFileVideo from 'virtual:icons/mdi/fileVideo';
import IconDelete from 'virtual:icons/mdi/closeThick';
import IconPreview from 'virtual:icons/mdi/openInNew';

import { computed, type FunctionalComponent } from 'vue';

const props = defineProps<{
file: File;
isRemovable: boolean;
isPreviewable?: boolean;
}>();

const emit = defineEmits<{
remove: [value: File];
}>();

const iconMapper: Record<string, FunctionalComponent> = {
document: IconFileText,
audio: IconFileMusic,
image: IconFileImage,
video: IconFileVideo,
};

const TypeIcon = computed(() => {
const type = props.file?.type.split('/')[0];
return iconMapper[type] || IconFileText;
});

function onClick() {
if (props.isRemovable) {
emit('remove', props.file);
}

if (props.isPreviewable) {
window.open(URL.createObjectURL(props.file));
}
}
</script>

<template>
<div class="chat-file" @click="onClick">
<TypeIcon />
<p class="chat-file-name">{{ file.name }}</p>
<IconDelete v-if="isRemovable" class="chat-file-delete" />
<IconPreview v-if="isPreviewable" class="chat-file-preview" />
</div>
</template>

<style scoped lang="scss">
.chat-file {
display: flex;
align-items: center;
flex-wrap: nowrap;
width: fit-content;
max-width: 15rem;
padding: 0.5rem;
border-radius: 0.25rem;
gap: 0.25rem;
font-size: 0.75rem;
background: white;
color: var(--chat--color-dark);
border: 1px solid var(--chat--color-dark);
cursor: pointer;
}

.chat-file-name-tooltip {
overflow: hidden;
}
.chat-file-name {
overflow: hidden;
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
}
.chat-file-delete,
.chat-file-preview {
background: none;
border: none;
display: none;
cursor: pointer;
flex-shrink: 0;

.chat-file:hover & {
display: block;
}
}
</style>
Loading
Loading