“vinit5112”
Upgrade UI
6f1f94e
raw
history blame
13.5 kB
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
import remarkGfm from 'remark-gfm';
import { ClipboardIcon, CheckIcon } from '@heroicons/react/24/outline';
const MessageBubble = ({ message, darkMode, isLast = false }) => {
const [copied, setCopied] = useState(false);
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy text: ', err);
}
};
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: false
});
};
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'} mb-4 md:mb-6 px-2 md:px-4`}
>
<div className={`max-w-[85%] md:max-w-[80%] lg:max-w-[70%] ${
message.role === 'user' ? 'order-2' : 'order-1'
}`}>
{/* Avatar and Timestamp - Mobile optimized */}
<div className={`flex items-center mb-2 ${
message.role === 'user' ? 'justify-end' : 'justify-start'
}`}>
{message.role === 'assistant' && (
<div className={`w-6 h-6 md:w-8 md:h-8 rounded-full flex items-center justify-center mr-2 md:mr-3 flex-shrink-0 ${
darkMode
? 'bg-gradient-to-br from-primary-600 to-purple-600'
: 'bg-gradient-to-br from-primary-500 to-purple-500'
}`}>
<svg className="w-3 h-3 md:w-4 md:h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</div>
)}
<span className={`text-xs md:text-sm font-medium ${
darkMode ? 'text-gray-400' : 'text-gray-500'
} ${message.role === 'user' ? 'order-2' : 'order-1'}`}>
{message.role === 'user' ? 'You' : 'CA Assistant'}
</span>
<span className={`text-xs ${
darkMode ? 'text-gray-500' : 'text-gray-400'
} ml-2 ${message.role === 'user' ? 'order-1 mr-2 ml-0' : 'order-2'}`}>
{formatTimestamp(message.timestamp)}
</span>
{message.role === 'user' && (
<div className={`w-6 h-6 md:w-8 md:h-8 rounded-full flex items-center justify-center ml-2 md:ml-3 flex-shrink-0 ${
darkMode
? 'bg-gradient-to-br from-blue-600 to-blue-700'
: 'bg-gradient-to-br from-blue-500 to-blue-600'
}`}>
<svg className="w-3 h-3 md:w-4 md:h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
)}
</div>
{/* Message Content - Mobile optimized */}
<div className={`relative group ${
message.role === 'user' ? 'text-right' : 'text-left'
}`}>
<div className={`inline-block p-3 md:p-4 rounded-2xl md:rounded-2xl relative shadow-md hover:shadow-lg transition-all duration-200 ${
message.role === 'user'
? darkMode
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white'
: 'bg-gradient-to-br from-blue-500 to-blue-600 text-white'
: darkMode
? 'bg-gradient-to-br from-gray-800 to-gray-850 border border-gray-700/50 text-gray-100'
: 'bg-gradient-to-br from-white to-gray-50 border border-gray-200/50 text-gray-900'
} max-w-full`}>
{/* Message Text - Mobile optimized */}
<div className={`prose prose-sm md:prose-base max-w-none ${
message.role === 'user'
? 'prose-invert'
: darkMode
? 'prose-invert prose-gray'
: 'prose-gray'
} ${message.role === 'user' ? 'text-left' : ''}`}>
{message.role === 'user' ? (
<p className="mb-0 text-sm md:text-base leading-relaxed break-words">{message.content}</p>
) : (
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
// Mobile-optimized code blocks
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<div className="relative my-3 md:my-4 rounded-lg md:rounded-xl overflow-hidden">
<div className={`flex items-center justify-between px-3 md:px-4 py-2 md:py-3 text-xs md:text-sm ${
darkMode ? 'bg-gray-900 text-gray-300' : 'bg-gray-800 text-gray-200'
}`}>
<span className="font-medium">{match[1]}</span>
<button
onClick={() => copyToClipboard(String(children).replace(/\n$/, ''))}
className={`flex items-center space-x-1 md:space-x-2 px-2 md:px-3 py-1 md:py-1.5 rounded-md transition-colors touch-manipulation ${
darkMode
? 'hover:bg-gray-800 active:bg-gray-700'
: 'hover:bg-gray-700 active:bg-gray-600'
}`}
title="Copy code"
>
{copied ? (
<CheckIcon className="w-3 h-3 md:w-4 md:h-4" />
) : (
<ClipboardIcon className="w-3 h-3 md:w-4 md:h-4" />
)}
<span className="text-xs hidden md:inline">
{copied ? 'Copied!' : 'Copy'}
</span>
</button>
</div>
<SyntaxHighlighter
style={darkMode ? oneDark : oneLight}
language={match[1]}
PreTag="div"
className="!m-0 text-xs md:text-sm"
customStyle={{
fontSize: '12px',
lineHeight: '1.4',
padding: '12px 16px',
}}
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
</div>
) : (
<code
className={`px-1.5 md:px-2 py-0.5 md:py-1 rounded text-xs md:text-sm font-mono ${
darkMode
? 'bg-gray-700 text-gray-200'
: 'bg-gray-200 text-gray-800'
}`}
{...props}
>
{children}
</code>
);
},
// Mobile-optimized paragraphs
p: ({ children }) => (
<p className="mb-3 md:mb-4 last:mb-0 text-sm md:text-base leading-relaxed break-words">
{children}
</p>
),
// Mobile-optimized lists
ul: ({ children }) => (
<ul className="mb-3 md:mb-4 ml-4 md:ml-6 space-y-1 md:space-y-2 text-sm md:text-base">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="mb-3 md:mb-4 ml-4 md:ml-6 space-y-1 md:space-y-2 text-sm md:text-base">
{children}
</ol>
),
li: ({ children }) => (
<li className="leading-relaxed break-words">
{children}
</li>
),
// Mobile-optimized headings
h1: ({ children }) => (
<h1 className="text-lg md:text-xl font-bold mb-2 md:mb-3 mt-4 md:mt-6 first:mt-0 break-words">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-base md:text-lg font-bold mb-2 md:mb-3 mt-3 md:mt-4 first:mt-0 break-words">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-sm md:text-base font-bold mb-1 md:mb-2 mt-2 md:mt-3 first:mt-0 break-words">
{children}
</h3>
),
// Mobile-optimized blockquotes
blockquote: ({ children }) => (
<blockquote className={`border-l-3 md:border-l-4 pl-3 md:pl-4 my-3 md:my-4 italic text-sm md:text-base ${
darkMode ? 'border-gray-600 text-gray-300' : 'border-gray-400 text-gray-600'
}`}>
{children}
</blockquote>
),
// Mobile-optimized tables
table: ({ children }) => (
<div className="overflow-x-auto my-3 md:my-4 -mx-1">
<table className={`min-w-full text-xs md:text-sm border-collapse ${
darkMode ? 'border-gray-600' : 'border-gray-300'
}`}>
{children}
</table>
</div>
),
th: ({ children }) => (
<th className={`border px-2 md:px-3 py-1 md:py-2 font-medium text-left ${
darkMode
? 'border-gray-600 bg-gray-700/50'
: 'border-gray-300 bg-gray-100'
}`}>
{children}
</th>
),
td: ({ children }) => (
<td className={`border px-2 md:px-3 py-1 md:py-2 ${
darkMode ? 'border-gray-600' : 'border-gray-300'
}`}>
{children}
</td>
),
}}
>
{message.content || '*Thinking...*'}
</ReactMarkdown>
)}
</div>
{/* Copy Button for Assistant Messages - Mobile optimized */}
{message.role === 'assistant' && message.content && (
<button
onClick={() => copyToClipboard(message.content)}
className={`absolute top-2 md:top-3 right-2 md:right-3 opacity-0 group-hover:opacity-100 transition-all duration-200 p-1.5 md:p-2 rounded-lg touch-manipulation ${
darkMode
? 'hover:bg-gray-700/70 active:bg-gray-600/70 text-gray-400 hover:text-gray-200'
: 'hover:bg-gray-200/70 active:bg-gray-300/70 text-gray-500 hover:text-gray-700'
} backdrop-blur-sm`}
title="Copy message"
>
{copied ? (
<CheckIcon className="w-3 h-3 md:w-4 md:h-4" />
) : (
<ClipboardIcon className="w-3 h-3 md:w-4 md:h-4" />
)}
</button>
)}
</div>
{/* Message tail/pointer - Mobile optimized */}
<div className={`absolute top-3 md:top-4 w-0 h-0 ${
message.role === 'user'
? 'right-0 border-l-8 md:border-l-10 border-t-8 md:border-t-10 border-transparent'
: 'left-0 border-r-8 md:border-r-10 border-t-8 md:border-t-10 border-transparent'
} ${
message.role === 'user'
? darkMode
? 'border-t-blue-600'
: 'border-t-blue-500'
: darkMode
? 'border-t-gray-800'
: 'border-t-white'
}`} />
</div>
</div>
</motion.div>
);
};
export default MessageBubble;