Skip to content

Commit 5977071

Browse files
authored
Merge pull request #17 from telepace/feature/ui0311
ui更新,滚动逻辑调整
2 parents 96cf49b + 8734c38 commit 5977071

File tree

3 files changed

+142
-68
lines changed

3 files changed

+142
-68
lines changed

src/components/Chat/ChatWindow.tsx

+84-31
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,107 @@ const ChatWindow: React.FC = () => {
99
const messagesEndRef = useRef<HTMLDivElement>(null);
1010
const messagesContainerRef = useRef<HTMLDivElement>(null);
1111
const prevMessagesLengthRef = useRef(messages.length);
12-
const hasScrolledDuringStreamRef = useRef(false);
12+
const shouldScrollRef = useRef(true);
13+
const userScrolledRef = useRef(false);
1314

1415
// 自动滚动到底部
1516
const scrollToBottom = () => {
16-
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
17+
messagesEndRef.current?.scrollIntoView({
18+
behavior: 'auto',
19+
block: 'end'
20+
});
1721
};
1822

19-
// 检查用户是否在消息底部附近
20-
const isNearBottom = () => {
23+
// 检查是否在底部(100px误差范围内)
24+
const isAtBottom = () => {
2125
const container = messagesContainerRef.current;
2226
if (!container) return true;
2327

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+
}
2748
};
2849

29-
// 只在消息数量增加且用户在底部时滚动到底部
50+
// 添加滚动监听
3051
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);
3560
}
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+
});
3676
}
77+
3778
prevMessagesLengthRef.current = messages.length;
3879
}, [messages]);
3980

40-
// 处理流式输出时的滚动逻辑
81+
// 处理流式输出时的滚动
4182
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) {
5195
scrollToBottom();
52-
hasScrolledDuringStreamRef.current = true;
96+
animationFrameId = requestAnimationFrame(scrollLoop);
5397
}
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+
};
60111
}
61-
}, [isStreaming, messages]);
112+
}, [isStreaming]);
62113

