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