Skip to content

Commit f4bb54f

Browse files
committed
feat: Implement auto-scrolling functionality in MessageList component, allowing users to control automatic scrolling behavior based on their interactions, enhancing user experience during chat sessions
1 parent 1f09f9e commit f4bb54f

File tree

2 files changed

+62
-13
lines changed

2 files changed

+62
-13
lines changed

src/renderer/src/components/pages/chat/MessageItem.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,6 @@ export default function MessageItem({
156156
onBranchNext?.(message.id)
157157
}
158158

159-
const getCurrentModel = () => {
160-
return llmConfigs.find((config) => config.id === message.modelId)
161-
}
162-
163159
const handleToggleCollapse = () => {
164160
onToggleCollapse?.(message.id)
165161
}

src/renderer/src/components/pages/chat/MessageList.tsx

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import React, { useRef, useEffect, useMemo } from 'react'
1+
import React, { useRef, useEffect, useMemo, useState, useCallback } from 'react'
22
import { ChatMessage, LLMConfig } from '../../../types/type'
33
import MessageItem from './MessageItem'
44
import WelcomeMessage from './WelcomeMessage'
55
import { MessageTree } from './messageTree'
6+
import { useStreamingMessage } from '../../../stores/messagesStore'
67

78
interface 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

Comments
 (0)