feat: Make deleting and renaming conversations work end-to-end (no-changelog) (#20846)

This commit is contained in:
Suguru Inoue 2025-10-16 12:31:04 +02:00 committed by GitHub
parent ff309d249b
commit aa1df2b568
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 101 additions and 17 deletions

View File

@ -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';

View File

@ -20,6 +20,7 @@ export {
ChatHubSendMessageRequest,
ChatHubRegenerateMessageRequest,
ChatHubEditMessageRequest,
ChatHubChangeConversationTitleRequest,
type ChatMessageId,
type ChatSessionId,
type ChatHubMessageDto,

View File

@ -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<ChatHubConversationResponse> {
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<void> {
await this.chatService.deleteSession(req.user.id, req.params.id);
}
}

View File

@ -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);
}
}

View File

@ -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<ChatHubConversationsResponse>(context, 'GET', apiEndpoint);
};
export const updateConversationTitleApi = async (
context: IRestApiContext,
conversationId: ChatSessionId,
title: string,
): Promise<ChatHubConversationResponse> => {
const apiEndpoint = `/chat/conversations/${conversationId}/rename`;
return await makeRestApiRequest<ChatHubConversationResponse>(context, 'POST', apiEndpoint, {
title,
});
};
export const deleteConversationApi = async (
context: IRestApiContext,
conversationId: ChatSessionId,
): Promise<void> => {
const apiEndpoint = `/chat/conversations/${conversationId}`;
await makeRestApiRequest(context, 'DELETE', apiEndpoint);
};
export const fetchSingleConversationApi = async (
context: IRestApiContext,
conversationId: string,
conversationId: ChatSessionId,
): Promise<ChatHubConversationResponse> => {
const apiEndpoint = `/chat/conversations/${conversationId}`;
return await makeRestApiRequest<ChatHubConversationResponse>(context, 'GET', apiEndpoint);

View File

@ -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 {

View File

@ -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 () => {