import { baseUrl } from "@/api/baseUrl"; import ClassificationModelWizardDialog from "@/components/classification/ClassificationModelWizardDialog"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { ImageShadowOverlay } from "@/components/overlay/ImageShadowOverlay"; import { Button, buttonVariants } from "@/components/ui/button"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import useOptimisticState from "@/hooks/use-optimistic-state"; import { cn } from "@/lib/utils"; import { CustomClassificationModelConfig, FrigateConfig, } from "@/types/frigateConfig"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { FaFolderPlus } from "react-icons/fa"; import { MdModelTraining } from "react-icons/md"; import { LuTrash2 } from "react-icons/lu"; import { FiMoreVertical } from "react-icons/fi"; import useSWR from "swr"; import Heading from "@/components/ui/heading"; import { useOverlayState } from "@/hooks/use-overlay-state"; import axios from "axios"; import { toast } from "sonner"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import BlurredIconButton from "@/components/button/BlurredIconButton"; const allModelTypes = ["objects", "states"] as const; type ModelType = (typeof allModelTypes)[number]; type ModelSelectionViewProps = { onClick: (model: CustomClassificationModelConfig) => void; }; export default function ModelSelectionView({ onClick, }: ModelSelectionViewProps) { const { t } = useTranslation(["views/classificationModel"]); const [page, setPage] = useOverlayState("objects", "objects"); const [pageToggle, setPageToggle] = useOptimisticState( page || "objects", setPage, 100, ); const { data: config, mutate: refreshConfig } = useSWR( "config", { revalidateOnFocus: false, }, ); // title useEffect(() => { document.title = t("documentTitle"); }, [t]); // data const classificationConfigs = useMemo(() => { if (!config) { return []; } return Object.values(config.classification.custom); }, [config]); const selectedClassificationConfigs = useMemo(() => { return classificationConfigs.filter((model) => { if (pageToggle == "objects" && model.object_config != undefined) { return true; } if (pageToggle == "states" && model.state_config != undefined) { return true; } return false; }); }, [classificationConfigs, pageToggle]); // new model wizard const [newModel, setNewModel] = useState(false); if (!config) { return ; } return (
{ setNewModel(false); refreshConfig(); }} />
{ if (value) { setPageToggle(value); } }} > {allModelTypes.map((item) => (
{t("menu." + item)}
))}
{selectedClassificationConfigs.length === 0 ? ( setNewModel(true)} modelType={pageToggle} /> ) : (
{selectedClassificationConfigs.map((config) => ( onClick(config)} onDelete={() => refreshConfig()} /> ))}
)}
); } function NoModelsView({ onCreateModel, modelType, }: { onCreateModel: () => void; modelType: ModelType; }) { const { t } = useTranslation(["views/classificationModel"]); const typeKey = modelType === "objects" ? "object" : "state"; return (
{t(`noModels.${typeKey}.title`)}
{t(`noModels.${typeKey}.description`)}
); } type ModelCardProps = { config: CustomClassificationModelConfig; onClick: () => void; onDelete: () => void; }; function ModelCard({ config, onClick, onDelete }: ModelCardProps) { const { t } = useTranslation(["views/classificationModel"]); const { data: dataset } = useSWR<{ [id: string]: string[]; }>(`classification/${config.name}/dataset`, { revalidateOnFocus: false }); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const bypassDialogRef = useRef(false); useKeyboardListener(["Shift"], (_, modifiers) => { bypassDialogRef.current = modifiers.shift; return false; }); const handleDelete = useCallback(async () => { await axios .delete(`classification/${config.name}`) .then((resp) => { if (resp.status == 200) { toast.success(t("toast.success.deletedModel", { count: 1 }), { position: "top-center", }); onDelete(); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error(t("toast.error.deleteModelFailed", { errorMessage }), { position: "top-center", }); }); }, [config, onDelete, t]); const handleDeleteClick = useCallback(() => { if (bypassDialogRef.current) { handleDelete(); } else { setDeleteDialogOpen(true); } }, [handleDelete]); const coverImage = useMemo(() => { if (!dataset) { return undefined; } const keys = Object.keys(dataset).filter((key) => key != "none"); const selectedKey = keys[0]; if (!dataset[selectedKey]) { return undefined; } return { name: selectedKey, img: dataset[selectedKey][0], }; }, [dataset]); return ( <> setDeleteDialogOpen(!deleteDialogOpen)} > {t("deleteModel.title")} {t("deleteModel.single", { name: config.name })} {t("button.cancel", { ns: "common" })} {t("button.delete", { ns: "common" })}
{config.name}
e.stopPropagation()}> {bypassDialogRef.current ? t("button.deleteNow", { ns: "common" }) : t("button.delete", { ns: "common" })}
); }