feat: add scroll-to-bottom button to chat + prevent forced scroll down (#23270)

This commit is contained in:
Aleksander Grygier 2026-05-18 16:17:21 +02:00 committed by GitHub
parent 1ff0fc1384
commit b9a2170fce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 70 additions and 2 deletions

View file

@ -8,6 +8,7 @@
ChatMessages,
ChatScreenDragOverlay,
ChatScreenProcessingInfo,
ChatScreenActionScrollDown,
DialogEmptyFileAlert,
DialogFileUploadError,
DialogChatError,
@ -338,7 +339,9 @@
});
function handleMessagesReady() {
if (!disableAutoScroll && !autoScroll.userScrolledUp) {
if (disableAutoScroll) return;
if (!autoScroll.userScrolledUp) {
requestAnimationFrame(() => {
autoScroll.scrollToBottom('instant');
});
@ -405,7 +408,7 @@
<div
class="pointer-events-none {isEmpty
? 'absolute bottom-[calc(50dvh-7rem)]'
: 'sticky bottom-4'} right-4 left-4 mt-auto pt-16 transition-all duration-200"
: 'sticky bottom-4'} right-4 left-4 mt-auto -mb-14 pt-16 transition-all duration-200"
>
{#if isEmpty}
<div class="mb-8 px-4 text-center" use:fadeInView={{ duration: 300 }}>
@ -419,6 +422,8 @@
</div>
{/if}
<ChatScreenActionScrollDown container={chatScrollContainer} />
{#if page.params.id}
<ChatScreenProcessingInfo />
{/if}

View file

@ -0,0 +1,48 @@
<script lang="ts">
import { ArrowDown } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
let { container }: { container: HTMLDivElement | undefined } = $props();
let show = $state(false);
function checkVisibility() {
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
const distanceFromBottom = scrollHeight - clientHeight - scrollTop;
show = distanceFromBottom > clientHeight * 0.5;
}
function scrollToBottom() {
if (container) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
});
}
}
$effect(() => {
const c = container;
if (c) {
c.addEventListener('scroll', checkVisibility);
checkVisibility();
return () => {
c.removeEventListener('scroll', checkVisibility);
};
}
});
</script>
<div class="pointer-events-auto relative z-50 mx-auto mb-4 flex max-w-[48rem] justify-center">
<Button
onclick={scrollToBottom}
variant="secondary"
size="icon"
class="h-10 w-10 rounded-full bg-background/80 shadow-lg backdrop-blur-sm transition-all duration-200 hover:bg-muted/80"
aria-label="Scroll to bottom"
style="transform: translateY({show ? '0' : '20px'}); opacity: {show ? 1 : 0};"
>
<ArrowDown class="h-4 w-4" />
</Button>
</div>

View file

@ -667,3 +667,10 @@ export { default as ChatScreenForm } from './ChatScreen/ChatScreenForm.svelte';
* Only visible when `isCurrentConversationLoading` is true.
*/
export { default as ChatScreenProcessingInfo } from './ChatScreen/ChatScreenProcessingInfo.svelte';
/**
* Scroll-to-bottom action button. Displays a floating button when the user
* has scrolled up more than half a viewport height from the bottom.
* Takes the chat container element as a prop to manage scroll state internally.
*/
export { default as ChatScreenActionScrollDown } from './ChatScreen/ChatScreenActionScrollDown.svelte';

View file

@ -100,6 +100,14 @@ export class AutoScrollController {
this._autoScrollEnabled = true;
}
/**
* Resets scroll state when switching conversations.
*/
resetScrollState(): void {
this._userScrolledUp = false;
this._autoScrollEnabled = true;
}
/**
* Starts the auto-scroll interval for continuous scrolling during streaming.
*/