Add search settings and rename general to ui settings

This commit is contained in:
Nicolas Mowen 2024-10-15 14:52:06 -06:00
parent e746956bab
commit 4716ab202f
4 changed files with 299 additions and 7 deletions

View File

@ -29,16 +29,18 @@ import { ZoneMaskFilterButton } from "@/components/filter/ZoneMaskFilter";
import { PolygonType } from "@/types/canvas"; import { PolygonType } from "@/types/canvas";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import scrollIntoView from "scroll-into-view-if-needed"; import scrollIntoView from "scroll-into-view-if-needed";
import GeneralSettingsView from "@/views/settings/GeneralSettingsView";
import CameraSettingsView from "@/views/settings/CameraSettingsView"; import CameraSettingsView from "@/views/settings/CameraSettingsView";
import ObjectSettingsView from "@/views/settings/ObjectSettingsView"; import ObjectSettingsView from "@/views/settings/ObjectSettingsView";
import MotionTunerView from "@/views/settings/MotionTunerView"; import MotionTunerView from "@/views/settings/MotionTunerView";
import MasksAndZonesView from "@/views/settings/MasksAndZonesView"; import MasksAndZonesView from "@/views/settings/MasksAndZonesView";
import AuthenticationView from "@/views/settings/AuthenticationView"; import AuthenticationView from "@/views/settings/AuthenticationView";
import NotificationView from "@/views/settings/NotificationsSettingsView"; import NotificationView from "@/views/settings/NotificationsSettingsView";
import SearchSettingsView from "@/views/settings/SearchSettingsView";
import UiSettingsView from "@/views/settings/UiSettingsView";
const allSettingsViews = [ const allSettingsViews = [
"general", "UI settings",
"search settings",
"camera settings", "camera settings",
"masks / zones", "masks / zones",
"motion tuner", "motion tuner",
@ -49,7 +51,7 @@ const allSettingsViews = [
type SettingsType = (typeof allSettingsViews)[number]; type SettingsType = (typeof allSettingsViews)[number];
export default function Settings() { export default function Settings() {
const [page, setPage] = useState<SettingsType>("general"); const [page, setPage] = useState<SettingsType>("UI settings");
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
const tabsRef = useRef<HTMLDivElement | null>(null); const tabsRef = useRef<HTMLDivElement | null>(null);
@ -140,7 +142,7 @@ export default function Settings() {
{Object.values(settingsViews).map((item) => ( {Object.values(settingsViews).map((item) => (
<ToggleGroupItem <ToggleGroupItem
key={item} key={item}
className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "general" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`} className={`flex scroll-mx-10 items-center justify-between gap-2 ${page == "UI settings" ? "last:mr-20" : ""} ${pageToggle == item ? "" : "*:text-muted-foreground"}`}
value={item} value={item}
data-nav-item={item} data-nav-item={item}
aria-label={`Select ${item}`} aria-label={`Select ${item}`}
@ -172,7 +174,10 @@ export default function Settings() {
)} )}
</div> </div>
<div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24"> <div className="mt-2 flex h-full w-full flex-col items-start md:h-dvh md:pb-24">
{page == "general" && <GeneralSettingsView />} {page == "UI settings" && <UiSettingsView />}
{page == "search settings" && (
<SearchSettingsView setUnsavedChanges={setUnsavedChanges} />
)}
{page == "debug" && ( {page == "debug" && (
<ObjectSettingsView selectedCamera={selectedCamera} /> <ObjectSettingsView selectedCamera={selectedCamera} />
)} )}

View File

@ -27,6 +27,8 @@ export const ATTRIBUTE_LABELS = [
"ups", "ups",
]; ];
export type SearchModelSize = "small" | "large";
export interface CameraConfig { export interface CameraConfig {
audio: { audio: {
enabled: boolean; enabled: boolean;
@ -418,7 +420,8 @@ export interface FrigateConfig {
semantic_search: { semantic_search: {
enabled: boolean; enabled: boolean;
model_size: string; reindex: boolean;
model_size: SearchModelSize;
}; };
snapshots: { snapshots: {

View File

@ -0,0 +1,284 @@
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<React.SetStateAction<boolean>>;
};
type SearchSettings = {
enabled?: boolean;
reindex?: boolean;
model_size?: SearchModelSize;
};
export default function SearchSettingsView({
setUnsavedChanges,
}: SearchSettingsViewProps) {
const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config");
const [changedValue, setChangedValue] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!;
const [searchSettings, setSearchSettings] = useState<SearchSettings>({
enabled: undefined,
reindex: undefined,
model_size: undefined,
});
const [origSearchSettings, setOrigSearchSettings] = useState<SearchSettings>({
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<SearchSettings>) => {
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 <ActivityIndicator />;
}
return (
<div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto rounded-lg border-[1px] border-secondary-foreground bg-background_alt p-2 md:order-none md:mb-0 md:mr-2 md:mt-0">
<Heading as="h3" className="my-2">
Search Settings
</Heading>
<div className="my-3 space-y-3 text-sm text-muted-foreground">
<p>
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. This feature works
by creating embeddings numerical vector representations for both
the images and text descriptions of your tracked objects. By
comparing these embeddings, Frigate assesses their similarities to
deliver relevant search results.
</p>
<div className="flex items-center text-primary">
<Link
to="https://docs.frigate.video/configuration/semantic_search"
target="_blank"
rel="noopener noreferrer"
className="inline"
>
Read the semantic search docs
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
</div>
<Separator className="my-2 flex bg-secondary" />
<div className="flex w-full flex-col space-y-6">
<div className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="improve-contrast">Enabled</Label>
</div>
<Switch
id="enabled"
className="ml-3"
disabled={searchSettings.enabled === undefined}
checked={searchSettings.enabled === true}
onCheckedChange={(isChecked) => {
handleSearchConfigChange({ enabled: isChecked });
}}
/>
</div>
<div className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="improve-contrast">Re-Index On Startup</Label>
<div className="text-sm text-muted-foreground">
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!
</div>
</div>
<Switch
id="enabled"
className="ml-3"
disabled={searchSettings.reindex === undefined}
checked={searchSettings.reindex === true}
onCheckedChange={(isChecked) => {
handleSearchConfigChange({ reindex: isChecked });
}}
/>
</div>
<Separator className="my-2 flex bg-secondary" />
<div className="mt-2 space-y-3">
<div className="space-y-0.5">
<div className="text-md">Model Size</div>
<div className="my-2 space-y-1 text-sm text-muted-foreground">
<p>
Configure the size of the model used for semantic search
embeddings:
</p>
<p>
Configuring the <em>small</em> model employs a quantized
version of the model that uses much less RAM and runs faster
on CPU with a very negligible difference in embedding quality.
</p>
<p>
Configuring the <em>large</em> model employs the full Jina
model and will automatically run on the GPU if applicable.
</p>
</div>
</div>
<Select
value={searchSettings.model_size}
onValueChange={(value) =>
handleSearchConfigChange({
model_size: value as SearchModelSize,
})
}
>
<SelectTrigger className="w-20">
{searchSettings.model_size}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{["small", "large"].map((size) => (
<SelectItem
key={size}
className="cursor-pointer"
value={size}
>
{size}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-1 flex-col justify-end">
<div className="flex flex-row gap-2 pt-5">
<Button className="flex flex-1" onClick={onCancel}>
Reset
</Button>
<Button
variant="select"
disabled={!changedValue || isLoading}
className="flex flex-1"
onClick={saveToConfig}
>
{isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>Saving...</span>
</div>
) : (
"Save"
)}
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@ -22,7 +22,7 @@ import {
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16]; const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
const WEEK_STARTS_ON = ["Sunday", "Monday"]; const WEEK_STARTS_ON = ["Sunday", "Monday"];
export default function GeneralSettingsView() { export default function UiSettingsView() {
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const clearStoredLayouts = useCallback(() => { const clearStoredLayouts = useCallback(() => {