SarahXia0405's picture
Upload 72 files
760b33c verified
raw
history blame
7.58 kB
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>
);
}