Skip to content

Commit

Permalink
Improve scroll anchor (vercel#275)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyphilemon authored Mar 20, 2024
1 parent b6cab64 commit 43c7cbb
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 101 deletions.
4 changes: 1 addition & 3 deletions app/(chat)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ export default async function ChatLayout({ children }: ChatLayoutProps) {
return (
<div className="relative flex h-[calc(100vh_-_theme(spacing.16))] overflow-hidden">
<SidebarDesktop />
<div className="group w-full overflow-auto pl-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]">
{children}
</div>
{children}
</div>
)
}
20 changes: 11 additions & 9 deletions components/button-scroll-to-bottom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
import * as React from 'react'

import { cn } from '@/lib/utils'
import { useAtBottom } from '@/lib/hooks/use-at-bottom'
import { Button, type ButtonProps } from '@/components/ui/button'
import { IconArrowDown } from '@/components/ui/icons'

export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
const isAtBottom = useAtBottom()
interface ButtonScrollToBottomProps extends ButtonProps {
isAtBottom: boolean
scrollToBottom: () => void
}

export function ButtonScrollToBottom({
className,
isAtBottom,
scrollToBottom,
...props
}: ButtonScrollToBottomProps) {
return (
<Button
variant="outline"
Expand All @@ -19,12 +26,7 @@ export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
isAtBottom ? 'opacity-0' : 'opacity-100',
className
)}
onClick={() =>
window.scrollTo({
top: document.body.offsetHeight,
behavior: 'smooth'
})
}
onClick={() => scrollToBottom()}
{...props}
>
<IconArrowDown />
Expand Down
35 changes: 22 additions & 13 deletions components/chat-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Separator } from '@/components/ui/separator'
import { UIState } from '@/lib/chat/actions'
import { Session } from '@/lib/types'
import Link from 'next/link'
import { ExclamationTriangleIcon } from '@radix-ui/react-icons'

