From d887b578c5070807cb3d57fa2122a8fc69ecc59d Mon Sep 17 00:00:00 2001 From: Octopus Date: Fri, 15 May 2026 09:53:35 +0800 Subject: [PATCH] fix: preserve uploaded file attachments after subsequent assistant messages (#13993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem When a user uploads a file attachment in their first message (Q1) and then sends a follow-up message (Q2) that triggers a backend response, the uploaded file attachment disappears from Q1 in the chat UI. Fixes #13959 ## Root Cause In `single-chat-box.tsx`, a `useEffect` hook syncs `derivedMessages` from `conversation?.messages` whenever the conversation data changes (e.g., after a new assistant reply arrives): ```typescript useEffect(() => { const messages = conversation?.messages; if (Array.isArray(messages)) { setDerivedMessages(messages); // ← overwrites local state } }, [conversation?.messages, setDerivedMessages]); ``` The problem is that `conversation.messages` comes from the server, which stores messages as plain JSON. Browser `File` objects (uploaded by the user) cannot be serialized to JSON, so they are never stored on the server. Each time the server data is applied to local state, the `files` array on the user's first message is lost. ## Fix Instead of replacing the local messages wholesale, preserve any `files` entries from the previous local state by ID before applying the server data: ```typescript useEffect(() => { const messages = conversation?.messages; if (Array.isArray(messages)) { setDerivedMessages((prevMessages) => { const filesMap = new Map( prevMessages .filter((m) => m.files?.length) .map((m) => [m.id, m.files]), ); if (filesMap.size === 0) { return messages; } return messages.map((m) => ({ ...m, files: filesMap.get(m.id) ?? m.files, })); }); } }, [conversation?.messages, setDerivedMessages]); ``` This is a minimal, targeted fix: when there are no local files to preserve the behavior is identical to before (early return with plain assignment). When local file objects exist they are re-attached to the corresponding server messages by ID. ## Summary by CodeRabbit * **Bug Fixes** * Improved search query processing to properly handle special characters and apostrophes in search terms and synonyms. * Fixed chat message file attachments to persist when syncing with server. * **Refactor** * Simplified OCR detection return values by removing timing metadata. --------- Co-authored-by: ximi --- .../chat/chat-box/single-chat-box.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/web/src/pages/next-chats/chat/chat-box/single-chat-box.tsx b/web/src/pages/next-chats/chat/chat-box/single-chat-box.tsx index 625441d359..a598d7f3ea 100644 --- a/web/src/pages/next-chats/chat/chat-box/single-chat-box.tsx +++ b/web/src/pages/next-chats/chat/chat-box/single-chat-box.tsx @@ -58,7 +58,23 @@ export function SingleChatBox({ useEffect(() => { const messages = conversation?.messages; if (Array.isArray(messages)) { - setDerivedMessages(messages); + setDerivedMessages((prevMessages) => { + // Preserve uploaded file objects from local state that the server doesn't + // persist (e.g. File instances). Build a map of message id → files from + // the current local state so they survive when server data is applied. + const filesMap = new Map( + prevMessages + .filter((m) => m.files?.length) + .map((m) => [m.id, m.files]), + ); + if (filesMap.size === 0) { + return messages; + } + return messages.map((m) => ({ + ...m, + files: filesMap.get(m.id) ?? m.files, + })); + }); } }, [conversation?.messages, setDerivedMessages]);