From 68de18f10dd2455f340b5f8cd8a44593679fd334 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:24:34 -0500 Subject: [PATCH] Settings UI tweaks (#22547) * fix genai settings ui - add roles widget to select roles for genai providers - add dropdown in semantic search to allow selection of embeddings genai provider * tweak grouping to prioritize fieldOrder before groups previously, groups were always rendered first. now fieldOrder is respected, and any fields in a group will cause the group and all the fields in that group to be rendered in order. this allows moving the enabled switches to the top of the section * mobile tweaks stack buttons, add more space on profiles pane, and move the overridden badge beneath the description * language consistency * prevent camera config sections from being regenerated for profiles * conditionally import axengine module to match other detectors * i18n * update vscode launch.json for new integrated browser * formatting --- .vscode/launch.json | 17 ++ frigate/config/camera/detect.py | 4 +- frigate/config/camera/snapshots.py | 2 +- frigate/config/config.py | 2 +- frigate/detectors/plugins/axengine.py | 7 +- generate_config_translations.py | 9 + web/public/locales/en/config/cameras.json | 10 +- web/public/locales/en/config/global.json | 20 ++- web/public/locales/en/views/settings.json | 12 ++ .../config-form/section-configs/audio.ts | 2 +- .../config-form/section-configs/detect.ts | 2 +- .../section-configs/face_recognition.ts | 2 +- .../config-form/section-configs/genai.ts | 55 +++--- .../config-form/section-configs/lpr.ts | 4 +- .../config-form/section-configs/motion.ts | 2 +- .../config-form/section-configs/record.ts | 2 +- .../section-configs/semantic_search.ts | 5 + .../config-form/section-configs/snapshots.ts | 2 +- .../config-form/sections/BaseSection.tsx | 2 +- .../config-form/theme/frigateTheme.ts | 4 + .../theme/templates/ObjectFieldTemplate.tsx | 109 ++++++------ .../theme/widgets/GenAIRolesWidget.tsx | 109 ++++++++++++ .../widgets/SemanticSearchModelWidget.tsx | 159 ++++++++++++++++++ web/src/lib/config-schema/transformer.ts | 37 +++- web/src/views/settings/ProfilesView.tsx | 20 +-- web/src/views/settings/SingleSectionPage.tsx | 91 +++++++--- 26 files changed, 552 insertions(+), 138 deletions(-) create mode 100644 web/src/components/config-form/theme/widgets/GenAIRolesWidget.tsx create mode 100644 web/src/components/config-form/theme/widgets/SemanticSearchModelWidget.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index 5c858267d..2d7b6c8fb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,6 +6,23 @@ "type": "debugpy", "request": "launch", "module": "frigate" + }, + { + "type": "editor-browser", + "request": "launch", + "name": "Vite: Launch in integrated browser", + "url": "http://localhost:5173" + }, + { + "type": "editor-browser", + "request": "launch", + "name": "Nginx: Launch in integrated browser", + "url": "http://localhost:5000" + }, + { + "type": "editor-browser", + "request": "attach", + "name": "Attach to integrated browser" } ] } diff --git a/frigate/config/camera/detect.py b/frigate/config/camera/detect.py index 19ba670a6..c0a2e7036 100644 --- a/frigate/config/camera/detect.py +++ b/frigate/config/camera/detect.py @@ -49,8 +49,8 @@ class StationaryConfig(FrigateBaseModel): class DetectConfig(FrigateBaseModel): enabled: bool = Field( default=False, - title="Detection enabled", - description="Enable or disable object detection for all cameras; can be overridden per-camera. Detection must be enabled for object tracking to run.", + title="Enable object detection", + description="Enable or disable object detection for all cameras; can be overridden per-camera.", ) height: Optional[int] = Field( default=None, diff --git a/frigate/config/camera/snapshots.py b/frigate/config/camera/snapshots.py index c367aad8e..5a7f8480c 100644 --- a/frigate/config/camera/snapshots.py +++ b/frigate/config/camera/snapshots.py @@ -29,7 +29,7 @@ class RetainConfig(FrigateBaseModel): class SnapshotsConfig(FrigateBaseModel): enabled: bool = Field( default=False, - title="Snapshots enabled", + title="Enable snapshots", description="Enable or disable saving snapshots for all cameras; can be overridden per-camera.", ) clean_copy: bool = Field( diff --git a/frigate/config/config.py b/frigate/config/config.py index ea21fa831..699092d7d 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -444,7 +444,7 @@ class FrigateConfig(FrigateBaseModel): # GenAI config (named provider configs: name -> GenAIConfig) genai: Dict[str, GenAIConfig] = Field( default_factory=dict, - title="Generative AI configuration (named providers).", + title="Generative AI configuration", description="Settings for integrated generative AI providers used to generate object descriptions and review summaries.", ) diff --git a/frigate/detectors/plugins/axengine.py b/frigate/detectors/plugins/axengine.py index 1752188d9..383fcd0bf 100644 --- a/frigate/detectors/plugins/axengine.py +++ b/frigate/detectors/plugins/axengine.py @@ -4,7 +4,6 @@ import re import urllib.request from typing import Literal -import axengine as axe from pydantic import ConfigDict from frigate.const import MODEL_CACHE_DIR @@ -37,6 +36,12 @@ class Axengine(DetectionApi): type_key = DETECTOR_KEY def __init__(self, config: AxengineDetectorConfig): + try: + import axengine as axe + except ModuleNotFoundError: + raise ImportError("AXEngine is not installed.") + return + logger.info("__init__ axengine") super().__init__(config) self.height = config.model.height diff --git a/generate_config_translations.py b/generate_config_translations.py index f41957561..df6c18f99 100644 --- a/generate_config_translations.py +++ b/generate_config_translations.py @@ -518,6 +518,15 @@ def main(): sanitize_camera_descriptions(camera_translations) + # Profiles contain the same sections as the camera itself; only keep + # label and description to avoid duplicating every camera section. + if "profiles" in camera_translations: + camera_translations["profiles"] = { + k: v + for k, v in camera_translations["profiles"].items() + if k in ("label", "description") + } + with open(cameras_file, "w", encoding="utf-8") as f: json.dump(camera_translations, f, indent=2, ensure_ascii=False) f.write("\n") diff --git a/web/public/locales/en/config/cameras.json b/web/public/locales/en/config/cameras.json index 0ae231c37..f14599e14 100644 --- a/web/public/locales/en/config/cameras.json +++ b/web/public/locales/en/config/cameras.json @@ -79,8 +79,8 @@ "label": "Object Detection", "description": "Settings for the detection/detect role used to run object detection and initialize trackers.", "enabled": { - "label": "Detection enabled", - "description": "Enable or disable object detection for this camera. Detection must be enabled for object tracking to run." + "label": "Enable object detection", + "description": "Enable or disable object detection for this camera." }, "height": { "label": "Detect height", @@ -628,7 +628,7 @@ "label": "Snapshots", "description": "Settings for saved JPEG snapshots of tracked objects for this camera.", "enabled": { - "label": "Snapshots enabled", + "label": "Enable snapshots", "description": "Enable or disable saving snapshots for this camera." }, "clean_copy": { @@ -860,6 +860,10 @@ "label": "Camera URL", "description": "URL to visit the camera directly from system page" }, + "profiles": { + "label": "Profiles", + "description": "Named config profiles with partial overrides that can be activated at runtime." + }, "zones": { "label": "Zones", "description": "Zones allow you to define a specific area of the frame so you can determine whether or not an object is within a particular area.", diff --git a/web/public/locales/en/config/global.json b/web/public/locales/en/config/global.json index fdfc4b389..8e3439528 100644 --- a/web/public/locales/en/config/global.json +++ b/web/public/locales/en/config/global.json @@ -1174,7 +1174,7 @@ } }, "genai": { - "label": "Generative AI configuration (named providers).", + "label": "Generative AI configuration", "description": "Settings for integrated generative AI providers used to generate object descriptions and review summaries.", "api_key": { "label": "API key", @@ -1293,8 +1293,8 @@ "label": "Object Detection", "description": "Settings for the detection/detect role used to run object detection and initialize trackers.", "enabled": { - "label": "Detection enabled", - "description": "Enable or disable object detection for all cameras; can be overridden per-camera. Detection must be enabled for object tracking to run." + "label": "Enable object detection", + "description": "Enable or disable object detection for all cameras; can be overridden per-camera." }, "height": { "label": "Detect height", @@ -1778,7 +1778,7 @@ "label": "Snapshots", "description": "Settings for saved JPEG snapshots of tracked objects for all cameras; can be overridden per-camera.", "enabled": { - "label": "Snapshots enabled", + "label": "Enable snapshots", "description": "Enable or disable saving snapshots for all cameras; can be overridden per-camera." }, "clean_copy": { @@ -2128,6 +2128,18 @@ "description": "Numeric order used to sort camera groups in the UI; larger numbers appear later." } }, + "profiles": { + "label": "Profiles", + "description": "Named profile definitions with friendly names. Camera profiles must reference names defined here.", + "friendly_name": { + "label": "Friendly name", + "description": "Display name for this profile shown in the UI." + } + }, + "active_profile": { + "label": "Active profile", + "description": "Currently active profile name. Runtime-only, not persisted in YAML." + }, "camera_mqtt": { "label": "MQTT", "description": "MQTT image publishing settings.", diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index f93439244..0dd96acbb 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1402,6 +1402,18 @@ "audio": "Audio" } }, + "genaiRoles": { + "options": { + "embeddings": "Embedding", + "vision": "Vision", + "tools": "Tools" + } + }, + "semanticSearchModel": { + "placeholder": "Select model…", + "builtIn": "Built-in Models", + "genaiProviders": "GenAI Providers" + }, "review": { "title": "Review Settings" }, diff --git a/web/src/components/config-form/section-configs/audio.ts b/web/src/components/config-form/section-configs/audio.ts index 09fe4e974..740d76f78 100644 --- a/web/src/components/config-form/section-configs/audio.ts +++ b/web/src/components/config-form/section-configs/audio.ts @@ -13,7 +13,7 @@ const audio: SectionConfigOverrides = { "num_threads", ], fieldGroups: { - detection: ["enabled", "listen", "filters"], + detection: ["listen", "filters"], sensitivity: ["min_volume", "max_not_heard"], }, hiddenFields: ["enabled_in_config"], diff --git a/web/src/components/config-form/section-configs/detect.ts b/web/src/components/config-form/section-configs/detect.ts index ecf5102b4..778620f1c 100644 --- a/web/src/components/config-form/section-configs/detect.ts +++ b/web/src/components/config-form/section-configs/detect.ts @@ -18,7 +18,7 @@ const detect: SectionConfigOverrides = { ], restartRequired: [], fieldGroups: { - resolution: ["enabled", "width", "height", "fps"], + resolution: ["width", "height", "fps"], tracking: ["min_initialized", "max_disappeared"], }, hiddenFields: ["enabled_in_config"], diff --git a/web/src/components/config-form/section-configs/face_recognition.ts b/web/src/components/config-form/section-configs/face_recognition.ts index 18e963940..ef9e43506 100644 --- a/web/src/components/config-form/section-configs/face_recognition.ts +++ b/web/src/components/config-form/section-configs/face_recognition.ts @@ -6,7 +6,7 @@ const faceRecognition: SectionConfigOverrides = { restartRequired: [], fieldOrder: ["enabled", "min_area"], hiddenFields: [], - advancedFields: ["min_area"], + advancedFields: [], overrideFields: ["enabled", "min_area"], }, global: { diff --git a/web/src/components/config-form/section-configs/genai.ts b/web/src/components/config-form/section-configs/genai.ts index 739659496..e37478f11 100644 --- a/web/src/components/config-form/section-configs/genai.ts +++ b/web/src/components/config-form/section-configs/genai.ts @@ -4,39 +4,50 @@ const genai: SectionConfigOverrides = { base: { sectionDocs: "/configuration/genai/config", restartRequired: [ - "provider", - "api_key", - "base_url", - "model", - "provider_options", - "runtime_options", + "*.provider", + "*.api_key", + "*.base_url", + "*.model", + "*.provider_options", + "*.runtime_options", ], - fieldOrder: [ - "provider", - "api_key", - "base_url", - "model", - "provider_options", - "runtime_options", - ], - advancedFields: ["base_url", "provider_options", "runtime_options"], + advancedFields: ["*.base_url", "*.provider_options", "*.runtime_options"], hiddenFields: ["genai.enabled_in_config"], uiSchema: { - api_key: { - "ui:options": { size: "md" }, + "ui:options": { disableNestedCard: true }, + "*": { + "ui:options": { disableNestedCard: true }, + "ui:order": [ + "provider", + "api_key", + "base_url", + "model", + "provider_options", + "runtime_options", + "*", + ], }, - base_url: { + "*.roles": { + "ui:widget": "genaiRoles", + }, + "*.api_key": { "ui:options": { size: "lg" }, }, - model: { - "ui:options": { size: "md" }, + "*.base_url": { + "ui:options": { size: "lg" }, }, - provider_options: { + "*.model": { + "ui:options": { size: "xs" }, + }, + "*.provider": { + "ui:options": { size: "xs" }, + }, + "*.provider_options": { additionalProperties: { "ui:options": { size: "lg" }, }, }, - runtime_options: { + "*.runtime_options": { additionalProperties: { "ui:options": { size: "lg" }, }, diff --git a/web/src/components/config-form/section-configs/lpr.ts b/web/src/components/config-form/section-configs/lpr.ts index c5e16eb23..514dba9be 100644 --- a/web/src/components/config-form/section-configs/lpr.ts +++ b/web/src/components/config-form/section-configs/lpr.ts @@ -7,9 +7,9 @@ const lpr: SectionConfigOverrides = { enhancement: "/configuration/license_plate_recognition#enhancement", }, restartRequired: [], - fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"], + fieldOrder: ["enabled", "min_area", "enhancement", "expire_time"], hiddenFields: [], - advancedFields: ["expire_time", "min_area", "enhancement"], + advancedFields: ["expire_time", "enhancement"], overrideFields: ["enabled", "min_area", "enhancement"], }, global: { diff --git a/web/src/components/config-form/section-configs/motion.ts b/web/src/components/config-form/section-configs/motion.ts index 38755ee20..4e9676bb2 100644 --- a/web/src/components/config-form/section-configs/motion.ts +++ b/web/src/components/config-form/section-configs/motion.ts @@ -23,7 +23,7 @@ const motion: SectionConfigOverrides = { "mqtt_off_delay", ], fieldGroups: { - sensitivity: ["enabled", "threshold", "contour_area"], + sensitivity: ["threshold", "contour_area"], algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"], }, uiSchema: { diff --git a/web/src/components/config-form/section-configs/record.ts b/web/src/components/config-form/section-configs/record.ts index 53803eed9..9cfc92127 100644 --- a/web/src/components/config-form/section-configs/record.ts +++ b/web/src/components/config-form/section-configs/record.ts @@ -15,7 +15,7 @@ const record: SectionConfigOverrides = { "export", ], fieldGroups: { - retention: ["enabled", "continuous", "motion"], + retention: ["continuous", "motion"], events: ["alerts", "detections"], }, hiddenFields: ["enabled_in_config", "sync_recordings"], diff --git a/web/src/components/config-form/section-configs/semantic_search.ts b/web/src/components/config-form/section-configs/semantic_search.ts index 2fea46782..34c1e149f 100644 --- a/web/src/components/config-form/section-configs/semantic_search.ts +++ b/web/src/components/config-form/section-configs/semantic_search.ts @@ -18,6 +18,11 @@ const semanticSearch: SectionConfigOverrides = { advancedFields: ["reindex", "device"], restartRequired: ["enabled", "model", "model_size", "device"], hiddenFields: ["reindex"], + uiSchema: { + model: { + "ui:widget": "semanticSearchModel", + }, + }, }, }; diff --git a/web/src/components/config-form/section-configs/snapshots.ts b/web/src/components/config-form/section-configs/snapshots.ts index 8f08fa843..126ecd496 100644 --- a/web/src/components/config-form/section-configs/snapshots.ts +++ b/web/src/components/config-form/section-configs/snapshots.ts @@ -13,7 +13,7 @@ const snapshots: SectionConfigOverrides = { "retain", ], fieldGroups: { - display: ["enabled", "bounding_box", "crop", "quality", "timestamp"], + display: ["bounding_box", "crop", "quality", "timestamp"], }, hiddenFields: ["enabled_in_config"], advancedFields: ["height", "quality", "retain"], diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index d2be6ded4..f171d9fe1 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -936,7 +936,7 @@ export function ConfigSection({ )} -