mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-10 08:48:29 +00:00
* Feat: v2 * feat: add chat functionality * First cut: integrations * Feat: add conversation API * Enhance conversation handling and memory management * Feat: added conversation --------- Co-authored-by: Manoj K <saimanoj58@gmail.com>
170 lines
4.1 KiB
TypeScript
170 lines
4.1 KiB
TypeScript
// @hidden
|
|
|
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
import { cn } from "~/lib/utils";
|
|
|
|
interface ScrollState {
|
|
isAtBottom: boolean;
|
|
autoScrollEnabled: boolean;
|
|
}
|
|
|
|
interface UseAutoScrollOptions {
|
|
offset?: number;
|
|
smooth?: boolean;
|
|
content?: React.ReactNode;
|
|
}
|
|
|
|
export function useAutoScroll(options: UseAutoScrollOptions = {}) {
|
|
const { offset = 20, smooth = false, content } = options;
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const lastContentHeight = useRef(0);
|
|
const userHasScrolled = useRef(false);
|
|
|
|
const [scrollState, setScrollState] = useState<ScrollState>({
|
|
isAtBottom: false,
|
|
autoScrollEnabled: true,
|
|
});
|
|
|
|
const checkIsAtBottom = useCallback(
|
|
(element: HTMLElement) => {
|
|
const { scrollTop, scrollHeight, clientHeight } = element;
|
|
const distanceToBottom = Math.abs(
|
|
scrollHeight - scrollTop - clientHeight,
|
|
);
|
|
return distanceToBottom <= offset;
|
|
},
|
|
[offset],
|
|
);
|
|
|
|
const scrollToBottom = useCallback(
|
|
(instant?: boolean) => {
|
|
if (scrollRef.current) {
|
|
const targetScrollTop =
|
|
scrollRef.current.scrollHeight - scrollRef.current.clientHeight;
|
|
|
|
if (instant) {
|
|
scrollRef.current.scrollTop = targetScrollTop;
|
|
} else {
|
|
scrollRef.current.scrollTo({
|
|
top: targetScrollTop,
|
|
behavior: smooth ? "smooth" : "auto",
|
|
});
|
|
}
|
|
|
|
setScrollState({
|
|
isAtBottom: true,
|
|
autoScrollEnabled: true,
|
|
});
|
|
userHasScrolled.current = false;
|
|
}
|
|
},
|
|
[smooth],
|
|
);
|
|
|
|
const handleScroll = useCallback(() => {
|
|
if (scrollRef.current) {
|
|
const atBottom = checkIsAtBottom(scrollRef.current);
|
|
|
|
setScrollState((prev) => ({
|
|
isAtBottom: atBottom,
|
|
// Re-enable auto-scroll if at the bottom
|
|
autoScrollEnabled: atBottom ? true : prev.autoScrollEnabled,
|
|
}));
|
|
}
|
|
}, [checkIsAtBottom]);
|
|
|
|
useEffect(() => {
|
|
const element = scrollRef.current;
|
|
if (element) {
|
|
element.addEventListener("scroll", handleScroll, { passive: true });
|
|
}
|
|
|
|
return () =>
|
|
element ? element.removeEventListener("scroll", handleScroll) : undefined;
|
|
}, [handleScroll]);
|
|
|
|
useEffect(() => {
|
|
const scrollElement = scrollRef.current;
|
|
if (!scrollElement) {
|
|
return;
|
|
}
|
|
|
|
const currentHeight = scrollElement.scrollHeight;
|
|
const hasNewContent = currentHeight !== lastContentHeight.current;
|
|
|
|
if (hasNewContent) {
|
|
if (scrollState.autoScrollEnabled) {
|
|
requestAnimationFrame(() => {
|
|
scrollToBottom(lastContentHeight.current === 0);
|
|
});
|
|
}
|
|
lastContentHeight.current = currentHeight;
|
|
}
|
|
}, [content, scrollState.autoScrollEnabled, scrollToBottom]);
|
|
|
|
useEffect(() => {
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
if (scrollState.autoScrollEnabled) {
|
|
scrollToBottom(true);
|
|
}
|
|
});
|
|
|
|
const element = scrollRef.current;
|
|
if (element) {
|
|
resizeObserver.observe(element);
|
|
}
|
|
|
|
return () => resizeObserver.disconnect();
|
|
}, [scrollState.autoScrollEnabled, scrollToBottom]);
|
|
|
|
const disableAutoScroll = useCallback(() => {
|
|
const atBottom = scrollRef.current
|
|
? checkIsAtBottom(scrollRef.current)
|
|
: false;
|
|
|
|
// Only disable if not at bottom
|
|
if (!atBottom) {
|
|
userHasScrolled.current = true;
|
|
setScrollState((prev) => ({
|
|
...prev,
|
|
autoScrollEnabled: false,
|
|
}));
|
|
}
|
|
}, [checkIsAtBottom]);
|
|
|
|
return {
|
|
scrollRef,
|
|
isAtBottom: scrollState.isAtBottom,
|
|
autoScrollEnabled: scrollState.autoScrollEnabled,
|
|
scrollToBottom: () => scrollToBottom(false),
|
|
disableAutoScroll,
|
|
};
|
|
}
|
|
|
|
export const ScrollAreaWithAutoScroll = ({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) => {
|
|
const { scrollRef } = useAutoScroll({
|
|
smooth: true,
|
|
content: children,
|
|
});
|
|
|
|
return (
|
|
<div
|
|
ref={scrollRef}
|
|
className={cn(
|
|
"flex grow flex-col items-center overflow-y-auto",
|
|
className,
|
|
)}
|
|
>
|
|
<div className="flex h-full w-full max-w-[97ch] flex-col pb-4">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|