feat(core): Make it possible to edit AI messages (no-changelog) (#21000)

This commit is contained in:
Jaakko Husso 2025-10-21 12:56:04 +03:00 committed by GitHub
parent 13614542eb
commit ef9e32b27a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 69 additions and 30 deletions

View File

@ -119,7 +119,7 @@ export class ChatHubController {
this.logger.debug(`Chat edit request received: ${JSON.stringify(payload)}`);
try {
await this.chatService.editHumanMessage(res, req.user, {
await this.chatService.editMessage(res, req.user, {
...payload,
sessionId,
editId,

View File

@ -62,6 +62,7 @@ import type {
import { ChatHubMessageRepository } from './chat-message.repository';
import { ChatHubSessionRepository } from './chat-session.repository';
import { getMaxContextWindowTokens } from './context-limits';
import { ChatHubSession } from './chat-hub-session.entity';
const providerNodeTypeMapping: Record<ChatHubProvider, INodeTypeNameVersion> = {
openai: {
@ -404,8 +405,8 @@ export class ChatHubService {
}
}
async editHumanMessage(res: Response, user: User, payload: EditMessagePayload) {
const { sessionId, editId, messageId, message, replyId } = payload;
async editMessage(res: Response, user: User, payload: EditMessagePayload) {
const { sessionId, editId } = payload;
const selectedModel: ModelWithCredentials = {
...payload.model,
credentialId: this.getCredentialId(payload.model.provider, payload.credentials),
@ -414,10 +415,24 @@ export class ChatHubService {
const session = await this.getChatSession(user, sessionId);
const messageToEdit = await this.getChatMessage(session.id, editId);
if (messageToEdit.type !== 'human') {
throw new BadRequestError('Can only edit human messages');
if (messageToEdit.type === 'human') {
await this.editHumanMessage(res, user, payload, session, messageToEdit, selectedModel);
} else if (messageToEdit.type === 'ai') {
await this.editAIMessage(payload.message, editId);
} else {
throw new BadRequestError('Only human and AI messages can be edited');
}
}
private async editHumanMessage(
res: Response,
user: User,
payload: EditMessagePayload,
session: ChatHubSession,
messageToEdit: ChatHubMessage,
selectedModel: ModelWithCredentials,
) {
const { sessionId, messageId, message, replyId } = payload;
const messages = Object.fromEntries((session.messages ?? []).map((m) => [m.id, m]));
const history = this.buildMessageHistory(messages, messageToEdit.previousMessageId);
@ -456,6 +471,11 @@ export class ChatHubService {
}
}
private async editAIMessage(content: string, messageId: ChatMessageId) {
// AI edits just change the original message without revisioning
await this.messageRepository.updateChatMessage(messageId, { content });
}
async regenerateAIMessage(res: Response, user: User, payload: RegenerateMessagePayload) {
const { sessionId, retryId, replyId } = payload;

View File

@ -26,7 +26,7 @@ export class ChatHubMessageRepository extends Repository<ChatHubMessage> {
async updateChatMessage(
id: ChatMessageId,
fields: { status: ChatHubMessageStatus; content?: string },
fields: Partial<{ status: ChatHubMessageStatus; content: string }>,
trx?: EntityManager,
) {
return await withTransaction(this.manager, trx, async (em) => {

View File

@ -263,7 +263,7 @@ function handleCancelEditMessage() {
}
function handleEditMessage(message: ChatHubMessageDto) {
if (chatStore.isResponding || message.type !== 'human' || !selectedModel.value) {
if (chatStore.isResponding || !['human', 'ai'].includes(message.type) || !selectedModel.value) {
return;
}

View File

@ -187,6 +187,20 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
conversation.activeMessageChain = computeActiveChain(conversation.messages, message.id);
}
function replaceMessageContent(
sessionId: ChatSessionId,
messageId: ChatMessageId,
content: string,
) {
const conversation = ensureConversation(sessionId);
const message = conversation.messages[messageId];
if (!message) {
throw new Error(`Message with ID ${messageId} not found in session ${sessionId}`);
}
message.content = content;
}
function appendMessage(sessionId: ChatSessionId, messageId: ChatMessageId, chunk: string) {
const conversation = ensureConversation(sessionId);
const message = conversation.messages[messageId];
@ -392,7 +406,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
function editMessage(
sessionId: ChatSessionId,
editId: ChatMessageId,
message: string,
content: string,
model: ChatHubConversationModel,
credentials: ChatHubSendMessageRequest['credentials'],
) {
@ -400,27 +414,32 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
const replyId = uuidv4();
const conversation = ensureConversation(sessionId);
const previousMessageId = conversation.messages[editId]?.previousMessageId ?? null;
const message = conversation.messages[editId];
const previousMessageId = message?.previousMessageId ?? null;
addMessage(sessionId, {
id: messageId,
sessionId,
type: 'human',
name: 'User',
content: message,
provider: null,
model: null,
workflowId: null,
executionId: null,
status: 'success',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
previousMessageId,
retryOfMessageId: null,
revisionOfMessageId: editId,
responses: [],
alternatives: [],
});
if (message?.type === 'human') {
addMessage(sessionId, {
id: messageId,
sessionId,
type: 'human',
name: message.name ?? 'User',
content,
provider: null,
model: null,
workflowId: null,
executionId: null,
status: 'success',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
previousMessageId,
retryOfMessageId: null,
revisionOfMessageId: editId,
responses: [],
alternatives: [],
});
} else if (message?.type === 'ai') {
replaceMessageContent(sessionId, editId, content);
}
editMessageApi(
rootStore.restApiContext,
@ -430,7 +449,7 @@ export const useChatStore = defineStore(CHAT_STORE, () => {
model,
messageId,
replyId,
message,
message: content,
credentials,
},
(chunk: StructuredChunk) => onStreamMessage(sessionId, chunk, replyId, messageId, null),

View File

@ -72,7 +72,7 @@ function handleCancelEdit() {
}
function handleConfirmEdit() {
if (message.type === 'ai' || !editedText.value.trim()) {
if (!editedText.value.trim()) {
return;
}