From 4d20425771a69b51277f5bbb340613e976bdbbb4 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Sun, 7 Dec 2025 22:24:26 +0700 Subject: [PATCH 1/3] feat: save/save as copy edited AI message functionality --- .../use-edit-message/src/use-edit-message.ts | 16 ++++- .../api/src/lib/chats/utils/index.ts | 3 +- .../prepare-copy-edited-message-payload.ts | 64 +++++++++++++++++++ .../prepare-edit-assistant-message-payload.ts | 40 ++++++++++++ .../prepare-update-message-in-chat-payload.ts | 36 ----------- 5 files changed, 119 insertions(+), 40 deletions(-) create mode 100644 libs/shared/data-access/api/src/lib/chats/utils/prepare-copy-edited-message-payload.ts create mode 100644 libs/shared/data-access/api/src/lib/chats/utils/prepare-edit-assistant-message-payload.ts delete mode 100644 libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts diff --git a/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts b/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts index 1b4e75b..b5a42fb 100644 --- a/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts +++ b/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts @@ -4,10 +4,12 @@ import { FormValues } from '@open-webui-react-native/mobile/shared/utils/form'; import { chatApi, ChatResponse, - prepareUpdateMessageInChatPayload, prepareCompleteChatPayload, prepareUpdateMessageToSendPayload, + prepareEditAssistantMessagePayload, + prepareCopyEditedMessagePayload, } from '@open-webui-react-native/shared/data-access/api'; +import { Role } from '@open-webui-react-native/shared/data-access/common'; import { socketService } from '@open-webui-react-native/shared/data-access/websocket'; interface UseEditMessageProps { @@ -47,7 +49,7 @@ export const useEditMessage = ({ chat, modelId }: UseEditMessageProps): typeof r return; } - const preparedChat = prepareUpdateMessageInChatPayload(chat, editingMessageId, message); + const preparedChat = prepareCopyEditedMessagePayload(chat, editingMessageId, message); await updateChat(preparedChat); cancelEditing(); @@ -60,10 +62,18 @@ export const useEditMessage = ({ chat, modelId }: UseEditMessageProps): typeof r const chatHistory = chat.chat.history; const editedMessage = chatHistory.messages[editingMessageId]; - const preparedChat = prepareUpdateMessageToSendPayload(chat, message, modelId, editedMessage.parentId); + let preparedChat: ChatResponse; + + if (editedMessage.role === Role.ASSISTANT) { + preparedChat = prepareEditAssistantMessagePayload(chat, editingMessageId, message); + } else { + preparedChat = prepareUpdateMessageToSendPayload(chat, message, modelId, editedMessage.parentId); + } await updateChat(preparedChat, { onSuccess: (data) => { + if (editedMessage.role === Role.ASSISTANT) return; + const completePayload = prepareCompleteChatPayload({ chatId: data.id, messageId: data.chat!.history.currentId, diff --git a/libs/shared/data-access/api/src/lib/chats/utils/index.ts b/libs/shared/data-access/api/src/lib/chats/utils/index.ts index 100137c..4715ff7 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/index.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/index.ts @@ -14,5 +14,6 @@ export * from './invalidate-archived-chat-list-query'; export * from './invalidate-search-chats-query'; export * from './create-messages-list'; export * from './patch-chat-with-selected-messages'; -export * from './prepare-update-message-in-chat-payload'; export * from './prepare-update-message-to-send-payload'; +export * from './prepare-edit-assistant-message-payload'; +export * from './prepare-copy-edited-message-payload'; diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-copy-edited-message-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-copy-edited-message-payload.ts new file mode 100644 index 0000000..06b4057 --- /dev/null +++ b/libs/shared/data-access/api/src/lib/chats/utils/prepare-copy-edited-message-payload.ts @@ -0,0 +1,64 @@ +import { Role } from '@open-webui-react-native/shared/data-access/common'; +import { ChatResponse, Message } from '../models'; + +export function prepareCopyEditedMessagePayload( + oldData: ChatResponse, + messageId: string, + newContent: string, +): ChatResponse { + const history = oldData.chat.history; + const messagesMap = { ...history.messages }; + + const target = messagesMap[messageId]; + if (!target) return oldData; + + const isAIMessage = target.role === Role.ASSISTANT; + + const updatedMessage: Message = { + ...target, + content: newContent, + }; + messagesMap[messageId] = updatedMessage; + + if (!isAIMessage) { + const patchedMessagesList = oldData.chat.messages.map((msg) => (msg.id === messageId ? updatedMessage : msg)); + const lastAssistantMessage = oldData.chat.history.lastAssistantMessage; + + return { + ...oldData, + chat: { + ...oldData.chat, + history: { + ...history, + messages: messagesMap, + lastAssistantMessage, + }, + messages: patchedMessagesList, + }, + }; + } + + const chain: Array = []; + let pointer: Message | undefined = updatedMessage; + + while (pointer) { + chain.unshift(pointer); + pointer = pointer.parentId ? messagesMap[pointer.parentId] : undefined; + } + + const newCurrentId = updatedMessage.id; + + return { + ...oldData, + chat: { + ...oldData.chat, + history: { + ...history, + messages: messagesMap, + currentId: newCurrentId, + lastAssistantMessage: updatedMessage, + }, + messages: chain, + }, + }; +} diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-edit-assistant-message-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-edit-assistant-message-payload.ts new file mode 100644 index 0000000..b971e24 --- /dev/null +++ b/libs/shared/data-access/api/src/lib/chats/utils/prepare-edit-assistant-message-payload.ts @@ -0,0 +1,40 @@ +import { Role } from '@open-webui-react-native/shared/data-access/common'; +import { ChatResponse } from '../models'; + +export function prepareEditAssistantMessagePayload( + oldData: ChatResponse, + messageId: string, + newContent: string, +): ChatResponse { + const history = oldData.chat.history; + const messagesMap = { ...history.messages }; + + const target = messagesMap[messageId]; + if (!target || target.role !== Role.ASSISTANT) return oldData; + + const updatedMessage = { + ...target, + content: newContent, + done: true, + }; + + messagesMap[messageId] = updatedMessage; + + const updatedMessagesList = oldData.chat.messages.map((m) => (m.id === messageId ? updatedMessage : m)); + + const lastAssistantMessage = + history.lastAssistantMessage?.id === messageId ? updatedMessage : history.lastAssistantMessage; + + return { + ...oldData, + chat: { + ...oldData.chat, + history: { + ...history, + messages: messagesMap, + lastAssistantMessage, + }, + messages: updatedMessagesList, + }, + }; +} diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts deleted file mode 100644 index ace2908..0000000 --- a/libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ChatResponse } from '../models'; - -export function prepareUpdateMessageInChatPayload( - oldData: ChatResponse, - messageId: string, - newContent: string, -): ChatResponse { - const messagesMap = { ...oldData.chat.history.messages }; - const message = messagesMap[messageId]; - - messagesMap[messageId] = { - ...message, - content: newContent, - }; - - const messagesArray = oldData.chat.messages.map((msg) => - msg.id === messageId ? { ...messagesMap[messageId] } : msg, - ); - - const lastAssistantMessage = oldData.chat.history.lastAssistantMessage; - - const newChatData: ChatResponse = { - ...oldData, - chat: { - ...oldData.chat, - history: { - ...oldData.chat.history, - messages: messagesMap, - lastAssistantMessage, - }, - messages: messagesArray, - }, - }; - - return newChatData; -} From 9412e3e36514bfada18de76768564e5785b48077 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Wed, 10 Dec 2025 00:15:11 +0700 Subject: [PATCH 2/3] fix: save as copy message parent messages tree fix --- .../prepare-copy-edited-message-payload.ts | 65 ++++++++----------- 1 file changed, 28 insertions(+), 37 deletions(-) diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-copy-edited-message-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-copy-edited-message-payload.ts index 06b4057..4202525 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/prepare-copy-edited-message-payload.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/prepare-copy-edited-message-payload.ts @@ -1,5 +1,7 @@ +import uuid from 'react-native-uuid'; import { Role } from '@open-webui-react-native/shared/data-access/common'; -import { ChatResponse, Message } from '../models'; +import { ChatResponse, History, Message } from '../models'; +import { createMessagesList } from './create-messages-list'; export function prepareCopyEditedMessagePayload( oldData: ChatResponse, @@ -9,56 +11,45 @@ export function prepareCopyEditedMessagePayload( const history = oldData.chat.history; const messagesMap = { ...history.messages }; - const target = messagesMap[messageId]; - if (!target) return oldData; + const original = messagesMap[messageId]; + if (!original || original.role !== Role.ASSISTANT) return oldData; - const isAIMessage = target.role === Role.ASSISTANT; + const newAssistantMessageId = uuid.v4(); - const updatedMessage: Message = { - ...target, + const parentId = original.parentId ?? original.id; + + const newAssistantMessage: Message = { + ...original, + id: newAssistantMessageId, + parentId, + childrenIds: [], content: newContent, + done: true, }; - messagesMap[messageId] = updatedMessage; - - if (!isAIMessage) { - const patchedMessagesList = oldData.chat.messages.map((msg) => (msg.id === messageId ? updatedMessage : msg)); - const lastAssistantMessage = oldData.chat.history.lastAssistantMessage; - return { - ...oldData, - chat: { - ...oldData.chat, - history: { - ...history, - messages: messagesMap, - lastAssistantMessage, - }, - messages: patchedMessagesList, - }, + if (parentId && messagesMap[parentId]) { + const parent = messagesMap[parentId]; + messagesMap[parentId] = { + ...parent, + childrenIds: [...(parent.childrenIds ?? []), newAssistantMessageId], }; } - const chain: Array = []; - let pointer: Message | undefined = updatedMessage; + messagesMap[newAssistantMessageId] = newAssistantMessage; - while (pointer) { - chain.unshift(pointer); - pointer = pointer.parentId ? messagesMap[pointer.parentId] : undefined; - } - - const newCurrentId = updatedMessage.id; + const updatedHistory: History = { + ...history, + currentId: newAssistantMessageId, + messages: messagesMap, + lastAssistantMessage: newAssistantMessage, + }; return { ...oldData, chat: { ...oldData.chat, - history: { - ...history, - messages: messagesMap, - currentId: newCurrentId, - lastAssistantMessage: updatedMessage, - }, - messages: chain, + history: updatedHistory, + messages: createMessagesList(updatedHistory, newAssistantMessageId), }, }; } From 786963ec6ef72efd6c1c6466332367d841d3cd3f Mon Sep 17 00:00:00 2001 From: tfomkin Date: Tue, 16 Dec 2025 22:41:04 +0700 Subject: [PATCH 3/3] fix: updated user message in chat functionality restored --- .../use-edit-message/src/use-edit-message.ts | 24 ++++++++----- .../api/src/lib/chats/utils/index.ts | 1 + .../prepare-update-message-in-chat-payload.ts | 36 +++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts diff --git a/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts b/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts index b5a42fb..63bf719 100644 --- a/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts +++ b/libs/mobile/chat/features/use-edit-message/src/use-edit-message.ts @@ -8,6 +8,8 @@ import { prepareUpdateMessageToSendPayload, prepareEditAssistantMessagePayload, prepareCopyEditedMessagePayload, + prepareUpdateMessageInChatPayload, + History, } from '@open-webui-react-native/shared/data-access/api'; import { Role } from '@open-webui-react-native/shared/data-access/common'; import { socketService } from '@open-webui-react-native/shared/data-access/websocket'; @@ -34,6 +36,12 @@ export const useEditMessage = ({ chat, modelId }: UseEditMessageProps): typeof r const { mutateAsync: updateChat, isPending: isChatUpdating } = chatApi.useUpdate(); const { mutate: completeChat } = chatApi.useCompleteChat(); + const isAssistantMessage = (chatHistory: History, messageId: string): boolean => { + const editedMessage = chatHistory.messages[messageId]; + + return editedMessage.role === Role.ASSISTANT; + }; + const startEditing = (messageId: string, content: string): void => { setEditingMessageId(messageId); reset({ editMessageInputValue: content }); @@ -49,7 +57,11 @@ export const useEditMessage = ({ chat, modelId }: UseEditMessageProps): typeof r return; } - const preparedChat = prepareCopyEditedMessagePayload(chat, editingMessageId, message); + const chatHistory = chat.chat.history; + + const preparedChat: ChatResponse = isAssistantMessage(chatHistory, editingMessageId) + ? prepareCopyEditedMessagePayload(chat, editingMessageId, message) + : prepareUpdateMessageInChatPayload(chat, editingMessageId, message); await updateChat(preparedChat); cancelEditing(); @@ -62,13 +74,9 @@ export const useEditMessage = ({ chat, modelId }: UseEditMessageProps): typeof r const chatHistory = chat.chat.history; const editedMessage = chatHistory.messages[editingMessageId]; - let preparedChat: ChatResponse; - - if (editedMessage.role === Role.ASSISTANT) { - preparedChat = prepareEditAssistantMessagePayload(chat, editingMessageId, message); - } else { - preparedChat = prepareUpdateMessageToSendPayload(chat, message, modelId, editedMessage.parentId); - } + const preparedChat: ChatResponse = isAssistantMessage(chatHistory, editingMessageId) + ? prepareEditAssistantMessagePayload(chat, editingMessageId, message) + : prepareUpdateMessageToSendPayload(chat, message, modelId, editedMessage.parentId); await updateChat(preparedChat, { onSuccess: (data) => { diff --git a/libs/shared/data-access/api/src/lib/chats/utils/index.ts b/libs/shared/data-access/api/src/lib/chats/utils/index.ts index 4715ff7..8cb9013 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/index.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/index.ts @@ -14,6 +14,7 @@ export * from './invalidate-archived-chat-list-query'; export * from './invalidate-search-chats-query'; export * from './create-messages-list'; export * from './patch-chat-with-selected-messages'; +export * from './prepare-update-message-in-chat-payload'; export * from './prepare-update-message-to-send-payload'; export * from './prepare-edit-assistant-message-payload'; export * from './prepare-copy-edited-message-payload'; diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts new file mode 100644 index 0000000..ace2908 --- /dev/null +++ b/libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts @@ -0,0 +1,36 @@ +import { ChatResponse } from '../models'; + +export function prepareUpdateMessageInChatPayload( + oldData: ChatResponse, + messageId: string, + newContent: string, +): ChatResponse { + const messagesMap = { ...oldData.chat.history.messages }; + const message = messagesMap[messageId]; + + messagesMap[messageId] = { + ...message, + content: newContent, + }; + + const messagesArray = oldData.chat.messages.map((msg) => + msg.id === messageId ? { ...messagesMap[messageId] } : msg, + ); + + const lastAssistantMessage = oldData.chat.history.lastAssistantMessage; + + const newChatData: ChatResponse = { + ...oldData, + chat: { + ...oldData.chat, + history: { + ...oldData.chat.history, + messages: messagesMap, + lastAssistantMessage, + }, + messages: messagesArray, + }, + }; + + return newChatData; +}