Javier commited on
Commit
88aeaf5
·
1 Parent(s): d725335
.gitignore CHANGED
@@ -17,7 +17,7 @@ models/
17
  *.onnx
18
  *.engine
19
  CLAUDE.md
20
- *.ts
21
  .claude
22
 
23
- design_previews/
 
17
  *.onnx
18
  *.engine
19
  CLAUDE.md
20
+ mobileclip2_b.ts
21
  .claude
22
 
23
+ design_previews/
frontend/src/lib/api.ts ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Client, handle_file } from "@gradio/client"
2
+ import type {
3
+ CompileResult,
4
+ DetectParams,
5
+ LocalConfig,
6
+ RunResult,
7
+ ValidationResult,
8
+ } from "./types"
9
+
10
+ // Single shared connection to the gradio.Server backend.
11
+ let clientPromise: Promise<Client> | null = null
12
+ function getClient(): Promise<Client> {
13
+ if (!clientPromise) {
14
+ const origin =
15
+ import.meta.env.DEV && import.meta.env.VITE_API_ORIGIN
16
+ ? import.meta.env.VITE_API_ORIGIN
17
+ : window.location.origin
18
+ clientPromise = Client.connect(origin)
19
+ }
20
+ return clientPromise
21
+ }
22
+
23
+ async function call<T>(endpoint: string, payload: Record<string, unknown>): Promise<T> {
24
+ const client = await getClient()
25
+ const result = await client.predict(endpoint, payload)
26
+ return (result.data as unknown[])[0] as T
27
+ }
28
+
29
+ export async function detectAndAutomate(
30
+ video: File,
31
+ rulesText: string,
32
+ params: DetectParams,
33
+ ): Promise<RunResult> {
34
+ return call<RunResult>("/detect_and_automate", {
35
+ video: handle_file(video),
36
+ classes: params.classes,
37
+ rules_text: rulesText,
38
+ confidence: params.confidence,
39
+ frame_stride: params.frameStride,
40
+ max_frames: params.maxFrames,
41
+ model_name: params.modelName,
42
+ image_size: params.imageSize,
43
+ device: params.device,
44
+ max_detections: params.maxDetections,
45
+ enable_webhooks: params.enableWebhooks,
46
+ webhook_url: params.webhookUrl,
47
+ })
48
+ }
49
+
50
+ export async function compileRules(
51
+ instruction: string,
52
+ classes: string,
53
+ baseUrl: string,
54
+ model: string,
55
+ ): Promise<CompileResult> {
56
+ return call<CompileResult>("/compile_rules", {
57
+ instruction,
58
+ classes,
59
+ base_url: baseUrl,
60
+ model,
61
+ })
62
+ }
63
+
64
+ export async function validateRules(rulesText: string): Promise<ValidationResult> {
65
+ return call<ValidationResult>("/validate_rules", { rules_text: rulesText })
66
+ }
67
+
68
+ export async function saveRules(rulesText: string): Promise<{ ok: boolean; rule_count: number }> {
69
+ return call("/save_rules", { rules_text: rulesText })
70
+ }
71
+
72
+ export async function loadRules(): Promise<{ rules_text: string | null }> {
73
+ return call("/load_rules", {})
74
+ }
75
+
76
+ export async function getConfig(): Promise<LocalConfig> {
77
+ return call<LocalConfig>("/get_config", {})
78
+ }
79
+
80
+ // Resolve a backend media path (e.g. "/media/foo.mp4") to a fully-qualified URL.
81
+ export function mediaUrl(path: string): string {
82
+ const origin =
83
+ import.meta.env.DEV && import.meta.env.VITE_API_ORIGIN
84
+ ? import.meta.env.VITE_API_ORIGIN
85
+ : window.location.origin
86
+ return path.startsWith("http") ? path : `${origin}${path}`
87
+ }
frontend/src/lib/types.ts ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export interface Detection {
2
+ frame_index: number
3
+ timestamp_sec: number
4
+ label: string
5
+ confidence: number
6
+ bbox_xyxy: [number, number, number, number]
7
+ bbox_xyxy_norm: [number, number, number, number]
8
+ }
9
+
10
+ export interface ActionEvent {
11
+ rule: string
12
+ action: string
13
+ type: "simulate" | "webhook"
14
+ frame_index: number
15
+ timestamp_sec: number
16
+ status: string
17
+ url: string | null
18
+ response_status: number | null
19
+ error: string | null
20
+ }
21
+
22
+ export interface RunStats {
23
+ detections: number
24
+ rules: number
25
+ actions: number
26
+ processed_frames: number
27
+ source_fps: number
28
+ output_fps: number
29
+ }
30
+
31
+ export interface RunResult {
32
+ status: "fired" | "no_match"
33
+ video_url: string
34
+ annotated_url: string
35
+ classes: string[]
36
+ stats: RunStats
37
+ detections: Detection[]
38
+ events: ActionEvent[]
39
+ }
40
+
41
+ export interface RuleSummary {
42
+ name: string
43
+ enabled: boolean
44
+ conditions: number
45
+ actions: { type: string; name: string }[]
46
+ }
47
+
48
+ export interface ValidationResult {
49
+ ok: boolean
50
+ error: string | null
51
+ rules: RuleSummary[]
52
+ }
53
+
54
+ export interface CompileResult {
55
+ rules_text: string
56
+ raw_text: string
57
+ rule_count: number
58
+ }
59
+
60
+ export interface LocalConfig {
61
+ camera_url: string | null
62
+ webhook_url: string | null
63
+ default_classes: string | null
64
+ default_detector_model: string | null
65
+ default_device: string | null
66
+ default_image_size: number | null
67
+ default_max_detections: number | null
68
+ llamacpp_base_url: string | null
69
+ llamacpp_model: string | null
70
+ }
71
+
72
+ export interface DetectParams {
73
+ classes: string
74
+ confidence: number
75
+ frameStride: number
76
+ maxFrames: number
77
+ modelName: string
78
+ imageSize: number
79
+ device: string
80
+ maxDetections: number
81
+ enableWebhooks: boolean
82
+ webhookUrl: string
83
+ }
frontend/src/lib/utils.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
frontend/src/modules/registry.ts ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ComponentType } from "react"
2
+ import type { LucideIcon } from "lucide-react"
3
+ import { Crosshair, Film, ListChecks, Radio, SlidersHorizontal } from "lucide-react"
4
+ import { DetectorPanel } from "./detector/DetectorPanel"
5
+ import { PreviewPanel } from "./preview/PreviewPanel"
6
+ import { RuleStudioPanel } from "./rules/RuleStudioPanel"
7
+ import { ActivityPanel } from "./activity/ActivityPanel"
8
+ import { SettingsPanel } from "./settings/SettingsPanel"
9
+
10
+ export type SectionId = "test" | "automation" | "settings"
11
+
12
+ /** Top-level horizontal bands, each separated by a rule. */
13
+ export interface DashboardSection {
14
+ id: SectionId
15
+ label: string
16
+ hint: string
17
+ order: number
18
+ }
19
+
20
+ export const SECTIONS: DashboardSection[] = [
21
+ { id: "test", label: "Test", hint: "detect · replay · fired actions", order: 1 },
22
+ { id: "automation", label: "Automations & Firings", hint: "compile · validate · log", order: 2 },
23
+ { id: "settings", label: "Settings", hint: "dispatch · compiler", order: 3 },
24
+ ]
25
+
26
+ /**
27
+ * A dashboard module. Add functionality by dropping a new folder under
28
+ * `modules/<name>/` that exports a Panel, then registering it here with the
29
+ * `section` it belongs to and its `span` (out of a 12-column section grid).
30
+ */
31
+ export interface DashboardModule {
32
+ id: string
33
+ index: string
34
+ eyebrow: string
35
+ title: string
36
+ icon: LucideIcon
37
+ section: SectionId
38
+ order: number
39
+ span: number
40
+ Panel: ComponentType
41
+ }
42
+
43
+ export const MODULES: DashboardModule[] = [
44
+ {
45
+ id: "detector",
46
+ index: "01",
47
+ eyebrow: "input",
48
+ title: "Detector",
49
+ icon: Crosshair,
50
+ section: "test",
51
+ order: 1,
52
+ span: 4,
53
+ Panel: DetectorPanel,
54
+ },
55
+ {
56
+ id: "replay",
57
+ index: "02",
58
+ eyebrow: "output",
59
+ title: "Annotated Replay",
60
+ icon: Film,
61
+ section: "test",
62
+ order: 2,
63
+ span: 8,
64
+ Panel: PreviewPanel,
65
+ },
66
+ {
67
+ id: "rules",
68
+ index: "03",
69
+ eyebrow: "automation",
70
+ title: "Rule Studio",
71
+ icon: ListChecks,
72
+ section: "automation",
73
+ order: 1,
74
+ span: 5,
75
+ Panel: RuleStudioPanel,
76
+ },
77
+ {
78
+ id: "activity",
79
+ index: "04",
80
+ eyebrow: "telemetry",
81
+ title: "Activity & Firings",
82
+ icon: Radio,
83
+ section: "automation",
84
+ order: 2,
85
+ span: 7,
86
+ Panel: ActivityPanel,
87
+ },
88
+ {
89
+ id: "dispatch",
90
+ index: "05",
91
+ eyebrow: "config",
92
+ title: "Dispatch & Compiler",
93
+ icon: SlidersHorizontal,
94
+ section: "settings",
95
+ order: 1,
96
+ span: 6,
97
+ Panel: SettingsPanel,
98
+ },
99
+ ]
100
+
101
+ export function modulesForSection(section: SectionId): DashboardModule[] {
102
+ return MODULES.filter((m) => m.section === section).sort((a, b) => a.order - b.order)
103
+ }
104
+
105
+ // Static map so Tailwind keeps the col-span classes (no dynamic interpolation).
106
+ export const SPAN_CLASS: Record<number, string> = {
107
+ 4: "lg:col-span-4",
108
+ 5: "lg:col-span-5",
109
+ 6: "lg:col-span-6",
110
+ 7: "lg:col-span-7",
111
+ 8: "lg:col-span-8",
112
+ 12: "lg:col-span-12",
113
+ }
frontend/src/styles/index.css CHANGED
@@ -1,6 +1,8 @@
1
  @import "tailwindcss";
