@n8n/chat 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md CHANGED
@@ -3,8 +3,9 @@
3
3
  Portions of this software are licensed as follows:
4
4
 
5
5
  - Content of branches other than the main branch (i.e. "master") are not licensed.
6
- - All source code files that contain ".ee." in their filename are licensed under the
7
- "n8n Enterprise License" defined in "LICENSE_EE.md".
6
+ - Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
7
+ To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
8
+ specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
8
9
  - All third party components incorporated into the n8n Software are licensed under the original license
9
10
  provided by the owner of the applicable component.
10
11
  - Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
package/README.md CHANGED
@@ -210,12 +210,31 @@ The Chat window is entirely customizable using CSS variables.
210
210
  --chat--window--width: 400px;
211
211
  --chat--window--height: 600px;
212
212
 
213
+ --chat--header-height: auto;
214
+ --chat--header--padding: var(--chat--spacing);
215
+ --chat--header--background: var(--chat--color-dark);
216
+ --chat--header--color: var(--chat--color-light);
217
+ --chat--header--border-top: none;
218
+ --chat--header--border-bottom: none;
219
+ --chat--header--border-bottom: none;
220
+ --chat--header--border-bottom: none;
221
+ --chat--heading--font-size: 2em;
222
+ --chat--header--color: var(--chat--color-light);
223
+ --chat--subtitle--font-size: inherit;
224
+ --chat--subtitle--line-height: 1.8;
225
+
213
226
  --chat--textarea--height: 50px;
214
227
 
228
+ --chat--message--font-size: 1rem;
229
+ --chat--message--padding: var(--chat--spacing);
230
+ --chat--message--border-radius: var(--chat--border-radius);
231
+ --chat--message-line-height: 1.8;
215
232
  --chat--message--bot--background: var(--chat--color-white);
216
233
  --chat--message--bot--color: var(--chat--color-dark);
234
+ --chat--message--bot--border: none;
217
235
  --chat--message--user--background: var(--chat--color-secondary);
218
236
  --chat--message--user--color: var(--chat--color-white);
237
+ --chat--message--user--border: none;
219
238
  --chat--message--pre--background: rgba(0, 0, 0, 0.05);
220
239
 
221
240
  --chat--toggle--background: var(--chat--color-primary);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@n8n/chat",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "main": "./chat.umd.js",
5
5
  "module": "./chat.es.js",
6
6
  "types": "./types/index.d.ts",
@@ -1,5 +1,7 @@
1
1
  <script setup lang="ts">
2
- import { nextTick, onMounted } from 'vue';
2
+ // eslint-disable-next-line import/no-unresolved
3
+ import Close from 'virtual:icons/mdi/close';
4
+ import { computed, nextTick, onMounted } from 'vue';
3
5
  import Layout from '@n8n/chat/components/Layout.vue';
4
6
  import GetStarted from '@n8n/chat/components/GetStarted.vue';
5
7
  import GetStartedFooter from '@n8n/chat/components/GetStartedFooter.vue';
@@ -14,7 +16,12 @@ const chatStore = useChat();
14
16
  const { messages, currentSessionId } = chatStore;
15
17
  const { options } = useOptions();
16
18
 
