diff --git a/packages/@n8n/api-types/src/chat-hub.ts b/packages/@n8n/api-types/src/chat-hub.ts index 83d77e1abfa..4379e8a1d7b 100644 --- a/packages/@n8n/api-types/src/chat-hub.ts +++ b/packages/@n8n/api-types/src/chat-hub.ts @@ -89,6 +89,10 @@ export class ChatHubEditMessageRequest extends Z.class({ ), }) {} +export class ChatHubChangeConversationTitleRequest extends Z.class({ + title: z.string(), +}) {} + export type ChatHubMessageType = 'human' | 'ai' | 'system' | 'tool' | 'generic'; export type ChatHubMessageState = 'active' | 'replaced'; diff --git a/packages/@n8n/api-types/src/index.ts b/packages/@n8n/api-types/src/index.ts index 23fe5ce10af..5ab3a6452bf 100644 --- a/packages/@n8n/api-types/src/index.ts +++ b/packages/@n8n/api-types/src/index.ts @@ -20,6 +20,7 @@ export { ChatHubSendMessageRequest, ChatHubRegenerateMessageRequest, ChatHubEditMessageRequest, + ChatHubChangeConversationTitleRequest, type ChatMessageId, type ChatSessionId, type ChatHubMessageDto, 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 cc684cf14c0..314bde751e9 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.controller.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.controller.ts @@ -5,10 +5,11 @@ import { ChatHubConversationResponse, ChatHubEditMessageRequest, ChatHubRegenerateMessageRequest, + ChatHubChangeConversationTitleRequest, } from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; import { AuthenticatedRequest } from '@n8n/db'; -import { RestController, Post, Body, GlobalScope, Get } from '@n8n/decorators'; +import { RestController, Post, Body, GlobalScope, Get, Delete } from '@n8n/decorators'; import type { Response } from 'express'; import { strict as assert } from 'node:assert'; @@ -184,4 +185,25 @@ export class ChatHubController { if (!res.writableEnded) res.end(); } } + + @Post('/conversations/:id/rename') + @GlobalScope('chatHub:message') + async updateConversationTitle( + req: AuthenticatedRequest<{ id: string }>, + _res: Response, + @Body payload: ChatHubChangeConversationTitleRequest, + ): Promise { + await this.chatService.updateSessionTitle(req.user.id, req.params.id, payload.title); + + return await this.chatService.getConversation(req.user.id, req.params.id); + } + + @Delete('/conversations/:id') + @GlobalScope('chatHub:message') + async deleteConversation( + req: AuthenticatedRequest<{ id: string }>, + _res: Response, + ): Promise { + await this.chatService.deleteSession(req.user.id, req.params.id); + } } 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 b553f758b04..ae159a7f9bc 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.service.ts @@ -943,7 +943,32 @@ export class ChatHubService { async deleteAllSessions() { const result = await this.sessionRepository.deleteAll(); - return result; } + + /** + * Updates the title of a session + */ + async updateSessionTitle(userId: string, sessionId: ChatSessionId, title: string) { + const session = await this.sessionRepository.getOneById(sessionId, userId); + + if (!session) { + throw new NotFoundError('Session not found'); + } + + return await this.sessionRepository.updateChatTitle(sessionId, title); + } + + /** + * Deletes a session + */ + async deleteSession(userId: string, sessionId: ChatSessionId) { + const session = await this.sessionRepository.getOneById(sessionId, userId); + + if (!session) { + throw new NotFoundError('Session not found'); + } + + await this.sessionRepository.deleteChatHubSession(sessionId); + } } diff --git a/packages/frontend/editor-ui/src/features/chatHub/chat.api.ts b/packages/frontend/editor-ui/src/features/chatHub/chat.api.ts index 9c45b3ae6a6..6423186df1e 100644 --- a/packages/frontend/editor-ui/src/features/chatHub/chat.api.ts +++ b/packages/frontend/editor-ui/src/features/chatHub/chat.api.ts @@ -8,6 +8,7 @@ import type { ChatHubConversationResponse, ChatHubRegenerateMessageRequest, ChatHubEditMessageRequest, + ChatSessionId, } from '@n8n/api-types'; import type { StructuredChunk } from './chat.types'; @@ -83,9 +84,28 @@ export const fetchConversationsApi = async ( return await makeRestApiRequest(context, 'GET', apiEndpoint); }; +export const updateConversationTitleApi = async ( + context: IRestApiContext, + conversationId: ChatSessionId, + title: string, +): Promise => { + const apiEndpoint = `/chat/conversations/${conversationId}/rename`; + return await makeRestApiRequest(context, 'POST', apiEndpoint, { + title, + }); +}; + +export const deleteConversationApi = async ( + context: IRestApiContext, + conversationId: ChatSessionId, +): Promise => { + const apiEndpoint = `/chat/conversations/${conversationId}`; + await makeRestApiRequest(context, 'DELETE', apiEndpoint); +}; + export const fetchSingleConversationApi = async ( context: IRestApiContext, - conversationId: string, + conversationId: ChatSessionId, ): Promise => { const apiEndpoint = `/chat/conversations/${conversationId}`; return await makeRestApiRequest(context, 'GET', apiEndpoint); diff --git a/packages/frontend/editor-ui/src/features/chatHub/chat.store.ts b/packages/frontend/editor-ui/src/features/chatHub/chat.store.ts index cab1a582aba..31292607f11 100644 --- a/packages/frontend/editor-ui/src/features/chatHub/chat.store.ts +++ b/packages/frontend/editor-ui/src/features/chatHub/chat.store.ts @@ -9,6 +9,8 @@ import { regenerateMessageApi, fetchConversationsApi as fetchSessionsApi, fetchSingleConversationApi as fetchMessagesApi, + updateConversationTitleApi, + deleteConversationApi, } from './chat.api'; import { useRootStore } from '@n8n/stores/useRootStore'; import type { @@ -417,20 +419,18 @@ export const useChatStore = defineStore(CHAT_STORE, () => { ); } - async function renameSession(sessionId: string, name: string) { - // Optimistic update - sessions.value = sessions.value.map((session) => - session.id === sessionId ? { ...session, title: name } : session, - ); + async function renameSession(sessionId: ChatSessionId, title: string) { + const updated = await updateConversationTitleApi(rootStore.restApiContext, sessionId, title); - // TODO: call the endpoint + sessions.value = sessions.value.map((session) => + session.id === sessionId ? updated.session : session, + ); } - async function deleteSession(sessionId: string) { - // Optimistic update - sessions.value = sessions.value.filter((session) => session.id !== sessionId); + async function deleteSession(sessionId: ChatSessionId) { + await deleteConversationApi(rootStore.restApiContext, sessionId); - // TODO: call the endpoint + sessions.value = sessions.value.filter((session) => session.id !== sessionId); } return { diff --git a/packages/frontend/editor-ui/src/features/chatHub/components/ChatSidebarContent.vue b/packages/frontend/editor-ui/src/features/chatHub/components/ChatSidebarContent.vue index 98a51209cfc..62da23b378b 100644 --- a/packages/frontend/editor-ui/src/features/chatHub/components/ChatSidebarContent.vue +++ b/packages/frontend/editor-ui/src/features/chatHub/components/ChatSidebarContent.vue @@ -53,8 +53,12 @@ function handleCancelRename() { } async function handleConfirmRename(sessionId: string, newLabel: string) { - await chatStore.renameSession(sessionId, newLabel); - renamingSessionId.value = undefined; + try { + await chatStore.renameSession(sessionId, newLabel); + renamingSessionId.value = undefined; + } catch (error) { + toast.showError(error, 'Could not update the conversation title.'); + } } async function handleDeleteSession(sessionId: string) { @@ -71,8 +75,16 @@ async function handleDeleteSession(sessionId: string) { return; } - await chatStore.deleteSession(sessionId); - toast.showMessage({ type: 'success', title: 'Conversation is deleted' }); + try { + await chatStore.deleteSession(sessionId); + toast.showMessage({ type: 'success', title: 'Conversation is deleted' }); + + if (sessionId === currentSessionId.value) { + void router.push({ name: CHAT_VIEW }); + } + } catch (error) { + toast.showError(error, 'Could not delete the conversation'); + } } onMounted(async () => {