Compare commits

...

2 Commits

Author SHA1 Message Date
pablonyx
f4afe91368 k 2025-02-25 12:18:08 -08:00
pablonyx
d10a665f61 k 2025-02-25 12:14:05 -08:00

View File

@@ -947,49 +947,59 @@ export function ChatPage({
const clientScrollToBottom = (fast?: boolean) => {
waitForScrollRef.current = true;
// Set the flag to indicate we're programmatically scrolling
isScrollingProgrammaticallyRef.current = true;
setTimeout(() => {
if (!endDivRef.current || !scrollableDivRef.current) {
console.error("endDivRef or scrollableDivRef not found");
isScrollingProgrammaticallyRef.current = false;
return;
}
const rect = endDivRef.current.getBoundingClientRect();
const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
if (isVisible) return;
if (isVisible) {
isScrollingProgrammaticallyRef.current = false;
return;
}
// Check if all messages are currently rendered
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,
});
// First, update the visible range to include the latest messages
const targetEnd = messageHistory.length;
const targetStart = Math.max(0, targetEnd - BUFFER_COUNT * 2);
// Wait for the state update and re-render before scrolling
setTimeout(() => {
endDivRef.current?.scrollIntoView({
// Update the last update time to prevent immediate re-calculation
lastVisibleRangeUpdateRef.current = Date.now();
updateCurrentVisibleRange(
{
start: targetStart,
end: targetEnd,
mostVisibleMessageId:
messageHistory[targetEnd - 1]?.messageId || null,
},
true
);
// Wait for render, then scroll
setTimeout(() => {
if (endDivRef.current) {
endDivRef.current.scrollIntoView({
behavior: fast ? "auto" : "smooth",
block: "end",
});
setHasPerformedInitialScroll(true);
}, 100);
} else {
// If all messages are already rendered, scroll immediately
endDivRef.current.scrollIntoView({
behavior: fast ? "auto" : "smooth",
});
}
setHasPerformedInitialScroll(true);
}
// Reset the flag after scrolling completes
setTimeout(() => {
isScrollingProgrammaticallyRef.current = false;
}, 300);
}, 50);
}, 50);
// Reset waitForScrollRef after 1.5 seconds
// Reset waitForScrollRef after scrolling completes
setTimeout(() => {
waitForScrollRef.current = false;
}, 1500);
@@ -2005,11 +2015,45 @@ export function ChatPage({
return;
}
const currentRange = visibleRange.get(loadedIdSessionRef.current);
const prevMostVisibleMessageId = currentRange?.mostVisibleMessageId;
setVisibleRange((prevState) => {
const newState = new Map(prevState);
newState.set(loadedIdSessionRef.current, newRange);
return newState;
});
// Preserve scroll position when the visible range changes
if (
prevMostVisibleMessageId &&
prevMostVisibleMessageId !== newRange.mostVisibleMessageId
) {
preserveScrollPosition(prevMostVisibleMessageId);
}
};
// Add this to preserve scroll position when updating the visible range
const preserveScrollPosition = (prevMostVisibleMessageId: number) => {
if (!prevMostVisibleMessageId) return;
// Set the flag to indicate we're programmatically scrolling
isScrollingProgrammaticallyRef.current = true;
setTimeout(() => {
const messageElement = document.getElementById(
`message-${prevMostVisibleMessageId}`
);
if (messageElement) {
// Scroll to the position where this message was visible before
messageElement.scrollIntoView({ block: "center", behavior: "auto" });
}
// Reset the flag after a short delay to allow the scroll to complete
setTimeout(() => {
isScrollingProgrammaticallyRef.current = false;
}, 150);
}, 0);
};
// Set first value for visibleRange state on page load / refresh.
@@ -2019,30 +2063,66 @@ export function ChatPage({
);
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;
// Start with the entire message history visible if it's smaller than 2x buffer
if (upToDatemessageHistory.length <= BUFFER_COUNT * 2) {
updateCurrentVisibleRange(
{
start: 0,
end: upToDatemessageHistory.length,
mostVisibleMessageId:
upToDatemessageHistory[upToDatemessageHistory.length - 1]
?.messageId || null,
},
true
);
} else {
// Otherwise, start at the end with a buffer
const newEnd = upToDatemessageHistory.length;
const newStart = Math.max(0, newEnd - BUFFER_COUNT * 2);
updateCurrentVisibleRange(
{
start: newStart,
end: newEnd,
mostVisibleMessageId: newMostVisibleMessageId,
},
true
);
updateCurrentVisibleRange(
{
start: newStart,
end: newEnd,
mostVisibleMessageId:
upToDatemessageHistory[newEnd - 1]?.messageId || null,
},
true
);
}
scrollInitialized.current = true;
}
};
// Add a ref to track the last update time to prevent rapid oscillations
const lastVisibleRangeUpdateRef = useRef<number>(0);
// Add a ref to track if we're currently in a scroll operation triggered by code
const isScrollingProgrammaticallyRef = useRef<boolean>(false);
const updateVisibleRangeBasedOnScroll = () => {
if (!scrollInitialized.current) return;
const scrollableDiv = scrollableDivRef.current;
if (!scrollableDiv) return;
// Skip if we're programmatically scrolling to avoid feedback loops
if (isScrollingProgrammaticallyRef.current) return;
// Throttle updates to prevent oscillation
const now = Date.now();
if (now - lastVisibleRangeUpdateRef.current < 100) return; // 100ms throttle
const viewportTop = scrollableDiv.scrollTop;
const viewportHeight = scrollableDiv.clientHeight;
let mostVisibleMessageIndex = -1;
const viewportBottom = viewportTop + viewportHeight;
// Track visibility percentage for each message
interface MessageVisibility {
index: number;
messageId: number;
visibilityPercentage: number;
}
let messageVisibility: MessageVisibility[] = [];
messageHistory.forEach((message, index) => {
const messageElement = document.getElementById(
@@ -2050,25 +2130,74 @@ export function ChatPage({
);
if (messageElement) {
const rect = messageElement.getBoundingClientRect();
const isVisible = rect.bottom <= viewportHeight && rect.bottom > 0;
if (isVisible && index > mostVisibleMessageIndex) {
mostVisibleMessageIndex = index;
const elementHeight = rect.height;
const elementTop =
rect.top -
scrollableDiv.getBoundingClientRect().top +
scrollableDiv.scrollTop;
const elementBottom = elementTop + elementHeight;
// Calculate how much of the element is visible
const visibleTop = Math.max(elementTop, viewportTop);
const visibleBottom = Math.min(elementBottom, viewportBottom);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
const visibilityPercentage = visibleHeight / elementHeight;
if (visibilityPercentage > 0) {
messageVisibility.push({
index,
messageId: message.messageId,
visibilityPercentage,
});
}
}
});
if (mostVisibleMessageIndex !== -1) {
const startIndex = Math.max(0, mostVisibleMessageIndex - BUFFER_COUNT);
const endIndex = Math.min(
messageHistory.length,
mostVisibleMessageIndex + BUFFER_COUNT + 1
// If we found visible messages
if (messageVisibility.length > 0) {
// Sort by visibility percentage
messageVisibility.sort(
(a, b) => b.visibilityPercentage - a.visibilityPercentage
);
const mostVisibleMessage = messageVisibility[0];
updateCurrentVisibleRange({
start: startIndex,
end: endIndex,
mostVisibleMessageId: messageHistory[mostVisibleMessageIndex].messageId,
});
// Get current range
const currentRange = visibleRange.get(loadedIdSessionRef.current);
// Only update if there's a significant change in the most visible message
// or if the current range doesn't include the most visible message
const shouldUpdate =
!currentRange ||
// If the most visible message is different and not close to the previous one
(currentRange.mostVisibleMessageId !== mostVisibleMessage.messageId &&
Math.abs(
messageHistory.findIndex(
(m) => m.messageId === currentRange.mostVisibleMessageId
) - mostVisibleMessage.index
) > 5) ||
// Or if the most visible message is outside the current range
mostVisibleMessage.index < currentRange.start ||
mostVisibleMessage.index >= currentRange.end;
if (shouldUpdate) {
// Calculate buffer around the most visible message
const bufferBefore = Math.max(
0,
mostVisibleMessage.index - BUFFER_COUNT
);
const bufferAfter = Math.min(
messageHistory.length,
mostVisibleMessage.index + BUFFER_COUNT + 1
);
lastVisibleRangeUpdateRef.current = now;
updateCurrentVisibleRange({
start: bufferBefore,
end: bufferAfter,
mostVisibleMessageId: mostVisibleMessage.messageId,
});
}
}
};
@@ -2686,8 +2815,21 @@ export function ChatPage({
: null;
return (
<div
className={`text-text ${
process.env.NODE_ENV === "development"
? i >= currentVisibleRange.start &&
i < currentVisibleRange.end
? "border-l-4 border-green-500"
: ""
: ""
}`}
id={`message-${message.messageId}`}
key={messageReactComponentKey}
ref={
i == messageHistory.length - 1
? lastMessageRef
: null
}
>
<HumanMessage
disableSwitchingForStreaming={
@@ -2789,7 +2931,14 @@ export function ChatPage({
return (
<div
className="text-text"
className={`text-text ${
process.env.NODE_ENV === "development"
? i >= currentVisibleRange.start &&
i < currentVisibleRange.end
? "border-l-4 border-green-500"
: ""
: ""
}`}
id={`message-${message.messageId}`}
key={messageReactComponentKey}
ref={
@@ -3356,6 +3505,36 @@ export function ChatPage({
</div>
{/* Right Sidebar - DocumentSidebar */}
</div>
{/* Debug Panel - Only visible in development mode */}
{process.env.NODE_ENV === "development" && (
<div className="fixed right-0 top-0 bg-black bg-opacity-75 text-white p-2 z-50 text-xs">
<div>
Visible: {currentVisibleRange.start} - {currentVisibleRange.end}
</div>
<div>Most visible: {currentVisibleRange.mostVisibleMessageId}</div>
<div>Total messages: {messageHistory.length}</div>
<div>
Programmatic scroll:{" "}
{isScrollingProgrammaticallyRef.current ? "Yes" : "No"}
</div>
<div>
Last update: {Date.now() - lastVisibleRangeUpdateRef.current}ms ago
</div>
<button
className="mt-2 bg-blue-500 hover:bg-blue-700 text-white font-bold py-1 px-2 rounded text-xs"
onClick={() => {
// Reset all scroll-related state
isScrollingProgrammaticallyRef.current = false;
lastVisibleRangeUpdateRef.current = 0;
scrollInitialized.current = false;
initializeVisibleRange();
}}
>
Reset Scroll State
</button>
</div>
)}
</>
);
}