Fix scroll-to-bottom on page load/refresh in builder page

The scroll-to-bottom functionality wasn't working properly on page load because
messages take time to load and render. This fix adds:

- Enhanced scrollChatToBottom() with image load detection
- ResizeObserver to detect content height changes
- Multiple scroll attempts with increasing delays (up to 2s)
- Additional scroll calls in selectSessionById with proper delays
- Scroll calls at end of initBuilder initialization

This ensures the chat area properly scrolls to the bottom even when content
loads asynchronously or images take time to render.
This commit is contained in:
southseact-3d
2026-02-08 16:51:06 +00:00
parent dacde39400
commit 55bada9ee2

View File

@@ -1520,7 +1520,7 @@ function closeHistoryModal() {
if (el.historyModal) el.historyModal.style.display = 'none'; if (el.historyModal) el.historyModal.style.display = 'none';
} }
function scrollChatToBottom() { function scrollChatToBottom(force = false) {
if (!el.chatArea) return; if (!el.chatArea) return;
const target = el.chatArea; const target = el.chatArea;
@@ -1533,14 +1533,18 @@ function scrollChatToBottom() {
// 1. Immediate scroll attempt // 1. Immediate scroll attempt
doScroll(); doScroll();
// 2. After a short delay to allow DOM updates // 2. After short delays to allow DOM updates
setTimeout(() => { doScroll(); }, 10); setTimeout(() => { doScroll(); }, 10);
setTimeout(() => { doScroll(); }, 50); setTimeout(() => { doScroll(); }, 50);
setTimeout(() => { doScroll(); }, 100); setTimeout(() => { doScroll(); }, 100);
setTimeout(() => { doScroll(); }, 250); setTimeout(() => { doScroll(); }, 250);
setTimeout(() => { doScroll(); }, 500); setTimeout(() => { doScroll(); }, 500);
// 3. Use requestAnimationFrame for smooth scrolling // 3. Longer delay for content to fully settle (especially on page load)
setTimeout(() => { doScroll(); }, 1000);
setTimeout(() => { doScroll(); }, 2000);
// 4. Use requestAnimationFrame for smooth scrolling
requestAnimationFrame(() => { requestAnimationFrame(() => {
doScroll(); doScroll();
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -1551,22 +1555,65 @@ function scrollChatToBottom() {
}); });
}); });
// 4. Scroll when images load (if any) // 5. Wait for ALL images to load before scrolling
const images = target.querySelectorAll('img'); const images = target.querySelectorAll('img');
let imagesToLoad = 0;
images.forEach((img) => { images.forEach((img) => {
if (!img.complete) { if (!img.complete) {
img.addEventListener('load', () => doScroll(), { once: true }); imagesToLoad++;
img.addEventListener('load', () => {
imagesToLoad--;
doScroll();
// Scroll again after a short delay to ensure layout is updated
setTimeout(() => doScroll(), 100);
}, { once: true });
img.addEventListener('error', () => {
imagesToLoad--;
doScroll();
}, { once: true });
} }
}); });
// 5. Also scroll when any content finishes rendering (using MutationObserver) // If there are images loading, scroll again after they all complete
if (imagesToLoad > 0) {
const checkImagesLoaded = setInterval(() => {
if (imagesToLoad === 0) {
clearInterval(checkImagesLoaded);
doScroll();
setTimeout(() => doScroll(), 100);
setTimeout(() => doScroll(), 500);
}
}, 100);
// Stop checking after 5 seconds to avoid infinite interval
setTimeout(() => clearInterval(checkImagesLoaded), 5000);
}
// 6. Use ResizeObserver to detect when content height changes
if (typeof ResizeObserver !== 'undefined') {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target === target) {
doScroll();
}
}
});
resizeObserver.observe(target);
// Stop observing after a while
setTimeout(() => resizeObserver.disconnect(), 3000);
}
// 7. Also scroll when any content finishes rendering (using MutationObserver)
if (typeof MutationObserver !== 'undefined') { if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
doScroll(); doScroll();
}); });
observer.observe(target, { childList: true, subtree: true }); observer.observe(target, { childList: true, subtree: true });
// Stop observing after a while to avoid memory leaks // Stop observing after a while to avoid memory leaks
setTimeout(() => observer.disconnect(), 2000); setTimeout(() => observer.disconnect(), 3000);
} }
} }
@@ -2775,15 +2822,20 @@ window.selectSessionById = async function(id) {
console.log('[BUILDER] Rendering UI with fresh session data'); console.log('[BUILDER] Rendering UI with fresh session data');
renderSessionMeta(freshSession); renderSessionMeta(freshSession);
renderMessages(freshSession); renderMessages(freshSession);
// Add a small delay to ensure DOM is updated before scrolling // Wait for content to fully render before scrolling
setTimeout(() => scrollChatToBottom(), 50); // Use multiple delays to catch content that loads asynchronously
setTimeout(() => scrollChatToBottom(), 100);
setTimeout(() => scrollChatToBottom(), 500);
setTimeout(() => scrollChatToBottom(), 1000);
} else { } else {
// If refresh failed, use the local session data instead // If refresh failed, use the local session data instead
console.warn('[BUILDER] Using local session data for UI render'); console.warn('[BUILDER] Using local session data for UI render');
renderSessionMeta(session); renderSessionMeta(session);
renderMessages(session); renderMessages(session);
// Add a small delay to ensure DOM is updated before scrolling // Wait for content to fully render before scrolling
setTimeout(() => scrollChatToBottom(), 50); setTimeout(() => scrollChatToBottom(), 100);
setTimeout(() => scrollChatToBottom(), 500);
setTimeout(() => scrollChatToBottom(), 1000);
} }
// Update URL to reflect the new session (but don't reload page) // Update URL to reflect the new session (but don't reload page)
@@ -4557,6 +4609,12 @@ window.addEventListener('focus', () => {
// Load provider limits for fallback model selection // Load provider limits for fallback model selection
loadProviderLimits(); loadProviderLimits();
// Scroll to bottom after initialization completes - use multiple delays
// to catch content that renders asynchronously
setTimeout(() => scrollChatToBottom(), 500);
setTimeout(() => scrollChatToBottom(), 1000);
setTimeout(() => scrollChatToBottom(), 2000);
})(); })();
async function loadProviderLimits() { async function loadProviderLimits() {