Skip to content

Commit

Permalink
feat(editor): Redesign Canvas Chat (#11634)
Browse files Browse the repository at this point in the history
  • Loading branch information
OlegIvaniv authored Nov 13, 2024
1 parent 93a6f85 commit a412ab7
Show file tree
Hide file tree
Showing 41 changed files with 2,451 additions and 1,063 deletions.
6 changes: 3 additions & 3 deletions cypress/composables/modals/chat-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

export function getManualChatModal() {
return cy.getByTestId('lmChat-modal');
return cy.getByTestId('canvas-chat');
}

export function getManualChatInput() {
Expand All @@ -19,11 +19,11 @@ export function getManualChatMessages() {
}

export function getManualChatModalCloseButton() {
return getManualChatModal().get('.el-dialog__close');
return cy.getByTestId('workflow-chat-button');
}

export function getManualChatModalLogs() {
return getManualChatModal().getByTestId('lm-chat-logs');
return cy.getByTestId('canvas-chat-logs');
}
export function getManualChatDialog() {
return getManualChatModal().getByTestId('workflow-lm-chat-dialog');
Expand Down
9 changes: 4 additions & 5 deletions cypress/e2e/30-langchain.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
} from './../constants';
import {
closeManualChatModal,
getManualChatDialog,
getManualChatMessages,
getManualChatModal,
getManualChatModalLogs,
Expand Down Expand Up @@ -168,7 +167,7 @@ describe('Langchain Integration', () => {
lastNodeExecuted: BASIC_LLM_CHAIN_NODE_NAME,
});

getManualChatDialog().should('contain', outputMessage);
getManualChatMessages().should('contain', outputMessage);
});

it('should be able to open and execute Agent node', () => {
Expand Down Expand Up @@ -208,7 +207,7 @@ describe('Langchain Integration', () => {
lastNodeExecuted: AGENT_NODE_NAME,
});

getManualChatDialog().should('contain', outputMessage);
getManualChatMessages().should('contain', outputMessage);
});

it('should add and use Manual Chat Trigger node together with Agent node', () => {
Expand All @@ -229,8 +228,6 @@ describe('Langchain Integration', () => {

clickManualChatButton();

getManualChatModalLogs().should('not.exist');

const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?';
const runData = [
Expand Down Expand Up @@ -335,6 +332,8 @@ describe('Langchain Integration', () => {
getManualChatModalLogsEntries().should('have.length', 1);

closeManualChatModal();
getManualChatModalLogs().should('not.exist');
getManualChatModal().should('not.exist');
});

it('should auto-add chat trigger and basic LLM chain when adding LLM node', () => {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"build:nodes": "turbo run build:nodes",
"typecheck": "turbo typecheck",
"dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner",
"dev:be": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui",
"dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core",
"clean": "turbo run clean --parallel",
"reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs",
Expand Down
12 changes: 12 additions & 0 deletions packages/@n8n/chat/src/__tests__/setup.ts
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
import '@testing-library/jest-dom';
import '@testing-library/jest-dom';
import { configure } from '@testing-library/vue';

configure({ testIdAttribute: 'data-test-id' });

window.ResizeObserver =
window.ResizeObserver ||
vi.fn().mockImplementation(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
unobserve: vi.fn(),
}));
32 changes: 23 additions & 9 deletions packages/@n8n/chat/src/components/ChatFile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,23 @@ const TypeIcon = computed(() => {
});
function onClick() {
if (props.isRemovable) {
emit('remove', props.file);
}
if (props.isPreviewable) {
window.open(URL.createObjectURL(props.file));
}
}
function onDelete() {
emit('remove', 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" />
<span v-if="isRemovable" class="chat-file-delete" @click.stop="onDelete">
<IconDelete />
</span>
<IconPreview v-else-if="isPreviewable" class="chat-file-preview" />
</div>
</template>

Expand Down Expand Up @@ -80,12 +81,25 @@ function onClick() {
.chat-file-preview {
background: none;
border: none;
display: none;
display: block;
cursor: pointer;
flex-shrink: 0;
}
.chat-file-delete {
position: relative;
&:hover {
color: red;
}
.chat-file:hover & {
display: block;
/* Increase hit area for better clickability */
&:before {
content: '';
position: absolute;
top: -10px;
right: -10px;
bottom: -10px;
left: -10px;
}
}
</style>
102 changes: 82 additions & 20 deletions packages/@n8n/chat/src/components/Input.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { useFileDialog } from '@vueuse/core';
import IconFilePlus from 'virtual:icons/mdi/filePlus';
import IconPaperclip from 'virtual:icons/mdi/paperclip';
import IconSend from 'virtual:icons/mdi/send';
import { computed, onMounted, onUnmounted, ref, unref } from 'vue';
Expand All @@ -9,23 +9,33 @@ import { chatEventBus } from '@n8n/chat/event-buses';
import ChatFile from './ChatFile.vue';
export interface ChatInputProps {
placeholder?: string;
}
const props = withDefaults(defineProps<ChatInputProps>(), {
placeholder: 'inputPlaceholder',
});
export interface ArrowKeyDownPayload {
key: 'ArrowUp' | 'ArrowDown';
currentInputValue: string;
}
const { t } = useI18n();
const emit = defineEmits<{
arrowKeyDown: [value: ArrowKeyDownPayload];
}>();
const { options } = useOptions();
const chatStore = useChat();
const { waitingForResponse } = chatStore;
const { t } = useI18n();
const files = ref<FileList | null>(null);
const chatTextArea = ref<HTMLTextAreaElement | null>(null);
const input = ref('');
const isSubmitting = ref(false);
const resizeObserver = ref<ResizeObserver | null>(null);
const isSubmitDisabled = computed(() => {
return input.value === '' || waitingForResponse.value || options.disabled?.value === true;
Expand Down Expand Up @@ -74,12 +84,30 @@ onMounted(() => {
chatEventBus.on('focusInput', focusChatInput);
chatEventBus.on('blurInput', blurChatInput);
chatEventBus.on('setInputValue', setInputValue);
if (chatTextArea.value) {
resizeObserver.value = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target === chatTextArea.value) {
adjustHeight({ target: chatTextArea.value } as unknown as Event);
}
}
});
// Start observing the textarea
resizeObserver.value.observe(chatTextArea.value);
}
});
onUnmounted(() => {
chatEventBus.off('focusInput', focusChatInput);
chatEventBus.off('blurInput', blurChatInput);
chatEventBus.off('setInputValue', setInputValue);
if (resizeObserver.value) {
resizeObserver.value.disconnect();
resizeObserver.value = null;
}
});
function blurChatInput() {
Expand Down Expand Up @@ -121,6 +149,7 @@ async function onSubmitKeydown(event: KeyboardEvent) {
}
await onSubmit(event);
adjustHeight({ target: chatTextArea.value } as unknown as Event);
}
function onFileRemove(file: File) {
Expand Down Expand Up @@ -151,27 +180,41 @@ function onOpenFileDialog() {
if (isFileUploadDisabled.value) return;
openFileDialog({ accept: unref(allowedFileTypes) });
}
function adjustHeight(event: Event) {
const textarea = event.target as HTMLTextAreaElement;
// Set to content minimum to get the right scrollHeight
textarea.style.height = 'var(--chat--textarea--height)';
// Get the new height, with a small buffer for padding
const newHeight = Math.min(textarea.scrollHeight, 480); // 30rem
textarea.style.height = `${newHeight}px`;
}
</script>

<template>
<div class="chat-input" :style="styleVars" @keydown.stop="onKeyDown">
<div class="chat-inputs">
<textarea
ref="chatTextArea"
data-test-id="chat-input"
v-model="input"
:disabled="isInputDisabled"
:placeholder="t('inputPlaceholder')"
:placeholder="t(props.placeholder)"
@keydown.enter="onSubmitKeydown"
@input="adjustHeight"
@mousedown="adjustHeight"
@focus="adjustHeight"
/>

<div class="chat-inputs-controls">
<button
v-if="isFileUploadAllowed"
:disabled="isFileUploadDisabled"
class="chat-input-send-button"
class="chat-input-file-button"
data-test-id="chat-attach-file-button"
@click="onOpenFileDialog"
>
<IconFilePlus height="24" width="24" />
<IconPaperclip height="24" width="24" />
</button>
<button :disabled="isSubmitDisabled" class="chat-input-send-button" @click="onSubmit">
<IconSend height="24" width="24" />
Expand All @@ -184,6 +227,7 @@ function onOpenFileDialog() {
:key="file.name"
:file="file"
:is-removable="true"
:is-previewable="true"
@remove="onFileRemove"
/>
</div>
Expand Down Expand Up @@ -217,13 +261,15 @@ function onOpenFileDialog() {
border-radius: var(--chat--input--border-radius, 0);
padding: 0.8rem;
padding-right: calc(0.8rem + (var(--controls-count, 1) * var(--chat--textarea--height)));
min-height: var(--chat--textarea--height);
max-height: var(--chat--textarea--max-height, var(--chat--textarea--height));
height: 100%;
min-height: var(--chat--textarea--height, 2.5rem); // Set a smaller initial height
max-height: var(--chat--textarea--max-height, 30rem);
height: var(--chat--textarea--height, 2.5rem); // Set initial height same as min-height
resize: none;
overflow-y: auto;
background: var(--chat--input--background, white);
resize: var(--chat--textarea--resize, none);
color: var(--chat--input--text-color, initial);
outline: none;
line-height: var(--chat--input--line-height, 1.5);
&:focus,
&:hover {
Expand All @@ -235,8 +281,10 @@ function onOpenFileDialog() {
display: flex;
position: absolute;
right: 0.5rem;
bottom: 0;
}
.chat-input-send-button {
.chat-input-send-button,
.chat-input-file-button {
height: var(--chat--textarea--height);
width: var(--chat--textarea--height);
background: var(--chat--input--send--button--background, white);
Expand All @@ -253,19 +301,33 @@ function onOpenFileDialog() {
min-width: fit-content;
}
&:hover,
&:focus {
background: var(
--chat--input--send--button--background-hover,
var(--chat--input--send--button--background)
);
color: var(--chat--input--send--button--color-hover, var(--chat--color-secondary-shade-50));
}
&[disabled] {
cursor: no-drop;
color: var(--chat--color-disabled);
}
.chat-input-send-button {
&:hover,
&:focus {
background: var(
--chat--input--send--button--background-hover,
var(--chat--input--send--button--background)
);
color: var(--chat--input--send--button--color-hover, var(--chat--color-secondary-shade-50));
}
}
}
.chat-input-file-button {
background: var(--chat--input--file--button--background, white);
color: var(--chat--input--file--button--color, var(--chat--color-secondary));
&:hover {
background: var(
--chat--input--file--button--background-hover,
var(--chat--input--file--button--background)
);
color: var(--chat--input--file--button--color-hover, var(--chat--color-secondary-shade-50));
}
}
.chat-files {
Expand All @@ -275,7 +337,7 @@ function onOpenFileDialog() {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 0.25rem;
gap: 0.5rem;
padding: var(--chat--files-spacing, 0.25rem);
}
</style>
Loading

0 comments on commit a412ab7

Please sign in to comment.