mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 16:21:50 +03:00
fix(webui): preserve semantic history anchors
This commit is contained in:
@@ -136,6 +136,7 @@ export function ThreadMessages({
|
||||
return (
|
||||
<ThreadDisplayUnit
|
||||
key={unitKeys[index]}
|
||||
unitKey={unitKeys[index]}
|
||||
unit={unit}
|
||||
marginTop={marginTop}
|
||||
userPromptId={userPromptId}
|
||||
@@ -225,6 +226,7 @@ function pendingTurnProjection(
|
||||
}
|
||||
|
||||
interface ThreadDisplayUnitProps {
|
||||
unitKey: string;
|
||||
unit: DisplayUnit;
|
||||
marginTop: string;
|
||||
userPromptId?: string;
|
||||
@@ -243,6 +245,7 @@ interface ThreadDisplayUnitProps {
|
||||
}
|
||||
|
||||
const ThreadDisplayUnit = memo(function ThreadDisplayUnit({
|
||||
unitKey,
|
||||
unit,
|
||||
marginTop,
|
||||
userPromptId,
|
||||
@@ -273,7 +276,7 @@ const ThreadDisplayUnit = memo(function ThreadDisplayUnit({
|
||||
<>
|
||||
<div
|
||||
className={`${marginTop}${stableDeferOffscreenRender ? " thread-render-unit" : ""}`}
|
||||
data-thread-display-unit
|
||||
data-thread-display-unit={unitKey}
|
||||
data-user-prompt-id={userPromptId}
|
||||
>
|
||||
{unit.type === "activity" ? (
|
||||
|
||||
@@ -73,7 +73,7 @@ export const INITIAL_HISTORY_WINDOW = 160;
|
||||
export const HISTORY_WINDOW_INCREMENT = 120;
|
||||
|
||||
interface HistoryScrollAnchor {
|
||||
element: HTMLElement;
|
||||
key: string;
|
||||
offsetTop: number;
|
||||
}
|
||||
|
||||
@@ -316,8 +316,10 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
for (const element of units) {
|
||||
const bounds = element.getBoundingClientRect();
|
||||
if (bounds.bottom <= scrollerTop + 0.5) continue;
|
||||
const key = element.dataset.threadDisplayUnit;
|
||||
if (!key) continue;
|
||||
historyScrollAnchorRef.current = {
|
||||
element,
|
||||
key,
|
||||
offsetTop: bounds.top - scrollerTop,
|
||||
};
|
||||
return true;
|
||||
@@ -328,15 +330,19 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
|
||||
const reconcileHistoryScrollAnchor = useCallback(() => {
|
||||
const scroller = scrollRef.current;
|
||||
const content = messageContentRef.current;
|
||||
const anchor = historyScrollAnchorRef.current;
|
||||
if (!scroller || !anchor) return false;
|
||||
if (!anchor.element.isConnected) {
|
||||
if (!scroller || !content || !anchor) return false;
|
||||
const element = Array.from(
|
||||
content.querySelectorAll<HTMLElement>("[data-thread-display-unit]"),
|
||||
).find((candidate) => candidate.dataset.threadDisplayUnit === anchor.key);
|
||||
if (!element) {
|
||||
historyScrollAnchorRef.current = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
const nextOffset =
|
||||
anchor.element.getBoundingClientRect().top
|
||||
element.getBoundingClientRect().top
|
||||
- scroller.getBoundingClientRect().top;
|
||||
const delta = nextOffset - anchor.offsetTop;
|
||||
if (Math.abs(delta) < 0.5) return true;
|
||||
@@ -344,7 +350,7 @@ export const ThreadViewport = forwardRef<ThreadViewportHandle, ThreadViewportPro
|
||||
const nextTop = Math.min(maxScrollTop, Math.max(0, scroller.scrollTop + delta));
|
||||
threadMotionRef.current?.jumpTo(nextTop);
|
||||
return true;
|
||||
}, [captureHistoryScrollAnchor]);
|
||||
}, []);
|
||||
|
||||
const scrollToBottomNow = useCallback((smooth = false) => {
|
||||
historyScrollAnchorRef.current = null;
|
||||
|
||||
@@ -1521,8 +1521,18 @@ describe("ThreadViewport", () => {
|
||||
dispatchUserScroll(scroller);
|
||||
});
|
||||
|
||||
const replacement = anchor.cloneNode(true) as HTMLElement;
|
||||
anchor.replaceWith(replacement);
|
||||
anchorDocumentTop += 180;
|
||||
scrollHeight += 180;
|
||||
Object.defineProperty(replacement, "getBoundingClientRect", {
|
||||
configurable: true,
|
||||
value: () => DOMRect.fromRect({
|
||||
y: anchorDocumentTop - scroller.scrollTop,
|
||||
width: 800,
|
||||
height: 40,
|
||||
}),
|
||||
});
|
||||
const content = screen.getByTestId("thread-message-region").firstElementChild;
|
||||
const observer = resizeObserver.observers.find((candidate) =>
|
||||
content ? candidate.elements.includes(content) : false,
|
||||
@@ -1533,7 +1543,7 @@ describe("ThreadViewport", () => {
|
||||
});
|
||||
|
||||
expect(scroller.scrollTop).toBe(260);
|
||||
expect(anchor.getBoundingClientRect().top).toBe(120);
|
||||
expect(replacement.getBoundingClientRect().top).toBe(120);
|
||||
} finally {
|
||||
resizeObserver.restore();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user