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(editor): Implement AI Assistant chat UI #9300

Merged
merged 31 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
13dced4
feat: Implement AI Assistant chat UI
MiloradFilipovic Apr 29, 2024
b346093
✨ Added more customization options for n8n chat components
MiloradFilipovic Apr 29, 2024
b7fb897
⚡ Added quick reply support, implemented closable window mode. More c…
MiloradFilipovic Apr 29, 2024
050af0b
⚡ Update node creator position, move chat width to variable. Document…
MiloradFilipovic Apr 30, 2024
68ad81e
✨ Implemented the 'Next step' AI popup
MiloradFilipovic Apr 30, 2024
80da72c
✨ Finished the next step popup implementation
MiloradFilipovic Apr 30, 2024
402feb9
Improving typing around next step popup
MiloradFilipovic Apr 30, 2024
dbbf6b5
Merge branch 'master' into ADO-2190-ai-assistant-ui
MiloradFilipovic May 2, 2024
d99ff11
⚡ Handling connection aborted event
MiloradFilipovic May 2, 2024
3384f0e
✨ Stop showing next step dialog once user has seen it. Updated prompt…
MiloradFilipovic May 2, 2024
64ea362
⚡ Refactoring jsPlumb logic, disabling the chat after the first user …
MiloradFilipovic May 2, 2024
12eea84
💄 Using `filter` for box shadow
MiloradFilipovic May 3, 2024
8f0456f
Merge branch 'master' into ADO-2190-ai-assistant-ui
MiloradFilipovic May 3, 2024
c07f107
✨ Adjusting message spacing, adding confirm dialog before close, upda…
MiloradFilipovic May 3, 2024
909ed04
📈 Added telemetry events
MiloradFilipovic May 3, 2024
9f3da1d
⚡ Adding posthog experiment
MiloradFilipovic May 3, 2024
e8d08c6
💄 Minor cleanup
MiloradFilipovic May 3, 2024
e58c0ee
🐛 Adding missing required property
MiloradFilipovic May 3, 2024
2180f03
👕 Updating import order
MiloradFilipovic May 3, 2024
b2bfe1b
👕 Fixing import order and removing extra spaces in NodeView
MiloradFilipovic May 3, 2024
2d2c899
🔨 Refactoring QuickReplies, using `filter` instead of `box-shadow`
MiloradFilipovic May 3, 2024
daf7cdd
Merge branch 'master' into ADO-2190-ai-assistant-ui
MiloradFilipovic May 6, 2024
f0323a8
👕 Updating typing and code readability
MiloradFilipovic May 6, 2024
a2b2207
⚡ Only disabling the experiment once users see the response
MiloradFilipovic May 6, 2024
eb8194f
⬆️ Updating `vue` dependency version in `@n8n/chat`
MiloradFilipovic May 6, 2024
7f6f9a4
Revert "⬆️ Updating `vue` dependency version in `@n8n/chat`"
MiloradFilipovic May 6, 2024
967b05b
⬆️ Updating dependencies using correct version of pnpm
MiloradFilipovic May 6, 2024
c114a86
📈 Adding initial bot message to telemetry event
MiloradFilipovic May 7, 2024
f685176
👕 Making session methods optional in chat types
MiloradFilipovic May 7, 2024
826c27d
⚡ Handling undefined session methods in chat component
MiloradFilipovic May 7, 2024
5ce9d74
⚡ Removing close event listener when AIAssistantChat unmounts
MiloradFilipovic May 7, 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
19 changes: 19 additions & 0 deletions packages/@n8n/chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,31 @@ The Chat window is entirely customizable using CSS variables.
--chat--window--width: 400px;
--chat--window--height: 600px;

--chat--header-height: auto;
--chat--header--padding: var(--chat--spacing);
--chat--header--background: var(--chat--color-dark);
--chat--header--color: var(--chat--color-light);
--chat--header--border-top: none;
--chat--header--border-bottom: none;
--chat--header--border-bottom: none;
--chat--header--border-bottom: none;
--chat--heading--font-size: 2em;
--chat--header--color: var(--chat--color-light);
--chat--subtitle--font-size: inherit;
--chat--subtitle--line-height: 1.8;

