@camstack/ui-library 0.1.25 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -23,11 +33,17 @@ __export(src_exports, {
23
33
  AppShell: () => AppShell,
24
34
  Badge: () => Badge,
25
35
  Button: () => Button,
36
+ CLASS_COLORS: () => CLASS_COLORS,
26
37
  Card: () => Card,
27
38
  Checkbox: () => Checkbox,
28
39
  CodeBlock: () => CodeBlock,
29
40
  ConfirmDialog: () => ConfirmDialog,
41
+ DEFAULT_CLASS_COLORS: () => DEFAULT_CLASS_COLORS,
42
+ DEFAULT_COLOR: () => DEFAULT_COLOR,
30
43
  DataTable: () => DataTable,
44
+ DetectionCanvas: () => DetectionCanvas,
45
+ DetectionResultTree: () => DetectionResultTree,
46
+ DevShell: () => DevShell,
31
47
  DeviceCard: () => DeviceCard,
32
48
  DeviceGrid: () => DeviceGrid,
33
49
  Dialog: () => Dialog,
@@ -46,10 +62,16 @@ __export(src_exports, {
46
62
  FloatingPanel: () => FloatingPanel,
47
63
  FormField: () => FormField,
48
64
  IconButton: () => IconButton,
65
+ ImageSelector: () => ImageSelector,
66
+ InferenceConfigSelector: () => InferenceConfigSelector,
49
67
  Input: () => Input,
50
68
  KeyValueList: () => KeyValueList,
51
69
  Label: () => Label,
70
+ LoginForm: () => LoginForm,
52
71
  PageHeader: () => PageHeader,
72
+ PipelineBuilder: () => PipelineBuilder,
73
+ PipelineRuntimeSelector: () => PipelineRuntimeSelector,
74
+ PipelineStep: () => PipelineStep,
53
75
  Popover: () => Popover,
54
76
  PopoverContent: () => PopoverContent,
55
77
  PopoverTrigger: () => PopoverTrigger,
@@ -62,6 +84,7 @@ __export(src_exports, {
62
84
  Skeleton: () => Skeleton,
63
85
  StatCard: () => StatCard,
64
86
  StatusBadge: () => StatusBadge,
87
+ StepTimings: () => StepTimings,
65
88
  Switch: () => Switch,
66
89
  Tabs: () => Tabs,
67
90
  TabsContent: () => TabsContent,
@@ -75,10 +98,13 @@ __export(src_exports, {
75
98
  createTheme: () => createTheme,
76
99
  darkColors: () => darkColors,
77
100
  defaultTheme: () => defaultTheme,
101
+ getClassColor: () => getClassColor,
78
102
  lightColors: () => lightColors,
103
+ mountAddonPage: () => mountAddonPage,
79
104
  providerIcons: () => providerIcons,
80
105
  statusIcons: () => statusIcons,
81
106
  themeToCss: () => themeToCss,
107
+ useDevShell: () => useDevShell,
82
108
  useThemeMode: () => useThemeMode
83
109
  });
84
110
  module.exports = __toCommonJS(src_exports);
@@ -2020,16 +2046,1716 @@ function DeviceGrid({
2020
2046
  }
2021
2047
  );
2022
2048
  }
2049
+
2050
+ // src/composites/pipeline-step.tsx
2051
+ var import_react23 = require("react");
2052
+ var import_lucide_react12 = require("lucide-react");
2053
+ var import_jsx_runtime39 = require("react/jsx-runtime");
2054
+ var ADDON_COLORS = {
2055
+ "object-detection": "border-l-blue-500",
2056
+ "motion-detection": "border-l-amber-500",
2057
+ "face-detection": "border-l-purple-500",
2058
+ "face-recognition": "border-l-violet-500",
2059
+ "plate-detection": "border-l-pink-500",
2060
+ "plate-recognition": "border-l-rose-500",
2061
+ "animal-classifier": "border-l-lime-500",
2062
+ "bird-nabirds-classifier": "border-l-emerald-500",
2063
+ "bird-global-classifier": "border-l-teal-500",
2064
+ "audio-classification": "border-l-green-500",
2065
+ "segmentation-refiner": "border-l-cyan-500"
2066
+ };
2067
+ var SLOT_FALLBACK = {
2068
+ detector: "border-l-blue-500",
2069
+ cropper: "border-l-purple-500",
2070
+ classifier: "border-l-lime-500",
2071
+ refiner: "border-l-cyan-500"
2072
+ };
2073
+ function borderColor(addonId, slot) {
2074
+ return ADDON_COLORS[addonId] ?? SLOT_FALLBACK[slot] ?? "border-l-primary";
2075
+ }
2076
+ var BACKEND_FORMAT = {
2077
+ cpu: "onnx",
2078
+ coreml: "coreml",
2079
+ openvino: "openvino",
2080
+ cuda: "onnx",
2081
+ tensorrt: "onnx",
2082
+ "onnx-py": "onnx",
2083
+ pytorch: "pt"
2084
+ };
2085
+ function backendsForRuntime(runtime, caps, schema) {
2086
+ const allBackends = runtime === "node" ? caps.runtimes.node.backends : caps.runtimes.python.backends;
2087
+ if (!schema) return allBackends;
2088
+ const availableFormats = /* @__PURE__ */ new Set();
2089
+ for (const m of schema.models) {
2090
+ for (const fmt of Object.keys(m.formats)) {
2091
+ availableFormats.add(fmt);
2092
+ }
2093
+ }
2094
+ if (runtime === "node") availableFormats.add("onnx");
2095
+ return allBackends.map((b) => {
2096
+ const neededFormat = BACKEND_FORMAT[b.id] ?? "onnx";
2097
+ const hasFormat = availableFormats.has(neededFormat);
2098
+ return { ...b, available: b.available && hasFormat };
2099
+ });
2100
+ }
2101
+ function modelsForStep(schema, runtime, backend, caps) {
2102
+ if (!schema) return [];
2103
+ const fmt = runtime === "node" ? "onnx" : caps.runtimes.python.backends.find((b) => b.id === backend)?.modelFormat ?? "onnx";
2104
+ return schema.models.filter((m) => m.formats[fmt]).map((m) => ({
2105
+ id: m.id,
2106
+ name: m.name,
2107
+ downloaded: m.formats[fmt]?.downloaded ?? false
2108
+ }));
2109
+ }
2110
+ function runtimeOptions(caps) {
2111
+ const opts = [
2112
+ { id: "node", label: "Node.js (ONNX)", available: true }
2113
+ ];
2114
+ if (caps.runtimes.python.available && caps.runtimes.python.backends.some((b) => b.available)) {
2115
+ opts.unshift({ id: "python", label: "Python", available: true });
2116
+ }
2117
+ return opts;
2118
+ }
2119
+ function PipelineStep({
2120
+ step,
2121
+ schema,
2122
+ allSchemas,
2123
+ capabilities,
2124
+ depth = 0,
2125
+ onChange,
2126
+ onDelete,
2127
+ readOnly = false
2128
+ }) {
2129
+ const [expanded, setExpanded] = (0, import_react23.useState)(false);
2130
+ const color = borderColor(step.addonId, step.slot);
2131
+ const backends = backendsForRuntime(step.runtime, capabilities, schema);
2132
+ const rtOptions = runtimeOptions(capabilities);
2133
+ const currentBackendAvailable = backends.find((b) => b.id === step.backend)?.available ?? false;
2134
+ if (!currentBackendAvailable && !readOnly) {
2135
+ const firstAvailable = backends.find((b) => b.available);
2136
+ if (firstAvailable && firstAvailable.id !== step.backend) {
2137
+ queueMicrotask(() => onChange({ ...step, backend: firstAvailable.id }));
2138
+ }
2139
+ }
2140
+ const models = modelsForStep(schema, step.runtime, step.backend, capabilities);
2141
+ if (models.length > 0 && !models.some((m) => m.id === step.modelId) && !readOnly) {
2142
+ const defaultModel = schema?.defaultModelId ? models.find((m) => m.id === schema.defaultModelId) : null;
2143
+ const fallback = defaultModel ?? models[0];
2144
+ if (fallback && fallback.id !== step.modelId) {
2145
+ queueMicrotask(() => onChange({ ...step, modelId: fallback.id }));
2146
+ }
2147
+ }
2148
+ function handleClick(e) {
2149
+ if (e.target.closest(".step-config")) return;
2150
+ setExpanded((v) => !v);
2151
+ }
2152
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("div", { className: "space-y-2", children: /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2153
+ "div",
2154
+ {
2155
+ className: cn(
2156
+ "rounded-xl border border-border bg-surface overflow-hidden border-l-4 shadow-sm",
2157
+ color,
2158
+ !step.enabled && "opacity-[0.45]"
2159
+ ),
2160
+ children: [
2161
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-2.5 px-3 py-2.5 cursor-pointer select-none", onClick: handleClick, children: [
2162
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "text-foreground-subtle", children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react12.ChevronDown, { className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(import_lucide_react12.ChevronRight, { className: "h-4 w-4" }) }),
2163
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex-1 min-w-0", children: [
2164
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "text-[10px] uppercase tracking-wider font-medium text-foreground-subtle/60 block leading-none", children: step.slot }),
2165
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "text-sm font-semibold text-foreground truncate block leading-tight", children: step.addonName }),
2166
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center gap-1 mt-0.5 flex-wrap", children: [
2167
+ step.inputClasses.map((c) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-blue-500/12 text-blue-400", children: c }, c)),
2168
+ step.inputClasses.length > 0 && step.outputClasses.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "text-foreground-subtle/40 text-[10px]", children: "\u2192" }),
2169
+ step.outputClasses.map((c) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-green-500/12 text-green-400", children: c }, c))
2170
+ ] })
2171
+ ] }),
2172
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2173
+ "button",
2174
+ {
2175
+ onClick: (e) => {
2176
+ e.stopPropagation();
2177
+ onChange({ ...step, enabled: !step.enabled });
2178
+ },
2179
+ className: cn(
2180
+ "relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors",
2181
+ step.enabled ? "bg-success" : "bg-foreground-subtle/30"
2182
+ ),
2183
+ children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: cn(
2184
+ "inline-block h-4 w-4 rounded-full bg-white shadow transition-transform",
2185
+ step.enabled ? "translate-x-6" : "translate-x-1"
2186
+ ) })
2187
+ }
2188
+ )
2189
+ ] }),
2190
+ expanded && /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "step-config border-t border-border bg-background px-4 py-4 space-y-3", children: [
2191
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "grid grid-cols-2 gap-3", children: [
2192
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2193
+ ConfigSelect,
2194
+ {
2195
+ label: "Agent",
2196
+ value: step.agentId,
2197
+ disabled: readOnly,
2198
+ options: [{ value: step.agentId || "hub", label: step.agentId || "Hub (local)" }],
2199
+ onChange: (v) => onChange({ ...step, agentId: v })
2200
+ }
2201
+ ),
2202
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2203
+ ConfigSelect,
2204
+ {
2205
+ label: "Runtime",
2206
+ value: step.runtime,
2207
+ disabled: readOnly,
2208
+ options: rtOptions.map((r) => ({ value: r.id, label: r.label, disabled: !r.available })),
2209
+ onChange: (v) => onChange({ ...step, runtime: v })
2210
+ }
2211
+ ),
2212
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2213
+ ConfigSelect,
2214
+ {
2215
+ label: "Backend",
2216
+ value: step.backend,
2217
+ disabled: readOnly,
2218
+ options: backends.map((b) => ({ value: b.id, label: b.label, disabled: !b.available })),
2219
+ onChange: (v) => onChange({ ...step, backend: v })
2220
+ }
2221
+ ),
2222
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2223
+ ConfigSelect,
2224
+ {
2225
+ label: "Model",
2226
+ value: step.modelId,
2227
+ disabled: readOnly,
2228
+ options: models.map((m) => ({ value: m.id, label: `${m.name}${m.downloaded ? " \u2713" : ""}` })),
2229
+ onChange: (v) => onChange({ ...step, modelId: v })
2230
+ }
2231
+ )
2232
+ ] }),
2233
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { children: [
2234
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { className: "flex items-center justify-between mb-1", children: [
2235
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("span", { className: "text-[10px] font-medium text-foreground-subtle uppercase tracking-wide", children: "Confidence" }),
2236
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "text-xs font-medium text-foreground tabular-nums", children: [
2237
+ (step.confidence * 100).toFixed(0),
2238
+ "%"
2239
+ ] })
2240
+ ] }),
2241
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
2242
+ "input",
2243
+ {
2244
+ type: "range",
2245
+ min: 0,
2246
+ max: 1,
2247
+ step: 0.01,
2248
+ value: step.confidence,
2249
+ disabled: readOnly,
2250
+ onChange: (e) => onChange({ ...step, confidence: Number(e.target.value) }),
2251
+ className: "w-full accent-primary h-1.5"
2252
+ }
2253
+ )
2254
+ ] })
2255
+ ] })
2256
+ ]
2257
+ }
2258
+ ) });
2259
+ }
2260
+ function ConfigSelect({ label, value, options, disabled, onChange }) {
2261
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("div", { children: [
2262
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("label", { className: "block text-[10px] font-medium text-foreground-subtle uppercase tracking-wide mb-1.5", children: label }),
2263
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
2264
+ "select",
2265
+ {
2266
+ value,
2267
+ onChange: (e) => onChange(e.target.value),
2268
+ disabled,
2269
+ className: "w-full rounded-lg border border-border bg-surface px-3 py-2 text-xs text-foreground focus:outline-none focus:border-primary/50",
2270
+ children: [
2271
+ options.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("option", { value, children: value || "default" }),
2272
+ options.map((o) => /* @__PURE__ */ (0, import_jsx_runtime39.jsx)("option", { value: o.value, disabled: o.disabled, children: o.label }, o.value))
2273
+ ]
2274
+ }
2275
+ )
2276
+ ] });
2277
+ }
2278
+
2279
+ // src/composites/pipeline-runtime-selector.tsx
2280
+ var import_lucide_react13 = require("lucide-react");
2281
+ var import_jsx_runtime40 = require("react/jsx-runtime");
2282
+ function PipelineRuntimeSelector({ options, value, onChange }) {
2283
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsx)("div", { className: "flex flex-wrap gap-2", children: options.map((opt) => {
2284
+ const active = opt.id === value;
2285
+ return /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)(
2286
+ "button",
2287
+ {
2288
+ onClick: () => opt.available && onChange(opt.id),
2289
+ disabled: !opt.available,
2290
+ className: `flex items-center gap-2 rounded-lg border px-3 py-2 text-xs font-medium transition-all ${active ? "border-primary/40 bg-primary/10 text-primary" : opt.available ? "border-border bg-surface text-foreground-subtle hover:bg-surface-hover hover:text-foreground" : "border-border/40 bg-surface/40 text-foreground-subtle/40 cursor-not-allowed"}`,
2291
+ children: [
2292
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react13.Cpu, { className: "h-3.5 w-3.5 shrink-0" }),
2293
+ opt.label,
2294
+ opt.isBest && /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "inline-flex items-center gap-0.5 rounded-full bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-semibold text-amber-400", children: [
2295
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(import_lucide_react13.Star, { className: "h-2.5 w-2.5" }),
2296
+ "Best"
2297
+ ] }),
2298
+ opt.platformScore != null && /* @__PURE__ */ (0, import_jsx_runtime40.jsxs)("span", { className: "text-[10px] text-foreground-subtle/60", children: [
2299
+ "(",
2300
+ opt.platformScore,
2301
+ ")"
2302
+ ] }),
2303
+ /* @__PURE__ */ (0, import_jsx_runtime40.jsx)(
2304
+ "span",
2305
+ {
2306
+ className: `h-1.5 w-1.5 rounded-full ${opt.available ? "bg-success" : "bg-danger"}`
2307
+ }
2308
+ )
2309
+ ]
2310
+ },
2311
+ opt.id
2312
+ );
2313
+ }) });
2314
+ }
2315
+
2316
+ // src/composites/pipeline-builder.tsx
2317
+ var import_react24 = require("react");
2318
+ var import_lucide_react14 = require("lucide-react");
2319
+
2320
+ // src/lib/validate-template.ts
2321
+ function validateTemplate(steps, schema) {
2322
+ const availableAddonIds = new Set(
2323
+ schema.slots.flatMap((s) => s.addons.map((a) => a.id))
2324
+ );
2325
+ const warnings = [];
2326
+ function validateStep(step) {
2327
+ if (!availableAddonIds.has(step.addonId)) {
2328
+ warnings.push(`Addon "${step.addonId}" is no longer available \u2014 step removed`);
2329
+ return null;
2330
+ }
2331
+ const addon = schema.slots.flatMap((s) => s.addons).find((a) => a.id === step.addonId);
2332
+ let modelId = step.modelId;
2333
+ if (addon && !addon.models.some((m) => m.id === modelId)) {
2334
+ const fallback = addon.defaultModelId;
2335
+ warnings.push(`Model "${modelId}" not available for ${step.addonId} \u2014 using "${fallback}"`);
2336
+ modelId = fallback;
2337
+ }
2338
+ const validChildren = step.children.map((c) => validateStep(c)).filter((c) => c !== null);
2339
+ return { ...step, modelId, children: validChildren };
2340
+ }
2341
+ const validSteps = steps.map((s) => validateStep(s)).filter((s) => s !== null);
2342
+ return {
2343
+ valid: warnings.length === 0,
2344
+ steps: validSteps,
2345
+ warnings
2346
+ };
2347
+ }
2348
+
2349
+ // src/composites/pipeline-builder.tsx
2350
+ var import_jsx_runtime41 = require("react/jsx-runtime");
2351
+ function buildSchemaMap(schema) {
2352
+ const map = /* @__PURE__ */ new Map();
2353
+ for (const slot of schema.slots) {
2354
+ for (const addon of slot.addons) {
2355
+ map.set(addon.id, addon);
2356
+ }
2357
+ }
2358
+ return map;
2359
+ }
2360
+ function createDefaultStep(addon, fallbackRuntime, fallbackBackend) {
2361
+ return {
2362
+ addonId: addon.id,
2363
+ addonName: addon.name,
2364
+ slot: addon.slot,
2365
+ inputClasses: [...addon.inputClasses],
2366
+ outputClasses: [...addon.outputClasses],
2367
+ enabled: true,
2368
+ agentId: "hub",
2369
+ // Use per-addon defaults from backend PlatformScorer, fallback to provided
2370
+ runtime: addon.defaultRuntime ?? fallbackRuntime ?? "node",
2371
+ backend: addon.defaultBackend ?? fallbackBackend ?? "cpu",
2372
+ modelId: addon.defaultModelId,
2373
+ confidence: addon.defaultConfidence,
2374
+ classFilters: [...addon.inputClasses],
2375
+ children: []
2376
+ };
2377
+ }
2378
+ function PlaceholderStep({ addon, onClick }) {
2379
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2380
+ "button",
2381
+ {
2382
+ type: "button",
2383
+ onClick,
2384
+ className: "w-full rounded-xl border-2 border-dashed border-border/60 px-4 py-3 text-left transition-all hover:border-primary/30 hover:bg-surface/60 group",
2385
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex items-center gap-3", children: [
2386
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.PlusCircle, { className: "h-[18px] w-[18px] text-foreground-subtle/30 group-hover:text-primary/60 shrink-0" }),
2387
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex-1 min-w-0", children: [
2388
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "text-[13px] font-medium text-foreground-subtle/50 group-hover:text-foreground-subtle block truncate", children: addon.name }),
2389
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex items-center gap-1 mt-0.5 flex-wrap", children: [
2390
+ addon.inputClasses.map((c) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-blue-500/8 text-blue-400/50", children: c }, c)),
2391
+ addon.inputClasses.length > 0 && addon.outputClasses.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "text-foreground-subtle/25 text-[10px]", children: "\u2192" }),
2392
+ addon.outputClasses.map((c) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "text-[9px] uppercase font-semibold tracking-wide px-1.5 py-0.5 rounded bg-green-500/8 text-green-400/50", children: c }, c))
2393
+ ] })
2394
+ ] })
2395
+ ] })
2396
+ }
2397
+ );
2398
+ }
2399
+ function PipelineBuilder({
2400
+ schema,
2401
+ capabilities,
2402
+ steps,
2403
+ onChange,
2404
+ templates,
2405
+ selectedTemplateId,
2406
+ onSelectTemplate,
2407
+ onSaveTemplate,
2408
+ onUpdateTemplate,
2409
+ onDeleteTemplate,
2410
+ readOnly = false,
2411
+ excludeAddons = []
2412
+ }) {
2413
+ const excluded = (0, import_react24.useMemo)(() => new Set(excludeAddons), [excludeAddons]);
2414
+ const schemaMap = (0, import_react24.useMemo)(() => buildSchemaMap(schema), [schema]);
2415
+ const [warnings, setWarnings] = (0, import_react24.useState)([]);
2416
+ const bestPlatformScore = capabilities.platformScores?.find((s) => s.available);
2417
+ const hasPython = capabilities.runtimes.python.available && capabilities.runtimes.python.backends.some((b) => b.available);
2418
+ const defaultRuntime = bestPlatformScore?.runtime ?? (hasPython ? "python" : "node");
2419
+ const defaultBackend = bestPlatformScore?.backend ?? (hasPython ? capabilities.runtimes.python.backends.find((b) => b.available)?.id ?? "cpu" : capabilities.runtimes.node.backends.find((b) => b.available && b.id !== "cpu")?.id ?? "cpu");
2420
+ const dirty = selectedTemplateId ? JSON.stringify(steps) !== JSON.stringify(templates.find((t) => t.id === selectedTemplateId)?.steps) : false;
2421
+ function handleSelectTemplate(e) {
2422
+ const id = e.target.value || null;
2423
+ if (id) {
2424
+ const tpl = templates.find((t) => t.id === id);
2425
+ if (tpl) {
2426
+ const result = validateTemplate(tpl.steps, schema);
2427
+ setWarnings([...result.warnings]);
2428
+ }
2429
+ } else {
2430
+ setWarnings([]);
2431
+ }
2432
+ onSelectTemplate(id);
2433
+ }
2434
+ function handleSave() {
2435
+ if (selectedTemplateId) onUpdateTemplate(selectedTemplateId, steps);
2436
+ }
2437
+ function handleSaveAs() {
2438
+ const name = window.prompt("Template name:");
2439
+ if (name?.trim()) onSaveTemplate(name.trim(), steps);
2440
+ }
2441
+ function handleDelete() {
2442
+ if (!selectedTemplateId) return;
2443
+ const tpl = templates.find((t) => t.id === selectedTemplateId);
2444
+ if (tpl && window.confirm(`Delete template "${tpl.name}"?`)) {
2445
+ onDeleteTemplate(selectedTemplateId);
2446
+ }
2447
+ }
2448
+ function handleStepChange(updated) {
2449
+ onChange(steps.map((s) => {
2450
+ if (s.addonId !== updated.addonId) return s;
2451
+ return autoEnableAncestors(updated);
2452
+ }));
2453
+ }
2454
+ function autoEnableAncestors(step) {
2455
+ const hasEnabledChild = step.children.some((c) => c.enabled || c.children.some((gc) => gc.enabled));
2456
+ return {
2457
+ ...step,
2458
+ enabled: step.enabled || hasEnabledChild,
2459
+ children: step.children.map((c) => {
2460
+ const hasEnabledGrandchild = c.children.some((gc) => gc.enabled);
2461
+ return {
2462
+ ...c,
2463
+ enabled: c.enabled || hasEnabledGrandchild
2464
+ };
2465
+ })
2466
+ };
2467
+ }
2468
+ function handleAddChildToStep(parentAddonId, addon) {
2469
+ const child = createDefaultStep(addon, defaultRuntime, defaultBackend);
2470
+ onChange(steps.map((s) => {
2471
+ if (s.addonId !== parentAddonId) return s;
2472
+ return { ...s, children: [...s.children, child] };
2473
+ }));
2474
+ }
2475
+ function collectIds(list) {
2476
+ const ids = /* @__PURE__ */ new Set();
2477
+ for (const s of list) {
2478
+ ids.add(s.addonId);
2479
+ for (const c of s.children) ids.add(c.addonId);
2480
+ const childIds = collectIds(s.children);
2481
+ for (const id of childIds) ids.add(id);
2482
+ }
2483
+ return ids;
2484
+ }
2485
+ const existingIds = collectIds(steps);
2486
+ function getChildPlaceholders(step) {
2487
+ const stepSchema = schemaMap.get(step.addonId);
2488
+ if (!stepSchema) return [];
2489
+ const childSlotIds = stepSchema.childSlots;
2490
+ const childIds = new Set(step.children.map((c) => c.addonId));
2491
+ const placeholders = [];
2492
+ for (const slot of schema.slots) {
2493
+ if (!childSlotIds.includes(slot.id)) continue;
2494
+ for (const addon of slot.addons) {
2495
+ if (childIds.has(addon.id) || excluded.has(addon.id)) continue;
2496
+ const compatible = addon.inputClasses.some((ic) => step.outputClasses.includes(ic));
2497
+ if (compatible) placeholders.push(addon);
2498
+ }
2499
+ }
2500
+ return placeholders;
2501
+ }
2502
+ function renderStep(step) {
2503
+ const childPlaceholders = getChildPlaceholders(step);
2504
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "space-y-1.5", children: [
2505
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2506
+ PipelineStep,
2507
+ {
2508
+ step,
2509
+ schema: schemaMap.get(step.addonId) ?? null,
2510
+ allSchemas: schemaMap,
2511
+ capabilities,
2512
+ onChange: handleStepChange,
2513
+ onDelete: readOnly ? void 0 : (id) => onChange(steps.filter((s) => s.addonId !== id)),
2514
+ readOnly
2515
+ }
2516
+ ),
2517
+ (step.children.length > 0 || childPlaceholders.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "ml-6 pl-4 border-l-2 border-dashed border-border/40 space-y-1.5", children: [
2518
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "text-[10px] font-semibold uppercase tracking-widest text-foreground-subtle/40", children: "Slot: Cropper / Classifier" }),
2519
+ step.children.map((child) => {
2520
+ const childChildPlaceholders = getChildPlaceholders(child);
2521
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "space-y-1.5", children: [
2522
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2523
+ PipelineStep,
2524
+ {
2525
+ step: child,
2526
+ schema: schemaMap.get(child.addonId) ?? null,
2527
+ allSchemas: schemaMap,
2528
+ capabilities,
2529
+ depth: 1,
2530
+ onChange: (updated) => {
2531
+ handleStepChange({
2532
+ ...step,
2533
+ children: step.children.map((c) => c.addonId === updated.addonId ? updated : c)
2534
+ });
2535
+ },
2536
+ onDelete: readOnly ? void 0 : (id) => {
2537
+ handleStepChange({
2538
+ ...step,
2539
+ children: step.children.filter((c) => c.addonId !== id)
2540
+ });
2541
+ },
2542
+ readOnly
2543
+ }
2544
+ ),
2545
+ (child.children.length > 0 || childChildPlaceholders.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "ml-6 pl-4 border-l-2 border-dashed border-border/30 space-y-1.5", children: [
2546
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "text-[10px] font-semibold uppercase tracking-widest text-foreground-subtle/30", children: "Slot: Recognizer" }),
2547
+ child.children.map((grandchild) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2548
+ PipelineStep,
2549
+ {
2550
+ step: grandchild,
2551
+ schema: schemaMap.get(grandchild.addonId) ?? null,
2552
+ allSchemas: schemaMap,
2553
+ capabilities,
2554
+ depth: 2,
2555
+ onChange: (updatedGrandchild) => {
2556
+ handleStepChange({
2557
+ ...step,
2558
+ children: step.children.map(
2559
+ (c) => c.addonId === child.addonId ? { ...c, children: c.children.map((gc) => gc.addonId === updatedGrandchild.addonId ? updatedGrandchild : gc) } : c
2560
+ )
2561
+ });
2562
+ },
2563
+ onDelete: readOnly ? void 0 : (id) => {
2564
+ handleStepChange({
2565
+ ...step,
2566
+ children: step.children.map(
2567
+ (c) => c.addonId === child.addonId ? { ...c, children: c.children.filter((gc) => gc.addonId !== id) } : c
2568
+ )
2569
+ });
2570
+ },
2571
+ readOnly
2572
+ },
2573
+ grandchild.addonId
2574
+ )),
2575
+ !readOnly && childChildPlaceholders.map((addon) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2576
+ PlaceholderStep,
2577
+ {
2578
+ addon,
2579
+ onClick: () => {
2580
+ const newChild = createDefaultStep(addon, defaultRuntime, defaultBackend);
2581
+ handleStepChange({
2582
+ ...step,
2583
+ children: step.children.map(
2584
+ (c) => c.addonId === child.addonId ? { ...c, children: [...c.children, newChild] } : c
2585
+ )
2586
+ });
2587
+ }
2588
+ },
2589
+ addon.id
2590
+ ))
2591
+ ] })
2592
+ ] }, child.addonId);
2593
+ }),
2594
+ !readOnly && childPlaceholders.map((addon) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2595
+ PlaceholderStep,
2596
+ {
2597
+ addon,
2598
+ onClick: () => handleAddChildToStep(step.addonId, addon)
2599
+ },
2600
+ addon.id
2601
+ ))
2602
+ ] })
2603
+ ] }, step.addonId);
2604
+ }
2605
+ const rootSlots = schema.slots.filter((s) => s.parentSlot === null).sort((a, b) => a.priority - b.priority);
2606
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "space-y-4", children: [
2607
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "rounded-xl border border-border bg-surface p-3", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex items-center gap-2", children: [
2608
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("div", { className: "relative flex-1 min-w-0", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)(
2609
+ "select",
2610
+ {
2611
+ value: selectedTemplateId ?? "",
2612
+ onChange: handleSelectTemplate,
2613
+ className: "w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:border-primary/50",
2614
+ children: [
2615
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("option", { value: "", children: "No template" }),
2616
+ templates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("option", { value: t.id, children: t.name }, t.id))
2617
+ ]
2618
+ }
2619
+ ) }),
2620
+ dirty && /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "h-1.5 w-1.5 rounded-full bg-amber-500 shrink-0" }),
2621
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2622
+ "button",
2623
+ {
2624
+ onClick: handleSave,
2625
+ disabled: !selectedTemplateId || readOnly,
2626
+ title: "Save",
2627
+ className: cn(
2628
+ "p-2 rounded-lg border border-border transition-colors",
2629
+ selectedTemplateId && !readOnly ? "text-foreground-subtle hover:bg-surface-hover" : "text-foreground-subtle/30 cursor-not-allowed"
2630
+ ),
2631
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.Save, { className: "h-4 w-4" })
2632
+ }
2633
+ ),
2634
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2635
+ "button",
2636
+ {
2637
+ onClick: handleSaveAs,
2638
+ disabled: readOnly,
2639
+ title: "Save As",
2640
+ className: cn(
2641
+ "p-2 rounded-lg border border-border transition-colors",
2642
+ !readOnly ? "text-foreground-subtle hover:bg-surface-hover" : "text-foreground-subtle/30 cursor-not-allowed"
2643
+ ),
2644
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.CopyPlus, { className: "h-4 w-4" })
2645
+ }
2646
+ ),
2647
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(
2648
+ "button",
2649
+ {
2650
+ onClick: handleDelete,
2651
+ disabled: !selectedTemplateId || readOnly,
2652
+ title: "Delete",
2653
+ className: cn(
2654
+ "p-2 rounded-lg border border-border transition-colors",
2655
+ selectedTemplateId && !readOnly ? "text-foreground-subtle hover:text-danger" : "text-foreground-subtle/30 cursor-not-allowed"
2656
+ ),
2657
+ children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.Trash2, { className: "h-4 w-4" })
2658
+ }
2659
+ )
2660
+ ] }) }),
2661
+ warnings.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 text-xs text-amber-400 space-y-1", children: [
2662
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "flex items-center justify-between", children: [
2663
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("span", { className: "font-medium", children: "Template loaded with warnings:" }),
2664
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsx)("button", { onClick: () => setWarnings([]), className: "text-amber-400/60 hover:text-amber-400", children: /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(import_lucide_react14.X, { className: "h-3.5 w-3.5" }) })
2665
+ ] }),
2666
+ warnings.map((w, i) => /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { children: [
2667
+ "\u2022 ",
2668
+ w
2669
+ ] }, i))
2670
+ ] }),
2671
+ rootSlots.map((slot) => {
2672
+ const slotSteps = steps.filter((s) => s.slot === slot.id && !excluded.has(s.addonId));
2673
+ const missingRootAddons = slot.addons.filter((a) => !existingIds.has(a.id) && !excluded.has(a.id));
2674
+ return /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("div", { className: "space-y-2", children: [
2675
+ /* @__PURE__ */ (0, import_jsx_runtime41.jsxs)("span", { className: "text-[10px] font-semibold uppercase tracking-widest text-foreground-subtle/50", children: [
2676
+ "Slot: ",
2677
+ slot.label
2678
+ ] }),
2679
+ slotSteps.map((step) => renderStep(step)),
2680
+ !readOnly && missingRootAddons.map((addon) => /* @__PURE__ */ (0, import_jsx_runtime41.jsx)(PlaceholderStep, { addon, onClick: () => {
2681
+ onChange([...steps, createDefaultStep(addon, defaultRuntime, defaultBackend)]);
2682
+ } }, addon.id))
2683
+ ] }, slot.id);
2684
+ })
2685
+ ] });
2686
+ }
2687
+
2688
+ // src/composites/detection-colors.ts
2689
+ var CLASS_COLORS = {
2690
+ // Primary detection classes
2691
+ person: "#3b82f6",
2692
+ // blue-500
2693
+ vehicle: "#f59e0b",
2694
+ // amber-500
2695
+ animal: "#22c55e",
2696
+ // green-500
2697
+ // Sub-detection classes
2698
+ face: "#a855f7",
2699
+ // purple-500
2700
+ plate: "#ec4899",
2701
+ // pink-500
2702
+ // Specific animal types
2703
+ bird: "#14b8a6",
2704
+ // teal-500
2705
+ dog: "#84cc16",
2706
+ // lime-500
2707
+ cat: "#f97316",
2708
+ // orange-500
2709
+ // Specific vehicle types
2710
+ car: "#f59e0b",
2711
+ // amber-500
2712
+ truck: "#d97706",
2713
+ // amber-600
2714
+ bus: "#b45309",
2715
+ // amber-700
2716
+ motorcycle: "#eab308",
2717
+ // yellow-500
2718
+ bicycle: "#ca8a04",
2719
+ // yellow-600
2720
+ // Other
2721
+ motion: "#facc15"
2722
+ // yellow-400
2723
+ };
2724
+ var FALLBACK_PALETTE = [
2725
+ "#ef4444",
2726
+ // red-500
2727
+ "#8b5cf6",
2728
+ // violet-500
2729
+ "#06b6d4",
2730
+ // cyan-500
2731
+ "#f97316",
2732
+ // orange-500
2733
+ "#10b981",
2734
+ // emerald-500
2735
+ "#6366f1",
2736
+ // indigo-500
2737
+ "#e11d48",
2738
+ // rose-600
2739
+ "#0891b2",
2740
+ // cyan-600
2741
+ "#7c3aed",
2742
+ // violet-600
2743
+ "#059669"
2744
+ // emerald-600
2745
+ ];
2746
+ var DEFAULT_COLOR = "#f59e42";
2747
+ function getClassColor(className, customColors) {
2748
+ if (customColors?.[className]) return customColors[className];
2749
+ if (customColors?.[className.toLowerCase()]) return customColors[className.toLowerCase()];
2750
+ if (CLASS_COLORS[className]) return CLASS_COLORS[className];
2751
+ if (CLASS_COLORS[className.toLowerCase()]) return CLASS_COLORS[className.toLowerCase()];
2752
+ let hash = 0;
2753
+ for (let i = 0; i < className.length; i++) {
2754
+ hash = hash * 31 + (className.codePointAt(i) ?? 0) >>> 0;
2755
+ }
2756
+ return FALLBACK_PALETTE[hash % FALLBACK_PALETTE.length] ?? DEFAULT_COLOR;
2757
+ }
2758
+
2759
+ // src/composites/detection-canvas.tsx
2760
+ var import_react25 = require("react");
2761
+ var import_jsx_runtime42 = require("react/jsx-runtime");
2762
+ var DEFAULT_CLASS_COLORS = CLASS_COLORS;
2763
+ function DetectionCanvas({
2764
+ src,
2765
+ imageWidth,
2766
+ imageHeight,
2767
+ detections = [],
2768
+ classColors,
2769
+ aspectRatio,
2770
+ className,
2771
+ placeholder,
2772
+ showConfidence = true,
2773
+ minConfidence = 0,
2774
+ borderWidth = 2
2775
+ }) {
2776
+ function getColor(className2) {
2777
+ return getClassColor(className2, classColors);
2778
+ }
2779
+ const ratio = aspectRatio ?? (imageWidth && imageHeight ? `${imageWidth}/${imageHeight}` : "16/9");
2780
+ const filteredDetections = detections.filter((d) => d.confidence >= minConfidence);
2781
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2782
+ "div",
2783
+ {
2784
+ className: cn(
2785
+ "rounded-lg border border-border bg-surface overflow-hidden relative",
2786
+ className
2787
+ ),
2788
+ style: { aspectRatio: ratio },
2789
+ children: src ? /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(import_jsx_runtime42.Fragment, { children: [
2790
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("img", { src, className: "absolute inset-0 w-full h-full object-fill", alt: "" }),
2791
+ filteredDetections.map(
2792
+ (d, i) => d.mask && d.maskWidth && d.maskHeight ? /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2793
+ MaskOverlay,
2794
+ {
2795
+ mask: d.mask,
2796
+ maskWidth: d.maskWidth,
2797
+ maskHeight: d.maskHeight,
2798
+ bbox: d.bbox,
2799
+ imageWidth,
2800
+ imageHeight,
2801
+ color: getColor(d.className)
2802
+ },
2803
+ `mask-${i}`
2804
+ ) : null
2805
+ ),
2806
+ filteredDetections.map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2807
+ BoundingBox,
2808
+ {
2809
+ detection: d,
2810
+ imageWidth,
2811
+ imageHeight,
2812
+ color: getColor(d.className),
2813
+ showConfidence,
2814
+ borderWidth: d.mask ? 1 : borderWidth,
2815
+ children: d.children?.filter((c) => {
2816
+ if (c.confidence < minConfidence) return false;
2817
+ const [cx1, cy1, cx2, cy2] = c.bbox;
2818
+ const cw = cx2 - cx1;
2819
+ const ch = cy2 - cy1;
2820
+ if (cw <= 0 || ch <= 0) return false;
2821
+ const [px1, py1, px2, py2] = d.bbox;
2822
+ const pw = px2 - px1;
2823
+ const ph = py2 - py1;
2824
+ if (pw > 0 && ph > 0 && cw * ch / (pw * ph) > 0.8) return false;
2825
+ return true;
2826
+ }).map((child, j) => /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2827
+ ChildBoundingBox,
2828
+ {
2829
+ child,
2830
+ parentBbox: d.bbox,
2831
+ color: getColor(child.className),
2832
+ showConfidence
2833
+ },
2834
+ `child-${j}`
2835
+ ))
2836
+ },
2837
+ `det-${i}`
2838
+ ))
2839
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime42.jsx)("div", { className: "w-full h-full flex items-center justify-center text-foreground-subtle text-sm", children: placeholder ?? "No image loaded" })
2840
+ }
2841
+ );
2842
+ }
2843
+ function BoundingBox({
2844
+ detection,
2845
+ imageWidth,
2846
+ imageHeight,
2847
+ color,
2848
+ showConfidence,
2849
+ borderWidth,
2850
+ children
2851
+ }) {
2852
+ const [x1, y1, x2, y2] = detection.bbox;
2853
+ const labelCount = 1 + (detection.labelsData?.length ?? 0);
2854
+ const labelHeightPx = labelCount * 16 + 4;
2855
+ const topPct = y1 / imageHeight * 100;
2856
+ const containerRef = (0, import_react25.useRef)(null);
2857
+ const showBelow = topPct < labelHeightPx / imageHeight * 100 * 1.5;
2858
+ const labelsElement = /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2859
+ "div",
2860
+ {
2861
+ className: `absolute left-0 flex flex-col items-start gap-px ${showBelow ? "" : ""}`,
2862
+ style: showBelow ? { top: "100%", marginTop: "2px" } : { bottom: "100%", marginBottom: "2px" },
2863
+ children: [
2864
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2865
+ "span",
2866
+ {
2867
+ className: "text-[10px] px-1 rounded-sm whitespace-nowrap text-white",
2868
+ style: { backgroundColor: color },
2869
+ children: [
2870
+ detection.className,
2871
+ showConfidence && ` ${(detection.confidence * 100).toFixed(0)}%`
2872
+ ]
2873
+ }
2874
+ ),
2875
+ detection.labelsData?.map((l, k) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2876
+ "span",
2877
+ {
2878
+ className: "text-[9px] font-semibold px-1 rounded-sm whitespace-nowrap text-white",
2879
+ style: { backgroundColor: getClassColor(l.addonId ?? l.label) },
2880
+ children: [
2881
+ l.label,
2882
+ " ",
2883
+ (l.score * 100).toFixed(0),
2884
+ "%"
2885
+ ]
2886
+ },
2887
+ k
2888
+ ))
2889
+ ]
2890
+ }
2891
+ );
2892
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2893
+ "div",
2894
+ {
2895
+ ref: containerRef,
2896
+ className: "absolute rounded-sm",
2897
+ style: {
2898
+ left: `${x1 / imageWidth * 100}%`,
2899
+ top: `${y1 / imageHeight * 100}%`,
2900
+ width: `${(x2 - x1) / imageWidth * 100}%`,
2901
+ height: `${(y2 - y1) / imageHeight * 100}%`,
2902
+ borderWidth: `${borderWidth}px`,
2903
+ borderStyle: "solid",
2904
+ borderColor: color
2905
+ },
2906
+ children: [
2907
+ labelsElement,
2908
+ children
2909
+ ]
2910
+ }
2911
+ );
2912
+ }
2913
+ function MaskOverlay({
2914
+ mask,
2915
+ maskWidth,
2916
+ maskHeight,
2917
+ bbox,
2918
+ imageWidth,
2919
+ imageHeight,
2920
+ color
2921
+ }) {
2922
+ const canvasRef = (0, import_react25.useRef)(null);
2923
+ (0, import_react25.useEffect)(() => {
2924
+ const canvas = canvasRef.current;
2925
+ if (!canvas) return;
2926
+ const ctx = canvas.getContext("2d");
2927
+ if (!ctx) return;
2928
+ const binary = atob(mask);
2929
+ const bytes = new Uint8Array(binary.length);
2930
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
2931
+ const r = parseInt(color.slice(1, 3), 16);
2932
+ const g = parseInt(color.slice(3, 5), 16);
2933
+ const b = parseInt(color.slice(5, 7), 16);
2934
+ canvas.width = maskWidth;
2935
+ canvas.height = maskHeight;
2936
+ const imageData = ctx.createImageData(maskWidth, maskHeight);
2937
+ const totalPixels = maskWidth * maskHeight;
2938
+ for (let i = 0; i < totalPixels; i++) {
2939
+ const val = i < bytes.length ? bytes[i] : 0;
2940
+ const px = i * 4;
2941
+ if (val > 0) {
2942
+ imageData.data[px] = r;
2943
+ imageData.data[px + 1] = g;
2944
+ imageData.data[px + 2] = b;
2945
+ imageData.data[px + 3] = 120;
2946
+ }
2947
+ }
2948
+ ctx.putImageData(imageData, 0, 0);
2949
+ }, [mask, maskWidth, maskHeight, color]);
2950
+ const [x1, y1, x2, y2] = bbox;
2951
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2952
+ "canvas",
2953
+ {
2954
+ ref: canvasRef,
2955
+ className: "absolute pointer-events-none",
2956
+ style: {
2957
+ left: `${x1 / imageWidth * 100}%`,
2958
+ top: `${y1 / imageHeight * 100}%`,
2959
+ width: `${(x2 - x1) / imageWidth * 100}%`,
2960
+ height: `${(y2 - y1) / imageHeight * 100}%`,
2961
+ imageRendering: "pixelated"
2962
+ }
2963
+ }
2964
+ );
2965
+ }
2966
+ function ChildBoundingBox({
2967
+ child,
2968
+ parentBbox,
2969
+ color,
2970
+ showConfidence
2971
+ }) {
2972
+ const [px1, py1, px2, py2] = parentBbox;
2973
+ const [cx1, cy1, cx2, cy2] = child.bbox;
2974
+ const pw = px2 - px1;
2975
+ const ph = py2 - py1;
2976
+ if (pw <= 0 || ph <= 0) return null;
2977
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsx)(
2978
+ "div",
2979
+ {
2980
+ className: "absolute rounded-sm",
2981
+ style: {
2982
+ left: `${Math.max(0, (cx1 - px1) / pw * 100)}%`,
2983
+ top: `${Math.max(0, (cy1 - py1) / ph * 100)}%`,
2984
+ width: `${Math.min(100, (cx2 - cx1) / pw * 100)}%`,
2985
+ height: `${Math.min(100, (cy2 - cy1) / ph * 100)}%`,
2986
+ borderWidth: "1px",
2987
+ borderStyle: "solid",
2988
+ borderColor: color
2989
+ },
2990
+ children: (() => {
2991
+ const labelCount = 1 + (child.labelsData?.length ?? 0);
2992
+ const relTop = (cy1 - py1) / ph * 100;
2993
+ const showBelow = relTop < labelCount * 6;
2994
+ return /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
2995
+ "div",
2996
+ {
2997
+ className: "absolute left-0 flex flex-col items-start gap-px",
2998
+ style: showBelow ? { top: "100%", marginTop: "1px" } : { bottom: "100%", marginBottom: "1px" },
2999
+ children: [
3000
+ /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3001
+ "span",
3002
+ {
3003
+ className: "text-[9px] px-0.5 rounded-sm whitespace-nowrap text-white",
3004
+ style: { backgroundColor: color },
3005
+ children: [
3006
+ child.className,
3007
+ showConfidence && ` ${(child.confidence * 100).toFixed(0)}%`
3008
+ ]
3009
+ }
3010
+ ),
3011
+ child.labelsData?.map((l, k) => /* @__PURE__ */ (0, import_jsx_runtime42.jsxs)(
3012
+ "span",
3013
+ {
3014
+ className: "text-[8px] font-semibold px-0.5 rounded-sm whitespace-nowrap text-white",
3015
+ style: { backgroundColor: getClassColor(l.addonId ?? l.label) },
3016
+ children: [
3017
+ l.label,
3018
+ " ",
3019
+ (l.score * 100).toFixed(0),
3020
+ "%"
3021
+ ]
3022
+ },
3023
+ k
3024
+ ))
3025
+ ]
3026
+ }
3027
+ );
3028
+ })()
3029
+ }
3030
+ );
3031
+ }
3032
+
3033
+ // src/composites/detection-result-tree.tsx
3034
+ var import_jsx_runtime43 = require("react/jsx-runtime");
3035
+ function DetectionResultTree({
3036
+ detections,
3037
+ classColors,
3038
+ className,
3039
+ hiddenKeys,
3040
+ onToggleVisibility
3041
+ }) {
3042
+ const colors = classColors;
3043
+ if (detections.length === 0) {
3044
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "text-sm text-foreground-subtle italic text-center py-4", children: "No detections" });
3045
+ }
3046
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className, children: [
3047
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "text-xs font-medium text-foreground-subtle uppercase tracking-wide mb-2", children: [
3048
+ "Detections (",
3049
+ detections.length,
3050
+ ")"
3051
+ ] }),
3052
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "space-y-2", children: detections.map((d, i) => /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3053
+ DetectionNode,
3054
+ {
3055
+ detection: d,
3056
+ path: String(i),
3057
+ colors,
3058
+ hiddenKeys,
3059
+ onToggleVisibility
3060
+ },
3061
+ i
3062
+ )) })
3063
+ ] });
3064
+ }
3065
+ function DetectionNode({
3066
+ detection,
3067
+ path,
3068
+ colors,
3069
+ hiddenKeys,
3070
+ onToggleVisibility
3071
+ }) {
3072
+ const color = getClassColor(detection.className, colors);
3073
+ const isVisible = !hiddenKeys?.has(path);
3074
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: `rounded-md border border-border bg-surface p-3 space-y-1 ${isVisible ? "" : "opacity-40"}`, children: [
3075
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex justify-between items-center", children: [
3076
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex items-center gap-2", children: [
3077
+ onToggleVisibility && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3078
+ "input",
3079
+ {
3080
+ type: "checkbox",
3081
+ checked: isVisible,
3082
+ onChange: () => onToggleVisibility(path, !isVisible),
3083
+ className: "h-3.5 w-3.5 rounded border-border accent-primary cursor-pointer shrink-0"
3084
+ }
3085
+ ),
3086
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3087
+ "span",
3088
+ {
3089
+ className: "h-2.5 w-2.5 rounded-full shrink-0",
3090
+ style: { backgroundColor: color }
3091
+ }
3092
+ ),
3093
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "text-sm font-medium text-foreground", children: detection.className }),
3094
+ detection.mask && detection.maskWidth && detection.maskHeight && /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "text-[9px] font-mono px-1 py-0.5 rounded bg-primary/10 text-primary", children: [
3095
+ "mask ",
3096
+ detection.maskWidth,
3097
+ "x",
3098
+ detection.maskHeight
3099
+ ] })
3100
+ ] }),
3101
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(ConfidenceBadge, { confidence: detection.confidence })
3102
+ ] }),
3103
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "text-[10px] text-foreground-subtle font-mono", children: [
3104
+ "bbox: [",
3105
+ detection.bbox.map((v) => Math.round(v)).join(", "),
3106
+ "]"
3107
+ ] }),
3108
+ detection.labelsData && detection.labelsData.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "flex flex-wrap gap-1 mt-1", children: detection.labelsData.map((l, k) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3109
+ "span",
3110
+ {
3111
+ className: "inline-flex items-center gap-1 text-[10px] font-medium px-1.5 py-0.5 rounded-full",
3112
+ style: { backgroundColor: getClassColor(l.addonId ?? l.label, colors) + "20", color: getClassColor(l.addonId ?? l.label, colors) },
3113
+ children: [
3114
+ l.label,
3115
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "opacity-60", children: [
3116
+ (l.score * 100).toFixed(0),
3117
+ "%"
3118
+ ] }),
3119
+ l.addonId && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "opacity-40 text-[8px]", children: l.addonId })
3120
+ ]
3121
+ },
3122
+ k
3123
+ )) }),
3124
+ detection.children && detection.children.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3125
+ ChildrenTree,
3126
+ {
3127
+ children: detection.children,
3128
+ parentPath: path,
3129
+ colors,
3130
+ hiddenKeys,
3131
+ onToggleVisibility
3132
+ }
3133
+ )
3134
+ ] });
3135
+ }
3136
+ function ChildrenTree({
3137
+ children,
3138
+ parentPath,
3139
+ colors,
3140
+ hiddenKeys,
3141
+ onToggleVisibility
3142
+ }) {
3143
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "ml-4 mt-1.5 space-y-1.5 border-l-2 border-border pl-3", children: children.map((child, j) => {
3144
+ const childPath = `${parentPath}.${j}`;
3145
+ const childColor = getClassColor(child.className, colors);
3146
+ const isVisible = !hiddenKeys?.has(childPath);
3147
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: `text-xs space-y-0.5 ${isVisible ? "" : "opacity-40"}`, children: [
3148
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("div", { className: "flex items-center gap-1.5", children: [
3149
+ onToggleVisibility && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3150
+ "input",
3151
+ {
3152
+ type: "checkbox",
3153
+ checked: isVisible,
3154
+ onChange: () => onToggleVisibility(childPath, !isVisible),
3155
+ className: "h-3 w-3 rounded border-border accent-primary cursor-pointer shrink-0"
3156
+ }
3157
+ ),
3158
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3159
+ "span",
3160
+ {
3161
+ className: "h-1.5 w-1.5 rounded-full shrink-0",
3162
+ style: { backgroundColor: childColor }
3163
+ }
3164
+ ),
3165
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("span", { className: "font-medium", style: { color: childColor }, children: child.className }),
3166
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "text-foreground-subtle", children: [
3167
+ (child.confidence * 100).toFixed(0),
3168
+ "%"
3169
+ ] }),
3170
+ child.mask && child.maskWidth && child.maskHeight && /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "text-[9px] font-mono px-1 py-0.5 rounded bg-primary/10 text-primary", children: [
3171
+ "mask ",
3172
+ child.maskWidth,
3173
+ "x",
3174
+ child.maskHeight
3175
+ ] })
3176
+ ] }),
3177
+ child.labelsData && child.labelsData.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)("div", { className: "flex flex-wrap gap-1 ml-5 mt-0.5", children: child.labelsData.map((l, k) => /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)(
3178
+ "span",
3179
+ {
3180
+ className: "inline-flex items-center gap-0.5 text-[9px] font-medium px-1 py-0.5 rounded-full",
3181
+ style: { backgroundColor: getClassColor(l.addonId ?? l.label, colors) + "20", color: getClassColor(l.addonId ?? l.label, colors) },
3182
+ children: [
3183
+ l.label,
3184
+ " ",
3185
+ /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: "opacity-60", children: [
3186
+ (l.score * 100).toFixed(0),
3187
+ "%"
3188
+ ] })
3189
+ ]
3190
+ },
3191
+ k
3192
+ )) }),
3193
+ child.children && child.children.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime43.jsx)(
3194
+ ChildrenTree,
3195
+ {
3196
+ children: child.children,
3197
+ parentPath: childPath,
3198
+ colors,
3199
+ hiddenKeys,
3200
+ onToggleVisibility
3201
+ }
3202
+ )
3203
+ ] }, j);
3204
+ }) });
3205
+ }
3206
+ function ConfidenceBadge({ confidence }) {
3207
+ const level = confidence >= 0.8 ? "bg-success/10 text-success" : confidence >= 0.5 ? "bg-warning/10 text-warning" : "bg-danger/10 text-danger";
3208
+ return /* @__PURE__ */ (0, import_jsx_runtime43.jsxs)("span", { className: `text-xs font-medium px-2 py-0.5 rounded-full ${level}`, children: [
3209
+ (confidence * 100).toFixed(1),
3210
+ "%"
3211
+ ] });
3212
+ }
3213
+
3214
+ // src/composites/step-timings.tsx
3215
+ var import_jsx_runtime44 = require("react/jsx-runtime");
3216
+ function StepTimings({ timings, totalMs, className }) {
3217
+ const entries = Object.entries(timings);
3218
+ if (entries.length === 0 && totalMs === void 0) return null;
3219
+ return /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: `rounded-lg border border-border bg-surface p-3 space-y-2 ${className ?? ""}`, children: [
3220
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("div", { className: "text-xs font-medium text-foreground-subtle uppercase tracking-wide", children: "Timings" }),
3221
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "space-y-1 text-xs", children: [
3222
+ entries.map(([step, ms]) => /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex justify-between", children: [
3223
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { className: "text-foreground-subtle", children: step }),
3224
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("span", { className: "font-mono text-foreground", children: [
3225
+ ms.toFixed(1),
3226
+ "ms"
3227
+ ] })
3228
+ ] }, step)),
3229
+ totalMs !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("div", { className: "flex justify-between pt-1 border-t border-border font-medium text-foreground", children: [
3230
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsx)("span", { children: "Total" }),
3231
+ /* @__PURE__ */ (0, import_jsx_runtime44.jsxs)("span", { className: "font-mono", children: [
3232
+ totalMs.toFixed(1),
3233
+ "ms"
3234
+ ] })
3235
+ ] })
3236
+ ] })
3237
+ ] });
3238
+ }
3239
+
3240
+ // src/composites/image-selector.tsx
3241
+ var import_jsx_runtime45 = require("react/jsx-runtime");
3242
+ function ImageSelector({
3243
+ images,
3244
+ selectedFilename,
3245
+ uploadedName,
3246
+ onSelect,
3247
+ onUpload,
3248
+ className
3249
+ }) {
3250
+ const handleUploadClick = () => {
3251
+ const input = document.createElement("input");
3252
+ input.type = "file";
3253
+ input.accept = "image/*";
3254
+ input.onchange = (ev) => {
3255
+ const file = ev.target.files?.[0];
3256
+ if (!file) return;
3257
+ const reader = new FileReader();
3258
+ reader.onload = (e) => {
3259
+ const dataUrl = e.target?.result;
3260
+ const b64 = dataUrl.split(",")[1];
3261
+ if (b64) onUpload(b64, file.name, dataUrl);
3262
+ };
3263
+ reader.readAsDataURL(file);
3264
+ };
3265
+ input.click();
3266
+ };
3267
+ return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: `flex flex-wrap items-center gap-2 ${className ?? ""}`, children: [
3268
+ images.map((img) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3269
+ "button",
3270
+ {
3271
+ onClick: () => onSelect(img.filename),
3272
+ className: `px-3 py-1.5 text-xs rounded-md border transition-colors ${selectedFilename === img.filename ? "bg-primary text-primary-foreground border-primary" : "bg-surface border-border text-foreground hover:border-primary/50"}`,
3273
+ children: img.id
3274
+ },
3275
+ img.filename
3276
+ )),
3277
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3278
+ "button",
3279
+ {
3280
+ onClick: handleUploadClick,
3281
+ className: "px-3 py-1.5 text-xs rounded-md border border-border bg-surface text-foreground hover:bg-surface-hover transition-colors",
3282
+ children: "Upload..."
3283
+ }
3284
+ ),
3285
+ uploadedName && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)("span", { className: "text-xs text-foreground-subtle", children: uploadedName })
3286
+ ] });
3287
+ }
3288
+
3289
+ // src/composites/inference-config-selector.tsx
3290
+ var import_jsx_runtime46 = require("react/jsx-runtime");
3291
+ var SELECT_CLASS = "w-full px-3 py-2 text-sm rounded-md border border-border bg-surface text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50";
3292
+ function InferenceConfigSelector({
3293
+ runtime,
3294
+ backend,
3295
+ modelId,
3296
+ agentId = "hub",
3297
+ runtimes,
3298
+ backends,
3299
+ models,
3300
+ agents = [],
3301
+ onRuntimeChange,
3302
+ onBackendChange,
3303
+ onModelChange,
3304
+ onAgentChange,
3305
+ layout = "grid",
3306
+ className,
3307
+ showAgent = false
3308
+ }) {
3309
+ const containerClass = layout === "grid" ? "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4" : layout === "horizontal" ? "flex flex-wrap items-end gap-4" : "space-y-3";
3310
+ return /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("div", { className: `${containerClass} ${className ?? ""}`, children: [
3311
+ showAgent && agents.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("label", { className: "space-y-1", children: [
3312
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "text-xs font-medium text-foreground-subtle", children: "Agent" }),
3313
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3314
+ "select",
3315
+ {
3316
+ value: agentId,
3317
+ onChange: (e) => onAgentChange?.(e.target.value),
3318
+ className: SELECT_CLASS,
3319
+ children: agents.map((a) => /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("option", { value: a.id, children: [
3320
+ a.name,
3321
+ " (",
3322
+ a.status,
3323
+ ")"
3324
+ ] }, a.id))
3325
+ }
3326
+ )
3327
+ ] }),
3328
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("label", { className: "space-y-1", children: [
3329
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "text-xs font-medium text-foreground-subtle", children: "Runtime" }),
3330
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3331
+ "select",
3332
+ {
3333
+ value: runtime,
3334
+ onChange: (e) => onRuntimeChange(e.target.value),
3335
+ className: SELECT_CLASS,
3336
+ children: runtimes.map((r) => /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("option", { value: r.value, disabled: !r.available, children: [
3337
+ r.label,
3338
+ !r.available ? " (unavailable)" : ""
3339
+ ] }, r.value))
3340
+ }
3341
+ )
3342
+ ] }),
3343
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("label", { className: "space-y-1", children: [
3344
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "text-xs font-medium text-foreground-subtle", children: "Backend" }),
3345
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3346
+ "select",
3347
+ {
3348
+ value: backend,
3349
+ onChange: (e) => onBackendChange(e.target.value),
3350
+ className: SELECT_CLASS,
3351
+ children: backends.map((b) => /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("option", { value: b.id, disabled: !b.available, children: [
3352
+ b.label,
3353
+ !b.available ? " (unavailable)" : ""
3354
+ ] }, b.id))
3355
+ }
3356
+ )
3357
+ ] }),
3358
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("label", { className: "space-y-1", children: [
3359
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("span", { className: "text-xs font-medium text-foreground-subtle", children: "Model" }),
3360
+ /* @__PURE__ */ (0, import_jsx_runtime46.jsx)(
3361
+ "select",
3362
+ {
3363
+ value: modelId,
3364
+ onChange: (e) => onModelChange(e.target.value),
3365
+ className: SELECT_CLASS,
3366
+ children: models.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime46.jsx)("option", { value: "", children: "No compatible models" }) : models.map((m) => /* @__PURE__ */ (0, import_jsx_runtime46.jsxs)("option", { value: m.id, children: [
3367
+ m.name,
3368
+ m.downloaded ? " \u2713" : ""
3369
+ ] }, m.id))
3370
+ }
3371
+ )
3372
+ ] })
3373
+ ] });
3374
+ }
3375
+
3376
+ // src/composites/mount-addon-page.tsx
3377
+ var import_react28 = require("react");
3378
+ var import_client2 = require("react-dom/client");
3379
+
3380
+ // src/composites/dev-shell.tsx
3381
+ var import_react27 = require("react");
3382
+ var import_client = require("@trpc/client");
3383
+ var import_superjson = __toESM(require("superjson"), 1);
3384
+
3385
+ // src/composites/login-form.tsx
3386
+ var import_react26 = require("react");
3387
+ var import_jsx_runtime47 = require("react/jsx-runtime");
3388
+ function EyeIcon({ className }) {
3389
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3390
+ "svg",
3391
+ {
3392
+ xmlns: "http://www.w3.org/2000/svg",
3393
+ viewBox: "0 0 24 24",
3394
+ fill: "none",
3395
+ stroke: "currentColor",
3396
+ strokeWidth: "2",
3397
+ strokeLinecap: "round",
3398
+ strokeLinejoin: "round",
3399
+ className,
3400
+ children: [
3401
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("path", { d: "M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" }),
3402
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("circle", { cx: "12", cy: "12", r: "3" })
3403
+ ]
3404
+ }
3405
+ );
3406
+ }
3407
+ function EyeOffIcon({ className }) {
3408
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3409
+ "svg",
3410
+ {
3411
+ xmlns: "http://www.w3.org/2000/svg",
3412
+ viewBox: "0 0 24 24",
3413
+ fill: "none",
3414
+ stroke: "currentColor",
3415
+ strokeWidth: "2",
3416
+ strokeLinecap: "round",
3417
+ strokeLinejoin: "round",
3418
+ className,
3419
+ children: [
3420
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("path", { d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" }),
3421
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("path", { d: "M14.084 14.158a3 3 0 0 1-4.242-4.242" }),
3422
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("path", { d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" }),
3423
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("path", { d: "m2 2 20 20" })
3424
+ ]
3425
+ }
3426
+ );
3427
+ }
3428
+ function SpinnerIcon({ className }) {
3429
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3430
+ "svg",
3431
+ {
3432
+ xmlns: "http://www.w3.org/2000/svg",
3433
+ viewBox: "0 0 24 24",
3434
+ fill: "none",
3435
+ stroke: "currentColor",
3436
+ strokeWidth: "2",
3437
+ strokeLinecap: "round",
3438
+ strokeLinejoin: "round",
3439
+ className,
3440
+ children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" })
3441
+ }
3442
+ );
3443
+ }
3444
+ function LoginForm({
3445
+ onLogin,
3446
+ serverUrl,
3447
+ logoSrc,
3448
+ error: externalError,
3449
+ className
3450
+ }) {
3451
+ const [username, setUsername] = (0, import_react26.useState)("");
3452
+ const [password, setPassword] = (0, import_react26.useState)("");
3453
+ const [showPassword, setShowPassword] = (0, import_react26.useState)(false);
3454
+ const [submitting, setSubmitting] = (0, import_react26.useState)(false);
3455
+ const [internalError, setInternalError] = (0, import_react26.useState)(null);
3456
+ const error = externalError ?? internalError;
3457
+ const handleSubmit = async (e) => {
3458
+ e.preventDefault();
3459
+ if (submitting) return;
3460
+ setInternalError(null);
3461
+ setSubmitting(true);
3462
+ try {
3463
+ await onLogin(username, password);
3464
+ } catch (err) {
3465
+ const message = err instanceof Error ? err.message : "Login failed. Please try again.";
3466
+ setInternalError(message);
3467
+ } finally {
3468
+ setSubmitting(false);
3469
+ }
3470
+ };
3471
+ return /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3472
+ "div",
3473
+ {
3474
+ className: cn(
3475
+ "flex min-h-screen items-center justify-center bg-background p-4",
3476
+ className
3477
+ ),
3478
+ children: /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "w-full max-w-sm", children: [
3479
+ logoSrc && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("div", { className: "flex justify-center mb-8", children: /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("img", { src: logoSrc, alt: "Logo", className: "h-12" }) }),
3480
+ serverUrl && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("p", { className: "mb-4 text-center text-xs text-foreground-subtle truncate", children: serverUrl }),
3481
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3482
+ "form",
3483
+ {
3484
+ onSubmit: handleSubmit,
3485
+ className: "space-y-4 rounded-xl border border-border bg-surface p-6 shadow-xl shadow-black/10",
3486
+ children: [
3487
+ error && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("div", { className: "rounded-md bg-danger/10 border border-danger/20 px-3 py-2 text-xs text-danger", children: error }),
3488
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "space-y-1.5", children: [
3489
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("label", { className: "text-xs font-medium text-foreground-subtle", children: "Username" }),
3490
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3491
+ "input",
3492
+ {
3493
+ type: "text",
3494
+ value: username,
3495
+ onChange: (e) => setUsername(e.target.value),
3496
+ autoComplete: "username",
3497
+ required: true,
3498
+ className: "w-full rounded-lg border border-border bg-background px-3 py-2.5 text-sm text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
3499
+ }
3500
+ )
3501
+ ] }),
3502
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "space-y-1.5", children: [
3503
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)("label", { className: "text-xs font-medium text-foreground-subtle", children: "Password" }),
3504
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)("div", { className: "relative", children: [
3505
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3506
+ "input",
3507
+ {
3508
+ type: showPassword ? "text" : "password",
3509
+ value: password,
3510
+ onChange: (e) => setPassword(e.target.value),
3511
+ autoComplete: "current-password",
3512
+ required: true,
3513
+ className: "w-full rounded-lg border border-border bg-background px-3 py-2.5 pr-10 text-sm text-foreground placeholder:text-foreground-subtle focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
3514
+ }
3515
+ ),
3516
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(
3517
+ "button",
3518
+ {
3519
+ type: "button",
3520
+ onClick: () => setShowPassword((prev) => !prev),
3521
+ className: "absolute right-2.5 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground",
3522
+ tabIndex: -1,
3523
+ children: showPassword ? /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(EyeOffIcon, { className: "h-4 w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(EyeIcon, { className: "h-4 w-4" })
3524
+ }
3525
+ )
3526
+ ] })
3527
+ ] }),
3528
+ /* @__PURE__ */ (0, import_jsx_runtime47.jsxs)(
3529
+ "button",
3530
+ {
3531
+ type: "submit",
3532
+ disabled: submitting,
3533
+ className: "w-full rounded-lg bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2",
3534
+ children: [
3535
+ submitting && /* @__PURE__ */ (0, import_jsx_runtime47.jsx)(SpinnerIcon, { className: "h-4 w-4 animate-spin" }),
3536
+ submitting ? "Logging in..." : "Log in"
3537
+ ]
3538
+ }
3539
+ )
3540
+ ]
3541
+ }
3542
+ )
3543
+ ] })
3544
+ }
3545
+ );
3546
+ }
3547
+
3548
+ // src/composites/dev-shell.tsx
3549
+ var import_jsx_runtime48 = require("react/jsx-runtime");
3550
+ var STORAGE_KEY = "camstack_dev_token";
3551
+ var DevShellContext = (0, import_react27.createContext)(null);
3552
+ function useDevShell() {
3553
+ const ctx = (0, import_react27.useContext)(DevShellContext);
3554
+ if (!ctx) {
3555
+ throw new Error("useDevShell must be used within a DevShell");
3556
+ }
3557
+ return ctx;
3558
+ }
3559
+ function getStoredToken() {
3560
+ if (typeof window === "undefined") return null;
3561
+ return localStorage.getItem(STORAGE_KEY);
3562
+ }
3563
+ function SunIcon({ className }) {
3564
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3565
+ "svg",
3566
+ {
3567
+ xmlns: "http://www.w3.org/2000/svg",
3568
+ viewBox: "0 0 24 24",
3569
+ fill: "none",
3570
+ stroke: "currentColor",
3571
+ strokeWidth: "2",
3572
+ strokeLinecap: "round",
3573
+ strokeLinejoin: "round",
3574
+ className,
3575
+ children: [
3576
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("circle", { cx: "12", cy: "12", r: "4" }),
3577
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("path", { d: "M12 2v2" }),
3578
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("path", { d: "M12 20v2" }),
3579
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("path", { d: "m4.93 4.93 1.41 1.41" }),
3580
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("path", { d: "m17.66 17.66 1.41 1.41" }),
3581
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("path", { d: "M2 12h2" }),
3582
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("path", { d: "M20 12h2" }),
3583
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("path", { d: "m6.34 17.66-1.41 1.41" }),
3584
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("path", { d: "m19.07 4.93-1.41 1.41" })
3585
+ ]
3586
+ }
3587
+ );
3588
+ }
3589
+ function MoonIcon({ className }) {
3590
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3591
+ "svg",
3592
+ {
3593
+ xmlns: "http://www.w3.org/2000/svg",
3594
+ viewBox: "0 0 24 24",
3595
+ fill: "none",
3596
+ stroke: "currentColor",
3597
+ strokeWidth: "2",
3598
+ strokeLinecap: "round",
3599
+ strokeLinejoin: "round",
3600
+ className,
3601
+ children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("path", { d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" })
3602
+ }
3603
+ );
3604
+ }
3605
+ function DevShellInner({
3606
+ children,
3607
+ serverUrl,
3608
+ title,
3609
+ token,
3610
+ onLogout
3611
+ }) {
3612
+ const theme = useThemeMode();
3613
+ const trpc = (0, import_react27.useMemo)(
3614
+ () => {
3615
+ const wsUrl = serverUrl.replace(/^http/, "ws") + "/trpc";
3616
+ const wsClient = (0, import_client.createWSClient)({
3617
+ url: wsUrl,
3618
+ connectionParams: () => ({ token })
3619
+ });
3620
+ return (0, import_client.createTRPCClient)({
3621
+ links: [
3622
+ (0, import_client.splitLink)({
3623
+ condition: (op) => op.type === "subscription",
3624
+ true: (0, import_client.wsLink)({ client: wsClient, transformer: import_superjson.default }),
3625
+ false: (0, import_client.httpLink)({
3626
+ url: `${serverUrl}/trpc`,
3627
+ transformer: import_superjson.default,
3628
+ headers: () => ({ authorization: `Bearer ${token}` })
3629
+ })
3630
+ })
3631
+ ]
3632
+ });
3633
+ },
3634
+ [serverUrl, token]
3635
+ );
3636
+ const contextValue = (0, import_react27.useMemo)(
3637
+ () => ({ trpc, token, logout: onLogout }),
3638
+ [trpc, token, onLogout]
3639
+ );
3640
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(DevShellContext.Provider, { value: contextValue, children: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "min-h-screen bg-background text-foreground", children: [
3641
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center justify-between border-b border-border bg-surface px-4 py-2", children: [
3642
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-2", children: [
3643
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "rounded bg-warning/20 px-2 py-0.5 text-xs font-bold text-warning", children: "DEV MODE" }),
3644
+ title && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-sm font-medium text-foreground", children: title }),
3645
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-xs text-foreground-subtle", children: serverUrl })
3646
+ ] }),
3647
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-2", children: [
3648
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
3649
+ "button",
3650
+ {
3651
+ type: "button",
3652
+ onClick: theme.toggleMode,
3653
+ className: "flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors",
3654
+ title: `Theme: ${theme.mode}`,
3655
+ children: [
3656
+ theme.resolvedMode === "dark" ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SunIcon, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(MoonIcon, { className: "h-3.5 w-3.5" }),
3657
+ theme.mode === "dark" ? "Dark" : theme.mode === "light" ? "Light" : "System"
3658
+ ]
3659
+ }
3660
+ ),
3661
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3662
+ "button",
3663
+ {
3664
+ type: "button",
3665
+ onClick: onLogout,
3666
+ className: "rounded-md px-2.5 py-1 text-xs font-medium text-danger hover:bg-danger/10 transition-colors",
3667
+ children: "Logout"
3668
+ }
3669
+ )
3670
+ ] })
3671
+ ] }),
3672
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "p-4", children: children({ trpc, theme }) })
3673
+ ] }) });
3674
+ }
3675
+ function DevShell({
3676
+ children,
3677
+ serverUrl = "https://localhost:4443",
3678
+ title
3679
+ }) {
3680
+ const [token, setToken] = (0, import_react27.useState)(getStoredToken);
3681
+ const handleLogin = (0, import_react27.useCallback)(
3682
+ async (username, password) => {
3683
+ const anonClient = (0, import_client.createTRPCClient)({
3684
+ links: [
3685
+ (0, import_client.httpLink)({
3686
+ url: `${serverUrl}/trpc`,
3687
+ transformer: import_superjson.default
3688
+ })
3689
+ ]
3690
+ });
3691
+ const res = await anonClient.auth.login.mutate({ username, password });
3692
+ if (!res?.token) throw new Error("No token returned");
3693
+ localStorage.setItem(STORAGE_KEY, res.token);
3694
+ setToken(res.token);
3695
+ },
3696
+ [serverUrl]
3697
+ );
3698
+ const handleLogout = (0, import_react27.useCallback)(() => {
3699
+ localStorage.removeItem(STORAGE_KEY);
3700
+ setToken(null);
3701
+ }, []);
3702
+ if (!token) {
3703
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ThemeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(LoginForm, { onLogin: handleLogin, serverUrl }) });
3704
+ }
3705
+ return /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(ThemeProvider, { children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
3706
+ DevShellInner,
3707
+ {
3708
+ serverUrl,
3709
+ title,
3710
+ token,
3711
+ onLogout: handleLogout,
3712
+ children
3713
+ }
3714
+ ) });
3715
+ }
3716
+
3717
+ // src/composites/mount-addon-page.tsx
3718
+ function mountAddonPage(PageComponent, options = {}) {
3719
+ const {
3720
+ serverUrl = "https://localhost:4443",
3721
+ title,
3722
+ rootId = "root"
3723
+ } = options;
3724
+ const root = document.getElementById(rootId);
3725
+ if (!root) {
3726
+ console.error(`[mountAddonPage] Element #${rootId} not found`);
3727
+ return;
3728
+ }
3729
+ (0, import_client2.createRoot)(root).render(
3730
+ (0, import_react28.createElement)(DevShell, {
3731
+ serverUrl,
3732
+ title,
3733
+ children: ({ trpc, theme }) => (0, import_react28.createElement)(PageComponent, {
3734
+ trpc,
3735
+ theme: { isDark: theme.resolvedMode === "dark" },
3736
+ navigate: (path) => {
3737
+ console.log("[dev] navigate:", path);
3738
+ }
3739
+ })
3740
+ })
3741
+ );
3742
+ }
2023
3743
  // Annotate the CommonJS export names for ESM import in node:
