import { memo, useEffect, useMemo, useRef } from 'react' import { motion } from 'framer-motion' import { Copy, FileText, Pencil, RefreshCw, Sparkles, Volume2, VolumeX } from 'lucide-react' import { renderMarkdown } from '../../utils/markdown' import TypingIndicator from './TypingIndicator' function addCodeCopyButtons(container) { container.querySelectorAll('pre').forEach((pre) => { if (pre.dataset.enhanced === 'true') return pre.dataset.enhanced = 'true' const wrapper = document.createElement('div') wrapper.className = 'code-shell' const toolbar = document.createElement('div') toolbar.className = 'code-toolbar' const badge = document.createElement('span') badge.className = 'code-badge' badge.textContent = pre.querySelector('code')?.className?.replace('hljs language-', '') || 'code' const button = document.createElement('button') button.className = 'code-copy-button' button.type = 'button' button.textContent = 'Copy' button.addEventListener('click', async () => { const code = pre.querySelector('code')?.textContent || pre.textContent || '' await navigator.clipboard.writeText(code) button.textContent = 'Copied' window.setTimeout(() => { button.textContent = 'Copy' }, 1400) }) toolbar.appendChild(badge) toolbar.appendChild(button) pre.parentNode.insertBefore(wrapper, pre) wrapper.appendChild(toolbar) wrapper.appendChild(pre) }) } function MessageBubble({ message, user, onCopyMessage, onRegenerate, onEdit, onSpeak, onPreviewFile, speaking, }) { const isUser = message.role === 'user' const contentRef = useRef(null) const html = useMemo( () => (isUser ? '' : renderMarkdown(message.content || '')), [isUser, message.content], ) useEffect(() => { if (!isUser && contentRef.current) { addCodeCopyButtons(contentRef.current) } }, [html, isUser]) return ( {!isUser ? ( ) : null} {message.attachments?.length ? ( {message.attachments.map((attachment, index) => ( onPreviewFile(attachment)} className={`inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium ${ isUser ? 'border-white/20 bg-white/10 text-white/90' : 'border-border bg-background text-muted-foreground hover:text-foreground' }`} > {attachment.filename} ))} ) : null} {isUser ? ( {message.content} ) : message.streaming && !message.content ? ( ) : ( )} {!isUser && message.streaming && message.content ? : null} {!isUser ? ( <> onCopyMessage(message)} /> onSpeak(message)} /> onRegenerate(message.id)} /> > ) : ( onEdit(message)} /> )} {message.model_used ? ( <> {(message.provider || 'assistant').replaceAll('-', ' ')} ยท {message.model_used} {`${(message.provider || 'assistant').replaceAll('-', ' ')} / ${message.model_used}`} > ) : null} {isUser ? ( {user?.avatar_url ? ( ) : ( (user?.username || 'You').slice(0, 1).toUpperCase() )} ) : null} ) } function MessageAction({ icon: Icon, label, onClick }) { return ( {label} ) } export default memo(MessageBubble)
{message.content}