--chat--textarea--height: 50px;

--chat--message--font-size: 1rem;
--chat--message--padding: var(--chat--spacing);
--chat--message--border-radius: var(--chat--border-radius);
--chat--message-line-height: 1.8;
--chat--message--bot--background: var(--chat--color-white);
--chat--message--bot--color: var(--chat--color-dark);
--chat--message--bot--border: none;
--chat--message--user--background: var(--chat--color-secondary);
--chat--message--user--color: var(--chat--color-white);
--chat--message--user--border: none;
--chat--message--pre--background: rgba(0, 0, 0, 0.05);

--chat--toggle--background: var(--chat--color-primary);
Expand Down
44 changes: 41 additions & 3 deletions packages/@n8n/chat/src/components/Chat.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script setup lang="ts">
import { nextTick, onMounted } from 'vue';
// eslint-disable-next-line import/no-unresolved
import Close from 'virtual:icons/mdi/close';
import { computed, nextTick, onMounted } from 'vue';
import Layout from '@n8n/chat/components/Layout.vue';
import GetStarted from '@n8n/chat/components/GetStarted.vue';
import GetStartedFooter from '@n8n/chat/components/GetStartedFooter.vue';
Expand All @@ -14,6 +16,8 @@ const chatStore = useChat();
const { messages, currentSessionId } = chatStore;
const { options } = useOptions();

const showCloseButton = computed(() => options.mode === 'window' && options.showWindowCloseButton);

