feat: add scroll-to-bottom button to chat + prevent forced scroll down (#23270)
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
ChatMessages,
|
ChatMessages,
|
||||||
ChatScreenDragOverlay,
|
ChatScreenDragOverlay,
|
||||||
ChatScreenProcessingInfo,
|
ChatScreenProcessingInfo,
|
||||||
|
ChatScreenActionScrollDown,
|
||||||
DialogEmptyFileAlert,
|
DialogEmptyFileAlert,
|
||||||
DialogFileUploadError,
|
DialogFileUploadError,
|
||||||
DialogChatError,
|
DialogChatError,
|
||||||
@@ -338,7 +339,9 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleMessagesReady() {
|
function handleMessagesReady() {
|
||||||
if (!disableAutoScroll && !autoScroll.userScrolledUp) {
|
if (disableAutoScroll) return;
|
||||||
|
|
||||||
|
if (!autoScroll.userScrolledUp) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
autoScroll.scrollToBottom('instant');
|
autoScroll.scrollToBottom('instant');
|
||||||
});
|
});
|
||||||
@@ -405,7 +408,7 @@
|
|||||||
<div
|
<div
|
||||||
class="pointer-events-none {isEmpty
|
class="pointer-events-none {isEmpty
|
||||||
? 'absolute bottom-[calc(50dvh-7rem)]'
|
? '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}
|
{#if isEmpty}
|
||||||
<div class="mb-8 px-4 text-center" use:fadeInView={{ duration: 300 }}>
|
<div class="mb-8 px-4 text-center" use:fadeInView={{ duration: 300 }}>
|
||||||
@@ -419,6 +422,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<ChatScreenActionScrollDown container={chatScrollContainer} />
|
||||||
|
|
||||||
{#if page.params.id}
|
{#if page.params.id}
|
||||||
<ChatScreenProcessingInfo />
|
<ChatScreenProcessingInfo />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -667,3 +667,10 @@ export { default as ChatScreenForm } from './ChatScreen/ChatScreenForm.svelte';
|
|||||||
* Only visible when `isCurrentConversationLoading` is true.
|
* Only visible when `isCurrentConversationLoading` is true.
|
||||||
*/
|
*/
|
||||||
export { default as ChatScreenProcessingInfo } from './ChatScreen/ChatScreenProcessingInfo.svelte';
|
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';
|
||||||
|
|||||||
@@ -100,6 +100,14 @@ export class AutoScrollController {
|
|||||||
this._autoScrollEnabled = true;
|
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.
|
* Starts the auto-scroll interval for continuous scrolling during streaming.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user