fix(editor): Fix opening of chat window when executing a child node (#8789)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
oleg 2024-03-21 09:23:15 +01:00 committed by GitHub
parent 5e84c2ab89
commit 5f53d76e39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 97 additions and 25 deletions

View file

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

View file

@ -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', () => {

View file

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

View file

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

View file

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

View file

@ -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<INodeUi | null>,

View file

@ -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<typeof useRouter> }
);
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<typeof useRouter> }
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);

View file

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

View file

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

View file

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

View file

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