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

WIP: Ask AI chat about node errors #9201

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion packages/@n8n/chat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ The Chat window is entirely customizable using CSS variables.
--chat--transition-duration: 0.15s;

--chat--window--width: 400px;
--chat--window--height: 600px;
--chat--window--height: 70vh;

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

Expand Down
3 changes: 2 additions & 1 deletion packages/@n8n/chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@
}
},
"dependencies": {
"@vueuse/core": "^10.5.0",
"highlight.js": "^11.8.0",
"markdown-it-link-attributes": "^4.0.1",
"uuid": "^8.3.2",
"vue": "^3.3.4",
"vue": "^3.4.21",
"vue-markdown-render": "^2.1.1"
},
"devDependencies": {
Expand Down
17 changes: 16 additions & 1 deletion packages/@n8n/chat/src/components/ChatWindow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import IconChat from 'virtual:icons/mdi/chat';
// eslint-disable-next-line import/no-unresolved
import IconChevronDown from 'virtual:icons/mdi/chevron-down';
import { nextTick, ref } from 'vue';
import { nextTick, ref, onMounted, onBeforeUnmount } from 'vue';
import Chat from '@n8n/chat/components/Chat.vue';
import { chatEventBus } from '@n8n/chat/event-buses';

Expand All @@ -18,6 +18,21 @@ function toggle() {
});
}
}
function openChat() {
isOpen.value = true;
}
function closeChat() {
isOpen.value = false;
}
onMounted(() => {
chatEventBus.on('open', openChat);
chatEventBus.on('close', closeChat);
});

onBeforeUnmount(() => {
chatEventBus.off('open', openChat);
chatEventBus.off('close', closeChat);
});
</script>

<template>
Expand Down
50 changes: 50 additions & 0 deletions packages/@n8n/chat/src/components/FileIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup>
// eslint-disable-next-line import/no-unresolved
import IconImage from 'virtual:icons/mdi/fileImage';
// eslint-disable-next-line import/no-unresolved
import IconDocument from 'virtual:icons/mdi/fileDocumentOutline';
import { computed } from 'vue';

const props = defineProps({
fileMime: {
type: String,
required: true,
},
fileSize: {
type: Number,
required: true,
},
fileName: {
type: String,
required: true,
},
size: {
type: Number,
default: 32,
},
});

const fileIconComponent = computed(() => {
if (props.fileMime.startsWith('image/')) {
return IconImage;
}
if (props.fileMime.startsWith('application/pdf')) {
return IconDocument;
}
return IconDocument;
});
</script>

<template>
<div class="file-icon">
<Component :is="getIcon" :width="`${size}px`" :height="`${size}px`" />
<!-- <getIcon /> -->
</div>
</template>

<style lang="scss">
.file-icon {
width: 32px;
height: 32px;
}
</style>
68 changes: 54 additions & 14 deletions packages/@n8n/chat/src/components/Input.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
<script setup lang="ts">
// eslint-disable-next-line import/no-unresolved
import IconSend from 'virtual:icons/mdi/send';
// eslint-disable-next-line import/no-unresolved
// import IconUpload from 'virtual:icons/mdi/fileUpload';
import { computed, ref } from 'vue';
// import { useFileDialog } from '@vueuse/core';
import { useI18n, useChat } from '@n8n/chat/composables';
// import FileIcon from '@n8n/chat/components/FileIcon.vue';

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

const input = ref('');

// const { files, open, reset, onChange } = useFileDialog({
// accept: 'image/*', // Set to accept only image files
// // multiple: true,
// });
const isSubmitDisabled = computed(() => {
return input.value === '' || waitingForResponse.value;
});
Expand All @@ -26,6 +33,10 @@ async function onSubmit(event: MouseEvent | KeyboardEvent) {
await chatStore.sendMessage(messageText);
}

