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"; type ClassificationSettings = { search: { enabled?: boolean; reindex?: boolean; model_size?: SearchModelSize; }; face: { enabled?: boolean; }; lpr: { 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 { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; const [classificationSettings, setClassificationSettings] = useState({ search: { enabled: undefined, reindex: undefined, model_size: undefined, }, face: { enabled: undefined, }, lpr: { enabled: undefined, }, }); const [origSearchSettings, setOrigSearchSettings] = useState({ search: { enabled: undefined, reindex: undefined, model_size: undefined, }, face: { enabled: undefined, }, lpr: { enabled: undefined, }, }); useEffect(() => { if (config) { if (classificationSettings?.search.enabled == undefined) { setClassificationSettings({ search: { enabled: config.semantic_search.enabled, reindex: config.semantic_search.reindex, model_size: config.semantic_search.model_size, }, face: { enabled: config.face_recognition.enabled, }, lpr: { enabled: config.lpr.enabled, }, }); } setOrigSearchSettings({ search: { enabled: config.semantic_search.enabled, reindex: config.semantic_search.reindex, model_size: config.semantic_search.model_size, }, face: { enabled: config.face_recognition.enabled, }, lpr: { enabled: config.lpr.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 }, })); setUnsavedChanges(true); setChangedValue(true); }; const saveToConfig = useCallback(async () => { setIsLoading(true); axios .put( `config/set?semantic_search.enabled=${classificationSettings.search.enabled ? "True" : "False"}&semantic_search.reindex=${classificationSettings.search.reindex ? "True" : "False"}&semantic_search.model_size=${classificationSettings.search.model_size}&face_recognition.enabled=${classificationSettings.face.enabled ? "True" : "False"}&lpr.enabled=${classificationSettings.lpr.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(() => { setIsLoading(false); }); }, [updateConfig, classificationSettings, t]); const onCancel = useCallback(() => { setClassificationSettings(origSearchSettings); setChangedValue(false); removeMessage("search_settings", "search_settings"); }, [origSearchSettings, removeMessage]); 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 }, }); }} />
{ handleClassificationConfigChange({ search: { reindex: isChecked }, }); }} />
classification.semanticSearch.reindexOnStartup.desc
{t("classification.semanticSearch.modelSize.label")}

classification.semanticSearch.modelSize.desc

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

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

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

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

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