1- import { useEffect , useMemo , useRef , useState } from "react" ;
1+ import { useEffect , useMemo , useRef , useState , useCallback } from "react" ;
22import { MessageCard } from "../../../components/message-card" ;
33import { Conversation } from "../../../pkg/gen/apiclient/chat/v2/chat_pb" ;
44import {
@@ -29,7 +29,6 @@ export const ChatBody = ({ conversation }: ChatBodyProps) => {
2929 const setCurrentConversation = useConversationStore ( ( s ) => s . setCurrentConversation ) ;
3030 const chatContainerRef = useRef < HTMLDivElement > ( null ) ;
3131 const lastUserMsgRef = useRef < HTMLDivElement > ( null ) ;
32- const expanderRef = useRef < HTMLDivElement > ( null ) ;
3332 const [ reloadSuccess , setReloadSuccess ] = useState ( ReloadStatus . Default ) ;
3433
3534 const conversationMode = useSettingStore ( ( s ) => s . conversationMode ) ;
@@ -54,42 +53,34 @@ export const ChatBody = ({ conversation }: ChatBodyProps) => {
5453 [ visibleMessages ]
5554 ) ;
5655
57- // Scroll to the top of the last user message
58- useEffect ( ( ) => {
59- if ( expanderRef . current ) {
60- expanderRef . current . style . height = "1000px" ;
61- }
62-
63- const chatContainerHeight = chatContainerRef . current ?. clientHeight ?? 0 ;
64- const expanderViewOffset =
65- ( expanderRef . current ?. getBoundingClientRect ( ) . top ?? 0 ) -
66- ( chatContainerRef . current ?. getBoundingClientRect ( ) . y ?? 0 ) ;
67-
68- let expanderHeight : number ;
69- if ( expanderViewOffset < 0 ) {
70- expanderHeight = 0 ;
71- } else {
72- expanderHeight = chatContainerHeight - expanderViewOffset ;
73- }
74-
75- if ( expanderRef . current ) {
76- const lastUserMsgHeight = lastUserMsgRef . current ?. clientHeight ?? 0 ;
77- expanderRef . current . style . height = chatContainerHeight - lastUserMsgHeight - 8 + "px" ;
78- }
56+ // Get the last user message ID to track when it changes
57+ const lastUserMessageId = useMemo ( ( ) => {
58+ if ( lastUserMessageIndex === - 1 ) return null ;
59+ return visibleMessages [ lastUserMessageIndex ] ?. id ?? null ;
60+ } , [ visibleMessages , lastUserMessageIndex ] ) ;
61+
62+ // Scroll the last user message to the top of the viewport (container only)
63+ const scrollToLastUserMessage = useCallback ( ( ) => {
64+ if ( ! lastUserMsgRef . current || ! chatContainerRef . current ) return ;
65+
66+ const container = chatContainerRef . current ;
67+ const target = lastUserMsgRef . current ;
68+
69+ container . scrollTo ( {
70+ top : target . offsetTop ,
71+ behavior : "smooth" ,
72+ } ) ;
73+ } , [ ] ) ;
7974
80- if ( lastUserMsgRef . current && chatContainerRef . current ) {
81- const container = chatContainerRef . current ;
82- const target = lastUserMsgRef . current ;
83- container . scrollTo ( {
84- top : target . offsetTop ,
85- behavior : "smooth" ,
86- } ) ;
87- } else {
88- if ( expanderRef . current ) {
89- expanderRef . current . style . height = ( expanderHeight < 0 ? 0 : expanderHeight ) + "px" ;
90- }
91- }
92- } , [ visibleMessages . length ] ) ;
75+ // Auto-scroll only when a new user message is added
76+ useEffect ( ( ) => {
77+ if ( ! lastUserMessageId ) return ;
78+
79+ // Use requestAnimationFrame to ensure DOM has updated
80+ requestAnimationFrame ( ( ) => {
81+ scrollToLastUserMessage ( ) ;
82+ } ) ;
83+ } , [ lastUserMessageId , scrollToLastUserMessage ] ) ;
9384
9485 // Render all messages using the unified DisplayMessage array
9586 const messageCards = useMemo (
@@ -119,38 +110,20 @@ export const ChatBody = ({ conversation }: ChatBodyProps) => {
119110 return < EmptyView /> ;
120111 }
121112
122- const expander = (
123- < div
124- style = { {
125- height : "0px" ,
126- backgroundColor : "transparent" ,
127- position : "absolute" ,
128- top : 0 ,
129- left : 0 ,
130- right : 0 ,
131- zIndex : 0 ,
132- pointerEvents : "none" ,
133- } }
134- aria-hidden = "true"
135- id = "expander"
136- ref = { expanderRef }
137- />
138- ) ;
139-
140113 return (
141114 < div className = "pd-app-tab-content-body" id = "pd-chat-item-container" ref = { chatContainerRef } >
142- < div id = "pd-chat-item-container-messages" style = { { zIndex : 3 } } >
115+ { /* Spacer that pushes content down and provides scroll space for last user message */ }
116+ < div className = "flex-1 min-h-0" aria-hidden = "true" />
117+
118+ < div id = "pd-chat-item-container-messages" >
143119 { messageCards }
144120 </ div >
145121
146- < div id = "pd-chat-item-container-status" style = { { position : "relative" } } >
147- < div id = "pd-chat-item-container-status-indicator" style = { { position : "relative" , zIndex : 2 } } >
148- < StatusIndicator conversation = { conversation } />
149- </ div >
150-
151- { expander }
122+ < div id = "pd-chat-item-container-status" className = "relative" >
123+ < StatusIndicator conversation = { conversation } />
124+
152125 { isDebugMode && (
153- < div className = "text-xs text-gray-300 z-1 noselect" >
126+ < div className = "text-xs text-gray-300 noselect" >
154127 < span > * Debug mode is enabled, </ span >
155128 < span
156129 className = { `${ reloadSuccess ? "text-emerald-300" : "text-gray-300" } underline cursor-pointer rnd-cancel` }
@@ -178,6 +151,13 @@ export const ChatBody = ({ conversation }: ChatBodyProps) => {
178151 </ div >
179152 ) }
180153 </ div >
154+
155+ { /* Bottom spacer to allow scrolling the last user message to the top */ }
156+ < div
157+ className = "flex-shrink-0"
158+ style = { { minHeight : "calc(100% - 80px)" } }
159+ aria-hidden = "true"
160+ />
181161 </ div >
182162 ) ;
183163} ;
0 commit comments