Skip to content

Commit 4ba2e04

Browse files
committed
fix: Fixes chat attachment preview interactivity & displaying issues
Introduces a dialog for previewing chat attachments with support for images, text, audio, and PDFs. The dialog handles various file types, displaying them appropriately. For PDFs, it offers options to view them as text or as a series of images. It also addresses an issue where clicking on an attachment in the list would not open a preview.
1 parent 4cad2b6 commit 4ba2e04

File tree

3 files changed

+185
-21
lines changed

3 files changed

+185
-21
lines changed

tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentFilePreview.svelte

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import { Button } from '$lib/components/ui/button';
33
import { X } from '@lucide/svelte';
4+
import { stopPropagation } from 'svelte/legacy';
45
56
interface Props {
67
class?: string;
@@ -115,19 +116,20 @@
115116
</div>
116117
{/if}
117118
{:else}
118-
<div class="bg-muted border-border flex items-center gap-2 rounded-lg border p-2 {className}">
119+
<button class="bg-muted border-border flex gap-3 items-center gap-2 rounded-lg border p-3 {className}"
120+
onclick={onClick}>
119121
<div
120122
class="bg-primary/10 text-primary flex h-8 w-8 items-center justify-center rounded text-xs font-medium"
121123
>
122124
{getFileTypeLabel(type)}
123125
</div>
124126

125-
<div class="flex flex-col">
127+
<div class="flex flex-col gap-1">
126128
<span class="text-foreground max-w-36 truncate text-sm font-medium md:max-w-72"
127129
>{name}</span
128130
>
129131
{#if size}
130-
<span class="text-muted-foreground text-xs">{formatFileSize(size)}</span>
132+
<span class="text-left text-muted-foreground text-xs">{formatFileSize(size)}</span>
131133
{/if}
132134
</div>
133135

@@ -137,10 +139,13 @@
137139
variant="ghost"
138140
size="sm"
139141
class="h-6 w-6 p-0"
140-
onclick={() => onRemove?.(id)}
142+
onclick={(e) => {
143+
e.stopPropagation();
144+
onRemove?.(id);
145+
}}
141146
>
142147
<X class="h-3 w-3" />
143148
</Button>
144149
{/if}
145-
</div>
150+
</button>
146151
{/if}

tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentPreviewDialog.svelte

Lines changed: 169 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<script lang="ts">
22
import * as Dialog from '$lib/components/ui/dialog';
3-
import { FileText, Image, Music, FileIcon } from '@lucide/svelte';
3+
import { FileText, Image, Music, FileIcon, Eye } from '@lucide/svelte';
4+
import { convertPDFToImage } from '$lib/utils/pdf-processing';
5+
import { Button } from '$lib/components/ui/button';
46
57
interface Props {
68
open: boolean;
@@ -62,6 +64,12 @@
6264
let isPdf = $derived(displayType === 'application/pdf');
6365
let isAudio = $derived(displayType.startsWith('audio/') || displayType === 'audio');
6466
67+
// PDF preview state
68+
let pdfViewMode = $state<'text' | 'pages'>('pages'); // Default to pages view
69+
let pdfImages = $state<string[]>([]);
70+
let pdfImagesLoading = $state(false);
71+
let pdfImagesError = $state<string | null>(null);
72+
6573
let IconComponent = $derived(() => {
6674
if (isImage) return Image;
6775
if (isText || isPdf) return FileText;
@@ -78,12 +86,74 @@
7886
7987
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
8088
}
89+
90+
async function loadPdfImages() {
91+
if (!isPdf || pdfImages.length > 0 || pdfImagesLoading) return;
92+
93+
if (import.meta.env.DEV) console.log('Loading PDF images...', { isPdf, attachment, uploadedFile });
94+
95+
pdfImagesLoading = true;
96+
pdfImagesError = null;
97+
98+
try {
99+
let file: File | null = null;
100+
101+
if (uploadedFile?.file) {
102+
if (import.meta.env.DEV) console.log('Using uploaded file:', uploadedFile.file);
103+
file = uploadedFile.file;
104+
} else if (attachment?.type === 'pdfFile') {
105+
if (import.meta.env.DEV) console.log('Processing stored PDF attachment:', attachment);
106+
107+
// Check if we have pre-processed images
108+
if ((attachment as any).images && Array.isArray((attachment as any).images)) {
109+
if (import.meta.env.DEV) console.log('Using pre-processed PDF images:', (attachment as any).images.length);
110+
pdfImages = (attachment as any).images;
111+
return;
112+
}
113+
114+
// Convert base64 back to File for processing
115+
if ((attachment as any).base64Data) {
116+
const base64Data = (attachment as any).base64Data;
117+
const byteCharacters = atob(base64Data);
118+
const byteNumbers = new Array(byteCharacters.length);
119+
for (let i = 0; i < byteCharacters.length; i++) {
120+
byteNumbers[i] = byteCharacters.charCodeAt(i);
121+
}
122+
const byteArray = new Uint8Array(byteNumbers);
123+
file = new File([byteArray], displayName, { type: 'application/pdf' });
124+
if (import.meta.env.DEV) console.log('Created file from base64 data, size:', file.size);
125+
}
126+
}
127+
128+
if (file) {
129+
if (import.meta.env.DEV) console.log('Converting PDF to images...');
130+
const images = await convertPDFToImage(file);
131+
if (import.meta.env.DEV) console.log('PDF conversion successful, got', images.length, 'images');
132+
pdfImages = images;
133+
} else {
134+
throw new Error('No PDF file available for conversion');
135+
}
136+
} catch (error) {
137+
if (import.meta.env.DEV) console.error('Failed to convert PDF to images:', error);
138+
pdfImagesError = error instanceof Error ? error.message : 'Failed to load PDF images';
139+
// Don't automatically fallback to text view, let user choose
140+
} finally {
141+
pdfImagesLoading = false;
142+
}
143+
}
144+
145+
// Load PDF images when dialog opens and it's a PDF
146+
$effect(() => {
147+
if (open && isPdf && pdfViewMode === 'pages') {
148+
loadPdfImages();
149+
}
150+
});
81151
</script>
82152

83153
<Dialog.Root bind:open>
84-
<Dialog.Content class="grid max-h-[90vh] max-w-4xl overflow-hidden sm:w-auto sm:max-w-6xl">
154+
<Dialog.Content class="grid max-h-[90vh] !p-10 max-w-5xl overflow-hidden sm:w-auto sm:max-w-6xl">
85155
<Dialog.Header class="flex-shrink-0">
86-
<div class="flex items-center space-x-4">
156+
<div class="flex items-center justify-between">
87157
<div class="flex items-center gap-3">
88158
{#if IconComponent}
89159
<IconComponent class="text-muted-foreground h-5 w-5" />
@@ -101,32 +171,117 @@
101171
</div>
102172
</div>
103173
</div>
174+
175+
{#if isPdf}
176+
<div class="flex items-center gap-2">
177+
<Button
178+
variant={pdfViewMode === 'text' ? 'default' : 'outline'}
179+
size="sm"
180+
onclick={() => pdfViewMode = 'text'}
181+
disabled={pdfImagesLoading}
182+
>
183+
<FileText class="h-4 w-4 mr-1" />
184+
Text
185+
</Button>
186+
<Button
187+
variant={pdfViewMode === 'pages' ? 'default' : 'outline'}
188+
size="sm"
189+
onclick={() => { pdfViewMode = 'pages'; loadPdfImages(); }}
190+
disabled={pdfImagesLoading}
191+
>
192+
{#if pdfImagesLoading}
193+
<div class="h-4 w-4 mr-1 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
194+
{:else}
195+
<Eye class="h-4 w-4 mr-1" />
196+
{/if}
197+
Pages
198+
</Button>
199+
</div>
200+
{/if}
104201
</div>
105202
</Dialog.Header>
106203

107204
<div class="flex-1 overflow-auto">
108205
{#if isImage && displayPreview}
109-
<div class="flex items-center justify-center p-4">
206+
<div class="flex items-center justify-center">
110207
<img
111208
src={displayPreview}
112209
alt={displayName}
113210
class="max-h-full rounded-lg object-contain shadow-lg"
114211
/>
115212
</div>
116-
{:else if (isText || isPdf) && displayTextContent}
117-
<div class="p-4">
118-
<div
119-
class="bg-muted max-h-[60vh] overflow-auto whitespace-pre-wrap break-words rounded-lg p-4 font-mono text-sm"
120-
>
121-
{displayTextContent}
213+
{:else if isPdf && pdfViewMode === 'pages'}
214+
{#if pdfImagesLoading}
215+
<div class="flex items-center justify-center p-8">
216+
<div class="text-center">
217+
<div class="h-8 w-8 mx-auto mb-4 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
218+
<p class="text-muted-foreground">Converting PDF to images...</p>
219+
</div>
122220
</div>
221+
{:else if pdfImagesError}
222+
<div class="flex items-center justify-center p-8">
223+
<div class="text-center">
224+
<FileText class="text-muted-foreground mx-auto mb-4 h-16 w-16" />
225+
<p class="text-muted-foreground mb-4">Failed to load PDF images</p>
226+
<p class="text-muted-foreground text-sm">{pdfImagesError}</p>
227+
<Button class="mt-4" onclick={() => { pdfViewMode = 'text'; }}>View as Text</Button>
228+
</div>
229+
</div>
230+
{:else if pdfImages.length > 0}
231+
<div class="max-h-[70vh] overflow-auto space-y-4">
232+
{#each pdfImages as image, index}
233+
<div class="text-center">
234+
<p class="text-muted-foreground mb-2 text-sm">Page {index + 1}</p>
235+
<img
236+
src={image}
237+
alt="PDF Page {index + 1}"
238+
class="mx-auto max-w-full rounded-lg shadow-lg"
239+
/>
240+
</div>
241+
{/each}
242+
</div>
243+
{:else}
244+
<div class="flex items-center justify-center p-8">
245+
<div class="text-center">
246+
<FileText class="text-muted-foreground mx-auto mb-4 h-16 w-16" />
247+
<p class="text-muted-foreground mb-4">No PDF pages available</p>
248+
</div>
249+
</div>
250+
{/if}
251+
{:else if (isText || (isPdf && pdfViewMode === 'text')) && displayTextContent}
252+
<div
253+
class="bg-muted max-h-[60vh] overflow-auto whitespace-pre-wrap break-words rounded-lg p-4 font-mono text-sm"
254+
>
255+
{displayTextContent}
123256
</div>
124-
{:else if isAudio && attachment?.type === 'audioFile'}
257+
{:else if isAudio}
125258
<div class="flex items-center justify-center p-8">
126-
<div class="text-center">
259+
<div class="w-full max-w-md text-center">
127260
<Music class="text-muted-foreground mx-auto mb-4 h-16 w-16" />
128-
129-
<p class="text-muted-foreground mb-4">Audio file preview not available</p>
261+
262+
{#if attachment?.type === 'audioFile'}
263+
<audio
264+
controls
265+
class="w-full mb-4"
266+
src="data:{(attachment as any).mimeType};base64,{(attachment as any).base64Data}"
267+
>
268+
Your browser does not support the audio element.
269+
</audio>
270+
{:else if uploadedFile?.preview}
271+
<audio
272+
controls
273+
class="w-full mb-4"
274+
src={uploadedFile.preview}
275+
>
276+
Your browser does not support the audio element.
277+
</audio>
278+
{:else}
279+
<p class="text-muted-foreground mb-4">Audio preview not available</p>
280+
{/if}
281+
282+
<p class="text-muted-foreground text-sm">
283+
{displayName}
284+
</p>
130285
</div>
131286
</div>
132287
{:else}

tools/server/webui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList.svelte

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@
9191
name: attachment.name,
9292
preview: attachment.base64Url,
9393
type: 'image',
94-
isImage: true
94+
isImage: true,
95+
attachment,
96+
attachmentIndex: index
9597
});
9698
} else if (attachment.type === 'textFile') {
9799
items.push({
@@ -108,7 +110,9 @@
108110
id: `attachment-${index}`,
109111
name: attachment.name,
110112
type: attachment.mimeType || 'audio',
111-
isImage: false
113+
isImage: false,
114+
attachment,
115+
attachmentIndex: index
112116
});
113117
} else if (attachment.type === 'pdfFile') {
114118
items.push({

0 commit comments

Comments
 (0)