mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Keep chat session when switching to other tabs (#19483)
This commit is contained in:
@@ -19,33 +19,33 @@ vi.mock('../logs.utils', () => {
|
||||
describe('useChatMessaging', () => {
|
||||
let chatMessaging: ReturnType<typeof useChatMessaging>;
|
||||
let chatTrigger: Ref<INodeUi | null>;
|
||||
let messages: Ref<ChatMessage[]>;
|
||||
let sessionId: Ref<string>;
|
||||
let sessionId: string;
|
||||
let executionResultData: ComputedRef<IRunExecutionData['resultData'] | undefined>;
|
||||
let onRunChatWorkflow: (
|
||||
payload: RunWorkflowChatPayload,
|
||||
) => Promise<IExecutionPushResponse | undefined>;
|
||||
let ws: Ref<WebSocket | null>;
|
||||
let executionData: IRunExecutionData['resultData'] | undefined = undefined;
|
||||
let onNewMessage: (message: ChatMessage) => void;
|
||||
|
||||
beforeEach(() => {
|
||||
executionData = undefined;
|
||||
createTestingPinia();
|
||||
chatTrigger = ref(null);
|
||||
messages = ref([]);
|
||||
sessionId = ref('session-id');
|
||||
sessionId = 'session-id';
|
||||
executionResultData = computed(() => executionData);
|
||||
onRunChatWorkflow = vi.fn().mockResolvedValue({
|
||||
executionId: 'execution-id',
|
||||
} as IExecutionPushResponse);
|
||||
onNewMessage = vi.fn();
|
||||
ws = ref(null);
|
||||
|
||||
chatMessaging = useChatMessaging({
|
||||
chatTrigger,
|
||||
messages,
|
||||
sessionId,
|
||||
executionResultData,
|
||||
onRunChatWorkflow,
|
||||
onNewMessage,
|
||||
ws,
|
||||
});
|
||||
});
|
||||
@@ -60,7 +60,13 @@ describe('useChatMessaging', () => {
|
||||
const messageText = 'Hello, world!';
|
||||
await chatMessaging.sendMessage(messageText);
|
||||
|
||||
expect(messages.value).toHaveLength(1);
|
||||
expect(onNewMessage).toHaveBeenCalledTimes(1);
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
id: expect.any(String),
|
||||
sender: 'user',
|
||||
sessionId: 'session-id',
|
||||
text: messageText,
|
||||
});
|
||||
});
|
||||
|
||||
it('should send message via WebSocket if open', async () => {
|
||||
@@ -74,7 +80,7 @@ describe('useChatMessaging', () => {
|
||||
|
||||
expect(ws.value.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({
|
||||
sessionId: sessionId.value,
|
||||
sessionId,
|
||||
action: 'sendMessage',
|
||||
chatInput: messageText,
|
||||
}),
|
||||
@@ -99,7 +105,14 @@ describe('useChatMessaging', () => {
|
||||
} as unknown as IRunExecutionData['resultData'];
|
||||
|
||||
await chatMessaging.sendMessage(messageText);
|
||||
expect(messages.value).toHaveLength(2);
|
||||
expect(onNewMessage).toHaveBeenCalledTimes(2);
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
id: expect.any(String),
|
||||
sender: 'user',
|
||||
sessionId: 'session-id',
|
||||
text: messageText,
|
||||
});
|
||||
expect(onNewMessage).toHaveBeenCalledWith('Last node response');
|
||||
});
|
||||
|
||||
it('should startWorkflowWithMessage and not add final message if responseMode is responseNode and version is 1.3', async () => {
|
||||
@@ -120,6 +133,12 @@ describe('useChatMessaging', () => {
|
||||
} as unknown as IRunExecutionData['resultData'];
|
||||
|
||||
await chatMessaging.sendMessage(messageText);
|
||||
expect(messages.value).toHaveLength(1);
|
||||
expect(onNewMessage).toHaveBeenCalledTimes(1);
|
||||
expect(onNewMessage).toHaveBeenCalledWith({
|
||||
id: expect.any(String),
|
||||
sender: 'user',
|
||||
sessionId: 'session-id',
|
||||
text: messageText,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -682,16 +682,18 @@ describe('LogsPanel', () => {
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(useChatMessaging, 'useChatMessaging').mockImplementation(({ messages }) => {
|
||||
messages.value.push(...mockMessages);
|
||||
vi.spyOn(useChatMessaging, 'useChatMessaging').mockImplementation(
|
||||
({ onNewMessage: addChatMessage }) => {
|
||||
addChatMessage(mockMessages[0]);
|
||||
|
||||
return {
|
||||
sendMessage: vi.fn(),
|
||||
previousMessageIndex: ref(0),
|
||||
isLoading: computed(() => false),
|
||||
setLoadingState: vi.fn(),
|
||||
};
|
||||
});
|
||||
return {
|
||||
sendMessage: vi.fn(),
|
||||
previousMessageIndex: ref(0),
|
||||
isLoading: computed(() => false),
|
||||
setLoadingState: vi.fn(),
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow copying session ID', async () => {
|
||||
@@ -826,16 +828,19 @@ describe('LogsPanel', () => {
|
||||
sender: 'bot',
|
||||
},
|
||||
];
|
||||
vi.spyOn(useChatMessaging, 'useChatMessaging').mockImplementation(({ messages }) => {
|
||||
messages.value.push(...mockMessages);
|
||||
vi.spyOn(useChatMessaging, 'useChatMessaging').mockImplementation(
|
||||
({ onNewMessage: addChatMessage }) => {
|
||||
addChatMessage(mockMessages[0]);
|
||||
addChatMessage(mockMessages[1]);
|
||||
|
||||
return {
|
||||
sendMessage: sendMessageSpy,
|
||||
previousMessageIndex: ref(0),
|
||||
isLoading: computed(() => false),
|
||||
setLoadingState: vi.fn(),
|
||||
};
|
||||
});
|
||||
return {
|
||||
sendMessage: sendMessageSpy,
|
||||
previousMessageIndex: ref(0),
|
||||
isLoading: computed(() => false),
|
||||
setLoadingState: vi.fn(),
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should repost user message with new execution', async () => {
|
||||
|
||||
@@ -28,22 +28,22 @@ export type RunWorkflowChatPayload = {
|
||||
};
|
||||
export interface ChatMessagingDependencies {
|
||||
chatTrigger: Ref<INodeUi | null>;
|
||||
messages: Ref<ChatMessage[]>;
|
||||
sessionId: Ref<string>;
|
||||
sessionId: string;
|
||||
executionResultData: ComputedRef<IRunExecutionData['resultData'] | undefined>;
|
||||
onRunChatWorkflow: (
|
||||
payload: RunWorkflowChatPayload,
|
||||
) => Promise<IExecutionPushResponse | undefined>;
|
||||
ws: Ref<WebSocket | null>;
|
||||
onNewMessage: (message: ChatMessage) => void;
|
||||
}
|
||||
|
||||
export function useChatMessaging({
|
||||
chatTrigger,
|
||||
messages,
|
||||
sessionId,
|
||||
executionResultData,
|
||||
onRunChatWorkflow,
|
||||
ws,
|
||||
onNewMessage,
|
||||
}: ChatMessagingDependencies) {
|
||||
const locale = useI18n();
|
||||
const { showError } = useToast();
|
||||
@@ -116,7 +116,7 @@ export function useChatMessaging({
|
||||
|
||||
const inputPayload: INodeExecutionData = {
|
||||
json: {
|
||||
sessionId: sessionId.value,
|
||||
sessionId,
|
||||
action: 'sendMessage',
|
||||
[inputKey]: message,
|
||||
},
|
||||
@@ -166,7 +166,7 @@ export function useChatMessaging({
|
||||
: undefined;
|
||||
|
||||
if (chatMessage !== undefined) {
|
||||
messages.value.push(chatMessage);
|
||||
onNewMessage(chatMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,16 +200,16 @@ export function useChatMessaging({
|
||||
const newMessage: ChatMessage & { sessionId: string } = {
|
||||
text: message,
|
||||
sender: 'user',
|
||||
sessionId: sessionId.value,
|
||||
sessionId,
|
||||
id: uuid(),
|
||||
files,
|
||||
};
|
||||
messages.value.push(newMessage);
|
||||
onNewMessage(newMessage);
|
||||
|
||||
if (ws.value?.readyState === WebSocket.OPEN && !isLoading.value) {
|
||||
ws.value.send(
|
||||
JSON.stringify({
|
||||
sessionId: sessionId.value,
|
||||
sessionId,
|
||||
action: 'sendMessage',
|
||||
chatInput: message,
|
||||
files: await processFiles(files),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useChatMessaging } from '@/features/logs/composables/useChatMessaging';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS } from '@/constants';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||
import { ChatOptionsSymbol } from '@n8n/chat/constants';
|
||||
@@ -44,8 +44,8 @@ export function useChatState(isReadOnly: boolean): ChatState {
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
|
||||
const ws = ref<WebSocket | null>(null);
|
||||
const messages = ref<ChatMessage[]>([]);
|
||||
const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
|
||||
const messages = computed(() => logsStore.chatSessionMessages);
|
||||
const currentSessionId = computed(() => logsStore.chatSessionId);
|
||||
|
||||
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
|
||||
const chatTriggerNode = computed(() => workflowsStore.allNodes.find(isChatNode) ?? null);
|
||||
@@ -68,10 +68,10 @@ export function useChatState(isReadOnly: boolean): ChatState {
|
||||
|
||||
const { sendMessage, isLoading, setLoadingState } = useChatMessaging({
|
||||
chatTrigger: chatTriggerNode,
|
||||
messages,
|
||||
sessionId: currentSessionId,
|
||||
sessionId: currentSessionId.value,
|
||||
executionResultData: computed(() => workflowsStore.getWorkflowExecution?.data?.resultData),
|
||||
onRunChatWorkflow,
|
||||
onNewMessage: logsStore.addChatMessage,
|
||||
ws,
|
||||
});
|
||||
|
||||
@@ -194,7 +194,7 @@ export function useChatState(isReadOnly: boolean): ChatState {
|
||||
sessionId: currentSessionId.value,
|
||||
id: uuid(),
|
||||
};
|
||||
messages.value.push(newMessage);
|
||||
logsStore.addChatMessage(newMessage);
|
||||
|
||||
if (logsStore.isOpen) {
|
||||
chatEventBus.emit('focusInput');
|
||||
@@ -216,8 +216,8 @@ export function useChatState(isReadOnly: boolean): ChatState {
|
||||
function refreshSession() {
|
||||
workflowsStore.setWorkflowExecutionData(null);
|
||||
nodeHelpers.updateNodesExecutionIssues();
|
||||
messages.value = [];
|
||||
currentSessionId.value = uuid().replace(/-/g, '');
|
||||
logsStore.resetChatSessionId();
|
||||
logsStore.resetMessages();
|
||||
|
||||
if (logsStore.isOpen) {
|
||||
chatEventBus.emit('focusInput');
|
||||
@@ -232,9 +232,22 @@ export function useChatState(isReadOnly: boolean): ChatState {
|
||||
window.open(route.href, '_blank');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => workflowsStore.workflowId,
|
||||
(_newWorkflowId, prevWorkflowId) => {
|
||||
if (prevWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshSession();
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
currentSessionId,
|
||||
messages: computed(() => (isReadOnly ? restoredChatMessages.value : messages.value)),
|
||||
currentSessionId: computed(() => logsStore.chatSessionId),
|
||||
messages: computed(() =>
|
||||
isReadOnly ? restoredChatMessages.value : logsStore.chatSessionMessages,
|
||||
),
|
||||
previousChatMessages,
|
||||
sendMessage,
|
||||
refreshSession,
|
||||
|
||||
@@ -10,6 +10,8 @@ import { useLocalStorage } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia';
|
||||
import { computed, ref } from 'vue';
|
||||
import { LOG_DETAILS_PANEL_STATE, LOGS_PANEL_STATE } from '@/features/logs/logs.constants';
|
||||
import type { ChatMessage } from '@n8n/chat/types';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export const useLogsStore = defineStore('logs', () => {
|
||||
const isOpen = useLocalStorage(LOCAL_STORAGE_LOGS_PANEL_OPEN, false);
|
||||
@@ -39,10 +41,25 @@ export const useLogsStore = defineStore('logs', () => {
|
||||
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const chatSessionId = ref<string>(getNewSessionId());
|
||||
const chatSessionMessages = ref<ChatMessage[]>([]);
|
||||
|
||||
function setHeight(value: number) {
|
||||
height.value = value;
|
||||
}
|
||||
|
||||
function getNewSessionId(): string {
|
||||
return uuid().replace(/-/g, '');
|
||||
}
|
||||
|
||||
function resetChatSessionId() {
|
||||
chatSessionId.value = getNewSessionId();
|
||||
}
|
||||
|
||||
function resetMessages() {
|
||||
chatSessionMessages.value = [];
|
||||
}
|
||||
|
||||
function toggleOpen(value?: boolean) {
|
||||
isOpen.value = value ?? !isOpen.value;
|
||||
}
|
||||
@@ -101,6 +118,10 @@ export const useLogsStore = defineStore('logs', () => {
|
||||
isLogSelectionSyncedWithCanvas.value = value ?? !isLogSelectionSyncedWithCanvas.value;
|
||||
}
|
||||
|
||||
function addChatMessage(message: ChatMessage) {
|
||||
chatSessionMessages.value.push(message);
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
isOpen: computed(() => state.value !== LOGS_PANEL_STATE.CLOSED),
|
||||
@@ -109,6 +130,9 @@ export const useLogsStore = defineStore('logs', () => {
|
||||
),
|
||||
height: computed(() => height.value),
|
||||
isLogSelectionSyncedWithCanvas: computed(() => isLogSelectionSyncedWithCanvas.value),
|
||||
chatSessionId: computed(() => chatSessionId.value),
|
||||
chatSessionMessages: computed(() => chatSessionMessages.value),
|
||||
addChatMessage,
|
||||
setHeight,
|
||||
toggleOpen,
|
||||
setPreferPoppedOut,
|
||||
@@ -116,5 +140,7 @@ export const useLogsStore = defineStore('logs', () => {
|
||||
toggleInputOpen,
|
||||
toggleOutputOpen,
|
||||
toggleLogSelectionSync,
|
||||
resetChatSessionId,
|
||||
resetMessages,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -207,6 +207,11 @@ export class CanvasPage extends BasePage {
|
||||
async clickExecutionsTab(): Promise<void> {
|
||||
await this.page.getByRole('radio', { name: 'Executions' }).click();
|
||||
}
|
||||
|
||||
async clickEditorTab(): Promise<void> {
|
||||
await this.page.getByRole('radio', { name: 'Editor' }).click();
|
||||
}
|
||||
|
||||
async setWorkflowName(name: string): Promise<void> {
|
||||
await this.clickByTestId('inline-edit-preview');
|
||||
await this.fillByTestId('inline-edit-input', name);
|
||||
|
||||
@@ -529,4 +529,24 @@ test.describe('Langchain Integration @capability:proxy', () => {
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field_4');
|
||||
});
|
||||
});
|
||||
|
||||
test('should keep the same session when switching tabs', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test_workflow_chat_partial_execution.json');
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
|
||||
await n8n.canvas.logsPanel.open();
|
||||
|
||||
// Send a message
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage('Test');
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field');
|
||||
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
|
||||
await n8n.canvas.clickEditorTab();
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field');
|
||||
|
||||
// Refresh session
|
||||
await n8n.page.getByTestId('refresh-session-button').click();
|
||||
await expect(n8n.canvas.getManualChatMessages()).not.toBeAttached();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user