Download src/components/message.tsx from IOE-IFAD/test: direct link, hf CLI and curl.
- Browser
- Download file 5.55 kB
-
https://huggingface.co/spaces/IOE-IFAD/test/resolve/main/src/components/message.tsx
- Command line
-
hf download hf://spaces/IOE-IFAD/test/src/components/message.tsx
-
curl -L -o message.tsx https://huggingface.co/spaces/IOE-IFAD/test/resolve/main/src/components/message.tsx
5.55 kB
| "use client"; | |
| import { Button } from "@/components/ui/button"; | |
| import type { Message } from "@/lib/types"; | |
| import { cn } from "@/lib/utils"; | |
| import { Bot, FileText, Star, User, Copy, Check } from "lucide-react"; | |
| import { useState } from "react"; | |
| import ReactMarkdown from "react-markdown"; | |
| import { useToast } from "@/hooks/use-toast"; | |
| type Props = { | |
| message: Message; | |
| handleTranscriptClick: (transcript: Message["transcripts"][0]) => void; | |
| onEvaluate: () => void; | |
| }; | |
| const MessageBlock = ({ | |
| message, | |
| handleTranscriptClick, | |
| onEvaluate, | |
| }: Props) => { | |
| const Icon = message.role === "user" ? User : Bot; | |
| const role = message.role === "user" ? "You" : "AI Assistant"; | |
| const [copied, setCopied] = useState(false); | |
| const { toast } = useToast(); | |
| const handleCopy = async () => { | |
| await navigator.clipboard.writeText(message.content); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| toast({ | |
| title: "Copied to clipboard", | |
| description: "The response has been copied successfully.", | |
| }); | |
| }; | |
| return ( | |
| <div className="w-full"> | |
| <div | |
| className={cn( | |
| "rounded-2xl p-4 sm:p-6 transition-all duration-200", | |
| "max-w-full overflow-x-auto break-words", // Prevent overflow on mobile | |
| message.role === "user" | |
| ? "bg-gradient-to-br from-primary/10 to-primary/5 border border-primary/20 ml-0 sm:ml-12" | |
| : "bg-gradient-to-br from-card to-card/50 border border-border/50 mr-0 sm:mr-12" | |
| )} | |
| style={{ wordBreak: "break-word" }} // Extra safety for long words | |
| > | |
| <div | |
| className={cn( | |
| "flex items-center gap-2 sm:gap-3 mb-3 sm:mb-4", | |
| message.role === "assistant" ? "justify-start" : "justify-end" | |
| )} | |
| > | |
| <div | |
| className={cn( | |
| "flex items-center gap-2", | |
| message.role === "user" && "flex-row-reverse" | |
| )} | |
| > | |
| <div | |
| className={cn( | |
| "w-7 h-7 sm:w-8 sm:h-8 rounded-full flex items-center justify-center flex-shrink-0", | |
| message.role === "user" | |
| ? "bg-primary text-primary-foreground" | |
| : "bg-gradient-to-br from-blue-500 to-purple-600 text-white" | |
| )} | |
| > | |
| <Icon className="h-3.5 w-3.5 sm:h-4 sm:w-4" /> | |
| </div> | |
| <div | |
| className={cn( | |
| "flex flex-col", | |
| message.role === "user" ? "items-end" : "items-start" | |
| )} | |
| > | |
| <span className="font-medium text-xs sm:text-sm">{role}</span> | |
| <span className="text-xs text-muted-foreground"> | |
| {message.timestamp.toLocaleTimeString()} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <div | |
| className={cn( | |
| "prose prose-sm dark:prose-invert max-w-full text-sm sm:text-base", // max-w-full for mobile | |
| message.role === "user" ? "text-right" : "text-left" | |
| )} | |
| style={{ wordBreak: "break-word" }} // Extra safety for markdown content | |
| > | |
| <ReactMarkdown | |
| components={{ | |
| a: ({ href, children, ...props }) => ( | |
| <a | |
| href={href} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| {...props} | |
| > | |
| {children} | |
| </a> | |
| ), | |
| }} | |
| > | |
| {message.content} | |
| </ReactMarkdown> | |
| </div> | |
| {message.transcripts.length > 0 && ( | |
| <div className="flex flex-wrap mt-4 sm:mt-6 gap-2"> | |
| {message.transcripts.map((transcript) => ( | |
| <Button | |
| key={transcript.id} | |
| variant="secondary" | |
| size="sm" | |
| className="text-xs h-auto min-h-7 sm:min-h-8 bg-muted/50 hover:bg-muted border border-border/50 transition-all duration-200 hover:scale-105 py-2 max-w-full overflow-x-auto" | |
| onClick={() => handleTranscriptClick(transcript)} | |
| style={{ wordBreak: "break-word" }} | |
| > | |
| <FileText className="mr-1 sm:mr-1.5 w-3" /> | |
| <span className="text-start whitespace-normal break-words"> | |
| {transcript.filename} @ {transcript.timestamp} | |
| </span> | |
| </Button> | |
| ))} | |
| </div> | |
| )} | |
| {message.role === "assistant" && ( | |
| <div className="mt-4 sm:mt-6 flex flex-col sm:flex-row justify-start gap-2"> | |
| {/* <Button | |
| onClick={onEvaluate} | |
| variant="ghost" | |
| size="sm" | |
| className="h-8 text-xs hover:bg-accent/50 transition-all duration-200 justify-start sm:justify-center" | |
| > | |
| <Star className="h-3.5 w-3.5 mr-1.5" /> | |
| Rate Answer | |
| </Button> */} | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-8 text-xs hover:bg-accent/50 transition-all duration-200 justify-start sm:justify-center" | |
| onClick={handleCopy} | |
| > | |
| {copied ? ( | |
| <Check className="h-3.5 w-3.5 mr-1.5 text-green-500" /> | |
| ) : ( | |
| <Copy className="h-3.5 w-3.5 mr-1.5" /> | |
| )} | |
| {copied ? "Copied!" : "Copy Response"} | |
| </Button> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default MessageBlock; | |