diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap
index 9309342614013..887807db97f00 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantButton/__snapshots__/AskAssistantButton.test.ts.snap
@@ -18,7 +18,7 @@ exports[`AskAssistantButton > renders button with unread messages correctly 1`]
- Ask Assistant
+ n8n AI
@@ -73,7 +73,7 @@ exports[`AskAssistantButton > renders default button correctly 1`] = `
- Ask Assistant
+ n8n AI
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts
index 26911fe49330b..71f5ac14c8068 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.stories.ts
@@ -1,7 +1,7 @@
import type { StoryFn } from '@storybook/vue3-vite';
import AskAssistantChat from './AskAssistantChat.vue';
-import type { ChatUI } from '../../types/assistant';
+import type { ChatUI, WorkflowSuggestion } from '../../types/assistant';
export default {
title: 'Assistant/AskAssistantChat',
@@ -21,7 +21,7 @@ const Template: StoryFn = (args, { argTypes }) => ({
components: {
AskAssistantChat,
},
- template: '
',
+ template: '',
methods,
});
@@ -33,6 +33,38 @@ DefaultPlaceholderChat.args = {
},
};
+const mockSuggestions: WorkflowSuggestion[] = [
+ {
+ id: 'invoice-pipeline',
+ summary: 'Invoice processing pipeline',
+ prompt:
+ 'Create an invoice parsing workflow using n8n forms. Extract key information and store in Airtable.',
+ },
+ {
+ id: 'ai-news-digest',
+ summary: 'Daily AI news digest',
+ prompt:
+ 'Create a workflow that fetches the latest AI news every morning at 8 AM and sends a summary via Telegram.',
+ },
+ {
+ id: 'rag-assistant',
+ summary: 'RAG knowledge assistant',
+ prompt:
+ 'Build a pipeline that accepts PDF files, chunks documents, and creates a chatbot that can answer questions.',
+ },
+];
+
+export const WithSuggestions = Template.bind({});
+WithSuggestions.args = {
+ user: {
+ firstName: 'Max',
+ lastName: 'Test',
+ },
+ suggestions: mockSuggestions,
+ creditsQuota: 100,
+ creditsRemaining: 75,
+};
+
export const Chat = Template.bind({});
Chat.args = {
user: {
diff --git a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue
index 6e332da020c86..c439cf2b45bda 100644
--- a/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue
+++ b/packages/frontend/@n8n/design-system/src/components/AskAssistantChat/AskAssistantChat.vue
@@ -1,9 +1,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/__snapshots__/N8nPromptInputSuggestions.test.ts.snap b/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/__snapshots__/N8nPromptInputSuggestions.test.ts.snap
new file mode 100644
index 0000000000000..bcde22c6e542f
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/__snapshots__/N8nPromptInputSuggestions.test.ts.snap
@@ -0,0 +1,48 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`N8nPromptInputSuggestions > renders correctly with default props 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/index.ts b/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/index.ts
new file mode 100644
index 0000000000000..395ad24f87b86
--- /dev/null
+++ b/packages/frontend/@n8n/design-system/src/components/N8nPromptInputSuggestions/index.ts
@@ -0,0 +1,4 @@
+import N8nPromptInputSuggestions from './N8nPromptInputSuggestions.vue';
+
+export default N8nPromptInputSuggestions;
+export type { WorkflowSuggestion } from '../../types/assistant';
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nScrollArea/N8nScrollArea.vue b/packages/frontend/@n8n/design-system/src/components/N8nScrollArea/N8nScrollArea.vue
index f21c947654eaf..4231be836ce83 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nScrollArea/N8nScrollArea.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nScrollArea/N8nScrollArea.vue
@@ -6,7 +6,7 @@ import {
ScrollAreaThumb,
ScrollAreaViewport,
} from 'reka-ui';
-import { computed } from 'vue';
+import { computed, ref, nextTick, type Ref } from 'vue';
export interface Props {
/**
@@ -58,6 +58,13 @@ const props = withDefaults(defineProps(), {
asChild: false,
});
+// Type for the ScrollAreaRoot instance with the viewport property
+interface ScrollAreaRootWithViewport {
+ viewport?: Ref | HTMLElement;
+}
+
+const rootRef = ref();
+
const viewportStyle = computed(() => {
const style: Record = {};
if (props.maxHeight) {
@@ -68,10 +75,96 @@ const viewportStyle = computed(() => {
}
return style;
});
+
+/**
+ * Gets the viewport element from the root ref
+ */
+function getViewportElement(): HTMLElement | undefined {
+ if (!rootRef.value?.viewport) return undefined;
+
+ const viewport = rootRef.value.viewport;
+
+ // If it's a Vue ref, unwrap it
+ if (typeof viewport === 'object' && 'value' in viewport) {
+ return viewport.value;
+ }
+
+ // If it's already an HTMLElement, use it directly
+ if (viewport instanceof HTMLElement) {
+ return viewport;
+ }
+
+ return undefined;
+}
+
+/**
+ * Scrolls the viewport to the bottom
+ * @param options - Options for controlling scroll behavior
+ */
+async function scrollToBottom(options: { smooth?: boolean } = {}) {
+ // Wait for DOM updates to ensure content is fully rendered
+ await nextTick();
+
+ const viewport = getViewportElement();
+
+ if (viewport && typeof viewport.scrollTo === 'function') {
+ viewport.scrollTo({
+ top: viewport.scrollHeight,
+ behavior: options.smooth ? 'smooth' : 'auto',
+ });
+ } else if (viewport) {
+ // Fallback for test environments or browsers that don't support scrollTo
+ viewport.scrollTop = viewport.scrollHeight;
+ }
+}
+
+/**
+ * Scrolls the viewport to the top
+ * @param options - Options for controlling scroll behavior
+ */
+async function scrollToTop(options: { smooth?: boolean } = {}) {
+ await nextTick();
+
+ const viewport = getViewportElement();
+
+ if (viewport && typeof viewport.scrollTo === 'function') {
+ viewport.scrollTo({
+ top: 0,
+ behavior: options.smooth ? 'smooth' : 'auto',
+ });
+ } else if (viewport) {
+ // Fallback for test environments or browsers that don't support scrollTo
+ viewport.scrollTop = 0;
+ }
+}
+
+/**
+ * Gets the current scroll position
+ */
+function getScrollPosition() {
+ const viewport = getViewportElement();
+
+ if (viewport) {
+ return {
+ top: viewport.scrollTop,
+ left: viewport.scrollLeft,
+ height: viewport.scrollHeight,
+ width: viewport.scrollWidth,
+ };
+ }
+ return null;
+}
+
+defineExpose({
+ scrollToBottom,
+ scrollToTop,
+ getScrollPosition,
+});
(
): msg is T & { id: string; read: boolean } {
return typeof msg.id === 'string' && typeof msg.read === 'boolean';
}
+
+// Workflow suggestion interface for the N8nPromptInputSuggestions component
+export interface WorkflowSuggestion {
+ id: string;
+ summary: string;
+ prompt: string;
+}
diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index c5babf936e29a..5b3dfeef620a0 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -217,6 +217,7 @@
"aiAssistant.builder.canvasPrompt.cancelButton": "Cancel",
"aiAssistant.builder.canvasPrompt.startManually.title": "Start manually",
"aiAssistant.builder.canvasPrompt.startManually.subTitle": "Add the first node",
+ "aiAssistant.builder.canvasPrompt.buildWithAI": "Build with AI",
"aiAssistant.builder.streamAbortedMessage": "Task aborted",
"aiAssistant.builder.executeMessage.description": "Complete these steps before executing your workflow:",
"aiAssistant.builder.executeMessage.noIssues": "Your workflow is ready to be executed",
@@ -245,7 +246,7 @@
"aiAssistant.prompts.currentView.credentialsList": "The user is currently looking at the list of credentials.",
"aiAssistant.prompts.currentView.executionsView": "The user is currently looking at the list of executions for the currently open workflow.",
"aiAssistant.prompts.currentView.workflowEditor": "The user is currently looking at the current workflow in n8n editor, without any specific node selected.",
- "aiAssistant.tooltip": "Ask Assistant",
+ "aiAssistant.tooltip": "n8n AI",
"banners.confirmEmail.message.1": "To secure your account and prevent future access issues, please confirm your",
"banners.confirmEmail.message.2": "email address.",
"banners.confirmEmail.button": "Confirm email",
diff --git a/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.vue b/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.vue
index ccb112550a0f8..d9bfc2a4e8f0b 100644
--- a/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.vue
+++ b/packages/frontend/editor-ui/src/components/AskAssistant/Agent/AskAssistantBuild.vue
@@ -12,8 +12,11 @@ import { isWorkflowUpdatedMessage } from '@n8n/design-system/types/assistant';
import { nodeViewEventBus } from '@/event-bus';
import ExecuteMessage from './ExecuteMessage.vue';
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
+import { WORKFLOW_SUGGESTIONS } from '@/constants/workflowSuggestions';
+import type { WorkflowSuggestion } from '@n8n/design-system/types/assistant';
import { N8nAskAssistantChat, N8nText } from '@n8n/design-system';
+
const emit = defineEmits<{
close: [];
}>();
@@ -32,6 +35,7 @@ const { goToUpgrade } = usePageRedirectionHelper();
const processedWorkflowUpdates = ref(new Set());
const trackedTools = ref(new Set());
const workflowUpdated = ref<{ start: string; end: string } | undefined>();
+const n8nChatRef = ref>();
const user = computed(() => ({
firstName: usersStore.currentUser?.firstName ?? '',
@@ -64,6 +68,14 @@ const creditsQuota = computed(() => builderStore.creditsQuota);
const creditsRemaining = computed(() => builderStore.creditsRemaining);
const showAskOwnerTooltip = computed(() => !usersStore.isInstanceOwner);
+const workflowSuggestions = computed(() => {
+ // Only show suggestions when no messages in chat yet (blank state)
+ if (builderStore.chatMessages.length === 0) {
+ return WORKFLOW_SUGGESTIONS;
+ }
+ return undefined;
+});
+
async function onUserMessage(content: string) {
const isNewWorkflow = workflowsStore.isNewWorkflow;
@@ -254,11 +266,18 @@ watch(
watch(currentRoute, () => {
onNewWorkflow();
});
+
+defineExpose({
+ focusInput: () => {
+ n8nChatRef.value?.focusInput();
+ },
+});
{
:credits-quota="creditsQuota"
:credits-remaining="creditsRemaining"
:show-ask-owner-tooltip="showAskOwnerTooltip"
+ :suggestions="workflowSuggestions"
:input-placeholder="i18n.baseText('aiAssistant.builder.assistantPlaceholder')"
@close="emit('close')"
@message="onUserMessage"
diff --git a/packages/frontend/editor-ui/src/components/AskAssistant/AssistantsHub.vue b/packages/frontend/editor-ui/src/components/AskAssistant/AssistantsHub.vue
index 31600f190447f..f57bc53434430 100644
--- a/packages/frontend/editor-ui/src/components/AskAssistant/AssistantsHub.vue
+++ b/packages/frontend/editor-ui/src/components/AskAssistant/AssistantsHub.vue
@@ -9,9 +9,13 @@ import AskAssistantChat from './Chat/AskAssistantChat.vue';
import { N8nResizeWrapper } from '@n8n/design-system';
import HubSwitcher from '@/components/AskAssistant/HubSwitcher.vue';
+
const builderStore = useBuilderStore();
const assistantStore = useAssistantStore();
+const askAssistantBuildRef = ref>();
+const askAssistantChatRef = ref>();
+
const isBuildMode = ref(builderStore.isAIBuilderEnabled);
const chatWidth = computed(() => {
@@ -27,12 +31,33 @@ function onResizeDebounced(data: { direction: string; x: number; width: number }
void useDebounce().callDebounced(onResize, { debounceTime: 10, trailing: true }, data);
}
-function toggleAssistantMode() {
+async function toggleAssistantMode() {
+ const wasOpen = builderStore.isAssistantOpen || assistantStore.isAssistantOpen;
+ const switchingToBuild = !isBuildMode.value;
+
isBuildMode.value = !isBuildMode.value;
- if (isBuildMode.value) {
- void builderStore.openChat();
+
+ if (wasOpen) {
+ // If chat is already open, just toggle the window flags without reloading
+ if (switchingToBuild) {
+ // Load sessions first if builder has no messages
+ if (builderStore.chatMessages.length === 0) {
+ await builderStore.fetchBuilderCredits();
+ await builderStore.loadSessions();
+ }
+ builderStore.chatWindowOpen = true;
+ assistantStore.chatWindowOpen = false;
+ } else {
+ assistantStore.chatWindowOpen = true;
+ builderStore.chatWindowOpen = false;
+ }
} else {
- assistantStore.openChat();
+ // Opening from closed state - use full open logic
+ if (isBuildMode.value) {
+ await builderStore.openChat();
+ } else {
+ assistantStore.openChat();
+ }
}
}
@@ -41,18 +66,24 @@ function onClose() {
assistantStore.closeChat();
}
+function onSlideEnterComplete() {
+ if (isBuildMode.value) {
+ askAssistantBuildRef.value?.focusInput();
+ } else {
+ askAssistantChatRef.value?.focusInput();
+ }
+}
+
const unsubscribeAssistantStore = assistantStore.$onAction(({ name }) => {
// When assistant is opened from error or credentials help
// switch from build mode to chat mode
- if (['initErrorHelper', 'initCredHelp', 'openChat'].includes(name)) {
+ if (['toggleChat', 'openChat', 'initErrorHelper', 'initCredHelp'].includes(name)) {
isBuildMode.value = false;
}
});
const unsubscribeBuilderStore = builderStore.$onAction(({ name }) => {
- // When assistant is opened from error or credentials help
- // switch from build mode to chat mode
- if (name === 'sendChatMessage') {
+ if (['toggleChat', 'openChat', 'sendChatMessage'].includes(name)) {
isBuildMode.value = true;
}
});
@@ -64,22 +95,23 @@ onBeforeUnmount(() => {
-
+
-
+
-
+
@@ -92,11 +124,16 @@ onBeforeUnmount(() => {
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
new file mode 100644
index 0000000000000..200748cc9388e
--- /dev/null
+++ b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeChoicePrompt.vue
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.baseText('nodeView.canvasAddButton.addFirstStep') }}
+
+
+
+
+
+ {{ i18n.baseText('generic.or') }}
+
+
+
+
+
+
+
+
+ {{ i18n.baseText('aiAssistant.builder.canvasPrompt.buildWithAI') }}
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeAIPrompt.test.ts.snap b/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeAIPrompt.test.ts.snap
deleted file mode 100644
index 3df3e7794e051..0000000000000
--- a/packages/frontend/editor-ui/src/components/canvas/elements/nodes/render-types/__snapshots__/CanvasNodeAIPrompt.test.ts.snap
+++ /dev/null
@@ -1,17 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`CanvasNodeAIPrompt > should render component correctly 1`] = `
-"
-
- What would you like to automate?
-
-
-
- or
-
- Start manuallyAdd the first node
-
-"
-`;
diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts
index 3338cb9b5fd7c..bd210ed2c0281 100644
--- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts
+++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts
@@ -15,7 +15,7 @@ import type {
CanvasConnectionPort,
CanvasNode,
CanvasNodeAddNodesRender,
- CanvasNodeAIPromptRender,
+ CanvasNodeChoicePromptRender,
CanvasNodeData,
CanvasNodeDefaultRender,
CanvasNodeDefaultRenderLabelSize,
@@ -96,9 +96,10 @@ export function useCanvasMapping({
options: {},
};
}
- function createAIPromptRenderType(): CanvasNodeAIPromptRender {
+
+ function createChoicePromptRenderType(): CanvasNodeChoicePromptRender {
return {
- type: CanvasNodeRenderType.AIPrompt,
+ type: CanvasNodeRenderType.ChoicePrompt,
options: {},
};
}
@@ -140,8 +141,8 @@ export function useCanvasMapping({
case `${CanvasNodeRenderType.AddNodes}`:
acc[node.id] = createAddNodesRenderType();
break;
- case `${CanvasNodeRenderType.AIPrompt}`:
- acc[node.id] = createAIPromptRenderType();
+ case `${CanvasNodeRenderType.ChoicePrompt}`:
+ acc[node.id] = createChoicePromptRenderType();
break;
default:
acc[node.id] = createDefaultNodeRenderType(node);
diff --git a/packages/frontend/editor-ui/src/composables/useStyles.ts b/packages/frontend/editor-ui/src/composables/useStyles.ts
index 8f54fbea3f845..b7080397a4551 100644
--- a/packages/frontend/editor-ui/src/composables/useStyles.ts
+++ b/packages/frontend/editor-ui/src/composables/useStyles.ts
@@ -3,11 +3,11 @@ const APP_Z_INDEXES = {
APP_HEADER: 99,
SELECT_BOX: 100,
CANVAS_ADD_BUTTON: 101,
- ASK_ASSISTANT_CHAT: 300,
APP_SIDEBAR: 999,
CANVAS_SELECT_BOX: 100,
TOP_BANNERS: 999,
NODE_CREATOR: 1700,
+ ASK_ASSISTANT_CHAT: 1750,
NDV: 1800,
MODALS: 2000,
TOASTS: 2100,
diff --git a/packages/frontend/editor-ui/src/stores/assistant.store.ts b/packages/frontend/editor-ui/src/stores/assistant.store.ts
index 579b26138e6d2..7deb1721bf9c6 100644
--- a/packages/frontend/editor-ui/src/stores/assistant.store.ts
+++ b/packages/frontend/editor-ui/src/stores/assistant.store.ts
@@ -30,11 +30,10 @@ import { useUIStore } from './ui.store';
import AiUpdatedCodeMessage from '@/components/AiUpdatedCodeMessage.vue';
import { useCredentialsStore } from './credentials.store';
import { useAIAssistantHelpers } from '@/composables/useAIAssistantHelpers';
-import { useBuilderStore } from './builder.store';
export const MAX_CHAT_WIDTH = 425;
-export const MIN_CHAT_WIDTH = 300;
-export const DEFAULT_CHAT_WIDTH = 330;
+export const MIN_CHAT_WIDTH = 380;
+export const DEFAULT_CHAT_WIDTH = 400;
export const ENABLED_VIEWS = [
...EDITABLE_CANVAS_VIEWS,
VIEWS.EXECUTION_PREVIEW,
@@ -63,7 +62,6 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
const locale = useI18n();
const telemetry = useTelemetry();
const assistantHelpers = useAIAssistantHelpers();
- const builderStore = useBuilderStore();
const suggestions = ref<{
[suggestionId: string]: {
@@ -173,15 +171,10 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
}, ASK_AI_SLIDE_OUT_DURATION_MS + 50);
}
- function toggleChatOpen() {
- if (chatWindowOpen.value) {
+ function toggleChat() {
+ if (isAssistantOpen.value) {
closeChat();
} else {
- if (builderStore.isAIBuilderEnabled) {
- // If builder is enabled, open it instead of assistant
- void builderStore.openChat();
- return;
- }
openChat();
}
}
@@ -852,9 +845,9 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
isFloatingButtonShown,
onNodeExecution,
trackUserOpenedAssistant,
- closeChat,
openChat,
- toggleChatOpen,
+ closeChat,
+ toggleChat,
updateWindowWidth,
isNodeErrorActive,
initErrorHelper,
diff --git a/packages/frontend/editor-ui/src/stores/builder.store.ts b/packages/frontend/editor-ui/src/stores/builder.store.ts
index a52f6fc4ff3ec..42bf85bbe0986 100644
--- a/packages/frontend/editor-ui/src/stores/builder.store.ts
+++ b/packages/frontend/editor-ui/src/stores/builder.store.ts
@@ -10,7 +10,7 @@ import { STORES } from '@n8n/stores';
import type { ChatUI } from '@n8n/design-system/types/assistant';
import { isToolMessage, isWorkflowUpdatedMessage } from '@n8n/design-system/types/assistant';
import { defineStore } from 'pinia';
-import { computed, ref } from 'vue';
+import { computed, watch, ref } from 'vue';
import { useRoute } from 'vue-router';
import { useSettingsStore } from './settings.store';
import { assert } from '@n8n/utils/assert';
@@ -160,14 +160,14 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
* Opens the chat panel and adjusts the canvas viewport to make room.
*/
async function openChat() {
- chatWindowOpen.value = true;
chatMessages.value = [];
+ await fetchBuilderCredits();
+ await loadSessions();
uiStore.appGridDimensions = {
...uiStore.appGridDimensions,
width: window.innerWidth - chatWidth.value,
};
- await fetchBuilderCredits();
- await loadSessions();
+ chatWindowOpen.value = true;
}
/**
@@ -192,6 +192,17 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
}, ASK_AI_SLIDE_OUT_DURATION_MS + 50);
}
+ /**
+ * Toggles between open and closed state for the chat panel.
+ */
+ async function toggleChat() {
+ if (isAssistantOpen.value) {
+ closeChat();
+ } else {
+ await openChat();
+ }
+ }
+
/**
* Updates chat panel width with enforced boundaries.
* Width is clamped between MIN_CHAT_WIDTH (330px) and MAX_CHAT_WIDTH (650px)
@@ -576,6 +587,17 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
creditsClaimed.value = claimed;
}
+ // Watch for route changes and close chat when leaving enabled views
+ watch(
+ () => route.name,
+ (newRoute) => {
+ // Close the chat window when navigating away from canvas/enabled views
+ if (!ENABLED_VIEWS.includes(newRoute as VIEWS) && chatWindowOpen.value) {
+ chatWindowOpen.value = false;
+ }
+ },
+ );
+
// Public API
return {
// State
@@ -605,6 +627,7 @@ export const useBuilderStore = defineStore(STORES.BUILDER, () => {
stopStreaming,
closeChat,
openChat,
+ toggleChat,
resetBuilderChat,
sendChatMessage,
loadSessions,
diff --git a/packages/frontend/editor-ui/src/types/canvas.ts b/packages/frontend/editor-ui/src/types/canvas.ts
index 4100aed17942f..098c6891c7ba8 100644
--- a/packages/frontend/editor-ui/src/types/canvas.ts
+++ b/packages/frontend/editor-ui/src/types/canvas.ts
@@ -44,7 +44,7 @@ export const enum CanvasNodeRenderType {
Default = 'default',
StickyNote = 'n8n-nodes-base.stickyNote',
AddNodes = 'n8n-nodes-internal.addNodes',
- AIPrompt = 'n8n-nodes-base.aiPrompt',
+ ChoicePrompt = 'n8n-nodes-internal.choicePrompt',
}
export type CanvasNodeDefaultRenderLabelSize = 'small' | 'medium' | 'large';
@@ -82,8 +82,8 @@ export type CanvasNodeAddNodesRender = {
options: Record;
};
-export type CanvasNodeAIPromptRender = {
- type: CanvasNodeRenderType.AIPrompt;
+export type CanvasNodeChoicePromptRender = {
+ type: CanvasNodeRenderType.ChoicePrompt;
options: Record;
};
@@ -134,7 +134,7 @@ export interface CanvasNodeData {
| CanvasNodeDefaultRender
| CanvasNodeStickyNoteRender
| CanvasNodeAddNodesRender
- | CanvasNodeAIPromptRender;
+ | CanvasNodeChoicePromptRender;
}
export type CanvasNode = Node;
diff --git a/packages/frontend/editor-ui/src/views/NodeView.vue b/packages/frontend/editor-ui/src/views/NodeView.vue
index 5961a1f5322b9..b25e620fc5552 100644
--- a/packages/frontend/editor-ui/src/views/NodeView.vue
+++ b/packages/frontend/editor-ui/src/views/NodeView.vue
@@ -434,10 +434,6 @@ async function initializeRoute(force = false) {
if (!isDemoRoute.value) {
await loadCredentials();
-
- // Fetch builder credits when initializing the route
- // Only needed if workflow is editable where builder can be used
- void builderStore.fetchBuilderCredits();
}
// If there is no workflow id, treat it as a new workflow
@@ -1475,6 +1471,7 @@ function removeSourceControlEventBindings() {
function addCommandBarEventBindings() {
canvasEventBus.on('create:sticky', onCreateSticky);
}
+
function removeCommandBarEventBindings() {
canvasEventBus.off('create:sticky', onCreateSticky);
}
@@ -1803,12 +1800,12 @@ watch(
parameters: {},
};
- const aiPromptItem: INodeUi = {
- id: CanvasNodeRenderType.AIPrompt,
- name: CanvasNodeRenderType.AIPrompt,
- type: CanvasNodeRenderType.AIPrompt,
+ const choicePromptItem: INodeUi = {
+ id: CanvasNodeRenderType.ChoicePrompt,
+ name: CanvasNodeRenderType.ChoicePrompt,
+ type: CanvasNodeRenderType.ChoicePrompt,
typeVersion: 1,
- position: [-300, -100],
+ position: [0, 0],
parameters: {},
draggable: false,
};
@@ -1817,7 +1814,7 @@ watch(
builderStore.isAIBuilderEnabled &&
builderStore.isAssistantEnabled &&
builderStore.assistantMessages.length === 0
- ? [aiPromptItem]
+ ? [choicePromptItem]
: [addNodesItem];
},
);