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);
}