Add chat scroll-to-bottom button
This commit is contained in:
parent
52ec17eee5
commit
cf5563c3ad
2 changed files with 49 additions and 7 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { IconPlus } from "@tabler/icons-react"
|
import { IconArrowDown, IconPlus } from "@tabler/icons-react"
|
||||||
import { useAtom } from "jotai"
|
import { useAtom } from "jotai"
|
||||||
import { type ChangeEvent, useRef, useState } from "react"
|
import { type ChangeEvent, useRef, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
@ -178,8 +178,14 @@ export function ChatPage() {
|
||||||
const hasStreamingMessage =
|
const hasStreamingMessage =
|
||||||
streamingEnabled &&
|
streamingEnabled &&
|
||||||
messages.some((message) => message.role === "assistant" && message.streaming)
|
messages.some((message) => message.role === "assistant" && message.streaming)
|
||||||
const { scrollRef, hasScrolled, handleScroll, handleManualScrollIntent } =
|
const {
|
||||||
useChatAutoScroll({
|
scrollRef,
|
||||||
|
isAtBottom,
|
||||||
|
hasScrolled,
|
||||||
|
handleScroll,
|
||||||
|
handleManualScrollIntent,
|
||||||
|
scrollToBottom,
|
||||||
|
} = useChatAutoScroll({
|
||||||
deps: [messages, isTyping],
|
deps: [messages, isTyping],
|
||||||
streaming: hasStreamingMessage,
|
streaming: hasStreamingMessage,
|
||||||
})
|
})
|
||||||
|
|
@ -259,7 +265,7 @@ export function ChatPage() {
|
||||||
canInput && (Boolean(input.trim()) || attachments.length > 0)
|
canInput && (Boolean(input.trim()) || attachments.length > 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background/95 flex h-full flex-col">
|
<div className="bg-background/95 relative flex h-full flex-col">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t("navigation.chat")}
|
title={t("navigation.chat")}
|
||||||
className={`transition-shadow ${
|
className={`transition-shadow ${
|
||||||
|
|
@ -374,6 +380,18 @@ export function ChatPage() {
|
||||||
onChange={handleImageSelection}
|
onChange={handleImageSelection}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!isAtBottom && messages.length > 0 && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-6 bottom-28 z-20 rounded-full shadow-lg md:right-8"
|
||||||
|
aria-label="Scroll to bottom"
|
||||||
|
onClick={() => scrollToBottom()}
|
||||||
|
>
|
||||||
|
<IconArrowDown className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<ChatComposer
|
<ChatComposer
|
||||||
input={input}
|
input={input}
|
||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,29 @@ export function useChatAutoScroll({
|
||||||
cancelAnimation()
|
cancelAnimation()
|
||||||
}, [cancelAnimation])
|
}, [cancelAnimation])
|
||||||
|
|
||||||
|
const scrollToBottom = useCallback(
|
||||||
|
(options?: { immediate?: boolean }) => {
|
||||||
|
const element = scrollRef.current
|
||||||
|
if (!element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stickyRef.current = true
|
||||||
|
setIsAtBottom(true)
|
||||||
|
targetScrollTopRef.current = getBottomScrollTop(element)
|
||||||
|
|
||||||
|
if (options?.immediate) {
|
||||||
|
cancelAnimation()
|
||||||
|
element.scrollTop = targetScrollTopRef.current
|
||||||
|
syncStateFromElement(element, { programmatic: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
animateToBottom()
|
||||||
|
},
|
||||||
|
[animateToBottom, cancelAnimation, syncStateFromElement],
|
||||||
|
)
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const element = scrollRef.current
|
const element = scrollRef.current
|
||||||
if (!element) {
|
if (!element) {
|
||||||
|
|
@ -164,5 +187,6 @@ export function useChatAutoScroll({
|
||||||
hasScrolled,
|
hasScrolled,
|
||||||
handleScroll,
|
handleScroll,
|
||||||
handleManualScrollIntent,
|
handleManualScrollIntent,
|
||||||
|
scrollToBottom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue