Spaces:
Sleeping
Sleeping
| import { Activity, Film, Loader2, Zap } from "lucide-react" | |
| import { useDashboard } from "@/lib/dashboard" | |
| import { mediaUrl } from "@/lib/api" | |
| import { Badge } from "@/components/ui/badge" | |
| import { cn } from "@/lib/utils" | |
| function Stat({ | |
| label, | |
| value, | |
| accent, | |
| }: { | |
| label: string | |
| value: string | |
| accent?: boolean | |
| }) { | |
| return ( | |
| <div className="rounded-lg border border-border bg-black/20 px-4 py-3"> | |
| <div className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-muted-foreground"> | |
| {label} | |
| </div> | |
| <div | |
| className={cn( | |
| "mt-1 font-display text-2xl font-semibold tabular-nums", | |
| accent ? "text-primary" : "text-foreground", | |
| )} | |
| > | |
| {value} | |
| </div> | |
| </div> | |
| ) | |
| } | |
| export function PreviewPanel() { | |
| const { result, running, videoPreviewUrl } = useDashboard() | |
| const fired = result?.status === "fired" | |
| const src = result ? mediaUrl(result.video_url) : videoPreviewUrl | |
| return ( | |
| <div className="space-y-4"> | |
| <div className="group relative aspect-video overflow-hidden rounded-lg border border-border bg-black"> | |
| {/* scanline / grid atmosphere */} | |
| <div | |
| className="pointer-events-none absolute inset-0 opacity-[0.07]" | |
| style={{ | |
| backgroundImage: | |
| "linear-gradient(oklch(1 0 0 / 0.5) 1px, transparent 1px), linear-gradient(90deg, oklch(1 0 0 / 0.5) 1px, transparent 1px)", | |
| backgroundSize: "32px 32px", | |
| }} | |
| /> | |
| {src ? ( | |
| <video | |
| key={src} | |
| src={src} | |
| controls | |
| autoPlay | |
| loop | |
| muted | |
| playsInline | |
| className="absolute inset-0 size-full object-contain" | |
| /> | |
| ) : ( | |
| <div className="absolute inset-0 flex flex-col items-center justify-center gap-3 text-muted-foreground"> | |
| <Film className="size-8 opacity-40" /> | |
| <p className="text-sm">Upload a clip and run detection to see the annotated replay</p> | |
| </div> | |
| )} | |
| {/* top HUD */} | |
| <div className="pointer-events-none absolute inset-x-0 top-0 flex items-start justify-between p-3"> | |
| <div className="flex gap-2"> | |
| {result?.classes?.slice(0, 4).map((c) => ( | |
| <span | |
| key={c} | |
| className="rounded-md border border-white/10 bg-black/50 px-2 py-1 font-mono text-[0.625rem] uppercase tracking-wider text-white/70 backdrop-blur-sm" | |
| > | |
| {c} | |
| </span> | |
| ))} | |
| </div> | |
| {result && | |
| (fired ? ( | |
| <Badge variant="live"> | |
| <span className="size-1.5 rounded-full bg-primary signal-dot" /> | |
| automation fired | |
| </Badge> | |
| ) : ( | |
| <Badge variant="warning">no match</Badge> | |
| ))} | |
| </div> | |
| {/* running overlay */} | |
| {running && ( | |
| <div className="absolute inset-0 flex items-center justify-center bg-black/60 backdrop-blur-sm"> | |
| <div className="flex flex-col items-center gap-3 text-primary"> | |
| <Loader2 className="size-7 animate-spin" /> | |
| <span className="font-mono text-xs uppercase tracking-widest">running inference</span> | |
| </div> | |
| </div> | |
| )} | |
| {/* fired banner */} | |
| {fired && result && ( | |
| <div className="absolute inset-x-0 bottom-0 flex items-center justify-between gap-3 bg-gradient-to-t from-primary/90 to-primary/70 px-4 py-2.5 text-primary-foreground"> | |
| <div className="flex items-center gap-2 text-sm font-semibold"> | |
| <Zap className="size-4" /> | |
| {result.events[0]?.rule} β {result.events[0]?.action} | |
| </div> | |
| <span className="font-mono text-xs tabular-nums"> | |
| {result.events[0]?.timestamp_sec.toFixed(1)}s | |
| </span> | |
| </div> | |
| )} | |
| </div> | |
| {/* stat strip */} | |
| <div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> | |
| <Stat label="Detections" value={result ? `${result.stats.detections}` : "β"} /> | |
| <Stat label="Rules" value={result ? `${result.stats.rules}` : "β"} /> | |
| <Stat label="Actions" value={result ? `${result.stats.actions}` : "β"} accent={fired} /> | |
| <Stat | |
| label="Frames" | |
| value={result ? `${result.stats.processed_frames}` : "β"} | |
| /> | |
| </div> | |
| {!result && ( | |
| <div className="flex items-center gap-2 text-xs text-muted-foreground"> | |
| <Activity className="size-3.5" /> | |
| Engine idle β awaiting first run. | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |