1import { convertPDFToImage, convertPDFToText } from './pdf-processing';
  2import { isSvgMimeType, svgBase64UrlToPngDataURL } from './svg-to-png';
  3import { isWebpMimeType, webpBase64UrlToPngDataURL } from './webp-to-png';
  4import { FileTypeCategory, AttachmentType } from '$lib/enums';
  5import { config, settingsStore } from '$lib/stores/settings.svelte';
  6import { modelsStore } from '$lib/stores/models.svelte';
  7import { getFileTypeCategory } from '$lib/utils';
  8import { readFileAsText, isLikelyTextFile } from './text-files';
  9import { toast } from 'svelte-sonner';
 10
 11function readFileAsBase64(file: File): Promise<string> {
 12	return new Promise((resolve, reject) => {
 13		const reader = new FileReader();
 14
 15		reader.onload = () => {
 16			// Extract base64 data without the data URL prefix
 17			const dataUrl = reader.result as string;
 18			const base64 = dataUrl.split(',')[1];
 19			resolve(base64);
 20		};
 21
 22		reader.onerror = () => reject(reader.error);
 23
 24		reader.readAsDataURL(file);
 25	});
 26}
 27
 28export interface FileProcessingResult {
 29	extras: DatabaseMessageExtra[];
 30	emptyFiles: string[];
 31}
 32
 33export async function parseFilesToMessageExtras(
 34	files: ChatUploadedFile[],
 35	activeModelId?: string
 36): Promise<FileProcessingResult> {
 37	const extras: DatabaseMessageExtra[] = [];
 38	const emptyFiles: string[] = [];
 39
 40	for (const file of files) {
 41		if (getFileTypeCategory(file.type) === FileTypeCategory.IMAGE) {
 42			if (file.preview) {
 43				let base64Url = file.preview;
 44
 45				if (isSvgMimeType(file.type)) {
 46					try {
 47						base64Url = await svgBase64UrlToPngDataURL(base64Url);
 48					} catch (error) {
 49						console.error('Failed to convert SVG to PNG for database storage:', error);
 50					}
 51				} else if (isWebpMimeType(file.type)) {
 52					try {
 53						base64Url = await webpBase64UrlToPngDataURL(base64Url);
 54					} catch (error) {
 55						console.error('Failed to convert WebP to PNG for database storage:', error);
 56					}
 57				}
 58
 59				extras.push({
 60					type: AttachmentType.IMAGE,
 61					name: file.name,
 62					base64Url
 63				});
 64			}
 65		} else if (getFileTypeCategory(file.type) === FileTypeCategory.AUDIO) {
 66			// Process audio files (MP3 and WAV)
 67			try {
 68				const base64Data = await readFileAsBase64(file.file);
 69
 70				extras.push({
 71					type: AttachmentType.AUDIO,
 72					name: file.name,
 73					base64Data: base64Data,
 74					mimeType: file.type
 75				});
 76			} catch (error) {
 77				console.error(`Failed to process audio file ${file.name}:`, error);
 78			}
 79		} else if (getFileTypeCategory(file.type) === FileTypeCategory.PDF) {
 80			try {
 81				// Always get base64 data for preview functionality
 82				const base64Data = await readFileAsBase64(file.file);
 83				const currentConfig = config();
 84				// Use per-model vision check for router mode
 85				const hasVisionSupport = activeModelId
 86					? modelsStore.modelSupportsVision(activeModelId)
 87					: false;
 88
 89				// Force PDF-to-text for non-vision models
 90				let shouldProcessAsImages = Boolean(currentConfig.pdfAsImage) && hasVisionSupport;
 91
 92				// If user had pdfAsImage enabled but model doesn't support vision, update setting and notify
 93				if (currentConfig.pdfAsImage && !hasVisionSupport) {
 94					console.log('Non-vision model detected: forcing PDF-to-text mode and updating settings');
 95
 96					// Update the setting in localStorage
 97					settingsStore.updateConfig('pdfAsImage', false);
 98
 99					// Show toast notification to user
100					toast.warning(
101						'PDF setting changed: Non-vision model detected, PDFs will be processed as text instead of images.',
102						{
103							duration: 5000
104						}
105					);
106
107					shouldProcessAsImages = false;
108				}
109
110				if (shouldProcessAsImages) {
111					// Process PDF as images (only for vision models)
112					try {
113						const images = await convertPDFToImage(file.file);
114
115						// Show success toast for PDF image processing
116						toast.success(
117							`PDF "${file.name}" processed as ${images.length} images for vision model.`,
118							{
119								duration: 3000
120							}
121						);
122
123						extras.push({
124							type: AttachmentType.PDF,
125							name: file.name,
126							content: `PDF file with ${images.length} pages`,
127							images: images,
128							processedAsImages: true,
129							base64Data: base64Data
130						});
131					} catch (imageError) {
132						console.warn(
133							`Failed to process PDF ${file.name} as images, falling back to text:`,
134							imageError
135						);
136
137						// Fallback to text processing
138						const content = await convertPDFToText(file.file);
139
140						extras.push({
141							type: AttachmentType.PDF,
142							name: file.name,
143							content: content,
144							processedAsImages: false,
145							base64Data: base64Data
146						});
147					}
148				} else {
149					// Process PDF as text (default or forced for non-vision models)
150					const content = await convertPDFToText(file.file);
151
152					// Show success toast for PDF text processing
153					toast.success(`PDF "${file.name}" processed as text content.`, {
154						duration: 3000
155					});
156
157					extras.push({
158						type: AttachmentType.PDF,
159						name: file.name,
160						content: content,
161						processedAsImages: false,
162						base64Data: base64Data
163					});
164				}
165			} catch (error) {
166				console.error(`Failed to process PDF file ${file.name}:`, error);
167			}
168		} else {
169			try {
170				const content = await readFileAsText(file.file);
171
172				// Check if file is empty
173				if (content.trim() === '') {
174					console.warn(`File ${file.name} is empty and will be skipped`);
175					emptyFiles.push(file.name);
176				} else if (isLikelyTextFile(content)) {
177					extras.push({
178						type: AttachmentType.TEXT,
179						name: file.name,
180						content: content
181					});
182				} else {
183					console.warn(`File ${file.name} appears to be binary and will be skipped`);
184				}
185			} catch (error) {
186				console.error(`Failed to read file ${file.name}:`, error);
187			}
188		}
189	}
190
191	return { extras, emptyFiles };
192}