diff --git a/src/App.jsx b/src/App.jsx index ed6a57a..000da60 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1325,183 +1325,6 @@ async function regenerateFromIndex(index, overrideUserText = null) { setIsAttachmentMenuOpen(false) }, [activeSessionId, activeSidebarMode, canAttachImages]) - // Persist the scrollTop of the session we are LEAVING (on chat change or when leaving the chat view) - useEffect(() => { - const leavingSessionId = activeSessionId; - const leavingMode = activeSidebarMode; - - return () => { - if (leavingMode === 'chats' && leavingSessionId) { - const top = typeof scrollTopsRef.current[leavingSessionId] === 'number' - ? scrollTopsRef.current[leavingSessionId] - : (chatRef.current ? chatRef.current.scrollTop : 0); - - setScrollPositions(prev => { - const updated = { ...prev, [leavingSessionId]: top }; - window.electronAPI.updateSettings({ scrollPositions: updated }); - return updated; - }); - } - }; - }, [activeSessionId, activeSidebarMode]); - - // Track scroll + whether user left bottom - useEffect(() => { - const chatDiv = chatRef.current; - if (!chatDiv) return; - - const handleScroll = () => { - const { scrollTop, scrollHeight, clientHeight } = chatDiv; - const isAtBottom = (scrollHeight - scrollTop - clientHeight) <= BOTTOM_EPSILON; - - if (activeSessionId) { - const prevScrollTop = prevScrollTopsRef.current[activeSessionId]; - const scrolledUp = typeof prevScrollTop === 'number' && scrollTop < prevScrollTop; - - scrollTopsRef.current[activeSessionId] = scrollTop; - - if (isAtBottom) { - setUserScrolledUp(activeSessionId, false); // User is at bottom, enable autoscroll - } else if (scrolledUp) { - setUserScrolledUp(activeSessionId, true); // User scrolled up, disable autoscroll - } - // If user scrolled down but not to bottom, maintain current userScrolledUp state - prevScrollTopsRef.current[activeSessionId] = scrollTop; - } - }; - - chatDiv.addEventListener('scroll', handleScroll); - return () => chatDiv.removeEventListener('scroll', handleScroll); - }, [activeSessionId, setUserScrolledUp]); - - // Auto-hide the tip if user returns to bottom in the active chat - useEffect(() => { - const sid = activeSessionId; - if (!sid) return; - if (userScrolledUpState[sid] === false) { - setNewMsgTip(prev => { - if (!(sid in prev)) return prev; - const rest = { ...prev }; - delete rest[sid]; - return rest; - }); - } - }, [activeSessionId, userScrolledUpState]); - - // --- Robust restoration: do it before paint, exactly once per open --- - useLayoutEffect(() => { - if (activeSidebarMode !== 'chats' || !activeSessionId) return; - - const div = chatRef.current; - if (!div) return; - - restoredForRef.current = null; - - const applyRestore = () => { - if (restoredForRef.current === activeSessionId) return; - - const liveSaved = typeof scrollTopsRef.current[activeSessionId] === 'number' - ? scrollTopsRef.current[activeSessionId] - : undefined; - const saved = typeof liveSaved === 'number' - ? liveSaved - : scrollPositions[activeSessionId]; - - if (typeof saved === 'number') { - div.scrollTop = saved; - restoredForRef.current = activeSessionId; - return; - } - if (messages.length > 0) { - // default: bottom when no saved position - div.scrollTop = div.scrollHeight; - restoredForRef.current = activeSessionId; - } - }; - - // Run immediately (pre-paint) and also schedule a fallback rAF - applyRestore(); - const r0 = requestAnimationFrame(applyRestore); - - // If content size/DOM changes after first paint, apply once - const onDomChange = () => { - if (restoredForRef.current !== activeSessionId) { - requestAnimationFrame(applyRestore); - } - }; - - const mo = new MutationObserver(onDomChange); - mo.observe(div, { childList: true, subtree: true }); - - const ro = new ResizeObserver(onDomChange); - ro.observe(div); - - return () => { - cancelAnimationFrame(r0); - mo.disconnect(); - ro.disconnect(); - }; - }, [activeSessionId, activeSidebarMode, messages.length, scrollPositions]); - - // If there is no saved scroll and content arrives later (e.g., on first app load), - // default to bottom exactly once for this open chat. - useEffect(() => { - if (activeSidebarMode !== 'chats' || !activeSessionId) return; - if (restoredForRef.current === activeSessionId) return; // already applied - - const liveSaved = typeof scrollTopsRef.current[activeSessionId] === 'number' - ? scrollTopsRef.current[activeSessionId] - : undefined; - const savedScrollTop = typeof liveSaved === 'number' - ? liveSaved - : scrollPositions[activeSessionId]; - - // Only when there is no saved position and we now have content - if (typeof savedScrollTop !== 'number' && messages.length > 0) { - requestAnimationFrame(() => { - const div = chatRef.current; - if (!div) return; - div.scrollTop = div.scrollHeight; - restoredForRef.current = activeSessionId; - }); - } - }, [messages.length, activeSessionId, activeSidebarMode, scrollPositions]); - - // Session-aware scroll helpers - const scrollToBottom = (behavior = 'smooth', sessionId = null) => { - const chatDiv = chatRef.current; - if (!chatDiv) return; - const target = sessionId ?? activeSessionIdRef.current; - if (activeSessionIdRef.current !== target) return; - chatDiv.scrollTo({ top: chatDiv.scrollHeight, behavior }); - setUserScrolledUp(target, false); - }; - - const scrollMessageToTop = (msgId, behavior = 'auto', sessionId = null) => { - const chatDiv = chatRef.current; - if (!chatDiv) return; - const target = sessionId ?? activeSessionIdRef.current; - if (activeSessionIdRef.current !== target) return; - const el = document.getElementById(msgId); - if (el) { - const top = Math.max(0, el.offsetTop - TOP_ALIGN_OFFSET); - chatDiv.scrollTo({ top, behavior }); - } - }; - - // Handler for new message tip click - const handleNewMsgTipClick = () => { - const sid = activeSessionIdRef.current; - const msgId = newMsgTip[sid]; - if (msgId) { - scrollMessageToTop(msgId, 'smooth', sid); - setNewMsgTip(prev => { - const { [sid]: _omit, ...rest } = prev; - return rest; - }); - } - }; - const handleChatDragEnter = (event) => { if (activeSidebarMode !== 'chats' || !hasFilePayload(event)) return event.preventDefault()