11"use client" ;
22
3- import React , { useLayoutEffect , useRef , useEffect , useCallback , useMemo } from "react" ;
3+ import React , { useLayoutEffect , useRef , useEffect , useCallback , useMemo , useState } from "react" ;
44
55interface Message {
66 role : string ;
@@ -24,18 +24,20 @@ interface ProcessedMessageProps {
2424}
2525
2626export default function MessageList ( { messages} : MessageListProps ) {
27- const scrollAreaRef = useRef < HTMLDivElement > ( null ) ;
27+ const [ scrollAreaRef , setScrollAreaRef ] = useState < HTMLDivElement | null > ( null ) ;
2828
2929 // Track if user is at bottom - default to true for initial scroll
3030 const isAtBottomRef = useRef ( true ) ;
3131 // Track the last known scroll height to detect new content
3232 const lastScrollHeightRef = useRef ( 0 ) ;
33+ // Track if we're currently doing a programmatic scroll
34+ const isProgrammaticScrollRef = useRef ( false ) ;
3335
3436 const checkIfAtBottom = useCallback ( ( ) => {
35- if ( ! scrollAreaRef . current ) return false ;
36- const { scrollTop, scrollHeight, clientHeight } = scrollAreaRef . current ;
37+ if ( ! scrollAreaRef ) return false ;
38+ const { scrollTop, scrollHeight, clientHeight } = scrollAreaRef ;
3739 return scrollTop + clientHeight >= scrollHeight - 10 ; // 10px tolerance
38- } , [ ] ) ;
40+ } , [ scrollAreaRef ] ) ;
3941
4042 // Track Ctrl (Windows/Linux) or Cmd (Mac) key state
4143 // This is so that underline is only visible when hover + cmd/ctrl
@@ -60,26 +62,30 @@ export default function MessageList({messages}: MessageListProps) {
6062
6163 // Update isAtBottom on scroll
6264 useEffect ( ( ) => {
63- const scrollContainer = scrollAreaRef . current ;
64- if ( ! scrollContainer ) return ;
65+ if ( ! scrollAreaRef ) return ;
6566
6667 const handleScroll = ( ) => {
68+ if ( isProgrammaticScrollRef . current ) return ;
6769 isAtBottomRef . current = checkIfAtBottom ( ) ;
6870 } ;
6971
7072 // Initial check
7173 handleScroll ( ) ;
7274
73- scrollContainer . addEventListener ( "scroll" , handleScroll ) ;
74- return ( ) => scrollContainer . removeEventListener ( "scroll" , handleScroll ) ;
75- } , [ checkIfAtBottom ] ) ;
75+ scrollAreaRef . addEventListener ( "scroll" , handleScroll ) ;
76+ scrollAreaRef . addEventListener ( "scrollend" , ( ) => isProgrammaticScrollRef . current = false ) ;
77+ return ( ) => {
78+ scrollAreaRef . removeEventListener ( "scroll" , handleScroll )
79+ scrollAreaRef . removeEventListener ( "scrollend" , ( ) => isProgrammaticScrollRef . current = false ) ;
80+
81+ } ;
82+ } , [ checkIfAtBottom , scrollAreaRef ] ) ;
7683
7784 // Handle auto-scrolling when messages change
7885 useLayoutEffect ( ( ) => {
79- if ( ! scrollAreaRef . current ) return ;
86+ if ( ! scrollAreaRef ) return ;
8087
81- const scrollContainer = scrollAreaRef . current ;
82- const currentScrollHeight = scrollContainer . scrollHeight ;
88+ const currentScrollHeight = scrollAreaRef . scrollHeight ;
8389
8490 // Check if this is new content (scroll height increased)
8591 const hasNewContent = currentScrollHeight > lastScrollHeightRef . current ;
@@ -95,7 +101,8 @@ export default function MessageList({messages}: MessageListProps) {
95101 hasNewContent &&
96102 ( isFirstRender || isAtBottomRef . current || isNewUserMessage )
97103 ) {
98- scrollContainer . scrollTo ( {
104+ isProgrammaticScrollRef . current = true ;
105+ scrollAreaRef . scrollTo ( {
99106 top : currentScrollHeight ,
100107 behavior : isFirstRender ? "instant" : "smooth" ,
101108 } ) ;
@@ -105,7 +112,7 @@ export default function MessageList({messages}: MessageListProps) {
105112
106113 // Update the last known scroll height
107114 lastScrollHeightRef . current = currentScrollHeight ;
108- } , [ messages ] ) ;
115+ } , [ messages , scrollAreaRef ] ) ;
109116
110117 // If no messages, show a placeholder
111118 if ( messages . length === 0 ) {
@@ -117,7 +124,7 @@ export default function MessageList({messages}: MessageListProps) {
117124 }
118125
119126 return (
120- < div className = "overflow-y-auto flex-1" ref = { scrollAreaRef } >
127+ < div className = "overflow-y-auto flex-1" ref = { setScrollAreaRef } >
121128 < div
122129 className = "p-4 flex flex-col gap-4 max-w-4xl mx-auto transition-all duration-300 ease-in-out min-h-0" >
123130 { messages . map ( ( message , index ) => (
@@ -191,7 +198,6 @@ const ProcessedMessage = React.memo(function ProcessedMessage({
191198
192199 const linkedContent = useMemo ( ( ) => {
193200 return messageContent . split ( urlRegex ) . map ( ( content , idx ) => {
194- console . log ( content )
195201 if ( urlRegex . test ( content ) ) {
196202 return (
197203 < a
0 commit comments