@@ -9,56 +9,107 @@ const ChatWindow: React.FC = () => {
9
9
const messagesEndRef = useRef < HTMLDivElement > ( null ) ;
10
10
const messagesContainerRef = useRef < HTMLDivElement > ( null ) ;
11
11
const prevMessagesLengthRef = useRef ( messages . length ) ;
12
- const hasScrolledDuringStreamRef = useRef ( false ) ;
12
+ const shouldScrollRef = useRef ( true ) ;
13
+ const userScrolledRef = useRef ( false ) ;
13
14
14
15
// 自动滚动到底部
15
16
const scrollToBottom = ( ) => {
16
- messagesEndRef . current ?. scrollIntoView ( { behavior : 'smooth' } ) ;
17
+ messagesEndRef . current ?. scrollIntoView ( {
18
+ behavior : 'auto' ,
19
+ block : 'end'
20
+ } ) ;
17
21
} ;
18
22
19
- // 检查用户是否在消息底部附近
20
- const isNearBottom = ( ) => {
23
+ // 检查是否在底部(100px误差范围内)
24
+ const isAtBottom = ( ) => {
21
25
const container = messagesContainerRef . current ;
22
26
if ( ! container ) return true ;
23
27
24
- // 判断滚动位置是否接近底部(20px误差范围内)
25
- const isAtBottom = container . scrollHeight - container . scrollTop - container . clientHeight < 20 ;
26
- return isAtBottom ;
28
+ // 如果内容高度小于容器高度,说明没有滚动条,视为在底部
29
+ if ( container . scrollHeight <= container . clientHeight ) {
30
+ return true ;
31
+ }
32
+
33
+ return container . scrollHeight - container . scrollTop - container . clientHeight < 150 ;
34
+ } ;
35
+
36
+ // 处理滚动事件
37
+ const handleScroll = ( ) => {
38
+ // 记录用户是否在底部
39
+ shouldScrollRef . current = isAtBottom ( ) ;
40
+
41
+ // 只有在流式输出时且用户明确向上滚动时才标记为用户滚动
42
+ if ( isStreaming ) {
43
+ const container = messagesContainerRef . current ;
44
+ if ( container && container . scrollHeight - container . scrollTop - container . clientHeight > 150 ) {
45
+ userScrolledRef . current = true ;
46
+ }
47
+ }
27
48
} ;
28
49
29
- // 只在消息数量增加且用户在底部时滚动到底部
50
+ // 添加滚动监听
30
51
useEffect ( ( ) => {
31
- if ( messages . length > prevMessagesLengthRef . current ) {
32
- // 只有当用户在底部附近时才自动滚动
33
- if ( isNearBottom ( ) ) {
34
- scrollToBottom ( ) ;
52
+ const container = messagesContainerRef . current ;
53
+ if ( container ) {
54
+ container . addEventListener ( 'scroll' , handleScroll ) ;
55
+ }
56
+
57
+ return ( ) => {
58
+ if ( container ) {
59
+ container . removeEventListener ( 'scroll' , handleScroll ) ;
35
60
}
61
+ } ;
62
+ } , [ isStreaming ] ) ;
63
+
64
+ // 处理消息更新
65
+ useEffect ( ( ) => {
66
+ // 当消息数量增加时
67
+ if ( messages . length > prevMessagesLengthRef . current ) {
68
+ // 使用 requestAnimationFrame 确保在下一帧渲染后再检查
69
+ requestAnimationFrame ( ( ) => {
70
+ // 消息少于3条时总是滚动到底部
71
+ const messageCount = Math . ceil ( messages . length / 2 ) ; // 计算对话轮次
72
+ if ( messageCount < 3 || shouldScrollRef . current ) {
73
+ scrollToBottom ( ) ;
74
+ }
75
+ } ) ;
36
76
}
77
+
37
78
prevMessagesLengthRef . current = messages . length ;
38
79
} , [ messages ] ) ;
39
80
40
- // 处理流式输出时的滚动逻辑
81
+ // 处理流式输出时的滚动
41
82
useEffect ( ( ) => {
42
- // 当流式输出开始时,重置滚动标记
43
- if ( isStreaming && ! hasScrolledDuringStreamRef . current ) {
44
- // 检查最后一条消息是否至少有3行,且用户在底部附近
45
- const lastMessage = messages [ messages . length - 1 ] ;
46
- if ( lastMessage && lastMessage . role === 'assistant' && isNearBottom ( ) ) {
47
- const lineCount = ( lastMessage . content . match ( / \n / g) || [ ] ) . length + 1 ;
48
-
49
- // 当达到至少3行且尚未滚动过时,执行一次滚动
50
- if ( lineCount >= 3 && ! hasScrolledDuringStreamRef . current ) {
83
+ // 流式开始时重置用户滚动标记
84
+ if ( isStreaming ) {
85
+ userScrolledRef . current = false ;
86
+
87
+ // 初始滚动到底部
88
+ scrollToBottom ( ) ;
89
+
90
+ // 使用 requestAnimationFrame 而不是 setInterval 来实现更平滑的滚动
91
+ let animationFrameId : number ;
92
+
93
+ const scrollLoop = ( ) => {
94
+ if ( isStreaming && ! userScrolledRef . current ) {
51
95
scrollToBottom ( ) ;
52
- hasScrolledDuringStreamRef . current = true ;
96
+ animationFrameId = requestAnimationFrame ( scrollLoop ) ;
53
97
}
54
- }
55
- }
56
-
57
- // 流式输出结束时,重置标记
58
- if ( ! isStreaming ) {
59
- hasScrolledDuringStreamRef . current = false ;
98
+ } ;
99
+
100
+ // 启动滚动循环
101
+ animationFrameId = requestAnimationFrame ( scrollLoop ) ;
102
+
103
+ return ( ) => {
104
+ // 清理
105
+ cancelAnimationFrame ( animationFrameId ) ;
106
+ // 流式输出结束时重置用户滚动标记
107
+ setTimeout ( ( ) => {
108
+ userScrolledRef . current = false ;
109
+ } , 500 ) ;
110
+ } ;
60
111
}
61
- } , [ isStreaming , messages ] ) ;
112
+ } , [ isStreaming ] ) ;
62
113
63
114
return (
64
115
< Card
@@ -80,7 +131,9 @@ const ChatWindow: React.FC = () => {
80
131
overflowY : 'auto' ,
81
132
padding : '16px' ,
82
133
display : 'flex' ,
83
- flexDirection : 'column'
134
+ flexDirection : 'column' ,
135
+ scrollBehavior : 'smooth' ,
136
+ WebkitOverflowScrolling : 'touch'
84
137
} }
85
138
>
86
139
{ messages . length === 0 ? (
0 commit comments