@n8n/chat 0.13.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/README.md +19 -0
- package/package.json +1 -1
- package/src/components/Chat.vue +48 -3
- package/src/components/Input.vue +21 -5
- package/src/components/Layout.vue +17 -0
- package/src/components/Message.vue +32 -4
- package/src/composables/useI18n.ts +6 -1
- package/src/constants/defaults.ts +1 -0
- package/src/css/_tokens.scss +2 -0
- package/src/types/chat.ts +2 -2
- package/src/types/messages.ts +15 -2
- package/src/types/options.ts +5 -0
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
package/src/components/Chat.vue
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
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
|
-
<
|
|
43
|
-
|
|
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>
|
package/src/components/Input.vue
CHANGED
|
@@ -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 || '<Empty response>';
|
|
23
|
+
return (message.value as ChatMessageText).text || '<Empty response>';
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
package/src/css/_tokens.scss
CHANGED
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
|
|
10
|
-
startNewSession
|
|
9
|
+
loadPreviousSession?: () => Promise<string | undefined>;
|
|
10
|
+
startNewSession?: () => Promise<void>;
|
|
11
11
|
sendMessage: (text: string) => Promise<void>;
|
|
12
12
|
}
|
package/src/types/messages.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
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
|
}
|
package/src/types/options.ts
CHANGED
|
@@ -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
|
}
|