diff --git a/backend/open_webui/routers/chats.py b/backend/open_webui/routers/chats.py index 9862b1edf5..6c1e2119a0 100644 --- a/backend/open_webui/routers/chats.py +++ b/backend/open_webui/routers/chats.py @@ -8,6 +8,7 @@ from fastapi.responses import StreamingResponse from open_webui.utils.misc import get_message_list +from open_webui.utils.middleware import serialize_output from open_webui.socket.main import get_event_emitter from open_webui.models.chats import ( ChatForm, @@ -967,6 +968,14 @@ async def update_chat_by_id( chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db) if chat: updated_chat = {**chat.chat, **form_data.chat} + + # Re-derive content from output for assistant messages so that + # frontend edits to output items are always reflected in content. + # serialize_output() is the single source of truth for this conversion. + for msg in updated_chat.get('history', {}).get('messages', {}).values(): + if msg.get('role') == 'assistant' and msg.get('output'): + msg['content'] = serialize_output(msg['output']) + chat = await Chats.update_chat_by_id(id, updated_chat, db=db) return ChatResponse(**chat.model_dump()) else: diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index cf512dccfb..ea6c89034d 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -159,11 +159,21 @@ if (!$temporaryChatEnabled) { history = history; await tick(); - await updateChatById(localStorage.token, chatId, { + const res = await updateChatById(localStorage.token, chatId, { history: history, messages: messages }); + // Refresh local message content from backend (e.g. re-derived via serialize_output) + if (res?.chat?.history?.messages) { + for (const [id, msg] of Object.entries(res.chat.history.messages)) { + if (history.messages[id] && (msg as any).content) { + history.messages[id].content = (msg as any).content; + } + } + history = history; + } + currentChatPage.set(1); await chats.set(await getChatList(localStorage.token, $currentChatPage)); } @@ -317,7 +327,7 @@ await updateChat(); }; - const editMessage = async (messageId, { content, files }, submit = true) => { + const editMessage = async (messageId, { content, files, output = undefined }, submit = true) => { if ((selectedModels ?? []).filter((id) => id).length === 0) { toast.error($i18n.t('Model not selected')); return; @@ -361,7 +371,7 @@ } } else { if (submit) { - // New response message + // New response message (Save As Copy) const responseMessageId = uuidv4(); const message = history.messages[messageId]; const parentId = message.parentId; @@ -373,6 +383,7 @@ childrenIds: [], files: undefined, content: content, + output: output ?? undefined, timestamp: Math.floor(Date.now() / 1000) // Unix epoch }; @@ -392,6 +403,9 @@ // Edit response message history.messages[messageId].originalContent = history.messages[messageId].content; history.messages[messageId].content = content; + if (output !== undefined) { + history.messages[messageId].output = output; + } await updateChat(); } } diff --git a/src/lib/components/chat/Messages/OutputEditView.svelte b/src/lib/components/chat/Messages/OutputEditView.svelte new file mode 100644 index 0000000000..8a58feb220 --- /dev/null +++ b/src/lib/components/chat/Messages/OutputEditView.svelte @@ -0,0 +1,367 @@ + + +
+ +
+ + + +
+ + {#if viewMode === 'json'} +
+ {#if jsonError} +
{jsonError}
+ {/if} + {:else} + +
+ {#each displayItems as di, idx} +
+ +
+
+ {getItemLabel(di)} +
+
+ + +
+ {#if di.type === 'message'} +