mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-09 21:41:15 +03:00
add semantic search model size widget
disables model_size select with n/a text when an embeddings genai provider is selected
This commit is contained in:
parent
01df9b15f2
commit
48d7ee4604
@ -1543,6 +1543,9 @@
|
|||||||
"builtIn": "Built-in Models",
|
"builtIn": "Built-in Models",
|
||||||
"genaiProviders": "GenAI Providers"
|
"genaiProviders": "GenAI Providers"
|
||||||
},
|
},
|
||||||
|
"semanticSearchModelSize": {
|
||||||
|
"notApplicable": "Not applicable for GenAI providers"
|
||||||
|
},
|
||||||
"review": {
|
"review": {
|
||||||
"title": "Review Settings"
|
"title": "Review Settings"
|
||||||
},
|
},
|
||||||
@ -1820,8 +1823,7 @@
|
|||||||
"mixedTypesSuggestion": "All detectors must use the same type. Remove existing detectors or select {{type}}."
|
"mixedTypesSuggestion": "All detectors must use the same type. Remove existing detectors or select {{type}}."
|
||||||
},
|
},
|
||||||
"semanticSearch": {
|
"semanticSearch": {
|
||||||
"jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended.",
|
"jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended."
|
||||||
"modelSizeIgnoredForProvider": "Model size only applies to the built-in Jina models. This value will be ignored when using a GenAI embedding provider."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
web/src/components/config-form/LiveFormDataContext.ts
Normal file
13
web/src/components/config-form/LiveFormDataContext.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
import type { ConfigSectionData } from "@/types/configForm";
|
||||||
|
|
||||||
|
// Mirrors the current section's in-flight form data so widgets can react
|
||||||
|
// to changes that RJSF wouldn't otherwise re-render them for. RJSF's
|
||||||
|
// Form memoizes SchemaField via deep equality and, in some transitions
|
||||||
|
// (notably reverting a field to its saved value), can skip re-rendering
|
||||||
|
// a widget even though the form data it depends on changed. useContext
|
||||||
|
// re-runs consumers directly on every provider value update, sidestepping
|
||||||
|
// that.
|
||||||
|
export const LiveFormDataContext = createContext<ConfigSectionData | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
@ -29,28 +29,13 @@ const semanticSearch: SectionConfigOverrides = {
|
|||||||
ctx.formData?.model === "jinav2" &&
|
ctx.formData?.model === "jinav2" &&
|
||||||
ctx.formData?.model_size === "small",
|
ctx.formData?.model_size === "small",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "model-size-ignored-for-provider",
|
|
||||||
field: "model_size",
|
|
||||||
messageKey: "configMessages.semanticSearch.modelSizeIgnoredForProvider",
|
|
||||||
severity: "info",
|
|
||||||
position: "after",
|
|
||||||
condition: (ctx) => {
|
|
||||||
const model = ctx.formData?.model;
|
|
||||||
return (
|
|
||||||
typeof model === "string" &&
|
|
||||||
model !== "" &&
|
|
||||||
model !== "jinav1" &&
|
|
||||||
model !== "jinav2"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
model: {
|
model: {
|
||||||
"ui:widget": "semanticSearchModel",
|
"ui:widget": "semanticSearchModel",
|
||||||
},
|
},
|
||||||
model_size: {
|
model_size: {
|
||||||
|
"ui:widget": "semanticSearchModelSize",
|
||||||
"ui:options": { size: "xs", enumI18nPrefix: "modelSize" },
|
"ui:options": { size: "xs", enumI18nPrefix: "modelSize" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -87,6 +87,7 @@ import type {
|
|||||||
import { useConfigMessages } from "@/hooks/use-config-messages";
|
import { useConfigMessages } from "@/hooks/use-config-messages";
|
||||||
import { ConfigMessageBanner } from "../ConfigMessageBanner";
|
import { ConfigMessageBanner } from "../ConfigMessageBanner";
|
||||||
import { FieldMessagesContext } from "../FieldMessagesContext";
|
import { FieldMessagesContext } from "../FieldMessagesContext";
|
||||||
|
import { LiveFormDataContext } from "../LiveFormDataContext";
|
||||||
|
|
||||||
export interface SectionConfig {
|
export interface SectionConfig {
|
||||||
/** Field ordering within the section */
|
/** Field ordering within the section */
|
||||||
@ -998,59 +999,63 @@ export function ConfigSection({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<ConfigMessageBanner messages={activeMessages} />
|
<ConfigMessageBanner messages={activeMessages} />
|
||||||
<FieldMessagesContext.Provider value={activeFieldMessages}>
|
<FieldMessagesContext.Provider value={activeFieldMessages}>
|
||||||
<ConfigForm
|
<LiveFormDataContext.Provider
|
||||||
key={formKey}
|
value={(currentFormData as ConfigSectionData | null) ?? null}
|
||||||
schema={modifiedSchema}
|
>
|
||||||
formData={currentFormData}
|
<ConfigForm
|
||||||
onChange={handleChange}
|
key={formKey}
|
||||||
onValidationChange={setHasValidationErrors}
|
schema={modifiedSchema}
|
||||||
fieldOrder={sectionConfig.fieldOrder}
|
formData={currentFormData}
|
||||||
fieldGroups={sectionConfig.fieldGroups}
|
onChange={handleChange}
|
||||||
hiddenFields={effectiveHiddenFields}
|
onValidationChange={setHasValidationErrors}
|
||||||
advancedFields={sectionConfig.advancedFields}
|
fieldOrder={sectionConfig.fieldOrder}
|
||||||
liveValidate={sectionConfig.liveValidate}
|
fieldGroups={sectionConfig.fieldGroups}
|
||||||
uiSchema={sectionConfig.uiSchema}
|
hiddenFields={effectiveHiddenFields}
|
||||||
disabled={disabled || isSaving}
|
advancedFields={sectionConfig.advancedFields}
|
||||||
readonly={readonly}
|
liveValidate={sectionConfig.liveValidate}
|
||||||
showSubmit={false}
|
uiSchema={sectionConfig.uiSchema}
|
||||||
i18nNamespace={configNamespace}
|
disabled={disabled || isSaving}
|
||||||
customValidate={customValidate}
|
readonly={readonly}
|
||||||
formContext={{
|
showSubmit={false}
|
||||||
level: effectiveLevel,
|
i18nNamespace={configNamespace}
|
||||||
cameraName,
|
customValidate={customValidate}
|
||||||
globalValue,
|
formContext={{
|
||||||
cameraValue,
|
level: effectiveLevel,
|
||||||
hasChanges,
|
cameraName,
|
||||||
extraHasChanges,
|
globalValue,
|
||||||
setExtraHasChanges,
|
cameraValue,
|
||||||
overrides: uiOverrides as JsonValue | undefined,
|
hasChanges,
|
||||||
formData: currentFormData as ConfigSectionData,
|
extraHasChanges,
|
||||||
baselineFormData: effectiveBaselineFormData as ConfigSectionData,
|
setExtraHasChanges,
|
||||||
pendingDataBySection,
|
overrides: uiOverrides as JsonValue | undefined,
|
||||||
onPendingDataChange,
|
formData: currentFormData as ConfigSectionData,
|
||||||
onFormDataChange: (data: ConfigSectionData) => handleChange(data),
|
baselineFormData: effectiveBaselineFormData as ConfigSectionData,
|
||||||
// For widgets that need access to full camera config (e.g., zone names)
|
pendingDataBySection,
|
||||||
fullCameraConfig:
|
onPendingDataChange,
|
||||||
effectiveLevel === "camera" && cameraName
|
onFormDataChange: (data: ConfigSectionData) => handleChange(data),
|
||||||
? config?.cameras?.[cameraName]
|
// For widgets that need access to full camera config (e.g., zone names)
|
||||||
: undefined,
|
fullCameraConfig:
|
||||||
fullConfig: config,
|
effectiveLevel === "camera" && cameraName
|
||||||
// When rendering camera-level sections, provide the section path so
|
? config?.cameras?.[cameraName]
|
||||||
// field templates can look up keys under the `config/cameras` namespace
|
: undefined,
|
||||||
// When using a consolidated global namespace, keys are nested
|
fullConfig: config,
|
||||||
// under the section name (e.g., `audio.label`) so provide the
|
// When rendering camera-level sections, provide the section path so
|
||||||
// section prefix to templates so they can attempt `${section}.${field}` lookups.
|
// field templates can look up keys under the `config/cameras` namespace
|
||||||
sectionI18nPrefix: sectionPath,
|
// When using a consolidated global namespace, keys are nested
|
||||||
t,
|
// under the section name (e.g., `audio.label`) so provide the
|
||||||
renderers: wrappedRenderers,
|
// section prefix to templates so they can attempt `${section}.${field}` lookups.
|
||||||
sectionDocs: sectionConfig.sectionDocs,
|
sectionI18nPrefix: sectionPath,
|
||||||
fieldDocs: sectionConfig.fieldDocs,
|
t,
|
||||||
hiddenFields: effectiveHiddenFields,
|
renderers: wrappedRenderers,
|
||||||
restartRequired: sectionConfig.restartRequired,
|
sectionDocs: sectionConfig.sectionDocs,
|
||||||
requiresRestart,
|
fieldDocs: sectionConfig.fieldDocs,
|
||||||
isProfile: !!profileName,
|
hiddenFields: effectiveHiddenFields,
|
||||||
}}
|
restartRequired: sectionConfig.restartRequired,
|
||||||
/>
|
requiresRestart,
|
||||||
|
isProfile: !!profileName,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LiveFormDataContext.Provider>
|
||||||
</FieldMessagesContext.Provider>
|
</FieldMessagesContext.Provider>
|
||||||
|
|
||||||
{!embedded && (
|
{!embedded && (
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget";
|
|||||||
import { CameraPathWidget } from "./widgets/CameraPathWidget";
|
import { CameraPathWidget } from "./widgets/CameraPathWidget";
|
||||||
import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget";
|
import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget";
|
||||||
import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget";
|
import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget";
|
||||||
|
import { SemanticSearchModelSizeWidget } from "./widgets/SemanticSearchModelSizeWidget";
|
||||||
import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget";
|
import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget";
|
||||||
|
|
||||||
import { FieldTemplate } from "./templates/FieldTemplate";
|
import { FieldTemplate } from "./templates/FieldTemplate";
|
||||||
@ -86,6 +87,7 @@ export const frigateTheme: FrigateTheme = {
|
|||||||
timezoneSelect: TimezoneSelectWidget,
|
timezoneSelect: TimezoneSelectWidget,
|
||||||
optionalField: OptionalFieldWidget,
|
optionalField: OptionalFieldWidget,
|
||||||
semanticSearchModel: SemanticSearchModelWidget,
|
semanticSearchModel: SemanticSearchModelWidget,
|
||||||
|
semanticSearchModelSize: SemanticSearchModelSizeWidget,
|
||||||
onvifProfile: OnvifProfileWidget,
|
onvifProfile: OnvifProfileWidget,
|
||||||
},
|
},
|
||||||
templates: {
|
templates: {
|
||||||
|
|||||||
@ -0,0 +1,57 @@
|
|||||||
|
// Disables model_size and shows "N/A" when a GenAI provider is selected.
|
||||||
|
// Reads model via LiveFormDataContext so it re-runs even when RJSF's
|
||||||
|
// SchemaField memoization would skip this widget.
|
||||||
|
import type { WidgetProps } from "@rjsf/utils";
|
||||||
|
import { useContext, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { LiveFormDataContext } from "../../LiveFormDataContext";
|
||||||
|
import { getSizedFieldClassName } from "../utils";
|
||||||
|
import { SelectWidget } from "./SelectWidget";
|
||||||
|
|
||||||
|
export function SemanticSearchModelSizeWidget(props: WidgetProps) {
|
||||||
|
const { t } = useTranslation(["views/settings"]);
|
||||||
|
const liveFormData = useContext(LiveFormDataContext);
|
||||||
|
const model = liveFormData?.model;
|
||||||
|
const isProvider =
|
||||||
|
typeof model === "string" &&
|
||||||
|
model !== "" &&
|
||||||
|
model !== "jinav1" &&
|
||||||
|
model !== "jinav2";
|
||||||
|
|
||||||
|
// Clear model_size while on a provider (buildOverrides converts to ""
|
||||||
|
// which the backend treats as "remove"). Restore the schema default
|
||||||
|
// when returning to a Jina model so the field isn't left empty.
|
||||||
|
const { value, onChange, schema } = props;
|
||||||
|
const schemaDefault = schema?.default as string | undefined;
|
||||||
|
useEffect(() => {
|
||||||
|
if (isProvider && value !== undefined) {
|
||||||
|
onChange(undefined);
|
||||||
|
} else if (!isProvider && value === undefined && schemaDefault) {
|
||||||
|
onChange(schemaDefault);
|
||||||
|
}
|
||||||
|
}, [isProvider, value, onChange, schemaDefault]);
|
||||||
|
|
||||||
|
if (isProvider) {
|
||||||
|
const fieldClassName = getSizedFieldClassName(props.options ?? {}, "sm");
|
||||||
|
return (
|
||||||
|
<Select value="" disabled>
|
||||||
|
<SelectTrigger className={fieldClassName}>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t("configForm.semanticSearchModelSize.notApplicable", {
|
||||||
|
defaultValue: "Not applicable for GenAI providers",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent />
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SelectWidget {...props} />;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user