diff --git a/client/src/common/types.ts b/client/src/common/types.ts index e65bd3221f..2ac16bb38c 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -112,7 +112,6 @@ export type TMessageProps = { isSearchView?: boolean; siblingIdx?: number; siblingCount?: number; - scrollToBottom?: () => void; setCurrentEditId?: React.Dispatch> | null; setSiblingIdx?: ((value: number) => void | React.Dispatch>) | null; }; diff --git a/client/src/components/Chat/Input/Files/Image.tsx b/client/src/components/Chat/Input/Files/Image.tsx index 7dbc81a519..fcf1dfe8c9 100644 --- a/client/src/components/Chat/Input/Files/Image.tsx +++ b/client/src/components/Chat/Input/Files/Image.tsx @@ -68,7 +68,6 @@ const Image = ({ cx="60" cy="60" /> - {/* */} { - if (isSubmitting && scrollToBottom && !abortScroll) { - scrollToBottom(); - } - }, [isSubmitting, text, scrollToBottom, abortScroll]); - - useEffect(() => { - if (scrollToBottom && autoScroll && conversationId !== 'new') { - scrollToBottom(); - } - }, [autoScroll, conversationId, scrollToBottom]); - - useEffect(() => { - if (!message) { - return; - } else if (isLast) { - setLatestMessage({ ...message }); - } - }, [isLast, message, setLatestMessage]); - if (!message) { return null; } - const enterEdit = (cancel?: boolean) => - setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId); - - const handleScroll = () => { - if (isSubmitting) { - setAbortScroll(true); - } else { - setAbortScroll(false); - } - }; - - // const commonClasses = - // 'w-full border-b text-gray-800 group border-black/10 dark:border-gray-900/50 dark:text-gray-100 dark:border-none'; - // const uniqueClasses = isCreatedByUser - // ? 'bg-white dark:bg-gray-800 dark:text-gray-20' - // : 'bg-white dark:bg-gray-800 dark:text-gray-70'; - - // const messageProps = { - // className: cn(commonClasses, uniqueClasses), - // titleclass: '', - // }; - - const icon = Icon({ - ...conversation, - ...message, - model: message?.model ?? conversation?.model, - size: 28.8, - }); - - const regenerateMessage = () => { - if (isSubmitting && isCreatedByUser) { - return; - } - - regenerate(message); - }; - - const copyToClipboard = (setIsCopied: React.Dispatch>) => { - setIsCopied(true); - copy(text ?? ''); - - setTimeout(() => { - setIsCopied(false); - }, 3000); - }; - return ( <>
diff --git a/client/src/components/Chat/Messages/MessagesView.tsx b/client/src/components/Chat/Messages/MessagesView.tsx index 3be5a51129..6130f33a10 100644 --- a/client/src/components/Chat/Messages/MessagesView.tsx +++ b/client/src/components/Chat/Messages/MessagesView.tsx @@ -1,10 +1,9 @@ -import { useLayoutEffect, useState, useRef, useCallback } from 'react'; +import { useState } from 'react'; import type { ReactNode } from 'react'; import type { TMessage } from 'librechat-data-provider'; import ScrollToBottom from '~/components/Messages/ScrollToBottom'; -import { useScreenshot, useScrollToRef } from '~/hooks'; +import { useScreenshot, useMessageScrolling } from '~/hooks'; import { CSSTransition } from 'react-transition-group'; -import { useChatContext } from '~/Providers'; import MultiMessage from './MultiMessage'; export default function MessagesView({ @@ -14,54 +13,19 @@ export default function MessagesView({ messagesTree?: TMessage[] | null; Header?: ReactNode; }) { - const timeoutIdRef = useRef(); - const scrollableRef = useRef(null); - const messagesEndRef = useRef(null); - const [showScrollButton, setShowScrollButton] = useState(false); - const [currentEditId, setCurrentEditId] = useState(-1); - const { conversation, setAbortScroll } = useChatContext(); - const { conversationId } = conversation ?? {}; - const { screenshotTargetRef } = useScreenshot(); + const [currentEditId, setCurrentEditId] = useState(-1); - const checkIfAtBottom = useCallback(() => { - if (!scrollableRef.current) { - return; - } + const { + conversation, + scrollableRef, + messagesEndRef, + showScrollButton, + handleSmoothToRef, + debouncedHandleScroll, + } = useMessageScrolling(_messagesTree); - const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current; - const diff = Math.abs(scrollHeight - scrollTop); - const percent = Math.abs(clientHeight - diff) / clientHeight; - const hasScrollbar = scrollHeight > clientHeight && percent >= 0.15; - setShowScrollButton(hasScrollbar); - }, [scrollableRef]); - - useLayoutEffect(() => { - const scrollableElement = scrollableRef.current; - if (!scrollableElement) { - return; - } - const timeoutId = setTimeout(checkIfAtBottom, 650); - - return () => { - clearTimeout(timeoutId); - }; - }, [checkIfAtBottom]); - - const debouncedHandleScroll = useCallback(() => { - clearTimeout(timeoutIdRef.current); - timeoutIdRef.current = setTimeout(checkIfAtBottom, 100); - }, [checkIfAtBottom]); - - const scrollCallback = () => setShowScrollButton(false); - const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef({ - targetRef: messagesEndRef, - callback: scrollCallback, - smoothCallback: () => { - scrollCallback(); - setAbortScroll(false); - }, - }); + const { conversationId } = conversation ?? {}; return (
@@ -86,9 +50,8 @@ export default function MessagesView({
diff --git a/client/src/components/Chat/Messages/MultiMessage.tsx b/client/src/components/Chat/Messages/MultiMessage.tsx index a1afbcc536..a62e9d384f 100644 --- a/client/src/components/Chat/Messages/MultiMessage.tsx +++ b/client/src/components/Chat/Messages/MultiMessage.tsx @@ -9,7 +9,6 @@ export default function MultiMessage({ // messageId is used recursively here messageId, messagesTree, - scrollToBottom, currentEditId, setCurrentEditId, }: TMessageProps) { @@ -45,7 +44,6 @@ export default function MultiMessage({ { + if (!message) { + return; + } else if (isLast) { + setLatestMessage({ ...message }); + } + }, [isLast, message, setLatestMessage]); + + const enterEdit = (cancel?: boolean) => + setCurrentEditId && setCurrentEditId(cancel ? -1 : messageId); + + const handleScroll = () => { + if (isSubmitting) { + setAbortScroll(true); + } else { + setAbortScroll(false); + } + }; + + const icon = Icon({ + ...conversation, + ...(message as TMessage), + model: message?.model ?? conversation?.model, + size: 28.8, + }); + + const regenerateMessage = () => { + if ((isSubmitting && isCreatedByUser) || !message) { + return; + } + + regenerate(message); + }; + + const copyToClipboard = (setIsCopied: React.Dispatch>) => { + setIsCopied(true); + copy(text ?? ''); + + setTimeout(() => { + setIsCopied(false); + }, 3000); + }; + + return { + ask, + icon, + edit, + isLast, + enterEdit, + conversation, + isSubmitting, + handleScroll, + latestMessage, + handleContinue, + copyToClipboard, + regenerateMessage, + }; +} diff --git a/client/src/hooks/Messages/useMessageScrolling.ts b/client/src/hooks/Messages/useMessageScrolling.ts new file mode 100644 index 0000000000..a523e0b17e --- /dev/null +++ b/client/src/hooks/Messages/useMessageScrolling.ts @@ -0,0 +1,83 @@ +import { useRecoilValue } from 'recoil'; +import { useLayoutEffect, useState, useRef, useCallback, useEffect } from 'react'; +import type { TMessage } from 'librechat-data-provider'; +import useScrollToRef from '../useScrollToRef'; +import { useChatContext } from '~/Providers'; +import store from '~/store'; + +export default function useMessageScrolling(messagesTree?: TMessage[] | null) { + const autoScroll = useRecoilValue(store.autoScroll); + + const timeoutIdRef = useRef(); + const scrollableRef = useRef(null); + const messagesEndRef = useRef(null); + const [showScrollButton, setShowScrollButton] = useState(false); + const { conversation, setAbortScroll, isSubmitting, abortScroll } = useChatContext(); + const { conversationId } = conversation ?? {}; + + const checkIfAtBottom = useCallback(() => { + if (!scrollableRef.current) { + return; + } + + const { scrollTop, scrollHeight, clientHeight } = scrollableRef.current; + const diff = Math.abs(scrollHeight - scrollTop); + const percent = Math.abs(clientHeight - diff) / clientHeight; + const hasScrollbar = scrollHeight > clientHeight && percent >= 0.15; + setShowScrollButton(hasScrollbar); + }, [scrollableRef]); + + useLayoutEffect(() => { + const scrollableElement = scrollableRef.current; + if (!scrollableElement) { + return; + } + const timeoutId = setTimeout(checkIfAtBottom, 650); + + return () => { + clearTimeout(timeoutId); + }; + }, [checkIfAtBottom]); + + const debouncedHandleScroll = useCallback(() => { + clearTimeout(timeoutIdRef.current); + timeoutIdRef.current = setTimeout(checkIfAtBottom, 100); + }, [checkIfAtBottom]); + + const scrollCallback = () => setShowScrollButton(false); + + const { scrollToRef: scrollToBottom, handleSmoothToRef } = useScrollToRef({ + targetRef: messagesEndRef, + callback: scrollCallback, + smoothCallback: () => { + scrollCallback(); + setAbortScroll(false); + }, + }); + + useEffect(() => { + if (!messagesTree) { + return; + } + + if (isSubmitting && scrollToBottom && !abortScroll) { + scrollToBottom(); + } + }, [isSubmitting, messagesTree, scrollToBottom, abortScroll]); + + useEffect(() => { + if (scrollToBottom && autoScroll && conversationId !== 'new') { + scrollToBottom(); + } + }, [autoScroll, conversationId, scrollToBottom]); + + return { + conversation, + scrollableRef, + messagesEndRef, + scrollToBottom, + showScrollButton, + handleSmoothToRef, + debouncedHandleScroll, + }; +} diff --git a/client/src/hooks/index.ts b/client/src/hooks/index.ts index 2cfa2a9f2b..c6ba2e27ff 100644 --- a/client/src/hooks/index.ts +++ b/client/src/hooks/index.ts @@ -1,3 +1,5 @@ +export * from './Messages'; + export * from './AuthContext'; export * from './ThemeContext'; export * from './ScreenshotContext'; diff --git a/client/src/hooks/useScrollToRef.ts b/client/src/hooks/useScrollToRef.ts index 57369c635c..941fa5aac7 100644 --- a/client/src/hooks/useScrollToRef.ts +++ b/client/src/hooks/useScrollToRef.ts @@ -8,29 +8,22 @@ type TUseScrollToRef = { }; export default function useScrollToRef({ targetRef, callback, smoothCallback }: TUseScrollToRef) { + const logAndScroll = (behavior: 'instant' | 'smooth', callbackFn: () => void) => { + // Debugging: + // console.log(`Scrolling with behavior: ${behavior}, Time: ${new Date().toISOString()}`); + targetRef.current?.scrollIntoView({ behavior }); + callbackFn(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps const scrollToRef = useCallback( - throttle( - () => { - targetRef.current?.scrollIntoView({ behavior: 'instant' }); - callback(); - }, - 450, - { leading: true }, - ), + throttle(() => logAndScroll('instant', callback), 450, { leading: true }), [targetRef], ); // eslint-disable-next-line react-hooks/exhaustive-deps const scrollToRefSmooth = useCallback( - throttle( - () => { - targetRef.current?.scrollIntoView({ behavior: 'smooth' }); - smoothCallback(); - }, - 750, - { leading: true }, - ), + throttle(() => logAndScroll('smooth', smoothCallback), 750, { leading: true }), [targetRef], );