Spaces:
Sleeping
Sleeping
File size: 7,766 Bytes
2db8ee1 d8c0ee6 2db8ee1 d8c0ee6 2db8ee1 d8c0ee6 2db8ee1 d8c0ee6 2db8ee1 d8c0ee6 2db8ee1 d8c0ee6 2db8ee1 d8c0ee6 2db8ee1 d8c0ee6 2db8ee1 d8c0ee6 2db8ee1 d8c0ee6 2db8ee1 d8c0ee6 2db8ee1 | 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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | import { useState } from 'react'
import ReactMarkdown from 'react-markdown'
import './MessageBubble.css'
function MessageBubble({ message, isLast }) {
const [showSources, setShowSources] = useState(false)
const [selectedImage, setSelectedImage] = useState(null)
const isUser = message.role === 'user'
const isError = message.role === 'error'
const isAssistant = message.role === 'assistant'
return (
<div className={`message-row ${message.role} ${isLast ? 'is-last' : ''}`}>
<div className="message-inner">
{/* Role icon */}
<div className="role-icon-wrapper">
{isAssistant && (
<div className="role-icon assistant-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path
d="M12 2L2 7l10 5 10-5-10-5z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 17l10 5 10-5M2 12l10 5 10-5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
)}
{isUser && (
<div className="role-icon user-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
</div>
)}
{isError && (
<div className="role-icon error-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" />
<path d="M12 8v4M12 16h.01" />
</svg>
</div>
)}
</div>
{/* Message body */}
<div className="message-body">
{/* Message Text — markdown for assistant, plain for user */}
<div className="message-text">
{isAssistant ? (
<ReactMarkdown>{message.content}</ReactMarkdown>
) : (
message.content
)}
</div>
{/* Images */}
{isAssistant && message.images && message.images.length > 0 && (
<div className="message-images">
{message.images.map((img, i) => (
<button
key={i}
className="image-thumb"
onClick={() => setSelectedImage(img)}
>
<img
src={img.url}
alt={`Reference image from page ${img.page}`}
loading="lazy"
/>
<span className="image-page">p.{img.page}</span>
</button>
))}
</div>
)}
{/* Sources */}
{isAssistant && message.sources && message.sources.length > 0 && (
<div className="message-sources">
<button
className="sources-toggle"
onClick={() => setShowSources(!showSources)}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
<path d="M14 2v6h6" />
</svg>
<span>{message.sources.length} source{message.sources.length !== 1 ? 's' : ''}</span>
<svg
className={`chevron ${showSources ? 'open' : ''}`}
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M6 9l6 6 6-6" />
</svg>
</button>
{showSources && (
<div className="sources-list">
{message.sources.map((source, i) => (
<div key={i} className="source-item">
<div className="source-header">
<span className="source-num">{i + 1}</span>
<span className="source-file">{source.source || 'Document'}</span>
{source.score && (
<span className="source-score">
{(source.score * 100).toFixed(0)}% match
</span>
)}
</div>
<p className="source-content">{source.content}</p>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
{/* Image Modal */}
{selectedImage && (
<div className="image-modal" onClick={() => setSelectedImage(null)}>
<div className="image-modal-content" onClick={e => e.stopPropagation()}>
<button
className="modal-close btn btn-ghost btn-icon"
onClick={() => setSelectedImage(null)}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
<img src={selectedImage.url} alt="Full size reference" />
<p className="image-caption">Page {selectedImage.page}</p>
</div>
</div>
)}
</div>
)
}
export default MessageBubble
|