Remove global state dependencies from composables

This commit is contained in:
Oleg Ivaniv 2024-11-07 08:47:19 +01:00
parent 078f1b0045
commit 32b4eb523b
No known key found for this signature in database
9 changed files with 244 additions and 260 deletions

View file

@ -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%;

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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)

View file

@ -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;

View file

@ -132,7 +132,6 @@ export function useResize(container: Ref<HTMLElement | undefined>) {
onWindowResize, onWindowResize,
onResizeDebounced, onResizeDebounced,
onResizeChatDebounced, onResizeChatDebounced,
// onResizeChat,
panelToContainerRatio, panelToContainerRatio,
}; };
} }

View file

@ -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!');
});
});

View file

@ -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) {