1- import React , { useRef , useEffect , useMemo } from 'react'
1+ import React , { useRef , useEffect , useMemo , useState , useCallback } from 'react'
22import { ChatMessage , LLMConfig } from '../../../types/type'
33import MessageItem from './MessageItem'
44import WelcomeMessage from './WelcomeMessage'
55import { MessageTree } from './messageTree'
6+ import { useStreamingMessage } from '../../../stores/messagesStore'
67
78interface MessageListProps {
89 chatId : string // 添加chatId prop
@@ -46,10 +47,14 @@ export default function MessageList({
4647 onOpenSettings
4748} : MessageListProps ) {
4849 const messagesEndRef = useRef < HTMLDivElement > ( null )
50+ const messagesContainerRef = useRef < HTMLDivElement > ( null )
4951 const prevMessagesLength = useRef < number > ( 0 )
5052 const prevStreamingContent = useRef < string > ( '' )
5153 const prevCurrentPath = useRef < string [ ] > ( [ ] )
5254 const isInitialRender = useRef < boolean > ( true )
55+
56+ // 控制是否自动滚动到底部
57+ const [ isAutoScrollEnabled , setIsAutoScrollEnabled ] = useState ( true )
5358
5459 // 使用传入的消息树(从父组件创建,避免重复创建)
5560 const messageTree = useMemo ( ( ) => {
@@ -69,13 +74,58 @@ export default function MessageList({
6974 }
7075 } , [ messages , currentPath , messageTree ] )
7176
77+ // 获取最后一条消息的ID,用于订阅其流式内容
78+ const lastMessageId = displayMessages . length > 0 ? displayMessages [ displayMessages . length - 1 ] . id : null
79+
80+ // 只订阅最后一条消息的流式内容
81+ const lastMessageStreaming = useStreamingMessage ( chatId , lastMessageId || '' )
82+
83+ // 计算当前的流式内容,用于触发滚动
84+ const currentStreamingContent = lastMessageStreaming ?. content || ''
85+
86+ // 检查是否已经滚动到底部
87+ const isAtBottom = useCallback ( ( ) => {
88+ const container = messagesContainerRef . current
89+ if ( ! container ) return true
90+
91+ const threshold = 20 // 20px 的阈值
92+ const { scrollTop, scrollHeight, clientHeight } = container
93+ return scrollHeight - scrollTop - clientHeight <= threshold
94+ } , [ ] )
95+
96+ // 处理用户滚动事件
97+ const handleScroll = useCallback ( ( ) => {
98+ if ( isInitialRender . current ) return // 忽略初次渲染时的滚动事件
99+
100+ const atBottom = isAtBottom ( )
101+
102+ if ( atBottom && ! isAutoScrollEnabled ) {
103+ // 用户滚动到底部,恢复自动滚动
104+ setIsAutoScrollEnabled ( true )
105+ } else if ( ! atBottom && isAutoScrollEnabled ) {
106+ // 用户向上滚动,停止自动滚动
107+ setIsAutoScrollEnabled ( false )
108+ }
109+ } , [ isAutoScrollEnabled , isAtBottom ] )
110+
111+ // 添加和移除滚动事件监听器
112+ useEffect ( ( ) => {
113+ const container = messagesContainerRef . current
114+ if ( ! container ) return
115+
116+ container . addEventListener ( 'scroll' , handleScroll , { passive : true } )
117+
118+ return ( ) => {
119+ container . removeEventListener ( 'scroll' , handleScroll )
120+ }
121+ } , [ handleScroll ] )
122+
72123 const scrollToBottom = ( behavior : 'smooth' | 'instant' = 'smooth' ) => {
73124 messagesEndRef . current ?. scrollIntoView ( { behavior } )
74125 }
75126
76127 useEffect ( ( ) => {
77128 const currentMessagesLength = messages . length
78- const currentStreamingContent = streamingContent || ''
79129 const currentPathString = JSON . stringify ( currentPath )
80130 const prevPathString = JSON . stringify ( prevCurrentPath . current )
81131
@@ -93,22 +143,25 @@ export default function MessageList({
93143 ( isInitialRender . current && currentMessagesLength > 0 ) ||
94144 pathChanged
95145
96- if ( shouldScroll ) {
146+ console . count ( 'shouldScroll' ) ;
147+
148+ // 只有在启用自动滚动时才执行滚动
149+ if ( shouldScroll && isAutoScrollEnabled ) {
97150 // 初次渲染时直接跳转到底部,其他情况平滑滚动
98151 const behavior = isInitialRender . current ? 'instant' : 'smooth'
99152 scrollToBottom ( behavior )
153+ }
100154
101- // 标记初次渲染已完成
102- if ( isInitialRender . current ) {
103- isInitialRender . current = false
104- }
155+ // 标记初次渲染已完成
156+ if ( isInitialRender . current ) {
157+ isInitialRender . current = false
105158 }
106159
107160 // 更新引用值
108161 prevMessagesLength . current = currentMessagesLength
109162 prevStreamingContent . current = currentStreamingContent
110163 prevCurrentPath . current = [ ...currentPath ]
111- } , [ messages . length , streamingContent , currentPath ] )
164+ } , [ messages . length , currentStreamingContent , currentPath , isAutoScrollEnabled ] )
112165
113166 // 处理兄弟分支切换
114167 const handleSiblingBranchSwitch = ( messageId : string , direction : 'previous' | 'next' ) => {
@@ -139,7 +192,7 @@ export default function MessageList({
139192 }
140193
141194 return (
142- < div className = "messages-container" >
195+ < div className = "messages-container" ref = { messagesContainerRef } >
143196 < div className = "messages-list" >
144197 { displayMessages . map ( ( message , index ) => (
145198 < MessageItem
0 commit comments