Spaces:
Sleeping
Sleeping
| import { useMemo } from "react" | |
| import { Inbox } from "lucide-react" | |
| import { useDashboard } from "@/lib/dashboard" | |
| import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" | |
| import { | |
| Table, | |
| TableBody, | |
| TableCell, | |
| TableHead, | |
| TableHeader, | |
| TableRow, | |
| } from "@/components/ui/table" | |
| import { Badge } from "@/components/ui/badge" | |
| // deterministic label → hue (matches the spirit of the backend's md5 colouring) | |
| function hueFor(label: string): number { | |
| let h = 0 | |
| for (let i = 0; i < label.length; i++) h = (h * 31 + label.charCodeAt(i)) % 360 | |
| return h | |
| } | |
| function StatusBadge({ status }: { status: string }) { | |
| const variant = status.includes("fail") || status.includes("missing") | |
| ? "danger" | |
| : status.includes("posted") | |
| ? "info" | |
| : status.includes("block") | |
| ? "warning" | |
| : "live" | |
| return <Badge variant={variant}>{status}</Badge> | |
| } | |
| export function ActivityPanel() { | |
| const { result } = useDashboard() | |
| const detections = result?.detections ?? [] | |
| const events = result?.events ?? [] | |
| const maxT = useMemo( | |
| () => | |
| Math.max( | |
| 0.001, | |
| ...detections.map((d) => d.timestamp_sec), | |
| ...events.map((e) => e.timestamp_sec), | |
| ), | |
| [detections, events], | |
| ) | |
| const labels = useMemo( | |
| () => Array.from(new Set(detections.map((d) => d.label))), | |
| [detections], | |
| ) | |
| if (!result) { | |
| return ( | |
| <div className="flex flex-col items-center gap-2 py-10 text-center text-muted-foreground"> | |
| <Inbox className="size-7 opacity-40" /> | |
| <p className="text-sm">Run detection to populate the activity timeline.</p> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div className="space-y-5"> | |
| {/* timeline track */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between font-mono text-[0.625rem] uppercase tracking-wider text-muted-foreground"> | |
| <span>0.0s</span> | |
| <span>detection timeline</span> | |
| <span>{maxT.toFixed(1)}s</span> | |
| </div> | |
| <div className="relative h-16 overflow-hidden rounded-lg border border-border bg-black/30"> | |
| {/* gridlines */} | |
| {[0.25, 0.5, 0.75].map((f) => ( | |
| <div | |
| key={f} | |
| className="absolute inset-y-0 w-px bg-white/5" | |
| style={{ left: `${f * 100}%` }} | |
| /> | |
| ))} | |
| {/* detection ticks */} | |
| {detections.map((d, i) => ( | |
| <span | |
| key={i} | |
| className="absolute top-1/2 size-1.5 -translate-y-1/2 rounded-full" | |
| style={{ | |
| left: `${(d.timestamp_sec / maxT) * 100}%`, | |
| backgroundColor: `oklch(0.75 0.13 ${hueFor(d.label)})`, | |
| opacity: 0.25 + d.confidence * 0.75, | |
| }} | |
| /> | |
| ))} | |
| {/* event markers */} | |
| {events.map((e, i) => ( | |
| <div | |
| key={i} | |
| className="absolute inset-y-0 w-0.5 bg-primary" | |
| style={{ left: `${(e.timestamp_sec / maxT) * 100}%` }} | |
| > | |
| <span className="absolute -top-0 left-1/2 size-2 -translate-x-1/2 rounded-full bg-primary signal-dot" /> | |
| </div> | |
| ))} | |
| </div> | |
| {/* legend */} | |
| <div className="flex flex-wrap gap-x-4 gap-y-1"> | |
| {labels.map((l) => ( | |
| <div key={l} className="flex items-center gap-1.5 text-[0.6875rem] text-muted-foreground"> | |
| <span | |
| className="size-2 rounded-full" | |
| style={{ backgroundColor: `oklch(0.75 0.13 ${hueFor(l)})` }} | |
| /> | |
| {l} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* tables */} | |
| <Tabs defaultValue="actions"> | |
| <TabsList> | |
| <TabsTrigger value="actions"> | |
| Actions | |
| <span className="ml-1 font-mono text-[0.625rem] text-muted-foreground"> | |
| {events.length} | |
| </span> | |
| </TabsTrigger> | |
| <TabsTrigger value="detections"> | |
| Detections | |
| <span className="ml-1 font-mono text-[0.625rem] text-muted-foreground"> | |
| {detections.length} | |
| </span> | |
| </TabsTrigger> | |
| </TabsList> | |
| <TabsContent value="actions"> | |
| {events.length ? ( | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead>Time</TableHead> | |
| <TableHead>Rule</TableHead> | |
| <TableHead>Action</TableHead> | |
| <TableHead>Status</TableHead> | |
| <TableHead className="text-right">Frame</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {events.map((e, i) => ( | |
| <TableRow key={i}> | |
| <TableCell className="font-mono text-xs tabular-nums text-muted-foreground"> | |
| {e.timestamp_sec.toFixed(1)}s | |
| </TableCell> | |
| <TableCell className="font-mono text-xs">{e.rule}</TableCell> | |
| <TableCell className="text-xs">{e.action}</TableCell> | |
| <TableCell> | |
| <StatusBadge status={e.status} /> | |
| </TableCell> | |
| <TableCell className="text-right font-mono text-xs tabular-nums text-muted-foreground"> | |
| {e.frame_index} | |
| </TableCell> | |
| </TableRow> | |
| ))} | |
| </TableBody> | |
| </Table> | |
| ) : ( | |
| <p className="py-6 text-center text-sm text-muted-foreground"> | |
| No actions fired this run. | |
| </p> | |
| )} | |
| </TabsContent> | |
| <TabsContent value="detections"> | |
| <div className="max-h-72 overflow-y-auto"> | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead className="text-right">Frame</TableHead> | |
| <TableHead>Label</TableHead> | |
| <TableHead className="text-right">Conf.</TableHead> | |
| <TableHead className="text-right">Time</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {detections.slice(0, 200).map((d, i) => ( | |
| <TableRow key={i}> | |
| <TableCell className="text-right font-mono text-xs tabular-nums text-muted-foreground"> | |
| {d.frame_index} | |
| </TableCell> | |
| <TableCell> | |
| <span className="flex items-center gap-1.5 text-xs"> | |
| <span | |
| className="size-2 rounded-full" | |
| style={{ backgroundColor: `oklch(0.75 0.13 ${hueFor(d.label)})` }} | |
| /> | |
| {d.label} | |
| </span> | |
| </TableCell> | |
| <TableCell className="text-right font-mono text-xs tabular-nums"> | |
| {d.confidence.toFixed(2)} | |
| </TableCell> | |
| <TableCell className="text-right font-mono text-xs tabular-nums text-muted-foreground"> | |
| {d.timestamp_sec.toFixed(1)}s | |
| </TableCell> | |
| </TableRow> | |
| ))} | |
| </TableBody> | |
| </Table> | |
| </div> | |
| </TabsContent> | |
| </Tabs> | |
| </div> | |
| ) | |
| } | |