From d2613e7065676142e6e883d25e0af36e1d8077bb Mon Sep 17 00:00:00 2001 From: jarvis24young <749843026@qq.com> Date: Fri, 22 May 2026 09:17:59 +0800 Subject: [PATCH] feat(mobile-web): add scroll-to-bottom floating button A floating button appears when the user scrolls up from the message list. Tapping it smoothly scrolls back to the latest messages. Uses programmaticScrollRef guard and ref-based change detection to suppress button flicker during auto-scroll (new messages, streaming updates). --- src/mobile-web/src/i18n/messages.ts | 3 ++ src/mobile-web/src/pages/ChatPage.tsx | 42 ++++++++++++++++- .../src/styles/components/chat.scss | 46 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/mobile-web/src/i18n/messages.ts b/src/mobile-web/src/i18n/messages.ts index 5df952386..c050f6f8e 100644 --- a/src/mobile-web/src/i18n/messages.ts +++ b/src/mobile-web/src/i18n/messages.ts @@ -143,6 +143,7 @@ export const messages: Record = { fileDownloading: 'Downloading...', fileDownloaded: 'Downloaded', clickToDownload: 'Click to download', + scrollToBottom: 'Scroll to bottom', }, tools: { explore: 'Explore', @@ -297,6 +298,7 @@ export const messages: Record = { fileDownloading: '下载中...', fileDownloaded: '已下载', clickToDownload: '点击下载', + scrollToBottom: '滚动到底部', }, tools: { explore: '探索', @@ -451,6 +453,7 @@ export const messages: Record = { fileDownloading: '下載中...', fileDownloaded: '已下載', clickToDownload: '點擊下載', + scrollToBottom: '捲動到底部', }, tools: { explore: '探索', diff --git a/src/mobile-web/src/pages/ChatPage.tsx b/src/mobile-web/src/pages/ChatPage.tsx index 538fad9f0..992f23e51 100644 --- a/src/mobile-web/src/pages/ChatPage.tsx +++ b/src/mobile-web/src/pages/ChatPage.tsx @@ -2001,6 +2001,7 @@ const ChatPage: React.FC = ({ sessionMgr, sessionId, sessionName, const messagesContainerRef = useRef(null); const [expandedMsgIds, setExpandedMsgIds] = useState>(new Set()); const [infoToast, setInfoToast] = useState(null); + const [showScrollToBottom, setShowScrollToBottom] = useState(false); const isStreaming = activeTurn != null && activeTurn.status === 'active'; @@ -2137,6 +2138,8 @@ const ChatPage: React.FC = ({ sessionMgr, sessionId, sessionName, }, [sessionMgr, sessionId, setMessages, setError, getMessages]); const isNearBottomRef = useRef(true); + const programmaticScrollRef = useRef(false); + const lastShowScrollToBottomRef = useRef(false); const BOTTOM_THRESHOLD = 80; const handleScroll = useCallback(() => { @@ -2144,7 +2147,18 @@ const ChatPage: React.FC = ({ sessionMgr, sessionId, sessionName, if (!container) return; const gap = container.scrollHeight - container.scrollTop - container.clientHeight; - isNearBottomRef.current = gap < BOTTOM_THRESHOLD; + const nearBottom = gap < BOTTOM_THRESHOLD; + isNearBottomRef.current = nearBottom; + if (nearBottom) { + programmaticScrollRef.current = false; + } + if (!programmaticScrollRef.current) { + const show = !nearBottom; + if (show !== lastShowScrollToBottomRef.current) { + lastShowScrollToBottomRef.current = show; + setShowScrollToBottom(show); + } + } if (container.scrollTop < 100 && hasMore && !isLoadingMore) { const msgs = getMessages(sessionId); @@ -2152,6 +2166,14 @@ const ChatPage: React.FC = ({ sessionMgr, sessionId, sessionName, } }, [hasMore, isLoadingMore, getMessages, sessionId, loadMessages]); + const scrollToBottom = useCallback(() => { + programmaticScrollRef.current = true; + isNearBottomRef.current = true; + setShowScrollToBottom(false); + lastShowScrollToBottomRef.current = false; + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, []); + // Initial load + start poller const initialScrollDone = useRef(false); const pendingInitialScroll = useRef(false); @@ -2231,6 +2253,7 @@ const ChatPage: React.FC = ({ sessionMgr, sessionId, sessionName, const isNewAppend = messages.length > prevMsgCountRef.current; prevMsgCountRef.current = messages.length; if (isNewAppend && !isLoadingMore && isNearBottomRef.current) { + programmaticScrollRef.current = true; messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); } } @@ -2239,11 +2262,13 @@ const ChatPage: React.FC = ({ sessionMgr, sessionId, sessionName, useEffect(() => { if (!initialScrollDone.current || !isStreaming) return; if (!isNearBottomRef.current) return; + programmaticScrollRef.current = true; messagesEndRef.current?.scrollIntoView({ behavior: 'auto' }); }, [activeTurn, isStreaming]); useEffect(() => { if (optimisticMsg) { + programmaticScrollRef.current = true; isNearBottomRef.current = true; messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); } @@ -2257,6 +2282,7 @@ const ChatPage: React.FC = ({ sessionMgr, sessionId, sessionName, if (!isNearBottomRef.current) return; const gap = container.scrollHeight - container.scrollTop - container.clientHeight; if (gap > 10 && gap < 400) { + programmaticScrollRef.current = true; container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' }); } }, 300); @@ -2692,8 +2718,22 @@ const ChatPage: React.FC = ({ sessionMgr, sessionId, sessionName, )}
+
+ {showScrollToBottom && ( + + )} + {/* Floating Input Bar — two-stage (matches desktop ChatInput) */}