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
| import React, { useState, useEffect } from 'react'; import Markdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
const MessageBubble = ({ message, isCurrentUser }) => { const [isVisible, setIsVisible] = useState(false); const [copied, setCopied] = useState(false);
useEffect(() => { const timer = setTimeout(() => setIsVisible(true), 50); return () => clearTimeout(timer); }, []);
const handleCopy = async () => { try { await navigator.clipboard.writeText(message.content); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy text: ', err); } };
const formatTimestamp = (timestamp) => { return new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); };
const CodeBlock = ({ node, inline, className, children, ...props }) => { const match = /language-(\w+)/.exec(className || '');
if (inline) { return <code className={className} {...props}>{children}</code>; }
return ( <SyntaxHighlighter style={atomDark} language={match ? match[1] : 'text'} PreTag="div" className="code-block" {...props} > {String(children).replace(/\n$/, '')} </SyntaxHighlighter> ); };
return ( <div className={`message-bubble ${isCurrentUser ? 'user-message' : 'ai-message'} ${isVisible ? 'visible' : ''}`}> <div className="message-content"> <div className="message-text"> <Markdown remarkPlugins={[remarkGfm]} components={{ code: CodeBlock, p: ({ node, ...props }) => <p {...props} />, li: ({ node, ...props }) => <li {...props} />, strong: ({ node, ...props }) => <strong {...props} />, em: ({ node, ...props }) => <em {...props} />, h1: ({ node, ...props }) => <h1 className="message-h1" {...props} />, h2: ({ node, ...props }) => <h2 className="message-h2" {...props} />, h3: ({ node, ...props }) => <h3 className="message-h3" {...props} />, pre: ({ node, ...props }) => <pre className="message-pre" {...props} />, a: ({ node, ...props }) => <a className="message-link" target="_blank" rel="noopener noreferrer" {...props} />, ol: ({ node, ...props }) => <ol className="message-ol" {...props} />, ul: ({ node, ...props }) => <ul className="message-ul" {...props} /> }} > {message.content} </Markdown> </div>
<div className="message-actions"> <button className="copy-button" onClick={handleCopy} title="Copy message" > {copied ? '✓ Copied!' : 'Copy'} </button>
{isCurrentUser && ( <span className="message-status"> {message.status === 'sending' && 'Sending...'} {message.status === 'received' && '✓ Sent'} {message.status === 'error' && '✗ Failed'} </span> )} </div>
<div className="message-footer"> <span className="message-timestamp"> {formatTimestamp(message.timestamp)} </span> {!isCurrentUser && ( <span className="message-source"> {message.model || 'AI Assistant'} </span> )} </div> </div> </div> ); };
export default MessageBubble;
|