213 lines
5.5 KiB
Svelte
213 lines
5.5 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
ChatAttachmentsPreviewCurrentItem,
|
|
ChatAttachmentsPreviewFileInfo,
|
|
ChatAttachmentsPreviewNavButtons,
|
|
ChatAttachmentsPreviewThumbnailStrip
|
|
} from '$lib/components/app';
|
|
import { modelsStore } from '$lib/stores/models.svelte';
|
|
import {
|
|
createBase64DataUrl,
|
|
formatFileSize,
|
|
getAttachmentDisplayItems,
|
|
getLanguageFromFilename,
|
|
isAudioFile,
|
|
isVideoFile,
|
|
isImageFile,
|
|
isMcpPrompt,
|
|
isMcpResource,
|
|
isPdfFile,
|
|
isTextFile
|
|
} from '$lib/utils';
|
|
|
|
interface PreviewItem {
|
|
id: string;
|
|
name: string;
|
|
size?: number;
|
|
preview?: string;
|
|
uploadedFile?: ChatUploadedFile;
|
|
attachment?: DatabaseMessageExtra;
|
|
textContent?: string;
|
|
isImage: boolean;
|
|
isAudio: boolean;
|
|
isVideo: boolean;
|
|
}
|
|
|
|
interface Props {
|
|
uploadedFiles?: ChatUploadedFile[];
|
|
attachments?: DatabaseMessageExtra[];
|
|
activeModelId?: string;
|
|
class?: string;
|
|
previewFocusIndex?: number;
|
|
}
|
|
|
|
let {
|
|
uploadedFiles = [],
|
|
attachments = [],
|
|
activeModelId,
|
|
class: className = '',
|
|
previewFocusIndex = 0
|
|
}: Props = $props();
|
|
|
|
let allItems = $derived(
|
|
getAttachmentDisplayItems({ uploadedFiles, attachments })
|
|
.filter((item) => !isMcpPrompt(item) && !isMcpResource(item))
|
|
.map(
|
|
(item): PreviewItem => ({
|
|
...item,
|
|
isImage: isImageFile(item.attachment, item.uploadedFile),
|
|
isAudio: isAudioFile(item.attachment, item.uploadedFile),
|
|
isVideo: isVideoFile(item.attachment, item.uploadedFile)
|
|
})
|
|
)
|
|
);
|
|
|
|
let currentIndex = $state(0);
|
|
|
|
$effect(() => {
|
|
if (previewFocusIndex >= 0 && previewFocusIndex < allItems.length) {
|
|
currentIndex = previewFocusIndex;
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
const handler = (e: Event) => {
|
|
const delta = (e as CustomEvent).detail;
|
|
|
|
if (delta < 0) {
|
|
currentIndex = currentIndex > 0 ? currentIndex - 1 : allItems.length - 1;
|
|
} else {
|
|
currentIndex = currentIndex < allItems.length - 1 ? currentIndex + 1 : 0;
|
|
}
|
|
};
|
|
|
|
document.addEventListener('chat-attachments-nav', handler);
|
|
|
|
return () => document.removeEventListener('chat-attachments-nav', handler);
|
|
});
|
|
|
|
$effect(() => {
|
|
const index = currentIndex;
|
|
setTimeout(() => {
|
|
const thumbnail = document.querySelector(`[data-thumbnail-index="${index}"]`);
|
|
|
|
thumbnail?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
|
}, 0);
|
|
});
|
|
|
|
let currentItem = $derived(allItems[currentIndex] ?? null);
|
|
let displayName = $derived(
|
|
currentItem?.name ||
|
|
currentItem?.uploadedFile?.name ||
|
|
currentItem?.attachment?.name ||
|
|
'Unknown File'
|
|
);
|
|
let isAudio = $derived(
|
|
currentItem ? isAudioFile(currentItem.attachment, currentItem.uploadedFile) : false
|
|
);
|
|
let isVideo = $derived(
|
|
currentItem ? isVideoFile(currentItem.attachment, currentItem.uploadedFile) : false
|
|
);
|
|
let isImage = $derived(
|
|
currentItem ? isImageFile(currentItem.attachment, currentItem.uploadedFile) : false
|
|
);
|
|
let isPdf = $derived(
|
|
currentItem ? isPdfFile(currentItem.attachment, currentItem.uploadedFile) : false
|
|
);
|
|
let isText = $derived(
|
|
currentItem ? isTextFile(currentItem.attachment, currentItem.uploadedFile) : false
|
|
);
|
|
|
|
let displayPreview = $derived(
|
|
currentItem?.uploadedFile?.preview ||
|
|
(isImage && currentItem?.attachment && 'base64Url' in currentItem.attachment
|
|
? currentItem.attachment.base64Url
|
|
: currentItem?.preview)
|
|
);
|
|
|
|
let displayTextContent = $derived(
|
|
currentItem?.uploadedFile?.textContent ||
|
|
(currentItem?.attachment && 'content' in currentItem.attachment
|
|
? currentItem.attachment.content
|
|
: currentItem?.textContent)
|
|
);
|
|
|
|
let language = $derived(getLanguageFromFilename(displayName));
|
|
|
|
let fileSize = $derived(currentItem?.size ? formatFileSize(currentItem.size) : '');
|
|
|
|
let hasVisionModality = $derived(
|
|
currentItem && activeModelId ? modelsStore.modelSupportsVision(activeModelId) : false
|
|
);
|
|
|
|
let audioSrc = $derived(
|
|
isAudio && currentItem
|
|
? (currentItem.uploadedFile?.preview ??
|
|
(currentItem.attachment &&
|
|
'mimeType' in currentItem.attachment &&
|
|
'base64Data' in currentItem.attachment
|
|
? createBase64DataUrl(
|
|
currentItem.attachment.mimeType,
|
|
currentItem.attachment.base64Data
|
|
)
|
|
: null))
|
|
: null
|
|
);
|
|
|
|
let videoSrc = $derived(
|
|
isVideo && currentItem
|
|
? (currentItem.uploadedFile?.preview ??
|
|
(currentItem.attachment &&
|
|
'mimeType' in currentItem.attachment &&
|
|
'base64Data' in currentItem.attachment
|
|
? createBase64DataUrl(
|
|
currentItem.attachment.mimeType,
|
|
currentItem.attachment.base64Data
|
|
)
|
|
: null))
|
|
: null
|
|
);
|
|
|
|
export function prev() {
|
|
currentIndex = currentIndex > 0 ? currentIndex - 1 : allItems.length - 1;
|
|
}
|
|
|
|
export function next() {
|
|
currentIndex = currentIndex < allItems.length - 1 ? currentIndex + 1 : 0;
|
|
}
|
|
|
|
function onNavigate(index: number) {
|
|
currentIndex = index;
|
|
}
|
|
</script>
|
|
|
|
<div class="{className} flex flex-col text-white">
|
|
<div class="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden">
|
|
<ChatAttachmentsPreviewNavButtons onPrev={prev} onNext={next} show={allItems.length > 1} />
|
|
|
|
<div class="flex h-full w-full flex-col items-center justify-start overflow-auto py-4">
|
|
{#if currentItem}
|
|
<ChatAttachmentsPreviewFileInfo {displayName} {fileSize} />
|
|
|
|
<ChatAttachmentsPreviewCurrentItem
|
|
{currentItem}
|
|
{isImage}
|
|
{isAudio}
|
|
{isVideo}
|
|
{isPdf}
|
|
{isText}
|
|
{displayPreview}
|
|
{displayTextContent}
|
|
{audioSrc}
|
|
{videoSrc}
|
|
{language}
|
|
{hasVisionModality}
|
|
{activeModelId}
|
|
/>
|
|
{/if}
|
|
|
|
<ChatAttachmentsPreviewThumbnailStrip items={allItems} {currentIndex} {onNavigate} />
|
|
</div>
|
|
</div>
|
|
</div>
|