Gemma4-WebGPU / src /components /DetectionApp.tsx
BryanBradfo's picture
Gemma 4 multimodal WebGPU detection Space
576d07a
Raw
History Blame Contribute Delete
6 kB
import { useState, useCallback } from "react";
import { RotateCcw, AlertTriangle } from "lucide-react";
import { useLLM } from "../hooks/useLLM";
import { ImageUpload } from "./ImageUpload";
import { ExampleGallery } from "./ExampleGallery";
import { DetectionOverlay } from "./DetectionOverlay";
import { ResultPanel } from "./ResultPanel";
import { urlToDataUrl } from "../utils/image-utils";
import { DEFAULT_DETECTION_PROMPT } from "../utils/coco-examples";
export function DetectionApp() {
const { detect, isGenerating, tps, dtype, result, stop } = useLLM();
const [currentImage, setCurrentImage] = useState<string | null>(null);
const [prompt, setPrompt] = useState(DEFAULT_DETECTION_PROMPT);
const [loading, setLoading] = useState(false);
const handleImageSelect = useCallback(
(dataUrl: string) => {
if (!dataUrl) {
setCurrentImage(null);
return;
}
setCurrentImage(dataUrl);
detect(dataUrl, prompt);
},
[detect, prompt],
);
const handleExampleSelect = useCallback(
async (url: string, examplePrompt: string) => {
setLoading(true);
try {
const dataUrl = await urlToDataUrl(url);
setCurrentImage(dataUrl);
setPrompt(examplePrompt);
detect(dataUrl, examplePrompt);
} catch (err) {
console.error("[Gemma4] Failed to load example:", err);
alert(`Failed to load example image (likely CORS):\n${err instanceof Error ? err.message : String(err)}`);
}
setLoading(false);
},
[detect],
);
const handleRetry = useCallback(() => {
if (currentImage) {
detect(currentImage, prompt);
}
}, [currentImage, prompt, detect]);
return (
<div className="flex flex-col h-full bg-[#0a0a0a] text-white animate-fade-in">
{/* Header */}
<header className="flex items-center justify-between px-4 py-3 sm:px-6 border-b border-white/10">
<span className="text-sm font-medium tracking-tight">Gemma 4</span>
<div className="flex items-center gap-2">
{isGenerating && (
<button
onClick={stop}
className="px-3 py-1.5 text-xs font-medium rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors cursor-pointer"
>
Stop
</button>
)}
{currentImage && !isGenerating && (
<button
onClick={handleRetry}
className="p-1.5 rounded-lg hover:bg-white/5 transition-colors text-white/60 hover:text-white cursor-pointer"
title="Retry detection"
>
<RotateCcw className="h-4 w-4" />
</button>
)}
</div>
</header>
{/* Main content */}
<div className="flex-1 overflow-y-auto p-4 sm:p-6">
<div className="max-w-4xl mx-auto space-y-6">
{/* q4 slow-path warning */}
{dtype === "q4" && (
<div className="flex items-start gap-3 rounded-xl border border-amber-500/40 bg-amber-500/10 p-3 text-sm text-amber-200">
<AlertTriangle className="h-5 w-5 shrink-0 mt-0.5 text-amber-400" />
<div>
<p className="font-medium">Running on q4 (slow path) — inference may fail with OOM on 8GB GPUs.</p>
<p className="mt-1 text-amber-200/70 text-xs leading-relaxed">
For faster, stabler inference, enable{" "}
<code className="font-mono bg-amber-500/20 px-1 rounded">chrome://flags/#enable-unsafe-webgpu</code> and{" "}
<code className="font-mono bg-amber-500/20 px-1 rounded">chrome://flags/#enable-vulkan</code>, then fully restart Chrome.
</p>
</div>
</div>
)}
{/* Prompt input */}
<div>
<label className="block text-sm font-medium text-white/60 mb-2">
Prompt
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
rows={2}
disabled={isGenerating}
className="w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 focus:border-[#1a73e8] focus:outline-none focus:ring-1 focus:ring-[#1a73e8] resize-none disabled:opacity-50"
placeholder="Enter your prompt..."
/>
</div>
{/* Image area */}
{currentImage ? (
<div className="space-y-4">
{result && result.detections.length > 0 ? (
<DetectionOverlay
imageUrl={currentImage}
detections={result.detections}
/>
) : (
<img
src={currentImage}
alt="Uploaded"
className="w-full max-h-[500px] object-contain rounded-xl border border-white/10"
/>
)}
<ResultPanel
result={result}
isGenerating={isGenerating}
tps={tps}
/>
{!isGenerating && (
<button
onClick={() => setCurrentImage(null)}
className="w-full py-3 rounded-xl border border-white/10 text-sm font-medium text-white/60 hover:text-white hover:border-white/30 transition-colors cursor-pointer"
>
Try another image
</button>
)}
</div>
) : (
<div className="space-y-6">
<ImageUpload
onImageSelect={handleImageSelect}
currentImage={null}
disabled={isGenerating || loading}
/>
<ExampleGallery
onSelect={handleExampleSelect}
disabled={isGenerating || loading}
/>
</div>
)}
</div>
</div>
</div>
);
}