@@ -62,11 +62,36 @@ export function MessageHistoryView({ messages, responding, abortController, clas
6262 const SCROLL_MEMORY_KEY = 'chat_scrollTop' ;
6363 const lastScrollHeightRef = useRef ( 0 ) ;
6464 const lastScrollSetTsRef = useRef ( 0 ) ;
65+ const lastMessageIdRef = useRef < string | null > ( null ) ;
66+ const getDynamicThreshold = useCallback ( ( el : HTMLDivElement | null , base : number ) => {
67+ const h = el ?. clientHeight ?? 0 ;
68+ const d = Math . floor ( h * 0.15 ) ;
69+ const t = Math . max ( 50 , d ) ;
70+ return Math . max ( 50 , Math . min ( base , t ) ) ;
71+ } , [ ] ) ;
6572 const hasAssistantContent = useMemo ( ( ) => {
6673 return messages . some (
6774 ( m ) => m . role === "assistant" && m . type === "text" && typeof m . content === "string" && m . content . trim ( ) . length > 0 ,
6875 ) ;
6976 } , [ messages ] ) ;
77+
78+ // 当开始生成助手回复时,主动滚动到底部,避免需要手动拖动
79+ useEffect ( ( ) => {
80+ if ( ! responding ) return ;
81+ setIsLockedByUser ( false ) ;
82+ const container = scrollElRef . current ?? containerRef . current ;
83+ if ( container ) {
84+ requestAnimationFrame ( ( ) => {
85+ container . scrollTop = container . scrollHeight ;
86+ lastScrollHeightRef . current = container . scrollHeight ;
87+ lastScrollSetTsRef . current = ( typeof performance !== 'undefined' ? performance . now ( ) : Date . now ( ) ) ;
88+ } ) ;
89+ } else if ( endRef . current ) {
90+ requestAnimationFrame ( ( ) => {
91+ endRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
92+ } ) ;
93+ }
94+ } , [ responding ] ) ;
7095 const showInlineSpinner = responding && ! hasAssistantContent ;
7196
7297 // 防抖处理滚动事件
@@ -115,8 +140,9 @@ export function MessageHistoryView({ messages, responding, abortController, clas
115140
116141 const { scrollTop, scrollHeight, clientHeight } = container ;
117142 const distanceToBottom = scrollHeight - scrollTop - clientHeight ;
118- const isAtBottom = distanceToBottom < 50 ;
119- setIsNearBottom ( distanceToBottom < 100 ) ;
143+ const t = getDynamicThreshold ( container , 100 ) ;
144+ const isAtBottom = distanceToBottom < Math . floor ( t * 0.5 ) ;
145+ setIsNearBottom ( distanceToBottom < t ) ;
120146
121147 if ( ! isAtBottom ) {
122148 setIsUserScrolling ( true ) ;
@@ -156,8 +182,9 @@ export function MessageHistoryView({ messages, responding, abortController, clas
156182 container . scrollTop += e . deltaY ;
157183 const { scrollTop, scrollHeight, clientHeight } = container ;
158184 const distanceToBottom = scrollHeight - scrollTop - clientHeight ;
159- setIsNearBottom ( distanceToBottom < 100 ) ;
160- if ( distanceToBottom >= 100 ) {
185+ const t = getDynamicThreshold ( container , 100 ) ;
186+ setIsNearBottom ( distanceToBottom < t ) ;
187+ if ( distanceToBottom >= t ) {
161188 setIsLockedByUser ( true ) ;
162189 }
163190 } ;
@@ -236,30 +263,34 @@ export function MessageHistoryView({ messages, responding, abortController, clas
236263 setIsLockedByUser ( true ) ;
237264 } ) ;
238265 }
266+ } else {
267+ requestAnimationFrame ( ( ) => {
268+ container . scrollTop = container . scrollHeight ;
269+ setIsLockedByUser ( false ) ;
270+ lastScrollHeightRef . current = container . scrollHeight ;
271+ lastScrollSetTsRef . current = ( typeof performance !== 'undefined' ? performance . now ( ) : Date . now ( ) ) ;
272+ } ) ;
239273 }
240274 } catch { }
241275 } , [ ] ) ;
242276
243277 // 优化的自动滚动逻辑
244278 useEffect ( ( ) => {
245- if ( ! isUserScrolling && isWindowVisible && ! isResizing ) {
279+ if ( ! isUserScrolling && isWindowVisible && ! isResizing && ! isLockedByUser ) {
246280 const scrollToBottom = ( ) => {
247281 if ( rafScrollPendingRef . current ) return ;
248282 rafScrollPendingRef . current = true ;
249283 requestAnimationFrame ( ( ) => {
250284 const container = scrollElRef . current ?? containerRef . current ;
251285 if ( container ) {
252286 const { scrollTop, scrollHeight, clientHeight } = container ;
253- const nearBottom = scrollHeight - scrollTop - clientHeight < 100 ;
254- if ( nearBottom && ! isLockedByUser ) {
255- const now = ( typeof performance !== 'undefined' ? performance . now ( ) : Date . now ( ) ) ;
256- const changed = scrollHeight > ( lastScrollHeightRef . current + 8 ) ;
257- const enoughTime = now - lastScrollSetTsRef . current > 50 ;
258- if ( changed && enoughTime ) {
259- container . scrollTop = scrollHeight ;
260- lastScrollHeightRef . current = scrollHeight ;
261- lastScrollSetTsRef . current = now ;
262- }
287+ const now = ( typeof performance !== 'undefined' ? performance . now ( ) : Date . now ( ) ) ;
288+ const changed = scrollHeight > ( lastScrollHeightRef . current + 4 ) ;
289+ const enoughTime = now - lastScrollSetTsRef . current > 32 ;
290+ if ( changed && enoughTime ) {
291+ container . scrollTop = scrollHeight ;
292+ lastScrollHeightRef . current = scrollHeight ;
293+ lastScrollSetTsRef . current = now ;
263294 }
264295 }
265296 rafScrollPendingRef . current = false ;
@@ -273,6 +304,44 @@ export function MessageHistoryView({ messages, responding, abortController, clas
273304 }
274305 } , [ messages , responding , isUserScrolling , isWindowVisible , isResizing , isLockedByUser ] ) ;
275306
307+ // 当用户发送新消息时,强制滚动到底部以显示最新内容
308+ useEffect ( ( ) => {
309+ const last = messages [ messages . length - 1 ] ;
310+ if ( ! last ) return ;
311+ const lastId = String ( last . id ?? "" ) ;
312+ const isNewLast = lastMessageIdRef . current !== lastId ;
313+ lastMessageIdRef . current = lastId ;
314+ if ( isNewLast && last . role === "user" ) {
315+ setIsLockedByUser ( false ) ;
316+ const container = scrollElRef . current ?? containerRef . current ;
317+ if ( container ) {
318+ requestAnimationFrame ( ( ) => {
319+ container . scrollTop = container . scrollHeight ;
320+ lastScrollHeightRef . current = container . scrollHeight ;
321+ lastScrollSetTsRef . current = ( typeof performance !== 'undefined' ? performance . now ( ) : Date . now ( ) ) ;
322+ } ) ;
323+ } else if ( endRef . current ) {
324+ requestAnimationFrame ( ( ) => {
325+ endRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
326+ } ) ;
327+ }
328+ }
329+ if ( isNewLast && last . role === "assistant" && ! isLockedByUser ) {
330+ const container = scrollElRef . current ?? containerRef . current ;
331+ if ( container ) {
332+ requestAnimationFrame ( ( ) => {
333+ container . scrollTop = container . scrollHeight ;
334+ lastScrollHeightRef . current = container . scrollHeight ;
335+ lastScrollSetTsRef . current = ( typeof performance !== 'undefined' ? performance . now ( ) : Date . now ( ) ) ;
336+ } ) ;
337+ } else if ( endRef . current ) {
338+ requestAnimationFrame ( ( ) => {
339+ endRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
340+ } ) ;
341+ }
342+ }
343+ } , [ messages ] ) ;
344+
276345 return (
277346 < div
278347 ref = { containerRef }
0 commit comments