Skip to content

Commit

Permalink
fix(editor): Fix opening of chat window when executing a child node (#…
Browse files Browse the repository at this point in the history
…8789)

Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
Co-authored-by: Michael Kret <michael.k@radency.com>
  • Loading branch information
2 people authored and krynble committed Mar 25, 2024
1 parent 953d1b4 commit e695927
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 25 deletions.
3 changes: 3 additions & 0 deletions cypress/composables/modals/chat-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
24 changes: 7 additions & 17 deletions cypress/e2e/30-langchain.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand All @@ -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', () => {
Expand All @@ -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: {
Expand All @@ -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', () => {
Expand Down
6 changes: 5 additions & 1 deletion packages/editor-ui/src/components/NodeExecuteButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down Expand Up @@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions packages/editor-ui/src/components/WorkflowLMChat.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand All @@ -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'),
Expand Down Expand Up @@ -197,6 +200,7 @@ export default defineComponent({
externalHooks,
workflowHelpers,
...useToast(),
...useMessage(),
};
},
data() {
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(() =>
Expand Down
6 changes: 5 additions & 1 deletion packages/editor-ui/src/composables/usePinnedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down
32 changes: 31 additions & 1 deletion packages/editor-ui/src/composables/useRunWorkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
Expand All @@ -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);
Expand Down
4 changes: 0 additions & 4 deletions packages/editor-ui/src/n8n-theme-variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
3 changes: 3 additions & 0 deletions packages/editor-ui/src/n8n-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions packages/editor-ui/src/plugins/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions packages/editor-ui/src/stores/workflows.store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
CHAT_TRIGGER_NODE_TYPE,
DEFAULT_NEW_WORKFLOW_NAME,
DUPLICATE_POSTFFIX,
EnterpriseEditionFeature,
Expand Down Expand Up @@ -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;
},
},
});

0 comments on commit e695927

Please sign in to comment.