(function () { // Target confirmed via DevTools: div.bubble-wrap[role="log"] inside #chatbot // scrollHeight > clientHeight = real scroll container. // autoscroll=False must be set in app.py — Gradio's own autoscroll // resets scrollTop after every update and overwrites our JS. function getContainer() { return document.querySelector('#chatbot .bubble-wrap'); } function scrollToBottom() { const el = getContainer(); if (el) el.scrollTop = el.scrollHeight; } // Keep scrolling every 100ms while content is still growing (streaming). // Stops 1.5s after scrollHeight stabilises. function pinUntilStable() { const el = getContainer(); if (!el) return; scrollToBottom(); let last = el.scrollHeight; let stable = 0; const id = setInterval(() => { const el = getContainer(); if (!el) { clearInterval(id); return; } scrollToBottom(); if (el.scrollHeight === last) { if (++stable >= 15) clearInterval(id); // 15 × 100ms = 1.5s stable } else { last = el.scrollHeight; stable = 0; } }, 100); } function hookTriggers() { // Enter key in textbox const textarea = document.querySelector('#question-box textarea'); if (textarea && !textarea._sh) { textarea._sh = true; textarea.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) pinUntilStable(); }); } // Followup buttons — Gradio replaces these after each response document.querySelectorAll('#followups button').forEach(btn => { if (!btn._sh) { btn._sh = true; btn.addEventListener('click', pinUntilStable); } }); } function attach() { const chatbot = document.querySelector('#chatbot'); if (!chatbot) { setTimeout(attach, 300); return; } // Watch for new message rows — fires when user sends or answer arrives let prevRows = 0; new MutationObserver(() => { const rows = chatbot.querySelectorAll('.message-row').length; if (rows !== prevRows) { prevRows = rows; pinUntilStable(); } hookTriggers(); // re-hook after Gradio replaces buttons }).observe(chatbot, { childList: true, subtree: true }); hookTriggers(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', attach); } else { attach(); } })(); // ── Header carousel ─────────────────────────────────────────────────────────── // Script lives here (not in header.html) because gr.HTML sanitises