fix session bottom scroll alignment (#1100)
This commit is contained in:
@@ -14,6 +14,7 @@ const { t } = useI18n();
|
|||||||
const { isDark } = useTheme();
|
const { isDark } = useTheme();
|
||||||
const { toolTraceVisible } = useToolTraceVisibility();
|
const { toolTraceVisible } = useToolTraceVisibility();
|
||||||
const listRef = ref<InstanceType<typeof VirtualMessageList> | null>(null);
|
const listRef = ref<InstanceType<typeof VirtualMessageList> | null>(null);
|
||||||
|
const pendingBottomSessionId = ref<string | null>(null);
|
||||||
|
|
||||||
function formatTokens(n: number): string {
|
function formatTokens(n: number): string {
|
||||||
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'
|
||||||
@@ -115,6 +116,7 @@ watch(
|
|||||||
() => chatStore.activeSessionId,
|
() => chatStore.activeSessionId,
|
||||||
(id) => {
|
(id) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
pendingBottomSessionId.value = id;
|
||||||
if (chatStore.focusMessageId) {
|
if (chatStore.focusMessageId) {
|
||||||
scrollToMessage(chatStore.focusMessageId);
|
scrollToMessage(chatStore.focusMessageId);
|
||||||
return;
|
return;
|
||||||
@@ -124,6 +126,20 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [chatStore.activeSessionId, chatStore.messages.length] as const,
|
||||||
|
([id, length]) => {
|
||||||
|
if (!id || pendingBottomSessionId.value !== id || length === 0) return;
|
||||||
|
pendingBottomSessionId.value = null;
|
||||||
|
if (chatStore.focusMessageId) {
|
||||||
|
scrollToMessage(chatStore.focusMessageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scrollToBottom();
|
||||||
|
},
|
||||||
|
{ flush: "post" },
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => chatStore.focusMessageId,
|
() => chatStore.focusMessageId,
|
||||||
(messageId) => {
|
(messageId) => {
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ const heightVersion = ref(0);
|
|||||||
const measuredHeights = new Map<string, number>();
|
const measuredHeights = new Map<string, number>();
|
||||||
const observedElements = new Map<string, HTMLElement>();
|
const observedElements = new Map<string, HTMLElement>();
|
||||||
const observers = new Map<string, ResizeObserver>();
|
const observers = new Map<string, ResizeObserver>();
|
||||||
|
let keepBottomUntil = 0;
|
||||||
|
let bottomFrame: number | null = null;
|
||||||
|
|
||||||
const messageKeys = computed(() => props.messages.map(messageKey));
|
const messageKeys = computed(() => props.messages.map(messageKey));
|
||||||
|
|
||||||
@@ -58,9 +60,10 @@ function setMeasuredHeight(key: string, height: number) {
|
|||||||
const oldHeight = itemHeight(key);
|
const oldHeight = itemHeight(key);
|
||||||
if (oldHeight === height) return;
|
if (oldHeight === height) return;
|
||||||
|
|
||||||
|
const el = scrollerRef.value;
|
||||||
|
const shouldKeepBottom = !!el && (Date.now() < keepBottomUntil || isNearBottom(64));
|
||||||
const index = messageKeys.value.indexOf(key);
|
const index = messageKeys.value.indexOf(key);
|
||||||
if (index >= 0) {
|
if (index >= 0 && !shouldKeepBottom) {
|
||||||
const el = scrollerRef.value;
|
|
||||||
const rowTop = layout.value.offsets[index] || 0;
|
const rowTop = layout.value.offsets[index] || 0;
|
||||||
const delta = height - oldHeight;
|
const delta = height - oldHeight;
|
||||||
if (el && rowTop < scrollTop.value && delta !== 0) {
|
if (el && rowTop < scrollTop.value && delta !== 0) {
|
||||||
@@ -71,6 +74,7 @@ function setMeasuredHeight(key: string, height: number) {
|
|||||||
|
|
||||||
measuredHeights.set(key, height);
|
measuredHeights.set(key, height);
|
||||||
heightVersion.value += 1;
|
heightVersion.value += 1;
|
||||||
|
if (shouldKeepBottom) scheduleScrollToBottom(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const layout = computed(() => {
|
const layout = computed(() => {
|
||||||
@@ -167,14 +171,34 @@ function isNearBottom(threshold = 200): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
|
keepBottomUntil = Date.now() + 700;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const el = scrollerRef.value;
|
scheduleScrollToBottom(3);
|
||||||
if (!el) return;
|
|
||||||
el.scrollTop = el.scrollHeight;
|
|
||||||
syncViewport();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setScrollToBottomNow() {
|
||||||
|
const el = scrollerRef.value;
|
||||||
|
if (!el) return;
|
||||||
|
el.scrollTop = Math.max(0, el.scrollHeight - el.clientHeight);
|
||||||
|
syncViewport();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleScrollToBottom(frames = 1) {
|
||||||
|
if (bottomFrame != null) cancelAnimationFrame(bottomFrame);
|
||||||
|
|
||||||
|
const step = (remaining: number) => {
|
||||||
|
setScrollToBottomNow();
|
||||||
|
if (remaining <= 1) {
|
||||||
|
bottomFrame = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bottomFrame = requestAnimationFrame(() => step(remaining - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
bottomFrame = requestAnimationFrame(() => step(frames));
|
||||||
|
}
|
||||||
|
|
||||||
function scrollToMessage(messageId: string) {
|
function scrollToMessage(messageId: string) {
|
||||||
const index = props.messages.findIndex(message => String(message.id) === messageId);
|
const index = props.messages.findIndex(message => String(message.id) === messageId);
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
@@ -237,6 +261,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
if (bottomFrame != null) cancelAnimationFrame(bottomFrame);
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
for (const observer of observers.values()) observer.disconnect();
|
for (const observer of observers.values()) observer.disconnect();
|
||||||
observers.clear();
|
observers.clear();
|
||||||
|
|||||||
Reference in New Issue
Block a user