From 16161a264296e22e223f6db0d46bc1799609612d Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 9 May 2026 01:43:21 +0300 Subject: [PATCH] feat(attachments): optimize composer images --- src/renderer/hooks/useComposerDraft.ts | 56 +++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/src/renderer/hooks/useComposerDraft.ts b/src/renderer/hooks/useComposerDraft.ts index 18df8931..f5a10e3d 100644 --- a/src/renderer/hooks/useComposerDraft.ts +++ b/src/renderer/hooks/useComposerDraft.ts @@ -17,6 +17,10 @@ import { type ComposerDraftSnapshot, composerDraftStorage, } from '@renderer/services/composerDraftStorage'; +import { + DEFAULT_AGENT_IMAGE_OPTIMIZATION_BUDGET, + optimizeImageForAgent, +} from '@features/agent-attachments/renderer'; import { fileToAttachmentPayload, MAX_FILES, @@ -105,6 +109,40 @@ function snapshotMatchesContent( ); } +function imageOutputFilename(filename: string, mimeType: 'image/png' | 'image/jpeg'): string { + const trimmed = filename.trim() || 'image'; + const withoutExtension = trimmed.replace(/\.[^.\\/]+$/, '') || 'image'; + return `${withoutExtension}.${mimeType === 'image/png' ? 'png' : 'jpg'}`; +} + +function blobToBase64(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const dataUrl = typeof reader.result === 'string' ? reader.result : ''; + resolve(dataUrl.split(',')[1] ?? ''); + }; + reader.onerror = () => reject(new Error('Failed to read optimized image')); + reader.readAsDataURL(blob); + }); +} + +async function fileToAgentAttachmentPayload(file: File): Promise { + const category = categorizeFile(file); + if (category !== 'image' || file.type === 'image/gif') { + return fileToAttachmentPayload(file); + } + + const optimized = await optimizeImageForAgent({ file }); + return { + id: crypto.randomUUID(), + filename: imageOutputFilename(file.name, optimized.optimized.mimeType), + mimeType: optimized.optimized.mimeType, + size: optimized.optimized.sizeBytes, + data: await blobToBase64(optimized.optimized.blob), + }; +} + // --------------------------------------------------------------------------- // Hook // --------------------------------------------------------------------------- @@ -403,23 +441,23 @@ export function useComposerDraft(teamName: string): UseComposerDraftResult { if (supported.length === 0) return; - let batchSize = 0; for (const file of supported) { const validation = validateAttachment(file); if (!validation.valid) { setAttachmentError(validation.error); return; } - batchSize += file.size; } const newPayloads: AttachmentPayload[] = []; for (const file of supported) { try { - const payload = await fileToAttachmentPayload(file); + const payload = await fileToAgentAttachmentPayload(file); newPayloads.push(payload); - } catch { - setAttachmentError(`Failed to read file: ${file.name}`); + } catch (error) { + const reason = + error instanceof Error ? error.message : `Failed to read file: ${file.name}`; + setAttachmentError(reason); return; } } @@ -430,10 +468,18 @@ export function useComposerDraft(teamName: string): UseComposerDraftResult { return; } const currentTotal = prev.reduce((sum, a) => sum + a.size, 0); + const batchSize = newPayloads.reduce((sum, a) => sum + a.size, 0); if (currentTotal + batchSize > MAX_TOTAL_SIZE) { setAttachmentError('Total attachment size exceeds 20MB limit'); return; } + const optimizedImageBytes = [...prev, ...newPayloads] + .filter((attachment) => attachment.mimeType.startsWith('image/')) + .reduce((sum, attachment) => sum + attachment.size, 0); + if (optimizedImageBytes > DEFAULT_AGENT_IMAGE_OPTIMIZATION_BUDGET.maxOutputBytesTotal) { + setAttachmentError('Optimized image attachments exceed the safe runtime size limit'); + return; + } const next = [...prev, ...newPayloads]; attachmentsRef.current = next;