| 21 | ]; |
| 22 | |
| 23 | export const ChatPanel: React.FC<ChatPanelProps> = ({ onChipClick }) => { |
| 24 | const { messages, isStreaming, streamingText } = useChatStore(); |
| 25 | const { agentRuns } = useAgentStore(); |
| 26 | const bottomRef = useRef<HTMLDivElement>(null); |
| 27 | const scrollRef = useRef<HTMLDivElement>(null); |
| 28 | const userScrolledUp = useRef(false); |
| 29 | const isAutoScrolling = useRef(false); |
| 30 | |
| 31 | const scrollToBottom = useCallback(() => { |
| 32 | const el = scrollRef.current; |
| 33 | if (!el) return; |
| 34 | isAutoScrolling.current = true; |
| 35 | requestAnimationFrame(() => { |
| 36 | if (!el) return; |
| 37 | el.scrollTop = el.scrollHeight; |
| 38 | // Reset flag after browser has applied the scroll |
| 39 | requestAnimationFrame(() => { isAutoScrolling.current = false; }); |
| 40 | }); |
| 41 | }, []); |
| 42 | |
| 43 | // Track user scroll intent — ignore scroll events caused by our own scrollToBottom |
| 44 | const handleScroll = useCallback(() => { |
| 45 | if (isAutoScrolling.current) return; |
| 46 | const el = scrollRef.current; |
| 47 | if (!el) return; |
| 48 | const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight; |
| 49 | userScrolledUp.current = distFromBottom > 150; |
| 50 | }, []); |
| 51 | |
| 52 | // Auto-scroll only when genuinely new messages are added |
| 53 | const prevMsgCount = useRef(messages.length); |
| 54 | |
| 55 | useEffect(() => { |
| 56 | const newMsg = messages.length > prevMsgCount.current; |
| 57 | prevMsgCount.current = messages.length; |
| 58 | |
| 59 | if (newMsg && !userScrolledUp.current) { |
| 60 | scrollToBottom(); |
| 61 | } |
| 62 | }, [messages.length, scrollToBottom]); |
| 63 | |
| 64 | // During active streaming only, follow new tokens (throttled) |
| 65 | const streamScrollTimer = useRef<ReturnType<typeof setTimeout> | null>(null); |
| 66 | |
| 67 | useEffect(() => { |
| 68 | if (!isStreaming || userScrolledUp.current) return; |
| 69 | if (streamScrollTimer.current) return; // throttle |
| 70 | streamScrollTimer.current = setTimeout(() => { |
| 71 | streamScrollTimer.current = null; |
| 72 | if (!userScrolledUp.current) scrollToBottom(); |
| 73 | }, 80); |
| 74 | }, [streamingText, isStreaming, scrollToBottom]); |
| 75 | |
| 76 | const isEmpty = messages.length === 0 && agentRuns.length === 0 && !isStreaming; |
| 77 | |
| 78 | if (isEmpty) { |
| 79 | return ( |
| 80 | <div className="flex-1 flex flex-col items-center pt-[12vh] text-center p-8"> |