2
  @import "tw-animate-css";
3
 
 
 
4
  @custom-variant dark (&:is(.dark *));
5
 
6
  /* ───────────────────────────────────────────────────────────────
 
1
  @import "tailwindcss";
2
  @import "tw-animate-css";
3
 
4
+ @source inline("lg:col-span-4 lg:col-span-5 lg:col-span-6 lg:col-span-7 lg:col-span-8 lg:col-span-12");
5
+
6
  @custom-variant dark (&:is(.dark *));
7
 
8
  /* ───────────────────────────────────────────────────────────────
frontend/src/vite-env.d.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_API_ORIGIN?: string
5
+ }
6
+
7
+ interface ImportMeta {
8
+ readonly env: ImportMetaEnv
9
+ }
frontend/vite.config.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from "node:path"
2
+ import { defineConfig } from "vite"
3
+ import react from "@vitejs/plugin-react"
4
+ import tailwindcss from "@tailwindcss/vite"
5
+
6
+ // The dashboard is served by gradio.Server in production (built assets in dist/).
7
+ // In dev, Vite proxies API + media calls to the running gradio.Server on :7860.
8
+ export default defineConfig({
9
+ plugins: [react(), tailwindcss()],
10
+ resolve: {
11
+ alias: { "@": path.resolve(__dirname, "./src") },
12
+ },
13
+ server: {
14
+ port: 5173,
15
+ proxy: {
16
+ "/gradio_api": { target: "http://127.0.0.1:7860", changeOrigin: true, ws: true },
17
+ "/media": { target: "http://127.0.0.1:7860", changeOrigin: true },
18
+ "/config": { target: "http://127.0.0.1:7860", changeOrigin: true },
19
+ "/upload": { target: "http://127.0.0.1:7860", changeOrigin: true },
20
+ },
21
+ },
22
+ build: {
23
+ outDir: "dist",
24
+ emptyOutDir: true,
25
+ },
26
+ })