/** * @file chatAttachments.js * @description Helpers for handling file attachments in the AI chat window. * Supports: images, PDFs, CSV, plain text, XML, and HTML. */ export const SUPPORTED_CATEGORIES = { IMAGE: 'image', PDF: 'pdf', CSV: 'csv', TEXT: 'text', XML: 'xml', HTML: 'html', }; export const ACCEPT_STRING = [ 'image/*', 'application/pdf', '.pdf', 'text/csv', '.csv', 'text/plain', '.txt', '.md', '.log', 'text/xml', 'application/xml', '.xml', 'text/html', '.html', '.htm', ].join(','); export const MAX_FILE_SIZE = 25 * 1024 * 1024; // 25 MB /** * Determine the attachment category based on MIME type or file extension. * @param {File|{ name: string, type?: string }} file * @returns {'image'|'pdf'|'csv'|'text'|'xml'|'html'|'unknown'} */ export function categorizeFile(file) { const name = (file?.name || '').toLowerCase(); const type = (file?.type || '').toLowerCase(); // Images if (type.startsWith('image/') || /\.(png|jpe?g|webp|gif|svg|avif|bmp|ico)$/.test(name)) { return SUPPORTED_CATEGORIES.IMAGE; } // PDFs if (type === 'application/pdf' || name.endsWith('.pdf')) { return SUPPORTED_CATEGORIES.PDF; } // CSV if ( type === 'text/csv' || type === 'application/csv' || type === 'text/comma-separated-values' || name.endsWith('.csv') ) { return SUPPORTED_CATEGORIES.CSV; } // XML if (type === 'text/xml' || type === 'application/xml' || name.endsWith('.xml')) { return SUPPORTED_CATEGORIES.XML; } // HTML if (type === 'text/html' || name.endsWith('.html') || name.endsWith('.htm')) { return SUPPORTED_CATEGORIES.HTML; } // Plain Text / Markdown / Log if ( type.startsWith('text/') || type === 'application/json' || /\.(txt|md|log|json|ini|env|yaml|yml)$/.test(name) ) { return SUPPORTED_CATEGORIES.TEXT; } return 'unknown'; } /** * Human-readable format of file size in B, KB, or MB. * @param {number} bytes * @returns {string} */ export function formatFileSize(bytes) { if (!bytes || Number.isNaN(bytes) || bytes <= 0) return '0 B'; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } /** * Read a browser File object and convert it to a structured attachment object. * @param {File} file * @returns {Promise} */ export async function readFileAttachment(file) { const category = categorizeFile(file); if (category === 'unknown') { throw new Error(`Das Dateiformat von "${file.name}" wird nicht unterstützt. Erlaubt sind Bilder, PDFs, CSV, Text, XML und HTML.`); } if (file.size > MAX_FILE_SIZE) { throw new Error(`Die Datei "${file.name}" ist mit ${formatFileSize(file.size)} zu groß (maximal 25 MB).`); } const id = `att-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; // For text-based formats, read as text and dataUrl const isTextBased = category === SUPPORTED_CATEGORIES.CSV || category === SUPPORTED_CATEGORIES.TEXT || category === SUPPORTED_CATEGORIES.XML || category === SUPPORTED_CATEGORIES.HTML; const dataUrl = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(new Error(`Fehler beim Lesen der Datei "${file.name}"`)); reader.readAsDataURL(file); }); let textContent = ''; let previewSnippet = ''; if (isTextBased) { try { textContent = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = () => reject(new Error(`Fehler beim Lesen von "${file.name}" als Text`)); reader.readAsText(file); }); // First 25 lines or 2000 chars as preview previewSnippet = textContent.slice(0, 2000); } catch { // Non-fatal if text reading fails } } return { id, name: file.name, size: file.size, mimeType: file.type || getMimeTypeFallback(category, file.name), category, dataUrl, textContent: textContent || undefined, previewSnippet: previewSnippet || undefined, uploadedAt: new Date().toISOString(), }; } /** * Fallback MIME type based on extension/category. */ function getMimeTypeFallback(category, filename) { const ext = (filename.split('.').pop() || '').toLowerCase(); switch (category) { case SUPPORTED_CATEGORIES.IMAGE: if (ext === 'png') return 'image/png'; if (ext === 'webp') return 'image/webp'; if (ext === 'svg') return 'image/svg+xml'; if (ext === 'gif') return 'image/gif'; return 'image/jpeg'; case SUPPORTED_CATEGORIES.PDF: return 'application/pdf'; case SUPPORTED_CATEGORIES.CSV: return 'text/csv'; case SUPPORTED_CATEGORIES.XML: return 'application/xml'; case SUPPORTED_CATEGORIES.HTML: return 'text/html'; default: return 'text/plain'; } } /** * Trigger download of an attachment in the browser. * @param {object} attachment */ export function downloadAttachment(attachment) { if (!attachment) return; const link = document.createElement('a'); link.href = attachment.dataUrl || (attachment.url ? attachment.url : ''); link.download = attachment.name || 'anhang'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }