mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Remove global state dependencies from composables
This commit is contained in:
parent
078f1b0045
commit
32b4eb523b
|
@ -1,44 +1,121 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { provide, watch, computed, ref, watchEffect } from 'vue';
|
import { provide, watch, computed, ref, watchEffect } from 'vue';
|
||||||
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
|
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { useCanvasStore } from '@/stores/canvas.store';
|
|
||||||
|
// Constants & Symbols
|
||||||
|
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
|
|
||||||
|
// Components
|
||||||
import ChatMessagesPanel from './components/ChatMessagesPanel.vue';
|
import ChatMessagesPanel from './components/ChatMessagesPanel.vue';
|
||||||
import ChatLogsPanel from './components/ChatLogsPanel.vue';
|
import ChatLogsPanel from './components/ChatLogsPanel.vue';
|
||||||
|
|
||||||
|
// Composables
|
||||||
import { useChatTrigger } from './composables/useChatTrigger';
|
import { useChatTrigger } from './composables/useChatTrigger';
|
||||||
import { useChatMessaging } from './composables/useChatMessaging';
|
import { useChatMessaging } from './composables/useChatMessaging';
|
||||||
import { useResize } from './composables/useResize';
|
import { useResize } from './composables/useResize';
|
||||||
import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
|
|
||||||
|
// Event Bus
|
||||||
|
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||||
|
|
||||||
|
// Stores
|
||||||
|
import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
|
||||||
const router = useRouter();
|
// Types
|
||||||
const uiStore = useUIStore();
|
import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types';
|
||||||
const locale = useI18n();
|
import type { RunWorkflowChatPayload } from './composables/useChatMessaging';
|
||||||
const { getCurrentWorkflow } = useWorkflowHelpers({ router });
|
|
||||||
const nodeHelpers = useNodeHelpers();
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
|
||||||
const canvasStore = useCanvasStore();
|
|
||||||
|
|
||||||
const messages = ref<ChatMessage[]>([]);
|
// Initialize stores and composables
|
||||||
const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
|
const setupStores = () => {
|
||||||
const isDisabled = ref(false);
|
const router = useRouter();
|
||||||
const container = ref<HTMLElement>();
|
const uiStore = useUIStore();
|
||||||
|
const locale = useI18n();
|
||||||
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const canvasStore = useCanvasStore();
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
||||||
|
return {
|
||||||
|
router,
|
||||||
|
uiStore,
|
||||||
|
locale,
|
||||||
|
nodeHelpers,
|
||||||
|
workflowsStore,
|
||||||
|
canvasStore,
|
||||||
|
nodeTypesStore,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Component state
|
||||||
|
const setupState = () => {
|
||||||
|
const messages = ref<ChatMessage[]>([]);
|
||||||
|
const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
|
||||||
|
const isDisabled = ref(false);
|
||||||
|
const container = ref<HTMLElement>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
currentSessionId,
|
||||||
|
isDisabled,
|
||||||
|
container,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize component
|
||||||
|
const { router, uiStore, locale, nodeHelpers, workflowsStore, canvasStore, nodeTypesStore } =
|
||||||
|
setupStores();
|
||||||
|
|
||||||
|
const { messages, currentSessionId, isDisabled, container } = setupState();
|
||||||
|
|
||||||
|
// Computed properties
|
||||||
|
const setupComputed = () => {
|
||||||
|
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
||||||
|
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
|
||||||
|
const allConnections = computed(() => workflowsStore.allConnections);
|
||||||
|
const isChatOpen = computed(() => workflowsStore.isChatPanelOpen);
|
||||||
|
const isLogsOpen = computed(() => workflowsStore.isLogsPanelOpen);
|
||||||
|
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
||||||
|
|
||||||
|
return {
|
||||||
|
workflow,
|
||||||
|
isLoading,
|
||||||
|
allConnections,
|
||||||
|
isChatOpen,
|
||||||
|
isLogsOpen,
|
||||||
|
previousChatMessages,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { workflow, isLoading, allConnections, isChatOpen, isLogsOpen, previousChatMessages } =
|
||||||
|
setupComputed();
|
||||||
|
|
||||||
|
// Initialize features
|
||||||
|
const { runWorkflow } = useRunWorkflow({ router });
|
||||||
|
|
||||||
const { chatTriggerNode, connectedNode, allowFileUploads, setChatTriggerNode, setConnectedNode } =
|
const { chatTriggerNode, connectedNode, allowFileUploads, setChatTriggerNode, setConnectedNode } =
|
||||||
useChatTrigger({ router });
|
useChatTrigger({
|
||||||
|
workflow,
|
||||||
|
getNodeByName: workflowsStore.getNodeByName,
|
||||||
|
getNodeType: nodeTypesStore.getNodeType,
|
||||||
|
});
|
||||||
|
|
||||||
const { sendMessage, getChatMessages } = useChatMessaging({
|
const { sendMessage, getChatMessages } = useChatMessaging({
|
||||||
chatTrigger: chatTriggerNode,
|
chatTrigger: chatTriggerNode,
|
||||||
|
connectedNode,
|
||||||
messages,
|
messages,
|
||||||
router,
|
|
||||||
sessionId: currentSessionId,
|
sessionId: currentSessionId,
|
||||||
|
workflow,
|
||||||
|
isLoading,
|
||||||
|
executionResultData: computed(() => workflowsStore.getWorkflowExecution?.data?.resultData),
|
||||||
|
getWorkflowResultDataByNodeName: workflowsStore.getWorkflowResultDataByNodeName,
|
||||||
|
onRunChatWorkflow,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -51,69 +128,86 @@ const {
|
||||||
onWindowResize,
|
onWindowResize,
|
||||||
} = useResize(container);
|
} = useResize(container);
|
||||||
|
|
||||||
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
|
// Chat configuration
|
||||||
const allConnections = computed(() => workflowsStore.allConnections);
|
const setupChatConfig = (): { chatConfig: Chat; chatOptions: ChatOptions } => {
|
||||||
const isChatOpen = computed(() => workflowsStore.isChatPanelOpen);
|
const chatConfig: Chat = {
|
||||||
const isLogsOpen = computed(() => workflowsStore.isLogsPanelOpen);
|
messages,
|
||||||
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
sendMessage,
|
||||||
|
initialMessages: ref([]),
|
||||||
|
currentSessionId,
|
||||||
|
waitingForResponse: isLoading,
|
||||||
|
};
|
||||||
|
|
||||||
const chatConfig: Chat = {
|
const chatOptions: ChatOptions = {
|
||||||
messages,
|
i18n: {
|
||||||
sendMessage,
|
en: {
|
||||||
initialMessages: ref([]),
|
title: '',
|
||||||
currentSessionId,
|
footer: '',
|
||||||
waitingForResponse: isLoading,
|
subtitle: '',
|
||||||
};
|
inputPlaceholder: locale.baseText('chat.window.chat.placeholder'),
|
||||||
|
getStarted: '',
|
||||||
const chatOptions: ChatOptions = {
|
closeButtonTooltip: '',
|
||||||
i18n: {
|
},
|
||||||
en: {
|
|
||||||
title: '',
|
|
||||||
footer: '',
|
|
||||||
subtitle: '',
|
|
||||||
inputPlaceholder: locale.baseText('chat.window.chat.placeholder'),
|
|
||||||
getStarted: '',
|
|
||||||
closeButtonTooltip: '',
|
|
||||||
},
|
},
|
||||||
},
|
webhookUrl: '',
|
||||||
webhookUrl: '',
|
mode: 'window',
|
||||||
mode: 'window',
|
showWindowCloseButton: true,
|
||||||
showWindowCloseButton: true,
|
disabled: isDisabled,
|
||||||
disabled: isDisabled,
|
allowFileUploads,
|
||||||
allowFileUploads,
|
allowedFilesMimeTypes: '',
|
||||||
allowedFilesMimeTypes: '',
|
};
|
||||||
|
|
||||||
|
return { chatConfig, chatOptions };
|
||||||
};
|
};
|
||||||
|
|
||||||
function displayExecution(executionId: string) {
|
const { chatConfig, chatOptions } = setupChatConfig();
|
||||||
const workflow = getCurrentWorkflow();
|
|
||||||
|
// Methods
|
||||||
|
const displayExecution = (executionId: string) => {
|
||||||
const route = router.resolve({
|
const route = router.resolve({
|
||||||
name: VIEWS.EXECUTION_PREVIEW,
|
name: VIEWS.EXECUTION_PREVIEW,
|
||||||
params: { name: workflow.id, executionId },
|
params: { name: workflow.value.id, executionId },
|
||||||
});
|
});
|
||||||
window.open(route.href, '_blank');
|
window.open(route.href, '_blank');
|
||||||
}
|
};
|
||||||
|
|
||||||
function refreshSession() {
|
const refreshSession = () => {
|
||||||
workflowsStore.setWorkflowExecutionData(null);
|
workflowsStore.setWorkflowExecutionData(null);
|
||||||
nodeHelpers.updateNodesExecutionIssues();
|
nodeHelpers.updateNodesExecutionIssues();
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
currentSessionId.value = uuid().replace(/-/g, '');
|
currentSessionId.value = uuid().replace(/-/g, '');
|
||||||
}
|
};
|
||||||
|
|
||||||
function closeLogs() {
|
const closeLogs = () => {
|
||||||
workflowsStore.setPanelOpen('logs', false);
|
workflowsStore.setPanelOpen('logs', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function onRunChatWorkflow(payload: RunWorkflowChatPayload) {
|
||||||
|
const response = await runWorkflow({
|
||||||
|
triggerNode: payload.triggerNode,
|
||||||
|
nodeData: payload.nodeData,
|
||||||
|
source: payload.source,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.appendChatMessage(payload.message);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provide chat context
|
||||||
provide(ChatSymbol, chatConfig);
|
provide(ChatSymbol, chatConfig);
|
||||||
provide(ChatOptionsSymbol, chatOptions);
|
provide(ChatOptionsSymbol, chatOptions);
|
||||||
|
|
||||||
|
// Watchers
|
||||||
watch(
|
watch(
|
||||||
() => isChatOpen.value,
|
() => isChatOpen.value,
|
||||||
(isOpen) => {
|
(isOpen) => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
setChatTriggerNode();
|
setChatTriggerNode();
|
||||||
setConnectedNode();
|
setConnectedNode();
|
||||||
messages.value = getChatMessages();
|
|
||||||
|
if (messages.value.length === 0) {
|
||||||
|
messages.value = getChatMessages();
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onWindowResize();
|
onWindowResize();
|
||||||
|
@ -138,18 +232,17 @@ watch(
|
||||||
);
|
);
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
canvasStore.setPanelHeight(isChatOpen.value || isLogsOpen.value ? height.value : 40);
|
canvasStore.setPanelHeight(isChatOpen.value || isLogsOpen.value ? height.value : 0);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n8n-resize-wrapper
|
<n8n-resize-wrapper
|
||||||
v-show="chatTriggerNode"
|
v-if="chatTriggerNode"
|
||||||
:is-resizing-enabled="isChatOpen || isLogsOpen"
|
:is-resizing-enabled="isChatOpen || isLogsOpen"
|
||||||
:supported-directions="['top']"
|
:supported-directions="['top']"
|
||||||
:class="[$style.resizeWrapper, !isChatOpen && !isLogsOpen && $style.empty]"
|
:class="[$style.resizeWrapper, !isChatOpen && !isLogsOpen && $style.empty]"
|
||||||
:height="height"
|
:height="height"
|
||||||
data-test-id="ask-assistant-sidebar"
|
|
||||||
:style="rootStyles"
|
:style="rootStyles"
|
||||||
@resize="onResizeDebounced"
|
@resize="onResizeDebounced"
|
||||||
>
|
>
|
||||||
|
@ -164,6 +257,7 @@ watchEffect(() => {
|
||||||
>
|
>
|
||||||
<div :class="$style.inner">
|
<div :class="$style.inner">
|
||||||
<ChatMessagesPanel
|
<ChatMessagesPanel
|
||||||
|
data-test-id="canvas-chat"
|
||||||
:messages="messages"
|
:messages="messages"
|
||||||
:session-id="currentSessionId"
|
:session-id="currentSessionId"
|
||||||
:past-chat-messages="previousChatMessages"
|
:past-chat-messages="previousChatMessages"
|
||||||
|
@ -176,6 +270,8 @@ watchEffect(() => {
|
||||||
<div v-if="isLogsOpen && connectedNode" :class="$style.logs">
|
<div v-if="isLogsOpen && connectedNode" :class="$style.logs">
|
||||||
<ChatLogsPanel
|
<ChatLogsPanel
|
||||||
:key="messages.length"
|
:key="messages.length"
|
||||||
|
:workflow="workflow"
|
||||||
|
data-test-id="canvas-chat-logs"
|
||||||
:node="connectedNode"
|
:node="connectedNode"
|
||||||
:slim="logsWidth < 700"
|
:slim="logsWidth < 700"
|
||||||
@close="closeLogs"
|
@close="closeLogs"
|
||||||
|
@ -192,7 +288,6 @@ watchEffect(() => {
|
||||||
min-height: 4rem;
|
min-height: 4rem;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
flex-basis: content;
|
flex-basis: content;
|
||||||
z-index: 300;
|
|
||||||
border-top: 1px solid var(--color-foreground-base);
|
border-top: 1px solid var(--color-foreground-base);
|
||||||
|
|
||||||
&.empty {
|
&.empty {
|
||||||
|
@ -201,6 +296,7 @@ watchEffect(() => {
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -208,12 +304,14 @@ watchEffect(() => {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatResizer {
|
.chatResizer {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
border-top: 1px solid var(--color-foreground-base);
|
border-top: 1px solid var(--color-foreground-base);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { INode } from 'n8n-workflow';
|
import type { INode, Workflow } from 'n8n-workflow';
|
||||||
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 { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
close: [];
|
close: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const locale = useI18n();
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
|
||||||
|
|
||||||
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
node: INode | null;
|
node: INode | null;
|
||||||
slim?: boolean;
|
slim?: boolean;
|
||||||
|
workflow: Workflow;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const locale = useI18n();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -159,7 +159,7 @@ function copySessionId() {
|
||||||
icon="redo"
|
icon="redo"
|
||||||
:label="locale.baseText('chat.window.chat.chatMessageOptions.repostMessage')"
|
:label="locale.baseText('chat.window.chat.chatMessageOptions.repostMessage')"
|
||||||
placement="left"
|
placement="left"
|
||||||
@click="repostMessage(message)"
|
@click.once="repostMessage(message)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MessageOptionAction
|
<MessageOptionAction
|
||||||
|
|
|
@ -23,7 +23,7 @@ defineProps({
|
||||||
<template #content>
|
<template #content>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</template>
|
</template>
|
||||||
<n8n-icon :class="$style.icon" :icon="icon" size="xsmall" @click="$emit('click')" />
|
<n8n-icon :class="$style.icon" :icon="icon" size="xsmall" @click="$attrs.onClick" />
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import type { Ref } from 'vue';
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
import { computed, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import type { ChatMessage, ChatMessageText } from '@n8n/chat/types';
|
import type { ChatMessage, ChatMessageText } from '@n8n/chat/types';
|
||||||
import { NodeConnectionType, CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow';
|
import { NodeConnectionType, CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow';
|
||||||
|
@ -10,41 +10,53 @@ import type {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
IBinaryData,
|
IBinaryData,
|
||||||
BinaryFileType,
|
BinaryFileType,
|
||||||
|
Workflow,
|
||||||
|
IRunExecutionData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useMessage } from '@/composables/useMessage';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import { usePinnedData } from '@/composables/usePinnedData';
|
import { usePinnedData } from '@/composables/usePinnedData';
|
||||||
import { get, isEmpty, last } from 'lodash-es';
|
import { get, isEmpty, last } from 'lodash-es';
|
||||||
import { MANUAL_CHAT_TRIGGER_NODE_TYPE, MODAL_CONFIRM } from '@/constants';
|
import { MANUAL_CHAT_TRIGGER_NODE_TYPE, MODAL_CONFIRM } from '@/constants';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
|
||||||
import type { useRouter } from 'vue-router';
|
|
||||||
import type { MemoryOutput } from '../types/chat';
|
import type { MemoryOutput } from '../types/chat';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import type { IExecutionPushResponse, INodeUi } from '@/Interface';
|
||||||
import type { INodeUi } from '@/Interface';
|
|
||||||
|
|
||||||
export function useChatMessaging({
|
export type RunWorkflowChatPayload = {
|
||||||
router,
|
triggerNode: string;
|
||||||
chatTrigger,
|
nodeData: ITaskData;
|
||||||
messages,
|
source: string;
|
||||||
sessionId,
|
message: string;
|
||||||
}: {
|
};
|
||||||
router: ReturnType<typeof useRouter>;
|
export interface ChatMessagingDependencies {
|
||||||
chatTrigger: Ref<INodeUi | null>;
|
chatTrigger: Ref<INodeUi | null>;
|
||||||
|
connectedNode: Ref<INodeUi | null>;
|
||||||
messages: Ref<ChatMessage[]>;
|
messages: Ref<ChatMessage[]>;
|
||||||
sessionId: Ref<string>;
|
sessionId: Ref<string>;
|
||||||
}) {
|
workflow: ComputedRef<Workflow>;
|
||||||
const previousMessageIndex = ref(0);
|
isLoading: ComputedRef<boolean>;
|
||||||
const workflowsStore = useWorkflowsStore();
|
executionResultData: ComputedRef<IRunExecutionData['resultData'] | undefined>;
|
||||||
const { runWorkflow } = useRunWorkflow({ router });
|
getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null;
|
||||||
const workflowHelpers = useWorkflowHelpers({ router });
|
onRunChatWorkflow: (
|
||||||
const { showError } = useToast();
|
payload: RunWorkflowChatPayload,
|
||||||
const uiStore = useUIStore();
|
) => Promise<IExecutionPushResponse | undefined>;
|
||||||
const locale = useI18n();
|
}
|
||||||
|
|
||||||
|
export function useChatMessaging({
|
||||||
|
chatTrigger,
|
||||||
|
connectedNode,
|
||||||
|
messages,
|
||||||
|
sessionId,
|
||||||
|
workflow,
|
||||||
|
isLoading,
|
||||||
|
executionResultData,
|
||||||
|
getWorkflowResultDataByNodeName,
|
||||||
|
onRunChatWorkflow,
|
||||||
|
}: ChatMessagingDependencies) {
|
||||||
|
const locale = useI18n();
|
||||||
|
const { showError } = useToast();
|
||||||
|
const previousMessageIndex = ref(0);
|
||||||
|
|
||||||
const isLoading = computed(() => uiStore.isActionActive.workflowRunning);
|
|
||||||
/** Converts a file to binary data */
|
/** Converts a file to binary data */
|
||||||
async function convertFileToBinaryData(file: File): Promise<IBinaryData> {
|
async function convertFileToBinaryData(file: File): Promise<IBinaryData> {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
@ -136,13 +148,19 @@ export function useChatMessaging({
|
||||||
source: [null],
|
source: [null],
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await runWorkflow({
|
const response = await onRunChatWorkflow({
|
||||||
triggerNode: triggerNode.name,
|
triggerNode: triggerNode.name,
|
||||||
nodeData,
|
nodeData,
|
||||||
source: 'RunData.ManualChatMessage',
|
source: 'RunData.ManualChatMessage',
|
||||||
|
message,
|
||||||
});
|
});
|
||||||
|
// const response = await runWorkflow({
|
||||||
|
// triggerNode: triggerNode.name,
|
||||||
|
// nodeData,
|
||||||
|
// source: 'RunData.ManualChatMessage',
|
||||||
|
// });
|
||||||
|
|
||||||
workflowsStore.appendChatMessage(message);
|
// workflowsStore.appendChatMessage(message);
|
||||||
if (!response?.executionId) {
|
if (!response?.executionId) {
|
||||||
showError(
|
showError(
|
||||||
new Error('It was not possible to start workflow!'),
|
new Error('It was not possible to start workflow!'),
|
||||||
|
@ -160,14 +178,12 @@ export function useChatMessaging({
|
||||||
if (!isLoading.value) {
|
if (!isLoading.value) {
|
||||||
clearInterval(waitInterval);
|
clearInterval(waitInterval);
|
||||||
|
|
||||||
const lastNodeExecuted =
|
const lastNodeExecuted = executionResultData.value?.lastNodeExecuted;
|
||||||
workflowsStore.getWorkflowExecution?.data?.resultData.lastNodeExecuted;
|
|
||||||
|
|
||||||
if (!lastNodeExecuted) return;
|
if (!lastNodeExecuted) return;
|
||||||
|
|
||||||
const nodeResponseDataArray =
|
const nodeResponseDataArray =
|
||||||
get(workflowsStore.getWorkflowExecution?.data?.resultData.runData, lastNodeExecuted) ??
|
get(executionResultData.value.runData, lastNodeExecuted) ?? [];
|
||||||
[];
|
|
||||||
|
|
||||||
const nodeResponseData = nodeResponseDataArray[nodeResponseDataArray.length - 1];
|
const nodeResponseData = nodeResponseDataArray[nodeResponseDataArray.length - 1];
|
||||||
|
|
||||||
|
@ -251,18 +267,20 @@ export function useChatMessaging({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChatMessages(): ChatMessageText[] {
|
function getChatMessages(): ChatMessageText[] {
|
||||||
if (!chatTrigger.value) return [];
|
console.log('Getting chat messages', connectedNode.value);
|
||||||
|
if (!connectedNode.value) return [];
|
||||||
|
|
||||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
|
||||||
const connectedMemoryInputs =
|
const connectedMemoryInputs =
|
||||||
workflow.connectionsByDestinationNode[chatTrigger.value.name]?.[NodeConnectionType.AiMemory];
|
workflow.value.connectionsByDestinationNode[connectedNode.value.name]?.[
|
||||||
|
NodeConnectionType.AiMemory
|
||||||
|
];
|
||||||
if (!connectedMemoryInputs) return [];
|
if (!connectedMemoryInputs) return [];
|
||||||
|
|
||||||
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
|
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
|
||||||
|
|
||||||
if (!memoryConnection) return [];
|
if (!memoryConnection) return [];
|
||||||
|
|
||||||
const nodeResultData = workflowsStore.getWorkflowResultDataByNodeName(memoryConnection.node);
|
const nodeResultData = getWorkflowResultDataByNodeName(memoryConnection.node);
|
||||||
|
|
||||||
const memoryOutputData = (nodeResultData ?? [])
|
const memoryOutputData = (nodeResultData ?? [])
|
||||||
.map((data) => get(data, ['data', NodeConnectionType.AiMemory, 0, 0, 'json']) as MemoryOutput)
|
.map((data) => get(data, ['data', NodeConnectionType.AiMemory, 0, 0, 'json']) as MemoryOutput)
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
|
import type { ComputedRef } from 'vue';
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import {
|
import {
|
||||||
CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE,
|
CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE,
|
||||||
NodeConnectionType,
|
NodeConnectionType,
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
type INode,
|
|
||||||
type INodeParameters,
|
|
||||||
type INodeType,
|
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
import type {
|
||||||
|
INodeTypeDescription,
|
||||||
|
Workflow,
|
||||||
|
INode,
|
||||||
|
INodeParameters,
|
||||||
|
INodeType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import {
|
import {
|
||||||
AI_CATEGORY_AGENTS,
|
AI_CATEGORY_AGENTS,
|
||||||
AI_CATEGORY_CHAINS,
|
AI_CATEGORY_CHAINS,
|
||||||
|
@ -17,19 +21,19 @@ import {
|
||||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import type { useRouter } from 'vue-router';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
||||||
|
|
||||||
export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter> }) {
|
export interface ChatTriggerDependencies {
|
||||||
|
getNodeByName: (name: string) => INodeUi | null;
|
||||||
|
getNodeType: (type: string, version: number) => INodeTypeDescription | null;
|
||||||
|
workflow: ComputedRef<Workflow>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChatTrigger({ getNodeByName, getNodeType, workflow }: ChatTriggerDependencies) {
|
||||||
const chatTriggerName = ref<string | null>(null);
|
const chatTriggerName = ref<string | null>(null);
|
||||||
const connectedNode = ref<INode | null>(null);
|
const connectedNode = ref<INode | null>(null);
|
||||||
const workflowsStore = useWorkflowsStore();
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
|
||||||
const workflowHelpers = useWorkflowHelpers({ router });
|
|
||||||
|
|
||||||
const chatTriggerNode = computed(() =>
|
const chatTriggerNode = computed(() =>
|
||||||
chatTriggerName.value ? workflowsStore.getNodeByName(chatTriggerName.value) : null,
|
chatTriggerName.value ? getNodeByName(chatTriggerName.value) : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const allowFileUploads = computed(() => {
|
const allowFileUploads = computed(() => {
|
||||||
|
@ -48,11 +52,9 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
|
||||||
|
|
||||||
/** Gets the chat trigger node from the workflow */
|
/** Gets the chat trigger node from the workflow */
|
||||||
function setChatTriggerNode() {
|
function setChatTriggerNode() {
|
||||||
const triggerNode = workflowHelpers
|
const triggerNode = workflow.value.queryNodes((nodeType: INodeType) =>
|
||||||
.getCurrentWorkflow()
|
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
|
||||||
.queryNodes((nodeType: INodeType) =>
|
);
|
||||||
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!triggerNode.length) {
|
if (!triggerNode.length) {
|
||||||
return;
|
return;
|
||||||
|
@ -62,25 +64,24 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
|
||||||
|
|
||||||
/** Sets the connected node after finding the trigger */
|
/** Sets the connected node after finding the trigger */
|
||||||
function setConnectedNode() {
|
function setConnectedNode() {
|
||||||
const workflow = workflowHelpers.getCurrentWorkflow();
|
|
||||||
const triggerNode = chatTriggerNode.value;
|
const triggerNode = chatTriggerNode.value;
|
||||||
|
|
||||||
if (!triggerNode) {
|
if (!triggerNode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatChildren = workflow.getChildNodes(triggerNode.name);
|
const chatChildren = workflow.value.getChildNodes(triggerNode.name);
|
||||||
|
|
||||||
const chatRootNode = chatChildren
|
const chatRootNode = chatChildren
|
||||||
.reverse()
|
.reverse()
|
||||||
.map((nodeName: string) => workflowsStore.getNodeByName(nodeName))
|
.map((nodeName: string) => getNodeByName(nodeName))
|
||||||
.filter((n): n is INodeUi => n !== null)
|
.filter((n): n is INodeUi => n !== null)
|
||||||
// Reverse the nodes to match the last node logs first
|
// Reverse the nodes to match the last node logs first
|
||||||
.reverse()
|
.reverse()
|
||||||
.find((storeNode: INodeUi): boolean => {
|
.find((storeNode: INodeUi): boolean => {
|
||||||
// Skip summarization nodes
|
// Skip summarization nodes
|
||||||
if (storeNode.type === CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE) return false;
|
if (storeNode.type === CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE) return false;
|
||||||
const nodeType = nodeTypesStore.getNodeType(storeNode.type, storeNode.typeVersion);
|
const nodeType = getNodeType(storeNode.type, storeNode.typeVersion);
|
||||||
|
|
||||||
if (!nodeType) return false;
|
if (!nodeType) return false;
|
||||||
|
|
||||||
|
@ -94,10 +95,10 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
|
||||||
let isCustomChainOrAgent = false;
|
let isCustomChainOrAgent = false;
|
||||||
if (nodeType.name === AI_CODE_NODE_TYPE) {
|
if (nodeType.name === AI_CODE_NODE_TYPE) {
|
||||||
// Get node connection types for inputs and outputs
|
// Get node connection types for inputs and outputs
|
||||||
const inputs = NodeHelpers.getNodeInputs(workflow, storeNode, nodeType);
|
const inputs = NodeHelpers.getNodeInputs(workflow.value, storeNode, nodeType);
|
||||||
const inputTypes = NodeHelpers.getConnectionTypes(inputs);
|
const inputTypes = NodeHelpers.getConnectionTypes(inputs);
|
||||||
|
|
||||||
const outputs = NodeHelpers.getNodeOutputs(workflow, storeNode, nodeType);
|
const outputs = NodeHelpers.getNodeOutputs(workflow.value, storeNode, nodeType);
|
||||||
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
||||||
|
|
||||||
// Validate if node has required AI connection types
|
// Validate if node has required AI connection types
|
||||||
|
@ -114,14 +115,14 @@ export function useChatTrigger({ router }: { router: ReturnType<typeof useRouter
|
||||||
if (!isAgent && !isChain && !isCustomChainOrAgent) return false;
|
if (!isAgent && !isChain && !isCustomChainOrAgent) return false;
|
||||||
|
|
||||||
// Check if this node is connected to the trigger node
|
// Check if this node is connected to the trigger node
|
||||||
const parentNodes = workflow.getParentNodes(storeNode.name);
|
const parentNodes = workflow.value.getParentNodes(storeNode.name);
|
||||||
const isChatChild = parentNodes.some(
|
const isChatChild = parentNodes.some(
|
||||||
(parentNodeName) => parentNodeName === triggerNode.name,
|
(parentNodeName) => parentNodeName === triggerNode.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
const resut = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
const result = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
||||||
return resut;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
connectedNode.value = chatRootNode ?? null;
|
connectedNode.value = chatRootNode ?? null;
|
||||||
|
|
|
@ -132,7 +132,6 @@ export function useResize(container: Ref<HTMLElement | undefined>) {
|
||||||
onWindowResize,
|
onWindowResize,
|
||||||
onResizeDebounced,
|
onResizeDebounced,
|
||||||
onResizeChatDebounced,
|
onResizeChatDebounced,
|
||||||
// onResizeChat,
|
|
||||||
panelToContainerRatio,
|
panelToContainerRatio,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
|
||||||
import { fireEvent, waitFor } from '@testing-library/vue';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
|
||||||
import type { IConnections, INode } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import WorkflowLMChatModal from '@/components/WorkflowLMChat/WorkflowLMChat.vue';
|
|
||||||
import { WORKFLOW_LM_CHAT_MODAL_KEY } from '@/constants';
|
|
||||||
import type { IWorkflowDb } from '@/Interface';
|
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
||||||
|
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
|
||||||
import { setupServer } from '@/__tests__/server';
|
|
||||||
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks';
|
|
||||||
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
|
|
||||||
|
|
||||||
const connections: IConnections = {
|
|
||||||
'Chat Trigger': {
|
|
||||||
main: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
node: 'Agent',
|
|
||||||
type: NodeConnectionType.Main,
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(WorkflowLMChatModal, {
|
|
||||||
props: {
|
|
||||||
teleported: false,
|
|
||||||
appendToBody: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function createPiniaWithAINodes(options = { withConnections: true, withAgentNode: true }) {
|
|
||||||
const { withConnections, withAgentNode } = options;
|
|
||||||
|
|
||||||
const chatTriggerNode = mockNodes[4];
|
|
||||||
const agentNode = mockNodes[5];
|
|
||||||
const nodes: INode[] = [chatTriggerNode];
|
|
||||||
if (withAgentNode) nodes.push(agentNode);
|
|
||||||
const workflow = mock<IWorkflowDb>({
|
|
||||||
nodes,
|
|
||||||
...(withConnections ? { connections } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const pinia = createPinia();
|
|
||||||
setActivePinia(pinia);
|
|
||||||
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
|
||||||
const uiStore = useUIStore();
|
|
||||||
|
|
||||||
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
|
|
||||||
workflowsStore.workflow = workflow;
|
|
||||||
|
|
||||||
await useSettingsStore().getSettings();
|
|
||||||
await useUsersStore().loginWithCookie();
|
|
||||||
uiStore.openModal(WORKFLOW_LM_CHAT_MODAL_KEY);
|
|
||||||
|
|
||||||
return pinia;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('WorkflowLMChatModal', () => {
|
|
||||||
let server: ReturnType<typeof setupServer>;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
server = setupServer();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
createAppModals();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
cleanupAppModals();
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
server.shutdown();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render correctly', async () => {
|
|
||||||
const { getByTestId } = renderComponent({
|
|
||||||
pinia: await createPiniaWithAINodes(),
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(getByTestId('lmChat-modal')).toBeInTheDocument());
|
|
||||||
|
|
||||||
expect(getByTestId('workflow-lm-chat-dialog')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send and display chat message', async () => {
|
|
||||||
const { getByTestId } = renderComponent({
|
|
||||||
pinia: await createPiniaWithAINodes({
|
|
||||||
withConnections: true,
|
|
||||||
withAgentNode: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(getByTestId('lmChat-modal')).toBeInTheDocument());
|
|
||||||
|
|
||||||
const chatDialog = getByTestId('workflow-lm-chat-dialog');
|
|
||||||
const chatInputsContainer = getByTestId('lm-chat-inputs');
|
|
||||||
const chatSendButton = chatInputsContainer.querySelector('.chat-input-send-button');
|
|
||||||
const chatInput = chatInputsContainer.querySelector('textarea');
|
|
||||||
|
|
||||||
if (chatInput && chatSendButton) {
|
|
||||||
await fireEvent.update(chatInput, 'Hello!');
|
|
||||||
await fireEvent.click(chatSendButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(chatDialog.querySelectorAll('.chat-message-from-user')).toHaveLength(1),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(chatDialog.querySelector('.chat-message-from-user')).toHaveTextContent('Hello!');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -37,7 +37,6 @@ import { get } from 'lodash-es';
|
||||||
import { useExecutionsStore } from '@/stores/executions.store';
|
import { useExecutionsStore } from '@/stores/executions.store';
|
||||||
import type { PushPayload } from '@n8n/api-types';
|
import type { PushPayload } from '@n8n/api-types';
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
import { useCanvasStore } from '@/stores/canvas.store';
|
|
||||||
|
|
||||||
const FORM_RELOAD = 'n8n_redirect_to_next_form_test_page';
|
const FORM_RELOAD = 'n8n_redirect_to_next_form_test_page';
|
||||||
|
|
||||||
|
@ -51,7 +50,6 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const executionsStore = useExecutionsStore();
|
const executionsStore = useExecutionsStore();
|
||||||
const canvasStore = useCanvasStore();
|
|
||||||
// Starts to execute a workflow on server
|
// Starts to execute a workflow on server
|
||||||
async function runWorkflowApi(runData: IStartRunData): Promise<IExecutionPushResponse> {
|
async function runWorkflowApi(runData: IStartRunData): Promise<IExecutionPushResponse> {
|
||||||
if (!rootStore.pushConnectionActive) {
|
if (!rootStore.pushConnectionActive) {
|
||||||
|
|
Loading…
Reference in a new issue