feat: close #2545 improve lazy load message list

This commit is contained in:
Yidadaa 2023-08-04 02:16:44 +08:00
parent 081d84f848
commit 203067c936
3 changed files with 118 additions and 115 deletions

View File

@ -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<HTMLDivElement>(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();
console.log("auto scroll", autoScroll);
if (autoScroll) {
scrollDomToBottom();
}
}, 30);
return () => clearInterval(intervalId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
});
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<RenderPompt[]>([]);
@ -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,7 +882,8 @@ export function Chat() {
: -1;
// preview messages
const messages = context
const renderMessages = useMemo(() => {
return context
.concat(session.messages as RenderMessage[])
.concat(
isLoading
@ -917,6 +911,51 @@ export function Chat() {
]
: [],
);
}, [
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 (
<Fragment key={i}>
<Fragment key={message.id}>
<div
className={
isUser ? styles["chat-message-user"] : styles["chat-message"]
@ -1148,7 +1187,8 @@ export function Chat() {
<Markdown
content={message.content}
loading={
(message.preview || message.content.length === 0) &&
(message.preview || message.streaming) &&
message.content.length === 0 &&
!isUser
}
onContextMenu={(e) => 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() {
</div>
);
}
export function Chat() {
const chatStore = useChatStore();
const sessionIndex = chatStore.currentSessionIndex;
return <_Chat key={sessionIndex}></_Chat>;
}

View File

@ -146,70 +146,23 @@ export function Markdown(
} & React.DOMAttributes<HTMLDivElement>,
) {
const mdRef = useRef<HTMLDivElement>(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 (
<div
className="markdown-body"
style={{
fontSize: `${props.fontSize ?? 14}px`,
height: getSize(renderedHeight.current),
width: getSize(renderedWidth.current),
direction: /[\u0600-\u06FF]/.test(props.content) ? "rtl" : "ltr",
}}
ref={mdRef}
onContextMenu={props.onContextMenu}
onDoubleClickCapture={props.onDoubleClickCapture}
>
{inView.current &&
(props.loading ? (
{props.loading ? (
<LoadingIcon />
) : (
<MarkdownContent content={props.content} />
))}
)}
</div>
);
}

View File

@ -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;