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