export interface ChatList {
messages: UIState
Expand All @@ -17,19 +18,27 @@ export function ChatList({ messages, session, isShared }: ChatList) {
return (
<div className="relative mx-auto max-w-2xl px-4">
{!isShared && !session ? (
<div className="mb-8 rounded-lg border bg-white p-4 dark:bg-zinc-950">
<p className="text-muted-foreground leading-normal">
Please{' '}
<Link href="/login" className="underline">
log in
</Link>{' '}
or{' '}
<Link href="/signup" className="underline">
sign up
</Link>{' '}
to save and revisit your chat history!
</p>
</div>
<>
<div className="group relative mb-4 flex items-start md:-ml-12">
<div className="bg-background flex size-[25px] shrink-0 select-none items-center justify-center rounded-md border shadow-sm">
<ExclamationTriangleIcon />
</div>
<div className="ml-4 flex-1 space-y-2 overflow-hidden px-1">
<p className="text-muted-foreground leading-normal">
Please{' '}
<Link href="/login" className="underline">
log in
</Link>{' '}
or{' '}
<Link href="/signup" className="underline">
sign up
</Link>{' '}
to save and revisit your chat history!
</p>
</div>
</div>
<Separator className="my-4" />
</>
) : null}

{messages.map((message, index) => (
Expand Down
28 changes: 20 additions & 8 deletions components/chat-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ export interface ChatPanelProps {
title?: string
input: string
setInput: (value: string) => void
isAtBottom: boolean
scrollToBottom: () => void
}

export function ChatPanel({ id, title, input, setInput }: ChatPanelProps) {
export function ChatPanel({
id,
title,
input,
setInput,
isAtBottom,
scrollToBottom
}: ChatPanelProps) {
const [aiState] = useAIState()
const [messages, setMessages] = useUIState<typeof AI>()
const { submitUserMessage } = useActions()
Expand All @@ -33,24 +42,27 @@ export function ChatPanel({ id, title, input, setInput }: ChatPanelProps) {
},
{
heading: 'What is the price of',
subheading: 'DOGE in the stock market?',
message: 'What is the price of DOGE in the stock market?'
subheading: '$DOGE right now?',
message: 'What is the price of $DOGE right now?'
},
{
heading: 'I would like to buy',
subheading: '42 DOGE coins',
message: `I would like to buy 42 DOGE coins`
subheading: '42 $DOGE',
message: `I would like to buy 42 $DOGE`
},
{
heading: 'What are some',
subheading: `recent events about DOGE?`,
message: `What are some recent events about DOGE?`
subheading: `recent events about $DOGE?`,
message: `What are some recent events about $DOGE?`
}
]

return (
<div className="fixed inset-x-0 bottom-0 w-full bg-gradient-to-b from-muted/30 from-0% to-muted/30 to-50% duration-300 ease-in-out animate-in dark:from-background/10 dark:from-10% dark:to-background/80 peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]">
<ButtonScrollToBottom />
<ButtonScrollToBottom
isAtBottom={isAtBottom}
scrollToBottom={scrollToBottom}
/>

<div className="mx-auto sm:max-w-2xl sm:px-4">
<div className="mb-4 grid grid-cols-2 gap-2 px-4 sm:px-0">
Expand Down
28 changes: 0 additions & 28 deletions components/chat-scroll-anchor.tsx

This file was deleted.

34 changes: 23 additions & 11 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { cn } from '@/lib/utils'
import { ChatList } from '@/components/chat-list'
import { ChatPanel } from '@/components/chat-panel'
import { EmptyScreen } from '@/components/empty-screen'
import { ChatScrollAnchor } from '@/components/chat-scroll-anchor'
import { useLocalStorage } from '@/lib/hooks/use-local-storage'
import { useEffect, useState } from 'react'
import { useUIState, useAIState } from 'ai/rsc'
import { Session } from '@/lib/types'
import { usePathname, useRouter } from 'next/navigation'
import { Message } from '@/lib/chat/actions'
import { useScrollAnchor } from '@/lib/hooks/use-scroll-anchor'
import { toast } from 'sonner'

export interface ChatProps extends React.ComponentProps<'div'> {
Expand All @@ -26,7 +26,6 @@ export function Chat({ id, className, session, missingKeys }: ChatProps) {
const [input, setInput] = useState('')
const [messages] = useUIState()
const [aiState] = useAIState()
const isLoading = true

const [_, setNewChatId] = useLocalStorage('newChatId', id)

Expand Down Expand Up @@ -55,19 +54,32 @@ export function Chat({ id, className, session, missingKeys }: ChatProps) {
})
}, [missingKeys])

const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
useScrollAnchor()

return (
<>
<div className={cn('pb-[200px] pt-4 md:pt-10', className)}>
<div
className="group w-full overflow-auto pl-0 peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]"
ref={scrollRef}
>
<div
className={cn('pb-[200px] pt-4 md:pt-10', className)}
ref={messagesRef}
>
{messages.length ? (
<>
<ChatList messages={messages} isShared={false} session={session} />
<ChatScrollAnchor trackVisibility={isLoading} />
</>
<ChatList messages={messages} isShared={false} session={session} />
) : (
<EmptyScreen setInput={setInput} />
<EmptyScreen />
)}
<div className="h-px w-full" ref={visibilityRef} />
</div>
<ChatPanel id={id} input={input} setInput={setInput} />
</>
<ChatPanel
id={id}
input={input}
setInput={setInput}
isAtBottom={isAtBottom}
scrollToBottom={scrollToBottom}
/>
</div>
)
}
2 changes: 1 addition & 1 deletion components/empty-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const exampleMessages = [
}
]

export function EmptyScreen({ setInput }: Pick<UseChatHelpers, 'setInput'>) {
export function EmptyScreen() {
return (
<div className="mx-auto max-w-2xl px-4">
<div className="flex flex-col gap-2 rounded-lg border bg-background p-8">
Expand Down
4 changes: 2 additions & 2 deletions components/sidebar-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ export function SidebarItem({ index, chat, children }: SidebarItemProps) {
tabIndex={-1}
className="focus:bg-muted focus:ring-1 focus:ring-ring"
>
<IconUsers className="mr-2" />
<IconUsers className="mr-2 mt-1 text-zinc-500" />
</TooltipTrigger>
<TooltipContent>This is a shared chat.</TooltipContent>
</Tooltip>
) : (
<IconMessage className="mr-2" />
<IconMessage className="mr-2 mt-1 text-zinc-500" />
)}
</div>
<Link
Expand Down
4 changes: 2 additions & 2 deletions components/stocks/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export function UserMessage({ children }: { children: React.ReactNode }) {
<div className="flex size-[25px] shrink-0 select-none items-center justify-center rounded-md border bg-background shadow-sm">
<IconUser />
</div>
<div className="ml-4 flex-1 space-y-2 overflow-hidden px-1">
<div className="ml-4 flex-1 space-y-2 overflow-hidden pl-2">
{children}
</div>
</div>
Expand Down Expand Up @@ -103,7 +103,7 @@ export function BotCard({
>
<IconOpenAI />
</div>
<div className="ml-4 flex-1 px-1">{children}</div>
<div className="ml-4 flex-1 pl-2">{children}</div>
</div>
)
}
Expand Down
2 changes: 1 addition & 1 deletion components/stocks/stock-purchase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function Purchase({
</div>

<button
className="mt-6 w-full rounded-lg bg-green-500 px-4 py-2 font-bold text-zinc-900 hover:bg-green-600"
className="mt-6 w-full rounded-lg bg-green-400 px-4 py-2 font-bold text-zinc-900 hover:bg-green-500"
onClick={async () => {
const response = await confirmPurchase(symbol, price, value)
setPurchasingUI(response.purchasingUI)
Expand Down
23 changes: 0 additions & 23 deletions lib/hooks/use-at-bottom.tsx

This file was deleted.

Loading

0 comments on commit 43c7cbb

Please sign in to comment.