Spaces:
Sleeping
Sleeping
| 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} | |
| /> | |
| </> | |
| ); | |
| }; | |