From 8db12ccf0abc5eb8e30e489a56c913945ac45b75 Mon Sep 17 00:00:00 2001 From: Tuukka Kantola Date: Tue, 25 Aug 2026 10:54:42 +0000 Subject: [PATCH] refactor(editor): Refine AI Assistant interaction panels (#36799) --- .../ai/instanceAi/InstanceAiThreadView.vue | 10 ++-- .../InstanceAiConfirmationPanel.test.ts | 45 +++++++++++++++-- .../__tests__/InstanceAiThreadView.test.ts | 6 +-- .../components/GatewayResourceDecision.vue | 2 +- .../InstanceAiConfirmationPanel.vue | 49 +++++++++---------- .../components/InstanceAiCredentialSetup.vue | 2 +- .../components/InstanceAiMcpConnectCard.vue | 3 +- .../components/InstanceAiQuestions.vue | 33 ++++++++++--- .../instanceAi/components/PlanReviewPanel.vue | 7 +-- .../ai/instanceAi/confirmationKinds.ts | 8 +-- .../components/WorkflowSetupCard.vue | 2 +- .../components/WorkflowSetupGroupCard.vue | 2 +- .../ai/shared/components/AiThinkingBlock.vue | 4 +- .../ai/shared/components/ChannelSetupCard.vue | 4 +- 14 files changed, 115 insertions(+), 62 deletions(-) diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue index 1b794174b9d..d622038f86c 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/InstanceAiThreadView.vue @@ -169,8 +169,8 @@ watch( const hasAssistantResponse = computed(() => displayedMessages.some((m) => m.role === 'assistant')); // True when at least one pending confirmation should occupy the chat-input -// slot (generic approvals + domain/web-search access). Drives the swap -// between the input and the floating confirmation panel. +// slot (questions, generic approvals, or domain/web-search access). Drives +// the swap between the input and the floating confirmation panel. const hasFloatingConfirmation = computed(() => thread.pendingConfirmations.some(isPendingItemFloating), ); @@ -1165,8 +1165,8 @@ async function dismissComposerContextChip() { :agent-node="builder" /> - @@ -1197,7 +1197,7 @@ async function dismissComposerContextChip() { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiConfirmationPanel.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiConfirmationPanel.test.ts index afd5a0b482b..9b7e534c289 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiConfirmationPanel.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiConfirmationPanel.test.ts @@ -92,7 +92,7 @@ vi.mock('../components/InstanceAiCredentialSetup.vue', () => ({ })); vi.mock('../workflowSetup/InstanceAiWorkflowSetup.vue', () => ({ default: { - template: '
', + template: '
', props: ['requestId', 'setupRequests', 'workflowId', 'message', 'projectId', 'credentialFlow'], }, })); @@ -710,11 +710,46 @@ describe('InstanceAiConfirmationPanel telemetry', () => { ], }; + it('renders questions only in the floating mount', () => { + injectPendingConfirmation(thread, questionsConfirmation); + + const floating = renderComponent({ props: { kind: 'floating' } }); + expect(floating.getByTestId('mock-questions')).toBeVisible(); + floating.unmount(); + + const inline = renderComponent({ props: { kind: 'inline' } }); + expect(inline.queryByTestId('mock-questions')).toBeNull(); + }); + + it('keeps setup metadata on the inline setup renderer', () => { + injectPendingConfirmation(thread, { + ...questionsConfirmation, + setupRequests: [ + { + node: { + name: 'Slack', + type: 'n8n-nodes-base.slack', + typeVersion: 2, + parameters: {}, + position: [0, 0], + id: 'node-1', + }, + isTrigger: false, + }, + ], + }); + + const inline = renderComponent({ props: { kind: 'inline' } }); + + expect(inline.getByTestId('mock-workflow-setup')).toBeVisible(); + expect(inline.queryByTestId('mock-questions')).toBeNull(); + }); + it('includes all available options and correct option_chosen for single-select', () => { injectPendingConfirmation(thread, questionsConfirmation); vi.spyOn(thread, 'confirmAction').mockResolvedValue(true); - renderComponent({ props: { kind: 'inline' } }); + renderComponent({ props: { kind: 'floating' } }); const answers: QuestionAnswer[] = [ { @@ -776,7 +811,7 @@ describe('InstanceAiConfirmationPanel telemetry', () => { injectPendingConfirmation(thread, questionsConfirmation); vi.spyOn(thread, 'confirmAction').mockResolvedValue(true); - renderComponent({ props: { kind: 'inline' } }); + renderComponent({ props: { kind: 'floating' } }); const answers: QuestionAnswer[] = [ { @@ -819,7 +854,7 @@ describe('InstanceAiConfirmationPanel telemetry', () => { injectPendingConfirmation(thread, questionsConfirmation); vi.spyOn(thread, 'confirmAction').mockResolvedValue(true); - renderComponent({ props: { kind: 'inline' } }); + renderComponent({ props: { kind: 'floating' } }); const answers: QuestionAnswer[] = [ { @@ -860,7 +895,7 @@ describe('InstanceAiConfirmationPanel telemetry', () => { injectPendingConfirmation(thread, questionsConfirmation); vi.spyOn(thread, 'confirmAction').mockResolvedValue(true); - renderComponent({ props: { kind: 'inline' } }); + renderComponent({ props: { kind: 'floating' } }); const answers: QuestionAnswer[] = [ { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts index 07affafc1b9..d59fe5755d0 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/__tests__/InstanceAiThreadView.test.ts @@ -1472,7 +1472,7 @@ describe('InstanceAiThreadView', () => { expect(queryByTestId('instance-ai-input-stub')).toBeNull(); }); - it('keeps the chat input visible when only inline confirmations are pending', () => { + it('swaps the chat input for the floating panel when questions are pending', () => { thread.pendingConfirmations = [ { messageId: 'msg-questions', @@ -1496,8 +1496,8 @@ describe('InstanceAiThreadView', () => { const { getByTestId, queryByTestId } = renderView({ props: { threadId: 'thread-1' } }); - expect(getByTestId('instance-ai-input-stub')).toBeTruthy(); - expect(queryByTestId('instance-ai-confirmation-panel-floating')).toBeNull(); + expect(getByTestId('instance-ai-confirmation-panel-floating')).toBeTruthy(); + expect(queryByTestId('instance-ai-input-stub')).toBeNull(); }); it('connects the route thread when navigating to a known thread', async () => { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/GatewayResourceDecision.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/GatewayResourceDecision.vue index 51b3bcbcdb6..5c5d204fe63 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/GatewayResourceDecision.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/GatewayResourceDecision.vue @@ -133,9 +133,9 @@ async function confirm(decision: InstanceGatewayResourceDecision) { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiCredentialSetup.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiCredentialSetup.vue index 917575e6e07..aab678dd28b 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiCredentialSetup.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiCredentialSetup.vue @@ -736,9 +736,9 @@ async function handleSetupAutomatically() { flex-direction: column; gap: var(--spacing--sm); padding: 0; - border: 2px solid var(--color--primary); border-radius: var(--radius--lg); background-color: var(--color--background--light-3); + box-shadow: var(--shadow--sm), var(--shadow--outline); } .header { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiMcpConnectCard.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiMcpConnectCard.vue index 4a34c152aeb..85ce3e1b10f 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiMcpConnectCard.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiMcpConnectCard.vue @@ -263,7 +263,8 @@ function openSettings(row: CardRow) { } .awaitingInput { - border: 2px solid var(--color--primary); + border: 0; + box-shadow: var(--shadow--sm), var(--shadow--outline); } .header { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiQuestions.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiQuestions.vue index 06d75e00bb6..4c1bc8f9b68 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiQuestions.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/components/InstanceAiQuestions.vue @@ -414,7 +414,14 @@ function onOptionMouseEnter(idx: number) { > {{ idx + 1 }} {{ option }} - + + +
{ flex-direction: column; gap: var(--spacing--sm); padding-top: var(--spacing--sm); - border: 2px solid var(--color--primary); border-radius: var(--radius--lg); background-color: var(--color--background--light-3); + box-shadow: var(--shadow--sm), var(--shadow--outline); } .header { diff --git a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupGroupCard.vue b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupGroupCard.vue index 10bc491f90b..933cddc0401 100644 --- a/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupGroupCard.vue +++ b/packages/frontend/editor-ui/src/features/ai/instanceAi/workflowSetup/components/WorkflowSetupGroupCard.vue @@ -118,9 +118,9 @@ function getSectionNodeType(section: WorkflowSetupSection) { .card { display: flex; flex-direction: column; - border: 2px solid var(--color--primary); border-radius: var(--radius--lg); background-color: var(--color--background--light-3); + box-shadow: var(--shadow--sm), var(--shadow--outline); } .header { diff --git a/packages/frontend/editor-ui/src/features/ai/shared/components/AiThinkingBlock.vue b/packages/frontend/editor-ui/src/features/ai/shared/components/AiThinkingBlock.vue index d1c84b87f6f..d1ec987761d 100644 --- a/packages/frontend/editor-ui/src/features/ai/shared/components/AiThinkingBlock.vue +++ b/packages/frontend/editor-ui/src/features/ai/shared/components/AiThinkingBlock.vue @@ -168,10 +168,10 @@ const title = computed<{ key: string; text: string }>(() => { @use '@n8n/design-system/css/mixins/motion'; .header { - display: flex; + display: inline-flex; align-items: center; gap: var(--spacing--2xs); - width: 100%; + max-width: 90%; border: 0; background: transparent; padding: var(--spacing--4xs) 0; diff --git a/packages/frontend/editor-ui/src/features/ai/shared/components/ChannelSetupCard.vue b/packages/frontend/editor-ui/src/features/ai/shared/components/ChannelSetupCard.vue index 4296d46ec58..42e36af9286 100644 --- a/packages/frontend/editor-ui/src/features/ai/shared/components/ChannelSetupCard.vue +++ b/packages/frontend/editor-ui/src/features/ai/shared/components/ChannelSetupCard.vue @@ -362,11 +362,9 @@ watch( flex-direction: column; gap: var(--spacing--sm); padding-top: var(--spacing--sm); - /* Waiting-for-input highlight (#33959) — ported from InstanceAiChannelSetup - when the card body moved here, so both surfaces get it. */ - border: 2px solid var(--color--primary); border-radius: var(--radius--lg); background-color: var(--background--surface); + box-shadow: var(--shadow--sm), var(--shadow--outline); } .header {