Spaces:
Running
Running
import { useState } from "react"; | |
import { ArrowRight, RefreshCcw, Copy, Check, Trash2, RotateCcw, ListFilter, ChevronLeft, ChevronRight } from "lucide-react"; | |
import { Button } from "@/components/ui/button"; | |
import { cn } from "@/lib/utils"; | |
import { format } from "date-fns"; | |
import { Avatar, AvatarFallback } from "../ui/avatar"; | |
import { ChatMessage } from "./ChatMessage"; | |
import { ThinkingAnimation } from "./ThinkingAnimation"; | |
import { toast } from '../ui/sonner'; | |
import { MessageActions } from "./MessageActions"; | |
import { MessageVariationControls } from "./MessageVariationControls"; | |
import { DeleteMessageDialog } from "./DeleteMessageDialog"; | |
import { MessageVariation, Message } from "@/types/chat"; | |
interface ChatBubbleProps { | |
message: Message; | |
onViewSearchResults?: (messageId: string) => void; | |
onRetry?: (messageId: string) => void; | |
onRegenerate?: (messageId: string) => void; | |
onDelete?: (messageId: string) => void; | |
onSelectVariation?: (messageId: string, variationId: string) => void; | |
} | |
export const ChatBubble = ({ | |
message, | |
onViewSearchResults, | |
onRetry, | |
onRegenerate, | |
onDelete, | |
onSelectVariation | |
}: ChatBubbleProps) => { | |
const [copied, setCopied] = useState(false); | |
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); | |
const isWelcomeMessage = message.id === "welcomems"; | |
const isSystem = message.sender === "system"; | |
const hasVariations = isSystem && message.variations && message.variations.length > 0; | |
// If the message has variations, display the active one or the first one | |
const displayContent = isSystem && hasVariations && message.activeVariation | |
? message.variations.find(v => v.id === message.activeVariation)?.content || message.content | |
: message.content; | |
const copyToClipboard = () => { | |
// Strip thinking content before copying | |
const cleanContent = displayContent.replace(/<think>[\s\S]*?<\/think>/g, '').trim(); | |
navigator.clipboard.writeText(cleanContent); | |
setCopied(true); | |
toast.success('Copied to clipboard!'); | |
setTimeout(() => setCopied(false), 2000); | |
}; | |
const handleDelete = () => { | |
setDeleteDialogOpen(false); | |
if (onDelete) { | |
onDelete(message.id); | |
} | |
}; | |
return ( | |
<> | |
<div | |
className={cn( | |
"group flex w-full animate-slide-in mb-2", | |
isSystem ? "justify-start" : "justify-end" | |
)} | |
> | |
<div className={cn( | |
"flex gap-3 max-w-[80%]", | |
isSystem ? "flex-row" : "flex-row-reverse" | |
)}> | |
<Avatar className={cn( | |
"h-8 w-8 border", | |
isSystem | |
? "bg-financial-accent dark:text-white shadow-lg" | |
: "bg-muted" | |
)}> | |
<AvatarFallback className="text-xs font-semibold"> | |
{isSystem ? "AI" : "You"} | |
</AvatarFallback> | |
</Avatar> | |
<div className="flex flex-col"> | |
<div className={cn( | |
"rounded-2xl shadow-lg message-bubble backdrop-blur-sm", | |
isSystem | |
? "bg-white/90 dark:bg-card/90 border border-border text-foreground message-bubble-ai" | |
: "bg-financial-accent/30 border border-financial-accent/30 dark:text-white message-bubble-user", | |
message.error && "border-destructive dark:border-red-500" | |
)}> | |
{message.isLoading ? ( | |
<ThinkingAnimation /> | |
) : ( | |
<ChatMessage | |
content={displayContent} | |
/> | |
)} | |
</div> | |
{/* Chat bubble footer */} | |
<div className="flex flex-row chat-bubble-footer justify-between items-center mt-1"> | |
{/* Time */} | |
<div className={cn( | |
"text-xs text-muted-foreground", | |
isSystem ? "text-left" : "text-right" | |
)}> | |
{format(message.timestamp, "h:mm a")} | |
</div> | |
{/* Controls */} | |
{!isWelcomeMessage && ( | |
<div className="controls flex gap-1 items-center"> | |
{/* Variation controls */} | |
{hasVariations && message.variations && message.variations.length > 1 && ( | |
<MessageVariationControls | |
message={message} | |
onSelectVariation={onSelectVariation} | |
/> | |
)} | |
<MessageActions | |
message={message} | |
onRetry={onRetry} | |
onRegenerate={onRegenerate} | |
onDelete={() => setDeleteDialogOpen(true)} | |
onCopy={copyToClipboard} | |
copied={copied} | |
/> | |
</div> | |
)} | |
</div> | |
</div> | |
</div> | |
</div> | |
<DeleteMessageDialog | |
isOpen={deleteDialogOpen} | |
onOpenChange={setDeleteDialogOpen} | |
onDelete={handleDelete} | |
/> | |
</> | |
); | |
}; | |