@n8n/chat 0.13.0 → 0.15.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/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.13.0",
3
+ "version": "0.15.0",
4
4
  "main": "./chat.umd.js",
5
5
  "module": "./chat.es.js",
6
6
  "types": "./types/index.d.ts",
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { nextTick, onMounted } from 'vue';
2
+ import Close from 'virtual:icons/mdi/close';
3
+ import { computed, nextTick, onMounted } from 'vue';
3
4
  import Layout from '@n8n/chat/components/Layout.vue';
4
5
  import GetStarted from '@n8n/chat/components/GetStarted.vue';
5
6
  import GetStartedFooter from '@n8n/chat/components/GetStartedFooter.vue';
@@ -14,7 +15,12 @@ const chatStore = useChat();
14
15
  const { messages, currentSessionId } = chatStore;
15
16
  const { options } = useOptions();
16
17
 
18
+ const showCloseButton = computed(() => options.mode === 'window' && options.showWindowCloseButton);
19
+
17
20
  async function getStarted() {
21
+ if (!chatStore.startNewSession) {
22
+ return;
23
+ }
18
24
  void chatStore.startNewSession();
19
25
  void nextTick(() => {
20
26
  chatEventBus.emit('scrollToBottom');
@@ -22,12 +28,19 @@ async function getStarted() {
22
28
  }
23
29
 
24
30
  async function initialize() {
31
+ if (!chatStore.loadPreviousSession) {
32
+ return;
33
+ }
25
34
  await chatStore.loadPreviousSession();
26
35
  void nextTick(() => {
27
36
  chatEventBus.emit('scrollToBottom');
28
37
  });
29
38
  }
30
39
 
40
+ function closeChat() {
41
+ chatEventBus.emit('close');
42
+ }
43
+
31
44
  onMounted(async () => {
32
45
  await initialize();
33
46
  if (!options.showWelcomeScreen && !currentSessionId.value) {
@@ -39,8 +52,20 @@ onMounted(async () => {
39
52
  <template>
40
53
  <Layout class="chat-wrapper">
41
54
  <template #header>
42
- <h1>{{ t('title') }}</h1>
43
- <p>{{ t('subtitle') }}</p>
55
+ <div class="chat-heading">
56
+ <h1>
57
+ {{ t('title') }}
58
+ </h1>
59
+ <button
60
+ v-if="showCloseButton"
61
+ class="chat-close-button"
62
+ :title="t('closeButtonTooltip')"
63
+ @click="closeChat"
64
+ >
65
+ <Close height="18" width="18" />
66
+ </button>
67
+ </div>
68
+ <p v-if="t('subtitle')">{{ t('subtitle') }}</p>
44
69
  </template>
45
70
  <GetStarted v-if="!currentSessionId && options.showWelcomeScreen" @click:button="getStarted" />
46
71
  <MessagesList v-else :messages="messages" />
@@ -50,3 +75,22 @@ onMounted(async () => {
50
75
  </template>
51
76
  </Layout>
52
77
  </template>
78
+
79
+ <style lang="scss">
80
+ .chat-heading {
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ }
85
+
86
+ .chat-close-button {
87
+ display: flex;
88
+ border: none;
89
+ background: none;
90
+ cursor: pointer;
91
+
92
+ &:hover {
93
+ color: var(--chat--close--button--color-hover, var(--chat--color-primary));
94
+ }
95
+ }
96
+ </style>
@@ -1,7 +1,5 @@
1
1
  <script lang="ts" setup>
2
- // eslint-disable-next-line import/no-unresolved
3
2
  import IconChat from 'virtual:icons/mdi/chat';
4
- // eslint-disable-next-line import/no-unresolved
5
3
  import IconChevronDown from 'virtual:icons/mdi/chevron-down';
6
4
  import { nextTick, ref } from 'vue';
7
5
  import Chat from '@n8n/chat/components/Chat.vue';
@@ -1,17 +1,29 @@
1
1
  <script setup lang="ts">
2
- // eslint-disable-next-line import/no-unresolved
3
2
  import IconSend from 'virtual:icons/mdi/send';
4
- import { computed, ref } from 'vue';
5
- import { useI18n, useChat } from '@n8n/chat/composables';
3
+ import { computed, onMounted, ref } from 'vue';
4
+ import { useI18n, useChat, useOptions } from '@n8n/chat/composables';
5
+ import { chatEventBus } from '@n8n/chat/event-buses';
6
6
 
7
+ const { options } = useOptions();
7
8
  const chatStore = useChat();
8
9
  const { waitingForResponse } = chatStore;
9
10
  const { t } = useI18n();
10
11
 
12
+ const chatTextArea = ref<HTMLTextAreaElement | null>(null);
11
13
  const input = ref('');
12
14
 
13
15
  const isSubmitDisabled = computed(() => {
14
- return input.value === '' || waitingForResponse.value;
16
+ return input.value === '' || waitingForResponse.value || options.disabled?.value === true;
17
+ });
18
+
19
+ const isInputDisabled = computed(() => options.disabled?.value === true);
20
+
21
+ onMounted(() => {
22
+ chatEventBus.on('focusInput', () => {
23
+ if (chatTextArea.value) {
24
+ chatTextArea.value.focus();
25
+ }
26
+ });
15
27
  });
16
28
 
17
29
  async function onSubmit(event: MouseEvent | KeyboardEvent) {
@@ -38,8 +50,10 @@ async function onSubmitKeydown(event: KeyboardEvent) {
38
50
  <template>
39
51
  <div class="chat-input">
40
52
  <textarea
53
+ ref="chatTextArea"
41
54
  v-model="input"
42
55
  rows="1"
56
+ :disabled="isInputDisabled"
43
57
  :placeholder="t('inputPlaceholder')"
44
58
  @keydown.enter="onSubmitKeydown"
45
59
  />
@@ -55,10 +69,11 @@ async function onSubmitKeydown(event: KeyboardEvent) {
55
69
  justify-content: center;
56
70
  align-items: center;
57
71
  width: 100%;
72
+ background: white;
58
73
 
59
74
  textarea {
60
75
  font-family: inherit;
61
- font-size: inherit;
76
+ font-size: var(--chat--input--font-size, inherit);
62
77
  width: 100%;
63
78
  border: 0;
64
79
  padding: var(--chat--spacing);
@@ -71,7 +86,7 @@ async function onSubmitKeydown(event: KeyboardEvent) {
71
86
  width: var(--chat--textarea--height);
72
87
  background: white;
73
88
  cursor: pointer;
74
- color: var(--chat--color-secondary);
89
+ color: var(--chat--input--send--button--color, var(--chat--color-secondary));
75
90
  border: 0;
76
91
  font-size: 24px;
77
92
  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
  }
@@ -0,0 +1,5 @@
1
+ declare module 'virtual:icons/*' {
2
+ import { FunctionalComponent, SVGAttributes } from 'vue';
3
+ const component: FunctionalComponent<SVGAttributes>;
4
+ export default component;
5
+ }
@@ -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
  }
package/vite.config.ts CHANGED
@@ -14,6 +14,7 @@ const plugins = [
14
14
  vue(),
15
15
  icons({
16
16
  compiler: 'vue3',
17
+ autoInstall: true,
17
18
  }),
18
19
  dts(),
19
20
  ];