add state motion and interval configs to edit dialog

This commit is contained in:
Josh Hawkins 2026-07-12 08:42:16 -05:00
parent d14932c0af
commit 25087007d6
3 changed files with 88 additions and 1 deletions

View File

@ -69,9 +69,15 @@
"enabledDesc": "Run this model. When disabled, it stops running and no longer classifies.", "enabledDesc": "Run this model. When disabled, it stops running and no longer classifies.",
"saveAttempts": "Save Attempts", "saveAttempts": "Save Attempts",
"saveAttemptsDesc": "Number of classification attempt images to keep for the recent classifications UI.", "saveAttemptsDesc": "Number of classification attempt images to keep for the recent classifications UI.",
"motion": "Run on Motion",
"motionDesc": "Run classification when motion is detected within the configured crop.",
"interval": "Interval",
"intervalDesc": "Seconds between periodic classification runs. Leave empty to run only on motion.",
"intervalPlaceholder": "No interval",
"stateClassesInfo": "Model updated. Retrain the model for the class changes to take effect.", "stateClassesInfo": "Model updated. Retrain the model for the class changes to take effect.",
"errors": { "errors": {
"saveAttemptsInvalid": "Save attempts must be a whole number of 0 or greater" "saveAttemptsInvalid": "Save attempts must be a whole number of 0 or greater",
"intervalInvalid": "Interval must be a whole number greater than 0"
} }
}, },
"deleteDatasetImages": { "deleteDatasetImages": {

View File

@ -61,6 +61,8 @@ type ObjectFormData = {
type StateFormData = { type StateFormData = {
enabled: boolean; enabled: boolean;
saveAttempts: number; saveAttempts: number;
motion: boolean;
interval?: number;
classes: string[]; classes: string[];
}; };
@ -128,6 +130,16 @@ export default function ClassificationModelEditDialog({
// State model // State model
return z.object({ return z.object({
...sharedFields, ...sharedFields,
motion: z.boolean(),
interval: z.preprocess(
(val) =>
val === "" || val === null || val === undefined ? undefined : val,
z.coerce
.number({ message: t("edit.errors.intervalInvalid") })
.int(t("edit.errors.intervalInvalid"))
.positive(t("edit.errors.intervalInvalid"))
.optional(),
),
classes: z classes: z
.array(z.string()) .array(z.string())
.min(1, t("wizard.step1.errors.classRequired")) .min(1, t("wizard.step1.errors.classRequired"))
@ -164,6 +176,8 @@ export default function ClassificationModelEditDialog({
: ({ : ({
enabled: model.enabled, enabled: model.enabled,
saveAttempts: model.save_attempts ?? defaultSaveAttempts, saveAttempts: model.save_attempts ?? defaultSaveAttempts,
motion: model.state_config?.motion ?? false,
interval: model.state_config?.interval,
classes: [""], // Will be populated from dataset classes: [""], // Will be populated from dataset
} as StateFormData), } as StateFormData),
mode: "onChange", mode: "onChange",
@ -191,6 +205,8 @@ export default function ClassificationModelEditDialog({
form.reset({ form.reset({
enabled: model.enabled, enabled: model.enabled,
saveAttempts: model.save_attempts ?? defaultSaveAttempts, saveAttempts: model.save_attempts ?? defaultSaveAttempts,
motion: model.state_config?.motion ?? false,
interval: model.state_config?.interval,
classes: [""], classes: [""],
} as StateFormData); } as StateFormData);
} }
@ -274,6 +290,7 @@ export default function ClassificationModelEditDialog({
setIsSaving(true); setIsSaving(true);
try { try {
if (isObjectModel) { if (isObjectModel) {
// object model save
const objectData = data as ObjectFormData; const objectData = data as ObjectFormData;
// Update the config // Update the config
@ -302,8 +319,18 @@ export default function ClassificationModelEditDialog({
position: "top-center", position: "top-center",
}); });
} else { } else {
// state model save
const stateData = data as StateFormData; const stateData = data as StateFormData;
const stateConfig: { motion: boolean; interval?: number | null } = {
motion: stateData.motion,
};
if (stateData.interval != null) {
stateConfig.interval = stateData.interval;
} else if (model.state_config?.interval != null) {
stateConfig.interval = null;
}
await axios.put("/config/set", { await axios.put("/config/set", {
requires_restart: 0, requires_restart: 0,
update_topic: `config/classification/custom/${model.name}`, update_topic: `config/classification/custom/${model.name}`,
@ -313,6 +340,7 @@ export default function ClassificationModelEditDialog({
[model.name]: { [model.name]: {
enabled: stateData.enabled, enabled: stateData.enabled,
save_attempts: stateData.saveAttempts, save_attempts: stateData.saveAttempts,
state_config: stateConfig,
}, },
}, },
}, },
@ -601,6 +629,58 @@ export default function ClassificationModelEditDialog({
</div> </div>
)} )}
{isStateModel && (
<>
<FormField
control={form.control}
name="motion"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between gap-4">
<div className="space-y-0.5">
<FormLabel className="text-primary-variant">
{t("edit.motion")}
</FormLabel>
<FormDescription className="text-xs">
{t("edit.motionDesc")}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="interval"
render={({ field }) => (
<FormItem>
<FormLabel className="text-primary-variant">
{t("edit.interval")}
</FormLabel>
<FormControl>
<Input
className="h-8"
inputMode="numeric"
placeholder={t("edit.intervalPlaceholder")}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormDescription className="text-xs">
{t("edit.intervalDesc")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<FormField <FormField
control={form.control} control={form.control}
name="saveAttempts" name="saveAttempts"

View File

@ -370,6 +370,7 @@ export type CustomClassificationModelConfig = {
}; };
}; };
motion: boolean; motion: boolean;
interval?: number;
}; };
}; };