mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
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:
parent
5e84c2ab89
commit
5f53d76e39
|
@ -25,6 +25,9 @@ export function getManualChatModalCloseButton() {
|
||||||
export function getManualChatModalLogs() {
|
export function getManualChatModalLogs() {
|
||||||
return getManualChatModal().getByTestId('lm-chat-logs');
|
return getManualChatModal().getByTestId('lm-chat-logs');
|
||||||
}
|
}
|
||||||
|
export function getManualChatDialog() {
|
||||||
|
return getManualChatModal().getByTestId('workflow-lm-chat-dialog');
|
||||||
|
}
|
||||||
|
|
||||||
export function getManualChatModalLogsTree() {
|
export function getManualChatModalLogsTree() {
|
||||||
return getManualChatModalLogs().getByTestId('lm-chat-logs-tree');
|
return getManualChatModalLogs().getByTestId('lm-chat-logs-tree');
|
||||||
|
|
|
@ -25,15 +25,12 @@ import {
|
||||||
clickCreateNewCredential,
|
clickCreateNewCredential,
|
||||||
clickExecuteNode,
|
clickExecuteNode,
|
||||||
clickGetBackToCanvas,
|
clickGetBackToCanvas,
|
||||||
getOutputPanelTable,
|
|
||||||
getParameterInputByName,
|
|
||||||
setParameterInputByName,
|
|
||||||
setParameterSelectByContent,
|
|
||||||
toggleParameterCheckboxInputByName,
|
toggleParameterCheckboxInputByName,
|
||||||
} from '../composables/ndv';
|
} from '../composables/ndv';
|
||||||
import { setCredentialValues } from '../composables/modals/credential-modal';
|
import { setCredentialValues } from '../composables/modals/credential-modal';
|
||||||
import {
|
import {
|
||||||
closeManualChatModal,
|
closeManualChatModal,
|
||||||
|
getManualChatDialog,
|
||||||
getManualChatMessages,
|
getManualChatMessages,
|
||||||
getManualChatModalLogs,
|
getManualChatModalLogs,
|
||||||
getManualChatModalLogsEntries,
|
getManualChatModalLogsEntries,
|
||||||
|
@ -98,15 +95,12 @@ describe('Langchain Integration', () => {
|
||||||
clickGetBackToCanvas();
|
clickGetBackToCanvas();
|
||||||
|
|
||||||
openNode(BASIC_LLM_CHAIN_NODE_NAME);
|
openNode(BASIC_LLM_CHAIN_NODE_NAME);
|
||||||
|
|
||||||
setParameterSelectByContent('promptType', 'Define below')
|
|
||||||
const inputMessage = 'Hello!';
|
const inputMessage = 'Hello!';
|
||||||
const outputMessage = 'Hi there! How can I assist you today?';
|
const outputMessage = 'Hi there! How can I assist you today?';
|
||||||
|
|
||||||
setParameterInputByName('text', inputMessage);
|
clickExecuteNode()
|
||||||
|
|
||||||
runMockWorkflowExcution({
|
runMockWorkflowExcution({
|
||||||
trigger: () => clickExecuteNode(),
|
trigger: () => sendManualChatMessage(inputMessage),
|
||||||
runData: [
|
runData: [
|
||||||
createMockNodeExecutionData(BASIC_LLM_CHAIN_NODE_NAME, {
|
createMockNodeExecutionData(BASIC_LLM_CHAIN_NODE_NAME, {
|
||||||
jsonData: {
|
jsonData: {
|
||||||
|
@ -120,8 +114,7 @@ describe('Langchain Integration', () => {
|
||||||
lastNodeExecuted: BASIC_LLM_CHAIN_NODE_NAME,
|
lastNodeExecuted: BASIC_LLM_CHAIN_NODE_NAME,
|
||||||
});
|
});
|
||||||
|
|
||||||
getOutputPanelTable().should('contain', 'output');
|
getManualChatDialog().should('contain', outputMessage);
|
||||||
getOutputPanelTable().should('contain', outputMessage);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to open and execute Agent node', () => {
|
it('should be able to open and execute Agent node', () => {
|
||||||
|
@ -141,11 +134,9 @@ describe('Langchain Integration', () => {
|
||||||
const inputMessage = 'Hello!';
|
const inputMessage = 'Hello!';
|
||||||
const outputMessage = 'Hi there! How can I assist you today?';
|
const outputMessage = 'Hi there! How can I assist you today?';
|
||||||
|
|
||||||
setParameterSelectByContent('promptType', 'Define below')
|
clickExecuteNode()
|
||||||
setParameterInputByName('text', inputMessage);
|
|
||||||
|
|
||||||
runMockWorkflowExcution({
|
runMockWorkflowExcution({
|
||||||
trigger: () => clickExecuteNode(),
|
trigger: () => sendManualChatMessage(inputMessage),
|
||||||
runData: [
|
runData: [
|
||||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||||
jsonData: {
|
jsonData: {
|
||||||
|
@ -159,8 +150,7 @@ describe('Langchain Integration', () => {
|
||||||
lastNodeExecuted: AGENT_NODE_NAME,
|
lastNodeExecuted: AGENT_NODE_NAME,
|
||||||
});
|
});
|
||||||
|
|
||||||
getOutputPanelTable().should('contain', 'output');
|
getManualChatDialog().should('contain', outputMessage);
|
||||||
getOutputPanelTable().should('contain', outputMessage);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add and use Manual Chat Trigger node together with Agent node', () => {
|
it('should add and use Manual Chat Trigger node together with Agent node', () => {
|
||||||
|
|
|
@ -126,6 +126,9 @@ export default defineComponent({
|
||||||
isChatNode(): boolean {
|
isChatNode(): boolean {
|
||||||
return Boolean(this.nodeType && this.nodeType.name === CHAT_TRIGGER_NODE_TYPE);
|
return Boolean(this.nodeType && this.nodeType.name === CHAT_TRIGGER_NODE_TYPE);
|
||||||
},
|
},
|
||||||
|
isChatChild(): boolean {
|
||||||
|
return this.workflowsStore.checkIfNodeHasChatParent(this.nodeName);
|
||||||
|
},
|
||||||
isFormTriggerNode(): boolean {
|
isFormTriggerNode(): boolean {
|
||||||
return Boolean(this.nodeType && this.nodeType.name === FORM_TRIGGER_NODE_TYPE);
|
return Boolean(this.nodeType && this.nodeType.name === FORM_TRIGGER_NODE_TYPE);
|
||||||
},
|
},
|
||||||
|
@ -226,7 +229,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
async onClick() {
|
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);
|
this.ndvStore.setActiveNodeName(null);
|
||||||
nodeViewEventBus.emit('openChat');
|
nodeViewEventBus.emit('openChat');
|
||||||
} else if (this.isListeningForEvents) {
|
} else if (this.isListeningForEvents) {
|
||||||
|
|
|
@ -122,6 +122,7 @@ import { defineAsyncComponent, defineComponent } from 'vue';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
|
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import Modal from '@/components/Modal.vue';
|
import Modal from '@/components/Modal.vue';
|
||||||
import {
|
import {
|
||||||
AI_CATEGORY_AGENTS,
|
AI_CATEGORY_AGENTS,
|
||||||
|
@ -132,6 +133,7 @@ import {
|
||||||
CHAT_EMBED_MODAL_KEY,
|
CHAT_EMBED_MODAL_KEY,
|
||||||
CHAT_TRIGGER_NODE_TYPE,
|
CHAT_TRIGGER_NODE_TYPE,
|
||||||
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
||||||
|
MODAL_CONFIRM,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
@ -153,6 +155,7 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||||
|
import { usePinnedData } from '@/composables/usePinnedData';
|
||||||
|
|
||||||
const RunDataAi = defineAsyncComponent(
|
const RunDataAi = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataAi/RunDataAi.vue'),
|
async () => await import('@/components/RunDataAi/RunDataAi.vue'),
|
||||||
|
@ -197,6 +200,7 @@ export default defineComponent({
|
||||||
externalHooks,
|
externalHooks,
|
||||||
workflowHelpers,
|
workflowHelpers,
|
||||||
...useToast(),
|
...useToast(),
|
||||||
|
...useMessage(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -273,6 +277,23 @@ export default defineComponent({
|
||||||
);
|
);
|
||||||
return;
|
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({
|
this.messages.push({
|
||||||
text: message,
|
text: message,
|
||||||
sender: 'user',
|
sender: 'user',
|
||||||
|
|
|
@ -139,7 +139,10 @@ describe('WorkflowLMChatModal', () => {
|
||||||
|
|
||||||
it('should send and display chat message', async () => {
|
it('should send and display chat message', async () => {
|
||||||
const wrapper = renderComponent({
|
const wrapper = renderComponent({
|
||||||
pinia: await createPiniaWithAINodes(),
|
pinia: await createPiniaWithAINodes({
|
||||||
|
withConnections: true,
|
||||||
|
withAgentNode: true,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
|
|
|
@ -28,7 +28,11 @@ export type PinDataSource =
|
||||||
| 'context-menu'
|
| 'context-menu'
|
||||||
| 'keyboard-shortcut';
|
| '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(
|
export function usePinnedData(
|
||||||
node: MaybeRef<INodeUi | null>,
|
node: MaybeRef<INodeUi | null>,
|
||||||
|
|
|
@ -24,7 +24,12 @@ import {
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
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 { useTitleChange } from '@/composables/useTitleChange';
|
||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
@ -198,6 +203,10 @@ export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { startNodeNames } = consolidatedData;
|
const { startNodeNames } = consolidatedData;
|
||||||
|
const destinationNodeType = options.destinationNode
|
||||||
|
? workflowsStore.getNodeByName(options.destinationNode)?.type
|
||||||
|
: '';
|
||||||
|
|
||||||
let { runData: newRunData } = consolidatedData;
|
let { runData: newRunData } = consolidatedData;
|
||||||
let executedNode: string | undefined;
|
let executedNode: string | undefined;
|
||||||
if (
|
if (
|
||||||
|
@ -217,6 +226,27 @@ export function useRunWorkflow(options: { router: ReturnType<typeof useRouter> }
|
||||||
executedNode = options.triggerNode;
|
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) => {
|
const startNodes: StartNodeData[] = startNodeNames.map((name) => {
|
||||||
// Find for each start node the source data
|
// Find for each start node the source data
|
||||||
let sourceData = get(runData, [name, 0, 'source', 0], null);
|
let sourceData = get(runData, [name, 0, 'source', 0], null);
|
||||||
|
|
|
@ -34,10 +34,6 @@ $badge-warning-color: var(--color-text-dark);
|
||||||
// Warning tooltip
|
// Warning tooltip
|
||||||
$warning-tooltip-color: var(--color-danger);
|
$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
|
// sass variable is used for scss files
|
||||||
$header-height: calc(var(--header-height) * 1px);
|
$header-height: calc(var(--header-height) * 1px);
|
||||||
|
|
||||||
|
|
|
@ -173,6 +173,9 @@
|
||||||
--node-error-output-color: #991818;
|
--node-error-output-color: #991818;
|
||||||
|
|
||||||
--chat--spacing: var(--spacing-s);
|
--chat--spacing: var(--spacing-s);
|
||||||
|
|
||||||
|
// Using native css variable enables us to use this value in JS
|
||||||
|
--header-height: 65;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
|
|
|
@ -148,6 +148,10 @@
|
||||||
"chat.window.chat.chatMessageOptions.reuseMessage": "Reuse Message",
|
"chat.window.chat.chatMessageOptions.reuseMessage": "Reuse Message",
|
||||||
"chat.window.chat.chatMessageOptions.repostMessage": "Repost Message",
|
"chat.window.chat.chatMessageOptions.repostMessage": "Repost Message",
|
||||||
"chat.window.chat.chatMessageOptions.executionId": "Execution ID",
|
"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.description": "Add chat to external applications using the n8n chat package.",
|
||||||
"chatEmbed.infoTip.link": "More info",
|
"chatEmbed.infoTip.link": "More info",
|
||||||
"chatEmbed.title": "Embed Chat in your website",
|
"chatEmbed.title": "Embed Chat in your website",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
CHAT_TRIGGER_NODE_TYPE,
|
||||||
DEFAULT_NEW_WORKFLOW_NAME,
|
DEFAULT_NEW_WORKFLOW_NAME,
|
||||||
DUPLICATE_POSTFFIX,
|
DUPLICATE_POSTFFIX,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
|
@ -1460,5 +1461,18 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
||||||
appendChatMessage(message: string): void {
|
appendChatMessage(message: string): void {
|
||||||
this.chatMessages.push(message);
|
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;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue