mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-20 13:05:49 +02:00
Add basic virtualization (#2370)
* add basic virtualization * functioning perfectly * squash * change ports * remove some comments * remove comment * update buffering clarity
This commit is contained in:
@@ -4,6 +4,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
|||||||
import {
|
import {
|
||||||
BackendChatSession,
|
BackendChatSession,
|
||||||
BackendMessage,
|
BackendMessage,
|
||||||
|
BUFFER_COUNT,
|
||||||
ChatFileType,
|
ChatFileType,
|
||||||
ChatSession,
|
ChatSession,
|
||||||
ChatSessionSharedStatus,
|
ChatSessionSharedStatus,
|
||||||
@@ -48,6 +49,7 @@ import {
|
|||||||
SetStateAction,
|
SetStateAction,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
@@ -349,6 +351,9 @@ export function ChatPage({
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const shouldScrollToBottom =
|
||||||
|
visibleRange.get(existingChatSessionId) === undefined ||
|
||||||
|
visibleRange.get(existingChatSessionId)?.end == 0;
|
||||||
|
|
||||||
clearSelectedDocuments();
|
clearSelectedDocuments();
|
||||||
setIsFetchingChatMessages(true);
|
setIsFetchingChatMessages(true);
|
||||||
@@ -384,10 +389,16 @@ export function ChatPage({
|
|||||||
|
|
||||||
// go to bottom. If initial load, then do a scroll,
|
// go to bottom. If initial load, then do a scroll,
|
||||||
// otherwise just appear at the bottom
|
// otherwise just appear at the bottom
|
||||||
if (!hasPerformedInitialScroll) {
|
if (shouldScrollToBottom) {
|
||||||
clientScrollToBottom();
|
scrollInitialized.current = false;
|
||||||
} else if (isChatSessionSwitch) {
|
}
|
||||||
clientScrollToBottom(true);
|
|
||||||
|
if (shouldScrollToBottom) {
|
||||||
|
if (!hasPerformedInitialScroll) {
|
||||||
|
clientScrollToBottom();
|
||||||
|
} else if (isChatSessionSwitch) {
|
||||||
|
clientScrollToBottom(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setIsFetchingChatMessages(false);
|
setIsFetchingChatMessages(false);
|
||||||
|
|
||||||
@@ -534,17 +545,6 @@ export function ChatPage({
|
|||||||
new Map([[chatSessionIdRef.current, "input"]])
|
new Map([[chatSessionIdRef.current, "input"]])
|
||||||
);
|
);
|
||||||
|
|
||||||
const [scrollHeight, setScrollHeight] = useState<Map<number | null, number>>(
|
|
||||||
new Map([[chatSessionIdRef.current, 0]])
|
|
||||||
);
|
|
||||||
const currentScrollHeight = () => {
|
|
||||||
return scrollHeight.get(currentSessionId());
|
|
||||||
};
|
|
||||||
|
|
||||||
const retrieveCurrentScrollHeight = (): number | null => {
|
|
||||||
return scrollHeight.get(currentSessionId()) || null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [regenerationState, setRegenerationState] = useState<
|
const [regenerationState, setRegenerationState] = useState<
|
||||||
Map<number | null, RegenerationState | null>
|
Map<number | null, RegenerationState | null>
|
||||||
>(new Map([[null, null]]));
|
>(new Map([[null, null]]));
|
||||||
@@ -780,7 +780,7 @@ export function ChatPage({
|
|||||||
|
|
||||||
const clientScrollToBottom = (fast?: boolean) => {
|
const clientScrollToBottom = (fast?: boolean) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!endDivRef.current) {
|
if (!endDivRef.current || !scrollableDivRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,13 +789,38 @@ export function ChatPage({
|
|||||||
|
|
||||||
if (isVisible) return;
|
if (isVisible) return;
|
||||||
|
|
||||||
endDivRef.current.scrollIntoView({ behavior: fast ? "auto" : "smooth" });
|
// Check if all messages are currently rendered
|
||||||
setHasPerformedInitialScroll(true);
|
if (currentVisibleRange.end < messageHistory.length) {
|
||||||
|
// Update visible range to include the last messages
|
||||||
|
updateCurrentVisibleRange({
|
||||||
|
start: Math.max(
|
||||||
|
0,
|
||||||
|
messageHistory.length -
|
||||||
|
(currentVisibleRange.end - currentVisibleRange.start)
|
||||||
|
),
|
||||||
|
end: messageHistory.length,
|
||||||
|
mostVisibleMessageId: currentVisibleRange.mostVisibleMessageId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the state update and re-render before scrolling
|
||||||
|
setTimeout(() => {
|
||||||
|
endDivRef.current?.scrollIntoView({
|
||||||
|
behavior: fast ? "auto" : "smooth",
|
||||||
|
});
|
||||||
|
setHasPerformedInitialScroll(true);
|
||||||
|
}, 0);
|
||||||
|
} else {
|
||||||
|
// If all messages are already rendered, scroll immediately
|
||||||
|
endDivRef.current.scrollIntoView({
|
||||||
|
behavior: fast ? "auto" : "smooth",
|
||||||
|
});
|
||||||
|
setHasPerformedInitialScroll(true);
|
||||||
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
};
|
};
|
||||||
|
|
||||||
const distance = 500; // distance that should "engage" the scroll
|
const distance = 500; // distance that should "engage" the scroll
|
||||||
const debounce = 100; // time for debouncing
|
const debounceNumber = 100; // time for debouncing
|
||||||
|
|
||||||
const [hasPerformedInitialScroll, setHasPerformedInitialScroll] = useState(
|
const [hasPerformedInitialScroll, setHasPerformedInitialScroll] = useState(
|
||||||
existingChatSessionId === null
|
existingChatSessionId === null
|
||||||
@@ -1516,9 +1541,129 @@ export function ChatPage({
|
|||||||
scrollDist,
|
scrollDist,
|
||||||
endDivRef,
|
endDivRef,
|
||||||
distance,
|
distance,
|
||||||
debounce,
|
debounceNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Virtualization + Scrolling related effects and functions
|
||||||
|
const scrollInitialized = useRef(false);
|
||||||
|
interface VisibleRange {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
mostVisibleMessageId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [visibleRange, setVisibleRange] = useState<
|
||||||
|
Map<number | null, VisibleRange>
|
||||||
|
>(() => {
|
||||||
|
const initialRange: VisibleRange = {
|
||||||
|
start: 0,
|
||||||
|
end: BUFFER_COUNT,
|
||||||
|
mostVisibleMessageId: null,
|
||||||
|
};
|
||||||
|
return new Map([[chatSessionIdRef.current, initialRange]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function used to update current visible range. Only method for updating `visibleRange` state.
|
||||||
|
const updateCurrentVisibleRange = (
|
||||||
|
newRange: VisibleRange,
|
||||||
|
forceUpdate?: boolean
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
scrollInitialized.current &&
|
||||||
|
visibleRange.get(loadedIdSessionRef.current) == undefined &&
|
||||||
|
!forceUpdate
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVisibleRange((prevState) => {
|
||||||
|
const newState = new Map(prevState);
|
||||||
|
newState.set(loadedIdSessionRef.current, newRange);
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set first value for visibleRange state on page load / refresh.
|
||||||
|
const initializeVisibleRange = () => {
|
||||||
|
const upToDatemessageHistory = buildLatestMessageChain(
|
||||||
|
currentMessageMap(completeMessageDetail)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!scrollInitialized.current && upToDatemessageHistory.length > 0) {
|
||||||
|
const newEnd = Math.max(upToDatemessageHistory.length, BUFFER_COUNT);
|
||||||
|
const newStart = Math.max(0, newEnd - BUFFER_COUNT);
|
||||||
|
const newMostVisibleMessageId =
|
||||||
|
upToDatemessageHistory[newEnd - 1]?.messageId;
|
||||||
|
|
||||||
|
updateCurrentVisibleRange(
|
||||||
|
{
|
||||||
|
start: newStart,
|
||||||
|
end: newEnd,
|
||||||
|
mostVisibleMessageId: newMostVisibleMessageId,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
scrollInitialized.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateVisibleRangeBasedOnScroll = () => {
|
||||||
|
if (!scrollInitialized.current) return;
|
||||||
|
const scrollableDiv = scrollableDivRef.current;
|
||||||
|
if (!scrollableDiv) return;
|
||||||
|
|
||||||
|
const viewportHeight = scrollableDiv.clientHeight;
|
||||||
|
let mostVisibleMessageIndex = -1;
|
||||||
|
|
||||||
|
messageHistory.forEach((message, index) => {
|
||||||
|
const messageElement = document.getElementById(
|
||||||
|
`message-${message.messageId}`
|
||||||
|
);
|
||||||
|
if (messageElement) {
|
||||||
|
const rect = messageElement.getBoundingClientRect();
|
||||||
|
const isVisible = rect.bottom <= viewportHeight && rect.bottom > 0;
|
||||||
|
if (isVisible && index > mostVisibleMessageIndex) {
|
||||||
|
mostVisibleMessageIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mostVisibleMessageIndex !== -1) {
|
||||||
|
const startIndex = Math.max(0, mostVisibleMessageIndex - BUFFER_COUNT);
|
||||||
|
const endIndex = Math.min(
|
||||||
|
messageHistory.length,
|
||||||
|
mostVisibleMessageIndex + BUFFER_COUNT + 1
|
||||||
|
);
|
||||||
|
|
||||||
|
updateCurrentVisibleRange({
|
||||||
|
start: startIndex,
|
||||||
|
end: endIndex,
|
||||||
|
mostVisibleMessageId: messageHistory[mostVisibleMessageIndex].messageId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeVisibleRange();
|
||||||
|
}, [router, messageHistory, chatSessionIdRef.current]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
updateVisibleRangeBasedOnScroll();
|
||||||
|
};
|
||||||
|
scrollableDivRef.current?.addEventListener("scroll", handleScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scrollableDivRef.current?.removeEventListener("scroll", handleScroll);
|
||||||
|
};
|
||||||
|
}, [messageHistory]);
|
||||||
|
|
||||||
|
const currentVisibleRange = visibleRange.get(currentSessionId()) || {
|
||||||
|
start: 0,
|
||||||
|
end: 0,
|
||||||
|
mostVisibleMessageId: null,
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const includes = checkAnyAssistantHasSearch(
|
const includes = checkAnyAssistantHasSearch(
|
||||||
messageHistory,
|
messageHistory,
|
||||||
@@ -1801,7 +1946,18 @@ export function ChatPage({
|
|||||||
(hasPerformedInitialScroll ? "" : "invisible")
|
(hasPerformedInitialScroll ? "" : "invisible")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{messageHistory.map((message, i) => {
|
{(messageHistory.length < BUFFER_COUNT
|
||||||
|
? messageHistory
|
||||||
|
: messageHistory.slice(
|
||||||
|
currentVisibleRange.start,
|
||||||
|
currentVisibleRange.end
|
||||||
|
)
|
||||||
|
).map((message, fauxIndex) => {
|
||||||
|
const i =
|
||||||
|
messageHistory.length < BUFFER_COUNT
|
||||||
|
? fauxIndex
|
||||||
|
: fauxIndex + currentVisibleRange.start;
|
||||||
|
|
||||||
const messageMap = currentMessageMap(
|
const messageMap = currentMessageMap(
|
||||||
completeMessageDetail
|
completeMessageDetail
|
||||||
);
|
);
|
||||||
@@ -1809,6 +1965,7 @@ export function ChatPage({
|
|||||||
const parentMessage = message.parentMessageId
|
const parentMessage = message.parentMessageId
|
||||||
? messageMap.get(message.parentMessageId)
|
? messageMap.get(message.parentMessageId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(currentSessionRegenerationState?.regenerating &&
|
(currentSessionRegenerationState?.regenerating &&
|
||||||
message.messageId >
|
message.messageId >
|
||||||
@@ -1824,7 +1981,10 @@ export function ChatPage({
|
|||||||
|
|
||||||
if (message.type === "user") {
|
if (message.type === "user") {
|
||||||
return (
|
return (
|
||||||
<div key={messageReactComponentKey}>
|
<div
|
||||||
|
id={`message-${message.messageId}`}
|
||||||
|
key={messageReactComponentKey}
|
||||||
|
>
|
||||||
<HumanMessage
|
<HumanMessage
|
||||||
stopGenerating={stopGenerating}
|
stopGenerating={stopGenerating}
|
||||||
content={message.message}
|
content={message.message}
|
||||||
@@ -1901,6 +2061,7 @@ export function ChatPage({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
id={`message-${message.messageId}`}
|
||||||
key={messageReactComponentKey}
|
key={messageReactComponentKey}
|
||||||
ref={
|
ref={
|
||||||
i == messageHistory.length - 1
|
i == messageHistory.length - 1
|
||||||
|
@@ -16,6 +16,9 @@ export enum ChatSessionSharedStatus {
|
|||||||
Public = "public",
|
Public = "public",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The number of messages to buffer on the client side.
|
||||||
|
export const BUFFER_COUNT = 35;
|
||||||
|
|
||||||
export interface RetrievalDetails {
|
export interface RetrievalDetails {
|
||||||
run_search: "always" | "never" | "auto";
|
run_search: "always" | "never" | "auto";
|
||||||
real_time: boolean;
|
real_time: boolean;
|
||||||
|
@@ -647,14 +647,14 @@ export async function useScrollonStream({
|
|||||||
scrollDist,
|
scrollDist,
|
||||||
endDivRef,
|
endDivRef,
|
||||||
distance,
|
distance,
|
||||||
debounce,
|
debounceNumber,
|
||||||
}: {
|
}: {
|
||||||
chatState: ChatState;
|
chatState: ChatState;
|
||||||
scrollableDivRef: RefObject<HTMLDivElement>;
|
scrollableDivRef: RefObject<HTMLDivElement>;
|
||||||
scrollDist: MutableRefObject<number>;
|
scrollDist: MutableRefObject<number>;
|
||||||
endDivRef: RefObject<HTMLDivElement>;
|
endDivRef: RefObject<HTMLDivElement>;
|
||||||
distance: number;
|
distance: number;
|
||||||
debounce: number;
|
debounceNumber: number;
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const preventScrollInterference = useRef<boolean>(false);
|
const preventScrollInterference = useRef<boolean>(false);
|
||||||
@@ -711,7 +711,7 @@ export async function useScrollonStream({
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
blockActionRef.current = false;
|
blockActionRef.current = false;
|
||||||
}, debounce);
|
}, debounceNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user