1<script lang="ts">
2 import { RemoveButton } from '$lib/components/app';
3 import { formatFileSize, getFileTypeLabel, getPreviewText, isTextFile } from '$lib/utils';
4 import { AttachmentType } from '$lib/enums';
5
6 interface Props {
7 class?: string;
8 id: string;
9 onClick?: (event?: MouseEvent) => void;
10 onRemove?: (id: string) => void;
11 name: string;
12 readonly?: boolean;
13 size?: number;
14 textContent?: string;
15 // Either uploaded file or stored attachment
16 uploadedFile?: ChatUploadedFile;
17 attachment?: DatabaseMessageExtra;
18 }
19
20 let {
21 class: className = '',
22 id,
23 onClick,
24 onRemove,
25 name,
26 readonly = false,
27 size,
28 textContent,
29 uploadedFile,
30 attachment
31 }: Props = $props();
32
33 let isText = $derived(isTextFile(attachment, uploadedFile));
34
35 let fileTypeLabel = $derived.by(() => {
36 if (uploadedFile?.type) {
37 return getFileTypeLabel(uploadedFile.type);
38 }
39
40 if (attachment) {
41 if ('mimeType' in attachment && attachment.mimeType) {
42 return getFileTypeLabel(attachment.mimeType);
43 }
44
45 if (attachment.type) {
46 return getFileTypeLabel(attachment.type);
47 }
48 }
49
50 return getFileTypeLabel(name);
51 });
52
53 let pdfProcessingMode = $derived.by(() => {
54 if (attachment?.type === AttachmentType.PDF) {
55 const pdfAttachment = attachment as DatabaseMessageExtraPdfFile;
56
57 return pdfAttachment.processedAsImages ? 'Sent as Image' : 'Sent as Text';
58 }
59 return null;
60 });
61</script>
62
63{#if isText}
64 {#if readonly}
65 <!-- Readonly mode (ChatMessage) -->
66 <button
67 class="cursor-pointer rounded-lg border border-border bg-muted p-3 transition-shadow hover:shadow-md {className} w-full max-w-2xl"
68 onclick={onClick}
69 aria-label={`Preview ${name}`}
70 type="button"
71 >
72 <div class="flex items-start gap-3">
73 <div class="flex min-w-0 flex-1 flex-col items-start text-left">
74 <span class="w-full truncate text-sm font-medium text-foreground">{name}</span>
75
76 {#if size}
77 <span class="text-xs text-muted-foreground">{formatFileSize(size)}</span>
78 {/if}
79
80 {#if textContent}
81 <div class="relative mt-2 w-full">
82 <div
83 class="overflow-hidden font-mono text-xs leading-relaxed break-words whitespace-pre-wrap text-muted-foreground"
84 >
85 {getPreviewText(textContent)}
86 </div>
87
88 {#if textContent.length > 150}
89 <div
90 class="pointer-events-none absolute right-0 bottom-0 left-0 h-6 bg-gradient-to-t from-muted to-transparent"
91 ></div>
92 {/if}
93 </div>
94 {/if}
95 </div>
96 </div>
97 </button>
98 {:else}
99 <!-- Non-readonly mode (ChatForm) -->
100 <button
101 class="group relative rounded-lg border border-border bg-muted p-3 {className} {textContent
102 ? 'max-h-24 max-w-72'
103 : 'max-w-36'} cursor-pointer text-left"
104 onclick={onClick}
105 >
106 <div class="absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100">
107 <RemoveButton {id} {onRemove} />
108 </div>
109
110 <div class="pr-8">
111 <span class="mb-3 block truncate text-sm font-medium text-foreground">{name}</span>
112
113 {#if textContent}
114 <div class="relative">
115 <div
116 class="overflow-hidden font-mono text-xs leading-relaxed break-words whitespace-pre-wrap text-muted-foreground"
117 style="max-height: 3rem; line-height: 1.2em;"
118 >
119 {getPreviewText(textContent)}
120 </div>
121
122 {#if textContent.length > 150}
123 <div
124 class="pointer-events-none absolute right-0 bottom-0 left-0 h-4 bg-gradient-to-t from-muted to-transparent"
125 ></div>
126 {/if}
127 </div>
128 {/if}
129 </div>
130 </button>
131 {/if}
132{:else}
133 <button
134 class="group flex items-center gap-3 rounded-lg border border-border bg-muted p-3 {className} relative"
135 onclick={onClick}
136 >
137 <div
138 class="flex h-8 w-8 items-center justify-center rounded bg-primary/10 text-xs font-medium text-primary"
139 >
140 {fileTypeLabel}
141 </div>
142
143 <div class="flex flex-col gap-0.5">
144 <span
145 class="max-w-24 truncate text-sm font-medium text-foreground {readonly
146 ? ''
147 : 'group-hover:pr-6'} md:max-w-32"
148 >
149 {name}
150 </span>
151
152 {#if pdfProcessingMode}
153 <span class="text-left text-xs text-muted-foreground">{pdfProcessingMode}</span>
154 {:else if size}
155 <span class="text-left text-xs text-muted-foreground">{formatFileSize(size)}</span>
156 {/if}
157 </div>
158
159 {#if !readonly}
160 <div class="absolute top-2 right-2 opacity-0 transition-opacity group-hover:opacity-100">
161 <RemoveButton {id} {onRemove} />
162 </div>
163 {/if}
164 </button>
165{/if}