From 11e111e372c7d2c8a606fb26af568aa21dcf7b34 Mon Sep 17 00:00:00 2001 From: Suguru Inoue Date: Thu, 20 Nov 2025 14:55:14 +0100 Subject: [PATCH] perf(editor): Render chat sidebar faster (no-changelog) (#22039) --- packages/@n8n/api-types/src/chat-hub.ts | 11 +- packages/@n8n/api-types/src/index.ts | 1 + .../chat-hub.service.integration.test.ts | 185 +++++++++++++++++- .../modules/chat-hub/chat-hub.controller.ts | 5 +- .../src/modules/chat-hub/chat-hub.service.ts | 46 +++-- .../chat-hub/chat-session.repository.ts | 34 +++- .../src/features/ai/chatHub/ChatView.vue | 16 +- .../src/features/ai/chatHub/chat.api.ts | 10 +- .../src/features/ai/chatHub/chat.store.ts | 104 +++++++--- .../chatHub/components/AgentEditorModal.vue | 2 +- .../ai/chatHub/components/ChatMessage.vue | 12 +- .../components/ChatSessionMenuItem.vue | 30 ++- .../chatHub/components/ChatSidebarContent.vue | 42 +++- .../chatHub/components/SkeletonMenuItem.vue | 46 +++++ .../ai/chatHub/composables/useAgent.ts | 15 -- 15 files changed, 463 insertions(+), 96 deletions(-) create mode 100644 packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonMenuItem.vue delete mode 100644 packages/frontend/editor-ui/src/features/ai/chatHub/composables/useAgent.ts diff --git a/packages/@n8n/api-types/src/chat-hub.ts b/packages/@n8n/api-types/src/chat-hub.ts index b994559b576..486fae50ddc 100644 --- a/packages/@n8n/api-types/src/chat-hub.ts +++ b/packages/@n8n/api-types/src/chat-hub.ts @@ -275,7 +275,16 @@ export interface ChatHubMessageDto { attachments: Array<{ fileName?: string; mimeType?: string }>; } -export type ChatHubConversationsResponse = ChatHubSessionDto[]; +export class ChatHubConversationsRequest extends Z.class({ + limit: z.coerce.number().int().min(1).max(100), + cursor: z.string().uuid().optional(), +}) {} + +export interface ChatHubConversationsResponse { + data: ChatHubSessionDto[]; + nextCursor: string | null; + hasMore: boolean; +} export interface ChatHubConversationDto { messages: Record; diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts index 22f31ddc50a..1f2646f4518 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -33,6 +33,7 @@ export { ChatHubRegenerateMessageRequest, ChatHubEditMessageRequest, ChatHubUpdateConversationRequest, + ChatHubConversationsRequest, type ChatMessageId, type ChatSessionId, type ChatHubMessageDto, diff --git a/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts b/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts index 8b9d3d47491..5df244e1f34 100644 --- a/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts +++ b/packages/cli/src/modules/chat-hub/__tests__/chat-hub.service.integration.test.ts @@ -48,9 +48,9 @@ describe('chatHub', () => { describe('getConversations', () => { it('should list empty conversations', async () => { - const conversations = await chatHubService.getConversations(member.id); + const conversations = await chatHubService.getConversations(member.id, 20); expect(conversations).toBeDefined(); - expect(conversations).toHaveLength(0); + expect(conversations.data).toHaveLength(0); }); it("should list user's own conversations in expected order", async () => { @@ -83,11 +83,182 @@ describe('chatHub', () => { tools: [], }); - const conversations = await chatHubService.getConversations(member.id); - expect(conversations).toHaveLength(3); - expect(conversations[0].id).toBe(session1.id); - expect(conversations[1].id).toBe(session2.id); - expect(conversations[2].id).toBe(session3.id); + const conversations = await chatHubService.getConversations(member.id, 20); + expect(conversations.data).toHaveLength(3); + expect(conversations.data[0].id).toBe(session1.id); + expect(conversations.data[1].id).toBe(session2.id); + expect(conversations.data[2].id).toBe(session3.id); + }); + + describe('pagination', () => { + it('should return hasMore=false and nextCursor=null when all sessions fit in one page', async () => { + await sessionsRepository.createChatSession({ + id: crypto.randomUUID(), + ownerId: member.id, + title: 'session 1', + lastMessageAt: new Date('2025-01-01T00:00:00Z'), + tools: [], + }); + + const conversations = await chatHubService.getConversations(member.id, 10); + + expect(conversations.data).toHaveLength(1); + expect(conversations.hasMore).toBe(false); + expect(conversations.nextCursor).toBeNull(); + }); + + it('should fetch next page using cursor', async () => { + const session1 = await sessionsRepository.createChatSession({ + id: crypto.randomUUID(), + ownerId: member.id, + title: 'session 1', + lastMessageAt: new Date('2025-01-05T00:00:00Z'), + tools: [], + }); + + const session2 = await sessionsRepository.createChatSession({ + id: crypto.randomUUID(), + ownerId: member.id, + title: 'session 2', + lastMessageAt: new Date('2025-01-04T00:00:00Z'), + tools: [], + }); + + const session3 = await sessionsRepository.createChatSession({ + id: crypto.randomUUID(), + ownerId: member.id, + title: 'session 3', + lastMessageAt: new Date('2025-01-03T00:00:00Z'), + tools: [], + }); + + const session4 = await sessionsRepository.createChatSession({ + id: crypto.randomUUID(), + ownerId: member.id, + title: 'session 4', + lastMessageAt: new Date('2025-01-02T00:00:00Z'), + tools: [], + }); + + // First page + const page1 = await chatHubService.getConversations(member.id, 2); + expect(page1.data).toHaveLength(2); + expect(page1.data[0].id).toBe(session1.id); + expect(page1.data[1].id).toBe(session2.id); + expect(page1.hasMore).toBe(true); + expect(page1.nextCursor).toBe(session2.id); + + // Second page using cursor + const page2 = await chatHubService.getConversations(member.id, 2, page1.nextCursor!); + expect(page2.data).toHaveLength(2); + expect(page2.data[0].id).toBe(session3.id); + expect(page2.data[1].id).toBe(session4.id); + expect(page2.hasMore).toBe(false); + expect(page2.nextCursor).toBeNull(); + }); + + it('should handle sessions with same lastMessageAt using id for ordering', async () => { + const sameDate = new Date('2025-01-01T00:00:00Z'); + + const session1 = await sessionsRepository.createChatSession({ + id: '00000000-0000-0000-0000-000000000001', + ownerId: member.id, + title: 'Session 1', + lastMessageAt: sameDate, + tools: [], + }); + + const session2 = await sessionsRepository.createChatSession({ + id: '00000000-0000-0000-0000-000000000002', + ownerId: member.id, + title: 'Session 2', + lastMessageAt: sameDate, + tools: [], + }); + + const session3 = await sessionsRepository.createChatSession({ + id: '00000000-0000-0000-0000-000000000003', + ownerId: member.id, + title: 'Session 3', + lastMessageAt: sameDate, + tools: [], + }); + + // Fetch first page + const page1 = await chatHubService.getConversations(member.id, 2); + expect(page1.data).toHaveLength(2); + expect(page1.data[0].id).toBe(session1.id); + expect(page1.data[1].id).toBe(session2.id); + expect(page1.hasMore).toBe(true); + + // Fetch second page + const page2 = await chatHubService.getConversations(member.id, 2, page1.nextCursor!); + expect(page2.data).toHaveLength(1); + expect(page2.data[0].id).toBe(session3.id); + expect(page2.hasMore).toBe(false); + }); + + it('should throw error when cursor session does not exist', async () => { + await sessionsRepository.createChatSession({ + id: crypto.randomUUID(), + ownerId: member.id, + title: 'session 1', + lastMessageAt: new Date('2025-01-01T00:00:00Z'), + tools: [], + }); + + const nonExistentCursor = '00000000-0000-0000-0000-000000000000'; + + await expect( + chatHubService.getConversations(member.id, 10, nonExistentCursor), + ).rejects.toThrow('Cursor session not found'); + }); + + it('should throw error when cursor session belongs to different user', async () => { + await sessionsRepository.createChatSession({ + id: crypto.randomUUID(), + ownerId: member.id, + title: 'Member Session', + lastMessageAt: new Date('2025-01-02T00:00:00Z'), + tools: [], + }); + + const adminSession = await sessionsRepository.createChatSession({ + id: crypto.randomUUID(), + ownerId: admin.id, + title: 'Admin Session', + lastMessageAt: new Date('2025-01-01T00:00:00Z'), + tools: [], + }); + + await expect( + chatHubService.getConversations(member.id, 10, adminSession.id), + ).rejects.toThrow('Cursor session not found'); + }); + + it('should handle sessions with null lastMessageAt', async () => { + const session1 = await sessionsRepository.createChatSession({ + id: crypto.randomUUID(), + ownerId: member.id, + title: 'Session with date', + lastMessageAt: new Date('2025-01-01T00:00:00Z'), + tools: [], + }); + + const session2 = await sessionsRepository.createChatSession({ + id: crypto.randomUUID(), + ownerId: member.id, + title: 'Session without date', + lastMessageAt: null, + tools: [], + }); + + const conversations = await chatHubService.getConversations(member.id, 10); + + expect(conversations.data).toHaveLength(2); + expect(conversations.data[0].id).toBe(session1.id); + expect(conversations.data[1].id).toBe(session2.id); + }); }); }); diff --git a/packages/cli/src/modules/chat-hub/chat-hub.controller.ts b/packages/cli/src/modules/chat-hub/chat-hub.controller.ts index 5037c96fd85..e5be73b40e2 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.controller.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.controller.ts @@ -10,6 +10,7 @@ import { ChatMessageId, ChatHubCreateAgentRequest, ChatHubUpdateAgentRequest, + ChatHubConversationsRequest, } from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; import { AuthenticatedRequest } from '@n8n/db'; @@ -22,6 +23,7 @@ import { Delete, Param, Patch, + Query, } from '@n8n/decorators'; import type { Response } from 'express'; import { strict as assert } from 'node:assert'; @@ -59,8 +61,9 @@ export class ChatHubController { async getConversations( req: AuthenticatedRequest, _res: Response, + @Query query: ChatHubConversationsRequest, ): Promise { - return await this.chatService.getConversations(req.user.id); + return await this.chatService.getConversations(req.user.id, query.limit, query.cursor); } @Get('/conversations/:sessionId') diff --git a/packages/cli/src/modules/chat-hub/chat-hub.service.ts b/packages/cli/src/modules/chat-hub/chat-hub.service.ts index 7d2fcb9d273..d55e45bf45d 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.service.ts @@ -1683,24 +1683,36 @@ export class ChatHubService { /** * Get all conversations for a user */ - async getConversations(userId: string): Promise { - const sessions = await this.sessionRepository.getManyByUserId(userId); + async getConversations( + userId: string, + limit: number, + cursor?: string, + ): Promise { + const sessions = await this.sessionRepository.getManyByUserId(userId, limit + 1, cursor); - return sessions.map((session) => ({ - id: session.id, - title: session.title, - ownerId: session.ownerId, - lastMessageAt: session.lastMessageAt?.toISOString() ?? null, - credentialId: session.credentialId, - provider: session.provider, - model: session.model, - workflowId: session.workflowId, - agentId: session.agentId, - agentName: session.agentName, - createdAt: session.createdAt.toISOString(), - updatedAt: session.updatedAt.toISOString(), - tools: session.tools, - })); + const hasMore = sessions.length > limit; + const data = hasMore ? sessions.slice(0, limit) : sessions; + const nextCursor = hasMore ? data[data.length - 1].id : null; + + return { + data: data.map((session) => ({ + id: session.id, + title: session.title, + ownerId: session.ownerId, + lastMessageAt: session.lastMessageAt?.toISOString() ?? null, + credentialId: session.credentialId, + provider: session.provider, + model: session.model, + workflowId: session.workflowId, + agentId: session.agentId, + agentName: session.agentName, + createdAt: session.createdAt.toISOString(), + updatedAt: session.updatedAt.toISOString(), + tools: session.tools, + })), + nextCursor, + hasMore, + }; } /** diff --git a/packages/cli/src/modules/chat-hub/chat-session.repository.ts b/packages/cli/src/modules/chat-hub/chat-session.repository.ts index b216dc9b310..3fa513cbfe2 100644 --- a/packages/cli/src/modules/chat-hub/chat-session.repository.ts +++ b/packages/cli/src/modules/chat-hub/chat-session.repository.ts @@ -2,6 +2,8 @@ import { withTransaction } from '@n8n/db'; import { Service } from '@n8n/di'; import { DataSource, EntityManager, Repository } from '@n8n/typeorm'; +import { NotFoundError } from '@/errors/response-errors/not-found.error'; + import { ChatHubSession } from './chat-hub-session.entity'; @Service() @@ -56,11 +58,33 @@ export class ChatHubSessionRepository extends Repository { }); } - async getManyByUserId(userId: string) { - return await this.find({ - where: { ownerId: userId }, - order: { lastMessageAt: 'DESC', id: 'ASC' }, - }); + async getManyByUserId(userId: string, limit: number, cursor?: string) { + const queryBuilder = this.createQueryBuilder('session') + .where('session.ownerId = :userId', { userId }) + .orderBy("COALESCE(session.lastMessageAt, '1970-01-01')", 'DESC') + .addOrderBy('session.id', 'ASC'); + + if (cursor) { + const cursorSession = await this.findOne({ + where: { id: cursor, ownerId: userId }, + }); + + if (!cursorSession) { + throw new NotFoundError('Cursor session not found'); + } + + queryBuilder.andWhere( + '(session.lastMessageAt < :lastMessageAt OR (session.lastMessageAt = :lastMessageAt AND session.id > :id))', + { + lastMessageAt: cursorSession.lastMessageAt, + id: cursorSession.id, + }, + ); + } + + queryBuilder.take(limit); + + return await queryBuilder.getMany(); } async getOneById(id: string, userId: string, trx?: EntityManager) { 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 c1965208d96..4971fa908db 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/ChatView.vue @@ -141,19 +141,19 @@ const modelFromQuery = computed(() => { } if (typeof agentId === 'string') { - return chatStore.getAgent({ provider: 'custom-agent', agentId }) ?? null; + return chatStore.getAgent({ provider: 'custom-agent', agentId }); } if (typeof workflowId === 'string') { - return chatStore.getAgent({ provider: 'n8n', workflowId }) ?? null; + return chatStore.getAgent({ provider: 'n8n', workflowId }); } return null; }); -const selectedModel = computed(() => { +const selectedModel = computed(() => { if (!chatStore.agentsReady) { - return undefined; + return null; } if (modelFromQuery.value) { @@ -163,14 +163,14 @@ const selectedModel = computed(() => { if (currentConversation.value?.provider) { const model = unflattenModel(currentConversation.value); - return model ? chatStore.getAgent(model) : undefined; + return model ? chatStore.getAgent(model) : null; } if (chatStore.streaming?.sessionId === sessionId.value) { return chatStore.getAgent(chatStore.streaming.model); } - return defaultModel.value ? chatStore.getAgent(defaultModel.value) : undefined; + return defaultModel.value ? chatStore.getAgent(defaultModel.value) : null; }); const { credentialsByProvider, selectCredential } = useChatCredentials( @@ -501,7 +501,7 @@ function onFilesDropped(files: File[]) { => { - const apiEndpoint = '/chat/conversations'; + const queryParams = new URLSearchParams(); + queryParams.append('limit', limit.toString()); + if (cursor) { + queryParams.append('cursor', cursor); + } + + const apiEndpoint = `/chat/conversations?${queryParams.toString()}`; return await makeRestApiRequest(context, 'GET', apiEndpoint); }; 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 939f6f4acd0..9d38c3905a9 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 @@ -35,6 +35,7 @@ import { type EnrichedStructuredChunk, type ChatHubMessageStatus, type ChatModelDto, + type ChatHubConversationsResponse, } from '@n8n/api-types'; import type { CredentialsMap, @@ -56,7 +57,9 @@ export const useChatStore = defineStore(CHAT_STORE, () => { const telemetry = useTelemetry(); const agents = ref(); - const sessions = ref(); + const sessions = ref(); + const sessionsLoadingMore = ref(false); + const currentEditingAgent = ref(null); const streaming = ref(); @@ -270,8 +273,39 @@ export const useChatStore = defineStore(CHAT_STORE, () => { return agents.value; } - async function fetchSessions() { - sessions.value = await fetchSessionsApi(rootStore.restApiContext); + async function fetchSessions(reset: boolean) { + if (sessionsLoadingMore.value) { + return; + } + + if (!reset && sessions.value && !sessions.value.hasMore && sessions.value.data.length > 0) { + return; + } + + if (!reset) { + sessionsLoadingMore.value = true; + } + + try { + const cursor = reset ? undefined : (sessions.value?.nextCursor ?? undefined); + const [response] = await Promise.all([ + fetchSessionsApi(rootStore.restApiContext, 40, cursor), + new Promise((resolve) => setTimeout(resolve, 500)), + ]); + + sessions.value = { + ...response, + data: [...(reset ? [] : (sessions.value?.data ?? [])), ...response.data], + }; + } finally { + sessionsLoadingMore.value = false; + } + } + + async function fetchMoreSessions() { + if (sessions.value?.hasMore && !sessionsLoadingMore.value) { + await fetchSessions(false); + } } async function fetchMessages(sessionId: string) { @@ -302,25 +336,30 @@ export const useChatStore = defineStore(CHAT_STORE, () => { addMessage(streaming.value.sessionId, message); - if (sessions.value?.some((session) => session.id === streaming.value?.sessionId)) { + if (sessions.value?.data.some((session) => session.id === streaming.value?.sessionId)) { return; } - sessions.value = [ - ...(sessions.value ?? []), - { - id: streaming.value.sessionId, - title: 'New Chat', - ownerId: '', - lastMessageAt: new Date().toISOString(), - credentialId: null, - agentName: null, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - tools: [], - ...flattenModel(streaming.value.model), - }, - ]; + sessions.value = { + hasMore: false, + nextCursor: null, + ...sessions.value, + data: [ + ...(sessions.value?.data ?? []), + { + id: streaming.value.sessionId, + title: 'New Chat', + ownerId: '', + lastMessageAt: new Date().toISOString(), + credentialId: null, + agentName: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + tools: [], + ...flattenModel(streaming.value.model), + }, + ], + }; } function ensureMessage(sessionId: ChatSessionId, messageId: ChatMessageId): ChatMessage { @@ -402,8 +441,8 @@ export const useChatStore = defineStore(CHAT_STORE, () => { 3, ); - // update the conversation list - await fetchSessions(); + // update the conversation list to reflect the new title + await fetchSessions(true); } function onStreamError(error: Error) { @@ -607,7 +646,11 @@ export const useChatStore = defineStore(CHAT_STORE, () => { } function updateSession(sessionId: ChatSessionId, toUpdate: Partial) { - sessions.value = sessions.value?.map((session) => + if (!sessions.value) { + return; + } + + sessions.value.data = sessions.value.data?.map((session) => session.id === sessionId ? { ...session, @@ -618,7 +661,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => { } async function updateToolsInSession(sessionId: ChatSessionId, tools: INode[]) { - const session = sessions.value?.find((s) => s.id === sessionId); + const session = sessions.value?.data?.find((s) => s.id === sessionId); if (!session) { throw new Error(`Session with ID ${sessionId} not found`); } @@ -644,7 +687,12 @@ export const useChatStore = defineStore(CHAT_STORE, () => { async function deleteSession(sessionId: ChatSessionId) { await deleteConversationApi(rootStore.restApiContext, sessionId); - sessions.value = sessions.value?.filter((session) => session.id !== sessionId); + if (sessions.value) { + sessions.value = { + ...sessions.value, + data: sessions.value.data?.filter((session) => session.id !== sessionId), + }; + } } function switchAlternative(sessionId: ChatSessionId, messageId: ChatMessageId) { @@ -730,13 +778,13 @@ export const useChatStore = defineStore(CHAT_STORE, () => { } function getAgent(model: ChatHubConversationModel) { - if (!agents.value) return; + if (!agents.value) return null; const agent = agents.value[model.provider].models.find((agent) => isMatchedAgent(agent, model)); if (!agent) { if (model.provider === 'custom-agent' || model.provider === 'n8n') { - return; + return null; } // Allow custom models chosen by ID even if they are not in the fetched list @@ -774,9 +822,11 @@ export const useChatStore = defineStore(CHAT_STORE, () => { /** * conversations */ - sessions: computed(() => sessions.value ?? []), + sessions: computed(() => sessions.value?.data ?? []), sessionsReady: computed(() => sessions.value !== undefined), + sessionsLoading: computed(() => sessionsLoadingMore.value), fetchSessions, + fetchMoreSessions, renameSession, updateSessionModel, deleteSession, 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 e9e2fea7fc3..10e36581ac1 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 @@ -75,7 +75,7 @@ function loadAgent() { name.value = customAgent.name; description.value = customAgent.description ?? ''; systemPrompt.value = customAgent.systemPrompt; - selectedModel.value = chatStore.getAgent(customAgent) ?? null; + selectedModel.value = chatStore.getAgent(customAgent); tools.value = customAgent.tools || []; if (customAgent.credentialId) { diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatMessage.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatMessage.vue index 34d8190d4ed..6eab2e15e91 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatMessage.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatMessage.vue @@ -3,7 +3,7 @@ import { useClipboard } from '@/app/composables/useClipboard'; import ChatAgentAvatar from '@/features/ai/chatHub/components/ChatAgentAvatar.vue'; import ChatTypingIndicator from '@/features/ai/chatHub/components/ChatTypingIndicator.vue'; import { useChatHubMarkdownOptions } from '@/features/ai/chatHub/composables/useChatHubMarkdownOptions'; -import type { ChatMessageId } from '@n8n/api-types'; +import type { ChatMessageId, ChatModelDto } from '@n8n/api-types'; import { N8nButton, N8nIcon, N8nInput } from '@n8n/design-system'; import { useSpeechSynthesis } from '@vueuse/core'; import type MarkdownIt from 'markdown-it'; @@ -13,7 +13,7 @@ import VueMarkdown from 'vue-markdown-render'; import type { ChatMessage } from '../chat.types'; import ChatMessageActions from './ChatMessageActions.vue'; import { unflattenModel } from '@/features/ai/chatHub/chat.utils'; -import { useAgent } from '@/features/ai/chatHub/composables/useAgent'; +import { useChatStore } from '@/features/ai/chatHub/chat.store'; import ChatFile from '@n8n/chat/components/ChatFile.vue'; import { buildChatAttachmentUrl } from '@/features/ai/chatHub/chat.api'; import { useRootStore } from '@n8n/stores/useRootStore'; @@ -38,6 +38,7 @@ const emit = defineEmits<{ }>(); const clipboard = useClipboard(); +const chatStore = useChatStore(); const rootStore = useRootStore(); const editedText = ref(''); @@ -52,8 +53,11 @@ const speech = useSpeechSynthesis(messageContent, { volume: 1, }); -const model = computed(() => unflattenModel(message)); -const agent = useAgent(model); +const agent = computed(() => { + const model = unflattenModel(message); + + return model ? chatStore.getAgent(model) : null; +}); const attachments = computed(() => message.attachments.map(({ fileName, mimeType }, index) => ({ diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSessionMenuItem.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSessionMenuItem.vue index 69bcb45d483..e71a8b259c4 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSessionMenuItem.vue +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/ChatSessionMenuItem.vue @@ -1,10 +1,10 @@ @@ -131,8 +145,18 @@ onMounted(() => { /> -
-
+
+
+ +
+
{{ group.group }} @@ -147,7 +171,12 @@ onMounted(() => { @confirm-rename="handleConfirmRename" @delete="handleDeleteSession" /> +
+ +
@@ -203,6 +232,11 @@ onMounted(() => { 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/SkeletonMenuItem.vue b/packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonMenuItem.vue new file mode 100644 index 00000000000..0bb40200df4 --- /dev/null +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/components/SkeletonMenuItem.vue @@ -0,0 +1,46 @@ + + + diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/composables/useAgent.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/composables/useAgent.ts deleted file mode 100644 index c3370ae2e04..00000000000 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/composables/useAgent.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ChatHubConversationModel, ChatModelDto } from '@n8n/api-types'; -import { computed, type ComputedRef, type MaybeRef, toValue } from 'vue'; -import { useChatStore } from '../chat.store'; - -export function useAgent( - model: MaybeRef, -): ComputedRef { - const chatStore = useChatStore(); - - return computed(() => { - const modelValue = toValue(model); - - return modelValue ? chatStore.getAgent(modelValue) : undefined; - }); -}