Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ChatMessages,
ChatScreenDragOverlay,
ChatScreenProcessingInfo,
ChatScreenActionScrollDown,
DialogEmptyFileAlert,
DialogFileUploadError,
DialogChatError,
Expand Down Expand Up @@ -338,7 +339,9 @@
});

function handleMessagesReady() {
if (!disableAutoScroll && !autoScroll.userScrolledUp) {
if (disableAutoScroll) return;

if (!autoScroll.userScrolledUp) {
requestAnimationFrame(() => {
autoScroll.scrollToBottom('instant');
});
Expand Down Expand Up @@ -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 }}>
Expand All @@ -419,6 +422,8 @@
</div>
{/if}

<ChatScreenActionScrollDown container={chatScrollContainer} />

{#if page.params.id}
<ChatScreenProcessingInfo />
{/if}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
7 changes: 7 additions & 0 deletions tools/ui/src/lib/components/app/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
8 changes: 8 additions & 0 deletions tools/ui/src/lib/hooks/use-auto-scroll.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down