From 203067c936b6f2e3375ee79041c33dafacfc0653 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Fri, 4 Aug 2023 02:16:44 +0800 Subject: [PATCH] feat: close #2545 improve lazy load message list --- app/components/chat.tsx | 173 +++++++++++++++++++++++------------- app/components/markdown.tsx | 57 ++---------- app/constant.ts | 3 + 3 files changed, 118 insertions(+), 115 deletions(-) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 58dc01bd..4ab96367 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -74,7 +74,13 @@ import { showToast, } from "./ui-lib"; import { useLocation, useNavigate } from "react-router-dom"; -import { LAST_INPUT_KEY, Path, REQUEST_TIMEOUT_MS } from "../constant"; +import { + CHAT_PAGE_SIZE, + LAST_INPUT_KEY, + MAX_RENDER_MSG_COUNT, + Path, + REQUEST_TIMEOUT_MS, +} from "../constant"; import { Avatar } from "./emoji"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; import { useMaskStore } from "../store/mask"; @@ -370,33 +376,31 @@ function ChatAction(props: { function useScrollToBottom() { // for auto-scroll const scrollRef = useRef(null); - const autoScroll = useRef(true); - const scrollToBottom = useCallback(() => { + const [autoScroll, setAutoScroll] = useState(true); + + function scrollDomToBottom() { const dom = scrollRef.current; if (dom) { - requestAnimationFrame(() => dom.scrollTo(0, dom.scrollHeight)); + requestAnimationFrame(() => { + setAutoScroll(true); + dom.scrollTo(0, dom.scrollHeight); + }); } - }, []); - const setAutoScroll = (enable: boolean) => { - autoScroll.current = enable; - }; + } // auto scroll useEffect(() => { - const intervalId = setInterval(() => { - if (autoScroll.current) { - scrollToBottom(); - } - }, 30); - return () => clearInterval(intervalId); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + console.log("auto scroll", autoScroll); + if (autoScroll) { + scrollDomToBottom(); + } + }); return { scrollRef, autoScroll, setAutoScroll, - scrollToBottom, + scrollDomToBottom, }; } @@ -595,7 +599,7 @@ export function EditMessageModal(props: { onClose: () => void }) { ); } -export function Chat() { +function _Chat() { type RenderMessage = ChatMessage & { preview?: boolean }; const chatStore = useChatStore(); @@ -609,21 +613,11 @@ export function Chat() { const [userInput, setUserInput] = useState(""); const [isLoading, setIsLoading] = useState(false); const { submitKey, shouldSubmit } = useSubmitHandler(); - const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom(); + const { scrollRef, setAutoScroll, scrollDomToBottom } = useScrollToBottom(); const [hitBottom, setHitBottom] = useState(true); const isMobileScreen = useMobileScreen(); const navigate = useNavigate(); - const lastBodyScroolTop = useRef(0); - const onChatBodyScroll = (e: HTMLElement) => { - const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 10; - setHitBottom(isTouchBottom); - - // only enable auto scroll when scroll down and touched bottom - setAutoScroll(e.scrollTop >= lastBodyScroolTop.current && isTouchBottom); - lastBodyScroolTop.current = e.scrollTop; - }; - // prompt hints const promptStore = usePromptStore(); const [promptHints, setPromptHints] = useState([]); @@ -865,10 +859,9 @@ export function Chat() { }); }; - const context: RenderMessage[] = session.mask.hideContext - ? [] - : session.mask.context.slice(); - + const context: RenderMessage[] = useMemo(() => { + return session.mask.hideContext ? [] : session.mask.context.slice(); + }, [session.mask.context, session.mask.hideContext]); const accessStore = useAccessStore(); if ( @@ -889,34 +882,80 @@ export function Chat() { : -1; // preview messages - const messages = context - .concat(session.messages as RenderMessage[]) - .concat( - isLoading - ? [ - { - ...createMessage({ - role: "assistant", - content: "……", - }), - preview: true, - }, - ] - : [], - ) - .concat( - userInput.length > 0 && config.sendPreviewBubble - ? [ - { - ...createMessage({ - role: "user", - content: userInput, - }), - preview: true, - }, - ] - : [], + const renderMessages = useMemo(() => { + return context + .concat(session.messages as RenderMessage[]) + .concat( + isLoading + ? [ + { + ...createMessage({ + role: "assistant", + content: "……", + }), + preview: true, + }, + ] + : [], + ) + .concat( + userInput.length > 0 && config.sendPreviewBubble + ? [ + { + ...createMessage({ + role: "user", + content: userInput, + }), + preview: true, + }, + ] + : [], + ); + }, [ + config.sendPreviewBubble, + context, + isLoading, + session.messages, + userInput, + ]); + + const [msgRenderIndex, setMsgRenderIndex] = useState( + renderMessages.length - CHAT_PAGE_SIZE, + ); + const messages = useMemo(() => { + const endRenderIndex = Math.min( + msgRenderIndex + 3 * CHAT_PAGE_SIZE, + renderMessages.length, ); + return renderMessages.slice(msgRenderIndex, endRenderIndex); + }, [msgRenderIndex, renderMessages]); + + const onChatBodyScroll = (e: HTMLElement) => { + const EDGE_THRESHOLD = 100; + const bottomHeight = e.scrollTop + e.clientHeight; + const isTouchTopEdge = e.scrollTop <= EDGE_THRESHOLD; + const isTouchBottomEdge = bottomHeight >= e.scrollHeight - EDGE_THRESHOLD; + const isHitBottom = bottomHeight >= e.scrollHeight - 10; + + if (isTouchTopEdge) { + setMsgRenderIndex(Math.max(0, msgRenderIndex - CHAT_PAGE_SIZE)); + } else if (isTouchBottomEdge) { + setMsgRenderIndex( + Math.min( + msgRenderIndex + CHAT_PAGE_SIZE, + renderMessages.length - CHAT_PAGE_SIZE, + ), + ); + } + + setHitBottom(isHitBottom); + setAutoScroll(isHitBottom); + }; + + function scrollToBottom() { + setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE); + scrollDomToBottom(); + } const [showPromptModal, setShowPromptModal] = useState(false); @@ -1064,7 +1103,7 @@ export function Chat() { const shouldShowClearContextDivider = i === clearContextIndex - 1; return ( - +
onRightClick(e, message)} @@ -1202,7 +1242,8 @@ export function Chat() { onInput={(e) => onInput(e.currentTarget.value)} value={userInput} onKeyDown={onInputKeyDown} - onFocus={() => setAutoScroll(true)} + onFocus={scrollToBottom} + onClick={scrollToBottom} rows={inputRows} autoFocus={autoFocus} style={{ @@ -1233,3 +1274,9 @@ export function Chat() {
); } + +export function Chat() { + const chatStore = useChatStore(); + const sessionIndex = chatStore.currentSessionIndex; + return <_Chat key={sessionIndex}>; +} diff --git a/app/components/markdown.tsx b/app/components/markdown.tsx index 3168641c..0c6a2d43 100644 --- a/app/components/markdown.tsx +++ b/app/components/markdown.tsx @@ -146,70 +146,23 @@ export function Markdown( } & React.DOMAttributes, ) { const mdRef = useRef(null); - const renderedHeight = useRef(0); - const renderedWidth = useRef(0); - const inView = useRef(!!props.defaultShow); - const [_, triggerRender] = useState(0); - const checkInView = useThrottledCallback( - () => { - const parent = props.parentRef?.current; - const md = mdRef.current; - if (parent && md && !props.defaultShow) { - const parentBounds = parent.getBoundingClientRect(); - const twoScreenHeight = Math.max(500, parentBounds.height * 2); - const mdBounds = md.getBoundingClientRect(); - const parentTop = parentBounds.top - twoScreenHeight; - const parentBottom = parentBounds.bottom + twoScreenHeight; - const isOverlap = - Math.max(parentTop, mdBounds.top) <= - Math.min(parentBottom, mdBounds.bottom); - inView.current = isOverlap; - triggerRender(Date.now()); - } - - if (inView.current && md) { - const rect = md.getBoundingClientRect(); - renderedHeight.current = Math.max(renderedHeight.current, rect.height); - renderedWidth.current = Math.max(renderedWidth.current, rect.width); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, - 300, - { - leading: true, - trailing: true, - }, - ); - - useEffect(() => { - props.parentRef?.current?.addEventListener("scroll", checkInView); - checkInView(); - return () => - props.parentRef?.current?.removeEventListener("scroll", checkInView); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const getSize = (x: number) => (!inView.current && x > 0 ? x : "auto"); return (
- {inView.current && - (props.loading ? ( - - ) : ( - - ))} + {props.loading ? ( + + ) : ( + + )}
); } diff --git a/app/constant.ts b/app/constant.ts index 250bd135..b4bb7b0f 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -109,3 +109,6 @@ export const DEFAULT_MODELS = [ available: true, }, ] as const; + +export const CHAT_PAGE_SIZE = 10; +export const MAX_RENDER_MSG_COUNT = 20;