2025-08-04 17:45:44 +02:00
|
|
|
import { cva, type VariantProps } from "class-variance-authority";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { Check, CheckCheck } from "lucide-react";
|
|
|
|
|
import * as React from "react";
|
2025-08-04 17:45:44 +02:00
|
|
|
import MessageLoading from "~/components/chat/message-loading";
|
|
|
|
|
import { Button, type ButtonProps } from "~/components/ui/button";
|
2025-08-28 18:27:07 +02:00
|
|
|
import { cn } from "~/lib/utils";
|
2025-08-04 17:45:44 +02:00
|
|
|
|
|
|
|
|
// ChatBubble
|
|
|
|
|
const chatBubbleVariant = cva(
|
2025-08-28 18:27:07 +02:00
|
|
|
"flex gap-2 max-w-[60%] items-end relative group",
|
|
|
|
|
{
|
2025-08-29 16:18:32 +02:00
|
|
|
defaultVariants: {
|
|
|
|
|
layout: "default",
|
|
|
|
|
variant: "received",
|
|
|
|
|
},
|
2025-08-28 18:27:07 +02:00
|
|
|
variants: {
|
2025-08-29 16:18:32 +02:00
|
|
|
layout: {
|
|
|
|
|
ai: "max-w-full w-full items-center",
|
|
|
|
|
default: "",
|
|
|
|
|
},
|
2025-08-28 18:27:07 +02:00
|
|
|
variant: {
|
|
|
|
|
received: "self-start",
|
|
|
|
|
sent: "self-end flex-row-reverse",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-08-04 17:45:44 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
interface ChatBubbleProps
|
2025-08-28 18:27:07 +02:00
|
|
|
extends React.HTMLAttributes<HTMLDivElement>,
|
|
|
|
|
VariantProps<typeof chatBubbleVariant> {}
|
2025-08-04 17:45:44 +02:00
|
|
|
|
|
|
|
|
const ChatBubble = React.forwardRef<HTMLDivElement, ChatBubbleProps>(
|
2025-08-28 18:27:07 +02:00
|
|
|
({ className, variant, layout, children, ...props }, ref) => (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
2025-08-29 16:18:32 +02:00
|
|
|
chatBubbleVariant({ className, layout, variant }),
|
2025-08-28 18:27:07 +02:00
|
|
|
"group relative",
|
|
|
|
|
)}
|
|
|
|
|
ref={ref}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{React.Children.map(children, (child) =>
|
|
|
|
|
React.isValidElement(child) && typeof child.type !== "string"
|
|
|
|
|
? React.cloneElement(child, {
|
|
|
|
|
layout,
|
2025-08-29 16:18:32 +02:00
|
|
|
variant,
|
2025-08-28 18:27:07 +02:00
|
|
|
} as Partial<React.ComponentProps<typeof child.type>>)
|
|
|
|
|
: child,
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
2025-08-04 17:45:44 +02:00
|
|
|
);
|
|
|
|
|
ChatBubble.displayName = "ChatBubble";
|
|
|
|
|
|
|
|
|
|
type ChatBubbleReadStatusProps = {
|
2025-08-28 18:27:07 +02:00
|
|
|
isread: boolean;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const ChatBubbleReadStatus = ({ isread }: ChatBubbleReadStatusProps) => {
|
2025-08-28 18:27:07 +02:00
|
|
|
if (isread) {
|
|
|
|
|
return <CheckCheck className="mt-2 size-4 text-green-500" />;
|
|
|
|
|
} else {
|
|
|
|
|
return <Check className="text-muted-foreground mt-2 size-4" />;
|
|
|
|
|
}
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ChatBubbleMessage
|
|
|
|
|
const chatBubbleMessageVariants = cva("p-2", {
|
2025-08-29 16:18:32 +02:00
|
|
|
defaultVariants: {
|
|
|
|
|
layout: "default",
|
|
|
|
|
variant: "received",
|
|
|
|
|
},
|
2025-08-28 18:27:07 +02:00
|
|
|
variants: {
|
2025-08-29 16:18:32 +02:00
|
|
|
layout: {
|
|
|
|
|
ai: "border-t w-full rounded-none bg-transparent",
|
|
|
|
|
default: "",
|
|
|
|
|
},
|
2025-08-28 18:27:07 +02:00
|
|
|
variant: {
|
|
|
|
|
received:
|
|
|
|
|
"bg-secondary text-secondary-foreground rounded-r-lg rounded-tl-lg",
|
|
|
|
|
sent: "bg-primary text-primary-foreground rounded-l-lg rounded-tr-lg",
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-08-04 17:45:44 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
interface ChatBubbleMessageProps
|
2025-08-28 18:27:07 +02:00
|
|
|
extends React.HTMLAttributes<HTMLDivElement>,
|
|
|
|
|
VariantProps<typeof chatBubbleMessageVariants> {
|
|
|
|
|
isLoading?: boolean;
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ChatBubbleMessage = React.forwardRef<
|
2025-08-28 18:27:07 +02:00
|
|
|
HTMLDivElement,
|
|
|
|
|
ChatBubbleMessageProps
|
2025-08-04 17:45:44 +02:00
|
|
|
>(
|
2025-08-28 18:27:07 +02:00
|
|
|
(
|
|
|
|
|
{ className, variant, layout, isLoading = false, children, ...props },
|
|
|
|
|
ref,
|
|
|
|
|
) => (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
2025-08-29 16:18:32 +02:00
|
|
|
chatBubbleMessageVariants({ className, layout, variant }),
|
2025-08-28 18:27:07 +02:00
|
|
|
"max-w-full break-words whitespace-pre-wrap",
|
|
|
|
|
)}
|
|
|
|
|
ref={ref}
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<MessageLoading />
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
children
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
2025-08-04 17:45:44 +02:00
|
|
|
);
|
|
|
|
|
ChatBubbleMessage.displayName = "ChatBubbleMessage";
|
|
|
|
|
|
|
|
|
|
// ChatBubbleTimestamp
|
|
|
|
|
interface ChatBubbleTimestampProps
|
2025-08-28 18:27:07 +02:00
|
|
|
extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
|
|
timestamp: string;
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ChatBubbleTimestamp: React.FC<ChatBubbleTimestampProps> = ({
|
2025-08-28 18:27:07 +02:00
|
|
|
timestamp,
|
|
|
|
|
className,
|
|
|
|
|
...props
|
2025-08-04 17:45:44 +02:00
|
|
|
}) => (
|
2025-08-28 18:27:07 +02:00
|
|
|
<div className={cn("mt-2 text-right text-xs", className)} {...props}>
|
|
|
|
|
{timestamp}
|
|
|
|
|
</div>
|
2025-08-04 17:45:44 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ChatBubbleAction
|
|
|
|
|
type ChatBubbleActionProps = ButtonProps & {
|
2025-08-28 18:27:07 +02:00
|
|
|
icon: React.ReactNode;
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ChatBubbleAction: React.FC<ChatBubbleActionProps> = ({
|
2025-08-28 18:27:07 +02:00
|
|
|
icon,
|
|
|
|
|
onClick,
|
|
|
|
|
className,
|
|
|
|
|
variant = "ghost",
|
|
|
|
|
size = "icon",
|
|
|
|
|
...props
|
2025-08-04 17:45:44 +02:00
|
|
|
}) => (
|
2025-08-28 18:27:07 +02:00
|
|
|
<Button
|
|
|
|
|
aria-label="Chat Bubble Action"
|
|
|
|
|
className={className}
|
|
|
|
|
onClick={onClick}
|
2025-08-29 16:18:32 +02:00
|
|
|
size={size}
|
|
|
|
|
variant={variant}
|
2025-08-28 18:27:07 +02:00
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{icon}
|
|
|
|
|
</Button>
|
2025-08-04 17:45:44 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
interface ChatBubbleActionWrapperProps
|
2025-08-28 18:27:07 +02:00
|
|
|
extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
|
|
variant?: "sent" | "received";
|
|
|
|
|
className?: string;
|
|
|
|
|
show: boolean;
|
2025-08-04 17:45:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ChatBubbleActionWrapper = React.forwardRef<
|
2025-08-28 18:27:07 +02:00
|
|
|
HTMLDivElement,
|
|
|
|
|
ChatBubbleActionWrapperProps
|
2025-08-04 17:45:44 +02:00
|
|
|
>(({ variant, className, show, children, ...props }, ref) => (
|
2025-08-28 18:27:07 +02:00
|
|
|
<>
|
|
|
|
|
{show && (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"absolute top-1/2 flex -translate-y-1/2 opacity-0 transition-opacity duration-200 group-hover:opacity-100",
|
|
|
|
|
variant === "sent"
|
|
|
|
|
? "-left-1 -translate-x-full flex-row-reverse"
|
|
|
|
|
: "-right-1 translate-x-full",
|
|
|
|
|
className,
|
|
|
|
|
)}
|
2025-08-29 16:18:32 +02:00
|
|
|
ref={ref}
|
2025-08-28 18:27:07 +02:00
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
2025-08-04 17:45:44 +02:00
|
|
|
));
|
|
|
|
|
ChatBubbleActionWrapper.displayName = "ChatBubbleActionWrapper";
|
|
|
|
|
|
|
|
|
|
export {
|
2025-08-28 18:27:07 +02:00
|
|
|
ChatBubble,
|
|
|
|
|
ChatBubbleMessage,
|
|
|
|
|
ChatBubbleTimestamp,
|
|
|
|
|
ChatBubbleAction,
|
|
|
|
|
ChatBubbleActionWrapper,
|
2025-08-04 17:45:44 +02:00
|
|
|
};
|