199 lines
5.3 KiB
JavaScript
199 lines
5.3 KiB
JavaScript
/**
|
|
* @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<object>}
|
|
*/
|
|
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);
|
|
}
|