mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-22 20:41:50 +02:00
fix(Chat Node): Correct descriptions, aliases, UX improvements (#24619)
This commit is contained in:
parent
a2908805d9
commit
55152ff93e
|
|
@ -51,7 +51,7 @@ export class Chat implements INodeType {
|
|||
subcategories: {
|
||||
HITL: ['Human in the Loop'],
|
||||
},
|
||||
alias: ['human', 'wait', 'hitl'],
|
||||
alias: ['human', 'wait', 'hitl', 'respond', 'approve', 'confirm', 'send', 'message'],
|
||||
resources: {
|
||||
primaryDocumentation: [
|
||||
{
|
||||
|
|
@ -295,7 +295,7 @@ export class Chat implements INodeType {
|
|||
if (parameters.options.responseMode !== 'responseNodes') {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'"Response Mode" in the chat trigger node must be set to "Respond Nodes"',
|
||||
'"Response Mode" in the chat trigger node must be set to "Using Response Nodes"',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,12 +95,18 @@ export const cssVariables = `
|
|||
--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 */
|
||||
|
|
|
|||
|
|
@ -310,12 +310,18 @@ The Chat window is entirely customizable using CSS variables.
|
|||
--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 */
|
||||
|
|
|
|||
|
|
@ -1,28 +1,38 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: 'primary' | 'secondary';
|
||||
element?: 'button' | 'a';
|
||||
disabled?: boolean;
|
||||
}>(),
|
||||
{
|
||||
type: 'primary',
|
||||
element: 'button',
|
||||
disabled: false,
|
||||
},
|
||||
);
|
||||
|
||||
const buttonTypeClass = computed(() => {
|
||||
return `chat-button-${props.type}`;
|
||||
return `chat-button-${props.type}${props.disabled ? '-disabled' : ''}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="element" :class="['chat-button', buttonTypeClass]">
|
||||
<slot />
|
||||
</component>
|
||||
<span :class="{ 'chat-button-wrapper-disabled': props.disabled }">
|
||||
<component :is="element" :class="['chat-button', buttonTypeClass]" v-bind="$attrs">
|
||||
<slot />
|
||||
</component>
|
||||
</span>
|
||||
</template>
|
||||
<style lang="scss">
|
||||
.chat-button-wrapper-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.chat-button {
|
||||
display: inline-flex;
|
||||
text-align: center;
|
||||
|
|
@ -44,10 +54,6 @@ const buttonTypeClass = computed(() => {
|
|||
outline: 0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-button-primary {
|
||||
|
|
@ -62,6 +68,13 @@ const buttonTypeClass = computed(() => {
|
|||
}
|
||||
}
|
||||
|
||||
.chat-button-primary-disabled {
|
||||
pointer-events: none;
|
||||
color: var(--chat--button--color--primary--disabled);
|
||||
background-color: var(--chat--button--background--primary--disabled);
|
||||
border: var(--chat--button--border--primary--disabled);
|
||||
}
|
||||
|
||||
.chat-button-secondary {
|
||||
color: var(--chat--button--color--secondary);
|
||||
background-color: var(--chat--button--background--secondary);
|
||||
|
|
@ -73,4 +86,11 @@ const buttonTypeClass = computed(() => {
|
|||
border: var(--chat--button--border--secondary--hover);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-button-secondary-disabled {
|
||||
pointer-events: none;
|
||||
color: var(--chat--button--color--secondary--disabled);
|
||||
background-color: var(--chat--button--background--secondary--disabled);
|
||||
border: var(--chat--button--border--secondary--disabled);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import Button from './Button.vue';
|
||||
import MarkdownRenderer from './MarkdownRenderer.vue';
|
||||
|
||||
|
|
@ -10,19 +12,32 @@ defineProps<{
|
|||
type: 'primary' | 'secondary';
|
||||
}>;
|
||||
}>();
|
||||
|
||||
const clickedButtonIndex = ref<number | null>(null);
|
||||
|
||||
const onClick = async (link: string, index: number) => {
|
||||
if (clickedButtonIndex.value !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(link);
|
||||
if (response.ok) {
|
||||
clickedButtonIndex.value = index;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MarkdownRenderer :text="text" />
|
||||
<div :class="$style.buttons">
|
||||
<template v-for="button in buttons" :key="button.text">
|
||||
<template v-for="(button, index) in buttons" :key="button.text">
|
||||
<Button
|
||||
element="a"
|
||||
:href="button.link"
|
||||
v-if="clickedButtonIndex === null || index === clickedButtonIndex"
|
||||
element="button"
|
||||
:type="button.type"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:disabled="index === clickedButtonIndex"
|
||||
@click="onClick(button.link, index)"
|
||||
>{{ button.text }}</Button
|
||||
>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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<ReturnType<typeof useWorkflowsStore>> & {
|
||||
|
|
@ -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()', () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user