Files
DB-GPT/web/new-components/chat/ChatContentContainer.tsx

154 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<any>) => {
const scrollRef = useRef<HTMLDivElement>(null);
const [isScrollToTop, setIsScrollToTop] = useState<boolean>(false);
const [showScrollButtons, setShowScrollButtons] = useState<boolean>(false);
const [isAtTop, setIsAtTop] = useState<boolean>(true);
const [isAtBottom, setIsAtBottom] = useState<boolean>(false);
const { history } = useContext(ChatContentContext);
const allowAutoScroll = useRef<boolean>(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 (
<div className='flex flex-1 overflow-hidden relative'>
<div ref={scrollRef} className='h-full w-full mx-auto overflow-y-auto'>
<ChatHeader isScrollToTop={isScrollToTop} />
<ChatCompletion />
</div>
{showScrollButtons && (
<div className='absolute right-6 bottom-24 flex flex-col gap-2'>
{!isAtTop && (
<button
onClick={scrollToTop}
className='w-10 h-10 bg-white dark:bg-[rgba(255,255,255,0.2)] border border-gray-200 dark:border-[rgba(255,255,255,0.2)] rounded-full flex items-center justify-center shadow-md hover:shadow-lg transition-shadow'
aria-label='Scroll to top'
>
<VerticalAlignTopOutlined className='text-[#525964] dark:text-[rgba(255,255,255,0.85)]' />
</button>
)}
{!isAtBottom && (
<button
onClick={scrollToBottom}
className='w-10 h-10 bg-white dark:bg-[rgba(255,255,255,0.2)] border border-gray-200 dark:border-[rgba(255,255,255,0.2)] rounded-full flex items-center justify-center shadow-md hover:shadow-lg transition-shadow'
aria-label='Scroll to bottom'
>
<VerticalAlignBottomOutlined className='text-[#525964] dark:text-[rgba(255,255,255,0.85)]' />
</button>
)}
</div>
)}
</div>
);
};
export default forwardRef(ChatContentContainer);