| 1 | /** |
| 2 | * AI Elements' Conversation (registry.ai-sdk.dev/conversation.json), in |
| 3 | * g1t's tokens: a scrolling log that follows new words to the bottom while |
| 4 | * the reader is there, and stops following the moment they scroll up to |
| 5 | * read (use-stick-to-bottom). A button takes them back down. |
| 6 | */ |
| 7 | import { ArrowDown } from "lucide-react"; |
| 8 | import type { ComponentProps, ReactNode } from "react"; |
| 9 | import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom"; |
| 10 | |
| 11 | import { cn } from "../../lib/cn"; |
| 12 | import { Button } from "../ui/button"; |
| 13 | |
| 14 | export type ConversationProps = ComponentProps<typeof StickToBottom>; |
| 15 | |
| 16 | export function Conversation({ className, ...props }: ConversationProps) { |
| 17 | return <StickToBottom className={cn("relative min-h-0 flex-1 overflow-y-hidden", className)} initial="smooth" resize="smooth" role="log" {...props} />; |
| 18 | } |
| 19 | |
| 20 | export function ConversationContent({ className, ...props }: ComponentProps<typeof StickToBottom.Content>) { |
| 21 | return <StickToBottom.Content className={cn("flex flex-col gap-6 px-5 py-4", className)} {...props} />; |
| 22 | } |
| 23 | |
| 24 | export function ConversationEmptyState({ className, title, description, icon, children, ...props }: ComponentProps<"div"> & { title?: string; description?: ReactNode; icon?: ReactNode }) { |
| 25 | return ( |
| 26 | <div className={cn("flex flex-col items-center justify-center gap-3 px-6 py-10 text-center", className)} {...props}> |
| 27 | {children ?? ( |
| 28 | <> |
| 29 | {icon && <div className="text-accent">{icon}</div>} |
| 30 | <div className="space-y-1"> |
| 31 | {title && <h3 className="text-sm font-medium text-fg">{title}</h3>} |
| 32 | {description && <p className="text-sm text-muted">{description}</p>} |
| 33 | </div> |
| 34 | </> |
| 35 | )} |
| 36 | </div> |
| 37 | ); |
| 38 | } |
| 39 | |
| 40 | /** Back to the newest words, shown only while the reader is scrolled up. */ |
| 41 | export function ConversationScrollButton({ className, ...props }: ComponentProps<typeof Button>) { |
| 42 | const { isAtBottom, scrollToBottom } = useStickToBottomContext(); |
| 43 | if (isAtBottom) return null; |
| 44 | return ( |
| 45 | <Button |
| 46 | type="button" |
| 47 | variant="outline" |
| 48 | size="icon-sm" |
| 49 | aria-label="Scroll to the newest" |
| 50 | onClick={() => void scrollToBottom()} |
| 51 | className={cn("absolute bottom-3 left-1/2 -translate-x-1/2 rounded-full bg-surface shadow-lg shadow-black/30", className)} |
| 52 | {...props} |
| 53 | > |
| 54 | <ArrowDown size={15} /> |
| 55 | </Button> |
| 56 | ); |
| 57 | } |