]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/blob
69b1724fb22c05a74a22b9bdb648e29195bdd4fe
[pkg/ggml/sources/llama.cpp] /
1 <script lang="ts">
2 import { Eye, FileText, Info } from '@lucide/svelte';
3 import { SyntaxHighlightedCode } from '$lib/components/app';
4 import * as Alert from '$lib/components/ui/alert';
5 import { Button } from '$lib/components/ui/button';
6 import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
7 import { PdfViewMode } from '$lib/enums';
8 import type { ChatAttachmentDisplayItem } from '$lib/types';
9 import { getLanguageFromFilename } from '$lib/utils';
10 import { convertPDFToImage } from '$lib/utils/browser-only';
11
12 interface Props {
13 currentItem: ChatAttachmentDisplayItem | null;
14 displayName: string;
15 displayTextContent: string | undefined;
16 hasVisionModality: boolean;
17 activeModelId?: string;
18 }
19
20 let { activeModelId, currentItem, displayName, displayTextContent, hasVisionModality }: Props =
21 $props();
22
23 let pdfViewMode = $state<PdfViewMode>(PdfViewMode.PAGES);
24 let pdfImages = $state<string[]>([]);
25 let pdfImagesLoading = $state(false);
26 let pdfImagesError = $state<string | null>(null);
27
28 let language = $derived(getLanguageFromFilename(displayName));
29
30 async function loadPdfImages() {
31 if (pdfImages.length > 0 || pdfImagesLoading || !currentItem) return;
32
33 pdfImagesLoading = true;
34 pdfImagesError = null;
35
36 try {
37 let file: File | null = null;
38
39 if (currentItem.uploadedFile?.file) {
40 file = currentItem.uploadedFile.file;
41 } else if (currentItem.attachment) {
42 // Check if we have pre-processed images
43 if (
44 'images' in currentItem.attachment &&
45 currentItem.attachment.images &&
46 Array.isArray(currentItem.attachment.images) &&
47 currentItem.attachment.images.length > 0
48 ) {
49 pdfImages = currentItem.attachment.images;
50
51 return;
52 }
53
54 // Convert base64 back to File for processing
55 if ('base64Data' in currentItem.attachment && currentItem.attachment.base64Data) {
56 const base64Data = currentItem.attachment.base64Data;
57 const byteCharacters = atob(base64Data);
58 const byteNumbers = new Array(byteCharacters.length);
59
60 for (let i = 0; i < byteCharacters.length; i++) {
61 byteNumbers[i] = byteCharacters.charCodeAt(i);
62 }
63 const byteArray = new Uint8Array(byteNumbers);
64
65 file = new File([byteArray], displayName, { type: 'application/pdf' });
66 }
67 }
68
69 if (file) {
70 pdfImages = await convertPDFToImage(file);
71 } else {
72 throw new Error('No PDF file available for conversion');
73 }
74 } catch (error) {
75 pdfImagesError = error instanceof Error ? error.message : 'Failed to load PDF images';
76 } finally {
77 pdfImagesLoading = false;
78 }
79 }
80
81 $effect(() => {
82 if (pdfViewMode === PdfViewMode.PAGES) {
83 loadPdfImages();
84 }
85 });
86 </script>
87
88 <div class="mb-4 flex items-center justify-end gap-2">
89 <Button
90 variant={pdfViewMode === PdfViewMode.TEXT ? 'default' : 'outline'}
91 size="sm"
92 onclick={() => (pdfViewMode = PdfViewMode.TEXT)}
93 disabled={pdfImagesLoading}
94 >
95 <FileText class="mr-1 {ICON_CLASS_DEFAULT}" />
96 Text
97 </Button>
98
99 <Button
100 variant={pdfViewMode === PdfViewMode.PAGES ? 'default' : 'outline'}
101 size="sm"
102 onclick={() => (pdfViewMode = PdfViewMode.PAGES)}
103 disabled={pdfImagesLoading}
104 >
105 {#if pdfImagesLoading}
106 <div
107 class="mr-1 {ICON_CLASS_DEFAULT} animate-spin rounded-full border-2 border-current border-t-transparent"
108 ></div>
109 {:else}
110 <Eye class="mr-1 {ICON_CLASS_DEFAULT}" />
111 {/if}
112 Pages
113 </Button>
114 </div>
115
116 {#if !hasVisionModality && activeModelId && currentItem}
117 <Alert.Root class="mb-4 max-w-4xl">
118 <Info class={ICON_CLASS_DEFAULT} />
119 <Alert.Title>Preview only</Alert.Title>
120 <Alert.Description>
121 <span class="inline-flex">
122 The selected model does not support vision. Only the extracted
123 <!-- svelte-ignore a11y_click_events_have_key_events -->
124 <!-- svelte-ignore a11y_no_static_element_interactions -->
125 <span
126 class="mx-1 cursor-pointer underline"
127 onclick={() => (pdfViewMode = PdfViewMode.TEXT)}
128 >
129 text
130 </span>
131 will be sent to the model.
132 </span>
133 </Alert.Description>
134 </Alert.Root>
135 {/if}
136
137 {#if pdfImagesLoading}
138 <div class="flex flex-1 items-center justify-center p-8">
139 <div class="text-center">
140 <div
141 class="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-white border-t-transparent"
142 ></div>
143 <p class="text-white/70">Converting PDF to images...</p>
144 </div>
145 </div>
146 {:else if pdfImagesError}
147 <div class="flex flex-1 items-center justify-center p-8">
148 <div class="text-center">
149 <FileText class="mx-auto mb-4 h-16 w-16 text-white/50" />
150 <p class="mb-4 text-white/70">Failed to load PDF images</p>
151 <p class="text-sm text-white/50">{pdfImagesError}</p>
152 </div>
153 </div>
154 {:else if pdfImages.length > 0}
155 {#each pdfImages as image, index (image)}
156 <p class="mb-2 text-sm text-white/50">Page {index + 1}</p>
157 <img src={image} alt="PDF Page {index + 1}" class="mx-auto max-w-[85vw] rounded-lg shadow-lg" />
158 <div class="h-4"></div>
159 {/each}
160 {:else}
161 <div class="flex flex-1 items-center justify-center p-8">
162 <div class="text-center">
163 <FileText class="mx-auto mb-4 h-16 w-16 text-white/50" />
164 <p class="text-white/70">No PDF pages available</p>
165 </div>
166 </div>
167 {/if}
168
169 {#if pdfViewMode === PdfViewMode.TEXT && displayTextContent}
170 <div class="px-4 pb-4">
171 <SyntaxHighlightedCode
172 class="max-w-4xl"
173 code={displayTextContent}
174 {language}
175 maxHeight="none"
176 />
177 </div>
178 {/if}