Enhance attachment handling in App.jsx
This commit is contained in:
166
src/App.jsx
166
src/App.jsx
@@ -226,39 +226,173 @@ function AssistantMessageContent({ content, streamOutput, sources }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageAttachmentStrip({ attachments, className = '', removable = false, onRemove }) {
|
const CHAT_IMAGE_EXTENSION_LIST = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tif', '.tiff', '.heic', '.avif']
|
||||||
|
const CHAT_FILE_EXTENSION_LIST = [
|
||||||
|
'.pdf', '.html', '.htm', '.txt', '.md', '.rst', '.epub',
|
||||||
|
'.mp3', '.wav', '.m4a', '.flac', '.ogg', '.opus', '.aac',
|
||||||
|
'.mp4', '.mkv', '.mov', '.webm', '.avi', '.ts',
|
||||||
|
]
|
||||||
|
const CHAT_IMAGE_EXTENSION_SET = new Set(CHAT_IMAGE_EXTENSION_LIST)
|
||||||
|
const CHAT_FILE_EXTENSION_SET = new Set(CHAT_FILE_EXTENSION_LIST)
|
||||||
|
const CHAT_AUDIO_EXTENSION_SET = new Set(['.mp3', '.wav', '.m4a', '.flac', '.ogg', '.opus', '.aac'])
|
||||||
|
const CHAT_VIDEO_EXTENSION_SET = new Set(['.mp4', '.mkv', '.mov', '.webm', '.avi', '.ts'])
|
||||||
|
const CHAT_TEXT_EXTENSION_SET = new Set(['.txt', '.md', '.rst'])
|
||||||
|
const CHAT_FILE_PICKER_FILTERS = [
|
||||||
|
{ name: 'Documents and Media', extensions: CHAT_FILE_EXTENSION_LIST.map(extension => extension.slice(1)) },
|
||||||
|
]
|
||||||
|
|
||||||
|
function getFileExtension(value) {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
const match = /(?:\.([A-Za-z0-9]+))$/.exec(text)
|
||||||
|
return match ? `.${match[1].toLowerCase()}` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileName(value, fallback = 'attachment') {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return fallback
|
||||||
|
const parts = text.split(/[\\/]/)
|
||||||
|
return parts[parts.length - 1] || fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessMimeTypeFromName(value) {
|
||||||
|
const extension = getFileExtension(value)
|
||||||
|
switch (extension) {
|
||||||
|
case '.pdf': return 'application/pdf'
|
||||||
|
case '.html':
|
||||||
|
case '.htm': return 'text/html'
|
||||||
|
case '.txt': return 'text/plain'
|
||||||
|
case '.md': return 'text/markdown'
|
||||||
|
case '.rst': return 'text/x-rst'
|
||||||
|
case '.epub': return 'application/epub+zip'
|
||||||
|
case '.mp3': return 'audio/mpeg'
|
||||||
|
case '.wav': return 'audio/wav'
|
||||||
|
case '.m4a': return 'audio/mp4'
|
||||||
|
case '.flac': return 'audio/flac'
|
||||||
|
case '.ogg': return 'audio/ogg'
|
||||||
|
case '.opus': return 'audio/opus'
|
||||||
|
case '.aac': return 'audio/aac'
|
||||||
|
case '.mp4': return 'video/mp4'
|
||||||
|
case '.mkv': return 'video/x-matroska'
|
||||||
|
case '.mov': return 'video/quicktime'
|
||||||
|
case '.webm': return 'video/webm'
|
||||||
|
case '.avi': return 'video/x-msvideo'
|
||||||
|
case '.ts': return 'video/mp2t'
|
||||||
|
default: return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentIsImage(attachment) {
|
||||||
|
return Boolean(attachment?.data_url) || String(attachment?.kind || '').toLowerCase() === 'image'
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentIsFile(attachment) {
|
||||||
|
return Boolean(attachment) && !attachmentIsImage(attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSupportedChatFilePath(value) {
|
||||||
|
return CHAT_FILE_EXTENSION_SET.has(getFileExtension(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSupportedChatImagePath(value) {
|
||||||
|
return CHAT_IMAGE_EXTENSION_SET.has(getFileExtension(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSupportedChatFile(file) {
|
||||||
|
if (!file || isImageFile(file)) return false
|
||||||
|
return isSupportedChatFilePath(file.name || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAttachmentDisplayName(attachment, fallback = 'attachment') {
|
||||||
|
return String(attachment?.name || '').trim() || getFileName(attachment?.source_path, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileAttachmentBadge(attachment) {
|
||||||
|
const mimeType = String(attachment?.mime_type || '').toLowerCase()
|
||||||
|
const extension = getFileExtension(attachment?.name || attachment?.source_path || '')
|
||||||
|
if (mimeType.startsWith('audio/') || CHAT_AUDIO_EXTENSION_SET.has(extension)) return 'AUDIO'
|
||||||
|
if (mimeType.startsWith('video/') || CHAT_VIDEO_EXTENSION_SET.has(extension)) return 'VIDEO'
|
||||||
|
if (CHAT_TEXT_EXTENSION_SET.has(extension)) return 'TEXT'
|
||||||
|
return 'DOC'
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildComposerAttachmentId(prefix = 'attachment') {
|
||||||
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildComposerFileAttachment({ sourcePath, name, mimeType, size }) {
|
||||||
|
const displayName = String(name || '').trim() || getFileName(sourcePath, 'file')
|
||||||
|
return {
|
||||||
|
id: buildComposerAttachmentId('file'),
|
||||||
|
kind: 'file',
|
||||||
|
name: displayName,
|
||||||
|
mime_type: String(mimeType || '').trim() || guessMimeTypeFromName(displayName || sourcePath),
|
||||||
|
source_path: sourcePath,
|
||||||
|
size: Number.isFinite(Number(size)) ? Number(size) : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function AttachmentStrip({ attachments, className = '', removable = false, onRemove }) {
|
||||||
if (!Array.isArray(attachments) || attachments.length === 0) {
|
if (!Array.isArray(attachments) || attachments.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`image-attachment-strip ${className}`.trim()}>
|
<div className={`attachment-strip ${className}`.trim()}>
|
||||||
{attachments.map((attachment, index) => {
|
{attachments.map((attachment, index) => {
|
||||||
const src = attachment?.data_url;
|
const isImage = attachmentIsImage(attachment)
|
||||||
if (!src) return null;
|
const keySeed = isImage
|
||||||
|
? attachment?.data_url?.length || attachment?.name || index
|
||||||
|
: attachment?.source_path || attachment?.name || index
|
||||||
|
const key = attachment.id || `${getAttachmentDisplayName(attachment, 'attachment')}-${index}-${keySeed}`
|
||||||
|
|
||||||
const key = attachment.id || `${attachment.name || 'image'}-${index}-${src.length}`;
|
if (isImage) {
|
||||||
|
const src = attachment?.data_url
|
||||||
|
if (!src) return null
|
||||||
|
return (
|
||||||
|
<div key={key} className="image-attachment-card">
|
||||||
|
{removable && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="attachment-remove"
|
||||||
|
onClick={() => onRemove?.(attachment.id)}
|
||||||
|
aria-label={`Remove ${getAttachmentDisplayName(attachment, 'image')}`}
|
||||||
|
title="Remove attachment"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
className="image-attachment-thumb"
|
||||||
|
src={src}
|
||||||
|
alt={getAttachmentDisplayName(attachment, `Attachment ${index + 1}`)}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = getAttachmentDisplayName(attachment, `Attachment ${index + 1}`)
|
||||||
return (
|
return (
|
||||||
<div key={key} className="image-attachment-card">
|
<div
|
||||||
|
key={key}
|
||||||
|
className="file-attachment-card"
|
||||||
|
title={attachment?.source_path || label}
|
||||||
|
>
|
||||||
{removable && (
|
{removable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="image-attachment-remove"
|
className="attachment-remove attachment-remove--file"
|
||||||
onClick={() => onRemove?.(attachment.id)}
|
onClick={() => onRemove?.(attachment.id)}
|
||||||
aria-label={`Remove ${attachment.name || 'image'}`}
|
aria-label={`Remove ${label}`}
|
||||||
title="Remove image"
|
title="Remove attachment"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<img
|
<span className="file-attachment-badge">{getFileAttachmentBadge(attachment)}</span>
|
||||||
className="image-attachment-thumb"
|
<span className="file-attachment-name">{label}</span>
|
||||||
src={src}
|
|
||||||
alt={attachment.name || `Attachment ${index + 1}`}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user