Spaces:
Running
Running
import React from 'react'; | |
import { motion } from 'framer-motion'; | |
import ReactMarkdown from 'react-markdown'; | |
import remarkGfm from 'remark-gfm'; | |
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; | |
import { tomorrow, prism } from 'react-syntax-highlighter/dist/esm/styles/prism'; | |
import { UserIcon, AcademicCapIcon } from '@heroicons/react/24/solid'; | |
const MessageBubble = ({ message, darkMode, isLast }) => { | |
const isUser = message.role === 'user'; | |
const messageVariants = { | |
hidden: { opacity: 0, y: 20 }, | |
visible: { | |
opacity: 1, | |
y: 0, | |
transition: { | |
duration: 0.3, | |
ease: "easeOut" | |
} | |
} | |
}; | |
const formatTime = (timestamp) => { | |
return new Date(timestamp).toLocaleTimeString([], { | |
hour: '2-digit', | |
minute: '2-digit' | |
}); | |
}; | |
return ( | |
<motion.div | |
variants={messageVariants} | |
initial="hidden" | |
animate="visible" | |
className={`flex gap-4 mb-6 ${isUser ? 'justify-end' : 'justify-start'}`} | |
> | |
{!isUser && ( | |
<div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ | |
darkMode ? 'bg-primary-600' : 'bg-primary-500' | |
}`}> | |
<AcademicCapIcon className="w-5 h-5 text-white" /> | |
</div> | |
)} | |
<div className={`max-w-[80%] ${isUser ? 'order-first' : ''}`}> | |
<div className={`rounded-2xl px-4 py-3 ${ | |
isUser | |
? darkMode | |
? 'bg-primary-600 text-white' | |
: 'bg-primary-500 text-white' | |
: darkMode | |
? 'bg-gray-800 border border-gray-700' | |
: 'bg-white border border-gray-200 shadow-sm' | |
}`}> | |
{isUser ? ( | |
<p className="whitespace-pre-wrap">{message.content}</p> | |
) : ( | |
<div className="message-content"> | |
<ReactMarkdown | |
remarkPlugins={[remarkGfm]} | |
components={{ | |
code({ node, inline, className, children, ...props }) { | |
const match = /language-(\w+)/.exec(className || ''); | |
return !inline && match ? ( | |
<SyntaxHighlighter | |
style={darkMode ? tomorrow : prism} | |
language={match[1]} | |
PreTag="div" | |
{...props} | |
> | |
{String(children).replace(/\n$/, '')} | |
</SyntaxHighlighter> | |
) : ( | |
<code className={className} {...props}> | |
{children} | |
</code> | |
); | |
}, | |
p: ({ children }) => <p className="mb-2 last:mb-0">{children}</p>, | |
ul: ({ children }) => <ul className="list-disc list-inside mb-2">{children}</ul>, | |
ol: ({ children }) => <ol className="list-decimal list-inside mb-2">{children}</ol>, | |
li: ({ children }) => <li className="mb-1">{children}</li>, | |
h1: ({ children }) => <h1 className="text-xl font-bold mb-2">{children}</h1>, | |
h2: ({ children }) => <h2 className="text-lg font-semibold mb-2">{children}</h2>, | |
h3: ({ children }) => <h3 className="text-md font-medium mb-2">{children}</h3>, | |
blockquote: ({ children }) => ( | |
<blockquote className={`border-l-4 pl-4 italic my-2 ${ | |
darkMode ? 'border-gray-600 text-gray-300' : 'border-gray-300 text-gray-600' | |
}`}> | |
{children} | |
</blockquote> | |
), | |
}} | |
> | |
{message.content} | |
</ReactMarkdown> | |
</div> | |
)} | |
</div> | |
{/* Timestamp and Sources */} | |
<div className={`text-xs mt-2 ${ | |
darkMode ? 'text-gray-500' : 'text-gray-400' | |
} ${isUser ? 'text-right' : 'text-left'}`}> | |
<span>{formatTime(message.timestamp)}</span> | |
{!isUser && message.sources && message.sources.length > 0 && ( | |
<div className="mt-2"> | |
<span className="font-medium">Sources: </span> | |
{message.sources.map((source, index) => ( | |
<span key={index} className={`inline-block mr-2 px-2 py-1 rounded text-xs ${ | |
darkMode | |
? 'bg-gray-700 text-gray-300' | |
: 'bg-gray-100 text-gray-600' | |
}`}> | |
{source} | |
</span> | |
))} | |
</div> | |
)} | |
</div> | |
</div> | |
{isUser && ( | |
<div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ | |
darkMode ? 'bg-gray-700' : 'bg-gray-300' | |
}`}> | |
<UserIcon className={`w-5 h-5 ${ | |
darkMode ? 'text-gray-300' : 'text-gray-600' | |
}`} /> | |
</div> | |
)} | |
</motion.div> | |
); | |
}; | |
export default MessageBubble; |