feat: stop costbox from displaying during responses and make scroll to bottom ux more consistent

This commit is contained in:
Dustin Healy 2025-08-23 11:13:03 -07:00
parent 794fe6fd11
commit ba8c09b361
4 changed files with 70 additions and 14 deletions

View file

@ -38,7 +38,7 @@ function ChatView({ index = 0 }: { index?: number }) {
const fileMap = useFileMapContext();
const [showCostBar, setShowCostBar] = useState(true);
const [showCostBar, setShowCostBar] = useState(false);
const lastScrollY = useRef(0);
const { data: messagesTree = null, isLoading } = useGetMessagesByConvoId(conversationId ?? '', {
@ -65,23 +65,33 @@ function ChatView({ index = 0 }: { index?: number }) {
useSSE(rootSubmission, chatHelpers, false);
useSSE(addedSubmission, addedChatHelpers, true);
useEffect(() => {
const handleScroll = (event: Event) => {
const target = event.target as HTMLElement;
const currentScrollY = target.scrollTop;
const scrollHeight = target.scrollHeight;
const clientHeight = target.clientHeight;
const checkIfAtBottom = useCallback(
(container: HTMLElement) => {
const currentScrollY = container.scrollTop;
const scrollHeight = container.scrollHeight;
const clientHeight = container.clientHeight;
const distanceFromBottom = scrollHeight - currentScrollY - clientHeight;
const isAtBottom = distanceFromBottom < 10;
setShowCostBar(isAtBottom);
const isStreaming = chatHelpers.isSubmitting || addedChatHelpers.isSubmitting;
setShowCostBar(isAtBottom && !isStreaming);
lastScrollY.current = currentScrollY;
},
[chatHelpers.isSubmitting, addedChatHelpers.isSubmitting],
);
useEffect(() => {
const handleScroll = (event: Event) => {
const target = event.target as HTMLElement;
checkIfAtBottom(target);
};
const findAndAttachScrollListener = () => {
const messagesContainer = document.querySelector('[class*="scrollbar-gutter-stable"]');
if (messagesContainer) {
checkIfAtBottom(messagesContainer as HTMLElement);
messagesContainer.addEventListener('scroll', handleScroll, { passive: true });
return () => {
messagesContainer.removeEventListener('scroll', handleScroll);
@ -93,7 +103,19 @@ function ChatView({ index = 0 }: { index?: number }) {
const cleanup = findAndAttachScrollListener();
return cleanup;
}, [messagesTree]);
}, [messagesTree, checkIfAtBottom]);
useEffect(() => {
const isStreaming = chatHelpers.isSubmitting || addedChatHelpers.isSubmitting;
if (isStreaming) {
setShowCostBar(false);
} else {
const messagesContainer = document.querySelector('[class*="scrollbar-gutter-stable"]');
if (messagesContainer) {
checkIfAtBottom(messagesContainer as HTMLElement);
}
}
}, [chatHelpers.isSubmitting, addedChatHelpers.isSubmitting, checkIfAtBottom]);
const methods = useForm<ChatFormValues>({
defaultValues: { text: '' },
@ -110,6 +132,7 @@ function ChatView({ index = 0 }: { index?: number }) {
} else if ((isLoading || isNavigating) && !isLandingPage) {
content = <LoadingSpinner />;
} else if (!isLandingPage) {
const isStreaming = chatHelpers.isSubmitting || addedChatHelpers.isSubmitting;
content = (
<MessagesView
messagesTree={messagesTree}
@ -117,7 +140,10 @@ function ChatView({ index = 0 }: { index?: number }) {
!isLandingPage &&
conversationCosts &&
conversationCosts.totals && (
<CostBar conversationCosts={conversationCosts} showCostBar={showCostBar} />
<CostBar
conversationCosts={conversationCosts}
showCostBar={showCostBar && !isStreaming}
/>
)
}
costs={conversationCosts}

View file

@ -48,7 +48,7 @@ export default function MessagesView({
width: '100%',
}}
>
<div className="flex flex-col pb-9 dark:bg-transparent">
<div className="flex flex-col dark:bg-transparent">
{(_messagesTree && _messagesTree.length == 0) || _messagesTree === null ? (
<div
className={cn(
@ -74,7 +74,7 @@ export default function MessagesView({
)}
<div
id="messages-end"
className="group h-0 w-full flex-shrink-0"
className="group h-1 w-full flex-shrink-0 pb-7"
ref={messagesEndRef}
/>
</div>

View file

@ -19,6 +19,7 @@ export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
const { conversationId } = conversation ?? {};
const timeoutIdRef = useRef<NodeJS.Timeout>();
const prevIsSubmittingRef = useRef<boolean>(false);
const debouncedSetShowScrollButton = useCallback((value: boolean) => {
clearTimeout(timeoutIdRef.current);
@ -60,7 +61,10 @@ export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
}
}, [debouncedSetShowScrollButton]);
const scrollCallback = () => debouncedSetShowScrollButton(false);
const scrollCallback = useCallback(
() => debouncedSetShowScrollButton(false),
[debouncedSetShowScrollButton],
);
const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef({
targetRef: messagesEndRef,
@ -71,6 +75,18 @@ export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
},
});
const smoothScrollToBottom = useCallback(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest',
});
scrollCallback();
setAbortScroll(false);
}
}, [scrollCallback, setAbortScroll]);
useEffect(() => {
if (!messagesTree || messagesTree.length === 0) {
return;
@ -91,6 +107,20 @@ export default function useMessageScrolling(messagesTree?: TMessage[] | null) {
};
}, [isSubmitting, messagesTree, scrollToBottom, abortScroll]);
useEffect(() => {
if (!messagesEndRef.current || !scrollableRef.current) {
return;
}
if (prevIsSubmittingRef.current && !isSubmitting && abortScroll !== true) {
setTimeout(() => {
smoothScrollToBottom();
}, 100);
}
prevIsSubmittingRef.current = isSubmitting;
}, [isSubmitting, smoothScrollToBottom, abortScroll]);
useEffect(() => {
if (!messagesEndRef.current || !scrollableRef.current) {
return;

View file

@ -31,7 +31,7 @@ export default function useScrollToRef({
// eslint-disable-next-line react-hooks/exhaustive-deps
const scrollToRef = useCallback(
throttle(() => logAndScroll('instant', callback), 145, { leading: true }),
throttle(() => logAndScroll('instant', callback), 100, { leading: true }),
[targetRef],
);