From b915f1f674b9b1f15276122b468dbd0172c3ea5f Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 26 Aug 2025 20:44:23 +0700 Subject: [PATCH 1/2] fix: handle paste image on linux --- web-app/src/containers/ChatInput.tsx | 61 ++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index f7be420c7..eafd438cf 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -374,15 +374,68 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { } } - const handlePaste = (e: React.ClipboardEvent) => { - const clipboardItems = e.clipboardData?.items - if (!clipboardItems) return - + const handlePaste = async (e: React.ClipboardEvent) => { // Only allow paste if model supports mmproj if (!hasMmproj) { return } + const clipboardItems = e.clipboardData?.items + + // Linux fallback: Use modern Clipboard API if clipboardData.items is unavailable + if ( + !clipboardItems && + navigator.clipboard && + 'read' in navigator.clipboard + ) { + e.preventDefault() + + try { + const clipboardContents = await navigator.clipboard.read() + const files: File[] = [] + + for (const item of clipboardContents) { + const imageTypes = item.types.filter((type) => + type.startsWith('image/') + ) + + for (const type of imageTypes) { + try { + const blob = await item.getType(type) + // Convert blob to File + const file = new File( + [blob], + `pasted-image.${type.split('/')[1]}`, + { type } + ) + files.push(file) + } catch (error) { + console.error('Error reading clipboard item:', error) + } + } + } + + if (files.length > 0) { + const syntheticEvent = { + target: { + files: files, + }, + } as unknown as React.ChangeEvent + + handleFileChange(syntheticEvent) + } + return + } catch (error) { + console.error('Error reading clipboard contents:', error) + return + } + } + + // Original logic for browsers with working clipboardData.items + if (!clipboardItems) { + return + } + const imageItems = Array.from(clipboardItems).filter((item) => item.type.startsWith('image/') ) From b93f77a9f54b4365d8710a4c35a199f9c8a8265a Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Tue, 26 Aug 2025 21:37:32 +0700 Subject: [PATCH 2/2] fix: handle copy image from browser in linux --- web-app/src/containers/ChatInput.tsx | 98 +++++++++++++++------------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index eafd438cf..8efd5e6a7 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -381,13 +381,53 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { } const clipboardItems = e.clipboardData?.items + let hasProcessedImage = false - // Linux fallback: Use modern Clipboard API if clipboardData.items is unavailable - if ( - !clipboardItems && - navigator.clipboard && - 'read' in navigator.clipboard - ) { + // Try clipboardData.items first (traditional method) + if (clipboardItems && clipboardItems.length > 0) { + const imageItems = Array.from(clipboardItems).filter((item) => + item.type.startsWith('image/') + ) + + if (imageItems.length > 0) { + e.preventDefault() + + const files: File[] = [] + let processedCount = 0 + + imageItems.forEach((item) => { + const file = item.getAsFile() + if (file) { + files.push(file) + } + processedCount++ + + // When all items are processed, handle the valid files + if (processedCount === imageItems.length) { + if (files.length > 0) { + const syntheticEvent = { + target: { + files: files, + }, + } as unknown as React.ChangeEvent + + handleFileChange(syntheticEvent) + hasProcessedImage = true + } + } + }) + + // If we found image items but couldn't get files, fall through to modern API + if (processedCount === imageItems.length && !hasProcessedImage) { + // Continue to modern clipboard API fallback below + } else { + return // Successfully processed with traditional method + } + } + } + + // Modern Clipboard API fallback (for Linux, images copied from web, etc.) + if (navigator.clipboard && 'read' in navigator.clipboard) { e.preventDefault() try { @@ -402,10 +442,11 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { for (const type of imageTypes) { try { const blob = await item.getType(type) - // Convert blob to File + // Convert blob to File with better naming + const extension = type.split('/')[1] || 'png' const file = new File( [blob], - `pasted-image.${type.split('/')[1]}`, + `pasted-image-${Date.now()}.${extension}`, { type } ) files.push(file) @@ -423,47 +464,16 @@ const ChatInput = ({ model, className, initialMessage }: ChatInputProps) => { } as unknown as React.ChangeEvent handleFileChange(syntheticEvent) + return } - return } catch (error) { - console.error('Error reading clipboard contents:', error) - return + console.error('Clipboard API access failed:', error) } } - // Original logic for browsers with working clipboardData.items - if (!clipboardItems) { - return - } - - const imageItems = Array.from(clipboardItems).filter((item) => - item.type.startsWith('image/') - ) - - if (imageItems.length > 0) { - e.preventDefault() - - const files: File[] = [] - let processedCount = 0 - - imageItems.forEach((item) => { - const file = item.getAsFile() - if (file) { - files.push(file) - } - processedCount++ - - // When all items are processed, handle the valid files - if (processedCount === imageItems.length && files.length > 0) { - const syntheticEvent = { - target: { - files: files, - }, - } as unknown as React.ChangeEvent - - handleFileChange(syntheticEvent) - } - }) + // If we reach here, no image was found or processed + if (!hasProcessedImage) { + console.log('No image data found in clipboard or clipboard access failed') } }