File size: 4,719 Bytes
d725335
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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>
  )
}