"use client"; import React, { useState, useRef, useCallback } from "react"; import { cn } from "@/lib/utils"; import { ImageIcon, X, Loader2 } from "lucide-react"; import { Textarea } from "@/components/ui/textarea"; import { getElectronAPI } from "@/lib/electron"; export interface FeatureImagePath { id: string; path: string; // Path to the temp file filename: string; mimeType: string; } interface DescriptionImageDropZoneProps { value: string; onChange: (value: string) => void; images: FeatureImagePath[]; onImagesChange: (images: FeatureImagePath[]) => void; placeholder?: string; className?: string; disabled?: boolean; maxFiles?: number; maxFileSize?: number; // in bytes, default 10MB } const ACCEPTED_IMAGE_TYPES = [ "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", ]; const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB export function DescriptionImageDropZone({ value, onChange, images, onImagesChange, placeholder = "Describe the feature...", className, disabled = false, maxFiles = 5, maxFileSize = DEFAULT_MAX_FILE_SIZE, }: DescriptionImageDropZoneProps) { const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [previewImages, setPreviewImages] = useState>( new Map() ); const fileInputRef = useRef(null); const fileToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === "string") { resolve(reader.result); } else { reject(new Error("Failed to read file as base64")); } }; reader.onerror = () => reject(new Error("Failed to read file")); reader.readAsDataURL(file); }); }; const saveImageToTemp = async ( base64Data: string, filename: string, mimeType: string ): Promise => { try { const api = getElectronAPI(); // Check if saveImageToTemp method exists if (!api.saveImageToTemp) { // Fallback for mock API - return a mock path console.log("[DescriptionImageDropZone] Using mock path for image"); return `/tmp/automaker-images/${Date.now()}_${filename}`; } const result = await api.saveImageToTemp(base64Data, filename, mimeType); if (result.success && result.path) { return result.path; } console.error("[DescriptionImageDropZone] Failed to save image:", result.error); return null; } catch (error) { console.error("[DescriptionImageDropZone] Error saving image:", error); return null; } }; const processFiles = useCallback( async (files: FileList) => { if (disabled || isProcessing) return; setIsProcessing(true); const newImages: FeatureImagePath[] = []; const newPreviews = new Map(previewImages); const errors: string[] = []; for (const file of Array.from(files)) { // Validate file type if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { errors.push( `${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.` ); continue; } // Validate file size if (file.size > maxFileSize) { const maxSizeMB = maxFileSize / (1024 * 1024); errors.push( `${file.name}: File too large. Maximum size is ${maxSizeMB}MB.` ); continue; } // Check if we've reached max files if (newImages.length + images.length >= maxFiles) { errors.push(`Maximum ${maxFiles} images allowed.`); break; } try { const base64 = await fileToBase64(file); const tempPath = await saveImageToTemp(base64, file.name, file.type); if (tempPath) { const imageId = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const imagePathRef: FeatureImagePath = { id: imageId, path: tempPath, filename: file.name, mimeType: file.type, }; newImages.push(imagePathRef); // Store preview for display newPreviews.set(imageId, base64); } else { errors.push(`${file.name}: Failed to save image.`); } } catch (error) { errors.push(`${file.name}: Failed to process image.`); } } if (errors.length > 0) { console.warn("Image upload errors:", errors); } if (newImages.length > 0) { onImagesChange([...images, ...newImages]); setPreviewImages(newPreviews); } setIsProcessing(false); }, [disabled, isProcessing, images, maxFiles, maxFileSize, onImagesChange, previewImages] ); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); if (disabled) return; const files = e.dataTransfer.files; if (files.length > 0) { processFiles(files); } }, [disabled, processFiles] ); const handleDragOver = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!disabled) { setIsDragOver(true); } }, [disabled] ); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }, []); const handleFileSelect = useCallback( (e: React.ChangeEvent) => { const files = e.target.files; if (files && files.length > 0) { processFiles(files); } // Reset the input so the same file can be selected again if (fileInputRef.current) { fileInputRef.current.value = ""; } }, [processFiles] ); const handleBrowseClick = useCallback(() => { if (!disabled && fileInputRef.current) { fileInputRef.current.click(); } }, [disabled]); const removeImage = useCallback( (imageId: string) => { onImagesChange(images.filter((img) => img.id !== imageId)); setPreviewImages((prev) => { const newMap = new Map(prev); newMap.delete(imageId); return newMap; }); }, [images, onImagesChange] ); return (
{/* Hidden file input */} {/* Drop zone wrapper */}
{/* Drag overlay */} {isDragOver && !disabled && (
Drop images here
)} {/* Textarea */}