Spaces:
Sleeping
Sleeping
| import React, { useState } from 'react'; | |
| import { Button } from './ui/button'; | |
| import { | |
| Copy, | |
| ThumbsUp, | |
| ThumbsDown, | |
| ChevronDown, | |
| ChevronUp, | |
| Check, | |
| Bot | |
| } from 'lucide-react'; | |
| import { Badge } from './ui/badge'; | |
| import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible'; | |
| import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog'; | |
| import { Textarea } from './ui/textarea'; | |
| import { Label } from './ui/label'; | |
| import type { Message as MessageType } from '../App'; | |
| import { toast } from 'sonner@2.0.3'; | |
| interface MessageProps { | |
| message: MessageType; | |
| showSenderInfo?: boolean; // For group chat mode | |
| } | |
| export function Message({ message, showSenderInfo = false }: MessageProps) { | |
| const [feedback, setFeedback] = useState<'helpful' | 'not-helpful' | null>(null); | |
| const [copied, setCopied] = useState(false); | |
| const [referencesOpen, setReferencesOpen] = useState(false); | |
| const [showFeedbackDialog, setShowFeedbackDialog] = useState(false); | |
| const [feedbackType, setFeedbackType] = useState<'helpful' | 'not-helpful' | null>(null); | |
| const [feedbackText, setFeedbackText] = useState(''); | |
| const isUser = message.role === 'user'; | |
| const handleCopy = async () => { | |
| await navigator.clipboard.writeText(message.content); | |
| setCopied(true); | |
| toast.success('Message copied to clipboard'); | |
| setTimeout(() => setCopied(false), 2000); | |
| }; | |
| const handleFeedback = (type: 'helpful' | 'not-helpful') => { | |
| setFeedback(type); | |
| toast.success(`Thanks for your feedback!`); | |
| }; | |
| const handleFeedbackDialogOpen = (type: 'helpful' | 'not-helpful') => { | |
| setFeedbackType(type); | |
| setShowFeedbackDialog(true); | |
| }; | |
| const handleFeedbackDialogClose = () => { | |
| setShowFeedbackDialog(false); | |
| setFeedbackType(null); | |
| setFeedbackText(''); | |
| }; | |
| const handleFeedbackDialogSubmit = () => { | |
| if (feedbackType) { | |
| setFeedback(feedbackType); | |
| toast.success(`Thanks for your feedback!`); | |
| } | |
| handleFeedbackDialogClose(); | |
| }; | |
| return ( | |
| <div className={`flex gap-3 ${isUser && !showSenderInfo ? 'justify-end' : 'justify-start'}`}> | |
| {/* Avatar */} | |
| {showSenderInfo && message.sender ? ( | |
| <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${ | |
| message.sender.isAI | |
| ? 'bg-gradient-to-br from-purple-500 to-blue-500' | |
| : 'bg-muted' | |
| }`}> | |
| {message.sender.isAI ? ( | |
| <Bot className="h-4 w-4 text-white" /> | |
| ) : ( | |
| <span className="text-sm"> | |
| {message.sender.name.split(' ').map(n => n[0]).join('').toUpperCase()} | |
| </span> | |
| )} | |
| </div> | |
| ) : !isUser ? ( | |
| <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center flex-shrink-0"> | |
| <span className="text-white text-sm">C</span> | |
| </div> | |
| ) : null} | |
| <div className={`flex flex-col gap-2 max-w-[80%] ${isUser && !showSenderInfo ? 'items-end' : 'items-start'}`}> | |
| {/* Sender name in group chat */} | |
| {showSenderInfo && message.sender && ( | |
| <div className="flex items-center gap-2 px-1"> | |
| <span className="text-xs">{message.sender.name}</span> | |
| {message.sender.isAI && ( | |
| <Badge variant="secondary" className="text-xs h-4 px-1">AI</Badge> | |
| )} | |
| </div> | |
| )} | |
| <div | |
| className={` | |
| rounded-2xl px-4 py-3 | |
| ${isUser && !showSenderInfo | |
| ? 'bg-primary text-primary-foreground' | |
| : 'bg-muted' | |
| } | |
| `} | |
| > | |
| <p className="whitespace-pre-wrap">{message.content}</p> | |
| </div> | |
| {/* References */} | |
| {message.references && message.references.length > 0 && ( | |
| <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}> | |
| <CollapsibleTrigger asChild> | |
| <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs"> | |
| {referencesOpen ? ( | |
| <ChevronUp className="h-3 w-3" /> | |
| ) : ( | |
| <ChevronDown className="h-3 w-3" /> | |
| )} | |
| {message.references.length} {message.references.length === 1 ? 'reference' : 'references'} | |
| </Button> | |
| </CollapsibleTrigger> | |
| <CollapsibleContent className="space-y-1 mt-1"> | |
| {message.references.map((ref, index) => ( | |
| <Badge key={index} variant="outline" className="text-xs"> | |
| {ref} | |
| </Badge> | |
| ))} | |
| </CollapsibleContent> | |
| </Collapsible> | |
| )} | |
| {/* Message Actions */} | |
| <div className="flex items-center gap-1"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-7 gap-1" | |
| onClick={handleCopy} | |
| > | |
| {copied ? ( | |
| <> | |
| <Check className="h-3 w-3" /> | |
| <span className="text-xs">Copied</span> | |
| </> | |
| ) : ( | |
| <> | |
| <Copy className="h-3 w-3" /> | |
| <span className="text-xs">Copy</span> | |
| </> | |
| )} | |
| </Button> | |
| {!isUser && ( | |
| <> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className={`h-7 gap-1 ${feedback === 'helpful' ? 'text-green-600' : ''}`} | |
| onClick={() => handleFeedbackDialogOpen('helpful')} | |
| > | |
| <ThumbsUp className="h-3 w-3" /> | |
| <span className="text-xs">Helpful</span> | |
| </Button> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className={`h-7 gap-1 ${feedback === 'not-helpful' ? 'text-red-600' : ''}`} | |
| onClick={() => handleFeedbackDialogOpen('not-helpful')} | |
| > | |
| <ThumbsDown className="h-3 w-3" /> | |
| <span className="text-xs">Not helpful</span> | |
| </Button> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| {isUser && !showSenderInfo && ( | |
| <div className="w-8 h-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0"> | |
| <span className="text-sm">👤</span> | |
| </div> | |
| )} | |
| {/* Feedback Dialog */} | |
| <Dialog open={showFeedbackDialog} onOpenChange={handleFeedbackDialogClose}> | |
| <DialogContent className="sm:max-w-[425px]"> | |
| <DialogHeader> | |
| <DialogTitle>Provide Feedback</DialogTitle> | |
| <DialogDescription> | |
| {feedbackType === 'helpful' ? 'Tell us why you found this message helpful.' : 'Tell us why you found this message not helpful.'} | |
| </DialogDescription> | |
| </DialogHeader> | |
| <Textarea | |
| className="h-20" | |
| value={feedbackText} | |
| onChange={(e) => setFeedbackText(e.target.value)} | |
| placeholder="Type your feedback here..." | |
| /> | |
| <DialogFooter> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| onClick={handleFeedbackDialogClose} | |
| > | |
| Cancel | |
| </Button> | |
| <Button | |
| type="button" | |
| onClick={handleFeedbackDialogSubmit} | |
| > | |
| Submit | |
| </Button> | |
| </DialogFooter> | |
| </DialogContent> | |
| </Dialog> | |
| </div> | |
| ); | |
| } |