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}