63114
return (
64115
<Card
@@ -80,7 +131,9 @@ const ChatWindow: React.FC = () => {
80131
overflowY: 'auto',
81132
padding: '16px',
82133
display: 'flex',
83-
flexDirection: 'column'
134+
flexDirection: 'column',
135+
scrollBehavior: 'smooth',
136+
WebkitOverflowScrolling: 'touch'
84137
}}
85138
>
86139
{messages.length === 0 ? (

src/components/Chat/Message.tsx

+2-15
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,6 @@ const Message: React.FC<MessageProps> = ({ message, observationId }) => {
2626
// 检查当前消息是否正在流式输出
2727
const isCurrentlyStreaming = isStreaming && streamingMessageId === message.id;
2828

29-
// 当消息内容更新时,应用高亮动画
30-
/* useEffect(() => {
31-
if (isCurrentlyStreaming && messageRef.current) {
32-
messageRef.current.classList.add('message-highlight');
33-
34-
const timer = setTimeout(() => {
35-
messageRef.current?.classList.remove('message-highlight');
36-
}, 300);
37-
38-
return () => clearTimeout(timer);
39-
}
40-
}, [message.content, isCurrentlyStreaming]); */
41-
4229
// 解析消息中的链接
4330
useEffect(() => {
4431
if (!isCurrentlyStreaming && message.content) {
@@ -79,7 +66,7 @@ const Message: React.FC<MessageProps> = ({ message, observationId }) => {
7966
<div
8067
style={{
8168
display: 'flex',
82-
marginBottom: 16,
69+
marginBottom: 32,
8370
flexDirection: isUser ? 'row-reverse' : 'row'
8471
}}
8572
>
@@ -95,7 +82,7 @@ const Message: React.FC<MessageProps> = ({ message, observationId }) => {
9582
ref={messageRef}
9683
className={`message-bubble ${isCurrentlyStreaming ? 'streaming-message' : ''}`}
9784
style={{
98-
backgroundColor: isUser ? '#E7E6E4' : '#fff',
85+
backgroundColor: isUser ? '#E8E7E5' : '#f5f5f5',
9986
position: 'relative',
10087
transition: 'background-color 0.3s ease'
10188
}}

src/styles.css

+56-22
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ code {
3131
::-webkit-scrollbar-thumb:hover {
3232
background: #a8a8a8;
3333
}
34-
3534
/* 聊天消息气泡样式 */
3635
.message-bubble {
3736
max-width: 80%;
38-
padding: 2px 20px;
37+
min-width: 20%;
38+
padding: 16px 24px 8px;
3939
border-radius: 8px;
4040
position: relative;
4141
border: 1.5px solid #000;
42-
box-shadow: 3px 3px 0 rgba(0, 0, 0, 1);
42+
box-shadow: 3px 3px 0 rgba(0, 0, 0, 1);
4343
transition: all 0.3s ease;
4444
margin-bottom: 8px;
4545
}
@@ -226,11 +226,11 @@ body.dark-theme {
226226

227227
.typing-cursor {
228228
display: inline-block;
229-
width: 8px;
230-
height: 16px;
231-
background-color: #1890ff;
232-
margin-left: 4px;
233-
animation: blink 1s infinite;
229+
width: 1px;
230+
height: 1.2em;
231+
background-color: rgba(0, 0, 0, 0.75);
232+
margin-left: 2px;
233+
animation: blink 0.8s ease infinite;
234234
vertical-align: middle;
235235
}
236236

@@ -244,14 +244,11 @@ body.dark-theme {
244244
}
245245

246246
/* 流式消息样式 */
247-
/*.streaming-message {
248-
border-left: 3px solid #1890ff;
249-
} */
247+
.streaming-message {
248+
border: 1.5px solid #000;
249+
box-shadow: 3px 3px 0 rgba(0, 0, 0, 1);
250+
}
250251

251-
/* 消息高亮动画 */
252-
/*.message-highlight {
253-
animation: highlight 0.3s ease;
254-
} */
255252

256253
@keyframes highlight {
257254
0% {
@@ -262,15 +259,11 @@ body.dark-theme {
262259
}
263260
}
264261

265-
/* 消息内容过渡效果 */
266-
.message-bubble p {
267-
transition: opacity 0.2s ease;
268-
}
269262

270-
/* 流式输出时的消息容器样式 */
263+
/* 流式输出时的消息容器样式
271264
.message-bubble.streaming-message {
272265
box-shadow: 0 0 8px rgba(82, 196, 26, 0.3);
273-
}
266+
} */
274267

275268
/* 链接预览样式 */
276269
.link-previews {
@@ -340,4 +333,45 @@ body.dark-theme {
340333
.message-bubble {
341334
max-width: 90%;
342335
}
343-
}
336+
}
337+
338+
/* 消息内容中段落的间距 */
339+
.message-bubble p {
340+
transition: opacity 0.2s ease;
341+
margin-bottom: 8px;
342+
line-height: 1.3;
343+
}
344+
345+
346+
/* Markdown 标题样式 */
347+
.message-bubble h1 {
348+
margin-top: 32px;
349+
margin-bottom: 16px;
350+
}
351+
352+
.message-bubble h2 {
353+
margin-top: 32px;
354+
margin-bottom: 14px;
355+
}
356+
357+
.message-bubble h3 {
358+
margin-top: 32px;
359+
margin-bottom: 12px;
360+
}
361+
362+
.message-bubble h4 {
363+
margin-top: 32px;
364+
margin-bottom: 10px;
365+
}
366+
367+
.message-bubble h5, .message-bubble h6 {
368+
margin-top: 24px;
369+
margin-bottom: 8px;
370+
}
371+
372+
/* 第一个标题不需要上边距 */
373+
.message-bubble *:first-child {
374+
margin-top: 0;
375+
}
376+
377+

0 commit comments

Comments
 (0)