diff --git a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
index d3feaab958c..8f3b527dd81 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nIcon/icons.ts
@@ -72,6 +72,7 @@ import IconLucideCircle from '~icons/lucide/circle';
import IconLucideCircleAlert from '~icons/lucide/circle-alert';
import IconLucideCircleCheck from '~icons/lucide/circle-check';
import IconLucideCircleDot from '~icons/lucide/circle-dot';
+import IconLucideCircleEllipsis from '~icons/lucide/circle-ellipsis';
import IconLucideCircleHelp from '~icons/lucide/circle-help';
import IconLucideCircleMinus from '~icons/lucide/circle-minus';
import IconLucideCirclePause from '~icons/lucide/circle-pause';
@@ -151,6 +152,7 @@ import IconLucideMaximize2 from '~icons/lucide/maximize-2';
import IconLucideMenu from '~icons/lucide/menu';
import IconLucideMessageCircle from '~icons/lucide/message-circle';
import IconLucideMessageSquare from '~icons/lucide/message-square';
+import IconLucideMessageSquarePlus from '~icons/lucide/message-square-plus';
import IconLucideMessagesSquare from '~icons/lucide/messages-square';
import IconLucideMic from '~icons/lucide/mic';
import IconLucideMilestone from '~icons/lucide/milestone';
@@ -518,6 +520,7 @@ export const updatedIconSet = {
'circle-alert': IconLucideCircleAlert,
'circle-check': IconLucideCircleCheck,
'circle-dot': IconLucideCircleDot,
+ 'circle-ellipsis': IconLucideCircleEllipsis,
'circle-help': IconLucideCircleHelp,
'circle-minus': IconLucideCircleMinus,
'circle-pause': IconLucideCirclePause,
@@ -597,6 +600,7 @@ export const updatedIconSet = {
menu: IconLucideMenu,
'message-circle': IconLucideMessageCircle,
'message-square': IconLucideMessageSquare,
+ 'message-square-plus': IconLucideMessageSquarePlus,
'messages-square': IconLucideMessagesSquare,
mic: IconLucideMic,
milestone: IconLucideMilestone,
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.stories.ts b/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.stories.ts
index 6bf53173801..d8007fae82c 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.stories.ts
+++ b/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.stories.ts
@@ -60,6 +60,16 @@ withSecondaryIconTooltip.args = {
},
};
+export const withBetaTag = template.bind({});
+withBetaTag.args = {
+ item: {
+ id: 'workflows',
+ icon: 'home',
+ label: 'Workflows',
+ beta: true,
+ },
+};
+
export const compact = template.bind({});
compact.args = {
item: {
diff --git a/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.vue b/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.vue
index cde4b1f019f..f1a2658ca1f 100644
--- a/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.vue
+++ b/packages/frontend/@n8n/design-system/src/components/N8nMenuItem/MenuItem.vue
@@ -3,6 +3,7 @@ import { computed } from 'vue';
import type { IMenuItem } from '@n8n/design-system/types';
+import BetaTag from '../BetaTag/BetaTag.vue';
import N8nIcon from '../N8nIcon';
import type { IconName } from '../N8nIcon/icons';
import N8nRoute from '../N8nRoute';
@@ -66,7 +67,7 @@ const iconColor = computed(() => {
:id="item.id"
:to="to"
role="menuitem"
- :class="[$style.menuItem, { [$style.active]: active }]"
+ :class="[$style.menuItem, { [$style.active]: active, [$style.compact]: compact }]"
:aria-label="props.ariaLabel"
data-test-id="menu-item"
@click="emit('click')"
@@ -83,7 +84,12 @@ const iconColor = computed(() => {
>
- {{ item.label }}
+
+
+ {{ item.label }}
+
+
+
@@ -125,6 +131,10 @@ const iconColor = computed(() => {
background-color: var(--color--background--light-1);
color: var(--color--text--shade-1);
}
+
+ &.compact {
+ gap: 0;
+ }
}
.menuItem:focus-visible {
@@ -136,7 +146,6 @@ const iconColor = computed(() => {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
- flex: 1;
line-height: var(--font-size--lg);
min-width: 0;
}
@@ -176,4 +185,13 @@ const iconColor = computed(() => {
color: var(--color--text--shade-1);
}
}
+
+.menuItemLabel {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ gap: var(--spacing--3xs);
+ flex: 1;
+ min-width: 0;
+}
diff --git a/packages/frontend/@n8n/design-system/src/types/menu.ts b/packages/frontend/@n8n/design-system/src/types/menu.ts
index d1bc0681616..8975951b670 100644
--- a/packages/frontend/@n8n/design-system/src/types/menu.ts
+++ b/packages/frontend/@n8n/design-system/src/types/menu.ts
@@ -36,6 +36,7 @@ export type IMenuItem = {
disabled?: boolean;
notification?: boolean;
size?: 'medium' | 'small';
+ beta?: boolean;
};
export interface ICustomMenuItem {
diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json
index 7c0aff192a2..7432d67bab8 100644
--- a/packages/frontend/@n8n/i18n/src/locales/en.json
+++ b/packages/frontend/@n8n/i18n/src/locales/en.json
@@ -456,6 +456,7 @@
"chatHub.sidebar.link.newChat": "New chat",
"chatHub.sidebar.link.workflowAgents": "Workflow agents",
"chatHub.sidebar.link.personalAgents": "Personal agents",
+ "chatHub.sidebar.loadMoreSessions": "Load more",
"chatEmbed.infoTip.description": "Add chat to external applications using the n8n chat package.",
"chatEmbed.infoTip.link": "More info",
"chatEmbed.title": "Embed Chat in your website",
@@ -3737,7 +3738,7 @@
"projects.menu.title": "Projects",
"projects.menu.personal": "Personal",
"projects.menu.addFirstProject": "Add project",
- "projects.menu.chat": "Chat (Beta)",
+ "projects.menu.chat": "Chat",
"projects.settings": "Project settings",
"projects.settings.info": "Project info",
"projects.settings.newProjectName": "My project",
diff --git a/packages/frontend/editor-ui/src/app/components/BottomMenu.vue b/packages/frontend/editor-ui/src/app/components/BottomMenu.vue
new file mode 100644
index 00000000000..1f492ba384e
--- /dev/null
+++ b/packages/frontend/editor-ui/src/app/components/BottomMenu.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ handleSelect(child.id)" />
+
+
+ What's new
+
+
+ handleSelect(child.id)" />
+
+
+
+
+
+ handleSelect(item.id)"
+ />
+
+
+
+
+
+
+
+
+ handleSelect(child.id)" />
+
+
+
+
+
+
+ handleSelect(item.id)"
+ />
+
+
+
+ handleSelect(item.id)"
+ />
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts b/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts
index 32a034e0383..0d877aafe2f 100644
--- a/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts
+++ b/packages/frontend/editor-ui/src/app/components/MainSidebar.test.ts
@@ -6,7 +6,6 @@ import { defaultSettings } from '@/__tests__/defaults';
import MainSidebar from '@/app/components/MainSidebar.vue';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useUIStore } from '@/app/stores/ui.store';
-import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
import { useVersionsStore } from '@/app/stores/versions.store';
import { useUsersStore } from '@/features/settings/users/users.store';
import { useTemplatesStore } from '@/features/workflows/templates/templates.store';
@@ -27,7 +26,6 @@ vi.mock('vue-router', () => ({
let renderComponent: ReturnType;
let settingsStore: MockedStore;
let uiStore: MockedStore;
-let sourceControlStore: MockedStore;
let versionsStore: MockedStore;
let usersStore: MockedStore;
let templatesStore: MockedStore;
@@ -54,7 +52,6 @@ describe('MainSidebar', () => {
});
settingsStore = mockedStore(useSettingsStore);
uiStore = mockedStore(useUIStore);
- sourceControlStore = mockedStore(useSourceControlStore);
versionsStore = mockedStore(useVersionsStore);
usersStore = mockedStore(useUsersStore);
templatesStore = mockedStore(useTemplatesStore);
@@ -83,23 +80,6 @@ describe('MainSidebar', () => {
expect(() => renderComponent()).not.toThrow();
});
- test.each([
- [false, true, true],
- [true, false, false],
- [true, true, false],
- [false, false, false],
- ])(
- 'should render readonly tooltip when is opened %s and the environment is readonly %s',
- (sidebarMenuCollapsed, branchReadOnly, shouldRender) => {
- uiStore.sidebarMenuCollapsed = sidebarMenuCollapsed;
- sourceControlStore.preferences.branchReadOnly = branchReadOnly;
-
- const { queryByTestId } = renderComponent();
-
- expect(queryByTestId('read-only-env-icon') !== null).toBe(shouldRender);
- },
- );
-
describe('Version Update CTA', () => {
it('should not render version update CTA when hasVersionUpdates is false', () => {
versionsStore.hasVersionUpdates = false;
diff --git a/packages/frontend/editor-ui/src/app/components/MainSidebar.vue b/packages/frontend/editor-ui/src/app/components/MainSidebar.vue
index 5e0d11bc79b..f933378dfe3 100644
--- a/packages/frontend/editor-ui/src/app/components/MainSidebar.vue
+++ b/packages/frontend/editor-ui/src/app/components/MainSidebar.vue
@@ -1,67 +1,43 @@
@@ -427,234 +341,23 @@ const onLogout = () => {
@resize="onResize"
@resizeend="onResizeEnd"
>
-
-
-
-
-
-
-
- {{ i18n.baseText('readOnlyEnv.tooltip.link') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ upgradeLabel }}
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
- handleSelect(child.id)" />
-
-
- What's new
-
-
- handleSelect(child.id)" />
-
-
-
-
-
- handleSelect(item.id)"
- />
-
-
-
-
-
-
-
-
- handleSelect(child.id)" />
-
-
-
-
-
-
- handleSelect(item.id)"
- />
-
-
-
- handleSelect(item.id)"
- />
-
-
-
+
@@ -672,93 +375,15 @@ const onLogout = () => {
border-right: var(--border);
background-color: var(--menu--color--background, var(--color--background--light-2));
- .header {
- display: flex;
- align-items: center;
- padding: var(--spacing--2xs) var(--spacing--3xs);
- margin-bottom: var(--spacing--2xs);
- justify-content: space-between;
- gap: var(--spacing--4xs);
-
- img {
- position: relative;
- left: 1px;
- height: 20px;
- margin-right: auto;
- }
- }
-
&.sideMenuCollapsed {
width: $sidebar-width;
min-width: auto;
-
- .header {
- flex-direction: column;
- border-bottom: var(--border);
- }
}
}
-.logo {
- margin-right: auto;
-}
-
.scrollArea {
height: 100%;
display: flex;
flex-direction: column;
}
-
-.bottomMenu {
- display: flex;
- flex-direction: column;
- margin-top: auto;
-}
-
-.sideMenuCollapsed .bottomMenu {
- border-top: var(--border);
-}
-
-.bottomMenuItems {
- padding: var(--spacing--3xs);
-}
-
-.popover {
- padding: var(--spacing--4xs);
- min-width: 260px;
- border-radius: var(--radius);
- background-color: var(--menu--color--background, var(--color--background--light-2));
-}
-
-.divider {
- display: block;
- width: 100%;
- padding-top: var(--spacing--3xs);
- border-bottom: var(--border);
- margin-bottom: var(--spacing--3xs);
- background-color: var(--color--border);
-}
-
-.popoverTitle {
- display: block;
- margin-bottom: var(--spacing--3xs);
- padding-left: var(--spacing--3xs);
- margin-top: var(--spacing--xs);
-}
-
-@media screen and (max-height: 470px) {
- :global(#help) {
- display: none;
- }
-}
-
-.readOnlyEnvironmentIcon {
- display: inline-block;
- color: white;
- background-color: var(--color--warning);
- align-self: center;
- padding: 2px;
- border-radius: var(--radius--sm);
- margin: 7px 12px 0 5px;
-}
diff --git a/packages/frontend/editor-ui/src/app/components/MainSidebarHeader.test.ts b/packages/frontend/editor-ui/src/app/components/MainSidebarHeader.test.ts
new file mode 100644
index 00000000000..fb232d9de6a
--- /dev/null
+++ b/packages/frontend/editor-ui/src/app/components/MainSidebarHeader.test.ts
@@ -0,0 +1,143 @@
+import { reactive } from 'vue';
+import { fireEvent } from '@testing-library/vue';
+import { createComponentRenderer } from '@/__tests__/render';
+import { createTestingPinia } from '@pinia/testing';
+import { type MockedStore, mockedStore } from '@/__tests__/utils';
+import { defaultSettings } from '@/__tests__/defaults';
+import { useSettingsStore } from '@/app/stores/settings.store';
+import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
+import MainSidebarHeader from './MainSidebarHeader.vue';
+import { COMMAND_BAR_EXPERIMENT } from '../constants';
+
+vi.mock('vue-router', () => ({
+ useRouter: () => ({
+ resolve: vi.fn(() => ({ meta: {} })),
+ }),
+ useRoute: () => reactive({}),
+ RouterLink: {
+ template: '',
+ },
+}));
+
+vi.mock('@/app/stores/posthog.store', () => ({
+ usePostHog: () => ({
+ isVariantEnabled: vi.fn().mockImplementation((name) => name === COMMAND_BAR_EXPERIMENT.name),
+ }),
+}));
+
+let settingsStore: MockedStore;
+let sourceControlStore: MockedStore;
+let pinia: ReturnType;
+
+describe('MainSidebarHeader', () => {
+ beforeEach(() => {
+ pinia = createTestingPinia();
+ settingsStore = mockedStore(useSettingsStore);
+ sourceControlStore = mockedStore(useSourceControlStore);
+ settingsStore.settings = defaultSettings;
+ });
+
+ it('renders header without error', () => {
+ expect(() =>
+ createComponentRenderer(MainSidebarHeader, {
+ pinia,
+ props: { isCollapsed: false },
+ })(),
+ ).not.toThrow();
+ });
+
+ it('renders collapsed header without error', () => {
+ expect(() =>
+ createComponentRenderer(MainSidebarHeader, {
+ pinia,
+ props: { isCollapsed: true },
+ })(),
+ ).not.toThrow();
+ });
+
+ test.each([
+ [false, true, true],
+ [true, false, false],
+ [true, true, false],
+ [false, false, false],
+ ])(
+ 'should render readonly tooltip when is collapsed %s and the environment is readonly %s',
+ (isCollapsed, branchReadOnly, shouldRender) => {
+ sourceControlStore.preferences.branchReadOnly = branchReadOnly;
+
+ const { queryByTestId } = createComponentRenderer(MainSidebarHeader, {
+ pinia,
+ props: { isCollapsed },
+ })();
+
+ expect(queryByTestId('read-only-env-icon') !== null).toBe(shouldRender);
+ },
+ );
+
+ it('should render beta icon when not collapsed and isBeta is true', () => {
+ const { queryByTestId } = createComponentRenderer(MainSidebarHeader, {
+ pinia,
+ props: { isCollapsed: false, isBeta: true },
+ })();
+
+ expect(queryByTestId('beta-icon') !== null).toBe(true);
+ });
+
+ it('renders the logo link only when not collapsed', async () => {
+ const { container, rerender } = createComponentRenderer(MainSidebarHeader, {
+ pinia,
+ props: { isCollapsed: false, hideCreate: true },
+ })();
+
+ expect(container.querySelector('a')).toBeInTheDocument();
+
+ await rerender({ isCollapsed: true, hideCreate: true });
+
+ expect(container.querySelector('a')).not.toBeInTheDocument();
+ });
+
+ it('displays create button', () => {
+ const { queryByTestId } = createComponentRenderer(MainSidebarHeader, {
+ pinia,
+ props: { isCollapsed: false },
+ })();
+
+ expect(queryByTestId('universal-add')).toBeInTheDocument();
+ });
+
+ it('hides create button when hideCreate is true', () => {
+ const { queryByTestId } = createComponentRenderer(MainSidebarHeader, {
+ pinia,
+ props: { isCollapsed: false, hideCreate: true },
+ })();
+
+ expect(queryByTestId('universal-add')).not.toBeInTheDocument();
+ });
+
+ it('emits "collapse" when toggle sidebar is clicked', async () => {
+ const { getByRole, emitted } = createComponentRenderer(MainSidebarHeader, {
+ pinia,
+ props: { isCollapsed: false, hideCreate: true },
+ })();
+
+ await fireEvent.click(getByRole('button', { name: 'Toggle sidebar' }));
+
+ expect(emitted('collapse')).toHaveLength(1);
+ });
+
+ it('emits "openCommandBar" with the click event when command bar button is clicked', async () => {
+ const { getByRole, emitted } = createComponentRenderer(MainSidebarHeader, {
+ pinia,
+ props: { isCollapsed: false, hideCreate: true },
+ })();
+
+ await fireEvent.click(getByRole('button', { name: 'Open command palette' }));
+
+ const emits = emitted('openCommandBar');
+ expect(emits).toHaveLength(1);
+
+ const [[event]] = emits as Array<[Event]>;
+ expect(event).toBeInstanceOf(MouseEvent);
+ expect((event as MouseEvent).type).toBe('click');
+ });
+});
diff --git a/packages/frontend/editor-ui/src/app/components/MainSidebarHeader.vue b/packages/frontend/editor-ui/src/app/components/MainSidebarHeader.vue
new file mode 100644
index 00000000000..c744f053fec
--- /dev/null
+++ b/packages/frontend/editor-ui/src/app/components/MainSidebarHeader.vue
@@ -0,0 +1,256 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.baseText('readOnlyEnv.tooltip.link') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ upgradeLabel }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/app/constants/modals.ts b/packages/frontend/editor-ui/src/app/constants/modals.ts
index 3447ab408db..7f7572d8294 100644
--- a/packages/frontend/editor-ui/src/app/constants/modals.ts
+++ b/packages/frontend/editor-ui/src/app/constants/modals.ts
@@ -31,7 +31,6 @@ export const WORKFLOW_EXTRACTION_NAME_MODAL_KEY = 'workflowExtractionName';
export const WHATS_NEW_MODAL_KEY = 'whatsNew';
export const WORKFLOW_DIFF_MODAL_KEY = 'workflowDiff';
export const PRE_BUILT_AGENTS_MODAL_KEY = 'preBuiltAgents';
-export const CHAT_HUB_SIDE_MENU_DRAWER_MODAL_KEY = 'chatHubSideMenuDrawer';
export const EXPERIMENT_TEMPLATE_RECO_V2_KEY = 'templateRecoV2';
export const EXPERIMENT_TEMPLATE_RECO_V3_KEY = 'templateRecoV3';
export const EXPERIMENT_TEMPLATES_DATA_QUALITY_KEY = 'templatesDataQuality';
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatPersonalAgentsView.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatPersonalAgentsView.vue
index 911901d52f0..485675c96a5 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatPersonalAgentsView.vue
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatPersonalAgentsView.vue
@@ -16,7 +16,6 @@ import type { ChatAgentFilter } from '@/features/ai/chatHub/chat.types';
import { useMediaQuery } from '@vueuse/core';
import { AGENT_EDITOR_MODAL_KEY, MOBILE_MEDIA_QUERY } from '@/features/ai/chatHub/constants';
import ChatLayout from '@/features/ai/chatHub/components/ChatLayout.vue';
-import ChatSidebarOpener from '@/features/ai/chatHub/components/ChatSidebarOpener.vue';
import SkeletonAgentCard from '@/features/ai/chatHub/components/SkeletonAgentCard.vue';
import { useI18n } from '@n8n/i18n';
@@ -134,7 +133,6 @@ watch(
-
@@ -152,17 +150,6 @@ watch(
position: relative;
}
-.menuButton {
- position: absolute;
- top: 0;
- left: 0;
- margin: var(--spacing--sm);
-
- .isMobileDevice & {
- margin: var(--spacing--2xs);
- }
-}
-
.header {
display: flex;
justify-content: space-between;
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue
index 0da39b9ff2f..18fca5d8137 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue
@@ -604,7 +604,6 @@ function onFilesDropped(files: File[]) {
:selected-model="selectedModel"
:credentials="credentialsByProvider"
:ready-to-show-model-selector="isNewSession || !!currentConversation"
- :is-new-session="isNewSession"
@select-model="handleSelectModel"
@edit-custom-agent="handleEditAgent"
@create-custom-agent="openNewAgentCreator"
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatWorkflowAgentsView.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatWorkflowAgentsView.vue
index 429a54bd7dd..30887571acd 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatWorkflowAgentsView.vue
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatWorkflowAgentsView.vue
@@ -14,7 +14,6 @@ import { useMediaQuery } from '@vueuse/core';
import { MOBILE_MEDIA_QUERY } from '@/features/ai/chatHub/constants';
import { useRouter } from 'vue-router';
import ChatLayout from '@/features/ai/chatHub/components/ChatLayout.vue';
-import ChatSidebarOpener from '@/features/ai/chatHub/components/ChatSidebarOpener.vue';
import SkeletonAgentCard from '@/features/ai/chatHub/components/SkeletonAgentCard.vue';
import { useI18n } from '@n8n/i18n';
@@ -96,7 +95,6 @@ watch(
/>
-
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.api.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.api.ts
index b61a737237d..05075c0fab3 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.api.ts
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.api.ts
@@ -16,8 +16,8 @@ import type {
ChatHubUpdateConversationRequest,
EnrichedStructuredChunk,
ChatHubLLMProvider,
+ ChatProviderSettingsDto,
} from '@n8n/api-types';
-import type { ChatProviderSettingsDto } from '@n8n/api-types';
// Workflows stream data as newline separated JSON objects (jsonl)
const STREAM_SEPARATOR = '\n';
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts
index 44fe5700dc3..fccf66fc08a 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/chat.store.ts
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
-import { CHAT_STORE } from './constants';
+import { CHAT_STORE, CHAT_SESSIONS_PAGE_SIZE } from './constants';
import { computed, ref } from 'vue';
import { v4 as uuidv4 } from 'uuid';
import { useI18n } from '@n8n/i18n';
@@ -322,7 +322,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
try {
const cursor = reset ? undefined : (sessions.value?.nextCursor ?? undefined);
const [response] = await Promise.all([
- fetchSessionsApi(rootStore.restApiContext, 40, cursor),
+ fetchSessionsApi(rootStore.restApiContext, CHAT_SESSIONS_PAGE_SIZE, cursor),
new Promise((resolve) => setTimeout(resolve, options.minLoadingTime ?? 0)),
]);
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue
index d325940b803..711585a63a2 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/AgentEditorModal.vue
@@ -147,11 +147,11 @@ watch(
watch(
[isOpened, isLoadingAgent, nameInputRef],
- async ([opened, isLoading, name]) => {
+ ([opened, isLoading, nameInput]) => {
if (opened && !isLoading) {
// autofocus attribute doesn't work in modal
// https://github.com/element-plus/element-plus/issues/15250
- name?.focus();
+ nameInput?.focus();
}
},
{ immediate: true, flush: 'post' },
@@ -308,8 +308,8 @@ function onSelectTools() {
:button-tooltip="i18n.baseText('chatHub.agent.editor.iconPicker.button.tooltip')"
/>
{{
agent?.name || i18n.baseText('chatHub.agent.unavailableAgent')
}}
-
- {{ agent.icon.value }}
-
-
-
+
+
+ {{ agent.icon.value }}
+
+
+
+
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarContent.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarContent.vue
index a1efeae331c..69e62af3084 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarContent.vue
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarContent.vue
@@ -1,40 +1,32 @@
-
-
-
-
+
+
-
+
+
+
-
-
+
-
+
{{ group.group }}
{
@confirm-rename="handleConfirmRename"
@delete="handleDeleteSession"
/>
+
+ chatStore.fetchMoreSessions({ minLoadingTime: 250 })"
+ />
-
-
-
@@ -227,11 +241,6 @@ onMounted(() => {
display: flex;
flex-direction: column;
align-items: stretch;
- padding-block: var(--spacing--4xs);
-
- &.isMobileDevice {
- padding-block: 0;
- }
}
.header {
@@ -253,36 +262,35 @@ onMounted(() => {
.links {
display: flex;
flex-direction: column;
- padding: 0 var(--spacing--xs) var(--spacing--sm) var(--spacing--xs);
+ padding: 0 var(--spacing--3xs) var(--spacing--2xs);
gap: 1px;
+
+ &.collapsed {
+ border-bottom: var(--border);
+ }
}
.historySections {
display: flex;
flex-direction: column;
- padding: 0 var(--spacing--xs) var(--spacing--sm) var(--spacing--xs);
- gap: var(--spacing--xs);
+ padding: var(--spacing--2xs) var(--spacing--3xs) var(--spacing--2xs);
+ gap: var(--spacing--sm);
- .isMobileDevice & {
- gap: var(--spacing--sm);
+ &.collapsed {
+ gap: 0;
}
}
.group {
display: flex;
flex-direction: column;
- gap: 1px;
+ gap: 2px;
}
.groupHeader {
padding: 0 var(--spacing--4xs) var(--spacing--3xs) var(--spacing--4xs);
}
-.loadMoreTrigger {
- height: 1px;
- width: 100%;
-}
-
.loading,
.empty {
padding: var(--spacing--xs);
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarLink.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarLink.vue
index 9310587baaa..b1266ad191c 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarLink.vue
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSidebarLink.vue
@@ -8,14 +8,18 @@ const {
active = false,
to,
label,
+ title,
menuItems = [],
icon,
+ compact,
} = defineProps<{
active?: boolean;
to: RouteLocationRaw;
label: string;
+ title: string;
menuItems?: Array>;
icon?: IconName;
+ compact?: boolean;
}>();
const emit = defineEmits<{
@@ -35,17 +39,20 @@ defineSlots<{
-
+
- {{ label }}
+
+ {{ label }}
+ {{ title }}
+
-import { useChatHubSidebarState } from '@/features/ai/chatHub/composables/useChatHubSidebarState';
-import { N8nIconButton } from '@n8n/design-system';
-import { useI18n } from '@n8n/i18n';
-
-const sidebar = useChatHubSidebarState();
-const i18n = useI18n();
-
-
-
-
-
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/composables/useChatHubSidebarState.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/composables/useChatHubSidebarState.ts
deleted file mode 100644
index 11540f8f708..00000000000
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/composables/useChatHubSidebarState.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import {
- CHAT_HUB_SIDE_MENU_DRAWER_MODAL_KEY,
- LOCAL_STORAGE_CHAT_HUB_STATIC_SIDEBAR,
-} from '@/app/constants';
-import { useUIStore } from '@/app/stores/ui.store';
-import { useLocalStorage, useMediaQuery } from '@vueuse/core';
-import { computed } from 'vue';
-import { MOBILE_MEDIA_QUERY } from '../constants';
-import { useUsersStore } from '@/features/settings/users/users.store';
-
-export function useChatHubSidebarState() {
- const isMobileDevice = useMediaQuery(MOBILE_MEDIA_QUERY);
- const uiStore = useUIStore();
- const usersStore = useUsersStore();
- const isStatic = useLocalStorage(
- LOCAL_STORAGE_CHAT_HUB_STATIC_SIDEBAR(usersStore.currentUserId ?? 'anonymous'),
- !isMobileDevice.value,
- { writeDefaults: false },
- );
- const canBeStatic = computed(() => !isMobileDevice.value);
-
- function toggleOpen(value?: boolean) {
- const isOpen = !!uiStore.isModalActiveById[CHAT_HUB_SIDE_MENU_DRAWER_MODAL_KEY];
- const newValue = value ?? !isOpen;
-
- if (newValue) {
- uiStore.openModal(CHAT_HUB_SIDE_MENU_DRAWER_MODAL_KEY);
- } else {
- uiStore.closeModal(CHAT_HUB_SIDE_MENU_DRAWER_MODAL_KEY);
- }
- }
-
- function toggleStatic(value?: boolean) {
- const newValue = value ?? !isStatic.value;
-
- isStatic.value = newValue;
- toggleOpen(newValue);
- }
-
- return {
- canBeStatic,
- isStatic: computed(() => canBeStatic.value && isStatic.value),
- isCollapsed: computed(
- () =>
- !isStatic.value && uiStore.isModalActiveById[CHAT_HUB_SIDE_MENU_DRAWER_MODAL_KEY] !== true,
- ),
- toggleOpen,
- toggleStatic,
- };
-}
diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/constants.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/constants.ts
index 0770da1935a..57078e7b709 100644
--- a/packages/frontend/editor-ui/src/features/ai/chatHub/constants.ts
+++ b/packages/frontend/editor-ui/src/features/ai/chatHub/constants.ts
@@ -8,6 +8,7 @@ export const CHAT_PERSONAL_AGENTS_VIEW = 'chat-personal-agents';
export const CHAT_SETTINGS_VIEW = 'chat-settings';
export const CHAT_STORE = 'chatStore';
+export const CHAT_SESSIONS_PAGE_SIZE = 18;
export const providerDisplayNames: Record = {
openai: 'OpenAI',
diff --git a/packages/frontend/editor-ui/src/features/collaboration/projects/components/ProjectNavigation.vue b/packages/frontend/editor-ui/src/features/collaboration/projects/components/ProjectNavigation.vue
index 0edc79c71ea..761867db820 100644
--- a/packages/frontend/editor-ui/src/features/collaboration/projects/components/ProjectNavigation.vue
+++ b/packages/frontend/editor-ui/src/features/collaboration/projects/components/ProjectNavigation.vue
@@ -95,6 +95,7 @@ const chat = computed(() => ({
label: locale.baseText('projects.menu.chat'),
position: 'bottom',
route: { to: { name: CHAT_VIEW } },
+ beta: true,
}));
async function onSourceControlPull() {
diff --git a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useChatHubCommands.ts b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useChatHubCommands.ts
index 0164581d24c..84e177d9eca 100644
--- a/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useChatHubCommands.ts
+++ b/packages/frontend/editor-ui/src/features/shared/commandBar/composables/useChatHubCommands.ts
@@ -212,7 +212,7 @@ export function useChatHubCommands(options: {
icon: {
component: N8nIcon,
props: {
- icon: 'square-pen',
+ icon: 'plus',
},
},
keywords: [
@@ -230,7 +230,7 @@ export function useChatHubCommands(options: {
icon: {
component: N8nIcon,
props: {
- icon: 'robot',
+ icon: 'message-square-plus',
},
},
keywords: [
@@ -249,7 +249,7 @@ export function useChatHubCommands(options: {
icon: {
component: N8nIcon,
props: {
- icon: 'layers',
+ icon: 'message-square',
color: 'text-light',
},
},