Skip to content
57 linesCodeBlameRaw
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 */
7import { ArrowDown } from "lucide-react";
8import type { ComponentProps, ReactNode } from "react";
9import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
10
11import { cn } from "../../lib/cn";
12import { Button } from "../ui/button";
13
14export type ConversationProps = ComponentProps<typeof StickToBottom>;
15
16export 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
20export 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
24export 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. */
41export 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}