Spaces:
Sleeping
Sleeping
Javier commited on
Commit ·
88aeaf5
1
Parent(s): d725335
frontend
Browse files- .gitignore +2 -2
- frontend/src/lib/api.ts +87 -0
- frontend/src/lib/types.ts +83 -0
- frontend/src/lib/utils.ts +6 -0
- frontend/src/modules/registry.ts +113 -0
- frontend/src/styles/index.css +2 -0
- frontend/src/vite-env.d.ts +9 -0
- frontend/vite.config.ts +26 -0
.gitignore
CHANGED
|
@@ -17,7 +17,7 @@ models/
|
|
| 17 |
*.onnx
|
| 18 |
*.engine
|
| 19 |
CLAUDE.md
|
| 20 |
-
|
| 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 |
+
})
|