import Heading from "@/components/ui/heading"; import { FrigateConfig, SearchModelSize } from "@/types/frigateConfig"; import useSWR from "swr"; import axios from "axios"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { useCallback, useContext, useEffect, useState } from "react"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Toaster } from "@/components/ui/sonner"; import { toast } from "sonner"; import { Separator } from "@/components/ui/separator"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, } from "@/components/ui/select"; import { Trans, useTranslation } from "react-i18next"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { buttonVariants } from "@/components/ui/button"; type ClassificationSettings = { search: { enabled?: boolean; model_size?: SearchModelSize; }; face: { enabled?: boolean; model_size?: SearchModelSize; }; lpr: { enabled?: boolean; }; bird: { enabled?: boolean; }; }; type ClassificationSettingsViewProps = { setUnsavedChanges: React.Dispatch>; }; export default function ClassificationSettingsView({ setUnsavedChanges, }: ClassificationSettingsViewProps) { const { t } = useTranslation("views/settings"); const { data: config, mutate: updateConfig } = useSWR("config"); const [changedValue, setChangedValue] = useState(false); const [isLoading, setIsLoading] = useState(false); const [isReindexDialogOpen, setIsReindexDialogOpen] = useState(false); const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; const [classificationSettings, setClassificationSettings] = useState({ search: { enabled: undefined, model_size: undefined }, face: { enabled: undefined, model_size: undefined }, lpr: { enabled: undefined }, bird: { enabled: undefined }, }); const [origSearchSettings, setOrigSearchSettings] = useState({ search: { enabled: undefined, model_size: undefined }, face: { enabled: undefined, model_size: undefined }, lpr: { enabled: undefined }, bird: { enabled: undefined }, }); useEffect(() => { if (config) { if (classificationSettings?.search.enabled == undefined) { setClassificationSettings({ search: { enabled: config.semantic_search.enabled, model_size: config.semantic_search.model_size, }, face: { enabled: config.face_recognition.enabled, model_size: config.face_recognition.model_size, }, lpr: { enabled: config.lpr.enabled }, bird: { enabled: config.classification.bird.enabled, }, }); } setOrigSearchSettings({ search: { enabled: config.semantic_search.enabled, model_size: config.semantic_search.model_size, }, face: { enabled: config.face_recognition.enabled, model_size: config.face_recognition.model_size, }, lpr: { enabled: config.lpr.enabled }, bird: { enabled: config.classification.bird.enabled }, }); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [config]); const handleClassificationConfigChange = ( newConfig: Partial, ) => { setClassificationSettings((prevConfig) => ({ search: { ...prevConfig.search, ...newConfig.search }, face: { ...prevConfig.face, ...newConfig.face }, lpr: { ...prevConfig.lpr, ...newConfig.lpr }, bird: { ...prevConfig.bird, ...newConfig.bird }, })); setUnsavedChanges(true); setChangedValue(true); }; const saveToConfig = useCallback(async () => { setIsLoading(true); axios .put( `config/set?semantic_search.enabled=${classificationSettings.search.enabled ? "True" : "False"}&semantic_search.model_size=${classificationSettings.search.model_size}&face_recognition.enabled=${classificationSettings.face.enabled ? "True" : "False"}&face_recognition.model_size=${classificationSettings.face.model_size}&lpr.enabled=${classificationSettings.lpr.enabled ? "True" : "False"}&classification.bird.enabled=${classificationSettings.bird.enabled ? "True" : "False"}`, { requires_restart: 0 }, ) .then((res) => { if (res.status === 200) { toast.success(t("classification.toast.success"), { position: "top-center", }); setChangedValue(false); updateConfig(); } else { toast.error( t("classification.toast.error", { errorMessage: res.statusText }), { position: "top-center" }, ); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("toast.save.error.title", { errorMessage, ns: "common" }), { position: "top-center" }, ); }) .finally(() => { addMessage( "search_settings_restart", `Restart required (Classification settings changed)`, undefined, "search_settings", ); setIsLoading(false); }); }, [classificationSettings, t, addMessage, updateConfig]); const onCancel = useCallback(() => { setClassificationSettings(origSearchSettings); setChangedValue(false); removeMessage("search_settings", "search_settings"); }, [origSearchSettings, removeMessage]); const onReindex = useCallback(() => { setIsLoading(true); axios .put("/reindex") .then((res) => { if (res.status === 202) { toast.success(t("classification.semanticSearch.reindexNow.success"), { position: "top-center", }); } else { toast.error( t("classification.semanticSearch.reindexNow.error", { errorMessage: res.statusText, }), { position: "top-center" }, ); } }) .catch((error) => { const errorMessage = error.response?.data?.message || error.response?.data?.detail || "Unknown error"; toast.error( t("classification.semanticSearch.reindexNow.error", { errorMessage, }), { position: "top-center" }, ); }) .finally(() => { setIsLoading(false); setIsReindexDialogOpen(false); }); }, [t]); useEffect(() => { if (changedValue) { addMessage( "search_settings", `Unsaved Classification settings changes`, undefined, "search_settings", ); } else { removeMessage("search_settings", "search_settings"); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [changedValue]); useEffect(() => { document.title = t("documentTitle.classification"); }, [t]); if (!config) { return ; } return (
{t("classification.title")} {t("classification.semanticSearch.title")}

{t("classification.semanticSearch.desc")}

{t("classification.semanticSearch.readTheDocumentation")}
{ handleClassificationConfigChange({ search: { enabled: isChecked }, }); }} />
classification.semanticSearch.reindexNow.desc
{t("classification.semanticSearch.modelSize.label")}

classification.semanticSearch.modelSize.desc

  • classification.semanticSearch.modelSize.small.desc
  • classification.semanticSearch.modelSize.large.desc
{t("classification.semanticSearch.reindexNow.confirmTitle")} classification.semanticSearch.reindexNow.confirmDesc setIsReindexDialogOpen(false)}> {t("button.cancel", { ns: "common" })} {t("classification.semanticSearch.reindexNow.confirmButton")}
{t("classification.faceRecognition.title")}

{t("classification.faceRecognition.desc")}

{t("classification.faceRecognition.readTheDocumentation")}
{ handleClassificationConfigChange({ face: { enabled: isChecked }, }); }} />
{t("classification.faceRecognition.modelSize.label")}

classification.faceRecognition.modelSize.desc

  • classification.faceRecognition.modelSize.small.desc
  • classification.faceRecognition.modelSize.large.desc
{t("classification.licensePlateRecognition.title")}

{t("classification.licensePlateRecognition.desc")}

{t( "classification.licensePlateRecognition.readTheDocumentation", )}
{ handleClassificationConfigChange({ lpr: { enabled: isChecked }, }); }} />
{t("classification.birdClassification.title")}

{t("classification.birdClassification.desc")}

{t("classification.semanticSearch.readTheDocumentation")}
{ handleClassificationConfigChange({ bird: { enabled: isChecked }, }); }} />
); }