mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-06-21 03:41:55 +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",
|
||||
"genaiProviders": "GenAI Providers"
|
||||
},
|
||||
"semanticSearchModelSize": {
|
||||
"notApplicable": "Not applicable for GenAI providers"
|
||||
},
|
||||
"review": {
|
||||
"title": "Review Settings"
|
||||
},
|
||||
@ -1820,8 +1823,7 @@
|
||||
"mixedTypesSuggestion": "All detectors must use the same type. Remove existing detectors or select {{type}}."
|
||||
},
|
||||
"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.",
|
||||
"modelSizeIgnoredForProvider": "Model size only applies to the built-in Jina models. This value will be ignored when using a GenAI embedding provider."
|
||||
"jinav2SmallModelSize": "The 'small' size with the Jina V2 model has high RAM and inference cost. The 'large' model with a discrete GPU is recommended."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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_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: {
|
||||
model: {
|
||||
"ui:widget": "semanticSearchModel",
|
||||
},
|
||||
model_size: {
|
||||
"ui:widget": "semanticSearchModelSize",
|
||||
"ui:options": { size: "xs", enumI18nPrefix: "modelSize" },
|
||||
},
|
||||
},
|
||||
|
||||
@ -87,6 +87,7 @@ import type {
|
||||
import { useConfigMessages } from "@/hooks/use-config-messages";
|
||||
import { ConfigMessageBanner } from "../ConfigMessageBanner";
|
||||
import { FieldMessagesContext } from "../FieldMessagesContext";
|
||||
import { LiveFormDataContext } from "../LiveFormDataContext";
|
||||
|
||||
export interface SectionConfig {
|
||||
/** Field ordering within the section */
|
||||
@ -998,59 +999,63 @@ export function ConfigSection({
|
||||
<div className="space-y-6">
|
||||
<ConfigMessageBanner messages={activeMessages} />
|
||||
<FieldMessagesContext.Provider value={activeFieldMessages}>
|
||||
<ConfigForm
|
||||
key={formKey}
|
||||
schema={modifiedSchema}
|
||||
formData={currentFormData}
|
||||
onChange={handleChange}
|
||||
onValidationChange={setHasValidationErrors}
|
||||
fieldOrder={sectionConfig.fieldOrder}
|
||||
fieldGroups={sectionConfig.fieldGroups}
|
||||
hiddenFields={effectiveHiddenFields}
|
||||
advancedFields={sectionConfig.advancedFields}
|
||||
liveValidate={sectionConfig.liveValidate}
|
||||
uiSchema={sectionConfig.uiSchema}
|
||||
disabled={disabled || isSaving}
|
||||
readonly={readonly}
|
||||
showSubmit={false}
|
||||
i18nNamespace={configNamespace}
|
||||
customValidate={customValidate}
|
||||
formContext={{
|
||||
level: effectiveLevel,
|
||||
cameraName,
|
||||
globalValue,
|
||||
cameraValue,
|
||||
hasChanges,
|
||||
extraHasChanges,
|
||||
setExtraHasChanges,
|
||||
overrides: uiOverrides as JsonValue | undefined,
|
||||
formData: currentFormData as ConfigSectionData,
|
||||
baselineFormData: effectiveBaselineFormData as ConfigSectionData,
|
||||
pendingDataBySection,
|
||||
onPendingDataChange,
|
||||
onFormDataChange: (data: ConfigSectionData) => handleChange(data),
|
||||
// For widgets that need access to full camera config (e.g., zone names)
|
||||
fullCameraConfig:
|
||||
effectiveLevel === "camera" && cameraName
|
||||
? config?.cameras?.[cameraName]
|
||||
: undefined,
|
||||
fullConfig: config,
|
||||
// When rendering camera-level sections, provide the section path so
|
||||
// field templates can look up keys under the `config/cameras` namespace
|
||||
// When using a consolidated global namespace, keys are nested
|
||||
// under the section name (e.g., `audio.label`) so provide the
|
||||
// section prefix to templates so they can attempt `${section}.${field}` lookups.
|
||||
sectionI18nPrefix: sectionPath,
|
||||
t,
|
||||
renderers: wrappedRenderers,
|
||||
sectionDocs: sectionConfig.sectionDocs,
|
||||
fieldDocs: sectionConfig.fieldDocs,
|
||||
hiddenFields: effectiveHiddenFields,
|
||||
restartRequired: sectionConfig.restartRequired,
|
||||
requiresRestart,
|
||||
isProfile: !!profileName,
|
||||
}}
|
||||
/>
|
||||
<LiveFormDataContext.Provider
|
||||
value={(currentFormData as ConfigSectionData | null) ?? null}
|
||||
>
|
||||
<ConfigForm
|
||||
key={formKey}
|
||||
schema={modifiedSchema}
|
||||
formData={currentFormData}
|
||||
onChange={handleChange}
|
||||
onValidationChange={setHasValidationErrors}
|
||||
fieldOrder={sectionConfig.fieldOrder}
|
||||
fieldGroups={sectionConfig.fieldGroups}
|
||||
hiddenFields={effectiveHiddenFields}
|
||||
advancedFields={sectionConfig.advancedFields}
|
||||
liveValidate={sectionConfig.liveValidate}
|
||||
uiSchema={sectionConfig.uiSchema}
|
||||
disabled={disabled || isSaving}
|
||||
readonly={readonly}
|
||||
showSubmit={false}
|
||||
i18nNamespace={configNamespace}
|
||||
customValidate={customValidate}
|
||||
formContext={{
|
||||
level: effectiveLevel,
|
||||
cameraName,
|
||||
globalValue,
|
||||
cameraValue,
|
||||
hasChanges,
|
||||
extraHasChanges,
|
||||
setExtraHasChanges,
|
||||
overrides: uiOverrides as JsonValue | undefined,
|
||||
formData: currentFormData as ConfigSectionData,
|
||||
baselineFormData: effectiveBaselineFormData as ConfigSectionData,
|
||||
pendingDataBySection,
|
||||
onPendingDataChange,
|
||||
onFormDataChange: (data: ConfigSectionData) => handleChange(data),
|
||||
// For widgets that need access to full camera config (e.g., zone names)
|
||||
fullCameraConfig:
|
||||
effectiveLevel === "camera" && cameraName
|
||||
? config?.cameras?.[cameraName]
|
||||
: undefined,
|
||||
fullConfig: config,
|
||||
// When rendering camera-level sections, provide the section path so
|
||||
// field templates can look up keys under the `config/cameras` namespace
|
||||
// When using a consolidated global namespace, keys are nested
|
||||
// under the section name (e.g., `audio.label`) so provide the
|
||||
// section prefix to templates so they can attempt `${section}.${field}` lookups.
|
||||
sectionI18nPrefix: sectionPath,
|
||||
t,
|
||||
renderers: wrappedRenderers,
|
||||
sectionDocs: sectionConfig.sectionDocs,
|
||||
fieldDocs: sectionConfig.fieldDocs,
|
||||
hiddenFields: effectiveHiddenFields,
|
||||
restartRequired: sectionConfig.restartRequired,
|
||||
requiresRestart,
|
||||
isProfile: !!profileName,
|
||||
}}
|
||||
/>
|
||||
</LiveFormDataContext.Provider>
|
||||
</FieldMessagesContext.Provider>
|
||||
|
||||
{!embedded && (
|
||||
|
||||
@ -31,6 +31,7 @@ import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget";
|
||||
import { CameraPathWidget } from "./widgets/CameraPathWidget";
|
||||
import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget";
|
||||
import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget";
|
||||
import { SemanticSearchModelSizeWidget } from "./widgets/SemanticSearchModelSizeWidget";
|
||||
import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget";
|
||||
|
||||
import { FieldTemplate } from "./templates/FieldTemplate";
|
||||
@ -86,6 +87,7 @@ export const frigateTheme: FrigateTheme = {
|
||||
timezoneSelect: TimezoneSelectWidget,
|
||||
optionalField: OptionalFieldWidget,
|
||||
semanticSearchModel: SemanticSearchModelWidget,
|
||||
semanticSearchModelSize: SemanticSearchModelSizeWidget,
|
||||
onvifProfile: OnvifProfileWidget,
|
||||
},
|
||||
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