From 32b4eb523b3a168014b98507f7a9ad709dea71f8 Mon Sep 17 00:00:00 2001 From: Oleg Ivaniv Date: Thu, 7 Nov 2024 08:47:19 +0100 Subject: [PATCH] Remove global state dependencies from composables --- .../src/components/CanvasChat/CanvasChat.vue | 220 +++++++++++++----- .../CanvasChat/components/ChatLogsPanel.vue | 11 +- .../components/ChatMessagesPanel.vue | 2 +- .../components/MessageOptionAction.vue | 2 +- .../composables/useChatMessaging.ts | 86 ++++--- .../CanvasChat/composables/useChatTrigger.ts | 53 ++--- .../CanvasChat/composables/useResize.ts | 1 - .../components/WorkflowLMChatModal.test.ts | 127 ---------- .../src/composables/useRunWorkflow.ts | 2 - 9 files changed, 244 insertions(+), 260 deletions(-) delete mode 100644 packages/editor-ui/src/components/WorkflowLMChatModal.test.ts diff --git a/packages/editor-ui/src/components/CanvasChat/CanvasChat.vue b/packages/editor-ui/src/components/CanvasChat/CanvasChat.vue index 8464ee14cf..cdb68d0227 100644 --- a/packages/editor-ui/src/components/CanvasChat/CanvasChat.vue +++ b/packages/editor-ui/src/components/CanvasChat/CanvasChat.vue @@ -1,44 +1,121 @@ diff --git a/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts b/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts index 5d12596a6a..cb7ff0655d 100644 --- a/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts +++ b/packages/editor-ui/src/components/CanvasChat/composables/useChatMessaging.ts @@ -1,5 +1,5 @@ -import type { Ref } from 'vue'; -import { computed, ref } from 'vue'; +import type { ComputedRef, Ref } from 'vue'; +import { ref } from 'vue'; import { v4 as uuid } from 'uuid'; import type { ChatMessage, ChatMessageText } from '@n8n/chat/types'; import { NodeConnectionType, CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow'; @@ -10,41 +10,53 @@ import type { IDataObject, IBinaryData, BinaryFileType, + Workflow, + IRunExecutionData, } from 'n8n-workflow'; -import { useWorkflowsStore } from '@/stores/workflows.store'; -import { useRunWorkflow } from '@/composables/useRunWorkflow'; import { useToast } from '@/composables/useToast'; import { useMessage } from '@/composables/useMessage'; import { usePinnedData } from '@/composables/usePinnedData'; import { get, isEmpty, last } from 'lodash-es'; import { MANUAL_CHAT_TRIGGER_NODE_TYPE, MODAL_CONFIRM } from '@/constants'; import { useI18n } from '@/composables/useI18n'; -import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; -import type { useRouter } from 'vue-router'; import type { MemoryOutput } from '../types/chat'; -import { useUIStore } from '@/stores/ui.store'; -import type { INodeUi } from '@/Interface'; +import type { IExecutionPushResponse, INodeUi } from '@/Interface'; -export function useChatMessaging({ - router, - chatTrigger, - messages, - sessionId, -}: { - router: ReturnType; +export type RunWorkflowChatPayload = { + triggerNode: string; + nodeData: ITaskData; + source: string; + message: string; +}; +export interface ChatMessagingDependencies { chatTrigger: Ref; + connectedNode: Ref; messages: Ref; sessionId: Ref; -}) { - const previousMessageIndex = ref(0); - const workflowsStore = useWorkflowsStore(); - const { runWorkflow } = useRunWorkflow({ router }); - const workflowHelpers = useWorkflowHelpers({ router }); - const { showError } = useToast(); - const uiStore = useUIStore(); - const locale = useI18n(); + workflow: ComputedRef; + isLoading: ComputedRef; + executionResultData: ComputedRef; + getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null; + onRunChatWorkflow: ( + payload: RunWorkflowChatPayload, + ) => Promise; +} + +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 */ async function convertFileToBinaryData(file: File): Promise { const reader = new FileReader(); @@ -136,13 +148,19 @@ export function useChatMessaging({ source: [null], }; - const response = await runWorkflow({ + const response = await onRunChatWorkflow({ triggerNode: triggerNode.name, nodeData, source: 'RunData.ManualChatMessage', + message, }); + // const response = await runWorkflow({ + // triggerNode: triggerNode.name, + // nodeData, + // source: 'RunData.ManualChatMessage', + // }); - workflowsStore.appendChatMessage(message); + // workflowsStore.appendChatMessage(message); if (!response?.executionId) { showError( new Error('It was not possible to start workflow!'), @@ -160,14 +178,12 @@ export function useChatMessaging({ if (!isLoading.value) { clearInterval(waitInterval); - const lastNodeExecuted = - workflowsStore.getWorkflowExecution?.data?.resultData.lastNodeExecuted; + const lastNodeExecuted = executionResultData.value?.lastNodeExecuted; if (!lastNodeExecuted) return; const nodeResponseDataArray = - get(workflowsStore.getWorkflowExecution?.data?.resultData.runData, lastNodeExecuted) ?? - []; + get(executionResultData.value.runData, lastNodeExecuted) ?? []; const nodeResponseData = nodeResponseDataArray[nodeResponseDataArray.length - 1]; @@ -251,18 +267,20 @@ export function useChatMessaging({ } function getChatMessages(): ChatMessageText[] { - if (!chatTrigger.value) return []; + console.log('Getting chat messages', connectedNode.value); + if (!connectedNode.value) return []; - const workflow = workflowHelpers.getCurrentWorkflow(); const connectedMemoryInputs = - workflow.connectionsByDestinationNode[chatTrigger.value.name]?.[NodeConnectionType.AiMemory]; + 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 nodeResultData = getWorkflowResultDataByNodeName(memoryConnection.node); const memoryOutputData = (nodeResultData ?? []) .map((data) => get(data, ['data', NodeConnectionType.AiMemory, 0, 0, 'json']) as MemoryOutput) diff --git a/packages/editor-ui/src/components/CanvasChat/composables/useChatTrigger.ts b/packages/editor-ui/src/components/CanvasChat/composables/useChatTrigger.ts index 753229fd7c..41d67cd61f 100644 --- a/packages/editor-ui/src/components/CanvasChat/composables/useChatTrigger.ts +++ b/packages/editor-ui/src/components/CanvasChat/composables/useChatTrigger.ts @@ -1,13 +1,17 @@ +import type { ComputedRef } from 'vue'; import { ref, computed } from 'vue'; import { CHAIN_SUMMARIZATION_LANGCHAIN_NODE_TYPE, NodeConnectionType, NodeHelpers, - type INode, - type INodeParameters, - type INodeType, } from 'n8n-workflow'; -import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; +import type { + INodeTypeDescription, + Workflow, + INode, + INodeParameters, + INodeType, +} from 'n8n-workflow'; import { AI_CATEGORY_AGENTS, AI_CATEGORY_CHAINS, @@ -17,19 +21,19 @@ import { MANUAL_CHAT_TRIGGER_NODE_TYPE, } from '@/constants'; 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 }) { +export interface ChatTriggerDependencies { + getNodeByName: (name: string) => INodeUi | null; + getNodeType: (type: string, version: number) => INodeTypeDescription | null; + workflow: ComputedRef; +} + +export function useChatTrigger({ getNodeByName, getNodeType, workflow }: ChatTriggerDependencies) { const chatTriggerName = ref(null); const connectedNode = ref(null); - const workflowsStore = useWorkflowsStore(); - const nodeTypesStore = useNodeTypesStore(); - const workflowHelpers = useWorkflowHelpers({ router }); const chatTriggerNode = computed(() => - chatTriggerName.value ? workflowsStore.getNodeByName(chatTriggerName.value) : null, + chatTriggerName.value ? getNodeByName(chatTriggerName.value) : null, ); const allowFileUploads = computed(() => { @@ -48,11 +52,9 @@ export function useChatTrigger({ router }: { router: ReturnType - [CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name), - ); + const triggerNode = workflow.value.queryNodes((nodeType: INodeType) => + [CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name), + ); if (!triggerNode.length) { return; @@ -62,25 +64,24 @@ export function useChatTrigger({ router }: { router: ReturnType workflowsStore.getNodeByName(nodeName)) + .map((nodeName: string) => getNodeByName(nodeName)) .filter((n): n is INodeUi => n !== null) // Reverse the nodes to match the last node logs first .reverse() .find((storeNode: INodeUi): boolean => { // Skip summarization nodes 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; @@ -94,10 +95,10 @@ export function useChatTrigger({ router }: { router: ReturnType parentNodeName === triggerNode.name, ); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const resut = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent)); - return resut; + const result = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent)); + return result; }); connectedNode.value = chatRootNode ?? null; diff --git a/packages/editor-ui/src/components/CanvasChat/composables/useResize.ts b/packages/editor-ui/src/components/CanvasChat/composables/useResize.ts index 5338376338..cdbfcb82c2 100644 --- a/packages/editor-ui/src/components/CanvasChat/composables/useResize.ts +++ b/packages/editor-ui/src/components/CanvasChat/composables/useResize.ts @@ -132,7 +132,6 @@ export function useResize(container: Ref) { onWindowResize, onResizeDebounced, onResizeChatDebounced, - // onResizeChat, panelToContainerRatio, }; } diff --git a/packages/editor-ui/src/components/WorkflowLMChatModal.test.ts b/packages/editor-ui/src/components/WorkflowLMChatModal.test.ts deleted file mode 100644 index 8fa20d0965..0000000000 --- a/packages/editor-ui/src/components/WorkflowLMChatModal.test.ts +++ /dev/null @@ -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({ - 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; - - 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!'); - }); -}); diff --git a/packages/editor-ui/src/composables/useRunWorkflow.ts b/packages/editor-ui/src/composables/useRunWorkflow.ts index 230a95a803..a944a93348 100644 --- a/packages/editor-ui/src/composables/useRunWorkflow.ts +++ b/packages/editor-ui/src/composables/useRunWorkflow.ts @@ -37,7 +37,6 @@ import { get } from 'lodash-es'; import { useExecutionsStore } from '@/stores/executions.store'; import type { PushPayload } from '@n8n/api-types'; import { useLocalStorage } from '@vueuse/core'; -import { useCanvasStore } from '@/stores/canvas.store'; const FORM_RELOAD = 'n8n_redirect_to_next_form_test_page'; @@ -51,7 +50,6 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType { if (!rootStore.pushConnectionActive) {