OwnGPT.v2 / client /src /components /MessageBubble.jsx
parthib07's picture
Upload 200 files
48b8720 verified
import { useEffect, useRef } from 'react'
import { renderMarkdown } from '../utils/markdown'
const PROVIDER_ICONS = {
groq: '⚑', google: '🌐', mistral: 'πŸ’«', huggingface: 'πŸ€—', nvidia: 'β—†', openrouter: 'πŸ”€',
}
function addCopyButtons(el) {
el.querySelectorAll('.hljs-pre').forEach(pre => {
if (pre.parentElement?.classList.contains('code-wrap')) return
const lang = pre.dataset.lang || 'code'
const wrap = document.createElement('div'); wrap.className = 'code-wrap'
const header = document.createElement('div'); header.className = 'code-header'
const langLabel = document.createElement('span'); langLabel.className = 'code-lang'; langLabel.textContent = lang
const copyBtn = document.createElement('button'); copyBtn.className = 'copy-btn'
copyBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy`
copyBtn.addEventListener('click', async () => {
const code = pre.querySelector('code')?.textContent || pre.textContent
await navigator.clipboard.writeText(code).catch(() => {})
copyBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg> Copied!`
copyBtn.classList.add('copied')
setTimeout(() => {
copyBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg> Copy`
copyBtn.classList.remove('copied')
}, 2000)
})
header.appendChild(langLabel); header.appendChild(copyBtn)
pre.parentNode.insertBefore(wrap, pre); wrap.appendChild(header); wrap.appendChild(pre)
})
}
export default function MessageBubble({ message, index = 0, user }) {
const { role, content, model_used, provider, attachments } = message
const bubbleRef = useRef(null)
const delay = Math.min(index * 60, 500) + 'ms'
const html = role === 'assistant' ? renderMarkdown(content || '') : null
useEffect(() => {
if (role === 'assistant' && bubbleRef.current) {
addCopyButtons(bubbleRef.current)
}
}, [content, role])
if (role === 'user') {
return (
<div className="msg-row user" style={{ animationDelay: delay }}>
<div className="user-bubble">
{attachments?.length > 0 && (
<div className="msg-atts">
{attachments.map((a, i) => (
<span key={i} className="msg-att-chip">
πŸ“Ž {a.filename}
</span>
))}
</div>
)}
<span style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{content}</span>
</div>
<div
className="msg-indicator user"
style={user?.avatar_url ? { backgroundImage: `url(${user.avatar_url})`, backgroundSize: 'cover', backgroundPosition: 'center', color: 'transparent', border: 'none' } : {}}
>
{!user?.avatar_url && 'You'}
</div>
</div>
)
}
return (
<div className="msg-row" style={{ animationDelay: delay }}>
<div className="msg-indicator ai">✦</div>
<div className="msg-card">
<div className="msg-actions">
<button
className="msg-action-btn"
onClick={() => navigator.clipboard.writeText(content).catch(() => {})}
title="Copy raw text"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
{(!content) ? (
<div className="typing-wave" style={{ padding: '10px 0' }}>
<div className="typing-dot" />
<div className="typing-dot" />
<div className="typing-dot" />
</div>
) : (
<div
ref={bubbleRef}
className="msg-markdown"
dangerouslySetInnerHTML={{ __html: html }}
/>
)}
{model_used && (
<div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 6 }}>
<span className="msg-badge">
{PROVIDER_ICONS[provider] || 'πŸ€–'} {provider} Β· {model_used}
</span>
</div>
)}
</div>
</div>
)
}