tiny-trigger / frontend /src /modules /preview /PreviewPanel.tsx
Javier
UI Functional, next fixing automations
d725335
Raw
History Blame Contribute Delete
4.72 kB
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>
)
}