import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { useTranslation } from "react-i18next"; import { useMemo } from "react"; import { LuX, LuPlus, LuInfo, LuExternalLink } from "react-icons/lu"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { getTranslatedLabel } from "@/utils/i18n"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; export type ModelType = "state" | "object"; export type ObjectClassificationType = "sub_label" | "attribute"; export type Step1FormData = { modelName: string; modelType: ModelType; objectLabel?: string; objectType?: ObjectClassificationType; classes: string[]; }; type Step1NameAndDefineProps = { initialData?: Partial; defaultModelType?: "state" | "object"; onNext: (data: Step1FormData) => void; onCancel: () => void; }; export default function Step1NameAndDefine({ initialData, defaultModelType, onNext, onCancel, }: Step1NameAndDefineProps) { const { t } = useTranslation(["views/classificationModel"]); const { data: config } = useSWR("config"); const { getLocaleDocUrl } = useDocDomain(); const objectLabels = useMemo(() => { if (!config) return []; const labels = new Set(); Object.values(config.cameras).forEach((cameraConfig) => { if (!cameraConfig.enabled || !cameraConfig.enabled_in_config) { return; } cameraConfig.objects.track.forEach((label) => { if (!config.model.all_attributes.includes(label)) { labels.add(label); } }); }); return [...labels].sort(); }, [config]); const step1FormData = z .object({ modelName: z .string() .min(1, t("wizard.step1.errors.nameRequired")) .max(64, t("wizard.step1.errors.nameLength")) .refine((value) => !/^\d+$/.test(value), { message: t("wizard.step1.errors.nameOnlyNumbers"), }), modelType: z.enum(["state", "object"]), objectLabel: z.string().optional(), objectType: z.enum(["sub_label", "attribute"]).optional(), classes: z .array(z.string()) .min(1, t("wizard.step1.errors.classRequired")) .refine( (classes) => { const nonEmpty = classes.filter((c) => c.trim().length > 0); return nonEmpty.length >= 1; }, { message: t("wizard.step1.errors.classRequired") }, ) .refine( (classes) => { const nonEmpty = classes.filter((c) => c.trim().length > 0); const unique = new Set(nonEmpty.map((c) => c.toLowerCase())); return unique.size === nonEmpty.length; }, { message: t("wizard.step1.errors.classesUnique") }, ), }) .refine( (data) => { // State models require at least 2 classes if (data.modelType === "state") { const nonEmpty = data.classes.filter((c) => c.trim().length > 0); return nonEmpty.length >= 2; } return true; }, { message: t("wizard.step1.errors.stateRequiresTwoClasses"), path: ["classes"], }, ) .refine( (data) => { if (data.modelType === "object") { return data.objectLabel !== undefined && data.objectLabel !== ""; } return true; }, { message: t("wizard.step1.errors.objectLabelRequired"), path: ["objectLabel"], }, ) .refine( (data) => { if (data.modelType === "object") { return data.objectType !== undefined; } return true; }, { message: t("wizard.step1.errors.objectTypeRequired"), path: ["objectType"], }, ); const form = useForm>({ resolver: zodResolver(step1FormData), defaultValues: { modelName: initialData?.modelName || "", modelType: initialData?.modelType || defaultModelType || "state", objectLabel: initialData?.objectLabel, objectType: initialData?.objectType || "sub_label", classes: initialData?.classes?.length ? initialData.classes : [""], }, mode: "onChange", }); const watchedClasses = form.watch("classes"); const watchedModelType = form.watch("modelType"); const watchedObjectType = form.watch("objectType"); const handleAddClass = () => { const currentClasses = form.getValues("classes"); form.setValue("classes", [...currentClasses, ""], { shouldValidate: true }); }; const handleRemoveClass = (index: number) => { const currentClasses = form.getValues("classes"); const newClasses = currentClasses.filter((_, i) => i !== index); // Ensure at least one field remains (even if empty) if (newClasses.length === 0) { form.setValue("classes", [""], { shouldValidate: true }); } else { form.setValue("classes", newClasses, { shouldValidate: true }); } }; const onSubmit = (data: z.infer) => { // Filter out empty classes const filteredClasses = data.classes.filter((c) => c.trim().length > 0); onNext({ ...data, classes: filteredClasses, }); }; return (
( {t("wizard.step1.name")} )} /> ( {t("wizard.step1.type")}
)} /> {watchedModelType === "object" && ( <> ( {t("wizard.step1.objectLabel")} )} /> (
{t("wizard.step1.classificationType")}
{t("wizard.step1.classificationTypeDesc")}
)} /> )}
{t("wizard.step1.classes")}
{watchedModelType === "state" ? t("wizard.step1.classesStateDesc") : t("wizard.step1.classesObjectDesc")}
{watchedClasses.map((_, index) => ( (
{watchedClasses.length > 1 && ( )}
)} /> ))}
{form.formState.errors.classes && (

{form.formState.errors.classes.message}

)}
); }