From a4c6e116423761a55c9ea153a76eb2f53c886501 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 11 May 2026 15:03:31 -0500 Subject: [PATCH] frigate+ pane updates - allow users to select a plus model from the select even when one was not previously loaded - always show model summary card - add model filter popover - add restart button totast --- web/public/locales/en/views/settings.json | 8 + .../settings/FrigatePlusSettingsView.tsx | 356 +++++++++++------- .../FrigatePlusCurrentModelSummary.tsx | 7 +- 3 files changed, 240 insertions(+), 131 deletions(-) diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 3edf44e46c..b73534d5d6 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1128,8 +1128,16 @@ "cameras": "Cameras", "loading": "Loading model information…", "error": "Failed to load model information", + "noModelLoaded": "No Frigate+ model is currently loaded.", "availableModels": "Available Models", "loadingAvailableModels": "Loading available models…", + "selectModel": "Select a model", + "noModelsAvailable": "No models available", + "filter": { + "ariaLabel": "Filter models by type", + "baseModels": "Base Models", + "fineTunedModels": "Fine-tuned Models" + }, "modelSelect": "Your available models on Frigate+ can be selected here. Note that only models compatible with your current detector configuration can be selected." }, "unsavedChanges": "Unsaved Frigate+ settings changes", diff --git a/web/src/views/settings/FrigatePlusSettingsView.tsx b/web/src/views/settings/FrigatePlusSettingsView.tsx index 4beeeea365..db9a4369b5 100644 --- a/web/src/views/settings/FrigatePlusSettingsView.tsx +++ b/web/src/views/settings/FrigatePlusSettingsView.tsx @@ -1,5 +1,5 @@ import Heading from "@/components/ui/heading"; -import { useCallback, useContext, useEffect, useState } from "react"; +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"; @@ -10,7 +10,7 @@ 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 } from "react-icons/lu"; +import { LuExternalLink, LuFilter } from "react-icons/lu"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { Select, @@ -19,6 +19,14 @@ import { 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 { @@ -26,6 +34,8 @@ import { 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; @@ -58,6 +68,8 @@ export default function FrigatePlusSettingsView({ 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)!; @@ -76,7 +88,7 @@ export default function FrigatePlusSettingsView({ }, ); - const { data: availableModels = {} } = useSWR< + const { data: availableModels = {}, isLoading: isLoadingModels } = useSWR< Record >("/plus/models", { fallbackData: {}, @@ -92,6 +104,19 @@ export default function FrigatePlusSettingsView({ }, }); + 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) { @@ -128,47 +153,60 @@ export default function FrigatePlusSettingsView({ const saveToConfig = useCallback(async () => { setIsLoading(true); - axios - .put(`config/set?model.path=plus://${frigatePlusSettings.model.id}`, { + try { + // Clear the existing model section so only the new path remains + await axios.put("config/set", { requires_restart: 0, - }) - .then((res) => { - if (res.status === 200) { - toast.success(t("frigatePlus.toast.success"), { - position: "top-center", - }); - setChangedValue(false); - updateConfig(); - } else { - toast.error( - t("frigatePlus.toast.error", { errorMessage: res.statusText }), - { - position: "top-center", - }, - ); - } - }) - .catch((error) => { - const errorMessage = - error.response?.data?.message || - error.response?.data?.detail || - "Unknown error"; + 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("toast.save.error.title", { errorMessage, ns: "common" }), + t("frigatePlus.toast.error", { errorMessage: res.statusText }), { position: "top-center", }, ); - }) - .finally(() => { - addMessage( - "plus_restart", - t("frigatePlus.restart_required"), - undefined, - "plus_restart", - ); - setIsLoading(false); + } + } 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(() => { @@ -255,11 +293,11 @@ export default function FrigatePlusSettingsView({ /> - {config?.model.plus && ( + {config?.plus?.enabled && ( )} - {config?.model.plus && ( + {config?.plus?.enabled && ( @@ -271,96 +309,157 @@ export default function FrigatePlusSettingsView({ } content={ - + handleFrigatePlusConfigChange({ + model: { id: value as string }, + }) + } + > - {new Date( - availableModels[ - frigatePlusSettings.model.id - ].trainDate, - ).toLocaleString() + - " " + - availableModels[frigatePlusSettings.model.id] - .baseModel + - " (" + - (availableModels[frigatePlusSettings.model.id] - .isBaseModel - ? t( - "frigatePlus.modelInfo.plusModelType.baseModel", - ) - : t( - "frigatePlus.modelInfo.plusModelType.userModel", - )) + - ") " + - availableModels[frigatePlusSettings.model.id] - .name + - " (" + - availableModels[frigatePlusSettings.model.id] - .width + - "x" + - availableModels[frigatePlusSettings.model.id] - .height + - ")"} - - ) : ( - - {t("frigatePlus.modelInfo.loadingAvailableModels")} - - )} - - - - {Object.entries(availableModels || {}).map( - ([id, model]) => ( - + + + + {filteredModelEntries.length === 0 ? ( +
+ {t("frigatePlus.modelInfo.noModelsAvailable")} +
+ ) : ( + filteredModelEntries.map(([id, model]) => ( + + {new Date(model.trainDate).toLocaleString()}{" "} +
+ {model.baseModel} {" ("} + {model.isBaseModel + ? t( + "frigatePlus.modelInfo.plusModelType.baseModel", + ) + : t( + "frigatePlus.modelInfo.plusModelType.userModel", + )} + {")"} +
+
+ {model.name} ( + {model.width + "x" + model.height}) +
+
+ {t( + "frigatePlus.modelInfo.supportedDetectors", + )} + : {model.supportedDetectors.join(", ")} +
+
+ {id} +
+
+ )) + )} +
+
+ + + + + + +
+
+ {t("frigatePlus.modelInfo.filter.ariaLabel")} +
+
+ + +
+
+ + +
+
+
+
+ } />
@@ -469,6 +568,11 @@ export default function FrigatePlusSettingsView({ + setRestartDialogOpen(false)} + onRestart={() => sendRestart("restart")} + /> ); } diff --git a/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx b/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx index 9a4d261308..8ff4bde305 100644 --- a/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx +++ b/web/src/views/settings/components/FrigatePlusCurrentModelSummary.tsx @@ -16,14 +16,11 @@ export default function FrigatePlusCurrentModelSummary({ return ( - {plusModel === undefined && ( + {!plusModel && (

- {t("frigatePlus.modelInfo.loading")} + {t("frigatePlus.modelInfo.noModelLoaded")}

)} - {plusModel === null && ( -

{t("frigatePlus.modelInfo.error")}

- )} {plusModel && (