From 5f53d76e39395a8effdfeba0677f333b509ec8c8 Mon Sep 17 00:00:00 2001 From: oleg Date: Thu, 21 Mar 2024 09:23:15 +0100 Subject: [PATCH] fix(editor): Fix opening of chat window when executing a child node (#8789) Signed-off-by: Oleg Ivaniv Co-authored-by: Michael Kret --- cypress/composables/modals/chat-modal.ts | 3 ++ cypress/e2e/30-langchain.cy.ts | 24 ++++---------- .../src/components/NodeExecuteButton.vue | 6 +++- .../src/components/WorkflowLMChat.vue | 21 ++++++++++++ .../__tests__/WorkflowLMChatModal.test.ts | 5 ++- .../src/composables/usePinnedData.ts | 6 +++- .../src/composables/useRunWorkflow.ts | 32 ++++++++++++++++++- .../editor-ui/src/n8n-theme-variables.scss | 4 --- packages/editor-ui/src/n8n-theme.scss | 3 ++ .../src/plugins/i18n/locales/en.json | 4 +++ .../editor-ui/src/stores/workflows.store.ts | 14 ++++++++ 11 files changed, 97 insertions(+), 25 deletions(-) diff --git a/cypress/composables/modals/chat-modal.ts b/cypress/composables/modals/chat-modal.ts index 268419f3c8..31e139c93e 100644 --- a/cypress/composables/modals/chat-modal.ts +++ b/cypress/composables/modals/chat-modal.ts @@ -25,6 +25,9 @@ export function getManualChatModalCloseButton() { export function getManualChatModalLogs() { return getManualChatModal().getByTestId('lm-chat-logs'); } +export function getManualChatDialog() { + return getManualChatModal().getByTestId('workflow-lm-chat-dialog'); +} export function getManualChatModalLogsTree() { return getManualChatModalLogs().getByTestId('lm-chat-logs-tree'); diff --git a/cypress/e2e/30-langchain.cy.ts b/cypress/e2e/30-langchain.cy.ts index fd98811f2c..6b69d3fb65 100644 --- a/cypress/e2e/30-langchain.cy.ts +++ b/cypress/e2e/30-langchain.cy.ts @@ -25,15 +25,12 @@ import { clickCreateNewCredential, clickExecuteNode, clickGetBackToCanvas, - getOutputPanelTable, - getParameterInputByName, - setParameterInputByName, - setParameterSelectByContent, toggleParameterCheckboxInputByName, } from '../composables/ndv'; import { setCredentialValues } from '../composables/modals/credential-modal'; import { closeManualChatModal, + getManualChatDialog, getManualChatMessages, getManualChatModalLogs, getManualChatModalLogsEntries, @@ -98,15 +95,12 @@ describe('Langchain Integration', () => { clickGetBackToCanvas(); openNode(BASIC_LLM_CHAIN_NODE_NAME); - - setParameterSelectByContent('promptType', 'Define below') const inputMessage = 'Hello!'; const outputMessage = 'Hi there! How can I assist you today?'; - setParameterInputByName('text', inputMessage); - + clickExecuteNode() runMockWorkflowExcution({ - trigger: () => clickExecuteNode(), + trigger: () => sendManualChatMessage(inputMessage), runData: [ createMockNodeExecutionData(BASIC_LLM_CHAIN_NODE_NAME, { jsonData: { @@ -120,8 +114,7 @@ describe('Langchain Integration', () => { lastNodeExecuted: BASIC_LLM_CHAIN_NODE_NAME, }); - getOutputPanelTable().should('contain', 'output'); - getOutputPanelTable().should('contain', outputMessage); + getManualChatDialog().should('contain', outputMessage); }); it('should be able to open and execute Agent node', () => { @@ -141,11 +134,9 @@ describe('Langchain Integration', () => { const inputMessage = 'Hello!'; const outputMessage = 'Hi there! How can I assist you today?'; - setParameterSelectByContent('promptType', 'Define below') - setParameterInputByName('text', inputMessage); - + clickExecuteNode() runMockWorkflowExcution({ - trigger: () => clickExecuteNode(), + trigger: () => sendManualChatMessage(inputMessage), runData: [ createMockNodeExecutionData(AGENT_NODE_NAME, { jsonData: { @@ -159,8 +150,7 @@ describe('Langchain Integration', () => { lastNodeExecuted: AGENT_NODE_NAME, }); - getOutputPanelTable().should('contain', 'output'); - getOutputPanelTable().should('contain', outputMessage); + getManualChatDialog().should('contain', outputMessage); }); it('should add and use Manual Chat Trigger node together with Agent node', () => { diff --git a/packages/editor-ui/src/components/NodeExecuteButton.vue b/packages/editor-ui/src/components/NodeExecuteButton.vue index d7e41f79e3..acf9db35db 100644 --- a/packages/editor-ui/src/components/NodeExecuteButton.vue +++ b/packages/editor-ui/src/components/NodeExecuteButton.vue @@ -126,6 +126,9 @@ export default defineComponent({ isChatNode(): boolean { return Boolean(this.nodeType && this.nodeType.name === CHAT_TRIGGER_NODE_TYPE); }, + isChatChild(): boolean { + return this.workflowsStore.checkIfNodeHasChatParent(this.nodeName); + }, isFormTriggerNode(): boolean { return Boolean(this.nodeType && this.nodeType.name === FORM_TRIGGER_NODE_TYPE); }, @@ -226,7 +229,8 @@ export default defineComponent({ }, async onClick() { - if (this.isChatNode) { + // Show chat if it's a chat node or a child of a chat node with no input data + if (this.isChatNode || (this.isChatChild && this.ndvStore.isDNVDataEmpty('input'))) { this.ndvStore.setActiveNodeName(null); nodeViewEventBus.emit('openChat'); } else if (this.isListeningForEvents) { diff --git a/packages/editor-ui/src/components/WorkflowLMChat.vue b/packages/editor-ui/src/components/WorkflowLMChat.vue index 53941274f6..3d4d19e7e5 100644 --- a/packages/editor-ui/src/components/WorkflowLMChat.vue +++ b/packages/editor-ui/src/components/WorkflowLMChat.vue @@ -122,6 +122,7 @@ import { defineAsyncComponent, defineComponent } from 'vue'; import { mapStores } from 'pinia'; import { useToast } from '@/composables/useToast'; +import { useMessage } from '@/composables/useMessage'; import Modal from '@/components/Modal.vue'; import { AI_CATEGORY_AGENTS, @@ -132,6 +133,7 @@ import { CHAT_EMBED_MODAL_KEY, CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE, + MODAL_CONFIRM, VIEWS, WORKFLOW_LM_CHAT_MODAL_KEY, } from '@/constants'; @@ -153,6 +155,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useRouter } from 'vue-router'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useRunWorkflow } from '@/composables/useRunWorkflow'; +import { usePinnedData } from '@/composables/usePinnedData'; const RunDataAi = defineAsyncComponent( async () => await import('@/components/RunDataAi/RunDataAi.vue'), @@ -197,6 +200,7 @@ export default defineComponent({ externalHooks, workflowHelpers, ...useToast(), + ...useMessage(), }; }, data() { @@ -273,6 +277,23 @@ export default defineComponent({ ); return; } + + const pinnedChatData = usePinnedData(this.getTriggerNode()); + if (pinnedChatData.hasData.value) { + const confirmResult = await this.confirm( + this.$locale.baseText('chat.window.chat.unpinAndExecute.description'), + this.$locale.baseText('chat.window.chat.unpinAndExecute.title'), + { + confirmButtonText: this.$locale.baseText('chat.window.chat.unpinAndExecute.confirm'), + cancelButtonText: this.$locale.baseText('chat.window.chat.unpinAndExecute.cancel'), + }, + ); + + if (!(confirmResult === MODAL_CONFIRM)) return; + + pinnedChatData.unsetData('unpin-and-send-chat-message-modal'); + } + this.messages.push({ text: message, sender: 'user', diff --git a/packages/editor-ui/src/components/__tests__/WorkflowLMChatModal.test.ts b/packages/editor-ui/src/components/__tests__/WorkflowLMChatModal.test.ts index 34dc0ceb96..cd78dbf162 100644 --- a/packages/editor-ui/src/components/__tests__/WorkflowLMChatModal.test.ts +++ b/packages/editor-ui/src/components/__tests__/WorkflowLMChatModal.test.ts @@ -139,7 +139,10 @@ describe('WorkflowLMChatModal', () => { it('should send and display chat message', async () => { const wrapper = renderComponent({ - pinia: await createPiniaWithAINodes(), + pinia: await createPiniaWithAINodes({ + withConnections: true, + withAgentNode: true, + }), }); await waitFor(() => diff --git a/packages/editor-ui/src/composables/usePinnedData.ts b/packages/editor-ui/src/composables/usePinnedData.ts index 31efcb950a..1e15a1eb56 100644 --- a/packages/editor-ui/src/composables/usePinnedData.ts +++ b/packages/editor-ui/src/composables/usePinnedData.ts @@ -28,7 +28,11 @@ export type PinDataSource = | 'context-menu' | 'keyboard-shortcut'; -export type UnpinDataSource = 'unpin-and-execute-modal' | 'context-menu' | 'keyboard-shortcut'; +export type UnpinDataSource = + | 'unpin-and-execute-modal' + | 'context-menu' + | 'keyboard-shortcut' + | 'unpin-and-send-chat-message-modal'; export function usePinnedData( node: MaybeRef, diff --git a/packages/editor-ui/src/composables/useRunWorkflow.ts b/packages/editor-ui/src/composables/useRunWorkflow.ts index 5e4cee4427..cfa1a2bedf 100644 --- a/packages/editor-ui/src/composables/useRunWorkflow.ts +++ b/packages/editor-ui/src/composables/useRunWorkflow.ts @@ -24,7 +24,12 @@ import { import { useToast } from '@/composables/useToast'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; -import { FORM_TRIGGER_NODE_TYPE, WAIT_NODE_TYPE } from '@/constants'; +import { + CHAT_TRIGGER_NODE_TYPE, + FORM_TRIGGER_NODE_TYPE, + WAIT_NODE_TYPE, + WORKFLOW_LM_CHAT_MODAL_KEY, +} from '@/constants'; import { useTitleChange } from '@/composables/useTitleChange'; import { useRootStore } from '@/stores/n8nRoot.store'; import { useUIStore } from '@/stores/ui.store'; @@ -198,6 +203,10 @@ export function useRunWorkflow(options: { router: ReturnType } ); const { startNodeNames } = consolidatedData; + const destinationNodeType = options.destinationNode + ? workflowsStore.getNodeByName(options.destinationNode)?.type + : ''; + let { runData: newRunData } = consolidatedData; let executedNode: string | undefined; if ( @@ -217,6 +226,27 @@ export function useRunWorkflow(options: { router: ReturnType } executedNode = options.triggerNode; } + // If the destination node is specified, check if it is a chat node or has a chat parent + if ( + options.destinationNode && + (workflowsStore.checkIfNodeHasChatParent(options.destinationNode) || + destinationNodeType === CHAT_TRIGGER_NODE_TYPE) + ) { + const startNode = workflow.getStartNode(options.destinationNode); + if (startNode && startNode.type === CHAT_TRIGGER_NODE_TYPE) { + // Check if the chat node has input data or pin data + const chatHasInputData = + nodeHelpers.getNodeInputData(startNode, 0, 0, 'input')?.length > 0; + const chatHasPinData = !!workflowData.pinData?.[startNode.name]; + + // If the chat node has no input data or pin data, open the chat modal + // and halt the execution + if (!chatHasInputData && !chatHasPinData) { + uiStore.openModal(WORKFLOW_LM_CHAT_MODAL_KEY); + return; + } + } + } const startNodes: StartNodeData[] = startNodeNames.map((name) => { // Find for each start node the source data let sourceData = get(runData, [name, 0, 'source', 0], null); diff --git a/packages/editor-ui/src/n8n-theme-variables.scss b/packages/editor-ui/src/n8n-theme-variables.scss index b2040dba3e..1e3a59ba9f 100644 --- a/packages/editor-ui/src/n8n-theme-variables.scss +++ b/packages/editor-ui/src/n8n-theme-variables.scss @@ -34,10 +34,6 @@ $badge-warning-color: var(--color-text-dark); // Warning tooltip $warning-tooltip-color: var(--color-danger); -:root { - // Using native css variable enables us to use this value in JS - --header-height: 65; -} // sass variable is used for scss files $header-height: calc(var(--header-height) * 1px); diff --git a/packages/editor-ui/src/n8n-theme.scss b/packages/editor-ui/src/n8n-theme.scss index d6bf0f9a81..93061ca129 100644 --- a/packages/editor-ui/src/n8n-theme.scss +++ b/packages/editor-ui/src/n8n-theme.scss @@ -173,6 +173,9 @@ --node-error-output-color: #991818; --chat--spacing: var(--spacing-s); + + // Using native css variable enables us to use this value in JS + --header-height: 65; } .clickable { diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 27a38464e3..0d6455d76f 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -148,6 +148,10 @@ "chat.window.chat.chatMessageOptions.reuseMessage": "Reuse Message", "chat.window.chat.chatMessageOptions.repostMessage": "Repost Message", "chat.window.chat.chatMessageOptions.executionId": "Execution ID", + "chat.window.chat.unpinAndExecute.description": "Sending the message overwrites the pinned chat node data.", + "chat.window.chat.unpinAndExecute.title": "Unpin chat output data?", + "chat.window.chat.unpinAndExecute.confirm": "Unpin and send", + "chat.window.chat.unpinAndExecute.cancel": "Cancel", "chatEmbed.infoTip.description": "Add chat to external applications using the n8n chat package.", "chatEmbed.infoTip.link": "More info", "chatEmbed.title": "Embed Chat in your website", diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index a61e311516..8d034c48c5 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -1,4 +1,5 @@ import { + CHAT_TRIGGER_NODE_TYPE, DEFAULT_NEW_WORKFLOW_NAME, DUPLICATE_POSTFFIX, EnterpriseEditionFeature, @@ -1460,5 +1461,18 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { appendChatMessage(message: string): void { this.chatMessages.push(message); }, + + checkIfNodeHasChatParent(nodeName: string): boolean { + const workflow = this.getCurrentWorkflow(); + const parents = workflow.getParentNodes(nodeName, 'main'); + + const matchedChatNode = parents.find((parent) => { + const parentNodeType = this.getNodeByName(parent)?.type; + + return parentNodeType === CHAT_TRIGGER_NODE_TYPE; + }); + + return !!matchedChatNode; + }, }, });