forked from Github/frigate
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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user