Gemma4-WebGPU / src /components /ImageUpload.tsx
BryanBradfo's picture
Gemma 4 multimodal WebGPU detection Space
576d07a
Raw
History Blame Contribute Delete
3.7 kB
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>
);
}