2024-09-11 17:41:16 +03:00
|
|
|
import SearchThumbnail from "@/components/card/SearchThumbnail";
|
2024-06-23 23:58:00 +03:00
|
|
|
import SearchFilterGroup from "@/components/filter/SearchFilterGroup";
|
|
|
|
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
2024-09-09 17:33:38 +03:00
|
|
|
import Chip from "@/components/indicators/Chip";
|
2024-08-14 17:02:21 +03:00
|
|
|
import SearchDetailDialog from "@/components/overlay/detail/SearchDetailDialog";
|
2024-06-23 23:58:00 +03:00
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Toaster } from "@/components/ui/sonner";
|
2024-09-09 17:33:38 +03:00
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
} from "@/components/ui/tooltip";
|
2024-07-11 01:01:31 +03:00
|
|
|
import { cn } from "@/lib/utils";
|
2024-09-10 19:23:20 +03:00
|
|
|
import { FrigateConfig } from "@/types/frigateConfig";
|
2024-09-11 22:20:41 +03:00
|
|
|
import { SearchFilter, SearchResult } from "@/types/search";
|
2024-09-09 17:33:38 +03:00
|
|
|
import { useCallback, useMemo, useState } from "react";
|
|
|
|
|
import { isMobileOnly } from "react-device-detect";
|
2024-09-10 19:23:20 +03:00
|
|
|
import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu";
|
|
|
|
|
import useSWR from "swr";
|
2024-09-11 17:41:16 +03:00
|
|
|
import ExploreView from "../explore/ExploreView";
|
2024-06-23 23:58:00 +03:00
|
|
|
|
|
|
|
|
type SearchViewProps = {
|
|
|
|
|
search: string;
|
|
|
|
|
searchTerm: string;
|
|
|
|
|
searchFilter?: SearchFilter;
|
|
|
|
|
searchResults?: SearchResult[];
|
|
|
|
|
isLoading: boolean;
|
|
|
|
|
setSearch: (search: string) => void;
|
|
|
|
|
setSimilaritySearch: (search: SearchResult) => void;
|
|
|
|
|
onUpdateFilter: (filter: SearchFilter) => void;
|
|
|
|
|
onOpenSearch: (item: SearchResult) => void;
|
|
|
|
|
};
|
|
|
|
|
export default function SearchView({
|
|
|
|
|
search,
|
|
|
|
|
searchTerm,
|
|
|
|
|
searchFilter,
|
|
|
|
|
searchResults,
|
|
|
|
|
isLoading,
|
|
|
|
|
setSearch,
|
|
|
|
|
setSimilaritySearch,
|
|
|
|
|
onUpdateFilter,
|
|
|
|
|
}: SearchViewProps) {
|
2024-09-10 19:23:20 +03:00
|
|
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
|
|
|
|
revalidateOnFocus: false,
|
|
|
|
|
});
|
|
|
|
|
|
2024-09-09 17:33:38 +03:00
|
|
|
// remove duplicate event ids
|
|
|
|
|
|
|
|
|
|
const uniqueResults = useMemo(() => {
|
|
|
|
|
return searchResults?.filter(
|
|
|
|
|
(value, index, self) =>
|
|
|
|
|
index === self.findIndex((v) => v.id === value.id),
|
|
|
|
|
);
|
|
|
|
|
}, [searchResults]);
|
|
|
|
|
|
2024-06-23 23:58:00 +03:00
|
|
|
// detail
|
|
|
|
|
|
|
|
|
|
const [searchDetail, setSearchDetail] = useState<SearchResult>();
|
|
|
|
|
|
|
|
|
|
// search interaction
|
|
|
|
|
|
2024-09-11 20:32:45 +03:00
|
|
|
const onSelectSearch = useCallback((item: SearchResult) => {
|
|
|
|
|
setSearchDetail(item);
|
2024-09-11 17:41:16 +03:00
|
|
|
}, []);
|
2024-06-23 23:58:00 +03:00
|
|
|
|
2024-09-09 17:33:38 +03:00
|
|
|
// confidence score - probably needs tweaking
|
|
|
|
|
|
|
|
|
|
const zScoreToConfidence = (score: number, source: string) => {
|
|
|
|
|
let midpoint, scale;
|
|
|
|
|
|
|
|
|
|
if (source === "thumbnail") {
|
|
|
|
|
midpoint = 2;
|
|
|
|
|
scale = 0.5;
|
|
|
|
|
} else {
|
|
|
|
|
midpoint = 0.5;
|
|
|
|
|
scale = 1.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sigmoid function: 1 / (1 + e^x)
|
|
|
|
|
const confidence = 1 / (1 + Math.exp((score - midpoint) * scale));
|
|
|
|
|
|
|
|
|
|
return Math.round(confidence * 100);
|
|
|
|
|
};
|
|
|
|
|
|
2024-09-10 19:23:20 +03:00
|
|
|
const hasExistingSearch = useMemo(
|
|
|
|
|
() => searchResults != undefined || searchFilter != undefined,
|
|
|
|
|
[searchResults, searchFilter],
|
|
|
|
|
);
|
|
|
|
|
|
2024-06-23 23:58:00 +03:00
|
|
|
return (
|
|
|
|
|
<div className="flex size-full flex-col pt-2 md:py-2">
|
|
|
|
|
<Toaster closeButton={true} />
|
|
|
|
|
<SearchDetailDialog
|
|
|
|
|
search={searchDetail}
|
|
|
|
|
setSearch={setSearchDetail}
|
|
|
|
|
setSimilarity={
|
|
|
|
|
searchDetail && (() => setSimilaritySearch(searchDetail))
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
|
2024-09-10 19:23:20 +03:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
2024-09-11 20:32:45 +03:00
|
|
|
"flex flex-col items-start space-y-2 pl-2 pr-2 md:mb-2 md:pl-3 lg:h-10 lg:flex-row lg:items-center lg:space-y-0",
|
2024-09-10 19:23:20 +03:00
|
|
|
config?.semantic_search?.enabled
|
|
|
|
|
? "justify-between"
|
|
|
|
|
: "justify-center",
|
2024-09-11 20:32:45 +03:00
|
|
|
isMobileOnly && "mb-2 h-auto flex-wrap gap-2 space-y-0",
|
2024-09-10 19:23:20 +03:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{config?.semantic_search?.enabled && (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"relative w-full",
|
2024-09-11 20:32:45 +03:00
|
|
|
hasExistingSearch ? "lg:mr-3 lg:w-1/3" : "lg:ml-[25%] lg:w-1/2",
|
2024-09-10 19:23:20 +03:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
className="text-md w-full bg-muted pr-10"
|
2024-09-12 03:53:58 +03:00
|
|
|
placeholder={"Search for a tracked object..."}
|
2024-09-11 22:20:41 +03:00
|
|
|
value={search}
|
2024-09-10 19:23:20 +03:00
|
|
|
onChange={(e) => setSearch(e.target.value)}
|
2024-09-06 22:26:32 +03:00
|
|
|
/>
|
2024-09-10 19:23:20 +03:00
|
|
|
{search && (
|
|
|
|
|
<LuXCircle
|
|
|
|
|
className="absolute right-2 top-1/2 h-5 w-5 -translate-y-1/2 cursor-pointer text-muted-foreground hover:text-primary"
|
|
|
|
|
onClick={() => setSearch("")}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2024-06-23 23:58:00 +03:00
|
|
|
|
2024-09-10 19:23:20 +03:00
|
|
|
{hasExistingSearch && (
|
|
|
|
|
<SearchFilterGroup
|
2024-09-11 20:32:45 +03:00
|
|
|
className={cn(
|
|
|
|
|
"w-full justify-between md:justify-start lg:justify-end",
|
|
|
|
|
)}
|
2024-09-10 19:23:20 +03:00
|
|
|
filter={searchFilter}
|
2024-09-11 22:20:41 +03:00
|
|
|
searchTerm={searchTerm}
|
2024-09-10 19:23:20 +03:00
|
|
|
onUpdateFilter={onUpdateFilter}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2024-06-23 23:58:00 +03:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="no-scrollbar flex flex-1 flex-wrap content-start gap-2 overflow-y-auto md:gap-4">
|
|
|
|
|
{searchTerm.length > 0 && searchResults?.length == 0 && (
|
|
|
|
|
<div className="absolute left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center text-center">
|
|
|
|
|
<LuSearchX className="size-16" />
|
2024-09-12 03:53:58 +03:00
|
|
|
No Tracked Objects Found
|
2024-06-23 23:58:00 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{isLoading && (
|
|
|
|
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
|
|
|
|
)}
|
|
|
|
|
|
2024-09-11 17:41:16 +03:00
|
|
|
{uniqueResults && (
|
|
|
|
|
<div className="mt-2 grid w-full gap-2 px-1 sm:grid-cols-2 md:mx-2 md:grid-cols-4 md:gap-4 3xl:grid-cols-6">
|
|
|
|
|
{uniqueResults &&
|
|
|
|
|
uniqueResults.map((value) => {
|
|
|
|
|
const selected = false;
|
|
|
|
|
|
|
|
|
|
return (
|
2024-07-11 01:01:31 +03:00
|
|
|
<div
|
2024-09-11 17:41:16 +03:00
|
|
|
key={value.id}
|
|
|
|
|
data-start={value.start_time}
|
|
|
|
|
className="review-item relative rounded-lg"
|
2024-07-11 01:01:31 +03:00
|
|
|
>
|
2024-09-11 17:41:16 +03:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"aspect-square size-full overflow-hidden rounded-lg",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<SearchThumbnail
|
|
|
|
|
searchResult={value}
|
|
|
|
|
findSimilar={() => setSimilaritySearch(value)}
|
2024-09-11 20:32:45 +03:00
|
|
|
onClick={() => onSelectSearch(value)}
|
2024-09-11 17:41:16 +03:00
|
|
|
/>
|
2024-09-11 22:20:41 +03:00
|
|
|
{searchTerm && (
|
2024-09-11 17:41:16 +03:00
|
|
|
<div className={cn("absolute right-2 top-2 z-40")}>
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger>
|
|
|
|
|
<Chip
|
|
|
|
|
className={`flex select-none items-center justify-between space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize text-white`}
|
|
|
|
|
>
|
|
|
|
|
{value.search_source == "thumbnail" ? (
|
|
|
|
|
<LuImage className="mr-1 size-3" />
|
|
|
|
|
) : (
|
|
|
|
|
<LuText className="mr-1 size-3" />
|
|
|
|
|
)}
|
|
|
|
|
{zScoreToConfidence(
|
|
|
|
|
value.search_distance,
|
|
|
|
|
value.search_source,
|
|
|
|
|
)}
|
|
|
|
|
%
|
|
|
|
|
</Chip>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>
|
|
|
|
|
Matched {value.search_source} at{" "}
|
2024-09-10 19:23:20 +03:00
|
|
|
{zScoreToConfidence(
|
|
|
|
|
value.search_distance,
|
|
|
|
|
value.search_source,
|
|
|
|
|
)}
|
|
|
|
|
%
|
2024-09-11 17:41:16 +03:00
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
className={`review-item-ring pointer-events-none absolute inset-0 z-10 size-full rounded-lg outline outline-[3px] -outline-offset-[2.8px] ${selected ? `shadow-severity_alert outline-severity_alert` : "outline-transparent duration-500"}`}
|
|
|
|
|
/>
|
2024-06-23 23:58:00 +03:00
|
|
|
</div>
|
2024-09-11 17:41:16 +03:00
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{!uniqueResults && !isLoading && (
|
|
|
|
|
<div className="flex size-full flex-col">
|
|
|
|
|
<ExploreView onSelectSearch={onSelectSearch} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2024-06-23 23:58:00 +03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|