11import Input from '@/components/ui/Input'
22import Button from '@/components/ui/Button'
33import { useCallback , useEffect , useRef , useState } from 'react'
4+ import { throttle } from '@/lib/utils'
45import { queryText , queryTextStream , Message } from '@/api/lightrag'
56import { errorMessage } from '@/lib/utils'
67import { useSettingsStore } from '@/stores/settings'
@@ -19,31 +20,28 @@ export default function RetrievalTesting() {
1920 const [ isLoading , setIsLoading ] = useState ( false )
2021 // Reference to track if we should follow scroll during streaming (using ref for synchronous updates)
2122 const shouldFollowScrollRef = useRef ( true )
22- // Reference to track if this is the first chunk of a streaming response
23- const isFirstChunkRef = useRef ( true )
23+ // Reference to track if user interaction is from the form area
24+ const isFormInteractionRef = useRef ( false )
25+ // Reference to track if scroll was triggered programmatically
26+ const programmaticScrollRef = useRef ( false )
27+ // Reference to track if we're currently receiving a streaming response
28+ const isReceivingResponseRef = useRef ( false )
2429 const messagesEndRef = useRef < HTMLDivElement > ( null )
2530 const messagesContainerRef = useRef < HTMLDivElement > ( null )
2631
27- // Check if the container is near the bottom
28- const isNearBottom = useCallback ( ( ) => {
29- const container = messagesContainerRef . current
30- if ( ! container ) return true // Default to true if no container reference
31-
32- // Calculate distance to bottom
33- const { scrollTop , scrollHeight , clientHeight } = container
34- const distanceToBottom = scrollHeight - scrollTop - clientHeight
35-
36- // Consider near bottom if less than 100px from bottom
37- return distanceToBottom < 100
32+ // Scroll to bottom function - restored smooth scrolling with better handling
33+ const scrollToBottom = useCallback ( ( ) => {
34+ // Set flag to indicate this is a programmatic scroll
35+ programmaticScrollRef . current = true
36+ // Use requestAnimationFrame for better performance
37+ requestAnimationFrame ( ( ) => {
38+ if ( messagesEndRef . current ) {
39+ // Use smooth scrolling for better user experience
40+ messagesEndRef . current . scrollIntoView ( { behavior : 'auto' } )
41+ }
42+ } )
3843 } , [ ] )
3944
40- const scrollToBottom = useCallback ( ( force = false ) => {
41- // Only scroll if forced or user is already near bottom
42- if ( force || isNearBottom ( ) ) {
43- messagesEndRef . current ?. scrollIntoView ( { behavior : 'smooth' } )
44- }
45- } , [ isNearBottom ] )
46-
4745 const handleSubmit = useCallback (
4846 async ( e : React . FormEvent ) => {
4947 e . preventDefault ( )
@@ -64,15 +62,15 @@ export default function RetrievalTesting() {
6462
6563 // Add messages to chatbox
6664 setMessages ( [ ...prevMessages , userMessage , assistantMessage ] )
67-
68- // Reset first chunk flag for new streaming response
69- isFirstChunkRef . current = true
70- // Enable follow scroll for new query
65+
66+ // Reset scroll following state for new query
7167 shouldFollowScrollRef . current = true
72-
68+ // Set flag to indicate we're receiving a response
69+ isReceivingResponseRef . current = true
70+
7371 // Force scroll to bottom after messages are rendered
7472 setTimeout ( ( ) => {
75- scrollToBottom ( true )
73+ scrollToBottom ( )
7674 } , 0 )
7775
7876 // Clear input and set loading
@@ -81,17 +79,6 @@ export default function RetrievalTesting() {
8179
8280 // Create a function to update the assistant's message
8381 const updateAssistantMessage = ( chunk : string , isError ?: boolean ) => {
84- // Check if this is the first chunk of the streaming response
85- if ( isFirstChunkRef . current ) {
86- // Determine scroll behavior based on initial position
87- shouldFollowScrollRef . current = isNearBottom ( ) ;
88- isFirstChunkRef . current = false ;
89- }
90-
91- // Save current scroll position before updating content
92- const container = messagesContainerRef . current ;
93- const currentScrollPosition = container ? container . scrollTop : 0 ;
94-
9582 assistantMessage . content += chunk
9683 setMessages ( ( prev ) => {
9784 const newMessages = [ ...prev ]
@@ -102,19 +89,13 @@ export default function RetrievalTesting() {
10289 }
10390 return newMessages
10491 } )
105-
106- // After updating content, check if we should scroll
107- // Use consistent scrolling behavior throughout the streaming response
92+
93+ // After updating content, scroll to bottom if auto- scroll is enabled
94+ // Use a longer delay to ensure DOM has updated
10895 if ( shouldFollowScrollRef . current ) {
109- scrollToBottom ( true ) ;
110- } else if ( container ) {
111- // If user was not near bottom, restore their scroll position
112- // This needs to be in a setTimeout to work after React updates the DOM
11396 setTimeout ( ( ) => {
114- if ( container ) {
115- container . scrollTop = currentScrollPosition ;
116- }
117- } , 0 ) ;
97+ scrollToBottom ( )
98+ } , 30 )
11899 }
119100 }
120101
@@ -152,6 +133,7 @@ export default function RetrievalTesting() {
152133 } finally {
153134 // Clear loading and add messages to state
154135 setIsLoading ( false )
136+ isReceivingResponseRef . current = false
155137 useSettingsStore
156138 . getState ( )
157139 . setRetrievalHistory ( [ ...prevMessages , userMessage , assistantMessage ] )
@@ -160,30 +142,76 @@ export default function RetrievalTesting() {
160142 [ inputValue , isLoading , messages , setMessages , t , scrollToBottom ]
161143 )
162144
163- // Add scroll event listener to detect when user manually scrolls
145+ // Add event listeners to detect when user manually interacts with the container
164146 useEffect ( ( ) => {
165147 const container = messagesContainerRef . current ;
166148 if ( ! container ) return ;
167-
168- const handleScroll = ( ) => {
169- const isNearBottomNow = isNearBottom ( ) ;
170-
171- // If user scrolls away from bottom while in auto-scroll mode, disable it
172- if ( shouldFollowScrollRef . current && ! isNearBottomNow ) {
149+
150+ // Handle significant mouse wheel events - only disable auto-scroll for deliberate scrolling
151+ const handleWheel = ( e : WheelEvent ) => {
152+ // Only consider significant wheel movements (more than 10px)
153+ if ( Math . abs ( e . deltaY ) > 10 && ! isFormInteractionRef . current ) {
173154 shouldFollowScrollRef . current = false ;
174155 }
175- // If user scrolls back to bottom while not in auto-scroll mode, re-enable it
176- else if ( ! shouldFollowScrollRef . current && isNearBottomNow ) {
177- shouldFollowScrollRef . current = true ;
156+ } ;
157+
158+ // Handle scroll events - only disable auto-scroll if not programmatically triggered
159+ // and if it's a significant scroll
160+ const handleScroll = throttle ( ( ) => {
161+ // If this is a programmatic scroll, don't disable auto-scroll
162+ if ( programmaticScrollRef . current ) {
163+ programmaticScrollRef . current = false ;
164+ return ;
165+ }
166+
167+ // If we're receiving a response, be more conservative about disabling auto-scroll
168+ if ( ! isFormInteractionRef . current && ! isReceivingResponseRef . current ) {
169+ shouldFollowScrollRef . current = false ;
178170 }
171+ } , 30 ) ;
172+
173+ // Add event listeners - only listen for wheel and scroll events
174+ container . addEventListener ( 'wheel' , handleWheel as EventListener ) ;
175+ container . addEventListener ( 'scroll' , handleScroll as EventListener ) ;
176+
177+ return ( ) => {
178+ container . removeEventListener ( 'wheel' , handleWheel as EventListener ) ;
179+ container . removeEventListener ( 'scroll' , handleScroll as EventListener ) ;
180+ } ;
181+ } , [ ] ) ;
182+
183+ // Add event listeners to the form area to prevent disabling auto-scroll when interacting with form
184+ useEffect ( ( ) => {
185+ const form = document . querySelector ( 'form' ) ;
186+ if ( ! form ) return ;
187+
188+ const handleFormMouseDown = ( ) => {
189+ // Set flag to indicate form interaction
190+ isFormInteractionRef . current = true ;
191+
192+ // Reset the flag after a short delay
193+ setTimeout ( ( ) => {
194+ isFormInteractionRef . current = false ;
195+ } , 500 ) ; // Give enough time for the form interaction to complete
179196 } ;
180-
181- container . addEventListener ( 'scroll' , handleScroll ) ;
182- return ( ) => container . removeEventListener ( 'scroll' , handleScroll ) ;
183- } , [ isNearBottom ] ) ; // Remove shouldFollowScroll from dependencies since we're using ref now
184197
185- const debouncedMessages = useDebounce ( messages , 100 )
186- useEffect ( ( ) => scrollToBottom ( false ) , [ debouncedMessages , scrollToBottom ] )
198+ form . addEventListener ( 'mousedown' , handleFormMouseDown ) ;
199+
200+ return ( ) => {
201+ form . removeEventListener ( 'mousedown' , handleFormMouseDown ) ;
202+ } ;
203+ } , [ ] ) ;
204+
205+ // Use a longer debounce time for better performance with large message updates
206+ const debouncedMessages = useDebounce ( messages , 150 )
207+ useEffect ( ( ) => {
208+ // Only auto-scroll if enabled
209+ if ( shouldFollowScrollRef . current ) {
210+ // Force scroll to bottom when messages change
211+ scrollToBottom ( )
212+ }
213+ } , [ debouncedMessages , scrollToBottom ] )
214+
187215
188216 const clearMessages = useCallback ( ( ) => {
189217 setMessages ( [ ] )
@@ -194,7 +222,15 @@ export default function RetrievalTesting() {
194222 < div className = "flex size-full gap-2 px-2 pb-12 overflow-hidden" >
195223 < div className = "flex grow flex-col gap-4" >
196224 < div className = "relative grow" >
197- < div ref = { messagesContainerRef } className = "bg-primary-foreground/60 absolute inset-0 flex flex-col overflow-auto rounded-lg border p-2" >
225+ < div
226+ ref = { messagesContainerRef }
227+ className = "bg-primary-foreground/60 absolute inset-0 flex flex-col overflow-auto rounded-lg border p-2"
228+ onClick = { ( ) => {
229+ if ( shouldFollowScrollRef . current ) {
230+ shouldFollowScrollRef . current = false ;
231+ }
232+ } }
233+ >
198234 < div className = "flex min-h-0 flex-1 flex-col gap-2" >
199235 { messages . length === 0 ? (
200236 < div className = "text-muted-foreground flex h-full items-center justify-center text-lg" >
0 commit comments