Skip to content

Commit 58844d4

Browse files
fix: issue with scroll sticking in user ui (#1860)
Signed-off-by: Ryan Hopper-Lowe <[email protected]>
1 parent 2847a3f commit 58844d4

File tree

4 files changed

+88
-50
lines changed

4 files changed

+88
-50
lines changed

ui/user/src/lib/actions/div.svelte.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type { Action } from 'svelte/action';
2+
3+
export type StickToBottomControls = {
4+
stickToBottom: () => void;
5+
};
6+
7+
type StickToBottomOptions = {
8+
contentEl?: HTMLElement;
9+
setControls?: (controls: StickToBottomControls) => void;
10+
};
11+
12+
export const sticktobottom: Action<HTMLElement, StickToBottomOptions | undefined> = (
13+
node,
14+
options = {}
15+
) => {
16+
let shouldStick = true;
17+
let resizeObserver: ResizeObserver | null = null;
18+
19+
function scrollToBottom() {
20+
node.scrollTop = node.scrollHeight - node.clientHeight;
21+
}
22+
23+
function isAtBottom() {
24+
return node.scrollHeight - node.scrollTop - node.clientHeight <= 40;
25+
}
26+
27+
$effect(() => {
28+
if (!options.contentEl) return;
29+
30+
resizeObserver = new ResizeObserver(() => {
31+
if (shouldStick) {
32+
scrollToBottom();
33+
}
34+
});
35+
36+
resizeObserver.observe(options.contentEl, { box: 'device-pixel-content-box' });
37+
38+
return () => {
39+
if (resizeObserver) {
40+
resizeObserver.disconnect();
41+
resizeObserver = null;
42+
}
43+
};
44+
});
45+
46+
// Handle wheel events to determine user scrolling intention
47+
$effect(() => {
48+
const handleWheel = (e: WheelEvent) => {
49+
// If user scrolls up, disable auto-scrolling
50+
if (e.deltaY < 0) {
51+
shouldStick = false;
52+
} else {
53+
// If user scrolls down to the bottom, re-enable auto-scrolling
54+
shouldStick = isAtBottom();
55+
}
56+
};
57+
58+
node.addEventListener('wheel', handleWheel, { passive: true });
59+
60+
// Clean up event listener when the effect is destroyed
61+
return () => node.removeEventListener('wheel', handleWheel);
62+
});
63+
64+
$effect(() => {
65+
options.setControls?.({ stickToBottom: () => (shouldStick = true) });
66+
});
67+
68+
// Return the action API
69+
return {
70+
update(newOptions) {
71+
options = { ...options, ...newOptions };
72+
}
73+
};
74+
};

ui/user/src/lib/actions/div.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

ui/user/src/lib/components/Thread.svelte

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<script lang="ts">
2-
import { sticktobottom } from '$lib/actions/div';
2+
import { sticktobottom, type StickToBottomControls } from '$lib/actions/div.svelte';
33
import Input from '$lib/components/messages/Input.svelte';
44
import Message from '$lib/components/messages/Message.svelte';
55
import { toHTMLFromMarkdown } from '$lib/markdown';
6-
import { type Assistant, EditorService, type Messages } from '$lib/services';
6+
import { EditorService, type Assistant, type Messages } from '$lib/services';
77
import { Thread } from '$lib/services/chat/thread.svelte';
88
import { assistants, context } from '$lib/stores';
99
import { onDestroy } from 'svelte';
@@ -78,6 +78,8 @@
7878
thread = newThread;
7979
});
8080
81+
let scrollControls = $state<StickToBottomControls>();
82+
8183
onDestroy(() => {
8284
thread?.close?.();
8385
});
@@ -96,12 +98,15 @@
9698
bind:this={container}
9799
class="flex h-full grow justify-center overflow-y-auto scrollbar-none"
98100
class:scroll-smooth={scrollSmooth}
99-
use:sticktobottom
101+
use:sticktobottom={{
102+
contentEl: messagesDiv,
103+
setControls: (controls) => (scrollControls = controls)
104+
}}
100105
>
101106
<div
102107
in:fade|global
103108
bind:this={messagesDiv}
104-
class="flex w-full max-w-[1000px] flex-col justify-start gap-8 p-5 transition-all"
109+
class="flex h-fit w-full max-w-[1000px] flex-col justify-start gap-8 p-5 transition-all"
105110
class:justify-center={!thread}
106111
>
107112
<div class="message-content self-center">
@@ -145,9 +150,9 @@
145150
onAbort={async () => {
146151
await thread?.abort();
147152
}}
148-
onSubmit={async (i) => {
149-
container?.scrollTo({ top: container?.scrollHeight - container?.clientHeight });
150-
await thread?.invoke(i);
153+
onSubmit={(i) => {
154+
scrollControls?.stickToBottom();
155+
thread?.invoke(i);
151156
}}
152157
/>
153158
{/if}

ui/user/src/lib/components/messages/Message.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@
399399
class:justify-end={msg.sent}
400400
>
401401
{#if !msg.sent}
402-
<div class="sticky top-10 z-10 mr-3"><MessageIcon {msg} /></div>
402+
<div class="mr-3"><MessageIcon {msg} /></div>
403403
{/if}
404404

405405
<div class="flex w-full flex-col" class:w-full={fullWidth}>
@@ -482,7 +482,7 @@
482482
}
483483
484484
.loading-container span[data-end-indicator] {
485-
@apply visible ml-1 inline-block size-4 animate-pulse rounded-full bg-gray-700 align-middle text-transparent dark:bg-gray-300;
485+
@apply visible relative -mt-[2px] ml-1 inline-block size-4 animate-pulse rounded-full bg-gray-400 align-middle text-transparent;
486486
}
487487
}
488488
</style>

0 commit comments

Comments
 (0)