ChatAttachmentThumbnailFile.svelte 4.4 KB

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