smooth scroller

smooth option for scroller

3clyp50 committed Jan 29, 2026 at 15:58 UTC a15069acc60340796a2ad43197c75cee9d0fc408
2 files changed +26 -19
webui/index.js
+1 -8
@@ -200,14 +200,7 @@ updateUserTime();
200 setInterval(updateUserTime, 1000);
201
202 function setMessages(...params) {
203 - const chatHistoryEl = document.getElementById("chat-history");
204 - let scroller;
205 -
206 - if (preferencesStore.autoScroll && chatHistoryEl) scroller = new msgs.Scroller(chatHistoryEl)
207 - const result = msgs.setMessages(...params);
208 - if (scroller) scroller.reApplyScroll();
209 -
210 - return result;
203 + return msgs.setMessages(...params);
204 }
205
206 globalThis.loadKnowledge = async function () {
webui/js/messages.js
+25 -11
@@ -69,6 +69,12 @@ export function setMessages(messages) {
69 const historyEmpty = !history || history.childElementCount === 0;
70 const isLargeAppend = !historyEmpty && messages.length > 10;
71 const cutoff = isLargeAppend ? Math.max(0, messages.length - 2) : 0;
72 + const massRender = historyEmpty || isLargeAppend;
73 +
74 + let mainScroller;
75 + if (preferencesStore.autoScroll && history) {
76 + mainScroller = new Scroller(history, { smooth: !massRender });
77 + }
78
79 // process messages
80 for (let i = 0; i < messages.length; i++) {
@@ -78,6 +84,8 @@ export function setMessages(messages) {
84
85 // reset _massRender flag
86 _massRender = false;
87 +
88 + if (mainScroller) mainScroller.reApplyScroll();
89 }
90
91 // entrypoint called from poll/WS communication, this is how all messages are rendered and updated
@@ -350,7 +358,9 @@ function drawProcessStep({
358 titleEl.textContent = title;
359
360 // auto-scroller of the step detail
353 - const detailScroller = new Scroller(stepDetailScroll); // scroller for step detail content
361 + const detailScroller = new Scroller(stepDetailScroll, {
362 + smooth: !isMassRender(),
363 + }); // scroller for step detail content
364
365 // update KVPs of the step detail
366 const kvpsTable = drawKvpsIncremental(stepDetailScroll, kvps);
@@ -366,7 +376,7 @@ function drawProcessStep({
376 stepDetailContent.textContent = content;
377
378 // reapply scroll position (autoscroll if bottom) - only when expanded already and not mass rendering
369 - if (isExpanded && !isMassRender()) detailScroller.reApplyScroll();
379 + if (isExpanded) detailScroller.reApplyScroll();
380
381 // Render action buttons: get/create container, clear, append
382 const stepActionBtns = ensureChild(
@@ -502,7 +512,7 @@ export function _drawMessage({
512 }
513
514 // reapply scroll position or autoscroll
505 - const scroller = new Scroller(bodyDiv);
515 + const scroller = new Scroller(bodyDiv, { smooth: !isMassRender() });
516
517 // Handle KVPs incrementally
518 drawKvpsIncremental(bodyDiv, kvps, false);
@@ -1587,22 +1597,26 @@ function adjustMarkdownRender(element) {
1597 }
1598
1599 export class Scroller {
1590 - constructor(element) {
1600 + constructor(element, { smooth = false } = {}) {
1601 this.element = element;
1602 + this.smooth = smooth;
1603 this.wasAtBottom = this.isAtBottom();
1604 }
1605
1606 isAtBottom(tolerance = 80) {
1596 - const scrollHeight = this.element.scrollHeight;
1597 - const clientHeight = this.element.clientHeight;
1598 - const distanceFromBottom =
1599 - scrollHeight - this.element.scrollTop - clientHeight;
1600 - return distanceFromBottom <= tolerance;
1607 + const { scrollHeight, clientHeight, scrollTop } = this.element;
1608 + return scrollHeight - scrollTop - clientHeight <= tolerance;
1609 + }
1610 +
1611 + scrollToBottom() {
1612 + const target = this.element.scrollHeight;
1613 + this.smooth
1614 + ? this.element.scrollTo({ top: target, behavior: "smooth" })
1615 + : (this.element.scrollTop = target);
1616 }
1617
1618 reApplyScroll() {
1604 - if (this.wasAtBottom && !this.isAtBottom())
1605 - this.element.scrollTop = this.element.scrollHeight;
1619 + if (this.wasAtBottom && !this.isAtBottom()) this.scrollToBottom();
1620 }
1621 }
1622