Merge pull request #6203 from Louis454545/fix/scroll-jump-during-streaming-5939

fix: prevent scroll position jump when AI completes response while user reading
This commit is contained in:
Louis 2025-08-20 13:06:32 +07:00 committed by GitHub
commit c3b043eafa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -37,6 +37,8 @@ function ThreadDetail() {
const [isAtBottom, setIsAtBottom] = useState(true)
const [hasScrollbar, setHasScrollbar] = useState(false)
const lastScrollTopRef = useRef(0)
const userIntendedPositionRef = useRef<number | null>(null)
const wasStreamingRef = useRef(false)
const { currentThreadId, setCurrentThreadId } = useThreads()
const { setCurrentAssistant, assistants } = useAssistant()
const { setMessages, deleteMessage } = useMessages()
@ -112,6 +114,8 @@ function ThreadDetail() {
scrollToBottom()
setIsAtBottom(true)
setIsUserScrolling(false)
userIntendedPositionRef.current = null
wasStreamingRef.current = false
checkScrollState()
return
}
@ -123,11 +127,39 @@ function ThreadDetail() {
scrollToBottom()
setIsAtBottom(true)
setIsUserScrolling(false)
userIntendedPositionRef.current = null
wasStreamingRef.current = false
checkScrollState()
}, [threadId])
// Single useEffect for all auto-scrolling logic
useEffect(() => {
// Track streaming state changes
const isCurrentlyStreaming = !!streamingContent
const justFinishedStreaming = wasStreamingRef.current && !isCurrentlyStreaming
wasStreamingRef.current = isCurrentlyStreaming
// If streaming just finished and user had an intended position, restore it
if (justFinishedStreaming && userIntendedPositionRef.current !== null) {
// Small delay to ensure DOM has updated
setTimeout(() => {
if (scrollContainerRef.current && userIntendedPositionRef.current !== null) {
scrollContainerRef.current.scrollTo({
top: userIntendedPositionRef.current,
behavior: 'smooth'
})
userIntendedPositionRef.current = null
setIsUserScrolling(false)
}
}, 100)
return
}
// Clear intended position when streaming starts fresh
if (isCurrentlyStreaming && !wasStreamingRef.current) {
userIntendedPositionRef.current = null
}
// Only auto-scroll when the user is not actively scrolling
// AND either at the bottom OR there's streaming content
if (!isUserScrolling && (streamingContent || isAtBottom) && messagesCount) {
@ -163,6 +195,11 @@ function ThreadDetail() {
// Detect if this is a user-initiated scroll
if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {
setIsUserScrolling(!isBottom)
// If user scrolls during streaming and moves away from bottom, record their intended position
if (streamingContent && !isBottom) {
userIntendedPositionRef.current = scrollTop
}
}
setIsAtBottom(isBottom)
setHasScrollbar(hasScroll)
@ -180,6 +217,11 @@ function ThreadDetail() {
// Detect if this is a user-initiated scroll
if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {
setIsUserScrolling(!isBottom)
// If user scrolls during streaming and moves away from bottom, record their intended position
if (streamingContent && !isBottom) {
userIntendedPositionRef.current = scrollTop
}
}
setIsAtBottom(isBottom)
setHasScrollbar(hasScroll)