mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Add unit tests
This commit is contained in:
parent
6f4a8171ce
commit
badf12f8f6
|
@ -196,6 +196,7 @@ function adjustHeight(event: Event) {
|
||||||
<div class="chat-inputs">
|
<div class="chat-inputs">
|
||||||
<textarea
|
<textarea
|
||||||
ref="chatTextArea"
|
ref="chatTextArea"
|
||||||
|
data-test-id="chat-input"
|
||||||
v-model="input"
|
v-model="input"
|
||||||
:disabled="isInputDisabled"
|
:disabled="isInputDisabled"
|
||||||
:placeholder="t(props.placeholder)"
|
:placeholder="t(props.placeholder)"
|
||||||
|
@ -210,6 +211,7 @@ function adjustHeight(event: Event) {
|
||||||
v-if="isFileUploadAllowed"
|
v-if="isFileUploadAllowed"
|
||||||
:disabled="isFileUploadDisabled"
|
:disabled="isFileUploadDisabled"
|
||||||
class="chat-input-file-button"
|
class="chat-input-file-button"
|
||||||
|
data-test-id="chat-attach-file-button"
|
||||||
@click="onOpenFileDialog"
|
@click="onOpenFileDialog"
|
||||||
>
|
>
|
||||||
<IconPaperclip height="24" width="24" />
|
<IconPaperclip height="24" width="24" />
|
||||||
|
|
|
@ -34,7 +34,12 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<Message ref="messageContainer" :class="classes" :message="message">
|
<Message
|
||||||
|
ref="messageContainer"
|
||||||
|
:class="classes"
|
||||||
|
:message="message"
|
||||||
|
data-test-id="chat-message-typing"
|
||||||
|
>
|
||||||
<div class="chat-message-typing-body">
|
<div class="chat-message-typing-body">
|
||||||
<span class="chat-message-typing-circle"></span>
|
<span class="chat-message-typing-circle"></span>
|
||||||
<span class="chat-message-typing-circle"></span>
|
<span class="chat-message-typing-circle"></span>
|
||||||
|
|
|
@ -54,12 +54,14 @@ export const mockNodeTypeDescription = ({
|
||||||
credentials = [],
|
credentials = [],
|
||||||
inputs = [NodeConnectionType.Main],
|
inputs = [NodeConnectionType.Main],
|
||||||
outputs = [NodeConnectionType.Main],
|
outputs = [NodeConnectionType.Main],
|
||||||
|
codex = {},
|
||||||
}: {
|
}: {
|
||||||
name?: INodeTypeDescription['name'];
|
name?: INodeTypeDescription['name'];
|
||||||
version?: INodeTypeDescription['version'];
|
version?: INodeTypeDescription['version'];
|
||||||
credentials?: INodeTypeDescription['credentials'];
|
credentials?: INodeTypeDescription['credentials'];
|
||||||
inputs?: INodeTypeDescription['inputs'];
|
inputs?: INodeTypeDescription['inputs'];
|
||||||
outputs?: INodeTypeDescription['outputs'];
|
outputs?: INodeTypeDescription['outputs'];
|
||||||
|
codex?: INodeTypeDescription['codex'];
|
||||||
} = {}) =>
|
} = {}) =>
|
||||||
mock<INodeTypeDescription>({
|
mock<INodeTypeDescription>({
|
||||||
name,
|
name,
|
||||||
|
@ -74,6 +76,7 @@ export const mockNodeTypeDescription = ({
|
||||||
group: EXECUTABLE_TRIGGER_NODE_TYPES.includes(name) ? ['trigger'] : [],
|
group: EXECUTABLE_TRIGGER_NODE_TYPES.includes(name) ? ['trigger'] : [],
|
||||||
inputs,
|
inputs,
|
||||||
outputs,
|
outputs,
|
||||||
|
codex,
|
||||||
credentials,
|
credentials,
|
||||||
documentationUrl: 'https://docs',
|
documentationUrl: 'https://docs',
|
||||||
webhooks: undefined,
|
webhooks: undefined,
|
||||||
|
|
586
packages/editor-ui/src/components/CanvasChat/CanvasChat.test.ts
Normal file
586
packages/editor-ui/src/components/CanvasChat/CanvasChat.test.ts
Normal file
|
@ -0,0 +1,586 @@
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
import { userEvent } from '@testing-library/user-event';
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import CanvasChat from './CanvasChat.vue';
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { createTestWorkflowObject } from '@/__tests__/mocks';
|
||||||
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
|
import { STORES } from '@/constants';
|
||||||
|
import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
|
||||||
|
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||||
|
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
|
import * as useChatMessaging from './composables/useChatMessaging';
|
||||||
|
import * as useChatTrigger from './composables/useChatTrigger';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
|
|
||||||
|
import type { IExecutionResponse, INodeUi } from '@/Interface';
|
||||||
|
import type { ChatMessage } from '@n8n/chat/types';
|
||||||
|
|
||||||
|
vi.mock('@/composables/useToast', () => {
|
||||||
|
const showMessage = vi.fn();
|
||||||
|
const showError = vi.fn();
|
||||||
|
return {
|
||||||
|
useToast: () => {
|
||||||
|
return {
|
||||||
|
showMessage,
|
||||||
|
showError,
|
||||||
|
clearAllStickyNotifications: vi.fn(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// Test data
|
||||||
|
const mockNodes: INodeUi[] = [
|
||||||
|
{
|
||||||
|
parameters: {
|
||||||
|
options: {
|
||||||
|
allowFileUploads: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
id: 'chat-trigger-id',
|
||||||
|
name: 'When chat message received',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.chatTrigger',
|
||||||
|
typeVersion: 1.1,
|
||||||
|
position: [740, 860],
|
||||||
|
webhookId: 'webhook-id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
parameters: {},
|
||||||
|
id: 'agent-id',
|
||||||
|
name: 'AI Agent',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.agent',
|
||||||
|
typeVersion: 1.7,
|
||||||
|
position: [960, 860],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockConnections = {
|
||||||
|
'When chat message received': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Agent',
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockWorkflowExecution = {
|
||||||
|
data: {
|
||||||
|
resultData: {
|
||||||
|
runData: {
|
||||||
|
'AI Agent': [
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
main: [[{ json: { output: 'AI response message' } }]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
lastNodeExecuted: 'AI Agent',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CanvasChat', () => {
|
||||||
|
const renderComponent = createComponentRenderer(CanvasChat, {
|
||||||
|
global: {
|
||||||
|
provide: {
|
||||||
|
[ChatSymbol as symbol]: {},
|
||||||
|
[ChatOptionsSymbol as symbol]: {},
|
||||||
|
},
|
||||||
|
plugins: [router],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||||
|
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
|
||||||
|
let canvasStore: ReturnType<typeof mockedStore<typeof useCanvasStore>>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const pinia = createTestingPinia({
|
||||||
|
initialState: {
|
||||||
|
[STORES.WORKFLOWS]: {
|
||||||
|
workflow: {
|
||||||
|
nodes: mockNodes,
|
||||||
|
connections: mockConnections,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[STORES.UI]: {
|
||||||
|
chatPanelOpen: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setActivePinia(pinia);
|
||||||
|
|
||||||
|
workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
|
uiStore = mockedStore(useUIStore);
|
||||||
|
canvasStore = mockedStore(useCanvasStore);
|
||||||
|
|
||||||
|
// Setup default mocks
|
||||||
|
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||||||
|
createTestWorkflowObject({
|
||||||
|
nodes: mockNodes,
|
||||||
|
connections: mockConnections,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
workflowsStore.getNodeByName.mockImplementation(
|
||||||
|
(name) => mockNodes.find((node) => node.name === name) ?? null,
|
||||||
|
);
|
||||||
|
workflowsStore.isChatPanelOpen = true;
|
||||||
|
workflowsStore.getWorkflowExecution = mockWorkflowExecution as unknown as IExecutionResponse;
|
||||||
|
workflowsStore.getPastChatMessages = ['Previous message 1', 'Previous message 2'];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('should render chat when panel is open', () => {
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
expect(getByTestId('canvas-chat')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not render chat when panel is closed', async () => {
|
||||||
|
workflowsStore.isChatPanelOpen = false;
|
||||||
|
const { queryByTestId } = renderComponent();
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByTestId('canvas-chat')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show correct input placeholder', async () => {
|
||||||
|
const { findByTestId } = renderComponent();
|
||||||
|
expect(await findByTestId('chat-input')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('message handling', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(chatEventBus, 'emit');
|
||||||
|
workflowsStore.runWorkflow.mockResolvedValue({ executionId: 'test-execution-id' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send message and show response', async () => {
|
||||||
|
const { findByTestId, findByText } = renderComponent();
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
const input = await findByTestId('chat-input');
|
||||||
|
await userEvent.type(input, 'Hello AI!');
|
||||||
|
await userEvent.keyboard('{Enter}');
|
||||||
|
|
||||||
|
// Verify message and response
|
||||||
|
expect(await findByText('Hello AI!')).toBeInTheDocument();
|
||||||
|
await waitFor(async () => {
|
||||||
|
expect(await findByText('AI response message')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify workflow execution
|
||||||
|
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
runData: {
|
||||||
|
'When chat message received': [
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
action: 'sendMessage',
|
||||||
|
chatInput: 'Hello AI!',
|
||||||
|
sessionId: expect.any(String),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
executionStatus: 'success',
|
||||||
|
executionTime: 0,
|
||||||
|
source: [null],
|
||||||
|
startTime: expect.any(Number),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show loading state during message processing', async () => {
|
||||||
|
const { findByTestId, queryByTestId } = renderComponent();
|
||||||
|
|
||||||
|
// Send message
|
||||||
|
const input = await findByTestId('chat-input');
|
||||||
|
await userEvent.type(input, 'Test message');
|
||||||
|
await userEvent.keyboard('{Enter}');
|
||||||
|
|
||||||
|
// Verify loading states
|
||||||
|
uiStore.isActionActive = { workflowRunning: true };
|
||||||
|
await waitFor(() => expect(queryByTestId('chat-message-typing')).toBeInTheDocument());
|
||||||
|
|
||||||
|
uiStore.isActionActive = { workflowRunning: false };
|
||||||
|
await waitFor(() => expect(queryByTestId('chat-message-typing')).not.toBeInTheDocument());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle workflow execution errors', async () => {
|
||||||
|
workflowsStore.runWorkflow.mockRejectedValueOnce(new Error());
|
||||||
|
|
||||||
|
const { findByTestId } = renderComponent();
|
||||||
|
|
||||||
|
const input = await findByTestId('chat-input');
|
||||||
|
await userEvent.type(input, 'Hello AI!');
|
||||||
|
await userEvent.keyboard('{Enter}');
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
expect(toast.showError).toHaveBeenCalledWith(new Error(), 'Problem running workflow');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('session management', () => {
|
||||||
|
const mockMessages: ChatMessage[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
text: 'Existing message',
|
||||||
|
sender: 'user',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(useChatMessaging, 'useChatMessaging').mockReturnValue({
|
||||||
|
getChatMessages: vi.fn().mockReturnValue(mockMessages),
|
||||||
|
sendMessage: vi.fn(),
|
||||||
|
extractResponseMessage: vi.fn(),
|
||||||
|
previousMessageIndex: ref(0),
|
||||||
|
waitForExecution: vi.fn(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow copying session ID', async () => {
|
||||||
|
const clipboardSpy = vi.fn();
|
||||||
|
document.execCommand = clipboardSpy;
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
|
await userEvent.click(getByTestId('chat-session-id'));
|
||||||
|
const toast = useToast();
|
||||||
|
expect(clipboardSpy).toHaveBeenCalledWith('copy');
|
||||||
|
expect(toast.showMessage).toHaveBeenCalledWith({
|
||||||
|
message: '',
|
||||||
|
title: 'Copied to clipboard',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should refresh session with confirmation when messages exist', async () => {
|
||||||
|
const { getByTestId, getByRole } = renderComponent();
|
||||||
|
|
||||||
|
const originalSessionId = getByTestId('chat-session-id').textContent;
|
||||||
|
await userEvent.click(getByTestId('refresh-session-button'));
|
||||||
|
|
||||||
|
const confirmButton = getByRole('dialog').querySelector('button.btn--confirm');
|
||||||
|
|
||||||
|
if (!confirmButton) throw new Error('Confirm button not found');
|
||||||
|
await userEvent.click(confirmButton);
|
||||||
|
|
||||||
|
expect(getByTestId('chat-session-id').textContent).not.toEqual(originalSessionId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resize functionality', () => {
|
||||||
|
it('should handle panel resizing', async () => {
|
||||||
|
const { container } = renderComponent();
|
||||||
|
|
||||||
|
const resizeWrapper = container.querySelector('.resizeWrapper');
|
||||||
|
if (!resizeWrapper) throw new Error('Resize wrapper not found');
|
||||||
|
|
||||||
|
await userEvent.pointer([
|
||||||
|
{ target: resizeWrapper, coords: { clientX: 0, clientY: 0 } },
|
||||||
|
{ coords: { clientX: 0, clientY: 100 } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(canvasStore.setPanelHeight).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should persist resize dimensions', () => {
|
||||||
|
const mockStorage = {
|
||||||
|
getItem: vi.fn(),
|
||||||
|
setItem: vi.fn(),
|
||||||
|
};
|
||||||
|
Object.defineProperty(window, 'localStorage', { value: mockStorage });
|
||||||
|
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
expect(mockStorage.getItem).toHaveBeenCalledWith('N8N_CANVAS_CHAT_HEIGHT');
|
||||||
|
expect(mockStorage.getItem).toHaveBeenCalledWith('N8N_CANVAS_CHAT_WIDTH');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('file handling', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(useChatMessaging, 'useChatMessaging').mockReturnValue({
|
||||||
|
getChatMessages: vi.fn().mockReturnValue([]),
|
||||||
|
sendMessage: vi.fn(),
|
||||||
|
extractResponseMessage: vi.fn(),
|
||||||
|
previousMessageIndex: ref(0),
|
||||||
|
waitForExecution: vi.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.isChatPanelOpen = true;
|
||||||
|
workflowsStore.allowFileUploads = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable file uploads when allowed by chat trigger node', async () => {
|
||||||
|
const allowFileUploads = ref(true);
|
||||||
|
const original = useChatTrigger.useChatTrigger;
|
||||||
|
vi.spyOn(useChatTrigger, 'useChatTrigger').mockImplementation((...args) => ({
|
||||||
|
...original(...args),
|
||||||
|
allowFileUploads: computed(() => allowFileUploads.value),
|
||||||
|
}));
|
||||||
|
const { getByTestId } = renderComponent();
|
||||||
|
|
||||||
|
const chatPanel = getByTestId('canvas-chat');
|
||||||
|
expect(chatPanel).toBeInTheDocument();
|
||||||
|
|
||||||
|
const fileInput = getByTestId('chat-attach-file-button');
|
||||||
|
expect(fileInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
allowFileUploads.value = false;
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(fileInput).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('message history handling', () => {
|
||||||
|
it('should properly navigate through message history with wrap-around', async () => {
|
||||||
|
const messages = ['Message 1', 'Message 2', 'Message 3'];
|
||||||
|
workflowsStore.getPastChatMessages = messages;
|
||||||
|
|
||||||
|
const { findByTestId } = renderComponent();
|
||||||
|
const input = await findByTestId('chat-input');
|
||||||
|
|
||||||
|
// First up should show most recent message
|
||||||
|
await userEvent.keyboard('{ArrowUp}');
|
||||||
|
expect(input).toHaveValue('Message 3');
|
||||||
|
|
||||||
|
// Second up should show second most recent
|
||||||
|
await userEvent.keyboard('{ArrowUp}');
|
||||||
|
expect(input).toHaveValue('Message 2');
|
||||||
|
|
||||||
|
// Third up should show oldest message
|
||||||
|
await userEvent.keyboard('{ArrowUp}');
|
||||||
|
expect(input).toHaveValue('Message 1');
|
||||||
|
|
||||||
|
// Fourth up should wrap around to most recent
|
||||||
|
await userEvent.keyboard('{ArrowUp}');
|
||||||
|
expect(input).toHaveValue('Message 3');
|
||||||
|
|
||||||
|
// Down arrow should go in reverse
|
||||||
|
await userEvent.keyboard('{ArrowDown}');
|
||||||
|
expect(input).toHaveValue('Message 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset message history navigation on new input', async () => {
|
||||||
|
workflowsStore.getPastChatMessages = ['Message 1', 'Message 2'];
|
||||||
|
const { findByTestId } = renderComponent();
|
||||||
|
const input = await findByTestId('chat-input');
|
||||||
|
|
||||||
|
// Navigate to oldest message
|
||||||
|
await userEvent.keyboard('{ArrowUp}'); // Most recent
|
||||||
|
await userEvent.keyboard('{ArrowUp}'); // Oldest
|
||||||
|
expect(input).toHaveValue('Message 1');
|
||||||
|
|
||||||
|
await userEvent.type(input, 'New message');
|
||||||
|
await userEvent.keyboard('{Enter}');
|
||||||
|
|
||||||
|
await userEvent.keyboard('{ArrowUp}');
|
||||||
|
expect(input).toHaveValue('Message 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('message reuse and repost', () => {
|
||||||
|
const sendMessageSpy = vi.fn();
|
||||||
|
beforeEach(() => {
|
||||||
|
const mockMessages: ChatMessage[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
text: 'Original message',
|
||||||
|
sender: 'user',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
text: 'AI response',
|
||||||
|
sender: 'bot',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
vi.spyOn(useChatMessaging, 'useChatMessaging').mockReturnValue({
|
||||||
|
getChatMessages: vi.fn().mockReturnValue(mockMessages),
|
||||||
|
sendMessage: sendMessageSpy,
|
||||||
|
extractResponseMessage: vi.fn(),
|
||||||
|
previousMessageIndex: ref(0),
|
||||||
|
waitForExecution: vi.fn(),
|
||||||
|
});
|
||||||
|
workflowsStore.messages = mockMessages;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should repost user message with new execution', async () => {
|
||||||
|
const { findByTestId } = renderComponent();
|
||||||
|
const repostButton = await findByTestId('repost-message-button');
|
||||||
|
|
||||||
|
await userEvent.click(repostButton);
|
||||||
|
|
||||||
|
expect(sendMessageSpy).toHaveBeenCalledWith('Original message');
|
||||||
|
// expect.objectContaining({
|
||||||
|
// runData: expect.objectContaining({
|
||||||
|
// 'When chat message received': expect.arrayContaining([
|
||||||
|
// expect.objectContaining({
|
||||||
|
// data: expect.objectContaining({
|
||||||
|
// main: expect.arrayContaining([
|
||||||
|
// expect.arrayContaining([
|
||||||
|
// expect.objectContaining({
|
||||||
|
// json: expect.objectContaining({
|
||||||
|
// chatInput: 'Original message',
|
||||||
|
// }),
|
||||||
|
// }),
|
||||||
|
// ]),
|
||||||
|
// ]),
|
||||||
|
// }),
|
||||||
|
// }),
|
||||||
|
// ]),
|
||||||
|
// }),
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show message options only for appropriate messages', async () => {
|
||||||
|
const { findByText, container } = renderComponent();
|
||||||
|
|
||||||
|
await findByText('Original message');
|
||||||
|
const userMessage = container.querySelector('.chat-message-from-user');
|
||||||
|
expect(
|
||||||
|
userMessage?.querySelector('[data-test-id="repost-message-button"]'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
userMessage?.querySelector('[data-test-id="reuse-message-button"]'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
await findByText('AI response');
|
||||||
|
const botMessage = container.querySelector('.chat-message-from-bot');
|
||||||
|
expect(
|
||||||
|
botMessage?.querySelector('[data-test-id="repost-message-button"]'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
botMessage?.querySelector('[data-test-id="reuse-message-button"]'),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('execution handling', () => {
|
||||||
|
it('should update UI when execution is completed', async () => {
|
||||||
|
const { findByTestId, queryByTestId } = renderComponent();
|
||||||
|
|
||||||
|
// Start execution
|
||||||
|
const input = await findByTestId('chat-input');
|
||||||
|
await userEvent.type(input, 'Test message');
|
||||||
|
await userEvent.keyboard('{Enter}');
|
||||||
|
|
||||||
|
// Simulate execution completion
|
||||||
|
uiStore.isActionActive = { workflowRunning: true };
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByTestId('chat-message-typing')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
uiStore.isActionActive = { workflowRunning: false };
|
||||||
|
workflowsStore.setWorkflowExecutionData(
|
||||||
|
mockWorkflowExecution as unknown as IExecutionResponse,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryByTestId('chat-message-typing')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('panel state synchronization', () => {
|
||||||
|
it('should update canvas height when chat or logs panel state changes', async () => {
|
||||||
|
renderComponent();
|
||||||
|
|
||||||
|
// Toggle logs panel
|
||||||
|
workflowsStore.isLogsPanelOpen = true;
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(canvasStore.setPanelHeight).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close chat panel
|
||||||
|
workflowsStore.isChatPanelOpen = false;
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(canvasStore.setPanelHeight).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve panel state across component remounts', async () => {
|
||||||
|
const { unmount, rerender } = renderComponent();
|
||||||
|
|
||||||
|
// Set initial state
|
||||||
|
workflowsStore.isChatPanelOpen = true;
|
||||||
|
workflowsStore.isLogsPanelOpen = true;
|
||||||
|
|
||||||
|
// Unmount and remount
|
||||||
|
unmount();
|
||||||
|
await rerender({});
|
||||||
|
|
||||||
|
expect(workflowsStore.isChatPanelOpen).toBe(true);
|
||||||
|
expect(workflowsStore.isLogsPanelOpen).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('keyboard shortcuts', () => {
|
||||||
|
it('should handle Enter key with modifier to start new line', async () => {
|
||||||
|
const { findByTestId } = renderComponent();
|
||||||
|
|
||||||
|
const input = await findByTestId('chat-input');
|
||||||
|
await userEvent.type(input, 'Line 1');
|
||||||
|
await userEvent.keyboard('{Shift>}{Enter}{/Shift}');
|
||||||
|
await userEvent.type(input, 'Line 2');
|
||||||
|
|
||||||
|
expect(input).toHaveValue('Line 1\nLine 2');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('chat synchronization', () => {
|
||||||
|
it('should load initial chat history when first opening panel', async () => {
|
||||||
|
const getChatMessagesSpy = vi.fn().mockReturnValue(['Previous message']);
|
||||||
|
vi.spyOn(useChatMessaging, 'useChatMessaging').mockReturnValue({
|
||||||
|
...vi.fn()(),
|
||||||
|
getChatMessages: getChatMessagesSpy,
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.isChatPanelOpen = false;
|
||||||
|
const { rerender } = renderComponent();
|
||||||
|
|
||||||
|
workflowsStore.isChatPanelOpen = true;
|
||||||
|
await rerender({});
|
||||||
|
|
||||||
|
expect(getChatMessagesSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -28,41 +28,32 @@ import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
|
||||||
export interface ChatProps {
|
const workflowsStore = useWorkflowsStore();
|
||||||
// Injected stores for testing
|
const uiStore = useUIStore();
|
||||||
workflowsStore?: ReturnType<typeof useWorkflowsStore>;
|
const canvasStore = useCanvasStore();
|
||||||
uiStore?: ReturnType<typeof useUIStore>;
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
canvasStore?: ReturnType<typeof useCanvasStore>;
|
const nodeHelpers = useNodeHelpers();
|
||||||
nodeTypesStore?: ReturnType<typeof useNodeTypesStore>;
|
const router = useRouter();
|
||||||
nodeHelpers?: ReturnType<typeof useNodeHelpers>;
|
|
||||||
initialSessionId?: string;
|
|
||||||
router?: Router;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Props for dependency injection (makes testing easier)
|
|
||||||
const props = withDefaults(defineProps<ChatProps>(), {
|
|
||||||
workflowsStore: () => useWorkflowsStore(),
|
|
||||||
uiStore: () => useUIStore(),
|
|
||||||
canvasStore: () => useCanvasStore(),
|
|
||||||
nodeTypesStore: () => useNodeTypesStore(),
|
|
||||||
nodeHelpers: () => useNodeHelpers(),
|
|
||||||
router: () => useRouter(),
|
|
||||||
initialSessionId: () => uuid().replace(/-/g, ''),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Component state
|
// Component state
|
||||||
const messages = ref<ChatMessage[]>([]);
|
const messages = ref<ChatMessage[]>([]);
|
||||||
const currentSessionId = ref<string>(props.initialSessionId);
|
const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
|
||||||
const isDisabled = ref(false);
|
const isDisabled = ref(false);
|
||||||
const container = ref<HTMLElement>();
|
const container = ref<HTMLElement>();
|
||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const workflow = computed(() => props.workflowsStore.getCurrentWorkflow());
|
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
||||||
const isLoading = computed(() => props.uiStore.isActionActive.workflowRunning);
|
const isLoading = computed(() => {
|
||||||
const allConnections = computed(() => props.workflowsStore.allConnections);
|
const result = uiStore.isActionActive.workflowRunning;
|
||||||
const isChatOpen = computed(() => props.workflowsStore.isChatPanelOpen);
|
return result;
|
||||||
const isLogsOpen = computed(() => props.workflowsStore.isLogsPanelOpen);
|
});
|
||||||
const previousChatMessages = computed(() => props.workflowsStore.getPastChatMessages);
|
const allConnections = computed(() => workflowsStore.allConnections);
|
||||||
|
const isChatOpen = computed(() => {
|
||||||
|
const result = workflowsStore.isChatPanelOpen;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
const isLogsOpen = computed(() => workflowsStore.isLogsPanelOpen);
|
||||||
|
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
||||||
|
|
||||||
// Expose internal state for testing
|
// Expose internal state for testing
|
||||||
defineExpose({
|
defineExpose({
|
||||||
|
@ -73,14 +64,15 @@ defineExpose({
|
||||||
isLoading,
|
isLoading,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { runWorkflow } = useRunWorkflow({ router: props.router });
|
const { runWorkflow } = useRunWorkflow({ router });
|
||||||
|
|
||||||
// Initialize features with injected dependencies
|
// Initialize features with injected dependencies
|
||||||
const { chatTriggerNode, connectedNode, allowFileUploads, setChatTriggerNode, setConnectedNode } =
|
const { chatTriggerNode, connectedNode, allowFileUploads, setChatTriggerNode, setConnectedNode } =
|
||||||
useChatTrigger({
|
useChatTrigger({
|
||||||
workflow,
|
workflow,
|
||||||
getNodeByName: props.workflowsStore.getNodeByName,
|
canvasNodes: workflowsStore.allNodes,
|
||||||
getNodeType: props.nodeTypesStore.getNodeType,
|
getNodeByName: workflowsStore.getNodeByName,
|
||||||
|
getNodeType: nodeTypesStore.getNodeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { sendMessage, getChatMessages } = useChatMessaging({
|
const { sendMessage, getChatMessages } = useChatMessaging({
|
||||||
|
@ -90,8 +82,8 @@ const { sendMessage, getChatMessages } = useChatMessaging({
|
||||||
sessionId: currentSessionId,
|
sessionId: currentSessionId,
|
||||||
workflow,
|
workflow,
|
||||||
isLoading,
|
isLoading,
|
||||||
executionResultData: computed(() => props.workflowsStore.getWorkflowExecution?.data?.resultData),
|
executionResultData: computed(() => workflowsStore.getWorkflowExecution?.data?.resultData),
|
||||||
getWorkflowResultDataByNodeName: props.workflowsStore.getWorkflowResultDataByNodeName,
|
getWorkflowResultDataByNodeName: workflowsStore.getWorkflowResultDataByNodeName,
|
||||||
onRunChatWorkflow,
|
onRunChatWorkflow,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -154,8 +146,8 @@ function displayExecution(params: { router: Router; workflowId: string; executio
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshSession(params: { messages: Ref<ChatMessage[]>; currentSessionId: Ref<string> }) {
|
function refreshSession(params: { messages: Ref<ChatMessage[]>; currentSessionId: Ref<string> }) {
|
||||||
props.workflowsStore.setWorkflowExecutionData(null);
|
workflowsStore.setWorkflowExecutionData(null);
|
||||||
props.nodeHelpers.updateNodesExecutionIssues();
|
nodeHelpers.updateNodesExecutionIssues();
|
||||||
params.messages.value = [];
|
params.messages.value = [];
|
||||||
params.currentSessionId.value = uuid().replace(/-/g, '');
|
params.currentSessionId.value = uuid().replace(/-/g, '');
|
||||||
}
|
}
|
||||||
|
@ -163,7 +155,7 @@ function refreshSession(params: { messages: Ref<ChatMessage[]>; currentSessionId
|
||||||
// Event handlers
|
// Event handlers
|
||||||
const handleDisplayExecution = (executionId: string) => {
|
const handleDisplayExecution = (executionId: string) => {
|
||||||
displayExecution({
|
displayExecution({
|
||||||
router: props.router,
|
router,
|
||||||
workflowId: workflow.value.id,
|
workflowId: workflow.value.id,
|
||||||
executionId,
|
executionId,
|
||||||
});
|
});
|
||||||
|
@ -177,7 +169,7 @@ const handleRefreshSession = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeLogs = () => {
|
const closeLogs = () => {
|
||||||
props.workflowsStore.setPanelOpen('logs', false);
|
workflowsStore.setPanelOpen('logs', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function onRunChatWorkflow(payload: RunWorkflowChatPayload) {
|
async function onRunChatWorkflow(payload: RunWorkflowChatPayload) {
|
||||||
|
@ -187,7 +179,7 @@ async function onRunChatWorkflow(payload: RunWorkflowChatPayload) {
|
||||||
source: payload.source,
|
source: payload.source,
|
||||||
});
|
});
|
||||||
|
|
||||||
props.workflowsStore.appendChatMessage(payload.message);
|
workflowsStore.appendChatMessage(payload.message);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,12 +216,13 @@ watch(
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => allConnections.value,
|
() => allConnections.value,
|
||||||
() => {
|
() => {
|
||||||
if (props.canvasStore.isLoading) return;
|
if (canvasStore.isLoading) return;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!chatTriggerNode.value) {
|
if (!chatTriggerNode.value) {
|
||||||
setChatTriggerNode();
|
setChatTriggerNode();
|
||||||
|
@ -241,7 +234,7 @@ watch(
|
||||||
);
|
);
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
props.canvasStore.setPanelHeight(isChatOpen.value || isLogsOpen.value ? height.value : 0);
|
canvasStore.setPanelHeight(isChatOpen.value || isLogsOpen.value ? height.value : 0);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -87,28 +87,48 @@ function onArrowKeyDown({ currentInputValue, key }: ArrowKeyDownPayload) {
|
||||||
currentInputValue.length === 0 || pastMessages.includes(currentInputValue);
|
currentInputValue.length === 0 || pastMessages.includes(currentInputValue);
|
||||||
|
|
||||||
if (isCurrentInputEmptyOrMatch && (key === 'ArrowUp' || key === 'ArrowDown')) {
|
if (isCurrentInputEmptyOrMatch && (key === 'ArrowUp' || key === 'ArrowDown')) {
|
||||||
// Blur the input when the user presses the up or down arrow key
|
// Exit if no messages
|
||||||
|
if (pastMessages.length === 0) return;
|
||||||
|
|
||||||
|
// Temporarily blur to avoid cursor position issues
|
||||||
chatEventBus.emit('blurInput');
|
chatEventBus.emit('blurInput');
|
||||||
|
|
||||||
if (pastMessages.length === 1) {
|
if (pastMessages.length === 1) {
|
||||||
previousMessageIndex.value = 0;
|
previousMessageIndex.value = 0;
|
||||||
} else if (key === 'ArrowUp') {
|
} else {
|
||||||
previousMessageIndex.value = (previousMessageIndex.value + 1) % pastMessages.length;
|
if (key === 'ArrowUp') {
|
||||||
} else if (key === 'ArrowDown') {
|
if (currentInputValue.length === 0 && previousMessageIndex.value === 0) {
|
||||||
previousMessageIndex.value =
|
// Start with most recent message
|
||||||
(previousMessageIndex.value - 1 + pastMessages.length) % pastMessages.length;
|
previousMessageIndex.value = pastMessages.length - 1;
|
||||||
|
} else {
|
||||||
|
// Move backwards through history
|
||||||
|
previousMessageIndex.value =
|
||||||
|
previousMessageIndex.value === 0
|
||||||
|
? pastMessages.length - 1
|
||||||
|
: previousMessageIndex.value - 1;
|
||||||
|
}
|
||||||
|
} else if (key === 'ArrowDown') {
|
||||||
|
// Move forwards through history
|
||||||
|
previousMessageIndex.value =
|
||||||
|
previousMessageIndex.value === pastMessages.length - 1
|
||||||
|
? 0
|
||||||
|
: previousMessageIndex.value + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chatEventBus.emit(
|
// Get message at current index
|
||||||
'setInputValue',
|
const selectedMessage = pastMessages[previousMessageIndex.value];
|
||||||
pastMessages[pastMessages.length - 1 - previousMessageIndex.value] ?? '',
|
chatEventBus.emit('setInputValue', selectedMessage);
|
||||||
);
|
|
||||||
|
|
||||||
// Refocus to move the cursor to the end of the input
|
// Refocus and move cursor to end
|
||||||
chatEventBus.emit('focusInput');
|
chatEventBus.emit('focusInput');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// Reset history navigation when typing new content that doesn't match history
|
||||||
|
if (!isCurrentInputEmptyOrMatch) {
|
||||||
|
previousMessageIndex.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
function copySessionId() {
|
function copySessionId() {
|
||||||
void clipboard.copy(props.sessionId);
|
void clipboard.copy(props.sessionId);
|
||||||
toast.showMessage({
|
toast.showMessage({
|
||||||
|
@ -129,10 +149,13 @@ function copySessionId() {
|
||||||
<template #content>
|
<template #content>
|
||||||
{{ sessionId }}
|
{{ sessionId }}
|
||||||
</template>
|
</template>
|
||||||
<span :class="$style.sessionId" @click="copySessionId">{{ sessionId }}</span>
|
<span :class="$style.sessionId" @click="copySessionId" data-test-id="chat-session-id">{{
|
||||||
|
sessionId
|
||||||
|
}}</span>
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
<n8n-icon-button
|
<n8n-icon-button
|
||||||
:class="$style.refreshSession"
|
:class="$style.refreshSession"
|
||||||
|
data-test-id="refresh-session-button"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
text
|
text
|
||||||
size="mini"
|
size="mini"
|
||||||
|
@ -148,6 +171,7 @@ function copySessionId() {
|
||||||
<MessageOptionTooltip
|
<MessageOptionTooltip
|
||||||
v-if="message.sender === 'bot' && !message.id.includes('preload')"
|
v-if="message.sender === 'bot' && !message.id.includes('preload')"
|
||||||
placement="right"
|
placement="right"
|
||||||
|
data-test-id="execution-id-tooltip"
|
||||||
>
|
>
|
||||||
{{ locale.baseText('chat.window.chat.chatMessageOptions.executionId') }}:
|
{{ locale.baseText('chat.window.chat.chatMessageOptions.executionId') }}:
|
||||||
<a href="#" @click="emit('displayExecution', message.id)">{{ message.id }}</a>
|
<a href="#" @click="emit('displayExecution', message.id)">{{ message.id }}</a>
|
||||||
|
|
|
@ -267,11 +267,10 @@ export function useChatMessaging({
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChatMessages(): ChatMessageText[] {
|
function getChatMessages(): ChatMessageText[] {
|
||||||
console.log('Getting chat messages', connectedNode.value);
|
|
||||||
if (!connectedNode.value) return [];
|
if (!connectedNode.value) return [];
|
||||||
|
|
||||||
const connectedMemoryInputs =
|
const connectedMemoryInputs =
|
||||||
workflow.value.connectionsByDestinationNode[connectedNode.value.name]?.[
|
workflow.value.connectionsByDestinationNode?.[connectedNode.value.name]?.[
|
||||||
NodeConnectionType.AiMemory
|
NodeConnectionType.AiMemory
|
||||||
];
|
];
|
||||||
if (!connectedMemoryInputs) return [];
|
if (!connectedMemoryInputs) return [];
|
||||||
|
|
|
@ -25,10 +25,16 @@ import type { INodeUi } from '@/Interface';
|
||||||
export interface ChatTriggerDependencies {
|
export interface ChatTriggerDependencies {
|
||||||
getNodeByName: (name: string) => INodeUi | null;
|
getNodeByName: (name: string) => INodeUi | null;
|
||||||
getNodeType: (type: string, version: number) => INodeTypeDescription | null;
|
getNodeType: (type: string, version: number) => INodeTypeDescription | null;
|
||||||
|
canvasNodes: INodeUi[];
|
||||||
workflow: ComputedRef<Workflow>;
|
workflow: ComputedRef<Workflow>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChatTrigger({ getNodeByName, getNodeType, workflow }: ChatTriggerDependencies) {
|
export function useChatTrigger({
|
||||||
|
getNodeByName,
|
||||||
|
getNodeType,
|
||||||
|
canvasNodes,
|
||||||
|
workflow,
|
||||||
|
}: ChatTriggerDependencies) {
|
||||||
const chatTriggerName = ref<string | null>(null);
|
const chatTriggerName = ref<string | null>(null);
|
||||||
const connectedNode = ref<INode | null>(null);
|
const connectedNode = ref<INode | null>(null);
|
||||||
|
|
||||||
|
@ -52,14 +58,14 @@ export function useChatTrigger({ getNodeByName, getNodeType, workflow }: ChatTri
|
||||||
|
|
||||||
/** Gets the chat trigger node from the workflow */
|
/** Gets the chat trigger node from the workflow */
|
||||||
function setChatTriggerNode() {
|
function setChatTriggerNode() {
|
||||||
const triggerNode = workflow.value.queryNodes((nodeType: INodeType) =>
|
const triggerNode = canvasNodes.find((node) =>
|
||||||
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(nodeType.description.name),
|
[CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE].includes(node.type),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!triggerNode.length) {
|
if (!triggerNode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
chatTriggerName.value = triggerNode[0].name;
|
chatTriggerName.value = triggerNode.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sets the connected node after finding the trigger */
|
/** Sets the connected node after finding the trigger */
|
||||||
|
@ -124,7 +130,6 @@ export function useChatTrigger({ getNodeByName, getNodeType, workflow }: ChatTri
|
||||||
const result = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
const result = Boolean(isChatChild && (isAgent || isChain || isCustomChainOrAgent));
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
connectedNode.value = chatRootNode ?? null;
|
connectedNode.value = chatRootNode ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue