Make streaming behavior more natural (#2336)

* fix scrolling behavior + add cursor to streaming chats

* lint

* linting

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2024-09-23 08:53:36 -07:00 committed by GitHub
parent 12b8af4654
commit d75fee0c07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -20,13 +20,16 @@ export default function ChatHistory({
regenerateAssistantMessage, regenerateAssistantMessage,
hasAttachments = false, hasAttachments = false,
}) { }) {
const lastScrollTopRef = useRef(0);
const { user } = useUser(); const { user } = useUser();
const { threadSlug = null } = useParams(); const { threadSlug = null } = useParams();
const { showing, showModal, hideModal } = useManageWorkspaceModal(); const { showing, showModal, hideModal } = useManageWorkspaceModal();
const [isAtBottom, setIsAtBottom] = useState(true); const [isAtBottom, setIsAtBottom] = useState(true);
const chatHistoryRef = useRef(null); const chatHistoryRef = useRef(null);
const [textSize, setTextSize] = useState("normal"); const [textSize, setTextSize] = useState("normal");
const [isUserScrolling, setIsUserScrolling] = useState(false);
const showScrollbar = Appearance.getSettings()?.showScrollbar || false; const showScrollbar = Appearance.getSettings()?.showScrollbar || false;
const isStreaming = history[history.length - 1]?.animate;
const getTextSizeClass = (size) => { const getTextSizeClass = (size) => {
switch (size) { switch (size) {
@ -58,35 +61,44 @@ export default function ChatHistory({
}, []); }, []);
useEffect(() => { useEffect(() => {
if (isAtBottom) scrollToBottom(); if (!isUserScrolling && (isAtBottom || isStreaming)) {
}, [history]); scrollToBottom(false); // Use instant scroll for auto-scrolling
}
}, [history, isAtBottom, isStreaming, isUserScrolling]);
const handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const isBottom = scrollHeight - scrollTop === clientHeight;
// Detect if this is a user-initiated scroll
if (Math.abs(scrollTop - lastScrollTopRef.current) > 10) {
setIsUserScrolling(!isBottom);
}
const handleScroll = () => {
const diff =
chatHistoryRef.current.scrollHeight -
chatHistoryRef.current.scrollTop -
chatHistoryRef.current.clientHeight;
// Fuzzy margin for what qualifies as "bottom". Stronger than straight comparison since that may change over time.
const isBottom = diff <= 10;
setIsAtBottom(isBottom); setIsAtBottom(isBottom);
lastScrollTopRef.current = scrollTop;
}; };
const debouncedScroll = debounce(handleScroll, 100); const debouncedScroll = debounce(handleScroll, 100);
useEffect(() => { useEffect(() => {
function watchScrollEvent() { const chatHistoryElement = chatHistoryRef.current;
if (!chatHistoryRef.current) return null; if (chatHistoryElement) {
const chatHistoryElement = chatHistoryRef.current;
if (!chatHistoryElement) return null;
chatHistoryElement.addEventListener("scroll", debouncedScroll); chatHistoryElement.addEventListener("scroll", debouncedScroll);
return () =>
chatHistoryElement.removeEventListener("scroll", debouncedScroll);
} }
watchScrollEvent();
}, []); }, []);
const scrollToBottom = () => { const scrollToBottom = (smooth = false) => {
if (chatHistoryRef.current) { if (chatHistoryRef.current) {
chatHistoryRef.current.scrollTo({ chatHistoryRef.current.scrollTo({
top: chatHistoryRef.current.scrollHeight, top: chatHistoryRef.current.scrollHeight,
behavior: "smooth",
// Smooth is on when user clicks the button but disabled during auto scroll
// We must disable this during auto scroll because it causes issues with
// detecting when we are at the bottom of the chat.
...(smooth ? { behavior: "smooth" } : {}),
}); });
} }
}; };
@ -197,6 +209,7 @@ export default function ChatHistory({
}`} }`}
id="chat-history" id="chat-history"
ref={chatHistoryRef} ref={chatHistoryRef}
onScroll={handleScroll}
> >
{history.map((props, index) => { {history.map((props, index) => {
const isLastBotReply = const isLastBotReply =
@ -251,12 +264,14 @@ export default function ChatHistory({
{!isAtBottom && ( {!isAtBottom && (
<div className="fixed bottom-40 right-10 md:right-20 z-50 cursor-pointer animate-pulse"> <div className="fixed bottom-40 right-10 md:right-20 z-50 cursor-pointer animate-pulse">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="p-1 rounded-full border border-white/10 bg-white/10 hover:bg-white/20 hover:text-white"> <div
<ArrowDown className="p-1 rounded-full border border-white/10 bg-white/10 hover:bg-white/20 hover:text-white"
weight="bold" onClick={() => {
className="text-white/60 w-5 h-5" scrollToBottom(true);
onClick={scrollToBottom} setIsUserScrolling(false);
/> }}
>
<ArrowDown weight="bold" className="text-white/60 w-5 h-5" />
</div> </div>
</div> </div>
</div> </div>