import Heading from "@/components/ui/heading"; import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { Toaster } from "@/components/ui/sonner"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { toast } from "sonner"; import useSWR from "swr"; import axios from "axios"; import { FrigateConfig } from "@/types/frigateConfig"; import { CheckCircle2, XCircle } from "lucide-react"; import { Trans, useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Link } from "react-router-dom"; import { LuExternalLink, LuFilter } from "react-icons/lu"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, } from "@/components/ui/select"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel"; import { SettingsGroupCard, SplitCardRow, } from "@/components/card/SettingsGroupCard"; import FrigatePlusCurrentModelSummary from "@/views/settings/components/FrigatePlusCurrentModelSummary"; import { useRestart } from "@/api/ws"; import RestartDialog from "@/components/overlay/dialog/RestartDialog"; type FrigatePlusModel = { id: string; type: string; name: string; isBaseModel: boolean; supportedDetectors: string[]; trainDate: string; baseModel: string; width: number; height: number; }; type FrigatePlusSettings = { model: { id?: string; }; }; type FrigateSettingsViewProps = { setUnsavedChanges: React.Dispatch>; }; export default function FrigatePlusSettingsView({ setUnsavedChanges, }: FrigateSettingsViewProps) { const { t } = useTranslation("views/settings"); const { getLocaleDocUrl } = useDocDomain(); const { data: config, mutate: updateConfig } = useSWR("config"); const [changedValue, setChangedValue] = useState(false); const [isLoading, setIsLoading] = useState(false); const [restartDialogOpen, setRestartDialogOpen] = useState(false); const { send: sendRestart } = useRestart(); const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; const [frigatePlusSettings, setFrigatePlusSettings] = useState({ model: { id: undefined, }, }); const [origPlusSettings, setOrigPlusSettings] = useState( { model: { id: undefined, }, }, ); const { data: availableModels = {}, isLoading: isLoadingModels } = useSWR< Record >("/plus/models", { fallbackData: {}, fetcher: async (url) => { const res = await axios.get(url, { withCredentials: true }); return res.data.reduce( (obj: Record, model: FrigatePlusModel) => { obj[model.id] = model; return obj; }, {}, ); }, }); const [showBaseModels, setShowBaseModels] = useState(true); const [showFineTunedModels, setShowFineTunedModels] = useState(true); const filteredModelEntries = useMemo( () => Object.entries(availableModels || {}).filter(([, model]) => model.isBaseModel ? showBaseModels : showFineTunedModels, ), [availableModels, showBaseModels, showFineTunedModels], ); const isFilterActive = !showBaseModels || !showFineTunedModels; useEffect(() => { if (config) { if (frigatePlusSettings?.model.id == undefined) { setFrigatePlusSettings({ model: { id: config.model.plus?.id, }, }); } setOrigPlusSettings({ model: { id: config.model.plus?.id, }, }); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [config]); const handleFrigatePlusConfigChange = ( newConfig: Partial, ) => { setFrigatePlusSettings((prevConfig) => ({ model: { ...prevConfig.model, ...newConfig.model, }, })); setUnsavedChanges(true); setChangedValue(true); }; const saveToConfig = useCallback(async () => { setIsLoading(true); try { // Clear the existing model section so only the new path remains await axios.put("config/set", { requires_restart: 0, config_data: { model: null }, }); const res = await axios.put("config/set", { requires_restart: 0, config_data: { model: { path: `plus://${frigatePlusSettings.model.id}` }, }, }); if (res.status === 200) { toast.success(t("frigatePlus.toast.success"), { position: "top-center", action: ( setRestartDialogOpen(true)}> ), }); setChangedValue(false); updateConfig(); } else { toast.error( t("frigatePlus.toast.error", { errorMessage: res.statusText }), { position: "top-center", }, ); } } catch (error) { const err = error as { response?: { data?: { message?: string; detail?: string } }; }; const errorMessage = err.response?.data?.message || err.response?.data?.detail || "Unknown error"; toast.error(t("toast.save.error.title", { errorMessage, ns: "common" }), { position: "top-center", }); } finally { addMessage( "plus_restart", t("frigatePlus.restart_required"), undefined, "plus_restart", ); setIsLoading(false); } }, [updateConfig, addMessage, frigatePlusSettings, t]); const onCancel = useCallback(() => { setFrigatePlusSettings(origPlusSettings); setChangedValue(false); removeMessage("plus_settings", "plus_settings"); }, [origPlusSettings, removeMessage]); useEffect(() => { if (changedValue) { addMessage( "plus_settings", t("frigatePlus.unsavedChanges"), undefined, "plus_settings", ); } else { removeMessage("plus_settings", "plus_settings"); } // we know that these deps are correct // eslint-disable-next-line react-hooks/exhaustive-deps }, [changedValue]); useEffect(() => { document.title = t("documentTitle.frigatePlus"); }, [t]); if (!config) { return ; } return (
{t("frigatePlus.title")}

{t("frigatePlus.description")}

{t("frigatePlus.apiKey.desc")}

{!config?.model.plus && (
{t("frigatePlus.apiKey.plusLink")}
)} } content={
{config?.plus?.enabled ? ( ) : ( )} {config?.plus?.enabled ? t("frigatePlus.apiKey.validated") : t("frigatePlus.apiKey.notValidated")}
} />
{config?.plus?.enabled && ( )} {config?.plus?.enabled && ( frigatePlus.modelInfo.modelSelect } content={
{t("frigatePlus.modelInfo.filter.ariaLabel")}
} />
)}

frigatePlus.snapshotConfig.desc

{t("readTheDocumentation", { ns: "common" })}
} content={
{Object.entries(config.cameras).map( ([name, camera]) => ( ), )}
{t("frigatePlus.snapshotConfig.table.camera")} {t("frigatePlus.snapshotConfig.table.snapshots")}
{camera.snapshots.enabled ? ( ) : ( )}
} />
{changedValue && (
{t("unsavedChanges")}
)}
{changedValue && ( )}
setRestartDialogOpen(false)} onRestart={() => sendRestart("restart")} />
); }