diff --git a/web/index.html b/web/index.html index 78b3465..c596841 100644 --- a/web/index.html +++ b/web/index.html @@ -4,6 +4,7 @@ PicoBot +
diff --git a/web/public/favicon.svg b/web/public/favicon.svg new file mode 100644 index 0000000..db7bc7a --- /dev/null +++ b/web/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/components/Chat/MessageList.tsx b/web/src/components/Chat/MessageList.tsx index 3f5ddb4..fcef3f3 100644 --- a/web/src/components/Chat/MessageList.tsx +++ b/web/src/components/Chat/MessageList.tsx @@ -15,6 +15,8 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps const prevShowBottomRef = useRef(false) const prevShowTopRef = useRef(false) const lastMessageCountRef = useRef(0) + const prevMessageCountRef = useRef(0) + const stickyLockUntilRef = useRef(0) const isProgrammaticScrollRef = useRef(false) const scrollTimerRef = useRef(0) @@ -28,6 +30,12 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps const el = containerRef.current if (!el) return + // sticky lock 期间:防止中间 DOM 状态的 scroll 事件打断历史加载 + if (Date.now() < stickyLockUntilRef.current) { + isStickyRef.current = true + return + } + const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight const nearBottom = distanceFromBottom < 120 @@ -81,13 +89,31 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps // ---- auto-scroll effect ---- useEffect(() => { - if (messages.length === 0) return + // 消息清空 → 重置所有追踪状态(话题/会话切换) + if (messages.length === 0) { + prevMessageCountRef.current = 0 + lastMessageCountRef.current = 0 + isStickyRef.current = true + return + } const lastMessage = messages[messages.length - 1] + const isFreshLoad = prevMessageCountRef.current === 0 && messages.length > 0 // 用户自己发的消息 → 始终滚到底部 if (lastMessage.role === 'user') { scrollToBottom('instant') + prevMessageCountRef.current = messages.length + return + } + + // 新加载(话题切换后首次收到消息)→ 强制滚到底部 + if (isFreshLoad) { + isStickyRef.current = true + lastMessageCountRef.current = 0 + stickyLockUntilRef.current = Date.now() + 1500 + scrollToBottom('instant') + prevMessageCountRef.current = messages.length return } @@ -102,6 +128,7 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps } lastMessageCountRef.current = messages.length + prevMessageCountRef.current = messages.length }, [messages, scrollToBottom]) // ---- ResizeObserver: 窗口大小变化时保持底部对齐 ---- @@ -119,6 +146,15 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps return () => observer.disconnect() }, []) + // ---- 组件挂载时滚动到底部 ---- + + useEffect(() => { + if (messages.length > 0) { + scrollToBottom('instant') + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + // ---- 清理定时器 ---- useEffect(() => {