async function getStarted() {
void chatStore.startNewSession();
void nextTick(() => {
Expand All @@ -28,6 +32,10 @@ async function initialize() {
});
}

function closeChat() {
chatEventBus.emit('close');
}

onMounted(async () => {
await initialize();
if (!options.showWelcomeScreen && !currentSessionId.value) {
Expand All @@ -39,8 +47,20 @@ onMounted(async () => {
<template>
<Layout class="chat-wrapper">
<template #header>
<h1>{{ t('title') }}</h1>
<p>{{ t('subtitle') }}</p>
<div class="chat-heading">
<h1>
{{ t('title') }}
</h1>
<div
MiloradFilipovic marked this conversation as resolved.
Show resolved Hide resolved
v-if="showCloseButton"
class="chat-close-button"
:title="t('closeButtonTooltip')"
@click="closeChat"
>
<Close height="18" width="18" />
</div>
</div>
<p v-if="t('subtitle')">{{ t('subtitle') }}</p>
</template>
<GetStarted v-if="!currentSessionId && options.showWelcomeScreen" @click:button="getStarted" />
<MessagesList v-else :messages="messages" />
Expand All @@ -50,3 +70,21 @@ onMounted(async () => {
</template>
</Layout>
</template>

<style lang="scss">
.chat-heading {
display: flex;
justify-content: space-between;
align-items: center;
}

.chat-close-button {
display: flex;
cursor: pointer;

&:hover {
color: var(--chat--close--button--color-hover, var(--chat--color-primary));
}
}
</style>
`;
MiloradFilipovic marked this conversation as resolved.
Show resolved Hide resolved
26 changes: 21 additions & 5 deletions packages/@n8n/chat/src/components/Input.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
<script setup lang="ts">
// eslint-disable-next-line import/no-unresolved
import IconSend from 'virtual:icons/mdi/send';
import { computed, ref } from 'vue';
import { useI18n, useChat } from '@n8n/chat/composables';
import { computed, onMounted, ref } from 'vue';
import { useI18n, useChat, useOptions } from '@n8n/chat/composables';
import { chatEventBus } from '@n8n/chat/event-buses';

const { options } = useOptions();
const chatStore = useChat();
const { waitingForResponse } = chatStore;
const { t } = useI18n();

const chatTextArea = ref(null);
const input = ref('');

const isSubmitDisabled = computed(() => {
return input.value === '' || waitingForResponse.value;
return input.value === '' || waitingForResponse.value || options.disabled?.value === true;
});

const isInputDisabled = computed(() => options.disabled?.value === true);

onMounted(() => {
chatEventBus.on('focusInput', () => {
if (chatTextArea.value) {
(chatTextArea.value as HTMLTextAreaElement).focus();
MiloradFilipovic marked this conversation as resolved.
Show resolved Hide resolved
}
});
});

async function onSubmit(event: MouseEvent | KeyboardEvent) {
Expand All @@ -38,8 +51,10 @@ async function onSubmitKeydown(event: KeyboardEvent) {
<template>
<div class="chat-input">
<textarea
ref="chatTextArea"
v-model="input"
rows="1"
:disabled="isInputDisabled"
:placeholder="t('inputPlaceholder')"
@keydown.enter="onSubmitKeydown"
/>
Expand All @@ -55,10 +70,11 @@ async function onSubmitKeydown(event: KeyboardEvent) {
justify-content: center;
align-items: center;
width: 100%;
background: white;

textarea {
font-family: inherit;
font-size: inherit;
font-size: var(--chat--input--font-size, inherit);
width: 100%;
border: 0;
padding: var(--chat--spacing);
Expand All @@ -71,7 +87,7 @@ async function onSubmitKeydown(event: KeyboardEvent) {
width: var(--chat--textarea--height);
background: white;
cursor: pointer;
color: var(--chat--color-secondary);
color: var(--chat--input--send--button--color, var(--chat--color-secondary));
border: 0;
font-size: 24px;
display: inline-flex;
Expand Down
17 changes: 17 additions & 0 deletions packages/@n8n/chat/src/components/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,26 @@ onBeforeUnmount(() => {
);

.chat-header {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1em;
height: var(--chat--header-height, auto);
padding: var(--chat--header--padding, var(--chat--spacing));
background: var(--chat--header--background, var(--chat--color-dark));
color: var(--chat--header--color, var(--chat--color-light));
border-top: var(--chat--header--border-top, none);
border-bottom: var(--chat--header--border-bottom, none);
border-left: var(--chat--header--border-left, none);
border-right: var(--chat--header--border-right, none);
h1 {
font-size: var(--chat--heading--font-size);
color: var(--chat--header--color, var(--chat--color-light));
}
p {
font-size: var(--chat--subtitle--font-size, inherit);
line-height: var(--chat--subtitle--line-height, 1.8);
}
}

.chat-body {
Expand Down
36 changes: 32 additions & 4 deletions packages/@n8n/chat/src/components/Message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import VueMarkdown from 'vue-markdown-render';
import hljs from 'highlight.js/lib/core';
import markdownLink from 'markdown-it-link-attributes';
import type MarkdownIt from 'markdown-it';
import type { ChatMessage } from '@n8n/chat/types';
import type { ChatMessage, ChatMessageText } from '@n8n/chat/types';
import { useOptions } from '@n8n/chat/composables';

const props = defineProps({
message: {
Expand All @@ -16,15 +17,17 @@ const props = defineProps({
});

const { message } = toRefs(props);
const { options } = useOptions();

const messageText = computed(() => {
return message.value.text || '&lt;Empty response&gt;';
return (message.value as ChatMessageText).text || '&lt;Empty response&gt;';
});

const classes = computed(() => {
return {
'chat-message-from-user': message.value.sender === 'user',
'chat-message-from-bot': message.value.sender === 'bot',
'chat-message-transparent': message.value.transparent === true,
};
});

Expand All @@ -48,11 +51,17 @@ const markdownOptions = {
return ''; // use external default escaping
},
};

const messageComponents = options.messageComponents ?? {};
</script>
<template>
<div class="chat-message" :class="classes">
<slot>
<template v-if="message.type === 'component' && messageComponents[message.key]">
<component :is="messageComponents[message.key]" v-bind="message.arguments" />
</template>
<VueMarkdown
v-else
class="chat-message-markdown"
:source="messageText"
:options="markdownOptions"
Expand All @@ -66,21 +75,40 @@ const markdownOptions = {
.chat-message {
display: block;
max-width: 80%;
font-size: var(--chat--message--font-size, 1rem);
padding: var(--chat--message--padding, var(--chat--spacing));
border-radius: var(--chat--message--border-radius, var(--chat--border-radius));

p {
line-height: var(--chat--message-line-height, 1.8);
word-wrap: break-word;
}

// Default message gap is half of the spacing
+ .chat-message {
margin-top: var(--chat--message--margin-bottom, calc(var(--chat--spacing) * 0.5));
}

// Spacing between messages from different senders is double the individual message gap
&.chat-message-from-user + &.chat-message-from-bot,
&.chat-message-from-bot + &.chat-message-from-user {
margin-top: var(--chat--spacing);
}

&.chat-message-from-bot {
background-color: var(--chat--message--bot--background);
&:not(.chat-message-transparent) {
background-color: var(--chat--message--bot--background);
border: var(--chat--message--bot--border, none);
}
color: var(--chat--message--bot--color);
border-bottom-left-radius: 0;
}

&.chat-message-from-user {
background-color: var(--chat--message--user--background);
&:not(.chat-message-transparent) {
background-color: var(--chat--message--user--background);
border: var(--chat--message--user--border, none);
}
color: var(--chat--message--user--color);
margin-left: auto;
border-bottom-right-radius: 0;
Expand Down
7 changes: 6 additions & 1 deletion packages/@n8n/chat/src/composables/useI18n.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { isRef } from 'vue';
import { useOptions } from '@n8n/chat/composables/useOptions';

export function useI18n() {
const { options } = useOptions();
const language = options?.defaultLanguage ?? 'en';

function t(key: string): string {
return options?.i18n?.[language]?.[key] ?? key;
const val = options?.i18n?.[language]?.[key];
if (isRef(val)) {
return val.value as string;
}
return val ?? key;
}

function te(key: string): boolean {
Expand Down
1 change: 1 addition & 0 deletions packages/@n8n/chat/src/constants/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const defaultOptions: ChatOptions = {
footer: '',
getStarted: 'New Conversation',
inputPlaceholder: 'Type your question..',
closeButtonTooltip: 'Close chat',
},
},
theme: {},
Expand Down
2 changes: 2 additions & 0 deletions packages/@n8n/chat/src/css/_tokens.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,6 @@
--chat--toggle--active--background: var(--chat--color-primary-shade-100);
--chat--toggle--color: var(--chat--color-white);
--chat--toggle--size: 64px;

--chat--heading--font-size: 2em;
}
17 changes: 15 additions & 2 deletions packages/@n8n/chat/src/types/messages.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
export interface ChatMessage {
id: string;
export type ChatMessage<T = Record<string, unknown>> = ChatMessageComponent<T> | ChatMessageText;

export interface ChatMessageComponent<T = Record<string, unknown>> extends ChatMessageBase {
type: 'component';
key: string;
arguments: T;
}

export interface ChatMessageText extends ChatMessageBase {
type?: 'text';
text: string;
}

interface ChatMessageBase {
id: string;
createdAt: string;
transparent?: boolean;
sender: 'user' | 'bot';
}
5 changes: 5 additions & 0 deletions packages/@n8n/chat/src/types/options.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Component, Ref } from 'vue';
export interface ChatOptions {
webhookUrl: string;
webhookConfig?: {
Expand All @@ -6,6 +7,7 @@ export interface ChatOptions {
};
target?: string | Element;
mode?: 'window' | 'fullscreen';
showWindowCloseButton?: boolean;
showWelcomeScreen?: boolean;
loadPreviousSession?: boolean;
chatInputKey?: string;
Expand All @@ -21,8 +23,11 @@ export interface ChatOptions {
footer: string;
getStarted: string;
inputPlaceholder: string;
closeButtonTooltip: string;
[message: string]: string;
}
>;
theme?: {};
messageComponents?: Record<string, Component>;
disabled?: Ref<boolean>;
}
Loading
Loading