diff --git a/chat/public/builder.html b/chat/public/builder.html index 2313648..9cc39f7 100644 --- a/chat/public/builder.html +++ b/chat/public/builder.html @@ -55,6 +55,10 @@ flex: 1; overflow-y: auto; min-height: 0; + -webkit-overflow-scrolling: touch; + scroll-behavior: auto; + transform: translateZ(0); + will-change: scroll-position; } .app-shell.builder-single>* { diff --git a/chat/public/builder.js b/chat/public/builder.js index f95bf05..4ac2a55 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(force = false) { +function scrollChatToBottom() { if (!el.chatArea) return; const target = el.chatArea; @@ -1529,91 +1529,48 @@ function scrollChatToBottom(force = false) { target.scrollTop = target.scrollHeight; }; - // Use multiple techniques to ensure scroll happens after content is rendered - // 1. Immediate scroll attempt + // Use multiple scroll techniques in sequence for reliability + // 1. Immediate scroll doScroll(); - // 2. After short delays to allow DOM updates - setTimeout(() => { doScroll(); }, 10); - setTimeout(() => { doScroll(); }, 50); - setTimeout(() => { doScroll(); }, 100); - setTimeout(() => { doScroll(); }, 250); - setTimeout(() => { doScroll(); }, 500); + // 2. After DOM updates + setTimeout(doScroll, 50); + setTimeout(doScroll, 100); + setTimeout(doScroll, 200); + setTimeout(doScroll, 500); - // 3. Longer delay for content to fully settle (especially on page load) - setTimeout(() => { doScroll(); }, 1000); - setTimeout(() => { doScroll(); }, 2000); + // 3. After content fully renders + setTimeout(doScroll, 1000); - // 4. Use requestAnimationFrame for smooth scrolling + // 4. requestAnimationFrame for smooth scrolling requestAnimationFrame(() => { doScroll(); - requestAnimationFrame(() => { - doScroll(); - requestAnimationFrame(() => { - doScroll(); - }); - }); + requestAnimationFrame(doScroll); }); - // 5. Wait for ALL images to load before scrolling - const images = target.querySelectorAll('img'); - let imagesToLoad = 0; - - images.forEach((img) => { - if (!img.complete) { - 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 }); - } - }); - - // 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(() => { + // 5. Scroll to last message element if it exists (most reliable method) + setTimeout(() => { + const lastMessage = target.querySelector('.message:last-child'); + if (lastMessage) { + lastMessage.scrollIntoView({ behavior: 'smooth', block: 'end' }); + } else { + // Fallback to regular scroll doScroll(); - }); + } + }, 100); + + // 6. ResizeObserver for dynamic content + if (typeof ResizeObserver !== 'undefined') { + const resizeObserver = new ResizeObserver(() => doScroll()); + resizeObserver.observe(target); + setTimeout(() => resizeObserver.disconnect(), 2000); + } + + // 7. MutationObserver for DOM changes + 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(), 3000); + setTimeout(() => observer.disconnect(), 2000); } } @@ -2027,7 +1984,22 @@ function renderMessages(session) { } } + // Scroll to bottom after DOM updates scrollChatToBottom(); + + // Additional scroll with longer delay to ensure all content is rendered + setTimeout(() => { + scrollChatToBottom(); + // Force scroll by accessing scrollHeight directly + if (el.chatArea) { + el.chatArea.scrollTop = el.chatArea.scrollHeight; + // Scroll last element into view as fallback + const lastMessage = el.chatArea.querySelector('.message:last-child'); + if (lastMessage) { + lastMessage.scrollIntoView({ block: 'end' }); + } + } + }, 200); updateExportButtonVisibility(session); }