56 lines
1.5 KiB
TypeScript
56 lines
1.5 KiB
TypeScript
import type { UIMessage } from "ai";
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
const NEAR_BOTTOM_THRESHOLD = 80;
|
|
|
|
interface UseChatScrollOptions {
|
|
messages: UIMessage[];
|
|
scrollRef: React.RefObject<HTMLDivElement | null>;
|
|
status: string;
|
|
}
|
|
|
|
export function useChatScroll({ messages, scrollRef, status }: UseChatScrollOptions) {
|
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
|
const isStreaming = status === "streaming";
|
|
|
|
const checkNearBottom = useCallback(() => {
|
|
const el = scrollRef.current;
|
|
if (!el) return true;
|
|
return el.scrollHeight - el.scrollTop - el.clientHeight < NEAR_BOTTOM_THRESHOLD;
|
|
}, [scrollRef]);
|
|
|
|
useEffect(() => {
|
|
const el = scrollRef.current;
|
|
if (!el) return;
|
|
|
|
setIsAtBottom(checkNearBottom());
|
|
|
|
const handleScroll = () => {
|
|
setIsAtBottom(checkNearBottom());
|
|
};
|
|
|
|
el.addEventListener("scroll", handleScroll, { passive: true });
|
|
return () => el.removeEventListener("scroll", handleScroll);
|
|
}, [scrollRef, checkNearBottom]);
|
|
|
|
useEffect(() => {
|
|
const el = scrollRef.current;
|
|
if (!el || !isAtBottom) return;
|
|
|
|
el.scrollTo({
|
|
behavior: isStreaming ? "instant" : "smooth",
|
|
top: el.scrollHeight,
|
|
});
|
|
}, [messages, isStreaming, isAtBottom, scrollRef]);
|
|
|
|
const scrollToBottom = useCallback(() => {
|
|
const el = scrollRef.current;
|
|
if (!el) return;
|
|
el.scrollTo({ behavior: "smooth", top: el.scrollHeight });
|
|
setIsAtBottom(true);
|
|
}, [scrollRef]);
|
|
|
|
return { isAtBottom, scrollToBottom };
|
|
}
|