diff --git a/packages/frontend/editor-ui/src/components/AskAssistant/Chat/AskAssistantFloatingButton.vue b/packages/frontend/editor-ui/src/components/AskAssistant/Chat/AskAssistantFloatingButton.vue index 8c3c24783d..9cc2e369cf 100644 --- a/packages/frontend/editor-ui/src/components/AskAssistant/Chat/AskAssistantFloatingButton.vue +++ b/packages/frontend/editor-ui/src/components/AskAssistant/Chat/AskAssistantFloatingButton.vue @@ -2,7 +2,7 @@ import { useI18n } from '@/composables/useI18n'; import { useStyles } from '@/composables/useStyles'; import { useAssistantStore } from '@/stores/assistant.store'; -import { useCanvasStore } from '@/stores/canvas.store'; +import { useLogsStore } from '@/stores/logs.store'; import AssistantAvatar from '@n8n/design-system/components/AskAssistantAvatar/AssistantAvatar.vue'; import AskAssistantButton from '@n8n/design-system/components/AskAssistantButton/AskAssistantButton.vue'; import { computed } from 'vue'; @@ -10,7 +10,7 @@ import { computed } from 'vue'; const assistantStore = useAssistantStore(); const i18n = useI18n(); const { APP_Z_INDEXES } = useStyles(); -const canvasStore = useCanvasStore(); +const logsStore = useLogsStore(); const lastUnread = computed(() => { const msg = assistantStore.lastUnread; @@ -41,7 +41,7 @@ const onClick = () => { v-if="assistantStore.canShowAssistantButtonsOnCanvas && !assistantStore.isAssistantOpen" :class="$style.container" data-test-id="ask-assistant-floating-button" - :style="{ '--canvas-panel-height-offset': `${canvasStore.panelHeight}px` }" + :style="{ '--canvas-panel-height-offset': `${logsStore.height}px` }" > { const showMessage = vi.fn(); @@ -139,7 +139,7 @@ describe('CanvasChat', () => { }); let workflowsStore: ReturnType>; - let canvasStore: ReturnType>; + let logsStore: ReturnType>; let nodeTypeStore: ReturnType>; beforeEach(() => { @@ -160,7 +160,7 @@ describe('CanvasChat', () => { setActivePinia(pinia); workflowsStore = mockedStore(useWorkflowsStore); - canvasStore = mockedStore(useCanvasStore); + logsStore = mockedStore(useLogsStore); nodeTypeStore = mockedStore(useNodeTypesStore); // Setup default mocks @@ -175,11 +175,12 @@ describe('CanvasChat', () => { return matchedNode; }); - workflowsStore.logsPanelState = LOGS_PANEL_STATE.ATTACHED; - workflowsStore.isLogsPanelOpen = true; + logsStore.isOpen = true; workflowsStore.getWorkflowExecution = mockWorkflowExecution as unknown as IExecutionResponse; workflowsStore.getPastChatMessages = ['Previous message 1', 'Previous message 2']; + logsStore.state = LOGS_PANEL_STATE.ATTACHED; + nodeTypeStore.getNodeType = vi.fn().mockImplementation((nodeTypeName) => { return mockNodeTypes.find((node) => node.name === nodeTypeName) ?? null; }); @@ -198,7 +199,7 @@ describe('CanvasChat', () => { }); it('should not render chat when panel is closed', async () => { - workflowsStore.logsPanelState = LOGS_PANEL_STATE.CLOSED; + logsStore.state = LOGS_PANEL_STATE.CLOSED; const { queryByTestId } = renderComponent(); await waitFor(() => { expect(queryByTestId('canvas-chat')).not.toBeInTheDocument(); @@ -363,7 +364,7 @@ describe('CanvasChat', () => { { coords: { clientX: 0, clientY: 100 } }, ]); - expect(canvasStore.setPanelHeight).toHaveBeenCalled(); + expect(logsStore.setHeight).toHaveBeenCalled(); }); it('should persist resize dimensions', () => { @@ -388,7 +389,7 @@ describe('CanvasChat', () => { isLoading: computed(() => false), }); - workflowsStore.logsPanelState = LOGS_PANEL_STATE.ATTACHED; + logsStore.state = LOGS_PANEL_STATE.ATTACHED; workflowsStore.allowFileUploads = true; }); @@ -544,15 +545,15 @@ describe('CanvasChat', () => { renderComponent(); // Toggle logs panel - workflowsStore.isLogsPanelOpen = true; + logsStore.isOpen = true; await waitFor(() => { - expect(canvasStore.setPanelHeight).toHaveBeenCalled(); + expect(logsStore.setHeight).toHaveBeenCalled(); }); // Close chat panel - workflowsStore.logsPanelState = LOGS_PANEL_STATE.CLOSED; + logsStore.state = LOGS_PANEL_STATE.CLOSED; await waitFor(() => { - expect(canvasStore.setPanelHeight).toHaveBeenCalledWith(0); + expect(logsStore.setHeight).toHaveBeenCalledWith(0); }); }); @@ -560,15 +561,15 @@ describe('CanvasChat', () => { const { unmount, rerender } = renderComponent(); // Set initial state - workflowsStore.logsPanelState = LOGS_PANEL_STATE.ATTACHED; - workflowsStore.isLogsPanelOpen = true; + logsStore.state = LOGS_PANEL_STATE.ATTACHED; + logsStore.isOpen = true; // Unmount and remount unmount(); await rerender({}); - expect(workflowsStore.logsPanelState).toBe(LOGS_PANEL_STATE.ATTACHED); - expect(workflowsStore.isLogsPanelOpen).toBe(true); + expect(logsStore.state).toBe(LOGS_PANEL_STATE.ATTACHED); + expect(logsStore.isOpen).toBe(true); }); }); diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue b/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue index 93ff52d5f1..802c8ab9a1 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/CanvasChat.vue @@ -9,16 +9,16 @@ import ChatLogsPanel from './components/ChatLogsPanel.vue'; import { useResize } from './composables/useResize'; // Types -import { useCanvasStore } from '@/stores/canvas.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { usePiPWindow } from '@/components/CanvasChat/composables/usePiPWindow'; import { N8nResizeWrapper } from '@n8n/design-system'; import { useTelemetry } from '@/composables/useTelemetry'; import { useChatState } from '@/components/CanvasChat/composables/useChatState'; import { LOGS_PANEL_STATE } from '@/components/CanvasChat/types/logs'; +import { useLogsStore } from '@/stores/logs.store'; const workflowsStore = useWorkflowsStore(); -const canvasStore = useCanvasStore(); +const logsStore = useLogsStore(); // Component state const container = ref(); @@ -28,7 +28,7 @@ const pipContent = useTemplateRef('pipContent'); // Computed properties const workflow = computed(() => workflowsStore.getCurrentWorkflow()); -const chatPanelState = computed(() => workflowsStore.logsPanelState); +const chatPanelState = computed(() => logsStore.state); const resultData = computed(() => workflowsStore.getWorkflowRunData); const telemetry = useTelemetry(); @@ -55,7 +55,7 @@ const { canPopOut, isPoppedOut, pipWindow } = usePiPWindow({ } telemetry.track('User toggled log view', { new_state: 'attached' }); - workflowsStore.setPreferPoppedOutLogsView(false); + logsStore.setPreferPoppedOut(false); }, }); @@ -78,22 +78,22 @@ defineExpose({ }); const closePanel = () => { - workflowsStore.toggleLogsPanelOpen(false); + logsStore.toggleOpen(false); }; function onPopOut() { telemetry.track('User toggled log view', { new_state: 'floating' }); - workflowsStore.toggleLogsPanelOpen(true); - workflowsStore.setPreferPoppedOutLogsView(true); + logsStore.toggleOpen(true); + logsStore.setPreferPoppedOut(true); } // Watchers watchEffect(() => { - canvasStore.setPanelHeight(chatPanelState.value === LOGS_PANEL_STATE.ATTACHED ? height.value : 0); + logsStore.setHeight(chatPanelState.value === LOGS_PANEL_STATE.ATTACHED ? height.value : 0); }); watch( - () => workflowsStore.logsPanelState, + chatPanelState, (state) => { if (state !== LOGS_PANEL_STATE.CLOSED) { setTimeout(() => { diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue index f21936909d..b3c4a0be7f 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/components/ChatMessagesPanel.vue @@ -10,8 +10,9 @@ import ChatInput from '@n8n/chat/components/Input.vue'; import { watch, computed, ref } from 'vue'; import { useClipboard } from '@/composables/useClipboard'; import { useToast } from '@/composables/useToast'; -import PanelHeader from '@/components/CanvasChat/future/components/PanelHeader.vue'; +import LogsPanelHeader from '@/components/CanvasChat/future/components/LogsPanelHeader.vue'; import { N8nButton, N8nIconButton, N8nTooltip } from '@n8n/design-system'; +import { useSettingsStore } from '@/stores/settings.store'; interface Props { pastChatMessages: string[]; @@ -40,6 +41,7 @@ const emit = defineEmits<{ const clipboard = useClipboard(); const locale = useI18n(); const toast = useToast(); +const settingsStore = useSettingsStore(); const previousMessageIndex = ref(0); @@ -140,7 +142,7 @@ async function copySessionId() { watch( () => props.isOpen, (isOpen) => { - if (isOpen) { + if (isOpen && !settingsStore.isNewLogsEnabled) { setTimeout(() => { chatEventBus.emit('focusInput'); }, 0); @@ -151,8 +153,13 @@ watch( - +
{{ locale.baseText('chat.window.title') }}
diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts b/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts index 0499d8d1d9..25fb1cafba 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/composables/useChatState.ts @@ -16,8 +16,8 @@ import { v4 as uuid } from 'uuid'; import type { Ref } from 'vue'; import { computed, provide, ref, watch } from 'vue'; import { useRouter } from 'vue-router'; -import { LOGS_PANEL_STATE } from '../types/logs'; import { restoreChatHistory } from '@/components/CanvasChat/utils'; +import { useLogsStore } from '@/stores/logs.store'; interface ChatState { currentSessionId: Ref; @@ -34,6 +34,7 @@ export function useChatState(isReadOnly: boolean): ChatState { const locale = useI18n(); const workflowsStore = useWorkflowsStore(); const nodeTypesStore = useNodeTypesStore(); + const logsStore = useLogsStore(); const router = useRouter(); const nodeHelpers = useNodeHelpers(); const { runWorkflow } = useRunWorkflow({ router }); @@ -42,7 +43,6 @@ export function useChatState(isReadOnly: boolean): ChatState { const currentSessionId = ref(uuid().replace(/-/g, '')); const previousChatMessages = computed(() => workflowsStore.getPastChatMessages); - const logsPanelState = computed(() => workflowsStore.logsPanelState); const workflow = computed(() => workflowsStore.getCurrentWorkflow()); // Initialize features with injected dependencies @@ -168,7 +168,7 @@ export function useChatState(isReadOnly: boolean): ChatState { messages.value = []; currentSessionId.value = uuid().replace(/-/g, ''); - if (logsPanelState.value !== LOGS_PANEL_STATE.CLOSED) { + if (logsStore.isOpen) { chatEventBus.emit('focusInput'); } } diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts index 565d1717f8..010854052f 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.test.ts @@ -19,7 +19,10 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { LOGS_PANEL_STATE } from '../types/logs'; import { IN_PROGRESS_EXECUTION_ID } from '@/constants'; import { useCanvasOperations } from '@/composables/useCanvasOperations'; +import { useNDVStore } from '@/stores/ndv.store'; +import { deepCopy } from 'n8n-workflow'; import { createTestTaskData } from '@/__tests__/mocks'; +import { useLogsStore } from '@/stores/logs.store'; describe('LogsPanel', () => { const VIEWPORT_HEIGHT = 800; @@ -28,6 +31,8 @@ describe('LogsPanel', () => { let settingsStore: ReturnType>; let workflowsStore: ReturnType>; let nodeTypeStore: ReturnType>; + let logsStore: ReturnType>; + let ndvStore: ReturnType>; function render() { return renderComponent(LogsPanel, { @@ -53,11 +58,15 @@ describe('LogsPanel', () => { workflowsStore = mockedStore(useWorkflowsStore); workflowsStore.setWorkflowExecutionData(null); - workflowsStore.toggleLogsPanelOpen(false); + + logsStore = mockedStore(useLogsStore); + logsStore.toggleOpen(false); nodeTypeStore = mockedStore(useNodeTypesStore); nodeTypeStore.setNodeTypes(nodeTypes); + ndvStore = mockedStore(useNDVStore); + Object.defineProperty(document.body, 'offsetHeight', { configurable: true, get() { @@ -161,11 +170,11 @@ describe('LogsPanel', () => { }); it('should open itself by pulling up the resizer', async () => { - workflowsStore.toggleLogsPanelOpen(false); + logsStore.toggleOpen(false); const rendered = render(); - expect(workflowsStore.logsPanelState).toBe(LOGS_PANEL_STATE.CLOSED); + expect(logsStore.state).toBe(LOGS_PANEL_STATE.CLOSED); expect(rendered.queryByTestId('logs-overview-body')).not.toBeInTheDocument(); await fireEvent.mouseDown(rendered.getByTestId('resize-handle')); @@ -174,17 +183,17 @@ describe('LogsPanel', () => { window.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, clientX: 0, clientY: 0 })); await waitFor(() => { - expect(workflowsStore.logsPanelState).toBe(LOGS_PANEL_STATE.ATTACHED); + expect(logsStore.state).toBe(LOGS_PANEL_STATE.ATTACHED); expect(rendered.queryByTestId('logs-overview-body')).toBeInTheDocument(); }); }); it('should close itself by pulling down the resizer', async () => { - workflowsStore.toggleLogsPanelOpen(true); + logsStore.toggleOpen(true); const rendered = render(); - expect(workflowsStore.logsPanelState).toBe(LOGS_PANEL_STATE.ATTACHED); + expect(logsStore.state).toBe(LOGS_PANEL_STATE.ATTACHED); expect(rendered.queryByTestId('logs-overview-body')).toBeInTheDocument(); await fireEvent.mouseDown(rendered.getByTestId('resize-handle')); @@ -197,13 +206,13 @@ describe('LogsPanel', () => { ); await waitFor(() => { - expect(workflowsStore.logsPanelState).toBe(LOGS_PANEL_STATE.CLOSED); + expect(logsStore.state).toBe(LOGS_PANEL_STATE.CLOSED); expect(rendered.queryByTestId('logs-overview-body')).not.toBeInTheDocument(); }); }); it('should reflect changes to execution data in workflow store if execution is in progress', async () => { - workflowsStore.toggleLogsPanelOpen(true); + logsStore.toggleOpen(true); workflowsStore.setWorkflow(aiChatWorkflow); workflowsStore.setWorkflowExecutionData({ ...aiChatExecutionResponse, @@ -271,8 +280,8 @@ describe('LogsPanel', () => { const router = useRouter(); const operations = useCanvasOperations({ router }); - workflowsStore.toggleLogsPanelOpen(true); - workflowsStore.setWorkflow(aiChatWorkflow); + logsStore.toggleOpen(true); + workflowsStore.setWorkflow(deepCopy(aiChatWorkflow)); workflowsStore.setWorkflowExecutionData({ ...aiChatExecutionResponse, id: '2345', @@ -293,4 +302,90 @@ describe('LogsPanel', () => { expect(workflowsStore.nodesByName['AI Agent']).toBeUndefined(); expect(rendered.queryByText('AI Agent')).toBeInTheDocument(); }); + + it('should open NDV if the button is clicked', async () => { + logsStore.toggleOpen(true); + workflowsStore.setWorkflow(aiChatWorkflow); + workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse); + + const rendered = render(); + const aiAgentRow = (await rendered.findAllByRole('treeitem'))[0]; + + expect(ndvStore.activeNodeName).toBe(null); + expect(ndvStore.output.run).toBe(undefined); + + await fireEvent.click(within(aiAgentRow).getAllByLabelText('Open...')[0]); + + await waitFor(() => { + expect(ndvStore.activeNodeName).toBe('AI Agent'); + expect(ndvStore.output.run).toBe(0); + }); + }); + + it('should toggle subtree when chevron icon button is pressed', async () => { + logsStore.toggleOpen(true); + workflowsStore.setWorkflow(aiChatWorkflow); + workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse); + + const rendered = render(); + const overview = within(rendered.getByTestId('logs-overview')); + + await waitFor(() => expect(overview.queryAllByRole('treeitem')).toHaveLength(2)); + expect(overview.queryByText('AI Agent')).toBeInTheDocument(); + expect(overview.queryByText('AI Model')).toBeInTheDocument(); + + // Close subtree of AI Agent + await fireEvent.click(overview.getAllByLabelText('Toggle row')[0]); + + await waitFor(() => expect(overview.queryAllByRole('treeitem')).toHaveLength(1)); + expect(overview.queryByText('AI Agent')).toBeInTheDocument(); + expect(overview.queryByText('AI Model')).not.toBeInTheDocument(); + + // Re-open subtree of AI Agent + await fireEvent.click(overview.getAllByLabelText('Toggle row')[0]); + + await waitFor(() => expect(overview.queryAllByRole('treeitem')).toHaveLength(2)); + expect(overview.queryByText('AI Agent')).toBeInTheDocument(); + expect(overview.queryByText('AI Model')).toBeInTheDocument(); + }); + + it('should toggle input and output panel when the button is clicked', async () => { + logsStore.toggleOpen(true); + logsStore.toggleInputOpen(false); + logsStore.toggleOutputOpen(true); + workflowsStore.setWorkflow(aiChatWorkflow); + workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse); + + const rendered = render(); + + const header = within(rendered.getByTestId('log-details-header')); + + expect(rendered.queryByTestId('log-details-input')).not.toBeInTheDocument(); + expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument(); + + await fireEvent.click(header.getByText('Input')); + + expect(rendered.queryByTestId('log-details-input')).toBeInTheDocument(); + expect(rendered.queryByTestId('log-details-output')).toBeInTheDocument(); + + await fireEvent.click(header.getByText('Output')); + + expect(rendered.queryByTestId('log-details-input')).toBeInTheDocument(); + expect(rendered.queryByTestId('log-details-output')).not.toBeInTheDocument(); + }); + + it('should allow to select previous and next row via keyboard shortcut', async () => { + logsStore.toggleOpen(true); + workflowsStore.setWorkflow(aiChatWorkflow); + workflowsStore.setWorkflowExecutionData(aiChatExecutionResponse); + + const rendered = render(); + const overview = rendered.getByTestId('logs-overview'); + + expect(await rendered.findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/); + await fireEvent.keyDown(overview, { key: 'K' }); + expect(await rendered.findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Agent/); + await fireEvent.keyDown(overview, { key: 'J' }); + expect(await rendered.findByRole('treeitem', { selected: true })).toHaveTextContent(/AI Model/); + }); }); diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue index 11c7f04507..060f741f56 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/LogsPanel.vue @@ -1,15 +1,19 @@