"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"; import { useAppStore } from "@/store/app-store"; export interface FeatureImagePath { id: string; path: string; // Path to the temp file filename: string; mimeType: string; } // Map to store preview data by image ID (persisted across component re-mounts) export type ImagePreviewMap = Map; 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 // Optional: pass preview map from parent to persist across tab switches previewMap?: ImagePreviewMap; onPreviewMapChange?: (map: ImagePreviewMap) => void; autoFocus?: boolean; error?: boolean; // Show error state with red border } 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, previewMap, onPreviewMapChange, autoFocus = false, error = false, }: DescriptionImageDropZoneProps) { const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); // Use parent-provided preview map if available, otherwise use local state const [localPreviewImages, setLocalPreviewImages] = useState>( () => new Map() ); // Determine which preview map to use - prefer parent-controlled state const previewImages = previewMap !== undefined ? previewMap : localPreviewImages; const setPreviewImages = useCallback((updater: Map | ((prev: Map) => Map)) => { if (onPreviewMapChange) { const currentMap = previewMap !== undefined ? previewMap : localPreviewImages; const newMap = typeof updater === 'function' ? updater(currentMap) : updater; onPreviewMapChange(newMap); } else { setLocalPreviewImages((prev) => { const newMap = typeof updater === 'function' ? updater(prev) : updater; return newMap; }); } }, [onPreviewMapChange, previewMap, localPreviewImages]); const fileInputRef = useRef(null); const currentProject = useAppStore((state) => state.currentProject); 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 = useCallback(async ( base64Data: string, filename: string, mimeType: string ): Promise => { try { const api = getElectronAPI(); // Check if saveImageToTemp method exists if (!api.saveImageToTemp) { // Fallback path when saveImageToTemp is not available console.log("[DescriptionImageDropZone] Using fallback path for image"); return `.automaker/images/${Date.now()}_${filename}`; } // Get projectPath from the store if available const projectPath = currentProject?.path; const result = await api.saveImageToTemp(base64Data, filename, mimeType, projectPath); 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; } }, [currentProject?.path]); 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).substring(2, 11)}`; 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, saveImageToTemp] ); 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 */}