char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aleksander Grygierui: Stores split refactor (#27240)521a64cd0

char/aliceai-t5-moe
5.4 KiB226 linesraw
1import { AUTO_SCROLL_AT_BOTTOM_THRESHOLD, AUTO_SCROLL_INTERVAL } from '$lib/constants';
2
3export interface AutoScrollOptions {
4	disabled?: boolean;
5}
6
7/**
8 * Creates an auto-scroll controller for a scrollable container.
9 *
10 * Features:
11 * - Auto-scrolls to bottom during streaming/loading
12 * - Stops auto-scroll when user manually scrolls up
13 * - Resumes auto-scroll when user scrolls back to bottom
14 */
15export class AutoScrollController {
16	private _autoScrollEnabled = $state(true);
17	private _container: HTMLElement | undefined;
18	private _disabled: boolean;
19	private _lastScrollTop = $state(0);
20	private _mutationObserver: MutationObserver | null = null;
21	private _observerEnabled = false;
22	private _rafPending = false;
23	private _scrollInterval: ReturnType<typeof setInterval> | undefined;
24	private _userScrolledUp = $state(false);
25	get autoScrollEnabled(): boolean {
26		return this._autoScrollEnabled;
27	}
28
29	get userScrolledUp(): boolean {
30		return this._userScrolledUp;
31	}
32
33	constructor(options: AutoScrollOptions = {}) {
34		this._disabled = options.disabled ?? false;
35	}
36
37	/**
38	 * Cleans up resources. Call this in onDestroy or when the component unmounts.
39	 */
40	destroy(): void {
41		this.stopInterval();
42		this._doStopObserving();
43	}
44
45	/**
46	 * Enables auto-scroll (e.g., when user sends a message).
47	 */
48	enable(): void {
49		if (this._disabled) return;
50
51		this._userScrolledUp = false;
52		this._autoScrollEnabled = true;
53	}
54
55	/**
56	 * Handles scroll events to detect user scroll direction and toggle auto-scroll.
57	 */
58	handleScroll(): void {
59		if (this._disabled || !this._container) return;
60
61		const { clientHeight, scrollHeight, scrollTop } = this._container;
62		const distanceFromBottom = scrollHeight - clientHeight - scrollTop;
63		const isScrollingUp = scrollTop < this._lastScrollTop;
64		const isAtBottom = distanceFromBottom < AUTO_SCROLL_AT_BOTTOM_THRESHOLD;
65
66		if (isScrollingUp && !isAtBottom) {
67			this._userScrolledUp = true;
68			this._autoScrollEnabled = false;
69		} else if (isAtBottom && this._userScrolledUp) {
70			this._userScrolledUp = false;
71			this._autoScrollEnabled = true;
72		}
73
74		this._lastScrollTop = scrollTop;
75	}
76
77	/**
78	 * Resets scroll state when switching conversations.
79	 */
80	resetScrollState(): void {
81		this._userScrolledUp = false;
82		this._autoScrollEnabled = !this._disabled;
83
84		if (this._container) {
85			this._lastScrollTop = this._container.scrollTop;
86		}
87	}
88
89	/**
90	 * Scrolls the container to the bottom instantly.
91	 */
92	scrollToBottom(): void {
93		if (this._disabled || !this._container) return;
94
95		this._container.scrollTop = this._container.scrollHeight;
96	}
97
98	/**
99	 * Binds the controller to a scrollable container element.
100	 */
101	setContainer(container: HTMLElement | undefined): void {
102		this._doStopObserving();
103		this._container = container;
104
105		if (this._observerEnabled && container && !this._disabled) {
106			this._doStartObserving();
107		}
108	}
109
110	/**
111	 * Updates the disabled state.
112	 */
113	setDisabled(disabled: boolean): void {
114		if (this._disabled === disabled) return;
115
116		this._disabled = disabled;
117
118		if (disabled) {
119			this._autoScrollEnabled = false;
120			this.stopInterval();
121			this._doStopObserving();
122		} else if (this._observerEnabled && this._container && !this._mutationObserver) {
123			this._doStartObserving();
124		}
125	}
126
127	/**
128	 * Starts the auto-scroll interval for continuous scrolling during streaming.
129	 */
130	startInterval(): void {
131		if (this._disabled || this._scrollInterval) return;
132
133		this._scrollInterval = setInterval(() => {
134			this.scrollToBottom();
135		}, AUTO_SCROLL_INTERVAL);
136	}
137
138	/**
139	 * Starts a MutationObserver on the container that auto-scrolls to bottom
140	 * on content changes. More responsive than interval-based polling.
141	 */
142	startObserving(): void {
143		this._observerEnabled = true;
144
145		if (this._container && !this._disabled && !this._mutationObserver) {
146			this._doStartObserving();
147		}
148	}
149
150	/**
151	 * Stops the auto-scroll interval.
152	 */
153	stopInterval(): void {
154		if (this._scrollInterval) {
155			clearInterval(this._scrollInterval);
156			this._scrollInterval = undefined;
157		}
158	}
159
160	/**
161	 * Stops the MutationObserver.
162	 */
163	stopObserving(): void {
164		this._observerEnabled = false;
165		this._doStopObserving();
166	}
167
168	/**
169	 * Updates the auto-scroll interval based on streaming state.
170	 * Call this in a $effect to automatically manage the interval.
171	 */
172	updateInterval(isStreaming: boolean): void {
173		if (this._disabled) {
174			this.stopInterval();
175
176			return;
177		}
178
179		if (isStreaming && this._autoScrollEnabled) {
180			if (!this._scrollInterval) {
181				this.startInterval();
182			}
183		} else {
184			this.stopInterval();
185		}
186	}
187
188	private _doStartObserving(): void {
189		if (!this._container || this._mutationObserver) return;
190
191		this._mutationObserver = new MutationObserver(() => {
192			if (!this._autoScrollEnabled || this._rafPending) return;
193
194			this._rafPending = true;
195			requestAnimationFrame(() => {
196				this._rafPending = false;
197
198				if (this._autoScrollEnabled && this._container) {
199					this._container.scrollTop = this._container.scrollHeight;
200				}
201			});
202		});
203
204		this._mutationObserver.observe(this._container, {
205			characterData: true,
206			childList: true,
207			subtree: true
208		});
209	}
210
211	private _doStopObserving(): void {
212		if (this._mutationObserver) {
213			this._mutationObserver.disconnect();
214			this._mutationObserver = null;
215		}
216
217		this._rafPending = false;
218	}
219}
220
221/**
222 * Creates a new AutoScrollController instance.
223 */
224export function createAutoScrollController(options: AutoScrollOptions = {}): AutoScrollController {
225	return new AutoScrollController(options);
226}