-
+
diff --git a/packages/frontend/@n8n/chat/src/css/_tokens.scss b/packages/frontend/@n8n/chat/src/css/_tokens.scss
index 4da4c408bfa..9852350d94a 100644
--- a/packages/frontend/@n8n/chat/src/css/_tokens.scss
+++ b/packages/frontend/@n8n/chat/src/css/_tokens.scss
@@ -95,12 +95,18 @@
--chat--button--color--primary--hover: var(--chat--color-light);
--chat--button--background--primary--hover: var(--chat--color-secondary-shade-50);
--chat--button--border--primary--hover: none;
+ --chat--button--color--primary--disabled: var(--chat--color-light);
+ --chat--button--background--primary--disabled: #81bbb1;
+ --chat--button--border--primary--disabled: none;
--chat--button--color--secondary: var(--chat--color-light);
--chat--button--background--secondary: hsl(0, 0%, 58%);
--chat--button--border--secondary: none;
--chat--button--color--secondary--hover: var(--chat--color-light);
--chat--button--background--secondary--hover: hsl(0, 0%, 51%);
--chat--button--border--secondary--hover: none;
+ --chat--button--color--secondary--disabled: var(--chat--color-light);
+ --chat--button--background--secondary--disabled: hsl(0, 0%, 78%);
+ --chat--button--border--secondary--disabled: none;
--chat--close--button--color-hover: var(--chat--color--primary);
/* Send and File Buttons */
diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index 757f8cf3507..bfbd7ba4c95 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -3190,6 +3190,8 @@
"workflowRun.showError.resolveOutstandingIssues": "Please resolve outstanding issues before you activate it",
"workflowRun.showError.unsupportedExecutionLogic.title": "Unsupported Execution Logic",
"workflowRun.showError.unsupportedExecutionLogic.description": "Execution Logic \"v2\" is not supported when filesystem mode is \"default\". Please change the Execution Logic to \"v1\" in workflow settings and update expressions accordingly.",
+ "workflowRun.showWarning.noChatResponseNodes.title": "No response node detected",
+ "workflowRun.showWarning.noChatResponseNodes.description": "Add a Chat or Respond to Webhook node to send a reply, or change the response mode in the Chat Trigger.",
"workflowRun.showMessage.message": "Please fix them before executing",
"workflowRun.showMessage.title": "Workflow has issues",
"workflowSettings.executionLogic.v0.title": "v0 (legacy)",
diff --git a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts
index c7c6e55d68e..4a5f004dc85 100644
--- a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts
+++ b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.test.ts
@@ -33,6 +33,13 @@ import { waitFor } from '@testing-library/vue';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { useRootStore } from '@n8n/stores/useRootStore';
import { useI18n } from '@n8n/i18n';
+import {
+ CHAT_TRIGGER_NODE_TYPE,
+ MANUAL_TRIGGER_NODE_TYPE,
+ CHAT_NODE_TYPE,
+ CHAT_TOOL_NODE_TYPE,
+ RESPOND_TO_WEBHOOK_NODE_TYPE,
+} from '../constants';
vi.mock('@/app/stores/workflows.store', () => {
const storeState: Partial> & {
@@ -780,6 +787,166 @@ describe('useRunWorkflow({ router })', () => {
expect(workflowsStore.runWorkflow).toHaveBeenCalledTimes(1);
expect(setWorkflowExecutionData).lastCalledWith(null);
});
+
+ describe('Chat trigger warnings', () => {
+ const mockExecutionResponse = { executionId: '123' };
+ const toast = useToast();
+ const i18n = useI18n();
+
+ beforeEach(() => {
+ vi.mocked(pushConnectionStore).isConnected = true;
+ vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
+ vi.mocked(workflowsStore).nodesIssuesExist = false;
+ vi.mocked(workflowsStore).workflowObject = {
+ name: 'Test Workflow',
+ } as Workflow;
+ vi.mocked(workflowsStore).getWorkflowRunData = {
+ NodeName: [],
+ };
+ });
+
+ it("should show a warning if there are no chat response nodes and chat trigger's response mode is `responseNodes`", async () => {
+ const { runWorkflow } = useRunWorkflow({ router });
+ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
+ id: 'workflowId',
+ nodes: [
+ {
+ name: 'Chat Trigger',
+ type: CHAT_TRIGGER_NODE_TYPE,
+ parameters: {
+ options: {
+ responseMode: 'responseNodes',
+ },
+ },
+ },
+ ],
+ } as unknown as WorkflowData);
+
+ const result = await runWorkflow({ triggerNode: 'Chat Trigger' });
+
+ expect(result).toEqual(mockExecutionResponse);
+ expect(toast.showMessage).toHaveBeenCalledWith({
+ title: i18n.baseText('workflowRun.showWarning.noChatResponseNodes.title'),
+ message: i18n.baseText('workflowRun.showWarning.noChatResponseNodes.description'),
+ type: 'warning',
+ });
+ });
+
+ it.each([['lastNode'], ['streaming']])(
+ "should not show a warning if there are no chat response nodes but chat trigger's response mode is `%s`",
+ async (responseMode) => {
+ const { runWorkflow } = useRunWorkflow({ router });
+ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
+ id: 'workflowId',
+ nodes: [
+ {
+ name: 'Chat Trigger',
+ type: CHAT_TRIGGER_NODE_TYPE,
+ parameters: {
+ options: {
+ responseMode,
+ },
+ },
+ },
+ ],
+ } as unknown as WorkflowData);
+
+ const result = await runWorkflow({ triggerNode: 'Chat Trigger' });
+
+ expect(result).toEqual(mockExecutionResponse);
+ expect(toast.showMessage).not.toHaveBeenCalled();
+ },
+ );
+
+ it('should not show a warning if the workflow was started from a different trigger node', async () => {
+ const { runWorkflow } = useRunWorkflow({ router });
+ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
+ id: 'workflowId',
+ nodes: [
+ {
+ name: 'Chat Trigger',
+ type: CHAT_TRIGGER_NODE_TYPE,
+ parameters: {
+ options: {
+ responseMode: 'responseNodes',
+ },
+ },
+ },
+ {
+ name: 'Manual Trigger',
+ type: MANUAL_TRIGGER_NODE_TYPE,
+ },
+ ],
+ } as unknown as WorkflowData);
+
+ const result = await runWorkflow({ triggerNode: 'Manual Trigger' });
+
+ expect(result).toEqual(mockExecutionResponse);
+ expect(toast.showMessage).not.toHaveBeenCalled();
+ });
+
+ it.each([[CHAT_NODE_TYPE], [CHAT_TOOL_NODE_TYPE], [RESPOND_TO_WEBHOOK_NODE_TYPE]])(
+ 'should not show a warning if the there are response nodes in the workflow (%s)',
+ async (responseNodeType) => {
+ const { runWorkflow } = useRunWorkflow({ router });
+ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
+ id: 'workflowId',
+ nodes: [
+ {
+ name: 'Chat Trigger',
+ type: CHAT_TRIGGER_NODE_TYPE,
+ parameters: {
+ options: {
+ responseMode: 'responseNodes',
+ },
+ },
+ },
+ {
+ name: 'Response Node',
+ type: responseNodeType,
+ },
+ ],
+ } as unknown as WorkflowData);
+
+ const result = await runWorkflow({ triggerNode: 'Chat Trigger' });
+
+ expect(result).toEqual(mockExecutionResponse);
+ expect(toast.showMessage).not.toHaveBeenCalled();
+ },
+ );
+
+ it('should show a warning if all the response nodes are disabled', async () => {
+ const { runWorkflow } = useRunWorkflow({ router });
+ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
+ id: 'workflowId',
+ nodes: [
+ {
+ name: 'Chat Trigger',
+ type: CHAT_TRIGGER_NODE_TYPE,
+ parameters: {
+ options: {
+ responseMode: 'responseNodes',
+ },
+ },
+ },
+ {
+ name: 'Response Node',
+ type: CHAT_NODE_TYPE,
+ disabled: true,
+ },
+ ],
+ } as unknown as WorkflowData);
+
+ const result = await runWorkflow({ triggerNode: 'Chat Trigger' });
+
+ expect(result).toEqual(mockExecutionResponse);
+ expect(toast.showMessage).toHaveBeenCalledWith({
+ title: i18n.baseText('workflowRun.showWarning.noChatResponseNodes.title'),
+ message: i18n.baseText('workflowRun.showWarning.noChatResponseNodes.description'),
+ type: 'warning',
+ });
+ });
+ });
});
describe('consolidateRunDataAndStartNodes()', () => {
diff --git a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts
index c6d1d8ab2ec..12dbc00cde7 100644
--- a/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts
+++ b/packages/frontend/editor-ui/src/app/composables/useRunWorkflow.ts
@@ -27,7 +27,13 @@ import { retry } from '@n8n/utils/retry';
import { useToast } from '@/app/composables/useToast';
import { useNodeHelpers } from '@/app/composables/useNodeHelpers';
-import { CHAT_TRIGGER_NODE_TYPE, IN_PROGRESS_EXECUTION_ID } from '@/app/constants';
+import {
+ CHAT_NODE_TYPE,
+ CHAT_TOOL_NODE_TYPE,
+ CHAT_TRIGGER_NODE_TYPE,
+ IN_PROGRESS_EXECUTION_ID,
+ RESPOND_TO_WEBHOOK_NODE_TYPE,
+} from '@/app/constants';
import { useRootStore } from '@n8n/stores/useRootStore';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
@@ -232,6 +238,27 @@ export function useRunWorkflow(useRunWorkflowOpts: {
const triggers = workflowData.nodes.filter(
(node) => node.type.toLowerCase().includes('trigger') && !node.disabled,
);
+ const chatTriggerNode = triggers.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE);
+ const chatTriggerNodeOptions = chatTriggerNode?.parameters?.options as IDataObject;
+ if (
+ options.triggerNode === chatTriggerNode?.name &&
+ chatTriggerNodeOptions?.responseMode === 'responseNodes'
+ ) {
+ const responseNodes = workflowData.nodes.filter(
+ (node) =>
+ !node.disabled &&
+ (node.type === CHAT_NODE_TYPE ||
+ node.type === CHAT_TOOL_NODE_TYPE ||
+ node.type === RESPOND_TO_WEBHOOK_NODE_TYPE),
+ );
+ if (!responseNodes?.length) {
+ toast.showMessage({
+ title: i18n.baseText('workflowRun.showWarning.noChatResponseNodes.title'),
+ message: i18n.baseText('workflowRun.showWarning.noChatResponseNodes.description'),
+ type: 'warning',
+ });
+ }
+ }
//if no destination node is specified
//and execution is not triggered from chat
@@ -240,17 +267,11 @@ export function useRunWorkflow(useRunWorkflowOpts: {
if (
!options.destinationNode &&
options.source !== 'RunData.ManualChatMessage' &&
- workflowData.nodes.some((node) => node.type === CHAT_TRIGGER_NODE_TYPE)
+ chatTriggerNode
) {
const otherTriggers = triggers.filter((node) => node.type !== CHAT_TRIGGER_NODE_TYPE);
-
if (otherTriggers.length) {
- const chatTriggerNode = workflowData.nodes.find(
- (node) => node.type === CHAT_TRIGGER_NODE_TYPE,
- );
- if (chatTriggerNode) {
- chatTriggerNode.disabled = true;
- }
+ chatTriggerNode.disabled = true;
}
}
diff --git a/packages/frontend/editor-ui/src/app/constants/nodeTypes.ts b/packages/frontend/editor-ui/src/app/constants/nodeTypes.ts
index 854526c1e73..8f2420262c8 100644
--- a/packages/frontend/editor-ui/src/app/constants/nodeTypes.ts
+++ b/packages/frontend/editor-ui/src/app/constants/nodeTypes.ts
@@ -35,6 +35,7 @@ export const MANUAL_CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.manualCha
export const MCP_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.mcpTrigger';
export const CHAT_TRIGGER_NODE_TYPE = '@n8n/n8n-nodes-langchain.chatTrigger';
export const CHAT_NODE_TYPE = '@n8n/n8n-nodes-langchain.chat';
+export const CHAT_TOOL_NODE_TYPE = '@n8n/n8n-nodes-langchain.chatTool';
export const AGENT_NODE_TYPE = '@n8n/n8n-nodes-langchain.agent';
export const OPEN_AI_CHAT_MODEL_NODE_TYPE = '@n8n/n8n-nodes-langchain.lmChatOpenAi';
export const OPEN_AI_NODE_TYPE = '@n8n/n8n-nodes-langchain.openAi';
diff --git a/packages/frontend/editor-ui/src/features/execution/logs/components/ChatMessagesPanel.vue b/packages/frontend/editor-ui/src/features/execution/logs/components/ChatMessagesPanel.vue
index 651b7ecece2..8541914c4c0 100644
--- a/packages/frontend/editor-ui/src/features/execution/logs/components/ChatMessagesPanel.vue
+++ b/packages/frontend/editor-ui/src/features/execution/logs/components/ChatMessagesPanel.vue
@@ -316,6 +316,11 @@ async function copySessionId() {
--button--color--background--primary--hover-active-focus
);
--chat--button--border--primary--hover: none;
+ --chat--button--color--primary--disabled: var(--button--color--text--primary--disabled);
+ --chat--button--background--primary--disabled: var(
+ --button--color--background--primary--disabled
+ );
+ --chat--button--border--primary--disabled: none;
--chat--button--color--secondary: var(--button--color--text--secondary);
--chat--button--background--secondary: var(--button--color--background--secondary);
--chat--button--border--secondary: var(--border-width) var(--button--border-color--secondary)
@@ -326,6 +331,10 @@ async function copySessionId() {
--chat--button--background--secondary--hover: var(--button--color--background--secondary--hover);
--chat--button--border--secondary--hover: var(--border-width)
var(--button--border-color--secondary--hover-active-focus) var(--border-style);
+ --chat--button--color--secondary--disabled: var(--button--color--text--secondary--disabled);
+ --chat--button--background--secondary--disabled: var(--button--color--background--secondary);
+ --chat--button--border--secondary--disabled: var(--border-width)
+ var(--button--border-color--secondary--disabled) var(--border-style);
height: 100%;
display: flex;
flex-direction: column;