2024
3744
  0 && (module.exports = {
2025
3745
  AppShell,
2026
3746
  Badge,
2027
3747
  Button,
3748
+ CLASS_COLORS,
2028
3749
  Card,
2029
3750
  Checkbox,
2030
3751
  CodeBlock,
2031
3752
  ConfirmDialog,
3753
+ DEFAULT_CLASS_COLORS,
3754
+ DEFAULT_COLOR,
2032
3755
  DataTable,
3756
+ DetectionCanvas,
3757
+ DetectionResultTree,
3758
+ DevShell,
2033
3759
  DeviceCard,
2034
3760
  DeviceGrid,
2035
3761
  Dialog,
@@ -2048,10 +3774,16 @@ function DeviceGrid({
2048
3774
  FloatingPanel,
2049
3775
  FormField,
2050
3776
  IconButton,
3777
+ ImageSelector,
3778
+ InferenceConfigSelector,
2051
3779
  Input,
2052
3780
  KeyValueList,
2053
3781
  Label,
3782
+ LoginForm,
2054
3783
  PageHeader,
3784
+ PipelineBuilder,
3785
+ PipelineRuntimeSelector,
3786
+ PipelineStep,
2055
3787
  Popover,
2056
3788
  PopoverContent,
2057
3789
  PopoverTrigger,
@@ -2064,6 +3796,7 @@ function DeviceGrid({
2064
3796
  Skeleton,
2065
3797
  StatCard,
2066
3798
  StatusBadge,
3799
+ StepTimings,
2067
3800
  Switch,
2068
3801
  Tabs,
2069
3802
  TabsContent,
@@ -2077,10 +3810,13 @@ function DeviceGrid({
2077
3810
  createTheme,
2078
3811
  darkColors,
2079
3812
  defaultTheme,
3813
+ getClassColor,
2080
3814
  lightColors,
3815
+ mountAddonPage,
2081
3816
  providerIcons,
2082
3817
  statusIcons,
2083
3818
  themeToCss,
3819
+ useDevShell,
2084
3820
  useThemeMode
2085
3821
  });
2086
3822
  //# sourceMappingURL=index.cjs.map