mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -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() {
|
||||
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');
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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(() =>
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue