From 55bada9ee2dd54e79fdf6a061d889686c4b654c9 Mon Sep 17 00:00:00 2001 From: southseact-3d Date: Sun, 8 Feb 2026 16:51:06 +0000 Subject: [PATCH] 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. --- chat/public/builder.js | 80 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/chat/public/builder.js b/chat/public/builder.js index a991072..f95bf05 100644 --- a/chat/public/builder.js +++ b/chat/public/builder.js @@ -1520,7 +1520,7 @@ function closeHistoryModal() { if (el.historyModal) el.historyModal.style.display = 'none'; } -function scrollChatToBottom() { +function scrollChatToBottom(force = false) { if (!el.chatArea) return; const target = el.chatArea; @@ -1533,14 +1533,18 @@ function scrollChatToBottom() { // 1. Immediate scroll attempt doScroll(); - // 2. After a short delay to allow DOM updates + // 2. After short delays to allow DOM updates setTimeout(() => { doScroll(); }, 10); setTimeout(() => { doScroll(); }, 50); setTimeout(() => { doScroll(); }, 100); setTimeout(() => { doScroll(); }, 250); 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(() => { doScroll(); 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'); + let imagesToLoad = 0; + images.forEach((img) => { 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') { const observer = new MutationObserver(() => { doScroll(); }); observer.observe(target, { childList: true, subtree: true }); // 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'); renderSessionMeta(freshSession); renderMessages(freshSession); - // Add a small delay to ensure DOM is updated before scrolling - setTimeout(() => scrollChatToBottom(), 50); + // Wait for content to fully render before scrolling + // Use multiple delays to catch content that loads asynchronously + setTimeout(() => scrollChatToBottom(), 100); + setTimeout(() => scrollChatToBottom(), 500); + setTimeout(() => scrollChatToBottom(), 1000); } else { // If refresh failed, use the local session data instead console.warn('[BUILDER] Using local session data for UI render'); renderSessionMeta(session); renderMessages(session); - // Add a small delay to ensure DOM is updated before scrolling - setTimeout(() => scrollChatToBottom(), 50); + // Wait for content to fully render before scrolling + setTimeout(() => scrollChatToBottom(), 100); + setTimeout(() => scrollChatToBottom(), 500); + setTimeout(() => scrollChatToBottom(), 1000); } // 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 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() {