1<script lang="ts">
  2	import { Button } from '$lib/components/ui/button';
  3	import * as Alert from '$lib/components/ui/alert';
  4	import { SyntaxHighlightedCode } from '$lib/components/app';
  5	import { FileText, Image, Music, FileIcon, Eye, Info } from '@lucide/svelte';
  6	import {
  7		isTextFile,
  8		isImageFile,
  9		isPdfFile,
 10		isAudioFile,
 11		getLanguageFromFilename
 12	} from '$lib/utils';
 13	import { convertPDFToImage } from '$lib/utils/browser-only';
 14	import { modelsStore } from '$lib/stores/models.svelte';
 15
 16	interface Props {
 17		// Either an uploaded file or a stored attachment
 18		uploadedFile?: ChatUploadedFile;
 19		attachment?: DatabaseMessageExtra;
 20		// For uploaded files
 21		preview?: string;
 22		name?: string;
 23		textContent?: string;
 24		// For checking vision modality
 25		activeModelId?: string;
 26	}
 27
 28	let { uploadedFile, attachment, preview, name, textContent, activeModelId }: Props = $props();
 29
 30	let hasVisionModality = $derived(
 31		activeModelId ? modelsStore.modelSupportsVision(activeModelId) : false
 32	);
 33
 34	let displayName = $derived(uploadedFile?.name || attachment?.name || name || 'Unknown File');
 35
 36	// Determine file type from uploaded file or attachment
 37	let isAudio = $derived(isAudioFile(attachment, uploadedFile));
 38	let isImage = $derived(isImageFile(attachment, uploadedFile));
 39	let isPdf = $derived(isPdfFile(attachment, uploadedFile));
 40	let isText = $derived(isTextFile(attachment, uploadedFile));
 41
 42	let displayPreview = $derived(
 43		uploadedFile?.preview ||
 44			(isImage && attachment && 'base64Url' in attachment ? attachment.base64Url : preview)
 45	);
 46
 47	let displayTextContent = $derived(
 48		uploadedFile?.textContent ||
 49			(attachment && 'content' in attachment ? attachment.content : textContent)
 50	);
 51
 52	let language = $derived(getLanguageFromFilename(displayName));
 53
 54	let IconComponent = $derived(() => {
 55		if (isImage) return Image;
 56		if (isText || isPdf) return FileText;
 57		if (isAudio) return Music;
 58
 59		return FileIcon;
 60	});
 61
 62	let pdfViewMode = $state<'text' | 'pages'>('pages');
 63
 64	let pdfImages = $state<string[]>([]);
 65
 66	let pdfImagesLoading = $state(false);
 67
 68	let pdfImagesError = $state<string | null>(null);
 69
 70	async function loadPdfImages() {
 71		if (!isPdf || pdfImages.length > 0 || pdfImagesLoading) return;
 72
 73		pdfImagesLoading = true;
 74		pdfImagesError = null;
 75
 76		try {
 77			let file: File | null = null;
 78
 79			if (uploadedFile?.file) {
 80				file = uploadedFile.file;
 81			} else if (isPdf && attachment) {
 82				// Check if we have pre-processed images
 83				if (
 84					'images' in attachment &&
 85					attachment.images &&
 86					Array.isArray(attachment.images) &&
 87					attachment.images.length > 0
 88				) {
 89					pdfImages = attachment.images;
 90					return;
 91				}
 92
 93				// Convert base64 back to File for processing
 94				if ('base64Data' in attachment && attachment.base64Data) {
 95					const base64Data = attachment.base64Data;
 96					const byteCharacters = atob(base64Data);
 97					const byteNumbers = new Array(byteCharacters.length);
 98					for (let i = 0; i < byteCharacters.length; i++) {
 99						byteNumbers[i] = byteCharacters.charCodeAt(i);
100					}
101					const byteArray = new Uint8Array(byteNumbers);
102					file = new File([byteArray], displayName, { type: 'application/pdf' });
103				}
104			}
105
106			if (file) {
107				pdfImages = await convertPDFToImage(file);
108			} else {
109				throw new Error('No PDF file available for conversion');
110			}
111		} catch (error) {
112			pdfImagesError = error instanceof Error ? error.message : 'Failed to load PDF images';
113		} finally {
114			pdfImagesLoading = false;
115		}
116	}
117
118	export function reset() {
119		pdfImages = [];
120		pdfImagesLoading = false;
121		pdfImagesError = null;
122		pdfViewMode = 'pages';
123	}
124
125	$effect(() => {
126		if (isPdf && pdfViewMode === 'pages') {
127			loadPdfImages();
128		}
129	});
130</script>
131
132<div class="space-y-4">
133	<div class="flex items-center justify-end gap-6">
134		{#if isPdf}
135			<div class="flex items-center gap-2">
136				<Button
137					variant={pdfViewMode === 'text' ? 'default' : 'outline'}
138					size="sm"
139					onclick={() => (pdfViewMode = 'text')}
140					disabled={pdfImagesLoading}
141				>
142					<FileText class="mr-1 h-4 w-4" />
143
144					Text
145				</Button>
146
147				<Button
148					variant={pdfViewMode === 'pages' ? 'default' : 'outline'}
149					size="sm"
150					onclick={() => {
151						pdfViewMode = 'pages';
152						loadPdfImages();
153					}}
154					disabled={pdfImagesLoading}
155				>
156					{#if pdfImagesLoading}
157						<div
158							class="mr-1 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
159						></div>
160					{:else}
161						<Eye class="mr-1 h-4 w-4" />
162					{/if}
163
164					Pages
165				</Button>
166			</div>
167		{/if}
168	</div>
169
170	<div class="flex-1 overflow-auto">
171		{#if isImage && displayPreview}
172			<div class="flex items-center justify-center">
173				<img
174					src={displayPreview}
175					alt={displayName}
176					class="max-h-full rounded-lg object-contain shadow-lg"
177				/>
178			</div>
179		{:else if isPdf && pdfViewMode === 'pages'}
180			{#if !hasVisionModality && activeModelId}
181				<Alert.Root class="mb-4">
182					<Info class="h-4 w-4" />
183					<Alert.Title>Preview only</Alert.Title>
184					<Alert.Description>
185						<span class="inline-flex">
186							The selected model does not support vision. Only the extracted
187							<!-- svelte-ignore a11y_click_events_have_key_events -->
188							<!-- svelte-ignore a11y_no_static_element_interactions -->
189							<span class="mx-1 cursor-pointer underline" onclick={() => (pdfViewMode = 'text')}>
190								text
191							</span>
192							will be sent to the model.
193						</span>
194					</Alert.Description>
195				</Alert.Root>
196			{/if}
197
198			{#if pdfImagesLoading}
199				<div class="flex items-center justify-center p-8">
200					<div class="text-center">
201						<div
202							class="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent"
203						></div>
204
205						<p class="text-muted-foreground">Converting PDF to images...</p>
206					</div>
207				</div>
208			{:else if pdfImagesError}
209				<div class="flex items-center justify-center p-8">
210					<div class="text-center">
211						<FileText class="mx-auto mb-4 h-16 w-16 text-muted-foreground" />
212
213						<p class="mb-4 text-muted-foreground">Failed to load PDF images</p>
214
215						<p class="text-sm text-muted-foreground">{pdfImagesError}</p>
216
217						<Button class="mt-4" onclick={() => (pdfViewMode = 'text')}>View as Text</Button>
218					</div>
219				</div>
220			{:else if pdfImages.length > 0}
221				<div class="max-h-[70vh] space-y-4 overflow-auto">
222					{#each pdfImages as image, index (image)}
223						<div class="text-center">
224							<p class="mb-2 text-sm text-muted-foreground">Page {index + 1}</p>
225
226							<img
227								src={image}
228								alt="PDF Page {index + 1}"
229								class="mx-auto max-w-full rounded-lg shadow-lg"
230							/>
231						</div>
232					{/each}
233				</div>
234			{:else}
235				<div class="flex items-center justify-center p-8">
236					<div class="text-center">
237						<FileText class="mx-auto mb-4 h-16 w-16 text-muted-foreground" />
238
239						<p class="mb-4 text-muted-foreground">No PDF pages available</p>
240					</div>
241				</div>
242			{/if}
243		{:else if (isText || (isPdf && pdfViewMode === 'text')) && displayTextContent}
244			<SyntaxHighlightedCode code={displayTextContent} {language} maxWidth="calc(69rem - 2rem)" />
245		{:else if isAudio}
246			<div class="flex items-center justify-center p-8">
247				<div class="w-full max-w-md text-center">
248					<Music class="mx-auto mb-4 h-16 w-16 text-muted-foreground" />
249
250					{#if uploadedFile?.preview}
251						<audio controls class="mb-4 w-full" src={uploadedFile.preview}>
252							Your browser does not support the audio element.
253						</audio>
254					{:else if isAudio && attachment && 'mimeType' in attachment && 'base64Data' in attachment}
255						<audio
256							controls
257							class="mb-4 w-full"
258							src={`data:${attachment.mimeType};base64,${attachment.base64Data}`}
259						>
260							Your browser does not support the audio element.
261						</audio>
262					{:else}
263						<p class="mb-4 text-muted-foreground">Audio preview not available</p>
264					{/if}
265
266					<p class="text-sm text-muted-foreground">
267						{displayName}
268					</p>
269				</div>
270			</div>
271		{:else}
272			<div class="flex items-center justify-center p-8">
273				<div class="text-center">
274					{#if IconComponent}
275						<IconComponent class="mx-auto mb-4 h-16 w-16 text-muted-foreground" />
276					{/if}
277
278					<p class="mb-4 text-muted-foreground">Preview not available for this file type</p>
279				</div>
280			</div>
281		{/if}
282	</div>
283</div>