2 import { X } from '@lucide/svelte';
10 import { ActionIcon } from '$lib/components/app';
11 import { AttachmentType } from '$lib/enums';
14 attachment?: DatabaseMessageExtra;
17 onclick?: (event: MouseEvent) => void;
18 onRemove?: (id: string) => void;
23 // Either uploaded file or stored attachment
24 uploadedFile?: ChatUploadedFile;
29 class: className = '',
40 let isPdf = $derived(isPdfFile(attachment, uploadedFile));
41 let isPdfWithContent = $derived(isPdf && !!textContent);
43 let isText = $derived(isTextFile(attachment, uploadedFile));
44 let isTextWithContent = $derived(isText && !!textContent);
46 let fileTypeLabel = $derived.by(() => {
47 if (uploadedFile?.type) {
48 return getFileTypeLabel(uploadedFile.type);
52 if ('mimeType' in attachment && attachment.mimeType) {
53 return getFileTypeLabel(attachment.mimeType);
56 if (attachment.type) {
57 return getFileTypeLabel(attachment.type);
61 return getFileTypeLabel(name);
64 let pdfProcessingMode = $derived.by(() => {
65 if (attachment?.type === AttachmentType.PDF) {
66 const pdfAttachment = attachment as DatabaseMessageExtraPdfFile;
68 return pdfAttachment.processedAsImages ? 'Sent as Image' : 'Sent as Text';
75 {#snippet textPreview(content: string)}
76 <div class="relative">
78 class="font-mono text-xs leading-relaxed break-words whitespace-pre-wrap text-muted-foreground {!readonly
79 ? 'max-h-3rem line-height-1.2'
82 {getPreviewText(content)}
85 {#if content.length > 150}
87 class="pointer-events-none absolute right-0 bottom-0 left-0 h-4 bg-gradient-to-t from-muted to-transparent {readonly
95 {#snippet removeButton()}
96 <div class="absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100">
97 <ActionIcon icon={X} tooltip="Remove" stopPropagationOnClick onclick={() => onRemove?.(id)} />
101 {#snippet fileIcon()}
103 class="flex h-8 w-8 items-center justify-center rounded bg-primary/10 text-xs font-medium text-primary"
109 {#snippet info(text: string | undefined)}
111 <span class="text-xs text-muted-foreground">{text}</span>
115 {#if isTextWithContent || isPdfWithContent}
117 aria-label={readonly ? `Preview ${name}` : undefined}
118 class="rounded-lg border border-border bg-muted p-3 {className} cursor-pointer {readonly
119 ? 'w-full max-w-2xl transition-shadow hover:shadow-md'
120 : `group relative text-left ${textContent ? 'max-h-24 max-w-72' : 'max-w-36'}`} overflow-hidden"
125 {@render removeButton()}
128 <div class={[!readonly && 'pr-8', 'overflow-hidden']}>
130 <div class="flex items-start gap-3">
131 <div class="flex min-w-0 flex-1 flex-col items-start text-left">
132 <span class="w-full truncate text-sm font-medium text-foreground">{name}</span>
134 {@render info(pdfProcessingMode || (size ? formatFileSize(size) : undefined))}
137 {@render textPreview(textContent)}
142 <span class="mb-3 block truncate text-sm font-medium text-foreground">{name}</span>
145 {@render textPreview(textContent)}
152 class="group flex items-center gap-3 rounded-lg border border-border bg-muted p-3 {className} relative"
158 <div class="flex flex-col items-start gap-0.5">
160 class="max-w-24 truncate text-sm font-medium text-foreground {readonly
162 : 'group-hover:pr-6'} md:max-w-32"
167 {@render info(pdfProcessingMode || (size ? formatFileSize(size) : undefined))}
171 {@render removeButton()}