forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuse-auto-scroll.ts
More file actions
54 lines (47 loc) · 1.86 KB
/
Copy pathuse-auto-scroll.ts
File metadata and controls
54 lines (47 loc) · 1.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Auto-scroll-to-bottom for the hand-rolled headless chat.
//
// `listRef` goes on the inner content node; the hook walks up to the
// nearest Radix `[data-slot='scroll-area-viewport']` ancestor (Radix wraps
// content in a `display: table` div, so `h-full` doesn't propagate and
// we can't put the ref on the scrollable element directly).
//
// `stickRef.current` is the read/write knob: while true, every render
// re-pins to the bottom; the scroll listener flips it false when the
// user scrolls more than 80px above the bottom. Callers can re-pin by
// setting `stickRef.current = true` (e.g., on send or reset).
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
export function useAutoScroll<T>(messages: T[], isRunning: boolean) {
const listRef = useRef<HTMLDivElement | null>(null);
const bottomRef = useRef<HTMLDivElement | null>(null);
const viewportRef = useRef<HTMLElement | null>(null);
const stickRef = useRef(true);
useEffect(() => {
const node = listRef.current;
if (!node) return;
viewportRef.current = node.closest(
"[data-slot='scroll-area-viewport']",
) as HTMLElement | null;
}, []);
const onScroll = useCallback(() => {
const el = viewportRef.current;
if (!el) return;
const distance = el.scrollHeight - el.scrollTop - el.clientHeight;
stickRef.current = distance < 80;
}, []);
useEffect(() => {
const el = viewportRef.current;
if (!el) return;
el.addEventListener("scroll", onScroll, { passive: true });
return () => el.removeEventListener("scroll", onScroll);
}, [onScroll]);
useLayoutEffect(() => {
if (!stickRef.current) return;
const el = viewportRef.current;
if (el) {
el.scrollTop = el.scrollHeight;
} else {
bottomRef.current?.scrollIntoView({ block: "end" });
}
}, [messages, isRunning]);
return { listRef, bottomRef, stickRef };
}