19
+ const showCloseButton = computed(() => options.mode === 'window' && options.showWindowCloseButton);
20
+
17
21
  async function getStarted() {
22
+ if (!chatStore.startNewSession) {
23
+ return;
24
+ }
18
25
  void chatStore.startNewSession();
19
26
  void nextTick(() => {
20
27
  chatEventBus.emit('scrollToBottom');
@@ -22,12 +29,19 @@ async function getStarted() {
22
29
  }
23
30
 
24
31
  async function initialize() {
32
+ if (!chatStore.loadPreviousSession) {
33
+ return;
34
+ }
25
35
  await chatStore.loadPreviousSession();
26
36
  void nextTick(() => {
27
37
  chatEventBus.emit('scrollToBottom');
28
38
  });
29
39
  }
30
40
 
41
+ function closeChat() {
42
+ chatEventBus.emit('close');
43
+ }
44
+
31
45
  onMounted(async () => {
32
46
  await initialize();
33
47
  if (!options.showWelcomeScreen && !currentSessionId.value) {
@@ -39,8 +53,20 @@ onMounted(async () => {
39
53
  <template>
40
54
  <Layout class="chat-wrapper">
41
55
  <template #header>
42
- <h1>{{ t('title') }}</h1>
43
- <p>{{ t('subtitle') }}</p>
56
+ <div class="chat-heading">
57
+ <h1>
58
+ {{ t('title') }}
59
+ </h1>
60
+ <button
61
+ v-if="showCloseButton"
62
+ class="chat-close-button"
63
+ :title="t('closeButtonTooltip')"
64
+ @click="closeChat"
65
+ >
66
+ <Close height="18" width="18" />
67
+ </button>
68
+ </div>
69
+ <p v-if="t('subtitle')">{{ t('subtitle') }}</p>
44
70
  </template>
45
71
  <GetStarted v-if="!currentSessionId && options.showWelcomeScreen" @click:button="getStarted" />
46
72
  <MessagesList v-else :messages="messages" />
@@ -50,3 +76,22 @@ onMounted(async () => {
50
76
  </template>
51
77
  </Layout>
52
78
  </template>
79
+
80
+ <style lang="scss">
81
+ .chat-heading {
82
+ display: flex;
83
+ justify-content: space-between;
84
+ align-items: center;
85
+ }
86
+
87
+ .chat-close-button {
88
+ display: flex;
89
+ border: none;
90
+ background: none;
91
+ cursor: pointer;
92
+
93
+ &:hover {
94
+ color: var(--chat--close--button--color-hover, var(--chat--color-primary));
95
+ }
96
+ }
97
+ </style>
@@ -1,17 +1,30 @@
1
1
  <script setup lang="ts">
2
2
  // eslint-disable-next-line import/no-unresolved
3
3
  import IconSend from 'virtual:icons/mdi/send';
4
- import { computed, ref } from 'vue';
5
- import { useI18n, useChat } from '@n8n/chat/composables';
4
+ import { computed, onMounted, ref } from 'vue';
5
+ import { useI18n, useChat, useOptions } from '@n8n/chat/composables';
6
+ import { chatEventBus } from '@n8n/chat/event-buses';
6
7
 
8
+ const { options } = useOptions();
7
9
  const chatStore = useChat();
8
10
  const { waitingForResponse } = chatStore;
9
11
  const { t } = useI18n();
10
12
 
13
+ const chatTextArea = ref<HTMLTextAreaElement | null>(null);
11
14
  const input = ref('');
12
15
 
13
16
  const isSubmitDisabled = computed(() => {
14
- return input.value === '' || waitingForResponse.value;
17
+ return input.value === '' || waitingForResponse.value || options.disabled?.value === true;
18
+ });
19
+
20
+ const isInputDisabled = computed(() => options.disabled?.value === true);
21
+
22
+ onMounted(() => {
23
+ chatEventBus.on('focusInput', () => {
24
+ if (chatTextArea.value) {
25
+ chatTextArea.value.focus();
26
+ }
27
+ });
15
28
  });
16
29
 
17
30
  async function onSubmit(event: MouseEvent | KeyboardEvent) {
@@ -38,8 +51,10 @@ async function onSubmitKeydown(event: KeyboardEvent) {
38
51
  <template>
39
52
  <div class="chat-input">
40
53
  <textarea
54
+ ref="chatTextArea"
41
55
  v-model="input"
42
56
  rows="1"
57
+ :disabled="isInputDisabled"
43
58
  :placeholder="t('inputPlaceholder')"
44
59
  @keydown.enter="onSubmitKeydown"
45
60
  />
@@ -55,10 +70,11 @@ async function onSubmitKeydown(event: KeyboardEvent) {
55
70
  justify-content: center;
56
71
  align-items: center;
57
72
  width: 100%;
73
+ background: white;
58
74
 
59
75
  textarea {
60
76
  font-family: inherit;
61
- font-size: inherit;
77
+ font-size: var(--chat--input--font-size, inherit);
62
78
  width: 100%;
63
79
  border: 0;
64
80
  padding: var(--chat--spacing);
@@ -71,7 +87,7 @@ async function onSubmitKeydown(event: KeyboardEvent) {
71
87
  width: var(--chat--textarea--height);
72
88
  background: white;
73
89
  cursor: pointer;
74
- color: var(--chat--color-secondary);
90
+ color: var(--chat--input--send--button--color, var(--chat--color-secondary));
75
91
  border: 0;
76
92
  font-size: 24px;
77
93
  display: inline-flex;
@@ -58,9 +58,26 @@ onBeforeUnmount(() => {
58
58
  );
59
59
 
60
60
  .chat-header {
61
+ display: flex;
62
+ flex-direction: column;
63
+ justify-content: center;
64
+ gap: 1em;
65
+ height: var(--chat--header-height, auto);
61
66
  padding: var(--chat--header--padding, var(--chat--spacing));
62
67
  background: var(--chat--header--background, var(--chat--color-dark));
63
68
  color: var(--chat--header--color, var(--chat--color-light));
69
+ border-top: var(--chat--header--border-top, none);
70
+ border-bottom: var(--chat--header--border-bottom, none);
71
+ border-left: var(--chat--header--border-left, none);
72
+ border-right: var(--chat--header--border-right, none);
73
+ h1 {
74
+ font-size: var(--chat--heading--font-size);
75
+ color: var(--chat--header--color, var(--chat--color-light));
76
+ }
77
+ p {
78
+ font-size: var(--chat--subtitle--font-size, inherit);
79
+ line-height: var(--chat--subtitle--line-height, 1.8);
80
+ }
64
81
  }
65
82
 
66
83
  .chat-body {
@@ -6,7 +6,8 @@ import VueMarkdown from 'vue-markdown-render';
6
6
  import hljs from 'highlight.js/lib/core';
7
7
  import markdownLink from 'markdown-it-link-attributes';
8
8
  import type MarkdownIt from 'markdown-it';
9
- import type { ChatMessage } from '@n8n/chat/types';
9
+ import type { ChatMessage, ChatMessageText } from '@n8n/chat/types';
10
+ import { useOptions } from '@n8n/chat/composables';
10
11
 
11
12
  const props = defineProps({
12
13
  message: {
@@ -16,15 +17,17 @@ const props = defineProps({
16
17
  });
17
18
 
18
19
  const { message } = toRefs(props);
20
+ const { options } = useOptions();
19
21
 
20
22
  const messageText = computed(() => {
21
- return message.value.text || '&lt;Empty response&gt;';
23
+ return (message.value as ChatMessageText).text || '&lt;Empty response&gt;';
22
24
  });
23
25
 
24
26
  const classes = computed(() => {
25
27
  return {
26
28
  'chat-message-from-user': message.value.sender === 'user',
27
29
  'chat-message-from-bot': message.value.sender === 'bot',
30
+ 'chat-message-transparent': message.value.transparent === true,
28
31
  };
29
32
  });
30
33
 
@@ -48,11 +51,17 @@ const markdownOptions = {
48
51
  return ''; // use external default escaping
49
52
  },
50
53
  };
54
+
55
+ const messageComponents = options.messageComponents ?? {};
51
56
  </script>
52
57
  <template>
53
58
  <div class="chat-message" :class="classes">
54
59
  <slot>
60
+ <template v-if="message.type === 'component' && messageComponents[message.key]">
61
+ <component :is="messageComponents[message.key]" v-bind="message.arguments" />
62
+ </template>
55
63
  <VueMarkdown
64
+ v-else
56
65
  class="chat-message-markdown"
57
66
  :source="messageText"
58
67
  :options="markdownOptions"
@@ -66,21 +75,40 @@ const markdownOptions = {
66
75
  .chat-message {
67
76
  display: block;
68
77
  max-width: 80%;
78
+ font-size: var(--chat--message--font-size, 1rem);
69
79
  padding: var(--chat--message--padding, var(--chat--spacing));
70
80
  border-radius: var(--chat--message--border-radius, var(--chat--border-radius));
71
81
 
82
+ p {
83
+ line-height: var(--chat--message-line-height, 1.8);
84
+ word-wrap: break-word;
85
+ }
86
+
87
+ // Default message gap is half of the spacing
72
88
  + .chat-message {
73
89
  margin-top: var(--chat--message--margin-bottom, calc(var(--chat--spacing) * 0.5));
74
90
  }
75
91
 
92
+ // Spacing between messages from different senders is double the individual message gap
93
+ &.chat-message-from-user + &.chat-message-from-bot,
94
+ &.chat-message-from-bot + &.chat-message-from-user {
95
+ margin-top: var(--chat--spacing);
96
+ }
97
+
76
98
  &.chat-message-from-bot {
77
- background-color: var(--chat--message--bot--background);
99
+ &:not(.chat-message-transparent) {
100
+ background-color: var(--chat--message--bot--background);
101
+ border: var(--chat--message--bot--border, none);
102
+ }
78
103
  color: var(--chat--message--bot--color);
79
104
  border-bottom-left-radius: 0;
80
105
  }
81
106
 
82
107
  &.chat-message-from-user {
83
- background-color: var(--chat--message--user--background);
108
+ &:not(.chat-message-transparent) {
109
+ background-color: var(--chat--message--user--background);
110
+ border: var(--chat--message--user--border, none);
111
+ }
84
112
  color: var(--chat--message--user--color);
85
113
  margin-left: auto;
86
114
  border-bottom-right-radius: 0;
@@ -1,3 +1,4 @@
1
+ import { isRef } from 'vue';
1
2
  import { useOptions } from '@n8n/chat/composables/useOptions';
2
3
 
3
4
  export function useI18n() {
@@ -5,7 +6,11 @@ export function useI18n() {
5
6
  const language = options?.defaultLanguage ?? 'en';
6
7
 
7
8
  function t(key: string): string {
8
- return options?.i18n?.[language]?.[key] ?? key;
9
+ const val = options?.i18n?.[language]?.[key];
10
+ if (isRef(val)) {
11
+ return val.value as string;
12
+ }
13
+ return val ?? key;
9
14
  }
10
15
 
11
16
  function te(key: string): boolean {
@@ -21,6 +21,7 @@ export const defaultOptions: ChatOptions = {
21
21
  footer: '',
22
22
  getStarted: 'New Conversation',
23
23
  inputPlaceholder: 'Type your question..',
24
+ closeButtonTooltip: 'Close chat',
24
25
  },
25
26
  },
26
27
  theme: {},
@@ -33,4 +33,6 @@
33
33
  --chat--toggle--active--background: var(--chat--color-primary-shade-100);
34
34
  --chat--toggle--color: var(--chat--color-white);
35
35
  --chat--toggle--size: 64px;
36
+
37
+ --chat--heading--font-size: 2em;
36
38
  }
package/src/types/chat.ts CHANGED
@@ -6,7 +6,7 @@ export interface Chat {
6
6
  messages: Ref<ChatMessage[]>;
7
7
  currentSessionId: Ref<string | null>;
8
8
  waitingForResponse: Ref<boolean>;
9
- loadPreviousSession: () => Promise<string | undefined>;
10
- startNewSession: () => Promise<void>;
9
+ loadPreviousSession?: () => Promise<string | undefined>;
10
+ startNewSession?: () => Promise<void>;
11
11
  sendMessage: (text: string) => Promise<void>;
12
12
  }
@@ -1,6 +1,19 @@
1
- export interface ChatMessage {
2
- id: string;
1
+ export type ChatMessage<T = Record<string, unknown>> = ChatMessageComponent<T> | ChatMessageText;
2
+
3
+ export interface ChatMessageComponent<T = Record<string, unknown>> extends ChatMessageBase {
4
+ type: 'component';
5
+ key: string;
6
+ arguments: T;
7
+ }
8
+
9
+ export interface ChatMessageText extends ChatMessageBase {
10
+ type?: 'text';
3
11
  text: string;
12
+ }
13
+
14
+ interface ChatMessageBase {
15
+ id: string;
4
16
  createdAt: string;
17
+ transparent?: boolean;
5
18
  sender: 'user' | 'bot';
6
19
  }
@@ -1,3 +1,4 @@
1
+ import type { Component, Ref } from 'vue';
1
2
  export interface ChatOptions {
2
3
  webhookUrl: string;
3
4
  webhookConfig?: {
@@ -6,6 +7,7 @@ export interface ChatOptions {
6
7
  };
7
8
  target?: string | Element;
8
9
  mode?: 'window' | 'fullscreen';
10
+ showWindowCloseButton?: boolean;
9
11
  showWelcomeScreen?: boolean;
10
12
  loadPreviousSession?: boolean;
11
13
  chatInputKey?: string;
@@ -21,8 +23,11 @@ export interface ChatOptions {
21
23
  footer: string;
22
24
  getStarted: string;
23
25
  inputPlaceholder: string;
26
+ closeButtonTooltip: string;
24
27
  [message: string]: string;
25
28
  }
26
29
  >;
27
30
  theme?: {};
31
+ messageComponents?: Record<string, Component>;
32
+ disabled?: Ref<boolean>;
28
33
  }