Fix scroll-to-bottom in builder page

- Simplified scrollChatToBottom() with more reliable techniques
- Added scrollIntoView on last message as fallback
- Added additional scroll with direct scrollTop assignment after delay
- Added CSS optimizations for scrolling behavior (-webkit-overflow-scrolling, transform, will-change)
- Added more robust scroll attempts in renderMessages after DOM updates
This commit is contained in:
southseact-3d
2026-02-08 17:16:07 +00:00
parent 55bada9ee2
commit c30d2ba715
2 changed files with 52 additions and 76 deletions

View File

@@ -55,6 +55,10 @@
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
min-height: 0; min-height: 0;
-webkit-overflow-scrolling: touch;
scroll-behavior: auto;
transform: translateZ(0);
will-change: scroll-position;
} }
.app-shell.builder-single>* { .app-shell.builder-single>* {

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(force = false) { function scrollChatToBottom() {
if (!el.chatArea) return; if (!el.chatArea) return;
const target = el.chatArea; const target = el.chatArea;
@@ -1529,91 +1529,48 @@ function scrollChatToBottom(force = false) {
target.scrollTop = target.scrollHeight; target.scrollTop = target.scrollHeight;
}; };
// Use multiple techniques to ensure scroll happens after content is rendered // Use multiple scroll techniques in sequence for reliability
// 1. Immediate scroll attempt // 1. Immediate scroll
doScroll(); doScroll();
// 2. After short delays to allow DOM updates // 2. After DOM updates
setTimeout(() => { doScroll(); }, 10); setTimeout(doScroll, 50);
setTimeout(() => { doScroll(); }, 50); setTimeout(doScroll, 100);
setTimeout(() => { doScroll(); }, 100); setTimeout(doScroll, 200);
setTimeout(() => { doScroll(); }, 250); setTimeout(doScroll, 500);
setTimeout(() => { doScroll(); }, 500);
// 3. Longer delay for content to fully settle (especially on page load) // 3. After content fully renders
setTimeout(() => { doScroll(); }, 1000); setTimeout(doScroll, 1000);
setTimeout(() => { doScroll(); }, 2000);
// 4. Use requestAnimationFrame for smooth scrolling // 4. requestAnimationFrame for smooth scrolling
requestAnimationFrame(() => { requestAnimationFrame(() => {
doScroll(); doScroll();
requestAnimationFrame(() => { requestAnimationFrame(doScroll);
doScroll();
requestAnimationFrame(() => {
doScroll();
});
});
}); });
// 5. Wait for ALL images to load before scrolling // 5. Scroll to last message element if it exists (most reliable method)
const images = target.querySelectorAll('img'); setTimeout(() => {
let imagesToLoad = 0; const lastMessage = target.querySelector('.message:last-child');
if (lastMessage) {
images.forEach((img) => { lastMessage.scrollIntoView({ behavior: 'smooth', block: 'end' });
if (!img.complete) { } else {
imagesToLoad++; // Fallback to regular scroll
img.addEventListener('load', () => {
imagesToLoad--;
doScroll(); 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); }, 100);
// Stop checking after 5 seconds to avoid infinite interval // 6. ResizeObserver for dynamic content
setTimeout(() => clearInterval(checkImagesLoaded), 5000);
}
// 6. Use ResizeObserver to detect when content height changes
if (typeof ResizeObserver !== 'undefined') { if (typeof ResizeObserver !== 'undefined') {
const resizeObserver = new ResizeObserver((entries) => { const resizeObserver = new ResizeObserver(() => doScroll());
for (const entry of entries) {
if (entry.target === target) {
doScroll();
}
}
});
resizeObserver.observe(target); resizeObserver.observe(target);
setTimeout(() => resizeObserver.disconnect(), 2000);
// Stop observing after a while
setTimeout(() => resizeObserver.disconnect(), 3000);
} }
// 7. Also scroll when any content finishes rendering (using MutationObserver) // 7. MutationObserver for DOM changes
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 setTimeout(() => observer.disconnect(), 2000);
setTimeout(() => observer.disconnect(), 3000);
} }
} }
@@ -2027,8 +1984,23 @@ function renderMessages(session) {
} }
} }
// Scroll to bottom after DOM updates
scrollChatToBottom(); 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); updateExportButtonVisibility(session);
} }