diff --git a/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte b/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte
index b761444871..7bb83f382b 100644
--- a/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte
+++ b/frontend/src/lib/components/ModelTable/EvidenceFilePreview.svelte
@@ -1,8 +1,9 @@
-{#snippet displayPreview()}
+{#snippet displayPreview(att: Attachment)}
- {#if attachment.type.startsWith('image')}
+ {#if att.type.startsWith('image')}

- {:else if attachment.type === 'application/pdf'}
+ {:else if att.type === 'application/pdf'}
{#if !display}
{/if}
@@ -89,18 +169,20 @@
{/snippet}
-{#if cell}
- {#if attachment}
- {#if attachment.type.startsWith('image') || attachment.type === 'application/pdf'}
- {@render displayPreview(attachment)}
- {:else if !attachment.fileExists}
- {m.couldNotFindAttachmentMessage()}
+
+ {#if cell}
+ {#if attachment}
+ {#if attachment.type.startsWith('image') || attachment.type === 'application/pdf'}
+ {@render displayPreview(attachment)}
+ {:else if !attachment.fileExists}
+
{m.couldNotFindAttachmentMessage()}
+ {:else}
+
{m.NoPreviewMessage()}
+ {/if}
{:else}
-
{m.NoPreviewMessage()}
+
+ {m.loading()}...
+
{/if}
- {:else}
-
- {m.loading()}...
-
{/if}
-{/if}
+
diff --git a/frontend/src/lib/stores/attachmentCache.ts b/frontend/src/lib/stores/attachmentCache.ts
new file mode 100644
index 0000000000..5c2be5b5d2
--- /dev/null
+++ b/frontend/src/lib/stores/attachmentCache.ts
@@ -0,0 +1,82 @@
+import { writable, get } from 'svelte/store';
+
+/**
+ * Global cache for attachment blob URLs
+ * Prevents duplicate downloads of the same evidence attachment
+ */
+
+interface CachedAttachment {
+ type: string;
+ url: string;
+ fileExists: boolean;
+}
+
+interface AttachmentCache {
+ [key: string]: CachedAttachment;
+}
+
+function createAttachmentCache() {
+ const { subscribe, set, update } = writable({});
+
+ return {
+ subscribe,
+ /**
+ * Get a cached attachment by key
+ */
+ get: (key: string): CachedAttachment | undefined => {
+ const cache = get({ subscribe });
+ return cache[key];
+ },
+ /**
+ * Store an attachment in the cache
+ */
+ set: (key: string, value: CachedAttachment) => {
+ update((cache) => {
+ const prev = cache[key];
+ if (prev?.url && prev.url !== value.url) {
+ URL.revokeObjectURL(prev.url);
+ }
+ return { ...cache, [key]: value };
+ });
+ },
+ /**
+ * Remove an attachment from the cache and revoke its blob URL
+ */
+ remove: (key: string) => {
+ update((cache) => {
+ if (cache[key]) {
+ URL.revokeObjectURL(cache[key].url);
+ delete cache[key];
+ }
+ return cache;
+ });
+ },
+ /**
+ * Clear all cached attachments and revoke all blob URLs
+ */
+ clear: () => {
+ update((cache) => {
+ Object.values(cache).forEach((attachment) => {
+ URL.revokeObjectURL(attachment.url);
+ });
+ return {};
+ });
+ },
+ /**
+ * Check if an attachment is in the cache
+ */
+ has: (key: string): boolean => {
+ const cache = get({ subscribe });
+ return key in cache;
+ }
+ };
+}
+
+export const attachmentCache = createAttachmentCache();
+
+/**
+ * Generate a cache key for an attachment
+ */
+export function generateAttachmentCacheKey(id: string, attachmentName: string): string {
+ return `${id}-${attachmentName}`;
+}