mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-20 15:18:21 +03:00
add review classification zones to review form
This commit is contained in:
parent
7835585378
commit
7f916dfb47
@ -9,16 +9,22 @@ const review: SectionConfigOverrides = {
|
|||||||
"enabled_in_config",
|
"enabled_in_config",
|
||||||
"alerts.labels",
|
"alerts.labels",
|
||||||
"alerts.enabled_in_config",
|
"alerts.enabled_in_config",
|
||||||
"alerts.required_zones",
|
|
||||||
"detections.labels",
|
"detections.labels",
|
||||||
"detections.enabled_in_config",
|
"detections.enabled_in_config",
|
||||||
"detections.required_zones",
|
|
||||||
"genai.enabled_in_config",
|
"genai.enabled_in_config",
|
||||||
],
|
],
|
||||||
advancedFields: [],
|
advancedFields: [],
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
alerts: {
|
alerts: {
|
||||||
"ui:before": { render: "CameraReviewSettingsView" },
|
"ui:before": { render: "CameraReviewStatusToggles" },
|
||||||
|
required_zones: {
|
||||||
|
"ui:widget": "hidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
detections: {
|
||||||
|
required_zones: {
|
||||||
|
"ui:widget": "hidden",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
genai: {
|
genai: {
|
||||||
additional_concerns: {
|
additional_concerns: {
|
||||||
|
|||||||
@ -0,0 +1,368 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
|
import get from "lodash/get";
|
||||||
|
import set from "lodash/set";
|
||||||
|
import { LuExternalLink } from "react-icons/lu";
|
||||||
|
import { MdCircle } from "react-icons/md";
|
||||||
|
import Heading from "@/components/ui/heading";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useDocDomain } from "@/hooks/use-doc-domain";
|
||||||
|
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
|
||||||
|
import { resolveZoneName } from "@/hooks/use-zone-friendly-name";
|
||||||
|
import { getTranslatedLabel } from "@/utils/i18n";
|
||||||
|
import { formatList } from "@/utils/stringUtil";
|
||||||
|
import type { ConfigSectionData, JsonObject } from "@/types/configForm";
|
||||||
|
import type { SectionRendererProps } from "./registry";
|
||||||
|
|
||||||
|
const EMPTY_ZONES: string[] = [];
|
||||||
|
|
||||||
|
function getRequiredZones(
|
||||||
|
formData: JsonObject | undefined,
|
||||||
|
path: string,
|
||||||
|
): string[] {
|
||||||
|
const value = get(formData, path);
|
||||||
|
return Array.isArray(value) ? (value as string[]) : EMPTY_ZONES;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CameraReviewClassification({
|
||||||
|
formContext,
|
||||||
|
selectedCamera,
|
||||||
|
}: SectionRendererProps) {
|
||||||
|
const { t } = useTranslation(["views/settings", "common"]);
|
||||||
|
const { getLocaleDocUrl } = useDocDomain();
|
||||||
|
const cameraName = formContext?.cameraName ?? selectedCamera;
|
||||||
|
const fullFormData = formContext?.formData as JsonObject | undefined;
|
||||||
|
const cameraConfig = formContext?.fullCameraConfig;
|
||||||
|
|
||||||
|
const alertsZones = useMemo(
|
||||||
|
() => getRequiredZones(fullFormData, "alerts.required_zones"),
|
||||||
|
[fullFormData],
|
||||||
|
);
|
||||||
|
const detectionsZones = useMemo(
|
||||||
|
() => getRequiredZones(fullFormData, "detections.required_zones"),
|
||||||
|
[fullFormData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectDetections, setSelectDetections] = useState(
|
||||||
|
detectionsZones.length > 0,
|
||||||
|
);
|
||||||
|
const previousCameraRef = useRef(cameraName);
|
||||||
|
const isSynced = formContext?.hasChanges === false;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cameraChanged = previousCameraRef.current !== cameraName;
|
||||||
|
if (cameraChanged) {
|
||||||
|
previousCameraRef.current = cameraName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraChanged || isSynced) {
|
||||||
|
setSelectDetections(detectionsZones.length > 0);
|
||||||
|
}
|
||||||
|
}, [cameraName, detectionsZones.length, isSynced]);
|
||||||
|
|
||||||
|
const zones = useMemo(() => {
|
||||||
|
if (!cameraConfig) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return Object.entries(cameraConfig.zones).map(([name, zoneData]) => {
|
||||||
|
const zone =
|
||||||
|
zoneData as (typeof cameraConfig.zones)[keyof typeof cameraConfig.zones];
|
||||||
|
return {
|
||||||
|
camera: cameraConfig.name,
|
||||||
|
name,
|
||||||
|
friendly_name: cameraConfig.zones[name].friendly_name,
|
||||||
|
objects: zone.objects,
|
||||||
|
color: zone.color,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [cameraConfig]);
|
||||||
|
|
||||||
|
const alertsLabels = useMemo(() => {
|
||||||
|
return cameraConfig?.review.alerts.labels
|
||||||
|
? formatList(
|
||||||
|
cameraConfig.review.alerts.labels.map((label: string) =>
|
||||||
|
getTranslatedLabel(
|
||||||
|
label,
|
||||||
|
cameraConfig?.audio?.listen?.includes(label) ? "audio" : "object",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: "";
|
||||||
|
}, [cameraConfig]);
|
||||||
|
|
||||||
|
const detectionsLabels = useMemo(() => {
|
||||||
|
return cameraConfig?.review.detections.labels
|
||||||
|
? formatList(
|
||||||
|
cameraConfig.review.detections.labels.map((label: string) =>
|
||||||
|
getTranslatedLabel(
|
||||||
|
label,
|
||||||
|
cameraConfig?.audio?.listen?.includes(label) ? "audio" : "object",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: "";
|
||||||
|
}, [cameraConfig]);
|
||||||
|
|
||||||
|
const selectCameraName = useCameraFriendlyName(cameraName);
|
||||||
|
|
||||||
|
const getZoneName = useCallback(
|
||||||
|
(zoneId: string, camId?: string) =>
|
||||||
|
resolveZoneName(formContext?.fullConfig, zoneId, camId),
|
||||||
|
[formContext?.fullConfig],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateFormData = useCallback(
|
||||||
|
(path: string, nextValue: string[]) => {
|
||||||
|
if (!formContext?.onFormDataChange || !fullFormData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextData = cloneDeep(fullFormData) as JsonObject;
|
||||||
|
set(nextData, path, nextValue);
|
||||||
|
formContext.onFormDataChange(nextData as ConfigSectionData);
|
||||||
|
},
|
||||||
|
[formContext, fullFormData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleZoneToggle = useCallback(
|
||||||
|
(path: string, zoneName: string) => {
|
||||||
|
const currentZones = getRequiredZones(fullFormData, path);
|
||||||
|
const nextZones = currentZones.includes(zoneName)
|
||||||
|
? currentZones.filter((value) => value !== zoneName)
|
||||||
|
: [...currentZones, zoneName];
|
||||||
|
updateFormData(path, nextZones);
|
||||||
|
},
|
||||||
|
[fullFormData, updateFormData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDetectionsToggle = useCallback(
|
||||||
|
(checked: boolean | string) => {
|
||||||
|
const isChecked = checked === true;
|
||||||
|
if (!isChecked) {
|
||||||
|
updateFormData("detections.required_zones", []);
|
||||||
|
}
|
||||||
|
setSelectDetections(isChecked);
|
||||||
|
},
|
||||||
|
[updateFormData],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cameraName || formContext?.level !== "camera") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Heading as="h4" className="my-2">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
cameraReview.reviewClassification.title
|
||||||
|
</Trans>
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<div className="max-w-6xl">
|
||||||
|
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 text-sm text-primary-variant">
|
||||||
|
<p>
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
cameraReview.reviewClassification.desc
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center text-primary">
|
||||||
|
<Link
|
||||||
|
to={getLocaleDocUrl("configuration/review")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline"
|
||||||
|
>
|
||||||
|
{t("readTheDocumentation", { ns: "common" })}
|
||||||
|
<LuExternalLink className="ml-2 inline-flex size-3" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-full max-w-5xl space-y-0",
|
||||||
|
zones && zones.length > 0 && "grid items-start gap-5 md:grid-cols-2",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{zones && zones.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="mb-2">
|
||||||
|
<Label className="flex flex-row items-center text-base">
|
||||||
|
<Trans ns="views/settings">camera.review.alerts</Trans>
|
||||||
|
<MdCircle className="ml-3 size-2 text-severity_alert" />
|
||||||
|
</Label>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
cameraReview.reviewClassification.selectAlertsZones
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-md rounded-lg bg-secondary p-4 md:max-w-full">
|
||||||
|
{zones.map((zone) => (
|
||||||
|
<div
|
||||||
|
key={zone.name}
|
||||||
|
className="mb-3 flex flex-row items-center space-x-3 space-y-0 last:mb-0"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
|
||||||
|
checked={alertsZones.includes(zone.name)}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
handleZoneToggle("alerts.required_zones", zone.name)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
className={cn(
|
||||||
|
"font-normal",
|
||||||
|
!zone.friendly_name && "smart-capitalize",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{zone.friendly_name || zone.name}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="font-normal text-destructive">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
cameraReview.reviewClassification.noDefinedZones
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2 text-sm">
|
||||||
|
{alertsZones.length > 0
|
||||||
|
? t("cameraReview.reviewClassification.zoneObjectAlertsTips", {
|
||||||
|
alertsLabels,
|
||||||
|
zone: formatList(
|
||||||
|
alertsZones.map((zone) => getZoneName(zone, cameraName)),
|
||||||
|
),
|
||||||
|
cameraName: selectCameraName,
|
||||||
|
})
|
||||||
|
: t("cameraReview.reviewClassification.objectAlertsTips", {
|
||||||
|
alertsLabels,
|
||||||
|
cameraName: selectCameraName,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{zones && zones.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="mb-2">
|
||||||
|
<Label className="flex flex-row items-center text-base">
|
||||||
|
<Trans ns="views/settings">camera.review.detections</Trans>
|
||||||
|
<MdCircle className="ml-3 size-2 text-severity_detection" />
|
||||||
|
</Label>
|
||||||
|
{selectDetections && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
cameraReview.reviewClassification.selectDetectionsZones
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectDetections && (
|
||||||
|
<div className="max-w-md rounded-lg bg-secondary p-4 md:max-w-full">
|
||||||
|
{zones.map((zone) => (
|
||||||
|
<div
|
||||||
|
key={zone.name}
|
||||||
|
className="mb-3 flex flex-row items-center space-x-3 space-y-0 last:mb-0"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
|
||||||
|
checked={detectionsZones.includes(zone.name)}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
handleZoneToggle(
|
||||||
|
"detections.required_zones",
|
||||||
|
zone.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
className={cn(
|
||||||
|
"font-normal",
|
||||||
|
!zone.friendly_name && "smart-capitalize",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{zone.friendly_name || zone.name}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-0 mt-3 flex flex-row items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="select-detections"
|
||||||
|
className="size-5 text-white accent-white data-[state=checked]:bg-selected data-[state=checked]:text-white"
|
||||||
|
checked={selectDetections}
|
||||||
|
onCheckedChange={handleDetectionsToggle}
|
||||||
|
/>
|
||||||
|
<div className="grid gap-1.5 leading-none">
|
||||||
|
<label
|
||||||
|
htmlFor="select-detections"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
cameraReview.reviewClassification.limitDetections
|
||||||
|
</Trans>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-2 text-sm">
|
||||||
|
{detectionsZones.length > 0 ? (
|
||||||
|
!selectDetections ? (
|
||||||
|
<Trans
|
||||||
|
i18nKey="cameraReview.reviewClassification.zoneObjectDetectionsTips.text"
|
||||||
|
values={{
|
||||||
|
detectionsLabels,
|
||||||
|
zone: formatList(
|
||||||
|
detectionsZones.map((zone) =>
|
||||||
|
getZoneName(zone, cameraName),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cameraName: selectCameraName,
|
||||||
|
}}
|
||||||
|
ns="views/settings"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Trans
|
||||||
|
i18nKey="cameraReview.reviewClassification.zoneObjectDetectionsTips.notSelectDetections"
|
||||||
|
values={{
|
||||||
|
detectionsLabels,
|
||||||
|
zone: formatList(
|
||||||
|
detectionsZones.map((zone) =>
|
||||||
|
getZoneName(zone, cameraName),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cameraName: selectCameraName,
|
||||||
|
}}
|
||||||
|
ns="views/settings"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Trans
|
||||||
|
i18nKey="cameraReview.reviewClassification.objectDetectionsTips"
|
||||||
|
values={{
|
||||||
|
detectionsLabels,
|
||||||
|
cameraName: selectCameraName,
|
||||||
|
}}
|
||||||
|
ns="views/settings"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
import Heading from "@/components/ui/heading";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import {
|
||||||
|
useAlertsState,
|
||||||
|
useDetectionsState,
|
||||||
|
useObjectDescriptionState,
|
||||||
|
useReviewDescriptionState,
|
||||||
|
} from "@/api/ws";
|
||||||
|
import type { SectionRendererProps } from "./registry";
|
||||||
|
import CameraReviewClassification from "./CameraReviewClassification";
|
||||||
|
|
||||||
|
export default function CameraReviewStatusToggles({
|
||||||
|
selectedCamera,
|
||||||
|
formContext,
|
||||||
|
}: SectionRendererProps) {
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
const cameraId = selectedCamera ?? "";
|
||||||
|
|
||||||
|
const cameraConfig = useMemo(() => {
|
||||||
|
if (config && selectedCamera) {
|
||||||
|
return config.cameras[selectedCamera];
|
||||||
|
}
|
||||||
|
}, [config, selectedCamera]);
|
||||||
|
|
||||||
|
const { payload: alertsState, send: sendAlerts } = useAlertsState(cameraId);
|
||||||
|
const { payload: detectionsState, send: sendDetections } =
|
||||||
|
useDetectionsState(cameraId);
|
||||||
|
|
||||||
|
const { payload: objDescState, send: sendObjDesc } =
|
||||||
|
useObjectDescriptionState(cameraId);
|
||||||
|
const { payload: revDescState, send: sendRevDesc } =
|
||||||
|
useReviewDescriptionState(cameraId);
|
||||||
|
|
||||||
|
if (!selectedCamera || !cameraConfig) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Heading as="h4" className="my-2">
|
||||||
|
<Trans ns="views/settings">cameraReview.title</Trans>
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 space-y-3 text-sm text-primary-variant">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Switch
|
||||||
|
id="alerts-enabled"
|
||||||
|
className="mr-3"
|
||||||
|
checked={alertsState == "ON"}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
sendAlerts(isChecked ? "ON" : "OFF");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="alerts-enabled">
|
||||||
|
<Trans ns="views/settings">cameraReview.review.alerts</Trans>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Switch
|
||||||
|
id="detections-enabled"
|
||||||
|
className="mr-3"
|
||||||
|
checked={detectionsState == "ON"}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
sendDetections(isChecked ? "ON" : "OFF");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="detections-enabled">
|
||||||
|
<Trans ns="views/settings">camera.review.detections</Trans>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-sm text-muted-foreground">
|
||||||
|
<Trans ns="views/settings">cameraReview.review.desc</Trans>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{cameraConfig?.objects?.genai?.enabled_in_config && (
|
||||||
|
<>
|
||||||
|
<Separator className="my-2 flex bg-secondary" />
|
||||||
|
|
||||||
|
<Heading as="h4" className="my-2">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
cameraReview.object_descriptions.title
|
||||||
|
</Trans>
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 space-y-3 text-sm text-primary-variant">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Switch
|
||||||
|
id="object-descriptions-enabled"
|
||||||
|
className="mr-3"
|
||||||
|
checked={objDescState == "ON"}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
sendObjDesc(isChecked ? "ON" : "OFF");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="object-descriptions-enabled">
|
||||||
|
<Trans>button.enabled</Trans>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-sm text-muted-foreground">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
cameraReview.object_descriptions.desc
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cameraConfig?.review?.genai?.enabled_in_config && (
|
||||||
|
<>
|
||||||
|
<Separator className="my-2 flex bg-secondary" />
|
||||||
|
|
||||||
|
<Heading as="h4" className="my-2">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
cameraReview.review_descriptions.title
|
||||||
|
</Trans>
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<div className="mb-5 mt-2 flex max-w-5xl flex-col gap-2 space-y-3 text-sm text-primary-variant">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Switch
|
||||||
|
id="review-descriptions-enabled"
|
||||||
|
className="mr-3"
|
||||||
|
checked={revDescState == "ON"}
|
||||||
|
onCheckedChange={(isChecked) => {
|
||||||
|
sendRevDesc(isChecked ? "ON" : "OFF");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<Label htmlFor="review-descriptions-enabled">
|
||||||
|
<Trans>button.enabled</Trans>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-sm text-muted-foreground">
|
||||||
|
<Trans ns="views/settings">
|
||||||
|
cameraReview.review_descriptions.desc
|
||||||
|
</Trans>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CameraReviewClassification
|
||||||
|
selectedCamera={selectedCamera}
|
||||||
|
formContext={formContext}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,12 +1,13 @@
|
|||||||
import { createElement } from "react";
|
|
||||||
import type { ComponentType } from "react";
|
import type { ComponentType } from "react";
|
||||||
import SemanticSearchReindex from "./SemanticSearchReindex.tsx";
|
import SemanticSearchReindex from "./SemanticSearchReindex.tsx";
|
||||||
import CameraReviewSettingsView from "@/views/settings/CameraReviewSettingsView.tsx";
|
import CameraReviewStatusToggles from "./CameraReviewStatusToggles";
|
||||||
|
import type { ConfigFormContext } from "@/types/configForm";
|
||||||
|
|
||||||
// Props that will be injected into all section renderers
|
// Props that will be injected into all section renderers
|
||||||
export type SectionRendererProps = {
|
export type SectionRendererProps = {
|
||||||
selectedCamera?: string;
|
selectedCamera?: string;
|
||||||
setUnsavedChanges?: (hasChanges: boolean) => void;
|
setUnsavedChanges?: (hasChanges: boolean) => void;
|
||||||
|
formContext?: ConfigFormContext;
|
||||||
[key: string]: unknown; // Allow additional props from uiSchema
|
[key: string]: unknown; // Allow additional props from uiSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -17,20 +18,6 @@ export type SectionRenderers = Record<
|
|||||||
Record<string, RendererComponent>
|
Record<string, RendererComponent>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const CameraReviewSettingsRenderer: RendererComponent = ({
|
|
||||||
selectedCamera,
|
|
||||||
setUnsavedChanges,
|
|
||||||
}) => {
|
|
||||||
if (!selectedCamera) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return createElement(CameraReviewSettingsView, {
|
|
||||||
selectedCamera,
|
|
||||||
setUnsavedChanges,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Section renderers registry
|
// Section renderers registry
|
||||||
// Used to register custom renderer components for specific config sections.
|
// Used to register custom renderer components for specific config sections.
|
||||||
// Maps a section key (e.g., `semantic_search`) to a mapping of renderer
|
// Maps a section key (e.g., `semantic_search`) to a mapping of renderer
|
||||||
@ -55,7 +42,7 @@ export const sectionRenderers: SectionRenderers = {
|
|||||||
SemanticSearchReindex,
|
SemanticSearchReindex,
|
||||||
},
|
},
|
||||||
review: {
|
review: {
|
||||||
CameraReviewSettingsView: CameraReviewSettingsRenderer,
|
CameraReviewStatusToggles,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -324,7 +324,9 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
const renderers = formContext?.renderers;
|
const renderers = formContext?.renderers;
|
||||||
const RenderComponent = renderers?.[renderKey];
|
const RenderComponent = renderers?.[renderKey];
|
||||||
if (RenderComponent) {
|
if (RenderComponent) {
|
||||||
return <RenderComponent {...(spec.props ?? {})} />;
|
return (
|
||||||
|
<RenderComponent {...(spec.props ?? {})} formContext={formContext} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,9 @@ export type ConfigFormContext = {
|
|||||||
cameraName?: string;
|
cameraName?: string;
|
||||||
globalValue?: JsonValue;
|
globalValue?: JsonValue;
|
||||||
cameraValue?: JsonValue;
|
cameraValue?: JsonValue;
|
||||||
|
hasChanges?: boolean;
|
||||||
|
formData?: JsonObject;
|
||||||
|
onFormDataChange?: (data: ConfigSectionData) => void;
|
||||||
fullCameraConfig?: CameraConfig;
|
fullCameraConfig?: CameraConfig;
|
||||||
fullConfig?: FrigateConfig;
|
fullConfig?: FrigateConfig;
|
||||||
i18nNamespace?: string;
|
i18nNamespace?: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user