diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json
index 06d9279f7..e075c7e0e 100644
--- a/web/public/locales/en/views/settings.json
+++ b/web/public/locales/en/views/settings.json
@@ -1433,8 +1433,7 @@
},
"reviewLabels": {
"summary": "{{count}} labels selected",
- "empty": "No labels available",
- "allNonAlertDetections": "All non-alert activity will be included as detections."
+ "empty": "No labels available"
},
"filters": {
"objectFieldLabel": "{{field}} for {{label}}"
@@ -1606,5 +1605,38 @@
"onvif": {
"profileAuto": "Auto",
"profileLoading": "Loading profiles..."
+ },
+ "configMessages": {
+ "review": {
+ "recordDisabled": "Recording is disabled, review items will not be generated.",
+ "detectDisabled": "Object detection is disabled. Review items require detected objects to categorize alerts and detections.",
+ "allNonAlertDetections": "All non-alert activity will be included as detections."
+ },
+ "audio": {
+ "noAudioRole": "No streams have the audio role defined. You must enable the audio role for audio detection to function."
+ },
+ "audioTranscription": {
+ "audioDetectionDisabled": "Audio detection is not enabled for this camera. Audio transcription requires audio detection to be active."
+ },
+ "detect": {
+ "fpsGreaterThanFive": "Setting the detect FPS higher than 5 is not recommended."
+ },
+ "faceRecognition": {
+ "globalDisabled": "Face recognition is not enabled at the global level. Enable it in global settings for camera-level face recognition to function.",
+ "personNotTracked": "Face recognition requires the 'person' object to be tracked. Ensure 'person' is in the object tracking list."
+ },
+ "lpr": {
+ "globalDisabled": "License plate recognition is not enabled at the global level. Enable it in global settings for camera-level LPR to function.",
+ "vehicleNotTracked": "License plate recognition requires 'car' or 'motorcycle' to be tracked."
+ },
+ "record": {
+ "noRecordRole": "No streams have the record role defined. Recording will not function."
+ },
+ "birdseye": {
+ "objectsModeDetectDisabled": "Birdseye is set to 'objects' mode, but object detection is disabled for this camera. The camera will not appear in Birdseye."
+ },
+ "snapshots": {
+ "detectDisabled": "Object detection is disabled. Snapshots are generated from tracked objects and will not be created."
+ }
}
}
diff --git a/web/src/components/config-form/ConfigFieldMessage.tsx b/web/src/components/config-form/ConfigFieldMessage.tsx
new file mode 100644
index 000000000..5c0a5c505
--- /dev/null
+++ b/web/src/components/config-form/ConfigFieldMessage.tsx
@@ -0,0 +1,48 @@
+import { useTranslation } from "react-i18next";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { LuInfo, LuTriangleAlert, LuCircleAlert } from "react-icons/lu";
+import type { MessageSeverity } from "./section-configs/types";
+
+const severityVariantMap: Record<
+ MessageSeverity,
+ "info" | "warning" | "destructive"
+> = {
+ info: "info",
+ warning: "warning",
+ error: "destructive",
+};
+
+function SeverityIcon({ severity }: { severity: string }) {
+ switch (severity) {
+ case "info":
+ return ;
+ case "warning":
+ return ;
+ case "error":
+ return ;
+ default:
+ return ;
+ }
+}
+
+type ConfigFieldMessageProps = {
+ messageKey: string;
+ severity: string;
+};
+
+export function ConfigFieldMessage({
+ messageKey,
+ severity,
+}: ConfigFieldMessageProps) {
+ const { t } = useTranslation("views/settings");
+
+ return (
+
+
+ {t(messageKey)}
+
+ );
+}
diff --git a/web/src/components/config-form/ConfigMessageBanner.tsx b/web/src/components/config-form/ConfigMessageBanner.tsx
new file mode 100644
index 000000000..f5b828000
--- /dev/null
+++ b/web/src/components/config-form/ConfigMessageBanner.tsx
@@ -0,0 +1,52 @@
+import { useTranslation } from "react-i18next";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { LuInfo, LuTriangleAlert, LuCircleAlert } from "react-icons/lu";
+import type {
+ ConditionalMessage,
+ MessageSeverity,
+} from "./section-configs/types";
+
+const severityVariantMap: Record<
+ MessageSeverity,
+ "info" | "warning" | "destructive"
+> = {
+ info: "info",
+ warning: "warning",
+ error: "destructive",
+};
+
+function SeverityIcon({ severity }: { severity: MessageSeverity }) {
+ switch (severity) {
+ case "info":
+ return ;
+ case "warning":
+ return ;
+ case "error":
+ return ;
+ }
+}
+
+type ConfigMessageBannerProps = {
+ messages: ConditionalMessage[];
+};
+
+export function ConfigMessageBanner({ messages }: ConfigMessageBannerProps) {
+ const { t } = useTranslation("views/settings");
+
+ if (messages.length === 0) return null;
+
+ return (
+
+ {messages.map((msg) => (
+
+
+ {t(msg.messageKey)}
+
+ ))}
+
+ );
+}
diff --git a/web/src/components/config-form/section-configs/audio.ts b/web/src/components/config-form/section-configs/audio.ts
index a112fa0db..31f19e93d 100644
--- a/web/src/components/config-form/section-configs/audio.ts
+++ b/web/src/components/config-form/section-configs/audio.ts
@@ -3,6 +3,21 @@ import type { SectionConfigOverrides } from "./types";
const audio: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/audio_detectors",
+ messages: [
+ {
+ key: "no-audio-role",
+ messageKey: "configMessages.audio.noAudioRole",
+ severity: "warning",
+ condition: (ctx) => {
+ if (ctx.level === "camera" && ctx.fullCameraConfig) {
+ return !ctx.fullCameraConfig.ffmpeg?.inputs?.some((input) =>
+ input.roles?.includes("audio"),
+ );
+ }
+ return false;
+ },
+ },
+ ],
restartRequired: [],
fieldOrder: [
"enabled",
diff --git a/web/src/components/config-form/section-configs/audio_transcription.ts b/web/src/components/config-form/section-configs/audio_transcription.ts
index 169a77954..8e8e70d77 100644
--- a/web/src/components/config-form/section-configs/audio_transcription.ts
+++ b/web/src/components/config-form/section-configs/audio_transcription.ts
@@ -3,6 +3,19 @@ import type { SectionConfigOverrides } from "./types";
const audioTranscription: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/audio_detectors#audio-transcription",
+ messages: [
+ {
+ key: "audio-detection-disabled",
+ messageKey: "configMessages.audioTranscription.audioDetectionDisabled",
+ severity: "warning",
+ condition: (ctx) => {
+ if (ctx.level === "camera" && ctx.fullCameraConfig) {
+ return ctx.fullCameraConfig.audio.enabled === false;
+ }
+ return false;
+ },
+ },
+ ],
restartRequired: [],
fieldOrder: ["enabled", "language", "device", "model_size"],
hiddenFields: ["enabled_in_config", "live_enabled"],
diff --git a/web/src/components/config-form/section-configs/birdseye.ts b/web/src/components/config-form/section-configs/birdseye.ts
index 63fae75d9..d621c9203 100644
--- a/web/src/components/config-form/section-configs/birdseye.ts
+++ b/web/src/components/config-form/section-configs/birdseye.ts
@@ -3,6 +3,20 @@ import type { SectionConfigOverrides } from "./types";
const birdseye: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/birdseye",
+ messages: [
+ {
+ key: "objects-mode-detect-disabled",
+ messageKey: "configMessages.birdseye.objectsModeDetectDisabled",
+ severity: "info",
+ condition: (ctx) => {
+ if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
+ return (
+ ctx.formData?.mode === "objects" &&
+ ctx.fullCameraConfig.detect?.enabled === false
+ );
+ },
+ },
+ ],
restartRequired: [],
fieldOrder: ["enabled", "mode", "order"],
hiddenFields: [],
diff --git a/web/src/components/config-form/section-configs/detect.ts b/web/src/components/config-form/section-configs/detect.ts
index 778620f1c..ef14d13fd 100644
--- a/web/src/components/config-form/section-configs/detect.ts
+++ b/web/src/components/config-form/section-configs/detect.ts
@@ -3,6 +3,21 @@ import type { SectionConfigOverrides } from "./types";
const detect: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/camera_specific",
+ fieldMessages: [
+ {
+ key: "fps-greater-than-five",
+ field: "fps",
+ messageKey: "configMessages.detect.fpsGreaterThanFive",
+ severity: "info",
+ position: "after",
+ condition: (ctx) => {
+ if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
+ const detectFps = ctx.formData?.fps as number | undefined;
+ const streamFps = ctx.fullCameraConfig.detect?.fps;
+ return detectFps != null && streamFps != null && detectFps > 5;
+ },
+ },
+ ],
fieldOrder: [
"enabled",
"width",
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 ef9e43506..822f6ffe0 100644
--- a/web/src/components/config-form/section-configs/face_recognition.ts
+++ b/web/src/components/config-form/section-configs/face_recognition.ts
@@ -3,6 +3,26 @@ import type { SectionConfigOverrides } from "./types";
const faceRecognition: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/face_recognition",
+ messages: [
+ {
+ key: "global-disabled",
+ messageKey: "configMessages.faceRecognition.globalDisabled",
+ severity: "warning",
+ condition: (ctx) => {
+ if (ctx.level !== "camera") return false;
+ return ctx.fullConfig.face_recognition?.enabled === false;
+ },
+ },
+ {
+ key: "person-not-tracked",
+ messageKey: "configMessages.faceRecognition.personNotTracked",
+ severity: "info",
+ condition: (ctx) => {
+ if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
+ return !ctx.fullCameraConfig.objects?.track?.includes("person");
+ },
+ },
+ ],
restartRequired: [],
fieldOrder: ["enabled", "min_area"],
hiddenFields: [],
diff --git a/web/src/components/config-form/section-configs/lpr.ts b/web/src/components/config-form/section-configs/lpr.ts
index 514dba9be..4997d766f 100644
--- a/web/src/components/config-form/section-configs/lpr.ts
+++ b/web/src/components/config-form/section-configs/lpr.ts
@@ -3,6 +3,28 @@ import type { SectionConfigOverrides } from "./types";
const lpr: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/license_plate_recognition",
+ messages: [
+ {
+ key: "global-disabled",
+ messageKey: "configMessages.lpr.globalDisabled",
+ severity: "warning",
+ condition: (ctx) => {
+ if (ctx.level !== "camera") return false;
+ return ctx.fullConfig.lpr?.enabled === false;
+ },
+ },
+ {
+ key: "vehicle-not-tracked",
+ messageKey: "configMessages.lpr.vehicleNotTracked",
+ severity: "info",
+ condition: (ctx) => {
+ if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
+ if (ctx.fullCameraConfig.type === "lpr") return false;
+ const tracked = ctx.fullCameraConfig.objects?.track ?? [];
+ return !tracked.some((o) => ["car", "motorcycle"].includes(o));
+ },
+ },
+ ],
fieldDocs: {
enhancement: "/configuration/license_plate_recognition#enhancement",
},
diff --git a/web/src/components/config-form/section-configs/record.ts b/web/src/components/config-form/section-configs/record.ts
index 05a21f224..35f3b1ef7 100644
--- a/web/src/components/config-form/section-configs/record.ts
+++ b/web/src/components/config-form/section-configs/record.ts
@@ -3,6 +3,19 @@ import type { SectionConfigOverrides } from "./types";
const record: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/record",
+ messages: [
+ {
+ key: "no-record-role",
+ messageKey: "configMessages.record.noRecordRole",
+ severity: "warning",
+ condition: (ctx) => {
+ if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
+ return !ctx.fullCameraConfig.ffmpeg?.inputs?.some((i) =>
+ i.roles?.includes("record"),
+ );
+ },
+ },
+ ],
restartRequired: [],
fieldOrder: [
"enabled",
diff --git a/web/src/components/config-form/section-configs/review.ts b/web/src/components/config-form/section-configs/review.ts
index e9e3169d7..6f769179d 100644
--- a/web/src/components/config-form/section-configs/review.ts
+++ b/web/src/components/config-form/section-configs/review.ts
@@ -3,6 +3,45 @@ import type { SectionConfigOverrides } from "./types";
const review: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/review",
+ messages: [
+ {
+ key: "record-disabled",
+ messageKey: "configMessages.review.recordDisabled",
+ severity: "warning",
+ condition: (ctx) => {
+ if (ctx.level === "camera" && ctx.fullCameraConfig) {
+ return ctx.fullCameraConfig.record.enabled === false;
+ }
+ return ctx.fullConfig.record?.enabled === false;
+ },
+ },
+ {
+ key: "detect-disabled",
+ messageKey: "configMessages.review.detectDisabled",
+ severity: "info",
+ condition: (ctx) => {
+ if (ctx.level === "camera" && ctx.fullCameraConfig) {
+ return ctx.fullCameraConfig.detect?.enabled === false;
+ }
+ return false;
+ },
+ },
+ ],
+ fieldMessages: [
+ {
+ key: "detections-all-non-alert",
+ field: "detections.labels",
+ messageKey: "configMessages.review.allNonAlertDetections",
+ severity: "info",
+ position: "after",
+ condition: (ctx) => {
+ const labels = (
+ ctx.formData?.detections as Record | undefined
+ )?.labels;
+ return !Array.isArray(labels) || labels.length === 0;
+ },
+ },
+ ],
fieldDocs: {
"alerts.labels": "/configuration/review/#alerts-and-detections",
"detections.labels": "/configuration/review/#alerts-and-detections",
@@ -35,8 +74,6 @@ const review: SectionConfigOverrides = {
"ui:widget": "reviewLabels",
"ui:options": {
suppressMultiSchema: true,
- emptySelectionHintKey:
- "configForm.reviewLabels.allNonAlertDetections",
},
},
required_zones: {
diff --git a/web/src/components/config-form/section-configs/snapshots.ts b/web/src/components/config-form/section-configs/snapshots.ts
index 126ecd496..7d08cc728 100644
--- a/web/src/components/config-form/section-configs/snapshots.ts
+++ b/web/src/components/config-form/section-configs/snapshots.ts
@@ -3,6 +3,17 @@ import type { SectionConfigOverrides } from "./types";
const snapshots: SectionConfigOverrides = {
base: {
sectionDocs: "/configuration/snapshots",
+ messages: [
+ {
+ key: "detect-disabled",
+ messageKey: "configMessages.snapshots.detectDisabled",
+ severity: "info",
+ condition: (ctx) => {
+ if (ctx.level !== "camera" || !ctx.fullCameraConfig) return false;
+ return ctx.fullCameraConfig.detect?.enabled === false;
+ },
+ },
+ ],
restartRequired: [],
fieldOrder: [
"enabled",
diff --git a/web/src/components/config-form/section-configs/types.ts b/web/src/components/config-form/section-configs/types.ts
index e2b308e08..9efeb2b32 100644
--- a/web/src/components/config-form/section-configs/types.ts
+++ b/web/src/components/config-form/section-configs/types.ts
@@ -1,5 +1,39 @@
+import type { FrigateConfig, CameraConfig } from "@/types/frigateConfig";
+import type { ConfigSectionData } from "@/types/configForm";
import type { SectionConfig } from "../sections/BaseSection";
+/** Context provided to message condition functions */
+export type MessageConditionContext = {
+ fullConfig: FrigateConfig;
+ fullCameraConfig?: CameraConfig;
+ level: "global" | "camera";
+ cameraName?: string;
+ formData: ConfigSectionData;
+};
+
+/** Severity levels for conditional messages */
+export type MessageSeverity = "info" | "warning" | "error";
+
+/** A conditional message definition */
+export type ConditionalMessage = {
+ /** Unique key for React list rendering and deduplication */
+ key: string;
+ /** Translation key resolved via t() in the views/settings namespace */
+ messageKey: string;
+ /** Severity level controlling visual styling */
+ severity: MessageSeverity;
+ /** Function returning true when the message should be shown */
+ condition: (ctx: MessageConditionContext) => boolean;
+};
+
+/** Field-level conditional message, adds field targeting */
+export type FieldConditionalMessage = ConditionalMessage & {
+ /** Dot-separated field path (e.g., "enabled", "alerts.labels") */
+ field: string;
+ /** Whether to render before or after the field (default: "before") */
+ position?: "before" | "after";
+};
+
export type SectionConfigOverrides = {
base?: SectionConfig;
global?: Partial;
diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx
index 8a35f4dff..96bd3efa9 100644
--- a/web/src/components/config-form/sections/BaseSection.tsx
+++ b/web/src/components/config-form/sections/BaseSection.tsx
@@ -71,6 +71,13 @@ import {
} from "@/utils/configUtil";
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
import { useRestart } from "@/api/ws";
+import type {
+ ConditionalMessage,
+ FieldConditionalMessage,
+ MessageConditionContext,
+} from "../section-configs/types";
+import { useConfigMessages } from "@/hooks/use-config-messages";
+import { ConfigMessageBanner } from "../ConfigMessageBanner";
export interface SectionConfig {
/** Field ordering within the section */
@@ -100,6 +107,10 @@ export interface SectionConfig {
formData: unknown,
errors: FormValidation,
) => FormValidation;
+ /** Conditional messages displayed as banners above the section form */
+ messages?: ConditionalMessage[];
+ /** Conditional messages displayed inline with specific fields */
+ fieldMessages?: FieldConditionalMessage[];
}
export interface BaseSectionProps {
@@ -536,6 +547,65 @@ export function ConfigSection({
const currentFormData = pendingData || formData;
const effectiveBaselineFormData = baselineSnapshot;
+ // Build context for conditional messages
+ const messageContext = useMemo(() => {
+ if (!config || !currentFormData) return undefined;
+ return {
+ fullConfig: config,
+ fullCameraConfig:
+ effectiveLevel === "camera" && cameraName
+ ? config.cameras?.[cameraName]
+ : undefined,
+ level: effectiveLevel,
+ cameraName,
+ formData: currentFormData as ConfigSectionData,
+ };
+ }, [config, currentFormData, effectiveLevel, cameraName]);
+
+ const { activeMessages, activeFieldMessages } = useConfigMessages(
+ sectionConfig.messages,
+ sectionConfig.fieldMessages,
+ messageContext,
+ );
+
+ // Merge field-level conditional messages into uiSchema
+ const effectiveUiSchema = useMemo(() => {
+ if (activeFieldMessages.length === 0) return sectionConfig.uiSchema;
+ const merged = { ...(sectionConfig.uiSchema ?? {}) };
+ for (const msg of activeFieldMessages) {
+ const segments = msg.field.split(".");
+ // Navigate to the nested uiSchema node, shallow-cloning along the way
+ let node = merged;
+ for (let i = 0; i < segments.length - 1; i++) {
+ const seg = segments[i];
+ node[seg] = { ...(node[seg] as Record) };
+ node = node[seg] as Record;
+ }
+ const leafKey = segments[segments.length - 1];
+ const existing = node[leafKey] as Record | undefined;
+ const existingMessages = ((existing?.["ui:messages"] as unknown[]) ??
+ []) as Array<{
+ key: string;
+ messageKey: string;
+ severity: string;
+ position?: string;
+ }>;
+ node[leafKey] = {
+ ...existing,
+ "ui:messages": [
+ ...existingMessages,
+ {
+ key: msg.key,
+ messageKey: msg.messageKey,
+ severity: msg.severity,
+ position: msg.position ?? "before",
+ },
+ ],
+ };
+ }
+ return merged;
+ }, [sectionConfig.uiSchema, activeFieldMessages]);
+
const currentOverrides = useMemo(() => {
if (!currentFormData || typeof currentFormData !== "object") {
return undefined;
@@ -874,6 +944,7 @@ export function ConfigSection({
const sectionContent = (
+
+ | undefined;
+ const beforeMessages = fieldMessageSpecs?.filter(
+ (m) => (m.position ?? "before") === "before",
+ );
+ const afterMessages = fieldMessageSpecs?.filter(
+ (m) => m.position === "after",
+ );
+ const beforeMessagesContent =
+ beforeMessages && beforeMessages.length > 0 ? (
+
+ {beforeMessages.map((m) => (
+
+ ))}
+
+ ) : null;
+ const afterMessagesContent =
+ afterMessages && afterMessages.length > 0 ? (
+
+ {afterMessages.map((m) => (
+
+ ))}
+
+ ) : null;
const WrapIfAdditionalTemplate = getTemplate(
"WrapIfAdditionalTemplate",
registry,
@@ -600,6 +641,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
>
{beforeContent}
+ {beforeMessagesContent}
{renderStandardLabel()}
{renderFieldLayout()}
@@ -607,6 +649,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
{errors}
{help}
+ {afterMessagesContent}
{afterContent}
diff --git a/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx b/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx
index a7351c8b7..46fb5345e 100644
--- a/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx
+++ b/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx
@@ -45,8 +45,6 @@ export type SwitchesWidgetOptions = {
enableSearch?: boolean;
/** Allow users to add custom entries not in the predefined list */
allowCustomEntries?: boolean;
- /** i18n key for a hint shown when no entities are selected */
- emptySelectionHintKey?: string;
};
function normalizeValue(value: unknown): string[] {
@@ -131,11 +129,6 @@ export function SwitchesWidget(props: WidgetProps) {
[props.options],
);
- const emptySelectionHintKey = useMemo(
- () => props.options?.emptySelectionHintKey as string | undefined,
- [props.options],
- );
-
const selectedEntities = useMemo(() => normalizeValue(value), [value]);
const [isOpen, setIsOpen] = useState(selectedEntities.length > 0);
const [searchTerm, setSearchTerm] = useState("");
@@ -215,12 +208,6 @@ export function SwitchesWidget(props: WidgetProps) {
- {emptySelectionHintKey && selectedEntities.length === 0 && t && (
-
- {t(emptySelectionHintKey, { ns: namespace })}
-
- )}
-
{allEntities.length === 0 && !allowCustomEntries ? (
{emptyMessage}
diff --git a/web/src/components/ui/alert.tsx b/web/src/components/ui/alert.tsx
index 32005241a..5a3d48b1d 100644
--- a/web/src/components/ui/alert.tsx
+++ b/web/src/components/ui/alert.tsx
@@ -11,6 +11,9 @@ const alertVariants = cva(
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ warning:
+ "border-amber-500/50 bg-amber-50 text-amber-800 dark:bg-amber-950/20 dark:text-amber-400 dark:border-amber-500/30 [&>svg]:text-amber-600 dark:[&>svg]:text-amber-400",
+ info: "border-blue-500/50 bg-blue-50 text-blue-800 dark:bg-blue-950/20 dark:text-blue-400 dark:border-blue-500/30 [&>svg]:text-blue-600 dark:[&>svg]:text-blue-400",
},
},
defaultVariants: {
diff --git a/web/src/hooks/use-config-messages.ts b/web/src/hooks/use-config-messages.ts
new file mode 100644
index 000000000..6c2b43853
--- /dev/null
+++ b/web/src/hooks/use-config-messages.ts
@@ -0,0 +1,27 @@
+import { useMemo } from "react";
+import type {
+ ConditionalMessage,
+ FieldConditionalMessage,
+ MessageConditionContext,
+} from "@/components/config-form/section-configs/types";
+
+export function useConfigMessages(
+ messages: ConditionalMessage[] | undefined,
+ fieldMessages: FieldConditionalMessage[] | undefined,
+ context: MessageConditionContext | undefined,
+): {
+ activeMessages: ConditionalMessage[];
+ activeFieldMessages: FieldConditionalMessage[];
+} {
+ const activeMessages = useMemo(() => {
+ if (!messages || !context) return [];
+ return messages.filter((msg) => msg.condition(context));
+ }, [messages, context]);
+
+ const activeFieldMessages = useMemo(() => {
+ if (!fieldMessages || !context) return [];
+ return fieldMessages.filter((msg) => msg.condition(context));
+ }, [fieldMessages, context]);
+
+ return { activeMessages, activeFieldMessages };
+}