From ac3efc5685fafaf150fc20980198853565a7a61e Mon Sep 17 00:00:00 2001 From: Michael Drury Date: Thu, 9 Oct 2025 10:53:22 +0100 Subject: [PATCH] fix: Chat side panel open/close behaviour and builder state accuracy (no-changelog) (#20525) --- .../frontend/@n8n/stores/src/constants.ts | 2 + packages/frontend/editor-ui/src/App.vue | 13 +- .../CredentialEdit/CredentialConfig.vue | 6 +- .../src/components/Error/NodeErrorView.vue | 8 +- .../src/components/Node/NodeCreation.vue | 10 +- .../Node/NodeCreator/NodeCreator.vue | 13 +- .../render-types/CanvasNodeChoicePrompt.vue | 18 +- .../handlers/builderCreditsUpdated.test.ts | 4 +- .../handlers/builderCreditsUpdated.ts | 2 +- .../assistant/assistant.store.test.ts | 46 ++- .../src/features/assistant/assistant.store.ts | 82 +--- .../assistant}/builder.store.test.ts | 61 +-- .../assistant}/builder.store.ts | 121 ++---- .../assistant/chatPanel.store.test.ts | 362 ++++++++++++++++++ .../src/features/assistant/chatPanel.store.ts | 199 ++++++++++ .../assistant/chatPanelState.store.ts | 24 ++ .../Agent/AskAssistantBuild.test.ts | 2 +- .../components/Agent/AskAssistantBuild.vue | 2 +- .../components/Agent/ExecuteMessage.test.ts | 2 +- .../components/Agent/ExecuteMessage.vue | 2 +- .../assistant/components/AssistantsHub.vue | 67 ++-- .../Chat/AskAssistantFloatingButton.vue | 16 +- .../Chat/NewAssistantSessionModal.vue | 8 +- .../src/features/assistant/constants.ts | 20 + .../frontend/editor-ui/src/views/NodeView.vue | 2 +- 25 files changed, 803 insertions(+), 289 deletions(-) rename packages/frontend/editor-ui/src/{stores => features/assistant}/builder.store.test.ts (97%) rename packages/frontend/editor-ui/src/{stores => features/assistant}/builder.store.ts (86%) create mode 100644 packages/frontend/editor-ui/src/features/assistant/chatPanel.store.test.ts create mode 100644 packages/frontend/editor-ui/src/features/assistant/chatPanel.store.ts create mode 100644 packages/frontend/editor-ui/src/features/assistant/chatPanelState.store.ts create mode 100644 packages/frontend/editor-ui/src/features/assistant/constants.ts diff --git a/packages/frontend/@n8n/stores/src/constants.ts b/packages/frontend/@n8n/stores/src/constants.ts index 7cea01c1aa1..a080721495f 100644 --- a/packages/frontend/@n8n/stores/src/constants.ts +++ b/packages/frontend/@n8n/stores/src/constants.ts @@ -24,6 +24,8 @@ export const STORES = { COLLABORATION: 'collaboration', ASSISTANT: 'assistant', BUILDER: 'builder', + CHAT_PANEL: 'chatPanel', + CHAT_PANEL_STATE: 'chatPanelState', BECOME_TEMPLATE_CREATOR: 'becomeTemplateCreator', PROJECTS: 'projects', API_KEYS: 'apiKeys', diff --git a/packages/frontend/editor-ui/src/App.vue b/packages/frontend/editor-ui/src/App.vue index 81a825e6e83..d71ac305e87 100644 --- a/packages/frontend/editor-ui/src/App.vue +++ b/packages/frontend/editor-ui/src/App.vue @@ -15,8 +15,8 @@ import { HIRING_BANNER, VIEWS, } from '@/constants'; +import { useChatPanelStore } from '@/features/assistant/chatPanel.store'; import { useAssistantStore } from '@/features/assistant/assistant.store'; -import { useBuilderStore } from '@/stores/builder.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useSettingsStore } from '@/stores/settings.store'; import { useUIStore } from '@/stores/ui.store'; @@ -38,7 +38,7 @@ import { hasPermission } from './utils/rbac/permissions'; const route = useRoute(); const rootStore = useRootStore(); const assistantStore = useAssistantStore(); -const builderStore = useBuilderStore(); +const chatPanelStore = useChatPanelStore(); const uiStore = useUIStore(); const usersStore = useUsersStore(); const settingsStore = useSettingsStore(); @@ -71,8 +71,7 @@ const isDemoMode = computed(() => route.name === VIEWS.DEMO); const hasContentFooter = ref(false); const appGrid = ref(null); -const assistantSidebarWidth = computed(() => assistantStore.chatWidth); -const builderSidebarWidth = computed(() => builderStore.chatWidth); +const chatPanelWidth = computed(() => chatPanelStore.width); useTelemetryContext({ ndv_source: computed(() => ndvStore.lastSetActiveNodeSource) }); @@ -107,8 +106,8 @@ const updateGridWidth = async () => { uiStore.appGridDimensions = { width, height }; } }; -// As assistant sidebar width changes, recalculate the total width regularly -watch([assistantSidebarWidth, builderSidebarWidth], async () => { +// As chat panel width changes, recalculate the total width regularly +watch(chatPanelWidth, async () => { await updateGridWidth(); }); @@ -211,6 +210,7 @@ useExposeCssVar('--ask-assistant-floating-button-bottom-offset', askAiFloatingBu grid-area: banners; z-index: var(--z-index-top-banners); } + .content { display: flex; flex-direction: column; @@ -230,6 +230,7 @@ useExposeCssVar('--ask-assistant-floating-button-bottom-offset', askAiFloatingBu display: block; } } + .contentWrapper { display: flex; grid-area: content; diff --git a/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialConfig.vue b/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialConfig.vue index 609e25610c7..902a5e32983 100644 --- a/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialConfig.vue +++ b/packages/frontend/editor-ui/src/components/CredentialEdit/CredentialConfig.vue @@ -30,6 +30,7 @@ import CopyInput from '../CopyInput.vue'; import CredentialInputs from './CredentialInputs.vue'; import GoogleAuthButton from './GoogleAuthButton.vue'; import OauthButton from './OauthButton.vue'; +import { useChatPanelStore } from '@/features/assistant/chatPanel.store'; import { useAssistantStore } from '@/features/assistant/assistant.store'; import FreeAiCreditsCallout from '@/components/FreeAiCreditsCallout.vue'; @@ -41,6 +42,7 @@ import { N8nNotice, N8nText, } from '@n8n/design-system'; + type Props = { mode: string; credentialType: ICredentialType; @@ -82,6 +84,7 @@ const rootStore = useRootStore(); const uiStore = useUIStore(); const workflowsStore = useWorkflowsStore(); const assistantStore = useAssistantStore(); +const chatPanelStore = useChatPanelStore(); const i18n = useI18n(); const telemetry = useTelemetry(); @@ -223,7 +226,7 @@ async function onAskAssistantClick() { }); return; } - await assistantStore.initCredHelp(props.credentialType); + await chatPanelStore.openWithCredHelp(props.credentialType); } watch(showOAuthSuccessBanner, (newValue, oldValue) => { @@ -414,6 +417,7 @@ watch(showOAuthSuccessBanner, (newValue, oldValue) => { .askAssistantButton { display: flex; align-items: center; + > span { margin-left: var(--spacing-3xs); font-size: var(--font-size-s); diff --git a/packages/frontend/editor-ui/src/components/Error/NodeErrorView.vue b/packages/frontend/editor-ui/src/components/Error/NodeErrorView.vue index f41433c4c7d..8ce4e15e241 100644 --- a/packages/frontend/editor-ui/src/components/Error/NodeErrorView.vue +++ b/packages/frontend/editor-ui/src/components/Error/NodeErrorView.vue @@ -20,6 +20,7 @@ import type { import { sanitizeHtml } from '@/utils/htmlUtils'; import { MAX_DISPLAY_DATA_SIZE, NEW_ASSISTANT_SESSION_MODAL, VIEWS } from '@/constants'; import type { BaseTextKey } from '@n8n/i18n'; +import { useChatPanelStore } from '@/features/assistant/chatPanel.store'; import { useAssistantStore } from '@/features/assistant/assistant.store'; import type { ChatRequest } from '@/features/assistant/assistant.types'; import { useUIStore } from '@/stores/ui.store'; @@ -32,6 +33,7 @@ import { N8nIconButton, N8nTooltip, } from '@n8n/design-system'; + type Props = { // TODO: .node can be undefined error: NodeError | NodeApiError | NodeOperationError; @@ -52,6 +54,7 @@ const ndvStore = useNDVStore(); const workflowsStore = useWorkflowsStore(); const rootStore = useRootStore(); const assistantStore = useAssistantStore(); +const chatPanelStore = useChatPanelStore(); const uiStore = useUIStore(); const workflowId = computed(() => workflowsStore.workflowId); @@ -444,7 +447,7 @@ async function onAskAssistantClick() { }); return; } - await assistantStore.initErrorHelper(errorHelp); + await chatPanelStore.openWithErrorHelper(errorHelp); assistantStore.trackUserOpenedAssistant({ source: 'error', task: 'error', @@ -754,6 +757,7 @@ async function onAskAssistantClick() { margin-bottom: var(--spacing-xs); margin-top: var(--spacing-xs); flex-direction: row-reverse; + span { margin-right: var(--spacing-5xs); margin-left: var(--spacing-5xs); @@ -776,6 +780,7 @@ async function onAskAssistantClick() { width: 100%; overflow: auto; background: var(--color-background-light); + code { font-size: var(--font-size-s); } @@ -797,6 +802,7 @@ async function onAskAssistantClick() { align-items: center; justify-content: center; cursor: pointer; + &:hover { color: var(--color-primary); } diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreation.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreation.vue index 41ac2032148..55390305184 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreation.vue +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreation.vue @@ -20,7 +20,8 @@ import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue'; import { useI18n } from '@n8n/i18n'; import { useTelemetry } from '@/composables/useTelemetry'; import { useAssistantStore } from '@/features/assistant/assistant.store'; -import { useBuilderStore } from '@/stores/builder.store'; +import { useBuilderStore } from '@/features/assistant/builder.store'; +import { useChatPanelStore } from '@/features/assistant/chatPanel.store'; import { N8nAssistantIcon, N8nButton, N8nIconButton, N8nTooltip } from '@n8n/design-system'; @@ -51,6 +52,7 @@ const i18n = useI18n(); const telemetry = useTelemetry(); const assistantStore = useAssistantStore(); const builderStore = useBuilderStore(); +const chatPanelStore = useChatPanelStore(); const { getAddedNodesAndConnections } = useActions(); @@ -101,11 +103,11 @@ function toggleFocusPanel() { async function onAskAssistantButtonClick() { if (builderStore.isAIBuilderEnabled) { - await builderStore.toggleChat(); + await chatPanelStore.toggle({ mode: 'builder' }); } else { - assistantStore.toggleChat(); + await chatPanelStore.toggle({ mode: 'assistant' }); } - if (builderStore.isAssistantOpen || assistantStore.isAssistantOpen) { + if (chatPanelStore.isOpen) { assistantStore.trackUserOpenedAssistant({ source: 'canvas', task: 'placeholder', diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue b/packages/frontend/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue index 8d8d5573633..97a05481c96 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/NodeCreator.vue @@ -12,8 +12,7 @@ import NodesListPanel from './Panel/NodesListPanel.vue'; import { useCredentialsStore } from '@/stores/credentials.store'; import { useUIStore } from '@/stores/ui.store'; import { DRAG_EVENT_DATA_KEY } from '@/constants'; -import { useAssistantStore } from '@/features/assistant/assistant.store'; -import { useBuilderStore } from '@/stores/builder.store'; +import { useChatPanelStore } from '@/features/assistant/chatPanel.store'; import type { NodeTypeSelectedPayload } from '@/Interface'; import { onClickOutside } from '@vueuse/core'; @@ -37,8 +36,7 @@ const emit = defineEmits<{ nodeTypeSelected: [value: NodeTypeSelectedPayload[]]; }>(); const uiStore = useUIStore(); -const assistantStore = useAssistantStore(); -const builderStore = useBuilderStore(); +const chatPanelStore = useChatPanelStore(); const { setShowScrim, setActions, setMergeNodes } = useNodeCreatorStore(); const { generateMergedNodesAndActions } = useActionsGenerator(); @@ -58,11 +56,8 @@ const nodeCreatorInlineStyle = computed(() => { }); function getRightOffset() { - if (assistantStore.isAssistantOpen) { - return assistantStore.chatWidth; - } - if (builderStore.isAssistantOpen) { - return builderStore.chatWidth; + if (chatPanelStore.isOpen) { + return chatPanelStore.width; } return 0; diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeChoicePrompt.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeChoicePrompt.vue index 200748cc938..b3e5308cf89 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeChoicePrompt.vue +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeChoicePrompt.vue @@ -1,15 +1,20 @@ @@ -55,12 +60,7 @@ async function onBuildWithAIClick() {
-
+