This commit is contained in:
seb
2026-10-06 11:40:02 +02:00
parent 03608017f3
commit 4769a87c54
6 changed files with 1197 additions and 20 deletions

View File

@@ -0,0 +1,198 @@
/**
* @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);
}