mirror of
https://litchi.icu/ngc2207/judge4c-demo.git
synced 2025-05-18 17:06:34 +00:00
feat: 添加shadcn-chat
This commit is contained in:
parent
778342b40b
commit
f2373ba8b0
202
src/components/ui/chat/chat-bubble.tsx
Normal file
202
src/components/ui/chat/chat-bubble.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import MessageLoading from "./message-loading";
|
||||
import { Button, ButtonProps } from "../button";
|
||||
|
||||
// ChatBubble
|
||||
const chatBubbleVariant = cva(
|
||||
"flex gap-2 max-w-[60%] items-end relative group",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
received: "self-start",
|
||||
sent: "self-end flex-row-reverse",
|
||||
},
|
||||
layout: {
|
||||
default: "",
|
||||
ai: "max-w-full w-full items-center",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "received",
|
||||
layout: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface ChatBubbleProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof chatBubbleVariant> {}
|
||||
|
||||
const ChatBubble = React.forwardRef<HTMLDivElement, ChatBubbleProps>(
|
||||
({ className, variant, layout, children, ...props }, ref) => (
|
||||
<div
|
||||
className={cn(
|
||||
chatBubbleVariant({ variant, layout, className }),
|
||||
"relative group",
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{React.Children.map(children, (child) =>
|
||||
React.isValidElement(child) && typeof child.type !== "string"
|
||||
? React.cloneElement(child, {
|
||||
variant,
|
||||
layout,
|
||||
} as React.ComponentProps<typeof child.type>)
|
||||
: child,
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
ChatBubble.displayName = "ChatBubble";
|
||||
|
||||
// ChatBubbleAvatar
|
||||
interface ChatBubbleAvatarProps {
|
||||
src?: string;
|
||||
fallback?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ChatBubbleAvatar: React.FC<ChatBubbleAvatarProps> = ({
|
||||
src,
|
||||
fallback,
|
||||
className,
|
||||
}) => (
|
||||
<Avatar className={className}>
|
||||
<AvatarImage src={src} alt="Avatar" />
|
||||
<AvatarFallback>{fallback}</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
|
||||
// ChatBubbleMessage
|
||||
const chatBubbleMessageVariants = cva("p-4", {
|
||||
variants: {
|
||||
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",
|
||||
},
|
||||
layout: {
|
||||
default: "",
|
||||
ai: "border-t w-full rounded-none bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "received",
|
||||
layout: "default",
|
||||
},
|
||||
});
|
||||
|
||||
interface ChatBubbleMessageProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof chatBubbleMessageVariants> {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const ChatBubbleMessage = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChatBubbleMessageProps
|
||||
>(
|
||||
(
|
||||
{ className, variant, layout, isLoading = false, children, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<div
|
||||
className={cn(
|
||||
chatBubbleMessageVariants({ variant, layout, className }),
|
||||
"break-words max-w-full whitespace-pre-wrap",
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center space-x-2">
|
||||
<MessageLoading />
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
ChatBubbleMessage.displayName = "ChatBubbleMessage";
|
||||
|
||||
// ChatBubbleTimestamp
|
||||
interface ChatBubbleTimestampProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
const ChatBubbleTimestamp: React.FC<ChatBubbleTimestampProps> = ({
|
||||
timestamp,
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div className={cn("text-xs mt-2 text-right", className)} {...props}>
|
||||
{timestamp}
|
||||
</div>
|
||||
);
|
||||
|
||||
// ChatBubbleAction
|
||||
type ChatBubbleActionProps = ButtonProps & {
|
||||
icon: React.ReactNode;
|
||||
};
|
||||
|
||||
const ChatBubbleAction: React.FC<ChatBubbleActionProps> = ({
|
||||
icon,
|
||||
onClick,
|
||||
className,
|
||||
variant = "ghost",
|
||||
size = "icon",
|
||||
...props
|
||||
}) => (
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
);
|
||||
|
||||
interface ChatBubbleActionWrapperProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
variant?: "sent" | "received";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ChatBubbleActionWrapper = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
ChatBubbleActionWrapperProps
|
||||
>(({ variant, className, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute top-1/2 -translate-y-1/2 flex opacity-0 group-hover:opacity-100 transition-opacity duration-200",
|
||||
variant === "sent"
|
||||
? "-left-1 -translate-x-full flex-row-reverse"
|
||||
: "-right-1 translate-x-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
ChatBubbleActionWrapper.displayName = "ChatBubbleActionWrapper";
|
||||
|
||||
export {
|
||||
ChatBubble,
|
||||
ChatBubbleAvatar,
|
||||
ChatBubbleMessage,
|
||||
ChatBubbleTimestamp,
|
||||
chatBubbleVariant,
|
||||
chatBubbleMessageVariants,
|
||||
ChatBubbleAction,
|
||||
ChatBubbleActionWrapper,
|
||||
};
|
23
src/components/ui/chat/chat-input.tsx
Normal file
23
src/components/ui/chat/chat-input.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import * as React from "react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ChatInputProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement>{}
|
||||
|
||||
const ChatInput = React.forwardRef<HTMLTextAreaElement, ChatInputProps>(
|
||||
({ className, ...props }, ref) => (
|
||||
<Textarea
|
||||
autoComplete="off"
|
||||
ref={ref}
|
||||
name="message"
|
||||
className={cn(
|
||||
"max-h-12 px-4 py-3 bg-background text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 w-full rounded-md flex items-center h-16 resize-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
ChatInput.displayName = "ChatInput";
|
||||
|
||||
export { ChatInput };
|
23
src/components/ui/chat/chat-message-list.tsx
Normal file
23
src/components/ui/chat/chat-message-list.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ChatMessageListProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
const ChatMessageList = React.forwardRef<HTMLDivElement, ChatMessageListProps>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col w-full h-full p-4 gap-6 overflow-y-auto",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
ChatMessageList.displayName = "ChatMessageList";
|
||||
|
||||
export { ChatMessageList };
|
153
src/components/ui/chat/expandable-chat.tsx
Normal file
153
src/components/ui/chat/expandable-chat.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import React, { useRef, useState } from "react";
|
||||
import { X, MessageCircle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export type ChatPosition = "bottom-right" | "bottom-left";
|
||||
export type ChatSize = "sm" | "md" | "lg" | "xl" | "full";
|
||||
|
||||
const chatConfig = {
|
||||
dimensions: {
|
||||
sm: "sm:max-w-sm sm:max-h-[500px]",
|
||||
md: "sm:max-w-md sm:max-h-[600px]",
|
||||
lg: "sm:max-w-lg sm:max-h-[700px]",
|
||||
xl: "sm:max-w-xl sm:max-h-[800px]",
|
||||
full: "sm:w-full sm:h-full",
|
||||
},
|
||||
positions: {
|
||||
"bottom-right": "bottom-5 right-5",
|
||||
"bottom-left": "bottom-5 left-5",
|
||||
},
|
||||
chatPositions: {
|
||||
"bottom-right": "sm:bottom-[calc(100%+10px)] sm:right-0",
|
||||
"bottom-left": "sm:bottom-[calc(100%+10px)] sm:left-0",
|
||||
},
|
||||
states: {
|
||||
open: "pointer-events-auto opacity-100 visible scale-100 translate-y-0",
|
||||
closed:
|
||||
"pointer-events-none opacity-0 invisible scale-100 sm:translate-y-5",
|
||||
},
|
||||
};
|
||||
|
||||
interface ExpandableChatProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
position?: ChatPosition;
|
||||
size?: ChatSize;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ExpandableChat: React.FC<ExpandableChatProps> = ({
|
||||
className,
|
||||
position = "bottom-right",
|
||||
size = "md",
|
||||
icon,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const chatRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toggleChat = () => setIsOpen(!isOpen);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(`fixed ${chatConfig.positions[position]} z-50`, className)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
ref={chatRef}
|
||||
className={cn(
|
||||
"flex flex-col bg-background border sm:rounded-lg shadow-md overflow-hidden transition-all duration-250 ease-out sm:absolute sm:w-[90vw] sm:h-[80vh] fixed inset-0 w-full h-full sm:inset-auto",
|
||||
chatConfig.chatPositions[position],
|
||||
chatConfig.dimensions[size],
|
||||
isOpen ? chatConfig.states.open : chatConfig.states.closed,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 sm:hidden"
|
||||
onClick={toggleChat}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<ExpandableChatToggle
|
||||
icon={icon}
|
||||
isOpen={isOpen}
|
||||
toggleChat={toggleChat}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ExpandableChat.displayName = "ExpandableChat";
|
||||
|
||||
const ExpandableChatHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn("flex items-center justify-between p-4 border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
ExpandableChatHeader.displayName = "ExpandableChatHeader";
|
||||
|
||||
const ExpandableChatBody: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => <div className={cn("flex-grow overflow-y-auto", className)} {...props} />;
|
||||
|
||||
ExpandableChatBody.displayName = "ExpandableChatBody";
|
||||
|
||||
const ExpandableChatFooter: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => <div className={cn("border-t p-4", className)} {...props} />;
|
||||
|
||||
ExpandableChatFooter.displayName = "ExpandableChatFooter";
|
||||
|
||||
interface ExpandableChatToggleProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
icon?: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
toggleChat: () => void;
|
||||
}
|
||||
|
||||
const ExpandableChatToggle: React.FC<ExpandableChatToggleProps> = ({
|
||||
className,
|
||||
icon,
|
||||
isOpen,
|
||||
toggleChat,
|
||||
...props
|
||||
}) => (
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={toggleChat}
|
||||
className={cn(
|
||||
"w-14 h-14 rounded-full shadow-md flex items-center justify-center hover:shadow-lg hover:shadow-black/30 transition-all duration-300",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{isOpen ? (
|
||||
<X className="h-6 w-6" />
|
||||
) : (
|
||||
icon || <MessageCircle className="h-6 w-6" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
ExpandableChatToggle.displayName = "ExpandableChatToggle";
|
||||
|
||||
export {
|
||||
ExpandableChat,
|
||||
ExpandableChatHeader,
|
||||
ExpandableChatBody,
|
||||
ExpandableChatFooter,
|
||||
};
|
45
src/components/ui/chat/message-loading.tsx
Normal file
45
src/components/ui/chat/message-loading.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
// @hidden
|
||||
export default function MessageLoading() {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="text-foreground"
|
||||
>
|
||||
<circle cx="4" cy="12" r="2" fill="currentColor">
|
||||
<animate
|
||||
id="spinner_qFRN"
|
||||
begin="0;spinner_OcgL.end+0.25s"
|
||||
attributeName="cy"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
values="12;6;12"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="12" cy="12" r="2" fill="currentColor">
|
||||
<animate
|
||||
begin="spinner_qFRN.begin+0.1s"
|
||||
attributeName="cy"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
values="12;6;12"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
/>
|
||||
</circle>
|
||||
<circle cx="20" cy="12" r="2" fill="currentColor">
|
||||
<animate
|
||||
id="spinner_OcgL"
|
||||
begin="spinner_qFRN.begin+0.2s"
|
||||
attributeName="cy"
|
||||
calcMode="spline"
|
||||
dur="0.6s"
|
||||
values="12;6;12"
|
||||
keySplines=".33,.66,.66,1;.33,0,.66,.33"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user