Spaces:
Running
Running
File size: 5,059 Bytes
deb090d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
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; |