From cb8c364b6ff2d43881dd58f19ac9a4a47d2fa6bb Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Fri, 17 Apr 2026 13:00:51 +0200 Subject: [PATCH] Enhance attachment handling in App.jsx --- src/App.jsx | 166 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 150 insertions(+), 16 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index fb11c5d..15da67c 100644 --- a/src/App.jsx +++ b/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) { return null; } return ( -
+
{attachments.map((attachment, index) => { - const src = attachment?.data_url; - if (!src) return null; + const isImage = attachmentIsImage(attachment) + 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 ( +
+ {removable && ( + + )} + {getAttachmentDisplayName(attachment, +
+ ) + } + + const label = getAttachmentDisplayName(attachment, `Attachment ${index + 1}`) return ( -
+
{removable && ( )} - {attachment.name + {getFileAttachmentBadge(attachment)} + {label}
- ); + ) })}
);