feat: implement auto scroll

This commit is contained in:
shadcn 2023-06-15 15:27:41 +04:00
parent a9993640ff
commit b8d3ba85f5
6 changed files with 71 additions and 17 deletions

View file

@ -3,26 +3,12 @@
import * as React from 'react' import * as React from 'react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useAtBottom } from '@/lib/hooks/use-at-bottom'
import { Button, type ButtonProps } from '@/components/ui/button' import { Button, type ButtonProps } from '@/components/ui/button'
import { IconArrowDown } from '@/components/ui/icons' import { IconArrowDown } from '@/components/ui/icons'
export function ButtonScrollToBottom({ className, ...props }: ButtonProps) { export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
const [isAtBottom, setIsAtBottom] = React.useState(false) const isAtBottom = useAtBottom()
React.useEffect(() => {
const handleScroll = () => {
setIsAtBottom(
window.innerHeight + window.scrollY > document.body.offsetHeight - 100
)
}
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll()
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])
return ( return (
<Button <Button

View file

@ -0,0 +1,29 @@
'use client'
import * as React from 'react'
import { useInView } from 'react-intersection-observer'
import { useAtBottom } from '@/lib/hooks/use-at-bottom'
interface ChatScrollAnchorProps {
trackVisibility?: boolean
}
export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
const isAtBottom = useAtBottom()
const { ref, entry, inView } = useInView({
trackVisibility,
delay: 100,
rootMargin: '0px 0px -150px 0px'
})
React.useEffect(() => {
if (isAtBottom && trackVisibility && !inView) {
entry?.target.scrollIntoView({
block: 'start'
})
}
}, [inView, entry, isAtBottom, trackVisibility])
return <div ref={ref} className="h-px w-full" />
}

View file

@ -6,6 +6,7 @@ import { cn } from '@/lib/utils'
import { ChatList } from '@/components/chat-list' import { ChatList } from '@/components/chat-list'
import { ChatPanel } from '@/components/chat-panel' import { ChatPanel } from '@/components/chat-panel'
import { EmptyScreen } from '@/components/empty-screen' import { EmptyScreen } from '@/components/empty-screen'
import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
export interface ChatProps extends React.ComponentProps<'div'> { export interface ChatProps extends React.ComponentProps<'div'> {
initialMessages?: Message[] initialMessages?: Message[]
@ -26,7 +27,10 @@ export function Chat({ id, initialMessages, className }: ChatProps) {
<> <>
<div className={cn('pb-[200px] pt-4 md:pt-10', className)}> <div className={cn('pb-[200px] pt-4 md:pt-10', className)}>
{messages.length ? ( {messages.length ? (
<ChatList messages={messages} /> <>
<ChatList messages={messages} />
<ChatScrollAnchor trackVisibility={isLoading} />
</>
) : ( ) : (
<EmptyScreen setInput={setInput} /> <EmptyScreen setInput={setInput} />
)} )}

View file

@ -0,0 +1,23 @@
import * as React from 'react'
export function useAtBottom(offset = 0) {
const [isAtBottom, setIsAtBottom] = React.useState(false)
React.useEffect(() => {
const handleScroll = () => {
setIsAtBottom(
window.innerHeight + window.scrollY ===
document.body.offsetHeight - offset
)
}
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll()
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [offset])
return isAtBottom
}

View file

@ -34,6 +34,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-intersection-observer": "^9.4.4",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.4.1", "react-textarea-autosize": "^8.4.1",

11
pnpm-lock.yaml generated
View file

@ -68,6 +68,9 @@ dependencies:
react-hot-toast: react-hot-toast:
specifier: ^2.4.1 specifier: ^2.4.1
version: 2.4.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0) version: 2.4.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0)
react-intersection-observer:
specifier: ^9.4.4
version: 9.4.4(react@18.2.0)
react-markdown: react-markdown:
specifier: ^8.0.7 specifier: ^8.0.7
version: 8.0.7(@types/react@18.2.6)(react@18.2.0) version: 8.0.7(@types/react@18.2.6)(react@18.2.0)
@ -4412,6 +4415,14 @@ packages:
- csstype - csstype
dev: false dev: false
/react-intersection-observer@9.4.4(react@18.2.0):
resolution: {integrity: sha512-KQr8ezJscRSQZR0/TjogG4mZttCzdPZj8fhDzAK+xrPYOVvFjabRNy+4EY3UhS3fJaJyIedGjA8y4yKX+nc08w==}
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/react-is@16.13.1: /react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}