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