- {messages.length === 0 && !streaming.active ? (
+ {isEmpty ? (
{isImageUrl(agent.avatar) ? (
@@ -141,6 +186,8 @@ export default function ChatBody(props: {
)}
+ {/* 底部锚点:用于辅助滚动定位 */}
+
>
)}
diff --git a/src/pages/chat/components/ChatPageH5.css b/src/pages/chat/components/ChatPageH5.css
index c788ba9..eecc9df 100644
--- a/src/pages/chat/components/ChatPageH5.css
+++ b/src/pages/chat/components/ChatPageH5.css
@@ -1,21 +1,35 @@
@media (max-width: 768px) {
.h5-chat-shell {
- /* 移除固定高度,允许跟随全局滚动容器 */
+ height: 100vh;
+ height: 100svh;
display: flex;
flex-direction: column;
+ overflow: hidden;
}
.h5-chat-main {
+ flex: 1;
display: flex;
flex-direction: column;
+ min-height: 0;
+ overflow: hidden;
}
.h5-chat-content-row {
- /* 移除内部滚动,改为由 App 的 main-content 统一处理 */
+ flex: 1;
+ display: flex;
+ flex-direction: column;
min-height: 0;
+ overflow: hidden;
+ }
+
+ .h5-chat-content-row .chat-body {
+ flex: 1;
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
}
.h5-chat-content-row .chat-body .messages-container {
- padding-bottom: 2rem;
+ padding: 12px 16px 32px;
}
}
diff --git a/src/pages/chat/components/ChatPageH5.tsx b/src/pages/chat/components/ChatPageH5.tsx
index d3d51e4..7220fab 100644
--- a/src/pages/chat/components/ChatPageH5.tsx
+++ b/src/pages/chat/components/ChatPageH5.tsx
@@ -122,6 +122,8 @@ export default function ChatPageH5({ logic }: { logic: ChatPageLogicOutput }) {
streaming={sender.streaming}
onRegenerate={sender.handleRegenerate}
onSwitchBranch={sender.handleSwitchBranch}
+ scrollBottom={logic.scrollBottom}
+ initialScrollDoneRef={logic.initialScrollDoneRef}
onCopy={(text, mode) => {
const content = mode === 'markdown' ? text : markdownToPlainText(text);
navigator.clipboard?.writeText(content).then(() => {
diff --git a/src/pages/chat/components/ChatPageWeb.tsx b/src/pages/chat/components/ChatPageWeb.tsx
index 1a57816..e6bd0f6 100644
--- a/src/pages/chat/components/ChatPageWeb.tsx
+++ b/src/pages/chat/components/ChatPageWeb.tsx
@@ -113,6 +113,8 @@ export default function ChatPageWeb({ logic }: { logic: ChatPageLogicOutput }) {
streaming={sender.streaming}
onRegenerate={sender.handleRegenerate}
onSwitchBranch={sender.handleSwitchBranch}
+ scrollBottom={logic.scrollBottom}
+ initialScrollDoneRef={logic.initialScrollDoneRef}
onCopy={(text, mode) => {
const content = mode === 'markdown' ? text : markdownToPlainText(text);
navigator.clipboard?.writeText(content).then(() => {
diff --git a/src/pages/chat/components/ChatPageWebBase.tsx b/src/pages/chat/components/ChatPageWebBase.tsx
index 7e15357..63cf57b 100644
--- a/src/pages/chat/components/ChatPageWebBase.tsx
+++ b/src/pages/chat/components/ChatPageWebBase.tsx
@@ -57,9 +57,11 @@ export default function ChatPageWebBase({ logic, viewport }: ChatPageWebVariantP
streaming={sender.streaming}
onRegenerate={sender.handleRegenerate}
onSwitchBranch={sender.handleSwitchBranch}
+ scrollBottom={logic.scrollBottom}
+ initialScrollDoneRef={logic.initialScrollDoneRef}
onCopy={(text, mode) => {
const content = mode === 'markdown' ? text : markdownToPlainText(text);
- navigator.clipboard?.writeText(content).then(() => message.success(mode === 'markdown' ? '已复制(Markdown)' : '已复制(纯文本)'));
+ navigator.clipboard?.writeText(content).then(() => message.success(mode === 'markdown' ? '已复制(Markdown)' : '已复制'));
}}
/>
{
diff --git a/src/pages/chat/components/messages/MessageItem.tsx b/src/pages/chat/components/messages/MessageItem.tsx
index 2771204..771d046 100644
--- a/src/pages/chat/components/messages/MessageItem.tsx
+++ b/src/pages/chat/components/messages/MessageItem.tsx
@@ -119,19 +119,31 @@ export default function MessageItem(props: {
)}
{message.meta?.aborted && 已停止}
- onCopy?.(formattedContent, 'plain') },
- { key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(formattedContent, 'markdown') }
- ]
- }}
- >
+ {isUser ? (
- } />
+ }
+ onClick={() => onCopy?.(formattedContent, 'plain')}
+ />
-
+ ) : (
+ onCopy?.(formattedContent, 'plain') },
+ { key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(formattedContent, 'markdown') }
+ ]
+ }}
+ >
+
+ } />
+
+
+ )}
{!isUser && (
} disabled={busy} onClick={() => onRegenerate?.(message.id)} />
diff --git a/src/pages/chat/hooks/useChatData.ts b/src/pages/chat/hooks/useChatData.ts
index 89dec08..580588e 100644
--- a/src/pages/chat/hooks/useChatData.ts
+++ b/src/pages/chat/hooks/useChatData.ts
@@ -21,6 +21,11 @@ export function useChatData(args: {
const [messages, setMessages] = useState([]);
const [branches, setBranches] = useState>({});
const loadSeqRef = useRef(0);
+ const initialHighlightDoneRef = useRef(false);
+
+ useEffect(() => {
+ initialHighlightDoneRef.current = false;
+ }, [agentId, roomId]);
const loadAgent = async () => {
if (!agentId) {
@@ -61,18 +66,8 @@ export function useChatData(args: {
setMessages(Array.isArray(his.messages) ? his.messages : []);
setBranches(his.branches || {});
- const checkScroll = () => {
- if (!initialScrollDoneRef.current) {
- scrollBottom(true);
- initialScrollDoneRef.current = true;
- } else {
- scrollBottom();
- }
- };
-
- requestAnimationFrame(checkScroll);
- setTimeout(checkScroll, 100);
- setTimeout(checkScroll, 500);
+ // 历史消息加载完成后的处理
+ // 注意:这里不再手动触发滚动,全部交给 ChatBody 的 useLayoutEffect 和 useEffect 处理
};
useEffect(() => {
@@ -109,8 +104,26 @@ export function useChatData(args: {
if (!highlightId) return;
const el = document.getElementById('msg-' + highlightId);
if (!el) return;
- el.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }, [highlightId]);
+
+ // 如果是房间加载后的第一次高亮(通常是系统自动选中的最后一条消息)
+ const isFirstHighlight = !initialHighlightDoneRef.current;
+
+ // 逻辑:如果是初始加载,且该消息是最后一条消息,则不触发 scrollIntoView(start)
+ // 这样可以保留 ChatBody 的 scrollBottom 效果(看到消息的尾部)
+ const isLastMessage = messages.length > 0 && messages[messages.length - 1].id === highlightId;
+
+ if (isFirstHighlight && isLastMessage) {
+ initialHighlightDoneRef.current = true;
+ return;
+ }
+
+ const behavior = isFirstHighlight ? 'instant' : 'smooth';
+ el.scrollIntoView({ behavior, block: 'start' });
+
+ if (isFirstHighlight) {
+ initialHighlightDoneRef.current = true;
+ }
+ }, [highlightId, messages]);
return {
agent,
diff --git a/src/pages/chat/hooks/useChatScroll.ts b/src/pages/chat/hooks/useChatScroll.ts
index f4c6685..de0b0db 100644
--- a/src/pages/chat/hooks/useChatScroll.ts
+++ b/src/pages/chat/hooks/useChatScroll.ts
@@ -1,4 +1,4 @@
-import { useEffect, useRef } from 'react';
+import { useEffect, useRef, useCallback } from 'react';
export function useChatScroll() {
const bodyRef = useRef(null);
@@ -13,7 +13,7 @@ export function useChatScroll() {
const lastUserScrollTypeRef = useRef('');
const attachRetryTimerRef = useRef(null);
- const scrollBottom = (force = false) => {
+ const scrollBottom = useCallback((force = false) => {
if (force) {
userScrollLockRef.current = false;
autoScrollRef.current = true;
@@ -32,9 +32,14 @@ export function useChatScroll() {
const el = bodyRef.current;
if (!el) return;
scrollProgrammaticAtRef.current = Date.now();
- el.scrollTop = el.scrollHeight;
+
+ // 恢复正向滚动:scrollTop = scrollHeight
+ el.scrollTo({
+ top: el.scrollHeight,
+ behavior: force ? 'instant' : 'smooth'
+ });
});
- };
+ }, []);
const cancelAutoScroll = (type: string) => {
userScrollLockRef.current = true;
@@ -57,9 +62,12 @@ export function useChatScroll() {
const isProgrammatic = now - scrollProgrammaticAtRef.current < 50;
const nextTop = el.scrollTop;
const lastTop = lastScrollTopRef.current;
+
+ // 恢复正向距离计算:距离底部 = 总高度 - 当前滚动高度 - 容器可见高度
const distance = el.scrollHeight - nextTop - el.clientHeight;
const scrollDelta = nextTop - lastTop;
+ // scrollDelta < 0 表示向上滚动
if (scrollDelta < 0 && !forceScrollPendingRef.current) {
cancelAutoScroll(isProgrammatic ? 'scroll(up)+programmatic' : 'scroll(up)');
}
diff --git a/src/styles.css b/src/styles.css
index de2aaa2..c1f3004 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -516,7 +516,7 @@ body {
}
.is-h5 .bubble {
- max-width: 94%;
+ /* max-width: 94%; */
}
.bubble.assistant p,