Add score filter to Explore view (#14397)

* backend score filtering and sorting

* score filter frontend

* use input for score filtering

* use correct score on search thumbnail

* add popover to explain top_score

* revert sublabel score calc

* update filters logic

* fix rounding on score

* wait until default view is loaded

* don't turn button to selected style for similarity searches

* clarify language

* fix alert dialog buttons to use correct destructive variant

* use root level top_score for very old events

* better arrangement of thumbnail footer items on smaller screens
This commit is contained in:
Josh Hawkins
2024-10-17 06:30:52 -05:00
committed by GitHub
parent edaccd86d6
commit 8173cd7776
16 changed files with 353 additions and 136 deletions

View File

@@ -62,6 +62,12 @@ import { Card, CardContent } from "@/components/ui/card";
import useImageLoaded from "@/hooks/use-image-loaded";
import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator";
import { GenericVideoPlayer } from "@/components/player/GenericVideoPlayer";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { LuInfo } from "react-icons/lu";
const SEARCH_TABS = [
"details",
@@ -279,7 +285,7 @@ function ObjectDetailsTab({
return 0;
}
const value = search.score ?? search.data.top_score;
const value = search.data.top_score;
return Math.round(value * 100);
}, [search]);
@@ -369,7 +375,24 @@ function ObjectDetailsTab({
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="text-sm text-primary/40">Score</div>
<div className="text-sm text-primary/40">
<div className="flex flex-row items-center gap-1">
Top Score
<Popover>
<PopoverTrigger asChild>
<div className="cursor-pointer p-0">
<LuInfo className="size-4" />
<span className="sr-only">Info</span>
</div>
</PopoverTrigger>
<PopoverContent className="w-80">
The top score is the highest median score for the tracked
object, so this may differ from the score shown on the
search result thumbnail.
</PopoverContent>
</Popover>
</div>
</div>
<div className="text-sm">
{score}%{subLabelScore && ` (${subLabelScore}%)`}
</div>

View File

@@ -23,6 +23,8 @@ import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { DropdownMenuSeparator } from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { DualThumbSlider } from "@/components/ui/slider";
import { Input } from "@/components/ui/input";
type SearchFilterDialogProps = {
config?: FrigateConfig;
@@ -46,6 +48,12 @@ export default function SearchFilterDialog({
const [currentFilter, setCurrentFilter] = useState(filter ?? {});
const { data: allSubLabels } = useSWR(["sub_labels", { split_joined: 1 }]);
useEffect(() => {
if (filter) {
setCurrentFilter(filter);
}
}, [filter]);
// state
const [open, setOpen] = useState(false);
@@ -54,9 +62,12 @@ export default function SearchFilterDialog({
() =>
currentFilter &&
(currentFilter.time_range ||
(currentFilter.min_score ?? 0) > 0.5 ||
(currentFilter.max_score ?? 1) < 1 ||
(currentFilter.zones?.length ?? 0) > 0 ||
(currentFilter.sub_labels?.length ?? 0) > 0 ||
(currentFilter.search_type?.length ?? 2) !== 2),
(!currentFilter.search_type?.includes("similarity") &&
(currentFilter.search_type?.length ?? 2) !== 2)),
[currentFilter],
);
@@ -97,6 +108,13 @@ export default function SearchFilterDialog({
setCurrentFilter({ ...currentFilter, sub_labels: newSubLabels })
}
/>
<ScoreFilterContent
minScore={currentFilter.min_score}
maxScore={currentFilter.max_score}
setScoreRange={(min, max) =>
setCurrentFilter({ ...currentFilter, min_score: min, max_score: max })
}
/>
{config?.semantic_search?.enabled &&
!currentFilter?.search_type?.includes("similarity") && (
<SearchTypeContent
@@ -133,6 +151,8 @@ export default function SearchFilterDialog({
zones: undefined,
sub_labels: undefined,
search_type: ["thumbnail", "description"],
min_score: undefined,
max_score: undefined,
}));
}}
>
@@ -420,6 +440,58 @@ export function SubFilterContent({
);
}
type ScoreFilterContentProps = {
minScore: number | undefined;
maxScore: number | undefined;
setScoreRange: (min: number | undefined, max: number | undefined) => void;
};
export function ScoreFilterContent({
minScore,
maxScore,
setScoreRange,
}: ScoreFilterContentProps) {
return (
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
<div className="mb-3 text-lg">Score</div>
<div className="flex items-center gap-1">
<Input
className="w-12"
inputMode="numeric"
value={Math.round((minScore ?? 0.5) * 100)}
onChange={(e) => {
const value = e.target.value;
if (value) {
setScoreRange(parseInt(value) / 100.0, maxScore ?? 1.0);
}
}}
/>
<DualThumbSlider
className="w-full"
min={0.5}
max={1.0}
step={0.01}
value={[minScore ?? 0.5, maxScore ?? 1.0]}
onValueChange={([min, max]) => setScoreRange(min, max)}
/>
<Input
className="w-12"
inputMode="numeric"
value={Math.round((maxScore ?? 1.0) * 100)}
onChange={(e) => {
const value = e.target.value;
if (value) {
setScoreRange(minScore ?? 0.5, parseInt(value) / 100.0);
}
}}
/>
</div>
</div>
);
}
type SearchTypeContentProps = {
searchSources: SearchSource[] | undefined;
setSearchSources: (sources: SearchSource[] | undefined) => void;