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 ( -