@@ -13,6 +13,7 @@ interface MessageListProps {
1313 streamingContent ?: string
1414 streamingTimestamp ?: number
1515 llmConfigs ?: LLMConfig [ ]
16+ selectedMessageId ?: string | null // 新增:选中的消息ID,用于滚动定位
1617 onRetryMessage ?: ( messageId : string ) => void
1718 onEditMessage ?: ( messageId : string , newContent : string ) => void
1819 onEditAndResendMessage ?: ( messageId : string , newContent : string ) => void
@@ -35,6 +36,7 @@ const MessageList = React.memo(function MessageList({
3536 isLoading = false ,
3637 streamingContent,
3738 llmConfigs = [ ] ,
39+ selectedMessageId,
3840 onRetryMessage,
3941 onEditMessage,
4042 onEditAndResendMessage,
@@ -48,9 +50,11 @@ const MessageList = React.memo(function MessageList({
4850} : MessageListProps ) {
4951 const messagesEndRef = useRef < HTMLDivElement > ( null )
5052 const messagesContainerRef = useRef < HTMLDivElement > ( null )
53+ const messageRefs = useRef < Map < string , HTMLDivElement > > ( new Map ( ) )
5154 const prevMessagesLength = useRef < number > ( 0 )
5255 const prevStreamingContent = useRef < string > ( '' )
5356 const prevCurrentPath = useRef < string [ ] > ( [ ] )
57+ const prevSelectedMessageId = useRef < string | null > ( null )
5458 const isInitialRender = useRef < boolean > ( true )
5559
5660 // 控制是否自动滚动到底部
@@ -128,32 +132,51 @@ const MessageList = React.memo(function MessageList({
128132 messagesEndRef . current ?. scrollIntoView ( { behavior } )
129133 }
130134
135+ // 滚动到指定的消息
136+ const scrollToMessage = ( messageId : string , behavior : 'smooth' | 'instant' = 'smooth' ) => {
137+ const messageElement = messageRefs . current . get ( messageId )
138+ if ( messageElement ) {
139+ messageElement . scrollIntoView ( { behavior, block : 'center' } )
140+ }
141+ }
142+
131143 useEffect ( ( ) => {
132144 const currentMessagesLength = messages . length
133145 const currentPathString = JSON . stringify ( currentPath )
134146 const prevPathString = JSON . stringify ( prevCurrentPath . current )
135147
136148 // 检查路径是否发生变化(排除初次渲染)
137149 const pathChanged = ! isInitialRender . current && currentPathString !== prevPathString
150+
151+ // 检查选中消息是否发生变化
152+ const selectedMessageChanged = ! isInitialRender . current && selectedMessageId !== prevSelectedMessageId . current
138153
139- // 只在以下情况下滚动到底部:
140- // 1. 消息总数增加(有新消息)
141- // 2. 流式内容发生变化(正在接收AI回复)
142- // 3. 初次渲染且有消息
143- // 4. 当前路径发生变化(用户点击消息树切换分支)
144- const shouldScroll =
154+ // 只在以下情况下滚动:
155+ // 1. 消息总数增加(有新消息)- 滚动到底部
156+ // 2. 流式内容发生变化(正在接收AI回复)- 滚动到底部
157+ // 3. 初次渲染且有消息 - 滚动到底部
158+ // 4. 当前路径发生变化(用户点击消息树切换分支)- 滚动到底部
159+ // 5. 选中消息发生变化(用户点击消息树中的特定消息)- 滚动到选中消息
160+ const shouldScrollToBottom =
145161 currentMessagesLength > prevMessagesLength . current ||
146162 currentStreamingContent !== prevStreamingContent . current ||
147163 ( isInitialRender . current && currentMessagesLength > 0 ) ||
148- pathChanged
164+ ( pathChanged && ! selectedMessageChanged )
165+
166+ const shouldScrollToMessage = selectedMessageChanged && selectedMessageId
149167
150168 console . count ( 'shouldScroll' ) ;
151169
152170 // 只有在启用自动滚动时才执行滚动
153- if ( shouldScroll && isAutoScrollEnabled ) {
154- // 初次渲染时直接跳转到底部,其他情况平滑滚动
155- const behavior = isInitialRender . current ? 'instant' : 'smooth'
156- scrollToBottom ( behavior )
171+ if ( isAutoScrollEnabled ) {
172+ if ( shouldScrollToMessage ) {
173+ // 滚动到选中的消息
174+ scrollToMessage ( selectedMessageId ! , 'smooth' )
175+ } else if ( shouldScrollToBottom ) {
176+ // 滚动到底部
177+ const behavior = isInitialRender . current ? 'instant' : 'smooth'
178+ scrollToBottom ( behavior )
179+ }
157180 }
158181
159182 // 标记初次渲染已完成
@@ -165,7 +188,8 @@ const MessageList = React.memo(function MessageList({
165188 prevMessagesLength . current = currentMessagesLength
166189 prevStreamingContent . current = currentStreamingContent
167190 prevCurrentPath . current = [ ...currentPath ]
168- } , [ messages . length , currentStreamingContent , currentPath , isAutoScrollEnabled ] )
191+ prevSelectedMessageId . current = selectedMessageId
192+ } , [ messages . length , currentStreamingContent , currentPath , selectedMessageId , isAutoScrollEnabled ] )
169193
170194 // 处理兄弟分支切换
171195 const handleSiblingBranchSwitch = ( messageId : string , direction : 'previous' | 'next' ) => {
@@ -199,30 +223,40 @@ const MessageList = React.memo(function MessageList({
199223 < div className = "messages-container" ref = { messagesContainerRef } >
200224 < div className = "messages-list" >
201225 { displayMessages . map ( ( message , index ) => (
202- < MessageItem
226+ < div
203227 key = { message . id }
204- message = { message }
205- isLoading = { isLoading }
206- isLastMessage = { index === displayMessages . length - 1 && ! streamingContent }
207- llmConfigs = { llmConfigs }
208- chatId = { chatId } // 传递chatId
209- // 分支导航props - 所有消息都使用兄弟分支导航
210- hasChildBranches = { messageTree . hasSiblingBranches ( message . id ) }
211- branchIndex = { messageTree . getCurrentSiblingBranchIndex ( message . id ) }
212- branchCount = { messageTree . getSiblingBranchCount ( message . id ) }
213- onBranchPrevious = { ( messageId ) => handleSiblingBranchSwitch ( messageId , 'previous' ) }
214- onBranchNext = { ( messageId ) => handleSiblingBranchSwitch ( messageId , 'next' ) }
215- // 原有的回调
216- onRetry = { onRetryMessage }
217- onEdit = { onEditMessage }
218- onEditAndResend = { onEditAndResendMessage }
219- onToggleFavorite = { onToggleFavorite }
220- onModelChange = { onModelChange }
221- onDelete = { onDeleteMessage }
222- // 折叠相关
223- isCollapsed = { collapsedMessages . includes ( message . id ) }
224- onToggleCollapse = { onToggleMessageCollapse }
225- />
228+ ref = { ( el ) => {
229+ if ( el ) {
230+ messageRefs . current . set ( message . id , el )
231+ } else {
232+ messageRefs . current . delete ( message . id )
233+ }
234+ } }
235+ >
236+ < MessageItem
237+ message = { message }
238+ isLoading = { isLoading }
239+ isLastMessage = { index === displayMessages . length - 1 && ! streamingContent }
240+ llmConfigs = { llmConfigs }
241+ chatId = { chatId } // 传递chatId
242+ // 分支导航props - 所有消息都使用兄弟分支导航
243+ hasChildBranches = { messageTree . hasSiblingBranches ( message . id ) }
244+ branchIndex = { messageTree . getCurrentSiblingBranchIndex ( message . id ) }
245+ branchCount = { messageTree . getSiblingBranchCount ( message . id ) }
246+ onBranchPrevious = { ( messageId ) => handleSiblingBranchSwitch ( messageId , 'previous' ) }
247+ onBranchNext = { ( messageId ) => handleSiblingBranchSwitch ( messageId , 'next' ) }
248+ // 原有的回调
249+ onRetry = { onRetryMessage }
250+ onEdit = { onEditMessage }
251+ onEditAndResend = { onEditAndResendMessage }
252+ onToggleFavorite = { onToggleFavorite }
253+ onModelChange = { onModelChange }
254+ onDelete = { onDeleteMessage }
255+ // 折叠相关
256+ isCollapsed = { collapsedMessages . includes ( message . id ) }
257+ onToggleCollapse = { onToggleMessageCollapse }
258+ />
259+ </ div >
226260 ) ) }
227261
228262 < div ref = { messagesEndRef } />
0 commit comments