import { useEffect, useRef, useState, useCallback } from 'react' import { MessageBubble } from './MessageBubble' import type { ChatMessage } from '../../types/protocol' import { Sparkles, ArrowDown, ArrowUp } from 'lucide-react' interface MessageListProps { messages: ChatMessage[] onNavigateToSubAgent?: (taskId: string, description: string) => void } export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps) { const bottomRef = useRef(null) const containerRef = useRef(null) const isStickyRef = useRef(true) 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) const [showScrollToBottom, setShowScrollToBottom] = useState(false) const [showScrollToTop, setShowScrollToTop] = useState(false) const [hasNewMessage, setHasNewMessage] = useState(false) // ---- scroll handlers ---- const handleScroll = useCallback(() => { 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 isStickyRef.current = nearBottom // 编程式滚动进行中(按钮点击触发),跳过按钮状态更新,避免闪烁 if (isProgrammaticScrollRef.current) return // 回到顶部按钮:滚过 0.8 个视口时显示 const shouldShowTop = el.scrollTop > el.clientHeight * 0.8 if (shouldShowTop !== prevShowTopRef.current) { prevShowTopRef.current = shouldShowTop setShowScrollToTop(shouldShowTop) } // 回到底部按钮:离开底部时显示 const shouldShowBottom = !nearBottom if (shouldShowBottom !== prevShowBottomRef.current) { prevShowBottomRef.current = shouldShowBottom setShowScrollToBottom(shouldShowBottom) } // 用户手动滚回底部时清除"有新消息"标记 if (nearBottom && hasNewMessage) { setHasNewMessage(false) } }, [hasNewMessage]) const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => { isStickyRef.current = true prevShowBottomRef.current = false setShowScrollToBottom(false) setHasNewMessage(false) isProgrammaticScrollRef.current = true clearTimeout(scrollTimerRef.current) scrollTimerRef.current = setTimeout(() => { isProgrammaticScrollRef.current = false }, 500) bottomRef.current?.scrollIntoView({ behavior }) }, []) const scrollToTop = useCallback(() => { isProgrammaticScrollRef.current = true clearTimeout(scrollTimerRef.current) scrollTimerRef.current = setTimeout(() => { isProgrammaticScrollRef.current = false }, 500) containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' }) }, []) // ---- auto-scroll effect ---- useEffect(() => { // 消息清空 → 重置所有追踪状态(话题/会话切换) 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 } // 用户在底部 → 自动跟随新消息 if (isStickyRef.current && bottomRef.current) { bottomRef.current.scrollIntoView({ behavior: 'instant' }) } // 用户不在底部且有新消息 → 标记未读 if (!isStickyRef.current && messages.length > lastMessageCountRef.current) { setHasNewMessage(true) } lastMessageCountRef.current = messages.length prevMessageCountRef.current = messages.length }, [messages, scrollToBottom]) // ---- ResizeObserver: 窗口大小变化时保持底部对齐 ---- useEffect(() => { const el = containerRef.current if (!el) return const observer = new ResizeObserver(() => { if (isStickyRef.current && bottomRef.current) { bottomRef.current.scrollIntoView({ behavior: 'instant' }) } }) observer.observe(el) return () => observer.disconnect() }, []) // ---- 组件挂载时滚动到底部 ---- useEffect(() => { if (messages.length > 0) { scrollToBottom('instant') } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // ---- 清理定时器 ---- useEffect(() => { return () => clearTimeout(scrollTimerRef.current) }, []) // ---- empty state ---- if (messages.length === 0) { return (

开始新的对话

在下方输入消息开始与 AI 助手聊天

/new 创建话题 /list 查看列表
) } // ---- main render ---- return (
{messages.map((message) => ( ))}
{/* 浮动导航按钮组 */}
{/* 回到顶部 */} {showScrollToTop && ( )} {/* 回到底部 */} {showScrollToBottom && ( )}
) }