// async function onUpload() {
// console.log('On upload');
// open();
// }
async function onSubmitKeydown(event: KeyboardEvent) {
if (event.shiftKey) {
return;
Expand All @@ -37,25 +48,45 @@ async function onSubmitKeydown(event: KeyboardEvent) {

<template>
<div class="chat-input">
<textarea
v-model="input"
rows="1"
:placeholder="t('inputPlaceholder')"
@keydown.enter="onSubmitKeydown"
/>
<button :disabled="isSubmitDisabled" class="chat-input-send-button" @click="onSubmit">
<IconSend height="32" width="32" />
</button>
<!-- TODO: File Upload -->
<!-- <div class="chat-input-files">
<div v-for="file in files" :key="file.name" class="chat-input-file">
<FileIcon :fileMime="file.type" :fileSize="file.size" />
</div>
</div> -->
<div class="chat-input-content">
<textarea
v-model="input"
rows="1"
:placeholder="t('inputPlaceholder') ?? ''"
@keydown.enter="onSubmitKeydown"
/>
<button
:disabled="isSubmitDisabled"
class="chat-input-button chat-input-button--send"
@click="onSubmit"
>
<IconSend height="32" width="32" />
</button>
<!-- TODO: File Upload -->
<!-- <button :disabled="false" class="chat-input-button" @click="onUpload">
<IconUpload height="32" width="32" />
</button> -->
</div>
</div>
</template>

<style lang="scss">
.chat-input {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
flex-direction: column;

.chat-input-content {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
textarea {
font-family: inherit;
font-size: inherit;
Expand All @@ -66,7 +97,7 @@ async function onSubmitKeydown(event: KeyboardEvent) {
resize: none;
}

.chat-input-send-button {
.chat-input-button {
height: var(--chat--textarea--height);
width: var(--chat--textarea--height);
background: white;
Expand All @@ -88,6 +119,15 @@ async function onSubmitKeydown(event: KeyboardEvent) {
cursor: default;
color: var(--chat--color-disabled);
}

&--send {
color: var(--chat--color-secondary);

&[disabled] {
cursor: default;
color: var(--chat--color-disabled);
}
}
}
}
</style>
4 changes: 4 additions & 0 deletions packages/@n8n/chat/src/components/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ onBeforeUnmount(() => {
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));
h1 {
font-size: 1.2rem;
color: inherit;
}
}

.chat-body {
Expand Down
21 changes: 17 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 @@ -74,13 +83,17 @@ const markdownOptions = {
}

&.chat-message-from-bot {
background-color: var(--chat--message--bot--background);
&:not(.chat-message-transparent) {
background-color: var(--chat--message--bot--background);
}
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);
}
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
2 changes: 1 addition & 1 deletion packages/@n8n/chat/src/css/_tokens.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
--chat--transition-duration: 0.15s;

--chat--window--width: 400px;
--chat--window--height: 600px;
--chat--window--height: 70vh;

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

Expand Down
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';
}
3 changes: 3 additions & 0 deletions packages/@n8n/chat/src/types/options.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Component } from 'vue';

export interface ChatOptions {
webhookUrl: string;
webhookConfig?: {
Expand Down Expand Up @@ -25,4 +27,5 @@ export interface ChatOptions {
}
>;
theme?: {};
messageComponents?: Record<string, Component>;
}
7 changes: 6 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,15 @@
"@langchain/community": "0.0.44",
"@langchain/core": "0.1.41",
"@langchain/openai": "0.0.16",
"@langchain/pinecone": "^0.0.3",
"@n8n/client-oauth2": "workspace:*",
"@n8n/localtunnel": "2.1.0",
"@n8n/n8n-nodes-langchain": "workspace:*",
"@n8n/permissions": "workspace:*",
"@n8n/typeorm": "0.3.20-8",
"@n8n_io/license-sdk": "2.10.0",
"@oclif/core": "3.18.1",
"@pinecone-database/pinecone": "2.1.0",
"@rudderstack/rudder-sdk-node": "2.0.7",
"@sentry/integrations": "7.87.0",
"@sentry/node": "7.87.0",
Expand All @@ -119,6 +121,7 @@
"csrf": "3.1.0",
"curlconverter": "3.21.0",
"dotenv": "8.6.0",
"duck-duck-scrape": "^2.2.5",
"express": "4.19.2",
"express-async-errors": "3.1.1",
"express-handlebars": "7.1.2",
Expand Down Expand Up @@ -181,6 +184,8 @@
"ws": "8.14.2",
"xml2js": "0.6.2",
"xmllint-wasm": "3.0.1",
"yamljs": "0.3.0"
"yamljs": "0.3.0",
"zod": "3.22.4",
"zod-to-json-schema": "3.22.4"
}
}