Spaces:
Running
Running
| import { useCallback, useRef, useState } from "react"; | |
| import { Upload, ImageIcon, X } from "lucide-react"; | |
| import { resizeImage } from "../utils/image-utils"; | |
| interface ImageUploadProps { | |
| onImageSelect: (dataUrl: string) => void; | |
| currentImage: string | null; | |
| disabled?: boolean; | |
| } | |
| export function ImageUpload({ onImageSelect, currentImage, disabled }: ImageUploadProps) { | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| const [isDragging, setIsDragging] = useState(false); | |
| const handleFile = useCallback( | |
| async (file: File) => { | |
| if (!file.type.startsWith("image/")) { | |
| console.warn("[Gemma4] Ignoring non-image file:", file.type); | |
| return; | |
| } | |
| try { | |
| const dataUrl = await resizeImage(file); | |
| onImageSelect(dataUrl); | |
| } catch (err) { | |
| console.error("[Gemma4] Failed to read image:", err); | |
| alert(`Failed to read image: ${err instanceof Error ? err.message : String(err)}`); | |
| } | |
| }, | |
| [onImageSelect], | |
| ); | |
| const handleDrop = useCallback( | |
| (e: React.DragEvent) => { | |
| e.preventDefault(); | |
| setIsDragging(false); | |
| const file = e.dataTransfer.files[0]; | |
| if (file) handleFile(file); | |
| }, | |
| [handleFile], | |
| ); | |
| const handlePaste = useCallback( | |
| (e: React.ClipboardEvent) => { | |
| const items = e.clipboardData.items; | |
| for (const item of items) { | |
| if (item.type.startsWith("image/")) { | |
| const file = item.getAsFile(); | |
| if (file) handleFile(file); | |
| break; | |
| } | |
| } | |
| }, | |
| [handleFile], | |
| ); | |
| if (currentImage) { | |
| return ( | |
| <div className="relative group"> | |
| <img | |
| src={currentImage} | |
| alt="Selected" | |
| className="w-full max-h-[400px] object-contain rounded-xl border border-white/10" | |
| /> | |
| {!disabled && ( | |
| <button | |
| onClick={() => onImageSelect("")} | |
| className="absolute top-2 right-2 p-1.5 rounded-lg bg-black/60 text-white/70 hover:text-white hover:bg-red-500/80 transition-colors opacity-0 group-hover:opacity-100" | |
| > | |
| <X className="h-4 w-4" /> | |
| </button> | |
| )} | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div | |
| onDragOver={(e) => { | |
| e.preventDefault(); | |
| setIsDragging(true); | |
| }} | |
| onDragLeave={() => setIsDragging(false)} | |
| onDrop={handleDrop} | |
| onPaste={handlePaste} | |
| tabIndex={0} | |
| className={`flex flex-col items-center justify-center gap-4 rounded-2xl border-2 border-dashed p-10 transition-all cursor-pointer ${ | |
| isDragging | |
| ? "border-[#1a73e8] bg-[#1a73e8]/10" | |
| : "border-white/10 hover:border-white/30 bg-white/5" | |
| } ${disabled ? "opacity-50 pointer-events-none" : ""}`} | |
| onClick={() => fileInputRef.current?.click()} | |
| > | |
| <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-[#1a73e8]/10 border border-white/10"> | |
| {isDragging ? ( | |
| <Upload className="h-8 w-8 text-[#1a73e8]" /> | |
| ) : ( | |
| <ImageIcon className="h-8 w-8 text-[#1a73e8]" /> | |
| )} | |
| </div> | |
| <div className="text-center"> | |
| <p className="text-base font-medium text-white"> | |
| {isDragging ? "Drop image here" : "Upload an image"} | |
| </p> | |
| <p className="mt-1 text-sm text-white/60"> | |
| Drag & drop, click to browse, or paste from clipboard | |
| </p> | |
| </div> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept="image/*" | |
| className="hidden" | |
| onChange={(e) => { | |
| const file = e.target.files?.[0]; | |
| if (file) handleFile(file); | |
| e.target.value = ""; | |
| }} | |
| /> | |
| </div> | |
| ); | |
| } | |