char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aleksander Grygierui: Stores split refactor (#27240)521a64cd0

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