diff --git a/packages/frontend/@n8n/design-system/src/components/CanvasThinkingPill/CanvasThinkingPill.test.ts b/packages/frontend/@n8n/design-system/src/components/CanvasThinkingPill/CanvasThinkingPill.test.ts new file mode 100644 index 0000000000..130dc4eef6 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/CanvasThinkingPill/CanvasThinkingPill.test.ts @@ -0,0 +1,10 @@ +import { render } from '@testing-library/vue'; + +import CanvasThinkingPill from './CanvasThinkingPill.vue'; + +describe('CanvasThinkingPill', () => { + it('renders canvas thinking pill correctly', () => { + const { container } = render(CanvasThinkingPill, {}); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/packages/frontend/@n8n/design-system/src/components/CanvasThinkingPill/CanvasThinkingPill.vue b/packages/frontend/@n8n/design-system/src/components/CanvasThinkingPill/CanvasThinkingPill.vue new file mode 100644 index 0000000000..2ca854014f --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/CanvasThinkingPill/CanvasThinkingPill.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/packages/frontend/@n8n/design-system/src/components/CanvasThinkingPill/__snapshots__/CanvasThinkingPill.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/CanvasThinkingPill/__snapshots__/CanvasThinkingPill.test.ts.snap new file mode 100644 index 0000000000..b7b3bae710 --- /dev/null +++ b/packages/frontend/@n8n/design-system/src/components/CanvasThinkingPill/__snapshots__/CanvasThinkingPill.test.ts.snap @@ -0,0 +1,53 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`CanvasThinkingPill > renders canvas thinking pill correctly 1`] = ` +
+
+
+ + + + + + + + + + +
+ + Working... + +
+
+`; diff --git a/packages/frontend/@n8n/design-system/src/locale/lang/en.ts b/packages/frontend/@n8n/design-system/src/locale/lang/en.ts index 571bcc0c2c..8c999be0ee 100644 --- a/packages/frontend/@n8n/design-system/src/locale/lang/en.ts +++ b/packages/frontend/@n8n/design-system/src/locale/lang/en.ts @@ -65,6 +65,7 @@ export default { 'assistantChat.inputPlaceholder': 'Enter your response...', 'assistantChat.copy': 'Copy', 'assistantChat.copied': 'Copied', + 'aiAssistant.builder.canvas.thinking': 'Working...', 'inlineAskAssistantButton.asked': 'Asked', 'iconPicker.button.defaultToolTip': 'Choose icon', 'iconPicker.tabs.icons': 'Icons', diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.test.ts b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.test.ts index 7b5fd1f1e5..88198a343f 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.test.ts +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.test.ts @@ -214,6 +214,16 @@ describe('CanvasNodeAIPrompt', () => { }); }); + it('should disable suggestion pills when builder is streaming', () => { + streaming.value = true; + const { container } = renderComponent(); + const pills = container.querySelectorAll('[role="group"] button'); + + pills.forEach((pill) => { + expect(pill).toHaveAttribute('disabled'); + }); + }); + it('should replace prompt when suggestion is clicked', async () => { const { container } = renderComponent(); const firstPill = container.querySelector('[role="group"] button'); @@ -311,6 +321,26 @@ describe('CanvasNodeAIPrompt', () => { NODE_CREATOR_OPEN_SOURCES.TRIGGER_PLACEHOLDER_BUTTON, ); }); + + it('should disable "Add node manually" button when builder is streaming', () => { + streaming.value = true; + const { container } = renderComponent(); + const addButton = container.querySelector('[aria-label="Add node manually"]'); + + expect(addButton).toHaveAttribute('disabled'); + }); + + it('should not open node creator when streaming', async () => { + streaming.value = true; + const { container } = renderComponent(); + const addButton = container.querySelector('[aria-label="Add node manually"]'); + + if (!addButton) throw new Error('Add button not found'); + + await fireEvent.click(addButton); + + expect(openNodeCreatorForTriggerNodes).not.toHaveBeenCalled(); + }); }); describe('event propagation', () => { diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.vue b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.vue index 6014ec3d87..424eaff1d5 100644 --- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.vue +++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeAIPrompt.vue @@ -91,6 +91,8 @@ async function onSuggestionClick(suggestion: WorkflowSuggestion) { * Opens the node creator for adding trigger nodes manually */ function onAddNodeClick() { + if (builderStore.streaming) return; + nodeCreatorStore.openNodeCreatorForTriggerNodes( NODE_CREATOR_OPEN_SOURCES.TRIGGER_PLACEHOLDER_BUTTON, ); @@ -145,6 +147,7 @@ function onAddNodeClick() { v-for="suggestion in suggestions" :key="suggestion.id" :class="$style.suggestionPill" + :disabled="builderStore.streaming" type="button" @click="onSuggestionClick(suggestion)" > @@ -159,7 +162,12 @@ function onAddNodeClick() {
-
@@ -284,7 +292,7 @@ function onAddNodeClick() { color: var(--color-text-dark); font-weight: var(--font-weight-regular); - &:hover { + &:hover:not(:disabled) { color: var(--color-primary); border-color: var(--color-primary); background: var(--color-background-xlight); diff --git a/packages/frontend/editor-ui/src/views/NodeView.vue b/packages/frontend/editor-ui/src/views/NodeView.vue index d26eb0ef52..9a2a43ce41 100644 --- a/packages/frontend/editor-ui/src/views/NodeView.vue +++ b/packages/frontend/editor-ui/src/views/NodeView.vue @@ -111,6 +111,7 @@ import { useNDVStore } from '@/stores/ndv.store'; import { getBounds, getNodesWithNormalizedPosition, getNodeViewTab } from '@/utils/nodeViewUtils'; import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/buttons/CanvasStopCurrentExecutionButton.vue'; import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue'; +import CanvasThinkingPill from '@n8n/design-system/components/CanvasThinkingPill/CanvasThinkingPill.vue'; import { nodeViewEventBus } from '@/event-bus'; import { tryToParseNumber } from '@/utils/typesUtils'; import { useTemplatesStore } from '@/stores/templates.store'; @@ -291,7 +292,8 @@ const isCanvasReadOnly = computed(() => { isDemoRoute.value || isReadOnlyEnvironment.value || !(workflowPermissions.value.update ?? projectPermissions.value.workflow.update) || - editableWorkflow.value.isArchived + editableWorkflow.value.isArchived || + builderStore.streaming ); }); @@ -2130,6 +2132,8 @@ onBeforeUnmount(() => { {{ i18n.baseText('readOnlyEnv.cantEditOrRun') }} + + { left: 50%; transform: translateX(-50%); } + +.thinkingPill { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + z-index: 10; +}