import { baseUrl } from "@/api/baseUrl"; import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import useOptimisticState from "@/hooks/use-optimistic-state"; import { cn } from "@/lib/utils"; import { CustomClassificationModelConfig } from "@/types/frigateConfig"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import axios from "axios"; import { useCallback, useMemo, useState } from "react"; import { isMobile } from "react-device-detect"; import { useTranslation } from "react-i18next"; import { LuPencil, LuTrash2 } from "react-icons/lu"; import useSWR from "swr"; type ModelTrainingViewProps = { model: CustomClassificationModelConfig; }; export default function ModelTrainingView({ model }: ModelTrainingViewProps) { const [page, setPage] = useState("train"); const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); // dataset const { data: trainImages } = useSWR( `classification/${model.name}/train`, ); const { data: dataset } = useSWR<{ [id: string]: string[] }>( `classification/${model.name}/dataset`, ); // actions const trainModel = useCallback(() => { axios.post(`classification/${model.name}/train`); }, [model]); return (
{}} onRename={() => {}} />
{pageToggle == "train" ? ( {}} onDelete={() => {}} /> ) : ( {}} /> )}
); } type LibrarySelectorProps = { pageToggle: string | undefined; dataset: { [id: string]: string[] }; trainImages: string[]; setPageToggle: (toggle: string) => void; onDelete: (name: string, ids: string[], isName: boolean) => void; onRename: (old_name: string, new_name: string) => void; }; function LibrarySelector({ pageToggle, dataset, trainImages, setPageToggle, onDelete, onRename, }: LibrarySelectorProps) { const { t } = useTranslation(["views/faceLibrary"]); const [confirmDelete, setConfirmDelete] = useState(null); const [renameFace, setRenameFace] = useState(null); const handleDeleteFace = useCallback( (name: string) => { // Get all image IDs for this face const imageIds = dataset?.[name] || []; onDelete(name, imageIds, true); setPageToggle("train"); }, [dataset, onDelete, setPageToggle], ); const handleSetOpen = useCallback( (open: boolean) => { setRenameFace(open ? renameFace : null); }, [renameFace], ); return ( <> !open && setConfirmDelete(null)} > {t("deleteFaceLibrary.title")} {t("deleteFaceLibrary.desc", { name: confirmDelete })}
{ onRename(renameFace!, newName); setRenameFace(null); }} defaultValue={renameFace || ""} regexPattern={/^[\p{L}\p{N}\s'_-]{1,50}$/u} regexErrorMessage={t("description.invalidName")} /> setPageToggle("train")} >
{t("train.title")}
({trainImages.length})
{trainImages.length > 0 && Object.keys(dataset).length > 0 && ( <>
{t("collections")}
)} {Object.keys(dataset).map((id) => (
setPageToggle(id)} > {id} ({dataset?.[id].length})
{t("button.renameFace")} {t("button.deleteFace")}
))}
); } type DatasetGridProps = { modelName: string; categoryName: string; images: string[]; onDelete: (modelName: string, categoryName: string, ids: string[]) => void; }; function DatasetGrid({ modelName, categoryName, images, onDelete, }: DatasetGridProps) { const { t } = useTranslation(["views/classificationModel"]); return (
{images.map((image) => (
{ //e.stopPropagation(); //onClickImages([data.raw], e.ctrlKey || e.metaKey); }} >
{ e.stopPropagation(); onDelete(modelName, categoryName, [image]); }} /> {t("button.deleteClassificationAttempts")}
))}
); } type TrainGridProps = { model: CustomClassificationModelConfig; trainImages: string[]; selected: boolean; onClickImages: (images: string[], ctrl: boolean) => void; onDelete: (name: string, ids: string[]) => void; }; function TrainGrid({ model, trainImages, selected, onClickImages, onDelete, }: TrainGridProps) { const { t } = useTranslation(["views/classificationModel"]); const trainData = useMemo( () => trainImages.map((raw) => { const parts = raw.replaceAll(".webp", "").split("-"); return { raw, timestamp: parts[0], label: parts[1], score: Number.parseFloat(parts[2]) * 100, }; }), [trainImages], ); return (
{trainData?.map((data) => (
{ e.stopPropagation(); onClickImages([data.raw], e.ctrlKey || e.metaKey); }} >
{data.label}
{data.score}%
{ e.stopPropagation(); onDelete("train", [data.raw]); }} /> {t("button.deleteClassificationAttempts")}
))}
); }