mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Apply design feedback
This commit is contained in:
parent
6f86529fb7
commit
078f1b0045
|
@ -30,22 +30,23 @@ const TypeIcon = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function onClick() {
|
function onClick() {
|
||||||
if (props.isRemovable) {
|
|
||||||
emit('remove', props.file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.isPreviewable) {
|
if (props.isPreviewable) {
|
||||||
window.open(URL.createObjectURL(props.file));
|
window.open(URL.createObjectURL(props.file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function onDelete() {
|
||||||
|
emit('remove', props.file);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="chat-file" @click="onClick">
|
<div class="chat-file" @click="onClick">
|
||||||
<TypeIcon />
|
<TypeIcon />
|
||||||
<p class="chat-file-name">{{ file.name }}</p>
|
<p class="chat-file-name">{{ file.name }}</p>
|
||||||
<IconDelete v-if="isRemovable" class="chat-file-delete" />
|
<span v-if="isRemovable" class="chat-file-delete" @click.stop="onDelete">
|
||||||
<IconPreview v-if="isPreviewable" class="chat-file-preview" />
|
<IconDelete />
|
||||||
|
</span>
|
||||||
|
<IconPreview v-else-if="isPreviewable" class="chat-file-preview" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -80,12 +81,25 @@ function onClick() {
|
||||||
.chat-file-preview {
|
.chat-file-preview {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
display: none;
|
display: block;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-file:hover & {
|
.chat-file-delete {
|
||||||
display: block;
|
position: relative;
|
||||||
|
&:hover {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Increase hit area for better clickability */
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: -10px;
|
||||||
|
bottom: -10px;
|
||||||
|
left: -10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,18 +1,28 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useFileDialog } from '@vueuse/core';
|
import { useFileDialog } from '@vueuse/core';
|
||||||
import IconFilePlus from 'virtual:icons/mdi/filePlus';
|
import IconPaperclip from 'virtual:icons/mdi/paperclip';
|
||||||
import IconSend from 'virtual:icons/mdi/send';
|
import IconSend from 'virtual:icons/mdi/send';
|
||||||
import { computed, onMounted, onUnmounted, ref, unref, watch } from 'vue';
|
import { computed, onMounted, onUnmounted, ref, unref } from 'vue';
|
||||||
|
|
||||||
import { useI18n, useChat, useOptions } from '@n8n/chat/composables';
|
import { useI18n, useChat, useOptions } from '@n8n/chat/composables';
|
||||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||||
|
|
||||||
import ChatFile from './ChatFile.vue';
|
import ChatFile from './ChatFile.vue';
|
||||||
|
|
||||||
|
export interface ChatInputProps {
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<ChatInputProps>(), {
|
||||||
|
placeholder: 'inputPlaceholder',
|
||||||
|
});
|
||||||
|
|
||||||
export interface ArrowKeyDownPayload {
|
export interface ArrowKeyDownPayload {
|
||||||
key: 'ArrowUp' | 'ArrowDown';
|
key: 'ArrowUp' | 'ArrowDown';
|
||||||
currentInputValue: string;
|
currentInputValue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
arrowKeyDown: [value: ArrowKeyDownPayload];
|
arrowKeyDown: [value: ArrowKeyDownPayload];
|
||||||
}>();
|
}>();
|
||||||
|
@ -20,7 +30,6 @@ const emit = defineEmits<{
|
||||||
const { options } = useOptions();
|
const { options } = useOptions();
|
||||||
const chatStore = useChat();
|
const chatStore = useChat();
|
||||||
const { waitingForResponse } = chatStore;
|
const { waitingForResponse } = chatStore;
|
||||||
const { t } = useI18n();
|
|
||||||
|
|
||||||
const files = ref<FileList | null>(null);
|
const files = ref<FileList | null>(null);
|
||||||
const chatTextArea = ref<HTMLTextAreaElement | null>(null);
|
const chatTextArea = ref<HTMLTextAreaElement | null>(null);
|
||||||
|
@ -189,7 +198,7 @@ function adjustHeight(event: Event) {
|
||||||
ref="chatTextArea"
|
ref="chatTextArea"
|
||||||
v-model="input"
|
v-model="input"
|
||||||
:disabled="isInputDisabled"
|
:disabled="isInputDisabled"
|
||||||
:placeholder="t('inputPlaceholder')"
|
:placeholder="t(props.placeholder)"
|
||||||
@keydown.enter="onSubmitKeydown"
|
@keydown.enter="onSubmitKeydown"
|
||||||
@input="adjustHeight"
|
@input="adjustHeight"
|
||||||
@mousedown="adjustHeight"
|
@mousedown="adjustHeight"
|
||||||
|
@ -200,10 +209,10 @@ function adjustHeight(event: Event) {
|
||||||
<button
|
<button
|
||||||
v-if="isFileUploadAllowed"
|
v-if="isFileUploadAllowed"
|
||||||
:disabled="isFileUploadDisabled"
|
:disabled="isFileUploadDisabled"
|
||||||
class="chat-input-send-button"
|
class="chat-input-file-button"
|
||||||
@click="onOpenFileDialog"
|
@click="onOpenFileDialog"
|
||||||
>
|
>
|
||||||
<IconFilePlus height="24" width="24" />
|
<IconPaperclip height="24" width="24" />
|
||||||
</button>
|
</button>
|
||||||
<button :disabled="isSubmitDisabled" class="chat-input-send-button" @click="onSubmit">
|
<button :disabled="isSubmitDisabled" class="chat-input-send-button" @click="onSubmit">
|
||||||
<IconSend height="24" width="24" />
|
<IconSend height="24" width="24" />
|
||||||
|
@ -216,6 +225,7 @@ function adjustHeight(event: Event) {
|
||||||
:key="file.name"
|
:key="file.name"
|
||||||
:file="file"
|
:file="file"
|
||||||
:is-removable="true"
|
:is-removable="true"
|
||||||
|
:is-previewable="true"
|
||||||
@remove="onFileRemove"
|
@remove="onFileRemove"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -270,7 +280,8 @@ function adjustHeight(event: Event) {
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
.chat-input-send-button {
|
.chat-input-send-button,
|
||||||
|
.chat-input-file-button {
|
||||||
height: var(--chat--textarea--height);
|
height: var(--chat--textarea--height);
|
||||||
width: var(--chat--textarea--height);
|
width: var(--chat--textarea--height);
|
||||||
background: var(--chat--input--send--button--background, white);
|
background: var(--chat--input--send--button--background, white);
|
||||||
|
@ -287,19 +298,33 @@ function adjustHeight(event: Event) {
|
||||||
min-width: fit-content;
|
min-width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
background: var(
|
|
||||||
--chat--input--send--button--background-hover,
|
|
||||||
var(--chat--input--send--button--background)
|
|
||||||
);
|
|
||||||
color: var(--chat--input--send--button--color-hover, var(--chat--color-secondary-shade-50));
|
|
||||||
}
|
|
||||||
|
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
cursor: no-drop;
|
cursor: no-drop;
|
||||||
color: var(--chat--color-disabled);
|
color: var(--chat--color-disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-input-send-button {
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background: var(
|
||||||
|
--chat--input--send--button--background-hover,
|
||||||
|
var(--chat--input--send--button--background)
|
||||||
|
);
|
||||||
|
color: var(--chat--input--send--button--color-hover, var(--chat--color-secondary-shade-50));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.chat-input-file-button {
|
||||||
|
background: var(--chat--input--file--button--background, white);
|
||||||
|
color: var(--chat--input--file--button--color, var(--chat--color-secondary));
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(
|
||||||
|
--chat--input--file--button--background-hover,
|
||||||
|
var(--chat--input--file--button--background)
|
||||||
|
);
|
||||||
|
color: var(--chat--input--file--button--color-hover, var(--chat--color-secondary-shade-50));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-files {
|
.chat-files {
|
||||||
|
|
|
@ -139,7 +139,7 @@ onMounted(async () => {
|
||||||
scroll-margin: 100px;
|
scroll-margin: 100px;
|
||||||
.chat-message-actions {
|
.chat-message-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 100%;
|
bottom: calc(100% - 0.5rem);
|
||||||
left: 0;
|
left: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-0.25rem);
|
transform: translateY(-0.25rem);
|
||||||
|
@ -151,6 +151,9 @@ onMounted(async () => {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
&.chat-message-from-bot .chat-message-actions {
|
||||||
|
bottom: calc(100% - 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.chat-message-actions {
|
.chat-message-actions {
|
||||||
|
|
|
@ -27,7 +27,7 @@ const workflowsStore = useWorkflowsStore();
|
||||||
const canvasStore = useCanvasStore();
|
const canvasStore = useCanvasStore();
|
||||||
|
|
||||||
const messages = ref<ChatMessage[]>([]);
|
const messages = ref<ChatMessage[]>([]);
|
||||||
const currentSessionId = ref<string>(uuid());
|
const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
|
||||||
const isDisabled = ref(false);
|
const isDisabled = ref(false);
|
||||||
const container = ref<HTMLElement>();
|
const container = ref<HTMLElement>();
|
||||||
|
|
||||||
|
@ -41,13 +41,20 @@ const { sendMessage, getChatMessages } = useChatMessaging({
|
||||||
sessionId: currentSessionId,
|
sessionId: currentSessionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { height, chatWidth, rootStyles, onResizeDebounced, onResizeChatDebounced, onWindowResize } =
|
const {
|
||||||
useResize(container);
|
height,
|
||||||
|
chatWidth,
|
||||||
|
rootStyles,
|
||||||
|
logsWidth,
|
||||||
|
onResizeDebounced,
|
||||||
|
onResizeChatDebounced,
|
||||||
|
onWindowResize,
|
||||||
|
} = useResize(container);
|
||||||
|
|
||||||
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
|
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
|
||||||
const allConnections = computed(() => workflowsStore.allConnections);
|
const allConnections = computed(() => workflowsStore.allConnections);
|
||||||
const isChatOpen = computed(() => canvasStore.isChatPanelOpen);
|
const isChatOpen = computed(() => workflowsStore.isChatPanelOpen);
|
||||||
const isLogsOpen = computed(() => canvasStore.isLogsPanelOpen);
|
const isLogsOpen = computed(() => workflowsStore.isLogsPanelOpen);
|
||||||
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
||||||
|
|
||||||
const chatConfig: Chat = {
|
const chatConfig: Chat = {
|
||||||
|
@ -90,19 +97,11 @@ function refreshSession() {
|
||||||
workflowsStore.setWorkflowExecutionData(null);
|
workflowsStore.setWorkflowExecutionData(null);
|
||||||
nodeHelpers.updateNodesExecutionIssues();
|
nodeHelpers.updateNodesExecutionIssues();
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
currentSessionId.value = uuid();
|
currentSessionId.value = uuid().replace(/-/g, '');
|
||||||
}
|
|
||||||
|
|
||||||
function toggleChat() {
|
|
||||||
canvasStore.setPanelOpen('chat', !isChatOpen.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleLogs() {
|
|
||||||
canvasStore.setPanelOpen('logs', !isLogsOpen.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeLogs() {
|
function closeLogs() {
|
||||||
canvasStore.setPanelOpen('logs', false);
|
workflowsStore.setPanelOpen('logs', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
provide(ChatSymbol, chatConfig);
|
provide(ChatSymbol, chatConfig);
|
||||||
|
@ -175,7 +174,12 @@ watchEffect(() => {
|
||||||
</div>
|
</div>
|
||||||
</n8n-resize-wrapper>
|
</n8n-resize-wrapper>
|
||||||
<div v-if="isLogsOpen && connectedNode" :class="$style.logs">
|
<div v-if="isLogsOpen && connectedNode" :class="$style.logs">
|
||||||
<ChatLogsPanel :key="messages.length" :node="connectedNode" @close="closeLogs" />
|
<ChatLogsPanel
|
||||||
|
:key="messages.length"
|
||||||
|
:node="connectedNode"
|
||||||
|
:slim="logsWidth < 700"
|
||||||
|
@close="closeLogs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -196,22 +200,6 @@ watchEffect(() => {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
|
||||||
& ::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& ::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
background: var(--color-foreground-dark);
|
|
||||||
border: 1px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
& ::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--color-foreground-xdark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -225,7 +213,6 @@ watchEffect(() => {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
.footer {
|
.footer {
|
||||||
border-top: 1px solid var(--color-foreground-base);
|
border-top: 1px solid var(--color-foreground-base);
|
||||||
|
|
|
@ -3,16 +3,19 @@ import type { INode } from 'n8n-workflow';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import RunDataAi from '@/components/RunDataAi/RunDataAi.vue';
|
import RunDataAi from '@/components/RunDataAi/RunDataAi.vue';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
close: [];
|
close: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const locale = useI18n();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
|
||||||
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
||||||
defineProps<{
|
defineProps<{
|
||||||
node: INode | null;
|
node: INode | null;
|
||||||
|
slim?: boolean;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -20,7 +23,12 @@ defineProps<{
|
||||||
<div :class="$style.logsWrapper" data-test-id="lm-chat-logs">
|
<div :class="$style.logsWrapper" data-test-id="lm-chat-logs">
|
||||||
<header :class="$style.logsHeader">
|
<header :class="$style.logsHeader">
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
Latest Logs <span v-if="node">from {{ node?.name }} node</span>
|
{{ locale.baseText('chat.window.logs') }}
|
||||||
|
<span v-if="node">
|
||||||
|
{{
|
||||||
|
locale.baseText('chat.window.logsFromNode', { interpolate: { nodeName: node.name } })
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<n8n-icon-button
|
<n8n-icon-button
|
||||||
:class="$style.close"
|
:class="$style.close"
|
||||||
|
@ -36,9 +44,8 @@ defineProps<{
|
||||||
v-if="node"
|
v-if="node"
|
||||||
:class="$style.runData"
|
:class="$style.runData"
|
||||||
:node="node"
|
:node="node"
|
||||||
hide-title
|
|
||||||
:workflow="workflow"
|
:workflow="workflow"
|
||||||
slim
|
:slim="slim"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type { ArrowKeyDownPayload } from '@n8n/chat/components/Input.vue';
|
||||||
import ChatInput from '@n8n/chat/components/Input.vue';
|
import ChatInput from '@n8n/chat/components/Input.vue';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import { MODAL_CONFIRM } from '@/constants';
|
import { MODAL_CONFIRM } from '@/constants';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useClipboard } from '@/composables/useClipboard';
|
import { useClipboard } from '@/composables/useClipboard';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
displayExecution: [id: string];
|
displayExecution: [id: string];
|
||||||
sendMessage: [message: string];
|
sendMessage: [message: string];
|
||||||
|
@ -33,6 +34,12 @@ const toast = useToast();
|
||||||
|
|
||||||
const previousMessageIndex = ref(0);
|
const previousMessageIndex = ref(0);
|
||||||
|
|
||||||
|
const inputPlaceholder = computed(() => {
|
||||||
|
if (props.messages.length > 0) {
|
||||||
|
return locale.baseText('chat.window.chat.placeholder');
|
||||||
|
}
|
||||||
|
return locale.baseText('chat.window.chat.placeholderPristine');
|
||||||
|
});
|
||||||
/** Checks if message is a text message */
|
/** Checks if message is a text message */
|
||||||
function isTextMessage(message: ChatMessage): message is ChatMessageText {
|
function isTextMessage(message: ChatMessage): message is ChatMessageText {
|
||||||
return message.type === 'text' || !message.type;
|
return message.type === 'text' || !message.type;
|
||||||
|
@ -61,11 +68,11 @@ async function onRefreshSession() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmResult = await messageComposable.confirm(
|
const confirmResult = await messageComposable.confirm(
|
||||||
'Are you sure you want to refresh the session? This will clear all messages and current execution data.',
|
locale.baseText('chat.window.session.reset.warning'),
|
||||||
{
|
{
|
||||||
title: 'Confirm to refresh the session',
|
title: locale.baseText('chat.window.session.reset.title'),
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
confirmButtonText: 'Refresh Session',
|
confirmButtonText: locale.baseText('chat.window.session.reset.confirm'),
|
||||||
showClose: true,
|
showClose: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -115,9 +122,9 @@ function copySessionId() {
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.chat" data-test-id="workflow-lm-chat-dialog">
|
<div :class="$style.chat" data-test-id="workflow-lm-chat-dialog">
|
||||||
<header :class="$style.chatHeader">
|
<header :class="$style.chatHeader">
|
||||||
<span>Chat</span>
|
<span>{{ locale.baseText('chat.window.title') }}</span>
|
||||||
<div :class="$style.session">
|
<div :class="$style.session">
|
||||||
<span>Session</span>
|
<span>{{ locale.baseText('chat.window.session.title') }}</span>
|
||||||
<n8n-tooltip placement="left">
|
<n8n-tooltip placement="left">
|
||||||
<template #content>
|
<template #content>
|
||||||
{{ sessionId }}
|
{{ sessionId }}
|
||||||
|
@ -130,7 +137,7 @@ function copySessionId() {
|
||||||
text
|
text
|
||||||
size="mini"
|
size="mini"
|
||||||
icon="redo"
|
icon="redo"
|
||||||
title="Refresh session"
|
:title="locale.baseText('chat.window.session.reset.confirm')"
|
||||||
@click="onRefreshSession"
|
@click="onRefreshSession"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -186,7 +193,11 @@ function copySessionId() {
|
||||||
@click="onArrowKeyDown({ currentInputValue: '', key: 'ArrowDown' })"
|
@click="onArrowKeyDown({ currentInputValue: '', key: 'ArrowDown' })"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ChatInput data-test-id="lm-chat-inputs" @arrow-key-down="onArrowKeyDown" />
|
<ChatInput
|
||||||
|
data-test-id="lm-chat-inputs"
|
||||||
|
:placeholder="inputPlaceholder"
|
||||||
|
@arrow-key-down="onArrowKeyDown"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -202,7 +213,7 @@ function copySessionId() {
|
||||||
--chat--message--user--color: var(--color-text-dark);
|
--chat--message--user--color: var(--color-text-dark);
|
||||||
--chat--message--bot--border: none;
|
--chat--message--bot--border: none;
|
||||||
--chat--message--user--border: none;
|
--chat--message--user--border: none;
|
||||||
--chat--color-typing: var(--color-text-dark);
|
--chat--color-typing: var(--color-text-light);
|
||||||
--chat--textarea--max-height: calc(var(--panel-height) * 0.5);
|
--chat--textarea--max-height: calc(var(--panel-height) * 0.5);
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -228,10 +239,10 @@ function copySessionId() {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-2xs);
|
gap: var(--spacing-2xs);
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
|
max-width: 70%;
|
||||||
}
|
}
|
||||||
.sessionId {
|
.sessionId {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 5rem;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -260,17 +271,19 @@ function copySessionId() {
|
||||||
}
|
}
|
||||||
|
|
||||||
.messagesInput {
|
.messagesInput {
|
||||||
--input-border-color: transparent;
|
--input-border-color: var(--border-color-base);
|
||||||
--chat--input--border: none;
|
--chat--input--border: none;
|
||||||
|
|
||||||
--chat--input--border-radius: 2rem;
|
--chat--input--border-radius: 0.5rem;
|
||||||
--chat--input--send--button--background: transparent;
|
--chat--input--send--button--background: transparent;
|
||||||
--chat--input--send--button--color: var(--color-button-secondary-font);
|
|
||||||
// --chat--input--send--button--color-hover: var(--color-primary);
|
|
||||||
--chat--input--send--button--color: var(--color-primary);
|
--chat--input--send--button--color: var(--color-primary);
|
||||||
|
--chat--input--file--button--background: transparent;
|
||||||
|
--chat--input--file--button--color: var(--color-primary);
|
||||||
--chat--input--border-active: var(--input-focus-border-color, var(--color-secondary));
|
--chat--input--border-active: var(--input-focus-border-color, var(--color-secondary));
|
||||||
--chat--files-spacing: var(--spacing-2xs) 0;
|
--chat--files-spacing: var(--spacing-2xs) 0;
|
||||||
--chat--input--background: transparent;
|
--chat--input--background: transparent;
|
||||||
|
--chat--input--file--button--color: var(--color-button-secondary-font);
|
||||||
|
--chat--input--file--button--color-hover: var(--color-primary);
|
||||||
|
|
||||||
[data-theme='dark'] & {
|
[data-theme='dark'] & {
|
||||||
--chat--input--text-color: var(--input-font-color, var(--color-text-dark));
|
--chat--input--text-color: var(--input-font-color, var(--color-text-dark));
|
||||||
|
@ -280,7 +293,7 @@ function copySessionId() {
|
||||||
}
|
}
|
||||||
|
|
||||||
padding: 0 0 0 var(--spacing-xs);
|
padding: 0 0 0 var(--spacing-xs);
|
||||||
margin: var(--chat--spacing);
|
margin: 0 var(--chat--spacing) var(--chat--spacing);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--color-lm-chat-bot-background);
|
background: var(--color-lm-chat-bot-background);
|
||||||
|
|
|
@ -2,17 +2,14 @@
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
/** Tooltip label */
|
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
/** Icon name */
|
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
/** Placement of the tooltip */
|
|
||||||
placement: {
|
placement: {
|
||||||
type: String as PropType<'left' | 'right' | 'top' | 'bottom'>,
|
type: String as PropType<'left' | 'right' | 'top' | 'bottom'>,
|
||||||
default: 'top',
|
default: 'top',
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
/** Placement of the tooltip */
|
|
||||||
placement: {
|
placement: {
|
||||||
type: String as PropType<'left' | 'right' | 'top' | 'bottom'>,
|
type: String as PropType<'left' | 'right' | 'top' | 'bottom'>,
|
||||||
default: 'top',
|
default: 'top',
|
||||||
|
|
|
@ -29,7 +29,7 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
|
||||||
const workflowHelpers = useWorkflowHelpers({ router });
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
|
||||||
const chatTriggerNode = computed(() =>
|
const chatTriggerNode = computed(() =>
|
||||||
chatTriggerName.value ? workflowsStore.getNodeByName(chatTriggerName.value) : undefined,
|
chatTriggerName.value ? workflowsStore.getNodeByName(chatTriggerName.value) : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const allowFileUploads = computed(() => {
|
const allowFileUploads = computed(() => {
|
||||||
|
@ -66,8 +66,6 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
|
||||||
const triggerNode = chatTriggerNode.value;
|
const triggerNode = chatTriggerNode.value;
|
||||||
|
|
||||||
if (!triggerNode) {
|
if (!triggerNode) {
|
||||||
console.error('Chat Trigger Node could not be found!');
|
|
||||||
// showError(new Error('Chat Trigger Node could not be found!'), 'Trigger Node not found');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export function useResize(container: Ref<HTMLElement | undefined>) {
|
||||||
minHeight: ref(0),
|
minHeight: ref(0),
|
||||||
maxHeight: ref(0),
|
maxHeight: ref(0),
|
||||||
chat: ref(0), // Chat panel width
|
chat: ref(0), // Chat panel width
|
||||||
|
logs: ref(0),
|
||||||
height: ref(0),
|
height: ref(0),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,6 +37,16 @@ export function useResize(container: Ref<HTMLElement | undefined>) {
|
||||||
'--chat-width': `${dimensions.chat.value}px`,
|
'--chat-width': `${dimensions.chat.value}px`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const panelToContainerRatio = computed(() => {
|
||||||
|
const chatRatio = dimensions.chat.value / dimensions.container.value;
|
||||||
|
const containerRatio = dimensions.container.value / window.screen.width;
|
||||||
|
return {
|
||||||
|
chat: chatRatio.toFixed(2),
|
||||||
|
logs: (1 - chatRatio).toFixed(2),
|
||||||
|
container: containerRatio.toFixed(2),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constrains height to min/max bounds and updates panel height
|
* Constrains height to min/max bounds and updates panel height
|
||||||
*/
|
*/
|
||||||
|
@ -57,6 +68,7 @@ export function useResize(container: Ref<HTMLElement | undefined>) {
|
||||||
const minWidth = containerWidth * MIN_WIDTH_PERCENTAGE;
|
const minWidth = containerWidth * MIN_WIDTH_PERCENTAGE;
|
||||||
|
|
||||||
dimensions.chat.value = Math.min(Math.max(width, minWidth), maxWidth);
|
dimensions.chat.value = Math.min(Math.max(width, minWidth), maxWidth);
|
||||||
|
dimensions.logs.value = dimensions.container.value - dimensions.chat.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onResizeChatDebounced(data: ResizeData) {
|
function onResizeChatDebounced(data: ResizeData) {
|
||||||
|
@ -115,10 +127,12 @@ export function useResize(container: Ref<HTMLElement | undefined>) {
|
||||||
return {
|
return {
|
||||||
height: dimensions.height,
|
height: dimensions.height,
|
||||||
chatWidth: dimensions.chat,
|
chatWidth: dimensions.chat,
|
||||||
|
logsWidth: dimensions.logs,
|
||||||
rootStyles,
|
rootStyles,
|
||||||
onWindowResize,
|
onWindowResize,
|
||||||
onResizeDebounced,
|
onResizeDebounced,
|
||||||
onResizeChatDebounced,
|
onResizeChatDebounced,
|
||||||
// onResizeChat,
|
// onResizeChat,
|
||||||
|
panelToContainerRatio,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ interface TreeNode {
|
||||||
export interface Props {
|
export interface Props {
|
||||||
node: INodeUi;
|
node: INodeUi;
|
||||||
runIndex?: number;
|
runIndex?: number;
|
||||||
hideTitle?: boolean;
|
|
||||||
slim?: boolean;
|
slim?: boolean;
|
||||||
workflow: Workflow;
|
workflow: Workflow;
|
||||||
}
|
}
|
||||||
|
@ -309,7 +308,7 @@ watch(() => props.runIndex, selectFirst, { immediate: true });
|
||||||
}
|
}
|
||||||
.tree {
|
.tree {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
min-width: 12.8rem;
|
min-width: 8rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
padding-right: var(--spacing-xs);
|
padding-right: var(--spacing-xs);
|
||||||
|
@ -364,7 +363,6 @@ watch(() => props.runIndex, selectFirst, { immediate: true });
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// gap: var(--spacing-3xs);
|
|
||||||
padding-right: var(--spacing-3xs);
|
padding-right: var(--spacing-3xs);
|
||||||
margin: var(--spacing-4xs) 0;
|
margin: var(--spacing-4xs) 0;
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
const emit = defineEmits<{
|
|
||||||
click: [];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
label: string;
|
|
||||||
icon: string;
|
|
||||||
placement: 'left' | 'right' | 'top' | 'bottom';
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<N8nTooltip :placement="placement">
|
|
||||||
<button :class="$style.button" :style="{ color: '#aaa' }" @click="emit('click')">
|
|
||||||
<N8nIcon :icon="icon" size="small" />
|
|
||||||
</button>
|
|
||||||
<template #content>
|
|
||||||
{{ label }}
|
|
||||||
</template>
|
|
||||||
</N8nTooltip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style module>
|
|
||||||
.button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
defineProps<{
|
|
||||||
placement: 'left' | 'right';
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<n8n-info-tip type="tooltip" theme="info-light" :tooltip-placement="placement">
|
|
||||||
<n8n-text :bold="true" size="small">
|
|
||||||
<slot />
|
|
||||||
</n8n-text>
|
|
||||||
</n8n-info-tip>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style module lang="scss"></style>
|
|
|
@ -1,699 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import type { Ref } from 'vue';
|
|
||||||
import { defineAsyncComponent, provide, ref, computed, onMounted, nextTick } from 'vue';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import Modal from '@/components/Modal.vue';
|
|
||||||
import {
|
|
||||||
AI_CATEGORY_AGENTS,
|
|
||||||
AI_CATEGORY_CHAINS,
|
|
||||||
AI_CODE_NODE_TYPE,
|
|
||||||
AI_SUBCATEGORY,
|
|
||||||
CHAT_EMBED_MODAL_KEY,
|
|
||||||
CHAT_TRIGGER_NODE_TYPE,
|
|
||||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
|
||||||
MODAL_CONFIRM,
|
|
||||||
VIEWS,
|
|
||||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
|
||||||
} from '@/constants';
|
|
||||||
|
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-unresolved
|
|
||||||
import MessagesList from '@n8n/chat/components/MessagesList.vue';
|
|
||||||
import type { ArrowKeyDownPayload } from '@n8n/chat/components/Input.vue';
|
|
||||||
import ChatInput from '@n8n/chat/components/Input.vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
|
||||||
import type { Chat, ChatMessage, ChatMessageText, ChatOptions } from '@n8n/chat/types';
|
|
||||||
import { useI18n } from '@/composables/useI18n';
|
|
||||||
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
|
|
||||||
import MessageOptionTooltip from './MessageOptionTooltip.vue';
|
|
||||||
import MessageOptionAction from './MessageOptionAction.vue';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
BinaryFileType,
|
|
||||||
IBinaryData,
|
|
||||||
IBinaryKeyData,
|
|
||||||
IDataObject,
|
|
||||||
INode,
|
|
||||||
INodeExecutionData,
|
|
||||||
INodeParameters,
|
|
||||||
INodeType,
|
|
||||||
ITaskData,
|
|
||||||
IUser,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import {
|
|
||||||
CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE,
|
|
||||||
NodeConnectionType,
|
|
||||||
NodeHelpers,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { useToast } from '@/composables/useToast';
|
|
||||||
import type { INodeUi } from '@/Interface';
|
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
||||||
import { createEventBus } from 'n8n-design-system';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useMessage } from '@/composables/useMessage';
|
|
||||||
import { usePinnedData } from '@/composables/usePinnedData';
|
|
||||||
import { get, last } from 'lodash-es';
|
|
||||||
import { isEmpty } from '@/utils/typesUtils';
|
|
||||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
|
||||||
|
|
||||||
const LazyRunDataAi = defineAsyncComponent(
|
|
||||||
async () => await import('@/components/RunDataAi/RunDataAi.vue'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: Add proper type
|
|
||||||
interface LangChainMessage {
|
|
||||||
id: string[];
|
|
||||||
kwargs: {
|
|
||||||
content: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MemoryOutput {
|
|
||||||
action: string;
|
|
||||||
chatHistory?: LangChainMessage[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { runWorkflow } = useRunWorkflow({ router });
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
|
||||||
const uiStore = useUIStore();
|
|
||||||
|
|
||||||
const { showError } = useToast();
|
|
||||||
const messages: Ref<ChatMessage[]> = ref([]);
|
|
||||||
const currentSessionId = ref<string>(String(Date.now()));
|
|
||||||
const isDisabled = ref(false);
|
|
||||||
|
|
||||||
const connectedNode = ref<INode | null>(null);
|
|
||||||
const chatTrigger = ref<INode | null>(null);
|
|
||||||
const modalBus = createEventBus();
|
|
||||||
const node = ref<INode | null>(null);
|
|
||||||
const previousMessageIndex = ref(0);
|
|
||||||
|
|
||||||
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
|
|
||||||
const allowFileUploads = computed(() => {
|
|
||||||
return (chatTrigger.value?.parameters?.options as INodeParameters)?.allowFileUploads === true;
|
|
||||||
});
|
|
||||||
const allowedFilesMimeTypes = computed(() => {
|
|
||||||
return (
|
|
||||||
(
|
|
||||||
chatTrigger.value?.parameters?.options as INodeParameters
|
|
||||||
)?.allowedFilesMimeTypes?.toString() ?? ''
|
|
||||||
);
|
|
||||||
});
|
|
||||||
const locale = useI18n();
|
|
||||||
|
|
||||||
const chatOptions: ChatOptions = {
|
|
||||||
i18n: {
|
|
||||||
en: {
|
|
||||||
title: '',
|
|
||||||
footer: '',
|
|
||||||
subtitle: '',
|
|
||||||
inputPlaceholder: locale.baseText('chat.window.chat.placeholder'),
|
|
||||||
getStarted: '',
|
|
||||||
closeButtonTooltip: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
webhookUrl: '',
|
|
||||||
mode: 'window',
|
|
||||||
showWindowCloseButton: true,
|
|
||||||
disabled: isDisabled,
|
|
||||||
allowFileUploads,
|
|
||||||
allowedFilesMimeTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
const chatConfig: Chat = {
|
|
||||||
messages,
|
|
||||||
sendMessage,
|
|
||||||
initialMessages: ref([]),
|
|
||||||
currentSessionId,
|
|
||||||
waitingForResponse: isLoading,
|
|
||||||
};
|
|
||||||
|
|
||||||
const messageVars = {
|
|
||||||
'--chat--message--bot--background': 'var(--color-lm-chat-bot-background)',
|
|
||||||
'--chat--message--user--background': 'var(--color-lm-chat-user-background)',
|
|
||||||
'--chat--message--bot--color': 'var(--color-text-dark)',
|
|
||||||
'--chat--message--user--color': 'var(--color-lm-chat-user-color)',
|
|
||||||
'--chat--message--bot--border': 'none',
|
|
||||||
'--chat--message--user--border': 'none',
|
|
||||||
'--chat--color-typing': 'var(--color-text-dark)',
|
|
||||||
};
|
|
||||||
|
|
||||||
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
|
||||||
function getTriggerNode() {
|
|
||||||
const triggerNode = workflow.value.queryNodes((nodeType: INodeType) =>
|
|
||||||
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!triggerNode.length) {
|
|
||||||
chatTrigger.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
chatTrigger.value = triggerNode[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setNode() {
|
|
||||||
const triggerNode = chatTrigger.value;
|
|
||||||
if (!triggerNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const childNodes = workflow.value.getChildNodes(triggerNode.name);
|
|
||||||
|
|
||||||
for (const childNode of childNodes) {
|
|
||||||
// Look for the first connected node with metadata
|
|
||||||
// TODO: Allow later users to change that in the UI
|
|
||||||
const connectedSubNodes = workflow.value.getParentNodes(childNode, 'ALL_NON_MAIN');
|
|
||||||
const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName);
|
|
||||||
|
|
||||||
if (!resultData && !Array.isArray(resultData)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resultData.some((data) => data?.[0].metadata)) {
|
|
||||||
node.value = workflowsStore.getNodeByName(childNode);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setConnectedNode() {
|
|
||||||
const triggerNode = chatTrigger.value;
|
|
||||||
|
|
||||||
if (!triggerNode) {
|
|
||||||
showError(new Error('Chat Trigger Node could not be found!'), 'Trigger Node not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatNode = workflowsStore.getNodes().find((storeNode: INodeUi): boolean => {
|
|
||||||
if (storeNode.type === CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE) return false;
|
|
||||||
const nodeType = nodeTypesStore.getNodeType(storeNode.type, storeNode.typeVersion);
|
|
||||||
if (!nodeType) return false;
|
|
||||||
|
|
||||||
const isAgent = nodeType.codex?.subcategories?.[AI_SUBCATEGORY]?.includes(AI_CATEGORY_AGENTS);
|
|
||||||
const isChain = nodeType.codex?.subcategories?.[AI_SUBCATEGORY]?.includes(AI_CATEGORY_CHAINS);
|
|
||||||
|
|
||||||
let isCustomChainOrAgent = false;
|
|
||||||
if (nodeType.name === AI_CODE_NODE_TYPE) {
|
|
||||||
const inputs = NodeHelpers.getNodeInputs(workflow.value, storeNode, nodeType);
|
|
||||||
const inputTypes = NodeHelpers.getConnectionTypes(inputs);
|
|
||||||
|
|
||||||
const outputs = NodeHelpers.getNodeOutputs(workflow.value, storeNode, nodeType);
|
|
||||||
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
|
||||||
|
|
||||||
if (
|
|
||||||
inputTypes.includes(NodeConnectionType.AiLanguageModel) &&
|
|
||||||
inputTypes.includes(NodeConnectionType.Main) &&
|
|
||||||
outputTypes.includes(NodeConnectionType.Main)
|
|
||||||
) {
|
|
||||||
isCustomChainOrAgent = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAgent && !isChain && !isCustomChainOrAgent) return false;
|
|
||||||
|
|
||||||
const parentNodes = workflow.value.getParentNodes(storeNode.name);
|
|
||||||
const isChatChild = parentNodes.some((parentNodeName) => parentNodeName === triggerNode.name);
|
|
||||||
|
|
||||||
return Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!chatNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedNode.value = chatNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function convertFileToBinaryData(file: File): Promise<IBinaryData> {
|
|
||||||
const reader = new FileReader();
|
|
||||||
return await new Promise((resolve, reject) => {
|
|
||||||
reader.onload = () => {
|
|
||||||
const binaryData: IBinaryData = {
|
|
||||||
data: (reader.result as string).split('base64,')?.[1] ?? '',
|
|
||||||
mimeType: file.type,
|
|
||||||
fileName: file.name,
|
|
||||||
fileSize: `${file.size} bytes`,
|
|
||||||
fileExtension: file.name.split('.').pop() ?? '',
|
|
||||||
fileType: file.type.split('/')[0] as BinaryFileType,
|
|
||||||
};
|
|
||||||
resolve(binaryData);
|
|
||||||
};
|
|
||||||
reader.onerror = () => {
|
|
||||||
reject(new Error('Failed to convert file to binary data'));
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getKeyedFiles(files: File[]): Promise<IBinaryKeyData> {
|
|
||||||
const binaryData: IBinaryKeyData = {};
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
files.map(async (file, index) => {
|
|
||||||
const data = await convertFileToBinaryData(file);
|
|
||||||
const key = `data${index}`;
|
|
||||||
|
|
||||||
binaryData[key] = data;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return binaryData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractFileMeta(file: File): IDataObject {
|
|
||||||
return {
|
|
||||||
fileName: file.name,
|
|
||||||
fileSize: `${file.size} bytes`,
|
|
||||||
fileExtension: file.name.split('.').pop() ?? '',
|
|
||||||
fileType: file.type.split('/')[0],
|
|
||||||
mimeType: file.type,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function startWorkflowWithMessage(message: string, files?: File[]): Promise<void> {
|
|
||||||
const triggerNode = chatTrigger.value;
|
|
||||||
|
|
||||||
if (!triggerNode) {
|
|
||||||
showError(new Error('Chat Trigger Node could not be found!'), 'Trigger Node not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let inputKey = 'chatInput';
|
|
||||||
if (triggerNode.type === MANUAL_CHAT_TRIGGER_NODE_TYPE && triggerNode.typeVersion < 1.1) {
|
|
||||||
inputKey = 'input';
|
|
||||||
}
|
|
||||||
if (triggerNode.type === CHAT_TRIGGER_NODE_TYPE) {
|
|
||||||
inputKey = 'chatInput';
|
|
||||||
}
|
|
||||||
|
|
||||||
const usersStore = useUsersStore();
|
|
||||||
const currentUser = usersStore.currentUser ?? ({} as IUser);
|
|
||||||
|
|
||||||
const inputPayload: INodeExecutionData = {
|
|
||||||
json: {
|
|
||||||
sessionId: `test-${currentUser.id || 'unknown'}`,
|
|
||||||
action: 'sendMessage',
|
|
||||||
[inputKey]: message,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
const filesMeta = files.map((file) => extractFileMeta(file));
|
|
||||||
const binaryData = await getKeyedFiles(files);
|
|
||||||
|
|
||||||
inputPayload.json.files = filesMeta;
|
|
||||||
inputPayload.binary = binaryData;
|
|
||||||
}
|
|
||||||
const nodeData: ITaskData = {
|
|
||||||
startTime: new Date().getTime(),
|
|
||||||
executionTime: 0,
|
|
||||||
executionStatus: 'success',
|
|
||||||
data: {
|
|
||||||
main: [[inputPayload]],
|
|
||||||
},
|
|
||||||
source: [null],
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await runWorkflow({
|
|
||||||
triggerNode: triggerNode.name,
|
|
||||||
nodeData,
|
|
||||||
source: 'RunData.ManualChatMessage',
|
|
||||||
});
|
|
||||||
|
|
||||||
workflowsStore.appendChatMessage(message);
|
|
||||||
if (!response) {
|
|
||||||
showError(new Error('It was not possible to start workflow!'), 'Workflow could not be started');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForExecution(response.executionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitForExecution(executionId?: string) {
|
|
||||||
const waitInterval = setInterval(() => {
|
|
||||||
if (!isLoading.value) {
|
|
||||||
clearInterval(waitInterval);
|
|
||||||
|
|
||||||
const lastNodeExecuted =
|
|
||||||
workflowsStore.getWorkflowExecution?.data?.resultData.lastNodeExecuted;
|
|
||||||
|
|
||||||
if (!lastNodeExecuted) return;
|
|
||||||
|
|
||||||
const nodeResponseDataArray =
|
|
||||||
get(workflowsStore.getWorkflowExecution?.data?.resultData.runData, lastNodeExecuted) ?? [];
|
|
||||||
|
|
||||||
const nodeResponseData = nodeResponseDataArray[nodeResponseDataArray.length - 1];
|
|
||||||
|
|
||||||
let responseMessage: string;
|
|
||||||
|
|
||||||
if (get(nodeResponseData, 'error')) {
|
|
||||||
responseMessage = '[ERROR: ' + get(nodeResponseData, 'error.message') + ']';
|
|
||||||
} else {
|
|
||||||
const responseData = get(nodeResponseData, 'data.main[0][0].json');
|
|
||||||
responseMessage = extractResponseMessage(responseData);
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.value.push({
|
|
||||||
text: responseMessage,
|
|
||||||
sender: 'bot',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
id: executionId ?? uuid(),
|
|
||||||
});
|
|
||||||
|
|
||||||
void nextTick(setNode);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractResponseMessage(responseData?: IDataObject) {
|
|
||||||
if (!responseData || isEmpty(responseData)) {
|
|
||||||
return locale.baseText('chat.window.chat.response.empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paths where the response message might be located
|
|
||||||
const paths = ['output', 'text', 'response.text'];
|
|
||||||
const matchedPath = paths.find((path) => get(responseData, path));
|
|
||||||
|
|
||||||
if (!matchedPath) return JSON.stringify(responseData, null, 2);
|
|
||||||
|
|
||||||
const matchedOutput = get(responseData, matchedPath);
|
|
||||||
if (typeof matchedOutput === 'object') {
|
|
||||||
return '```json\n' + JSON.stringify(matchedOutput, null, 2) + '\n```';
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchedOutput?.toString() ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendMessage(message: string, files?: File[]) {
|
|
||||||
previousMessageIndex.value = 0;
|
|
||||||
if (message.trim() === '' && (!files || files.length === 0)) {
|
|
||||||
showError(
|
|
||||||
new Error(locale.baseText('chat.window.chat.provideMessage')),
|
|
||||||
locale.baseText('chat.window.chat.emptyChatMessage'),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pinnedChatData = usePinnedData(chatTrigger.value);
|
|
||||||
if (pinnedChatData.hasData.value) {
|
|
||||||
const confirmResult = await useMessage().confirm(
|
|
||||||
locale.baseText('chat.window.chat.unpinAndExecute.description'),
|
|
||||||
locale.baseText('chat.window.chat.unpinAndExecute.title'),
|
|
||||||
{
|
|
||||||
confirmButtonText: locale.baseText('chat.window.chat.unpinAndExecute.confirm'),
|
|
||||||
cancelButtonText: locale.baseText('chat.window.chat.unpinAndExecute.cancel'),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!(confirmResult === MODAL_CONFIRM)) return;
|
|
||||||
|
|
||||||
pinnedChatData.unsetData('unpin-and-send-chat-message-modal');
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMessage: ChatMessage = {
|
|
||||||
text: message,
|
|
||||||
sender: 'user',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
id: uuid(),
|
|
||||||
files,
|
|
||||||
};
|
|
||||||
messages.value.push(newMessage);
|
|
||||||
|
|
||||||
await startWorkflowWithMessage(newMessage.text, files);
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayExecution(executionId: string) {
|
|
||||||
const route = router.resolve({
|
|
||||||
name: VIEWS.EXECUTION_PREVIEW,
|
|
||||||
params: { name: workflow.value.id, executionId },
|
|
||||||
});
|
|
||||||
window.open(route.href, '_blank');
|
|
||||||
}
|
|
||||||
function isTextMessage(message: ChatMessage): message is ChatMessageText {
|
|
||||||
return message.type === 'text' || !message.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
function repostMessage(message: ChatMessageText) {
|
|
||||||
void sendMessage(message.text);
|
|
||||||
}
|
|
||||||
function reuseMessage(message: ChatMessageText) {
|
|
||||||
chatEventBus.emit('setInputValue', message.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getChatMessages(): ChatMessageText[] {
|
|
||||||
if (!connectedNode.value) return [];
|
|
||||||
|
|
||||||
const connectedMemoryInputs =
|
|
||||||
workflow.value.connectionsByDestinationNode[connectedNode.value.name][
|
|
||||||
NodeConnectionType.AiMemory
|
|
||||||
];
|
|
||||||
if (!connectedMemoryInputs) return [];
|
|
||||||
|
|
||||||
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
|
|
||||||
|
|
||||||
if (!memoryConnection) return [];
|
|
||||||
|
|
||||||
const nodeResultData = workflowsStore.getWorkflowResultDataByNodeName(memoryConnection.node);
|
|
||||||
|
|
||||||
const memoryOutputData = (nodeResultData ?? [])
|
|
||||||
.map((data) => get(data, ['data', NodeConnectionType.AiMemory, 0, 0, 'json']) as MemoryOutput)
|
|
||||||
.find((data) => data.action === 'saveContext');
|
|
||||||
|
|
||||||
return (memoryOutputData?.chatHistory ?? []).map((message, index) => {
|
|
||||||
return {
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
text: message.kwargs.content,
|
|
||||||
id: `preload__${index}`,
|
|
||||||
sender: last(message.id) === 'HumanMessage' ? 'user' : 'bot',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onArrowKeyDown({ currentInputValue, key }: ArrowKeyDownPayload) {
|
|
||||||
const pastMessages = workflowsStore.getPastChatMessages;
|
|
||||||
const isCurrentInputEmptyOrMatch =
|
|
||||||
currentInputValue.length === 0 || pastMessages.includes(currentInputValue);
|
|
||||||
|
|
||||||
if (isCurrentInputEmptyOrMatch && (key === 'ArrowUp' || key === 'ArrowDown')) {
|
|
||||||
// Blur the input when the user presses the up or down arrow key
|
|
||||||
chatEventBus.emit('blurInput');
|
|
||||||
|
|
||||||
if (pastMessages.length === 1) {
|
|
||||||
previousMessageIndex.value = 0;
|
|
||||||
} else if (key === 'ArrowUp') {
|
|
||||||
previousMessageIndex.value = (previousMessageIndex.value + 1) % pastMessages.length;
|
|
||||||
} else if (key === 'ArrowDown') {
|
|
||||||
previousMessageIndex.value =
|
|
||||||
(previousMessageIndex.value - 1 + pastMessages.length) % pastMessages.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
chatEventBus.emit(
|
|
||||||
'setInputValue',
|
|
||||||
pastMessages[pastMessages.length - 1 - previousMessageIndex.value] ?? '',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Refocus to move the cursor to the end of the input
|
|
||||||
chatEventBus.emit('focusInput');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
provide(ChatSymbol, chatConfig);
|
|
||||||
provide(ChatOptionsSymbol, chatOptions);
|
|
||||||
onMounted(() => {
|
|
||||||
getTriggerNode();
|
|
||||||
setConnectedNode();
|
|
||||||
messages.value = getChatMessages();
|
|
||||||
setNode();
|
|
||||||
|
|
||||||
setTimeout(() => chatEventBus.emit('focusInput'), 0);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Modal
|
|
||||||
:name="WORKFLOW_LM_CHAT_MODAL_KEY"
|
|
||||||
width="80%"
|
|
||||||
max-height="80%"
|
|
||||||
:title="
|
|
||||||
locale.baseText('chat.window.title', {
|
|
||||||
interpolate: {
|
|
||||||
nodeName: connectedNode?.name || locale.baseText('chat.window.noChatNode'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
"
|
|
||||||
:event-bus="modalBus"
|
|
||||||
:scrollable="false"
|
|
||||||
@keydown.stop
|
|
||||||
>
|
|
||||||
<template #content>
|
|
||||||
<div
|
|
||||||
:class="$style.workflowLmChat"
|
|
||||||
data-test-id="workflow-lm-chat-dialog"
|
|
||||||
:style="messageVars"
|
|
||||||
>
|
|
||||||
<MessagesList :messages="messages" :class="[$style.messages, 'ignore-key-press']">
|
|
||||||
<template #beforeMessage="{ message }">
|
|
||||||
<MessageOptionTooltip
|
|
||||||
v-if="message.sender === 'bot' && !message.id.includes('preload')"
|
|
||||||
placement="right"
|
|
||||||
>
|
|
||||||
{{ locale.baseText('chat.window.chat.chatMessageOptions.executionId') }}:
|
|
||||||
<a href="#" @click="displayExecution(message.id)">{{ message.id }}</a>
|
|
||||||
</MessageOptionTooltip>
|
|
||||||
|
|
||||||
<MessageOptionAction
|
|
||||||
v-if="isTextMessage(message) && message.sender === 'user'"
|
|
||||||
data-test-id="repost-message-button"
|
|
||||||
icon="redo"
|
|
||||||
:label="locale.baseText('chat.window.chat.chatMessageOptions.repostMessage')"
|
|
||||||
placement="left"
|
|
||||||
@click="repostMessage(message)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MessageOptionAction
|
|
||||||
v-if="isTextMessage(message) && message.sender === 'user'"
|
|
||||||
data-test-id="reuse-message-button"
|
|
||||||
icon="copy"
|
|
||||||
:label="locale.baseText('chat.window.chat.chatMessageOptions.reuseMessage')"
|
|
||||||
placement="left"
|
|
||||||
@click="reuseMessage(message)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</MessagesList>
|
|
||||||
<div v-if="node" :class="$style.logsWrapper" data-test-id="lm-chat-logs">
|
|
||||||
<n8n-text :class="$style.logsTitle" tag="p" size="large">{{
|
|
||||||
locale.baseText('chat.window.logs')
|
|
||||||
}}</n8n-text>
|
|
||||||
<div :class="$style.logs">
|
|
||||||
<LazyRunDataAi
|
|
||||||
:key="messages.length"
|
|
||||||
:node="node"
|
|
||||||
hide-title
|
|
||||||
slim
|
|
||||||
:workflow="workflow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<ChatInput
|
|
||||||
:class="$style.messagesInput"
|
|
||||||
data-test-id="lm-chat-inputs"
|
|
||||||
@arrow-key-down="onArrowKeyDown"
|
|
||||||
/>
|
|
||||||
<n8n-info-tip class="mt-s">
|
|
||||||
{{ locale.baseText('chatEmbed.infoTip.description') }}
|
|
||||||
<a @click="uiStore.openModal(CHAT_EMBED_MODAL_KEY)">
|
|
||||||
{{ locale.baseText('chatEmbed.infoTip.link') }}
|
|
||||||
</a>
|
|
||||||
</n8n-info-tip>
|
|
||||||
</template>
|
|
||||||
</Modal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.chat-message-markdown ul,
|
|
||||||
.chat-message-markdown ol {
|
|
||||||
padding: 0 0 0 1em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style module lang="scss">
|
|
||||||
.no-node-connected {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.workflowLmChat {
|
|
||||||
--chat--spacing: var(--spacing-m);
|
|
||||||
--chat--message--padding: var(--spacing-xs);
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 9999;
|
|
||||||
min-height: 10rem;
|
|
||||||
|
|
||||||
@media (min-height: 34rem) {
|
|
||||||
min-height: 14.5rem;
|
|
||||||
}
|
|
||||||
@media (min-height: 47rem) {
|
|
||||||
min-height: 25rem;
|
|
||||||
}
|
|
||||||
& ::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& ::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
background: var(--color-foreground-dark);
|
|
||||||
border: 1px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
& ::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--color-foreground-xdark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logsWrapper {
|
|
||||||
--node-icon-color: var(--color-text-base);
|
|
||||||
border: 1px solid var(--color-foreground-base);
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--spacing-xs) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logsTitle {
|
|
||||||
margin: 0 var(--spacing-s) var(--spacing-s);
|
|
||||||
}
|
|
||||||
|
|
||||||
.messages {
|
|
||||||
background-color: var(--color-lm-chat-messages-background);
|
|
||||||
border: 1px solid var(--color-foreground-base);
|
|
||||||
border-radius: var(--border-radius-base);
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
padding-top: 1.5em;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
& * {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.messagesInput {
|
|
||||||
--chat--input--border: var(--input-border-color, var(--border-color-base))
|
|
||||||
var(--input-border-style, var(--border-style-base))
|
|
||||||
var(--input-border-width, var(--border-width-base));
|
|
||||||
|
|
||||||
--chat--input--border-radius: var(--border-radius-base) 0 0 var(--border-radius-base);
|
|
||||||
--chat--input--send--button--background: transparent;
|
|
||||||
--chat--input--send--button--color: var(--color-button-secondary-font);
|
|
||||||
--chat--input--send--button--color-hover: var(--color-primary);
|
|
||||||
--chat--input--border-active: var(--input-focus-border-color, var(--color-secondary));
|
|
||||||
--chat--files-spacing: var(--spacing-2xs) 0;
|
|
||||||
--chat--input--background: var(--color-lm-chat-bot-background);
|
|
||||||
|
|
||||||
[data-theme='dark'] & {
|
|
||||||
--chat--input--text-color: var(--input-font-color, var(--color-text-dark));
|
|
||||||
}
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
--chat--input--text-color: var(--input-font-color, var(--color-text-dark));
|
|
||||||
}
|
|
||||||
|
|
||||||
border-bottom-right-radius: var(--border-radius-base);
|
|
||||||
border-top-right-radius: var(--border-radius-base);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -171,7 +171,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
||||||
// If the chat node has no input data or pin data, open the chat modal
|
// If the chat node has no input data or pin data, open the chat modal
|
||||||
// and halt the execution
|
// and halt the execution
|
||||||
if (!chatHasInputData && !chatHasPinData) {
|
if (!chatHasInputData && !chatHasPinData) {
|
||||||
canvasStore.setPanelOpen('chat', true);
|
workflowsStore.setPanelOpen('chat', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ export function useToast() {
|
||||||
dangerouslyUseHTMLString: false,
|
dangerouslyUseHTMLString: false,
|
||||||
position: 'bottom-right',
|
position: 'bottom-right',
|
||||||
zIndex: APP_Z_INDEXES.TOASTS, // above NDV and modal overlays
|
zIndex: APP_Z_INDEXES.TOASTS, // above NDV and modal overlays
|
||||||
offset: settingsStore.isAiAssistantEnabled || canvasStore.isChatPanelOpen ? 64 : 0,
|
offset: settingsStore.isAiAssistantEnabled || workflowsStore.isChatPanelOpen ? 64 : 0,
|
||||||
appendTo: '#app-grid',
|
appendTo: '#app-grid',
|
||||||
customClass: 'content-toast',
|
customClass: 'content-toast',
|
||||||
};
|
};
|
||||||
|
|
|
@ -169,11 +169,13 @@
|
||||||
"binaryDataDisplay.backToOverviewPage": "Back to overview page",
|
"binaryDataDisplay.backToOverviewPage": "Back to overview page",
|
||||||
"binaryDataDisplay.noDataFoundToDisplay": "No data found to display",
|
"binaryDataDisplay.noDataFoundToDisplay": "No data found to display",
|
||||||
"binaryDataDisplay.yourBrowserDoesNotSupport": "Your browser does not support the video element. Kindly update it to latest version.",
|
"binaryDataDisplay.yourBrowserDoesNotSupport": "Your browser does not support the video element. Kindly update it to latest version.",
|
||||||
"chat.window.title": "Chat Window ({nodeName})",
|
"chat.window.title": "Chat",
|
||||||
"chat.window.logs": "Log (for last message)",
|
"chat.window.logs": "Latest Logs",
|
||||||
|
"chat.window.logsFromNode": "from {nodeName} node",
|
||||||
"chat.window.noChatNode": "No Chat Node",
|
"chat.window.noChatNode": "No Chat Node",
|
||||||
"chat.window.noExecution": "Nothing got executed yet",
|
"chat.window.noExecution": "Nothing got executed yet",
|
||||||
"chat.window.chat.placeholder": "Type a message, or press ‘up’ arrow for previous one",
|
"chat.window.chat.placeholder": "Type a message, or press ‘up’ arrow for previous one",
|
||||||
|
"chat.window.chat.placeholderPristine": "Type a message",
|
||||||
"chat.window.chat.sendButtonText": "Send",
|
"chat.window.chat.sendButtonText": "Send",
|
||||||
"chat.window.chat.provideMessage": "Please provide a message",
|
"chat.window.chat.provideMessage": "Please provide a message",
|
||||||
"chat.window.chat.emptyChatMessage": "Empty chat message",
|
"chat.window.chat.emptyChatMessage": "Empty chat message",
|
||||||
|
@ -185,6 +187,10 @@
|
||||||
"chat.window.chat.unpinAndExecute.confirm": "Unpin and send",
|
"chat.window.chat.unpinAndExecute.confirm": "Unpin and send",
|
||||||
"chat.window.chat.unpinAndExecute.cancel": "Cancel",
|
"chat.window.chat.unpinAndExecute.cancel": "Cancel",
|
||||||
"chat.window.chat.response.empty": "[No response. Make sure the last executed node outputs the content to display here]",
|
"chat.window.chat.response.empty": "[No response. Make sure the last executed node outputs the content to display here]",
|
||||||
|
"chat.window.session.title": "Session",
|
||||||
|
"chat.window.session.reset.warning": "Are you sure you want to refresh the session? This will clear all messages and current execution data.",
|
||||||
|
"chat.window.session.reset.title": "Confirm to refresh the session",
|
||||||
|
"chat.window.session.reset.confirm": "Refresh Session",
|
||||||
"chatEmbed.infoTip.description": "Add chat to external applications using the n8n chat package.",
|
"chatEmbed.infoTip.description": "Add chat to external applications using the n8n chat package.",
|
||||||
"chatEmbed.infoTip.link": "More info",
|
"chatEmbed.infoTip.link": "More info",
|
||||||
"chatEmbed.title": "Embed Chat in your website",
|
"chatEmbed.title": "Embed Chat in your website",
|
||||||
|
|
|
@ -55,8 +55,6 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
const isDragging = ref<boolean>(false);
|
const isDragging = ref<boolean>(false);
|
||||||
const lastSelectedConnection = ref<Connection>();
|
const lastSelectedConnection = ref<Connection>();
|
||||||
const newNodeInsertPosition = ref<XYPosition | null>(null);
|
const newNodeInsertPosition = ref<XYPosition | null>(null);
|
||||||
const isChatPanelOpen = ref(false);
|
|
||||||
const isLogsPanelOpen = ref(false);
|
|
||||||
const panelHeight = ref(0);
|
const panelHeight = ref(0);
|
||||||
|
|
||||||
const nodes = computed<INodeUi[]>(() => workflowStore.allNodes);
|
const nodes = computed<INodeUi[]>(() => workflowStore.allNodes);
|
||||||
|
@ -326,17 +324,6 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
|
|
||||||
watch(readOnlyEnv, setReadOnly);
|
watch(readOnlyEnv, setReadOnly);
|
||||||
|
|
||||||
function setPanelOpen(panel: 'chat' | 'logs', isOpen: boolean) {
|
|
||||||
if (panel === 'chat') {
|
|
||||||
isChatPanelOpen.value = isOpen;
|
|
||||||
isLogsPanelOpen.value = isOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (panel === 'logs') {
|
|
||||||
isLogsPanelOpen.value = isOpen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setPanelHeight(height: number) {
|
function setPanelHeight(height: number) {
|
||||||
panelHeight.value = height;
|
panelHeight.value = height;
|
||||||
}
|
}
|
||||||
|
@ -350,11 +337,8 @@ export const useCanvasStore = defineStore('canvas', () => {
|
||||||
isLoading: loadingService.isLoading,
|
isLoading: loadingService.isLoading,
|
||||||
aiNodes,
|
aiNodes,
|
||||||
lastSelectedConnection: lastSelectedConnectionComputed,
|
lastSelectedConnection: lastSelectedConnectionComputed,
|
||||||
isChatPanelOpen: computed(() => isChatPanelOpen.value),
|
|
||||||
isLogsPanelOpen: computed(() => isLogsPanelOpen.value),
|
|
||||||
panelHeight: computed(() => panelHeight.value),
|
panelHeight: computed(() => panelHeight.value),
|
||||||
setPanelHeight,
|
setPanelHeight,
|
||||||
setPanelOpen,
|
|
||||||
setReadOnly,
|
setReadOnly,
|
||||||
setLastSelectedConnection,
|
setLastSelectedConnection,
|
||||||
startLoading: loadingService.startLoading,
|
startLoading: loadingService.startLoading,
|
||||||
|
|
|
@ -139,6 +139,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
const nodeMetadata = ref<NodeMetadataMap>({});
|
const nodeMetadata = ref<NodeMetadataMap>({});
|
||||||
const isInDebugMode = ref(false);
|
const isInDebugMode = ref(false);
|
||||||
const chatMessages = ref<string[]>([]);
|
const chatMessages = ref<string[]>([]);
|
||||||
|
const isChatPanelOpen = ref(false);
|
||||||
|
const isLogsPanelOpen = ref(false);
|
||||||
|
|
||||||
const workflowName = computed(() => workflow.value.name);
|
const workflowName = computed(() => workflow.value.name);
|
||||||
|
|
||||||
|
@ -1119,6 +1121,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
const { [node.name]: removedNodeMetadata, ...remainingNodeMetadata } = nodeMetadata.value;
|
const { [node.name]: removedNodeMetadata, ...remainingNodeMetadata } = nodeMetadata.value;
|
||||||
nodeMetadata.value = remainingNodeMetadata;
|
nodeMetadata.value = remainingNodeMetadata;
|
||||||
|
|
||||||
|
// If chat trigger node is removed, close chat
|
||||||
|
if (node.type === CHAT_TRIGGER_NODE_TYPE) {
|
||||||
|
setPanelOpen('chat', false);
|
||||||
|
}
|
||||||
|
|
||||||
if (workflow.value.pinData && workflow.value.pinData.hasOwnProperty(node.name)) {
|
if (workflow.value.pinData && workflow.value.pinData.hasOwnProperty(node.name)) {
|
||||||
const { [node.name]: removedPinData, ...remainingPinData } = workflow.value.pinData;
|
const { [node.name]: removedPinData, ...remainingPinData } = workflow.value.pinData;
|
||||||
workflow.value = {
|
workflow.value = {
|
||||||
|
@ -1617,6 +1624,14 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
// End Canvas V2 Functions
|
// End Canvas V2 Functions
|
||||||
//
|
//
|
||||||
|
|
||||||
|
function setPanelOpen(panel: 'chat' | 'logs', isOpen: boolean) {
|
||||||
|
if (panel === 'chat') {
|
||||||
|
isChatPanelOpen.value = isOpen;
|
||||||
|
}
|
||||||
|
// Logs panel open/close is tied to the chat panel open/close
|
||||||
|
isLogsPanelOpen.value = isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
workflow,
|
workflow,
|
||||||
usedCredentials,
|
usedCredentials,
|
||||||
|
@ -1661,6 +1676,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
getWorkflowExecution,
|
getWorkflowExecution,
|
||||||
getTotalFinishedExecutionsCount,
|
getTotalFinishedExecutionsCount,
|
||||||
getPastChatMessages,
|
getPastChatMessages,
|
||||||
|
isChatPanelOpen: computed(() => isChatPanelOpen.value),
|
||||||
|
isLogsPanelOpen: computed(() => isLogsPanelOpen.value),
|
||||||
|
setPanelOpen,
|
||||||
outgoingConnectionsByNodeName,
|
outgoingConnectionsByNodeName,
|
||||||
incomingConnectionsByNodeName,
|
incomingConnectionsByNodeName,
|
||||||
nodeHasOutputConnection,
|
nodeHasOutputConnection,
|
||||||
|
|
|
@ -248,7 +248,7 @@ const keyBindingsEnabled = computed(() => {
|
||||||
return !ndvStore.activeNode && uiStore.activeModals.length === 0;
|
return !ndvStore.activeNode && uiStore.activeModals.length === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isChatOpen = computed(() => canvasStore.isChatPanelOpen);
|
const isChatOpen = computed(() => workflowsStore.isChatPanelOpen);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialization
|
* Initialization
|
||||||
|
@ -1205,7 +1205,7 @@ const chatTriggerNodePinnedData = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onOpenChat() {
|
async function onOpenChat() {
|
||||||
canvasStore.setPanelOpen('chat', !canvasStore.isChatPanelOpen);
|
workflowsStore.setPanelOpen('chat', !workflowsStore.isChatPanelOpen);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
workflow_id: workflowId.value,
|
workflow_id: workflowId.value,
|
||||||
|
|
|
@ -459,9 +459,6 @@ export default defineComponent({
|
||||||
|
|
||||||
return this.containsChatNodes && this.triggerNodes.length === 1 && !this.pinnedChatNodeData;
|
return this.containsChatNodes && this.triggerNodes.length === 1 && !this.pinnedChatNodeData;
|
||||||
},
|
},
|
||||||
canvasChatNode() {
|
|
||||||
return this.nodes.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE);
|
|
||||||
},
|
|
||||||
pinnedChatNodeData() {
|
pinnedChatNodeData() {
|
||||||
if (!this.canvasChatNode) return null;
|
if (!this.canvasChatNode) return null;
|
||||||
|
|
||||||
|
@ -513,7 +510,7 @@ export default defineComponent({
|
||||||
return getResourcePermissions(project?.scopes);
|
return getResourcePermissions(project?.scopes);
|
||||||
},
|
},
|
||||||
isChatOpen() {
|
isChatOpen() {
|
||||||
return this.canvasStore.isChatPanelOpen;
|
return this.workflowsStore.isChatPanelOpen;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -865,7 +862,7 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
this.$telemetry.track('User clicked chat open button', telemetryPayload);
|
this.$telemetry.track('User clicked chat open button', telemetryPayload);
|
||||||
void this.externalHooks.run('nodeView.onOpenChat', telemetryPayload);
|
void this.externalHooks.run('nodeView.onOpenChat', telemetryPayload);
|
||||||
this.canvasStore.setPanelOpen('chat', !this.canvasStore.isChatPanelOpen);
|
this.workflowsStore.setPanelOpen('chat', !this.workflowsStore.isChatPanelOpen);
|
||||||
},
|
},
|
||||||
|
|
||||||
async onRunWorkflow() {
|
async onRunWorkflow() {
|
||||||
|
|
Loading…
Reference in a new issue