"use client"; import { ChevronDown } from "lucide-react"; import { type HTMLMotionProps, motion, useReducedMotion, } from "motion/react"; import { cloneElement, type ComponentPropsWithRef, createContext, type ReactElement, type ReactNode, type Ref, useCallback, useContext, useId, useState, } from "react"; import { EASE_OUT, SPRING_LAYOUT, SPRING_SWAP, } from "@/lib/ease"; import { cn } from "@/lib/utils"; import { MessageSideContext } from "@/components/agents/message-context"; export type MessageBubbleVariant = | "solid" | "soft" | "tint" | "outline" | "ghost" | "danger"; export type MessageBubbleAlign = "start" | "end"; interface MessageBubbleContextValue { align?: MessageBubbleAlign; animateIn: boolean; variant: MessageBubbleVariant; } const MessageBubbleContext = createContext({ animateIn: true, variant: "soft", }); const MessageBubbleLayoutContext = createContext<() => void>(() => {}); export interface MessageBubbleProps extends Omit, "children"> { variant?: MessageBubbleVariant; /** Defaults to the surrounding Message alignment when omitted. */ align?: MessageBubbleAlign; /** Plays the bubble entrance once when this component mounts. */ animateIn?: boolean; children?: ReactNode; } export interface MessageBubbleContentProps extends ComponentPropsWithRef<"div"> { /** Replaces the content element while preserving bubble styling. */ render?: ReactElement; } export interface MessageBubbleGroupProps extends ComponentPropsWithRef<"div"> { spacing?: "compact" | "default"; } export interface MessageBubbleCollapsibleProps extends ComponentPropsWithRef<"div"> { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; collapsedLines?: 2 | 3 | 4 | 5 | 6; moreLabel?: ReactNode; lessLabel?: ReactNode; contentClassName?: string; triggerClassName?: string; children?: ReactNode; } function mergeRefs(...refs: Array | undefined>) { return (node: T | null) => { for (const ref of refs) { if (typeof ref === "function") ref(node); else if (ref) ref.current = node; } }; } const BUBBLE_CONTENT_REVEAL = { duration: 0.12, ease: EASE_OUT, delay: 0.04, } as const; // Sent bubbles should pop into place quickly with one restrained overshoot. const BUBBLE_POP = { type: "spring", stiffness: 520, damping: 27, mass: 0.52, } as const; export function MessageBubble({ variant = "soft", align, animateIn = false, className, children, initial, animate, exit, transition, layout, ...props }: MessageBubbleProps) { const reduce = useReducedMotion() ?? false; const messageSide = useContext(MessageSideContext); const resolvedAlign = align ?? messageSide ?? "start"; return ( {children} ); } function bubbleContentClass( variant: MessageBubbleVariant, interactive: boolean, ) { return cn( "relative z-0 min-w-9 max-w-[82%] rounded-2xl px-3.5 py-2.5 text-sm leading-6 text-foreground", "[&_a]:font-medium [&_a]:underline [&_a]:underline-offset-4 [&_code]:rounded [&_code]:bg-background/60 [&_code]:px-1 [&_code]:py-0.5 [&_code]:font-mono [&_code]:text-[0.9em] [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_p+p]:mt-2 [&_pre]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-xl [&_pre]:bg-background/60 [&_pre]:p-3 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5", variant === "solid" && "text-background", variant === "ghost" && "w-full max-w-none rounded-none px-0 py-0", variant === "danger" && "text-destructive", interactive && "cursor-pointer text-left outline-none transition-[background-color,color,transform] duration-150 hover:brightness-[0.98] focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.99]", ); } function bubbleSurfaceClass( variant: MessageBubbleVariant, align: MessageBubbleAlign, ) { return cn( "pointer-events-none absolute inset-0 -z-10 rounded-[inherit]", align === "end" ? "origin-bottom-right" : "origin-bottom-left", variant === "solid" && "bg-foreground", variant === "soft" && "bg-muted", variant === "tint" && "bg-primary/10", variant === "outline" && "border border-border/70 bg-background", variant === "danger" && "bg-destructive/10", ); } export function MessageBubbleContent({ render, className, children, ref, ...props }: MessageBubbleContentProps) { const reduce = useReducedMotion() ?? false; const { align = "start", animateIn, variant } = useContext(MessageBubbleContext); const [layoutVersion, setLayoutVersion] = useState(0); const notifyLayout = useCallback( () => setLayoutVersion((version) => version + 1), [], ); const interactive = render?.type === "button" || render?.type === "a"; const classes = cn(bubbleContentClass(variant, interactive), className); const composedChildren = ( <> {variant !== "ghost" ? (