- {page == "general" &&
}
+ {page == "UI settings" &&
}
+ {page == "search settings" && (
+
+ )}
{page == "debug" && (
)}
diff --git a/web/src/types/frigateConfig.ts b/web/src/types/frigateConfig.ts
index 2c54b289e..76d9cfa67 100644
--- a/web/src/types/frigateConfig.ts
+++ b/web/src/types/frigateConfig.ts
@@ -27,6 +27,8 @@ export const ATTRIBUTE_LABELS = [
"ups",
];
+export type SearchModelSize = "small" | "large";
+
export interface CameraConfig {
audio: {
enabled: boolean;
@@ -418,7 +420,8 @@ export interface FrigateConfig {
semantic_search: {
enabled: boolean;
- model_size: string;
+ reindex: boolean;
+ model_size: SearchModelSize;
};
snapshots: {
diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx
index bc4f5b54d..9427cdcff 100644
--- a/web/src/views/search/SearchView.tsx
+++ b/web/src/views/search/SearchView.tsx
@@ -73,13 +73,17 @@ export default function SearchView({
const [columnCount, setColumnCount] = usePersistence("exploreGridColumns", 4);
const effectiveColumnCount = useMemo(() => columnCount ?? 4, [columnCount]);
- const gridClassName = cn("grid w-full gap-2 px-1 gap-2 lg:gap-4 md:mx-2", {
- "sm:grid-cols-2": effectiveColumnCount <= 2,
- "sm:grid-cols-3": effectiveColumnCount === 3,
- "sm:grid-cols-4": effectiveColumnCount === 4,
- "sm:grid-cols-5": effectiveColumnCount === 5,
- "sm:grid-cols-6": effectiveColumnCount === 6,
- });
+ const gridClassName = cn(
+ "grid w-full gap-2 px-1 gap-2 lg:gap-4 md:mx-2",
+ isMobileOnly && "grid-cols-2",
+ {
+ "sm:grid-cols-2": effectiveColumnCount <= 2,
+ "sm:grid-cols-3": effectiveColumnCount === 3,
+ "sm:grid-cols-4": effectiveColumnCount === 4,
+ "sm:grid-cols-5": effectiveColumnCount === 5,
+ "sm:grid-cols-6": effectiveColumnCount === 6,
+ },
+ );
// suggestions values
diff --git a/web/src/views/settings/SearchSettingsView.tsx b/web/src/views/settings/SearchSettingsView.tsx
new file mode 100644
index 000000000..a08816675
--- /dev/null
+++ b/web/src/views/settings/SearchSettingsView.tsx
@@ -0,0 +1,288 @@
+import Heading from "@/components/ui/heading";
+import { FrigateConfig, SearchModelSize } from "@/types/frigateConfig";
+import useSWR from "swr";
+import axios from "axios";
+import ActivityIndicator from "@/components/indicators/activity-indicator";
+import { useCallback, useContext, useEffect, useState } from "react";
+import { Label } from "@/components/ui/label";
+import { Button } from "@/components/ui/button";
+import { Switch } from "@/components/ui/switch";
+import { Toaster } from "@/components/ui/sonner";
+import { toast } from "sonner";
+import { Separator } from "@/components/ui/separator";
+import { Link } from "react-router-dom";
+import { LuExternalLink } from "react-icons/lu";
+import { StatusBarMessagesContext } from "@/context/statusbar-provider";
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+} from "@/components/ui/select";
+
+type SearchSettingsViewProps = {
+ setUnsavedChanges: React.Dispatch
>;
+};
+
+type SearchSettings = {
+ enabled?: boolean;
+ reindex?: boolean;
+ model_size?: SearchModelSize;
+};
+
+export default function SearchSettingsView({
+ setUnsavedChanges,
+}: SearchSettingsViewProps) {
+ const { data: config, mutate: updateConfig } =
+ useSWR("config");
+ const [changedValue, setChangedValue] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
+
+ const [searchSettings, setSearchSettings] = useState({
+ enabled: undefined,
+ reindex: undefined,
+ model_size: undefined,
+ });
+
+ const [origSearchSettings, setOrigSearchSettings] = useState({
+ enabled: undefined,
+ reindex: undefined,
+ model_size: undefined,
+ });
+
+ useEffect(() => {
+ if (config) {
+ if (searchSettings?.enabled == undefined) {
+ setSearchSettings({
+ enabled: config.semantic_search.enabled,
+ reindex: config.semantic_search.reindex,
+ model_size: config.semantic_search.model_size,
+ });
+ }
+
+ setOrigSearchSettings({
+ enabled: config.semantic_search.enabled,
+ reindex: config.semantic_search.reindex,
+ model_size: config.semantic_search.model_size,
+ });
+ }
+ // we know that these deps are correct
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [config]);
+
+ const handleSearchConfigChange = (newConfig: Partial) => {
+ setSearchSettings((prevConfig) => ({ ...prevConfig, ...newConfig }));
+ setUnsavedChanges(true);
+ setChangedValue(true);
+ };
+
+ const saveToConfig = useCallback(async () => {
+ setIsLoading(true);
+
+ axios
+ .put(
+ `config/set?semantic_search.enabled=${searchSettings.enabled}&semantic_search.reindex=${searchSettings.reindex}&semantic_search.model_size=${searchSettings.model_size}`,
+ )
+ .then((res) => {
+ if (res.status === 200) {
+ toast.success("Search settings have been saved.", {
+ position: "top-center",
+ });
+ setChangedValue(false);
+ updateConfig();
+ } else {
+ toast.error(`Failed to save config changes: ${res.statusText}`, {
+ position: "top-center",
+ });
+ }
+ })
+ .catch((error) => {
+ toast.error(
+ `Failed to save config changes: ${error.response.data.message}`,
+ { position: "top-center" },
+ );
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ }, [
+ updateConfig,
+ searchSettings.enabled,
+ searchSettings.reindex,
+ searchSettings.model_size,
+ ]);
+
+ const onCancel = useCallback(() => {
+ setSearchSettings(origSearchSettings);
+ setChangedValue(false);
+ removeMessage("search_settings", "search_settings");
+ }, [origSearchSettings, removeMessage]);
+
+ useEffect(() => {
+ if (changedValue) {
+ addMessage(
+ "search_settings",
+ `Unsaved search settings changes`,
+ undefined,
+ "search_settings",
+ );
+ } else {
+ removeMessage("search_settings", "search_settings");
+ }
+ // we know that these deps are correct
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [changedValue]);
+
+ useEffect(() => {
+ document.title = "Search Settings - Frigate";
+ }, []);
+
+ if (!config) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ Search Settings
+
+
+
+ Semantic Search
+
+
+
+
+ Semantic Search in Frigate allows you to find tracked objects
+ within your review items using either the image itself, a
+ user-defined text description, or an automatically generated one.
+
+
+
+
+ Read the Documentation
+
+
+
+
+
+
+
+
+
{
+ handleSearchConfigChange({ enabled: isChecked });
+ }}
+ />
+
+ Enabled
+
+
+
+
+
{
+ handleSearchConfigChange({ reindex: isChecked });
+ }}
+ />
+
+ Re-Index On Startup
+
+
+
+ Re-indexing will reprocess all thumbnails and descriptions (if
+ enabled) and apply the embeddings on each startup.{" "}
+ Don't forget to disable the option after restarting!
+
+
+
+
+
Model Size
+
+
+ The size of the model used for semantic search embeddings.
+
+
+
+ Using small employs a quantized version of the
+ model that uses less RAM and runs faster on CPU with a very
+ negligible difference in embedding quality.
+
+
+ Using large employs the full Jina model and will
+ automatically run on the GPU if applicable.
+
+
+
+
+
+ handleSearchConfigChange({
+ model_size: value as SearchModelSize,
+ })
+ }
+ >
+
+ {searchSettings.model_size}
+
+
+
+ {["small", "large"].map((size) => (
+
+ {size}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ Reset
+
+
+ {isLoading ? (
+
+ ) : (
+ "Save"
+ )}
+
+
+
+
+ );
+}
diff --git a/web/src/views/settings/GeneralSettingsView.tsx b/web/src/views/settings/UiSettingsView.tsx
similarity index 99%
rename from web/src/views/settings/GeneralSettingsView.tsx
rename to web/src/views/settings/UiSettingsView.tsx
index 0cb7689f6..c212073c1 100644
--- a/web/src/views/settings/GeneralSettingsView.tsx
+++ b/web/src/views/settings/UiSettingsView.tsx
@@ -22,7 +22,7 @@ import {
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
const WEEK_STARTS_ON = ["Sunday", "Monday"];
-export default function GeneralSettingsView() {
+export default function UiSettingsView() {
const { data: config } = useSWR("config");
const clearStoredLayouts = useCallback(() => {