mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(editor): Canvas chat UI & UX improvements (#11924)
This commit is contained in:
@@ -38,12 +38,12 @@ const isSubmitting = ref(false);
|
|||||||
const resizeObserver = ref<ResizeObserver | null>(null);
|
const resizeObserver = ref<ResizeObserver | null>(null);
|
||||||
|
|
||||||
const isSubmitDisabled = computed(() => {
|
const isSubmitDisabled = computed(() => {
|
||||||
return input.value === '' || waitingForResponse.value || options.disabled?.value === true;
|
return input.value === '' || unref(waitingForResponse) || options.disabled?.value === true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isInputDisabled = computed(() => options.disabled?.value === true);
|
const isInputDisabled = computed(() => options.disabled?.value === true);
|
||||||
const isFileUploadDisabled = computed(
|
const isFileUploadDisabled = computed(
|
||||||
() => isFileUploadAllowed.value && waitingForResponse.value && !options.disabled?.value,
|
() => isFileUploadAllowed.value && unref(waitingForResponse) && !options.disabled?.value,
|
||||||
);
|
);
|
||||||
const isFileUploadAllowed = computed(() => unref(options.allowFileUploads) === true);
|
const isFileUploadAllowed = computed(() => unref(options.allowFileUploads) === true);
|
||||||
const allowedFileTypes = computed(() => unref(options.allowedFilesMimeTypes));
|
const allowedFileTypes = computed(() => unref(options.allowedFilesMimeTypes));
|
||||||
@@ -194,10 +194,13 @@ function adjustHeight(event: Event) {
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-input" :style="styleVars" @keydown.stop="onKeyDown">
|
<div class="chat-input" :style="styleVars" @keydown.stop="onKeyDown">
|
||||||
<div class="chat-inputs">
|
<div class="chat-inputs">
|
||||||
|
<div v-if="$slots.leftPanel" class="chat-input-left-panel">
|
||||||
|
<slot name="leftPanel" />
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
ref="chatTextArea"
|
ref="chatTextArea"
|
||||||
data-test-id="chat-input"
|
|
||||||
v-model="input"
|
v-model="input"
|
||||||
|
data-test-id="chat-input"
|
||||||
:disabled="isInputDisabled"
|
:disabled="isInputDisabled"
|
||||||
:placeholder="t(props.placeholder)"
|
:placeholder="t(props.placeholder)"
|
||||||
@keydown.enter="onSubmitKeydown"
|
@keydown.enter="onSubmitKeydown"
|
||||||
@@ -251,7 +254,7 @@ function adjustHeight(event: Event) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
@@ -259,8 +262,7 @@ function adjustHeight(event: Event) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border: var(--chat--input--border, 0);
|
border: var(--chat--input--border, 0);
|
||||||
border-radius: var(--chat--input--border-radius, 0);
|
border-radius: var(--chat--input--border-radius, 0);
|
||||||
padding: 0.8rem;
|
padding: var(--chat--input--padding, 0.8rem);
|
||||||
padding-right: calc(0.8rem + (var(--controls-count, 1) * var(--chat--textarea--height)));
|
|
||||||
min-height: var(--chat--textarea--height, 2.5rem); // Set a smaller initial height
|
min-height: var(--chat--textarea--height, 2.5rem); // Set a smaller initial height
|
||||||
max-height: var(--chat--textarea--max-height, 30rem);
|
max-height: var(--chat--textarea--max-height, 30rem);
|
||||||
height: var(--chat--textarea--height, 2.5rem); // Set initial height same as min-height
|
height: var(--chat--textarea--height, 2.5rem); // Set initial height same as min-height
|
||||||
@@ -271,6 +273,9 @@ function adjustHeight(event: Event) {
|
|||||||
outline: none;
|
outline: none;
|
||||||
line-height: var(--chat--input--line-height, 1.5);
|
line-height: var(--chat--input--line-height, 1.5);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
font-size: var(--chat--input--placeholder--font-size, var(--chat--input--font-size, inherit));
|
||||||
|
}
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--chat--input--border-active, 0);
|
border-color: var(--chat--input--border-active, 0);
|
||||||
@@ -279,9 +284,6 @@ function adjustHeight(event: Event) {
|
|||||||
}
|
}
|
||||||
.chat-inputs-controls {
|
.chat-inputs-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
|
||||||
right: 0.5rem;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
}
|
||||||
.chat-input-send-button,
|
.chat-input-send-button,
|
||||||
.chat-input-file-button {
|
.chat-input-file-button {
|
||||||
@@ -340,4 +342,9 @@ function adjustHeight(event: Event) {
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: var(--chat--files-spacing, 0.25rem);
|
padding: var(--chat--files-spacing, 0.25rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-input-left-panel {
|
||||||
|
width: var(--chat--input--left--panel--width, 2rem);
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ onMounted(async () => {
|
|||||||
font-size: var(--chat--message--font-size, 1rem);
|
font-size: var(--chat--message--font-size, 1rem);
|
||||||
padding: var(--chat--message--padding, var(--chat--spacing));
|
padding: var(--chat--message--padding, var(--chat--spacing));
|
||||||
border-radius: var(--chat--message--border-radius, var(--chat--border-radius));
|
border-radius: var(--chat--message--border-radius, var(--chat--border-radius));
|
||||||
scroll-margin: 100px;
|
scroll-margin: 3rem;
|
||||||
|
|
||||||
.chat-message-actions {
|
.chat-message-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(100% - 0.5rem);
|
bottom: calc(100% - 0.5rem);
|
||||||
@@ -151,9 +152,6 @@ onMounted(async () => {
|
|||||||
left: auto;
|
left: auto;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
&.chat-message-from-bot .chat-message-actions {
|
|
||||||
bottom: calc(100% - 1rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.chat-message-actions {
|
.chat-message-actions {
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ body {
|
|||||||
4. Prevent font size adjustment after orientation changes (IE, iOS)
|
4. Prevent font size adjustment after orientation changes (IE, iOS)
|
||||||
5. Prevent overflow from long words (all)
|
5. Prevent overflow from long words (all)
|
||||||
*/
|
*/
|
||||||
font-size: 110%; /* 2 */
|
line-height: 1.4; /* 3 */
|
||||||
line-height: 1.6; /* 3 */
|
|
||||||
-webkit-text-size-adjust: 100%; /* 4 */
|
-webkit-text-size-adjust: 100%; /* 4 */
|
||||||
word-break: break-word; /* 5 */
|
word-break: break-word; /* 5 */
|
||||||
|
|
||||||
@@ -407,7 +406,7 @@ body {
|
|||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
margin: 3.2rem 0 0.8em;
|
margin: 2rem 0 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -641,4 +640,15 @@ body {
|
|||||||
body > a:first-child:focus {
|
body > a:first-child:focus {
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lists
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const inputSchemaField: INodeProperties = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const promptTypeOptions: INodeProperties = {
|
export const promptTypeOptions: INodeProperties = {
|
||||||
displayName: 'Prompt Source',
|
displayName: 'Prompt Source (User Message)',
|
||||||
name: 'promptType',
|
name: 'promptType',
|
||||||
type: 'options',
|
type: 'options',
|
||||||
options: [
|
options: [
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { waitFor } from '@testing-library/vue';
|
|||||||
import { userEvent } from '@testing-library/user-event';
|
import { userEvent } from '@testing-library/user-event';
|
||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
|
||||||
import CanvasChat from './CanvasChat.vue';
|
import CanvasChat from './CanvasChat.vue';
|
||||||
@@ -15,7 +16,6 @@ import { ChatOptionsSymbol, ChatSymbol } from '@n8n/chat/constants';
|
|||||||
import { chatEventBus } from '@n8n/chat/event-buses';
|
import { chatEventBus } from '@n8n/chat/event-buses';
|
||||||
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useCanvasStore } from '@/stores/canvas.store';
|
import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
import * as useChatMessaging from './composables/useChatMessaging';
|
import * as useChatMessaging from './composables/useChatMessaging';
|
||||||
import * as useChatTrigger from './composables/useChatTrigger';
|
import * as useChatTrigger from './composables/useChatTrigger';
|
||||||
@@ -23,6 +23,7 @@ import { useToast } from '@/composables/useToast';
|
|||||||
|
|
||||||
import type { IExecutionResponse, INodeUi } from '@/Interface';
|
import type { IExecutionResponse, INodeUi } from '@/Interface';
|
||||||
import type { ChatMessage } from '@n8n/chat/types';
|
import type { ChatMessage } from '@n8n/chat/types';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
|
||||||
vi.mock('@/composables/useToast', () => {
|
vi.mock('@/composables/useToast', () => {
|
||||||
const showMessage = vi.fn();
|
const showMessage = vi.fn();
|
||||||
@@ -61,6 +62,26 @@ const mockNodes: INodeUi[] = [
|
|||||||
position: [960, 860],
|
position: [960, 860],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const mockNodeTypes: INodeTypeDescription[] = [
|
||||||
|
{
|
||||||
|
displayName: 'AI Agent',
|
||||||
|
name: '@n8n/n8n-nodes-langchain.agent',
|
||||||
|
properties: [],
|
||||||
|
defaults: {
|
||||||
|
name: 'AI Agent',
|
||||||
|
},
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
version: 0,
|
||||||
|
group: [],
|
||||||
|
description: '',
|
||||||
|
codex: {
|
||||||
|
subcategories: {
|
||||||
|
AI: ['Agents'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const mockConnections = {
|
const mockConnections = {
|
||||||
'When chat message received': {
|
'When chat message received': {
|
||||||
@@ -110,8 +131,8 @@ describe('CanvasChat', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
|
||||||
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
|
|
||||||
let canvasStore: ReturnType<typeof mockedStore<typeof useCanvasStore>>;
|
let canvasStore: ReturnType<typeof mockedStore<typeof useCanvasStore>>;
|
||||||
|
let nodeTypeStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const pinia = createTestingPinia({
|
const pinia = createTestingPinia({
|
||||||
@@ -131,8 +152,8 @@ describe('CanvasChat', () => {
|
|||||||
setActivePinia(pinia);
|
setActivePinia(pinia);
|
||||||
|
|
||||||
workflowsStore = mockedStore(useWorkflowsStore);
|
workflowsStore = mockedStore(useWorkflowsStore);
|
||||||
uiStore = mockedStore(useUIStore);
|
|
||||||
canvasStore = mockedStore(useCanvasStore);
|
canvasStore = mockedStore(useCanvasStore);
|
||||||
|
nodeTypeStore = mockedStore(useNodeTypesStore);
|
||||||
|
|
||||||
// Setup default mocks
|
// Setup default mocks
|
||||||
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
workflowsStore.getCurrentWorkflow.mockReturnValue(
|
||||||
@@ -141,12 +162,21 @@ describe('CanvasChat', () => {
|
|||||||
connections: mockConnections,
|
connections: mockConnections,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
workflowsStore.getNodeByName.mockImplementation(
|
workflowsStore.getNodeByName.mockImplementation((name) => {
|
||||||
(name) => mockNodes.find((node) => node.name === name) ?? null,
|
const matchedNode = mockNodes.find((node) => node.name === name) ?? null;
|
||||||
);
|
|
||||||
|
return matchedNode;
|
||||||
|
});
|
||||||
workflowsStore.isChatPanelOpen = true;
|
workflowsStore.isChatPanelOpen = true;
|
||||||
|
workflowsStore.isLogsPanelOpen = true;
|
||||||
workflowsStore.getWorkflowExecution = mockWorkflowExecution as unknown as IExecutionResponse;
|
workflowsStore.getWorkflowExecution = mockWorkflowExecution as unknown as IExecutionResponse;
|
||||||
workflowsStore.getPastChatMessages = ['Previous message 1', 'Previous message 2'];
|
workflowsStore.getPastChatMessages = ['Previous message 1', 'Previous message 2'];
|
||||||
|
|
||||||
|
nodeTypeStore.getNodeType = vi.fn().mockImplementation((nodeTypeName) => {
|
||||||
|
return mockNodeTypes.find((node) => node.name === nodeTypeName) ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowsStore.runWorkflow.mockResolvedValue({ executionId: 'test-execution-issd' });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -190,6 +220,10 @@ describe('CanvasChat', () => {
|
|||||||
// Verify message and response
|
// Verify message and response
|
||||||
expect(await findByText('Hello AI!')).toBeInTheDocument();
|
expect(await findByText('Hello AI!')).toBeInTheDocument();
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
|
workflowsStore.getWorkflowExecution = {
|
||||||
|
...(mockWorkflowExecution as unknown as IExecutionResponse),
|
||||||
|
status: 'success',
|
||||||
|
};
|
||||||
expect(await findByText('AI response message')).toBeInTheDocument();
|
expect(await findByText('AI response message')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -231,11 +265,12 @@ describe('CanvasChat', () => {
|
|||||||
await userEvent.type(input, 'Test message');
|
await userEvent.type(input, 'Test message');
|
||||||
await userEvent.keyboard('{Enter}');
|
await userEvent.keyboard('{Enter}');
|
||||||
|
|
||||||
// Verify loading states
|
|
||||||
uiStore.isActionActive = { workflowRunning: true };
|
|
||||||
await waitFor(() => expect(queryByTestId('chat-message-typing')).toBeInTheDocument());
|
await waitFor(() => expect(queryByTestId('chat-message-typing')).toBeInTheDocument());
|
||||||
|
|
||||||
uiStore.isActionActive = { workflowRunning: false };
|
workflowsStore.getWorkflowExecution = {
|
||||||
|
...(mockWorkflowExecution as unknown as IExecutionResponse),
|
||||||
|
status: 'success',
|
||||||
|
};
|
||||||
await waitFor(() => expect(queryByTestId('chat-message-typing')).not.toBeInTheDocument());
|
await waitFor(() => expect(queryByTestId('chat-message-typing')).not.toBeInTheDocument());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -269,7 +304,7 @@ describe('CanvasChat', () => {
|
|||||||
sendMessage: vi.fn(),
|
sendMessage: vi.fn(),
|
||||||
extractResponseMessage: vi.fn(),
|
extractResponseMessage: vi.fn(),
|
||||||
previousMessageIndex: ref(0),
|
previousMessageIndex: ref(0),
|
||||||
waitForExecution: vi.fn(),
|
isLoading: computed(() => false),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -339,7 +374,7 @@ describe('CanvasChat', () => {
|
|||||||
sendMessage: vi.fn(),
|
sendMessage: vi.fn(),
|
||||||
extractResponseMessage: vi.fn(),
|
extractResponseMessage: vi.fn(),
|
||||||
previousMessageIndex: ref(0),
|
previousMessageIndex: ref(0),
|
||||||
waitForExecution: vi.fn(),
|
isLoading: computed(() => false),
|
||||||
});
|
});
|
||||||
|
|
||||||
workflowsStore.isChatPanelOpen = true;
|
workflowsStore.isChatPanelOpen = true;
|
||||||
@@ -437,7 +472,7 @@ describe('CanvasChat', () => {
|
|||||||
sendMessage: sendMessageSpy,
|
sendMessage: sendMessageSpy,
|
||||||
extractResponseMessage: vi.fn(),
|
extractResponseMessage: vi.fn(),
|
||||||
previousMessageIndex: ref(0),
|
previousMessageIndex: ref(0),
|
||||||
waitForExecution: vi.fn(),
|
isLoading: computed(() => false),
|
||||||
});
|
});
|
||||||
workflowsStore.messages = mockMessages;
|
workflowsStore.messages = mockMessages;
|
||||||
});
|
});
|
||||||
@@ -449,26 +484,25 @@ describe('CanvasChat', () => {
|
|||||||
await userEvent.click(repostButton);
|
await userEvent.click(repostButton);
|
||||||
|
|
||||||
expect(sendMessageSpy).toHaveBeenCalledWith('Original message');
|
expect(sendMessageSpy).toHaveBeenCalledWith('Original message');
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// runData: expect.objectContaining({
|
runData: expect.objectContaining({
|
||||||
// 'When chat message received': expect.arrayContaining([
|
'When chat message received': expect.arrayContaining([
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
// main: expect.arrayContaining([
|
main: expect.arrayContaining([
|
||||||
// expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
// expect.objectContaining({
|
expect.objectContaining({
|
||||||
// json: expect.objectContaining({
|
json: expect.objectContaining({
|
||||||
// chatInput: 'Original message',
|
chatInput: 'Original message',
|
||||||
// }),
|
}),
|
||||||
// }),
|
}),
|
||||||
// ]),
|
]),
|
||||||
// ]),
|
]),
|
||||||
// }),
|
}),
|
||||||
// }),
|
}),
|
||||||
// ]),
|
]),
|
||||||
// }),
|
}),
|
||||||
// }),
|
});
|
||||||
// );
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show message options only for appropriate messages', async () => {
|
it('should show message options only for appropriate messages', async () => {
|
||||||
@@ -494,32 +528,6 @@ describe('CanvasChat', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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', () => {
|
describe('panel state synchronization', () => {
|
||||||
it('should update canvas height when chat or logs panel state changes', async () => {
|
it('should update canvas height when chat or logs panel state changes', async () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|||||||
@@ -25,11 +25,9 @@ import type { Chat, ChatMessage, ChatOptions } from '@n8n/chat/types';
|
|||||||
import type { RunWorkflowChatPayload } from './composables/useChatMessaging';
|
import type { RunWorkflowChatPayload } from './composables/useChatMessaging';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useCanvasStore } from '@/stores/canvas.store';
|
import { useCanvasStore } from '@/stores/canvas.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const uiStore = useUIStore();
|
|
||||||
const canvasStore = useCanvasStore();
|
const canvasStore = useCanvasStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
@@ -43,10 +41,7 @@ const container = ref<HTMLElement>();
|
|||||||
|
|
||||||
// Computed properties
|
// Computed properties
|
||||||
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
|
||||||
const isLoading = computed(() => {
|
|
||||||
const result = uiStore.isActionActive.workflowRunning;
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
const allConnections = computed(() => workflowsStore.allConnections);
|
const allConnections = computed(() => workflowsStore.allConnections);
|
||||||
const isChatOpen = computed(() => {
|
const isChatOpen = computed(() => {
|
||||||
const result = workflowsStore.isChatPanelOpen;
|
const result = workflowsStore.isChatPanelOpen;
|
||||||
@@ -55,34 +50,38 @@ const isChatOpen = computed(() => {
|
|||||||
const canvasNodes = computed(() => workflowsStore.allNodes);
|
const canvasNodes = computed(() => workflowsStore.allNodes);
|
||||||
const isLogsOpen = computed(() => workflowsStore.isLogsPanelOpen);
|
const isLogsOpen = computed(() => workflowsStore.isLogsPanelOpen);
|
||||||
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
||||||
|
const resultData = computed(() => workflowsStore.getWorkflowRunData);
|
||||||
// Expose internal state for testing
|
// Expose internal state for testing
|
||||||
defineExpose({
|
defineExpose({
|
||||||
messages,
|
messages,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
workflow,
|
workflow,
|
||||||
isLoading,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { runWorkflow } = useRunWorkflow({ router });
|
const { runWorkflow } = useRunWorkflow({ router });
|
||||||
|
|
||||||
// Initialize features with injected dependencies
|
// Initialize features with injected dependencies
|
||||||
const { chatTriggerNode, connectedNode, allowFileUploads, setChatTriggerNode, setConnectedNode } =
|
const {
|
||||||
useChatTrigger({
|
chatTriggerNode,
|
||||||
|
connectedNode,
|
||||||
|
allowFileUploads,
|
||||||
|
allowedFilesMimeTypes,
|
||||||
|
setChatTriggerNode,
|
||||||
|
setConnectedNode,
|
||||||
|
} = useChatTrigger({
|
||||||
workflow,
|
workflow,
|
||||||
canvasNodes,
|
canvasNodes,
|
||||||
getNodeByName: workflowsStore.getNodeByName,
|
getNodeByName: workflowsStore.getNodeByName,
|
||||||
getNodeType: nodeTypesStore.getNodeType,
|
getNodeType: nodeTypesStore.getNodeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { sendMessage, getChatMessages } = useChatMessaging({
|
const { sendMessage, getChatMessages, isLoading } = useChatMessaging({
|
||||||
chatTrigger: chatTriggerNode,
|
chatTrigger: chatTriggerNode,
|
||||||
connectedNode,
|
connectedNode,
|
||||||
messages,
|
messages,
|
||||||
sessionId: currentSessionId,
|
sessionId: currentSessionId,
|
||||||
workflow,
|
workflow,
|
||||||
isLoading,
|
|
||||||
executionResultData: computed(() => workflowsStore.getWorkflowExecution?.data?.resultData),
|
executionResultData: computed(() => workflowsStore.getWorkflowExecution?.data?.resultData),
|
||||||
getWorkflowResultDataByNodeName: workflowsStore.getWorkflowResultDataByNodeName,
|
getWorkflowResultDataByNodeName: workflowsStore.getWorkflowResultDataByNodeName,
|
||||||
onRunChatWorkflow,
|
onRunChatWorkflow,
|
||||||
@@ -132,7 +131,7 @@ function createChatConfig(params: {
|
|||||||
showWindowCloseButton: true,
|
showWindowCloseButton: true,
|
||||||
disabled: params.isDisabled,
|
disabled: params.isDisabled,
|
||||||
allowFileUploads: params.allowFileUploads,
|
allowFileUploads: params.allowFileUploads,
|
||||||
allowedFilesMimeTypes: '',
|
allowedFilesMimeTypes,
|
||||||
};
|
};
|
||||||
|
|
||||||
return { chatConfig, chatOptions };
|
return { chatConfig, chatOptions };
|
||||||
@@ -173,16 +172,51 @@ const closePanel = () => {
|
|||||||
workflowsStore.setPanelOpen('chat', false);
|
workflowsStore.setPanelOpen('chat', false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This function creates a promise that resolves when the workflow execution completes
|
||||||
|
// It's used to handle the loading state while waiting for the workflow to finish
|
||||||
|
async function createExecutionPromise() {
|
||||||
|
let resolvePromise: () => void;
|
||||||
|
const promise = new Promise<void>((resolve) => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes in the workflow execution status
|
||||||
|
const stopWatch = watch(
|
||||||
|
() => workflowsStore.getWorkflowExecution?.status,
|
||||||
|
(newStatus) => {
|
||||||
|
// If the status is no longer 'running', resolve the promise
|
||||||
|
if (newStatus && newStatus !== 'running') {
|
||||||
|
resolvePromise();
|
||||||
|
// Stop the watcher when the promise is resolved
|
||||||
|
stopWatch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }, // Check the status immediately when the watcher is set up
|
||||||
|
);
|
||||||
|
|
||||||
|
// Return the promise, which will resolve when the workflow execution is complete
|
||||||
|
// This allows the caller to await the execution and handle the loading state appropriately
|
||||||
|
return await promise;
|
||||||
|
}
|
||||||
|
|
||||||
async function onRunChatWorkflow(payload: RunWorkflowChatPayload) {
|
async function onRunChatWorkflow(payload: RunWorkflowChatPayload) {
|
||||||
|
try {
|
||||||
const response = await runWorkflow({
|
const response = await runWorkflow({
|
||||||
triggerNode: payload.triggerNode,
|
triggerNode: payload.triggerNode,
|
||||||
nodeData: payload.nodeData,
|
nodeData: payload.nodeData,
|
||||||
source: payload.source,
|
source: payload.source,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
await createExecutionPromise();
|
||||||
workflowsStore.appendChatMessage(payload.message);
|
workflowsStore.appendChatMessage(payload.message);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize chat config
|
// Initialize chat config
|
||||||
const { chatConfig, chatOptions } = createChatConfig({
|
const { chatConfig, chatOptions } = createChatConfig({
|
||||||
@@ -264,6 +298,8 @@ watchEffect(() => {
|
|||||||
:messages="messages"
|
:messages="messages"
|
||||||
:session-id="currentSessionId"
|
:session-id="currentSessionId"
|
||||||
:past-chat-messages="previousChatMessages"
|
:past-chat-messages="previousChatMessages"
|
||||||
|
:show-close-button="!connectedNode"
|
||||||
|
@close="closePanel"
|
||||||
@refresh-session="handleRefreshSession"
|
@refresh-session="handleRefreshSession"
|
||||||
@display-execution="handleDisplayExecution"
|
@display-execution="handleDisplayExecution"
|
||||||
@send-message="sendMessage"
|
@send-message="sendMessage"
|
||||||
@@ -272,7 +308,7 @@ watchEffect(() => {
|
|||||||
</n8n-resize-wrapper>
|
</n8n-resize-wrapper>
|
||||||
<div v-if="isLogsOpen && connectedNode" :class="$style.logs">
|
<div v-if="isLogsOpen && connectedNode" :class="$style.logs">
|
||||||
<ChatLogsPanel
|
<ChatLogsPanel
|
||||||
:key="messages.length"
|
:key="`${resultData?.length ?? messages?.length}`"
|
||||||
:workflow="workflow"
|
:workflow="workflow"
|
||||||
data-test-id="canvas-chat-logs"
|
data-test-id="canvas-chat-logs"
|
||||||
:node="connectedNode"
|
:node="connectedNode"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ interface Props {
|
|||||||
pastChatMessages: string[];
|
pastChatMessages: string[];
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
showCloseButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
@@ -25,6 +26,7 @@ const emit = defineEmits<{
|
|||||||
displayExecution: [id: string];
|
displayExecution: [id: string];
|
||||||
sendMessage: [message: string];
|
sendMessage: [message: string];
|
||||||
refreshSession: [];
|
refreshSession: [];
|
||||||
|
close: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const messageComposable = useMessage();
|
const messageComposable = useMessage();
|
||||||
@@ -142,7 +144,7 @@ function copySessionId() {
|
|||||||
<template>
|
<template>
|
||||||
<div :class="$style.chat" data-test-id="workflow-lm-chat-dialog">
|
<div :class="$style.chat" data-test-id="workflow-lm-chat-dialog">
|
||||||
<header :class="$style.chatHeader">
|
<header :class="$style.chatHeader">
|
||||||
<span>{{ locale.baseText('chat.window.title') }}</span>
|
<span :class="$style.chatTitle">{{ locale.baseText('chat.window.title') }}</span>
|
||||||
<div :class="$style.session">
|
<div :class="$style.session">
|
||||||
<span>{{ locale.baseText('chat.window.session.title') }}</span>
|
<span>{{ locale.baseText('chat.window.session.title') }}</span>
|
||||||
<n8n-tooltip placement="left">
|
<n8n-tooltip placement="left">
|
||||||
@@ -154,15 +156,24 @@ function copySessionId() {
|
|||||||
}}</span>
|
}}</span>
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
<n8n-icon-button
|
<n8n-icon-button
|
||||||
:class="$style.refreshSession"
|
:class="$style.headerButton"
|
||||||
data-test-id="refresh-session-button"
|
data-test-id="refresh-session-button"
|
||||||
type="tertiary"
|
outline
|
||||||
text
|
type="secondary"
|
||||||
size="mini"
|
size="mini"
|
||||||
icon="undo"
|
icon="undo"
|
||||||
:title="locale.baseText('chat.window.session.reset.confirm')"
|
:title="locale.baseText('chat.window.session.reset.confirm')"
|
||||||
@click="onRefreshSession"
|
@click="onRefreshSession"
|
||||||
/>
|
/>
|
||||||
|
<n8n-icon-button
|
||||||
|
v-if="showCloseButton"
|
||||||
|
:class="$style.headerButton"
|
||||||
|
outline
|
||||||
|
type="secondary"
|
||||||
|
size="mini"
|
||||||
|
icon="times"
|
||||||
|
@click="emit('close')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main :class="$style.chatBody">
|
<main :class="$style.chatBody">
|
||||||
@@ -199,7 +210,13 @@ function copySessionId() {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div :class="$style.messagesInput">
|
<div :class="$style.messagesInput">
|
||||||
<div v-if="pastChatMessages.length > 0" :class="$style.messagesHistory">
|
<ChatInput
|
||||||
|
data-test-id="lm-chat-inputs"
|
||||||
|
:placeholder="inputPlaceholder"
|
||||||
|
@arrow-key-down="onArrowKeyDown"
|
||||||
|
>
|
||||||
|
<template v-if="pastChatMessages.length > 0" #leftPanel>
|
||||||
|
<div :class="$style.messagesHistory">
|
||||||
<n8n-button
|
<n8n-button
|
||||||
title="Navigate to previous message"
|
title="Navigate to previous message"
|
||||||
icon="chevron-up"
|
icon="chevron-up"
|
||||||
@@ -217,11 +234,8 @@ function copySessionId() {
|
|||||||
@click="onArrowKeyDown({ currentInputValue: '', key: 'ArrowDown' })"
|
@click="onArrowKeyDown({ currentInputValue: '', key: 'ArrowDown' })"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ChatInput
|
</template>
|
||||||
data-test-id="lm-chat-inputs"
|
</ChatInput>
|
||||||
:placeholder="inputPlaceholder"
|
|
||||||
@arrow-key-down="onArrowKeyDown"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -229,19 +243,22 @@ function copySessionId() {
|
|||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.chat {
|
.chat {
|
||||||
--chat--spacing: var(--spacing-xs);
|
--chat--spacing: var(--spacing-xs);
|
||||||
--chat--message--padding: var(--spacing-xs);
|
--chat--message--padding: var(--spacing-2xs);
|
||||||
--chat--message--font-size: var(--font-size-s);
|
--chat--message--font-size: var(--font-size-xs);
|
||||||
--chat--input--font-size: var(--font-size-s);
|
--chat--input--font-size: var(--font-size-s);
|
||||||
|
--chat--input--placeholder--font-size: var(--font-size-xs);
|
||||||
--chat--message--bot--background: transparent;
|
--chat--message--bot--background: transparent;
|
||||||
--chat--message--user--background: var(--color-text-lighter);
|
--chat--message--user--background: var(--color-text-lighter);
|
||||||
--chat--message--bot--color: var(--color-text-dark);
|
--chat--message--bot--color: var(--color-text-dark);
|
||||||
--chat--message--user--color: var(--color-text-dark);
|
--chat--message--user--color: var(--color-text-dark);
|
||||||
--chat--message--bot--border: none;
|
--chat--message--bot--border: none;
|
||||||
--chat--message--user--border: none;
|
--chat--message--user--border: none;
|
||||||
|
--chat--message--user--border: none;
|
||||||
|
--chat--input--padding: var(--spacing-xs);
|
||||||
--chat--color-typing: var(--color-text-light);
|
--chat--color-typing: var(--color-text-light);
|
||||||
--chat--textarea--max-height: calc(var(--panel-height) * 0.5);
|
--chat--textarea--max-height: calc(var(--panel-height) * 0.3);
|
||||||
--chat--message--pre--background: var(--color-foreground-light);
|
--chat--message--pre--background: var(--color-foreground-light);
|
||||||
|
--chat--textarea--height: 2.5rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -260,6 +277,9 @@ function copySessionId() {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.chatTitle {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
.session {
|
.session {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -275,8 +295,9 @@ function copySessionId() {
|
|||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.refreshSession {
|
.headerButton {
|
||||||
max-height: 1.1rem;
|
max-height: 1.1rem;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
.chatBody {
|
.chatBody {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -306,7 +327,7 @@ function copySessionId() {
|
|||||||
--chat--input--file--button--background: transparent;
|
--chat--input--file--button--background: transparent;
|
||||||
--chat--input--file--button--color: var(--color-primary);
|
--chat--input--file--button--color: var(--color-primary);
|
||||||
--chat--input--border-active: var(--input-focus-border-color, var(--color-secondary));
|
--chat--input--border-active: var(--input-focus-border-color, var(--color-secondary));
|
||||||
--chat--files-spacing: var(--spacing-2xs) 0;
|
--chat--files-spacing: var(--spacing-2xs);
|
||||||
--chat--input--background: transparent;
|
--chat--input--background: transparent;
|
||||||
--chat--input--file--button--color: var(--color-button-secondary-font);
|
--chat--input--file--button--color: var(--color-button-secondary-font);
|
||||||
--chat--input--file--button--color-hover: var(--color-primary);
|
--chat--input--file--button--color-hover: var(--color-primary);
|
||||||
@@ -318,7 +339,7 @@ function copySessionId() {
|
|||||||
--chat--input--text-color: var(--input-font-color, var(--color-text-dark));
|
--chat--input--text-color: var(--input-font-color, var(--color-text-dark));
|
||||||
}
|
}
|
||||||
|
|
||||||
padding: 0 0 0 var(--spacing-xs);
|
padding: var(--spacing-5xs);
|
||||||
margin: 0 var(--chat--spacing) var(--chat--spacing);
|
margin: 0 var(--chat--spacing) var(--chat--spacing);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -333,16 +354,4 @@ function copySessionId() {
|
|||||||
--input-border-color: #4538a3;
|
--input-border-color: #4538a3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.messagesHistory {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-bottom: var(--spacing-3xs);
|
|
||||||
|
|
||||||
button:first-child {
|
|
||||||
margin-top: var(--spacing-4xs);
|
|
||||||
margin-bottom: calc(-1 * var(--spacing-4xs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ComputedRef, Ref } from 'vue';
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
import { ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import type { ChatMessage, ChatMessageText } from '@n8n/chat/types';
|
import type { ChatMessage, ChatMessageText } from '@n8n/chat/types';
|
||||||
import { NodeConnectionType, CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow';
|
import { NodeConnectionType, CHAT_TRIGGER_NODE_TYPE } from 'n8n-workflow';
|
||||||
@@ -34,7 +34,6 @@ export interface ChatMessagingDependencies {
|
|||||||
messages: Ref<ChatMessage[]>;
|
messages: Ref<ChatMessage[]>;
|
||||||
sessionId: Ref<string>;
|
sessionId: Ref<string>;
|
||||||
workflow: ComputedRef<Workflow>;
|
workflow: ComputedRef<Workflow>;
|
||||||
isLoading: ComputedRef<boolean>;
|
|
||||||
executionResultData: ComputedRef<IRunExecutionData['resultData'] | undefined>;
|
executionResultData: ComputedRef<IRunExecutionData['resultData'] | undefined>;
|
||||||
getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null;
|
getWorkflowResultDataByNodeName: (nodeName: string) => ITaskData[] | null;
|
||||||
onRunChatWorkflow: (
|
onRunChatWorkflow: (
|
||||||
@@ -48,7 +47,6 @@ export function useChatMessaging({
|
|||||||
messages,
|
messages,
|
||||||
sessionId,
|
sessionId,
|
||||||
workflow,
|
workflow,
|
||||||
isLoading,
|
|
||||||
executionResultData,
|
executionResultData,
|
||||||
getWorkflowResultDataByNodeName,
|
getWorkflowResultDataByNodeName,
|
||||||
onRunChatWorkflow,
|
onRunChatWorkflow,
|
||||||
@@ -56,6 +54,7 @@ export function useChatMessaging({
|
|||||||
const locale = useI18n();
|
const locale = useI18n();
|
||||||
const { showError } = useToast();
|
const { showError } = useToast();
|
||||||
const previousMessageIndex = ref(0);
|
const previousMessageIndex = ref(0);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
/** Converts a file to binary data */
|
/** Converts a file to binary data */
|
||||||
async function convertFileToBinaryData(file: File): Promise<IBinaryData> {
|
async function convertFileToBinaryData(file: File): Promise<IBinaryData> {
|
||||||
@@ -147,37 +146,27 @@ export function useChatMessaging({
|
|||||||
},
|
},
|
||||||
source: [null],
|
source: [null],
|
||||||
};
|
};
|
||||||
|
isLoading.value = true;
|
||||||
const response = await onRunChatWorkflow({
|
const response = await onRunChatWorkflow({
|
||||||
triggerNode: triggerNode.name,
|
triggerNode: triggerNode.name,
|
||||||
nodeData,
|
nodeData,
|
||||||
source: 'RunData.ManualChatMessage',
|
source: 'RunData.ManualChatMessage',
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
|
isLoading.value = false;
|
||||||
if (!response?.executionId) {
|
if (!response?.executionId) {
|
||||||
showError(
|
|
||||||
new Error('It was not possible to start workflow!'),
|
|
||||||
'Workflow could not be started',
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForExecution(response.executionId);
|
processExecutionResultData(response.executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Waits for workflow execution to complete */
|
function processExecutionResultData(executionId: string) {
|
||||||
function waitForExecution(executionId: string) {
|
|
||||||
const waitInterval = setInterval(() => {
|
|
||||||
if (!isLoading.value) {
|
|
||||||
clearInterval(waitInterval);
|
|
||||||
|
|
||||||
const lastNodeExecuted = executionResultData.value?.lastNodeExecuted;
|
const lastNodeExecuted = executionResultData.value?.lastNodeExecuted;
|
||||||
|
|
||||||
if (!lastNodeExecuted) return;
|
if (!lastNodeExecuted) return;
|
||||||
|
|
||||||
const nodeResponseDataArray =
|
const nodeResponseDataArray = get(executionResultData.value.runData, lastNodeExecuted) ?? [];
|
||||||
get(executionResultData.value.runData, lastNodeExecuted) ?? [];
|
|
||||||
|
|
||||||
const nodeResponseData = nodeResponseDataArray[nodeResponseDataArray.length - 1];
|
const nodeResponseData = nodeResponseDataArray[nodeResponseDataArray.length - 1];
|
||||||
|
|
||||||
@@ -189,7 +178,7 @@ export function useChatMessaging({
|
|||||||
const responseData = get(nodeResponseData, 'data.main[0][0].json');
|
const responseData = get(nodeResponseData, 'data.main[0][0].json');
|
||||||
responseMessage = extractResponseMessage(responseData);
|
responseMessage = extractResponseMessage(responseData);
|
||||||
}
|
}
|
||||||
|
isLoading.value = false;
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
text: responseMessage,
|
text: responseMessage,
|
||||||
sender: 'bot',
|
sender: 'bot',
|
||||||
@@ -197,8 +186,6 @@ export function useChatMessaging({
|
|||||||
id: executionId ?? uuid(),
|
id: executionId ?? uuid(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Extracts response message from workflow output */
|
/** Extracts response message from workflow output */
|
||||||
function extractResponseMessage(responseData?: IDataObject) {
|
function extractResponseMessage(responseData?: IDataObject) {
|
||||||
@@ -291,9 +278,9 @@ export function useChatMessaging({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
previousMessageIndex,
|
previousMessageIndex,
|
||||||
|
isLoading: computed(() => isLoading.value),
|
||||||
sendMessage,
|
sendMessage,
|
||||||
extractResponseMessage,
|
extractResponseMessage,
|
||||||
waitForExecution,
|
|
||||||
getChatMessages,
|
getChatMessages,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ defineExpose({ editor });
|
|||||||
:global(.cm-content) {
|
:global(.cm-content) {
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
&[aria-readonly='true'] {
|
&[aria-readonly='true'] {
|
||||||
--disabled-fill: var(--color-background-medium);
|
--disabled-fill: var(--color-background-base);
|
||||||
background-color: var(--disabled-fill, var(--color-background-light));
|
background-color: var(--disabled-fill, var(--color-background-light));
|
||||||
color: var(--disabled-color, var(--color-text-base));
|
color: var(--disabled-color, var(--color-text-base));
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ defineExpose({
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
:deep(.cm-content) {
|
:deep(.cm-content) {
|
||||||
--disabled-fill: var(--color-background-medium);
|
--disabled-fill: var(--color-background-base);
|
||||||
padding-left: var(--spacing-2xs);
|
padding-left: var(--spacing-2xs);
|
||||||
|
|
||||||
&[aria-readonly='true'] {
|
&[aria-readonly='true'] {
|
||||||
|
|||||||
@@ -260,12 +260,13 @@ onMounted(() => {
|
|||||||
.contentText {
|
.contentText {
|
||||||
padding-top: var(--spacing-s);
|
padding-top: var(--spacing-s);
|
||||||
padding-left: var(--spacing-m);
|
padding-left: var(--spacing-m);
|
||||||
|
padding-right: var(--spacing-m);
|
||||||
font-size: var(--font-size-s);
|
font-size: var(--font-size-s);
|
||||||
}
|
}
|
||||||
.block {
|
.block {
|
||||||
padding: 0 0 var(--spacing-2xs) var(--spacing-2xs);
|
padding: var(--spacing-s) 0 var(--spacing-2xs) var(--spacing-2xs);
|
||||||
background: var(--color-foreground-light);
|
border: 1px solid var(--color-foreground-light);
|
||||||
margin-top: var(--spacing-xl);
|
margin-top: var(--spacing-s);
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
}
|
}
|
||||||
:root .blockContent {
|
:root .blockContent {
|
||||||
|
|||||||
@@ -167,10 +167,6 @@ function getTreeNodeData(nodeName: string, currentDepth: number): TreeNode[] {
|
|||||||
const aiData = computed<AIResult[]>(() => {
|
const aiData = computed<AIResult[]>(() => {
|
||||||
const result: AIResult[] = [];
|
const result: AIResult[] = [];
|
||||||
const connectedSubNodes = props.workflow.getParentNodes(props.node.name, 'ALL_NON_MAIN');
|
const connectedSubNodes = props.workflow.getParentNodes(props.node.name, 'ALL_NON_MAIN');
|
||||||
const rootNodeResult = workflowsStore.getWorkflowResultDataByNodeName(props.node.name);
|
|
||||||
const rootNodeStartTime = rootNodeResult?.[props.runIndex ?? 0]?.startTime ?? 0;
|
|
||||||
const rootNodeEndTime =
|
|
||||||
rootNodeStartTime + (rootNodeResult?.[props.runIndex ?? 0]?.executionTime ?? 0);
|
|
||||||
|
|
||||||
connectedSubNodes.forEach((nodeName) => {
|
connectedSubNodes.forEach((nodeName) => {
|
||||||
const nodeRunData = workflowsStore.getWorkflowResultDataByNodeName(nodeName) ?? [];
|
const nodeRunData = workflowsStore.getWorkflowResultDataByNodeName(nodeName) ?? [];
|
||||||
@@ -193,15 +189,7 @@ const aiData = computed<AIResult[]>(() => {
|
|||||||
return aTime - bTime;
|
return aTime - bTime;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only show data that is within the root node's execution time
|
return result;
|
||||||
// This is because sub-node could be connected to multiple root nodes
|
|
||||||
const currentNodeResult = result.filter((r) => {
|
|
||||||
const startTime = r.data?.metadata?.startTime ?? 0;
|
|
||||||
|
|
||||||
return startTime >= rootNodeStartTime && startTime < rootNodeEndTime;
|
|
||||||
});
|
|
||||||
|
|
||||||
return currentNodeResult;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const executionTree = computed<TreeNode[]>(() => {
|
const executionTree = computed<TreeNode[]>(() => {
|
||||||
@@ -361,10 +349,8 @@ watch(() => props.runIndex, selectFirst, { immediate: true });
|
|||||||
margin-right: var(--spacing-4xs);
|
margin-right: var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
.isSelected {
|
.isSelected {
|
||||||
.nodeIcon {
|
|
||||||
background-color: var(--color-foreground-base);
|
background-color: var(--color-foreground-base);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.treeNode {
|
.treeNode {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
border-radius: var(--border-radius-base);
|
border-radius: var(--border-radius-base);
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
export interface Props {
|
export interface Props {
|
||||||
outline?: boolean;
|
type: 'primary' | 'tertiary';
|
||||||
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<N8nButton
|
<N8nButton
|
||||||
label="Chat"
|
:label="label"
|
||||||
size="large"
|
size="large"
|
||||||
icon="comment"
|
icon="comment"
|
||||||
type="primary"
|
:type="type"
|
||||||
:outline="outline"
|
|
||||||
data-test-id="workflow-chat-button"
|
data-test-id="workflow-chat-button"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`CanvasChatButton > should render correctly 1`] = `"<button class="button button primary large withIcon" aria-live="polite" data-test-id="workflow-chat-button"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-comment fa-w-16 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="comment" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path class="" fill="currentColor" d="M256 32C114.6 32 0 125.1 0 240c0 49.6 21.4 95 57 130.7C44.5 421.1 2.7 466 2.2 466.5c-2.2 2.3-2.8 5.7-1.5 8.7S4.8 480 8 480c66.3 0 116-31.8 140.6-51.4 32.7 12.3 69 19.4 107.4 19.4 141.4 0 256-93.1 256-208S397.4 32 256 32z"></path></svg></span></span><span>Chat</span></button>"`;
|
exports[`CanvasChatButton > should render correctly 1`] = `
|
||||||
|
"<button class="button button primary large withIcon" aria-live="polite" data-test-id="workflow-chat-button"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-comment fa-w-16 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="comment" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path class="" fill="currentColor" d="M256 32C114.6 32 0 125.1 0 240c0 49.6 21.4 95 57 130.7C44.5 421.1 2.7 466 2.2 466.5c-2.2 2.3-2.8 5.7-1.5 8.7S4.8 480 8 480c66.3 0 116-31.8 140.6-51.4 32.7 12.3 69 19.4 107.4 19.4 141.4 0 256-93.1 256-208S397.4 32 256 32z"></path></svg></span></span>
|
||||||
|
<!--v-if-->
|
||||||
|
</button>"
|
||||||
|
`;
|
||||||
|
|||||||
@@ -170,6 +170,7 @@
|
|||||||
"binaryDataDisplay.backToOverviewPage": "Back to overview page",
|
"binaryDataDisplay.backToOverviewPage": "Back to overview page",
|
||||||
"binaryDataDisplay.noDataFoundToDisplay": "No data found to display",
|
"binaryDataDisplay.noDataFoundToDisplay": "No data found to display",
|
||||||
"binaryDataDisplay.yourBrowserDoesNotSupport": "Your browser does not support the video element. Kindly update it to latest version.",
|
"binaryDataDisplay.yourBrowserDoesNotSupport": "Your browser does not support the video element. Kindly update it to latest version.",
|
||||||
|
"chat.hide": "Hide chat",
|
||||||
"chat.window.title": "Chat",
|
"chat.window.title": "Chat",
|
||||||
"chat.window.logs": "Latest Logs",
|
"chat.window.logs": "Latest Logs",
|
||||||
"chat.window.logsFromNode": "from {nodeName} node",
|
"chat.window.logsFromNode": "from {nodeName} node",
|
||||||
|
|||||||
@@ -1616,7 +1616,8 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
<CanvasChatButton
|
<CanvasChatButton
|
||||||
v-if="containsChatTriggerNodes"
|
v-if="containsChatTriggerNodes"
|
||||||
:outline="isChatOpen === false"
|
:type="isChatOpen ? 'tertiary' : 'primary'"
|
||||||
|
:label="isChatOpen ? i18n.baseText('chat.hide') : i18n.baseText('chat.window.title')"
|
||||||
@click="onOpenChat"
|
@click="onOpenChat"
|
||||||
/>
|
/>
|
||||||
<CanvasStopCurrentExecutionButton
|
<CanvasStopCurrentExecutionButton
|
||||||
|
|||||||
@@ -4626,11 +4626,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
<n8n-button
|
<n8n-button
|
||||||
v-if="containsChatNodes"
|
v-if="containsChatNodes"
|
||||||
label="Chat"
|
:label="isChatOpen ? i18n.baseText('chat.hide') : i18n.baseText('chat.window.title')"
|
||||||
size="large"
|
size="large"
|
||||||
icon="comment"
|
icon="comment"
|
||||||
type="primary"
|
:type="isChatOpen ? 'tertiary' : 'primary'"
|
||||||
:outline="isChatOpen === false"
|
|
||||||
data-test-id="workflow-chat-button"
|
data-test-id="workflow-chat-button"
|
||||||
@click.stop="onOpenChat"
|
@click.stop="onOpenChat"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user