import ChatHeader from '@/new-components/chat/header/ChatHeader'; import { ChatContentContext } from '@/pages/chat'; import { VerticalAlignBottomOutlined, VerticalAlignTopOutlined } from '@ant-design/icons'; import dynamic from 'next/dynamic'; import React, { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react'; const ChatCompletion = dynamic(() => import('@/new-components/chat/content/ChatCompletion'), { ssr: false }); // eslint-disable-next-line no-empty-pattern const ChatContentContainer = ({}, ref: React.ForwardedRef) => { const scrollRef = useRef(null); const [isScrollToTop, setIsScrollToTop] = useState(false); const [showScrollButtons, setShowScrollButtons] = useState(false); const [isAtTop, setIsAtTop] = useState(true); const [isAtBottom, setIsAtBottom] = useState(false); const { history } = useContext(ChatContentContext); const allowAutoScroll = useRef(true); useImperativeHandle(ref, () => { return scrollRef.current; }); const handleScroll = () => { if (!scrollRef.current) return; const container = scrollRef.current; const scrollTop = container.scrollTop; const scrollHeight = container.scrollHeight; const clientHeight = container.clientHeight; const buffer = 20; // Check Scroll direction const lastScrollTop = Number(container?.dataset?.lastScrollTop) || 0; const direction = scrollTop > lastScrollTop ? 'down' : 'up'; container.dataset.lastScrollTop = String(scrollTop); // only allow auto scroll when user is near bottom allowAutoScroll.current = direction === 'down'; // Check if we're at the top setIsAtTop(scrollTop <= buffer); // Check if we're at the bottom setIsAtBottom(scrollTop + clientHeight >= scrollHeight - buffer); // Header visibility if (scrollTop >= 42 + 32) { setIsScrollToTop(true); } else { setIsScrollToTop(false); } // Show scroll buttons when content is scrollable const isScrollable = scrollHeight > clientHeight; setShowScrollButtons(isScrollable); }; useEffect(() => { if (scrollRef.current) { scrollRef.current.addEventListener('scroll', handleScroll); // Check initially if content is scrollable const isScrollable = scrollRef.current.scrollHeight > scrollRef.current.clientHeight; setShowScrollButtons(isScrollable); } return () => { // eslint-disable-next-line react-hooks/exhaustive-deps scrollRef.current && scrollRef.current.removeEventListener('scroll', handleScroll); }; }, []); const scrollToBottomSmooth = useCallback(() => { if (!scrollRef.current || !allowAutoScroll.current) return; const container = scrollRef.current; const { scrollTop, scrollHeight, clientHeight } = container; // 只有当用户接近底部时才自动滚动 const buffer = Math.max(50, clientHeight * 0.1); const isNearBottom = scrollTop + clientHeight >= scrollHeight - buffer; if (!isNearBottom) { return; } // use requestAnimationFrame to smooth scroll const frameId = requestAnimationFrame(() => { // 直接设置scrollTop来实现快速滚动,不使用平滑滚动以避免卡顿 // container.scrollTop = container.scrollHeight; container.scrollTo({ top: container.scrollHeight, behavior: 'auto', }); }); return () => cancelAnimationFrame(frameId); }, []); useEffect(() => { // 监听 history 变化和最后一条消息的 context 变化 scrollToBottomSmooth(); }, [history, history[history.length - 1]?.context]); const scrollToTop = () => { if (scrollRef.current) { scrollRef.current.scrollTo({ top: 0, behavior: 'smooth', }); } }; const scrollToBottom = () => { if (scrollRef.current) { scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth', }); } }; return (
{showScrollButtons && (
{!isAtTop && ( )} {!isAtBottom && ( )}
)}
); }; export default forwardRef(ChatContentContainer);