Spaces:
Running
Running
| 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> | |
| ); | |
| } | |