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 { 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-26 23:30:56 +03:00
|
|
|
import { SearchFilter, SearchResult, SearchSource } from "@/types/search";
|
2024-09-13 06:07:35 +03:00
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
2024-10-02 15:59:53 +03:00
|
|
|
import { isDesktop, isMobileOnly } from "react-device-detect";
|
|
|
|
|
import { LuColumns, LuImage, LuSearchX, LuText } from "react-icons/lu";
|
2024-09-10 19:23:20 +03:00
|
|
|
import useSWR from "swr";
|
2024-09-11 17:41:16 +03:00
|
|
|
import ExploreView from "../explore/ExploreView";
|
2024-09-13 06:07:35 +03:00
|
|
|
import useKeyboardListener, {
|
|
|
|
|
KeyModifiers,
|
|
|
|
|
} from "@/hooks/use-keyboard-listener";
|
|
|
|
|
import scrollIntoView from "scroll-into-view-if-needed";
|
2024-09-18 21:18:16 +03:00
|
|
|
import InputWithTags from "@/components/input/InputWithTags";
|
2024-09-23 15:39:20 +03:00
|
|
|
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
2024-09-24 17:14:51 +03:00
|
|
|
import { isEqual } from "lodash";
|
2024-09-25 19:05:40 +03:00
|
|
|
import { formatDateToLocaleString } from "@/utils/dateUtil";
|
2024-09-26 23:30:56 +03:00
|
|
|
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
2024-10-02 15:59:53 +03:00
|
|
|
import { Slider } from "@/components/ui/slider";
|
|
|
|
|
import {
|
|
|
|
|
Popover,
|
|
|
|
|
PopoverContent,
|
|
|
|
|
PopoverTrigger,
|
|
|
|
|
} from "@/components/ui/popover";
|
|
|
|
|
import { usePersistence } from "@/hooks/use-persistence";
|
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;
|
2024-09-18 21:18:16 +03:00
|
|
|
setSearchFilter: (filter: SearchFilter) => void;
|
2024-06-23 23:58:00 +03:00
|
|
|
onUpdateFilter: (filter: SearchFilter) => void;
|
2024-09-14 16:42:56 +03:00
|
|
|
loadMore: () => void;
|
|
|
|
|
hasMore: boolean;
|
2024-06-23 23:58:00 +03:00
|
|
|
};
|
|
|
|
|
export default function SearchView({
|
|
|
|
|
search,
|
|
|
|
|
searchTerm,
|
|
|
|
|
searchFilter,
|
|
|
|
|
searchResults,
|
|
|
|
|
isLoading,
|
|
|
|
|
setSearch,
|
|
|
|
|
setSimilaritySearch,
|
2024-09-18 21:18:16 +03:00
|
|
|
setSearchFilter,
|
2024-06-23 23:58:00 +03:00
|
|
|
onUpdateFilter,
|
2024-09-14 16:42:56 +03:00
|
|
|
loadMore,
|
|
|
|
|
hasMore,
|
2024-06-23 23:58:00 +03:00
|
|
|
}: SearchViewProps) {
|
2024-10-02 17:32:12 +03:00
|
|
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
2024-09-10 19:23:20 +03:00
|
|
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
|
|
|
|
revalidateOnFocus: false,
|
|
|
|
|
});
|
|
|
|
|
|
2024-10-02 15:59:53 +03:00
|
|
|
// grid
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
"sm:grid-cols-7": effectiveColumnCount === 7,
|
|
|
|
|
"sm:grid-cols-8": effectiveColumnCount >= 8,
|
|
|
|
|
});
|
|
|
|
|
|
2024-09-18 21:18:16 +03:00
|
|
|
// suggestions values
|
|
|
|
|
|
|
|
|
|
const allLabels = useMemo<string[]>(() => {
|
|
|
|
|
if (!config) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const labels = new Set<string>();
|
|
|
|
|
const cameras = searchFilter?.cameras || Object.keys(config.cameras);
|
|
|
|
|
|
|
|
|
|
cameras.forEach((camera) => {
|
|
|
|
|
if (camera == "birdseye") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const cameraConfig = config.cameras[camera];
|
|
|
|
|
cameraConfig.objects.track.forEach((label) => {
|
|
|
|
|
labels.add(label);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (cameraConfig.audio.enabled_in_config) {
|
|
|
|
|
cameraConfig.audio.listen.forEach((label) => {
|
|
|
|
|
labels.add(label);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return [...labels].sort();
|
|
|
|
|
}, [config, searchFilter]);
|
|
|
|
|
|
|
|
|
|
const { data: allSubLabels } = useSWR("sub_labels");
|
|
|
|
|
|
|
|
|
|
const allZones = useMemo<string[]>(() => {
|
|
|
|
|
if (!config) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const zones = new Set<string>();
|
|
|
|
|
const cameras = searchFilter?.cameras || Object.keys(config.cameras);
|
|
|
|
|
|
|
|
|
|
cameras.forEach((camera) => {
|
|
|
|
|
if (camera == "birdseye") {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const cameraConfig = config.cameras[camera];
|
|
|
|
|
Object.entries(cameraConfig.zones).map(([name, _]) => {
|
|
|
|
|
zones.add(name);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return [...zones].sort();
|
|
|
|
|
}, [config, searchFilter]);
|
|
|
|
|
|
|
|
|
|
const suggestionsValues = useMemo(
|
|
|
|
|
() => ({
|
|
|
|
|
cameras: Object.keys(config?.cameras || {}),
|
|
|
|
|
labels: Object.values(allLabels || {}),
|
|
|
|
|
zones: Object.values(allZones || {}),
|
|
|
|
|
sub_labels: allSubLabels,
|
|
|
|
|
search_type: ["thumbnail", "description"] as SearchSource[],
|
2024-09-25 20:15:08 +03:00
|
|
|
time_range:
|
|
|
|
|
config?.ui.time_format == "24hour"
|
|
|
|
|
? ["00:00-23:59"]
|
|
|
|
|
: ["12:00AM-11:59PM"],
|
2024-09-25 19:05:40 +03:00
|
|
|
before: [formatDateToLocaleString()],
|
|
|
|
|
after: [formatDateToLocaleString(-5)],
|
2024-09-18 21:18:16 +03:00
|
|
|
}),
|
|
|
|
|
[config, allLabels, allZones, allSubLabels],
|
|
|
|
|
);
|
|
|
|
|
|
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-13 06:07:35 +03:00
|
|
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
|
|
|
|
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
|
|
|
|
|
|
|
|
const onSelectSearch = useCallback((item: SearchResult, index: number) => {
|
2024-09-11 20:32:45 +03:00
|
|
|
setSearchDetail(item);
|
2024-09-13 06:07:35 +03:00
|
|
|
setSelectedIndex(index);
|
2024-09-11 17:41:16 +03:00
|
|
|
}, []);
|
2024-06-23 23:58:00 +03:00
|
|
|
|
2024-09-24 17:14:51 +03:00
|
|
|
// update search detail when results change
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (searchDetail && searchResults) {
|
|
|
|
|
const flattenedResults = searchResults.flat();
|
|
|
|
|
const updatedSearchDetail = flattenedResults.find(
|
|
|
|
|
(result) => result.id === searchDetail.id,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (updatedSearchDetail && !isEqual(updatedSearchDetail, searchDetail)) {
|
|
|
|
|
setSearchDetail(updatedSearchDetail);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [searchResults, searchDetail]);
|
|
|
|
|
|
2024-10-11 21:11:11 +03:00
|
|
|
// confidence score
|
2024-09-09 17:33:38 +03:00
|
|
|
|
2024-10-07 23:30:45 +03:00
|
|
|
const zScoreToConfidence = (score: number) => {
|
2024-10-11 21:11:11 +03:00
|
|
|
// Normalizing is not needed for similarity searches
|
|
|
|
|
// Sigmoid function for normalized: 1 / (1 + e^x)
|
|
|
|
|
// Cosine for similarity
|
|
|
|
|
if (searchFilter) {
|
|
|
|
|
const notNormalized = searchFilter?.search_type?.includes("similarity");
|
2024-09-09 17:33:38 +03:00
|
|
|
|
2024-10-11 21:11:11 +03:00
|
|
|
const confidence = notNormalized ? 1 - score : 1 / (1 + Math.exp(score));
|
|
|
|
|
|
|
|
|
|
return Math.round(confidence * 100);
|
|
|
|
|
}
|
2024-09-09 17:33:38 +03:00
|
|
|
};
|
|
|
|
|
|
2024-09-10 19:23:20 +03:00
|
|
|
const hasExistingSearch = useMemo(
|
|
|
|
|
() => searchResults != undefined || searchFilter != undefined,
|
|
|
|
|
[searchResults, searchFilter],
|
|
|
|
|
);
|
|
|
|
|
|
2024-09-13 06:07:35 +03:00
|
|
|
// keyboard listener
|
|
|
|
|
|
2024-09-25 21:45:42 +03:00
|
|
|
const [inputFocused, setInputFocused] = useState(false);
|
|
|
|
|
|
2024-09-13 06:07:35 +03:00
|
|
|
const onKeyboardShortcut = useCallback(
|
|
|
|
|
(key: string | null, modifiers: KeyModifiers) => {
|
2024-09-25 21:45:42 +03:00
|
|
|
if (!modifiers.down || !uniqueResults || inputFocused) {
|
2024-09-13 06:07:35 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (key) {
|
|
|
|
|
case "ArrowLeft":
|
|
|
|
|
setSelectedIndex((prevIndex) => {
|
|
|
|
|
const newIndex =
|
|
|
|
|
prevIndex === null
|
|
|
|
|
? uniqueResults.length - 1
|
|
|
|
|
: (prevIndex - 1 + uniqueResults.length) % uniqueResults.length;
|
|
|
|
|
setSearchDetail(uniqueResults[newIndex]);
|
|
|
|
|
return newIndex;
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case "ArrowRight":
|
|
|
|
|
setSelectedIndex((prevIndex) => {
|
|
|
|
|
const newIndex =
|
|
|
|
|
prevIndex === null ? 0 : (prevIndex + 1) % uniqueResults.length;
|
|
|
|
|
setSearchDetail(uniqueResults[newIndex]);
|
|
|
|
|
return newIndex;
|
|
|
|
|
});
|
|
|
|
|
break;
|
2024-10-02 17:32:12 +03:00
|
|
|
case "PageDown":
|
|
|
|
|
contentRef.current?.scrollBy({
|
|
|
|
|
top: contentRef.current.clientHeight / 2,
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case "PageUp":
|
|
|
|
|
contentRef.current?.scrollBy({
|
|
|
|
|
top: -contentRef.current.clientHeight / 2,
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
});
|
|
|
|
|
break;
|
2024-09-13 06:07:35 +03:00
|
|
|
}
|
|
|
|
|
},
|
2024-09-25 21:45:42 +03:00
|
|
|
[uniqueResults, inputFocused],
|
2024-09-13 06:07:35 +03:00
|
|
|
);
|
|
|
|
|
|
2024-09-25 21:45:42 +03:00
|
|
|
useKeyboardListener(
|
2024-10-02 17:32:12 +03:00
|
|
|
["ArrowLeft", "ArrowRight", "PageDown", "PageUp"],
|
2024-09-25 21:45:42 +03:00
|
|
|
onKeyboardShortcut,
|
|
|
|
|
!inputFocused,
|
|
|
|
|
);
|
2024-09-13 06:07:35 +03:00
|
|
|
|
|
|
|
|
// scroll into view
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (
|
|
|
|
|
selectedIndex !== null &&
|
|
|
|
|
uniqueResults &&
|
|
|
|
|
itemRefs.current?.[selectedIndex]
|
|
|
|
|
) {
|
|
|
|
|
scrollIntoView(itemRefs.current[selectedIndex], {
|
|
|
|
|
block: "center",
|
|
|
|
|
behavior: "smooth",
|
|
|
|
|
scrollMode: "if-needed",
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-09-14 16:42:56 +03:00
|
|
|
// we only want to scroll when the index changes
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [selectedIndex]);
|
|
|
|
|
|
|
|
|
|
// observer for loading more
|
|
|
|
|
|
|
|
|
|
const observerTarget = useRef<HTMLDivElement>(null);
|
|
|
|
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const observer = new IntersectionObserver(
|
|
|
|
|
(entries) => {
|
|
|
|
|
if (entries[0].isIntersecting && hasMore && !isLoading) {
|
|
|
|
|
loadMore();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ threshold: 1.0 },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (observerTarget.current) {
|
|
|
|
|
observer.observe(observerTarget.current);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
observerRef.current = observer;
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (observerRef.current) {
|
|
|
|
|
observerRef.current.disconnect();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}, [hasMore, isLoading, loadMore]);
|
2024-09-13 06:07:35 +03:00
|
|
|
|
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-18 21:18:16 +03:00
|
|
|
"flex flex-col items-start space-y-2 pl-2 pr-2 md:mb-2 md:pl-3 lg:relative 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 && (
|
2024-09-18 21:18:16 +03:00
|
|
|
<div className={cn("z-[41] w-full lg:absolute lg:top-0 lg:w-1/3")}>
|
|
|
|
|
<InputWithTags
|
2024-09-25 21:45:42 +03:00
|
|
|
inputFocused={inputFocused}
|
|
|
|
|
setInputFocused={setInputFocused}
|
2024-09-18 21:18:16 +03:00
|
|
|
filters={searchFilter ?? {}}
|
|
|
|
|
setFilters={setSearchFilter}
|
|
|
|
|
search={search}
|
|
|
|
|
setSearch={setSearch}
|
|
|
|
|
allSuggestions={suggestionsValues}
|
2024-09-06 22:26:32 +03:00
|
|
|
/>
|
2024-09-10 19:23:20 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
2024-06-23 23:58:00 +03:00
|
|
|
|
2024-09-10 19:23:20 +03:00
|
|
|
{hasExistingSearch && (
|
2024-09-23 15:39:20 +03:00
|
|
|
<ScrollArea className="w-full whitespace-nowrap lg:ml-[35%]">
|
|
|
|
|
<div className="flex flex-row">
|
|
|
|
|
<SearchFilterGroup
|
|
|
|
|
className={cn(
|
|
|
|
|
"w-full justify-between md:justify-start lg:justify-end",
|
|
|
|
|
)}
|
|
|
|
|
filter={searchFilter}
|
|
|
|
|
onUpdateFilter={onUpdateFilter}
|
|
|
|
|
/>
|
|
|
|
|
<ScrollBar orientation="horizontal" className="h-0" />
|
|
|
|
|
</div>
|
|
|
|
|
</ScrollArea>
|
2024-09-10 19:23:20 +03:00
|
|
|
)}
|
2024-06-23 23:58:00 +03:00
|
|
|
</div>
|
|
|
|
|
|
2024-10-02 17:32:12 +03:00
|
|
|
<div
|
|
|
|
|
ref={contentRef}
|
|
|
|
|
className="no-scrollbar flex flex-1 flex-wrap content-start gap-2 overflow-y-auto"
|
|
|
|
|
>
|
2024-09-14 16:42:56 +03:00
|
|
|
{uniqueResults?.length == 0 && !isLoading && (
|
2024-06-23 23:58:00 +03:00
|
|
|
<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>
|
|
|
|
|
)}
|
|
|
|
|
|
2024-09-14 16:42:56 +03:00
|
|
|
{uniqueResults?.length == 0 &&
|
|
|
|
|
isLoading &&
|
2024-09-14 23:08:46 +03:00
|
|
|
(searchTerm ||
|
|
|
|
|
(searchFilter && Object.keys(searchFilter).length !== 0)) && (
|
2024-09-14 16:42:56 +03:00
|
|
|
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
|
|
|
|
)}
|
2024-06-23 23:58:00 +03:00
|
|
|
|
2024-09-11 17:41:16 +03:00
|
|
|
{uniqueResults && (
|
2024-10-02 15:59:53 +03:00
|
|
|
<div className={gridClassName}>
|
2024-09-11 17:41:16 +03:00
|
|
|
{uniqueResults &&
|
2024-09-13 06:07:35 +03:00
|
|
|
uniqueResults.map((value, index) => {
|
|
|
|
|
const selected = selectedIndex === index;
|
2024-09-11 17:41:16 +03:00
|
|
|
|
|
|
|
|
return (
|
2024-07-11 01:01:31 +03:00
|
|
|
<div
|
2024-09-11 17:41:16 +03:00
|
|
|
key={value.id}
|
2024-09-13 06:07:35 +03:00
|
|
|
ref={(item) => (itemRefs.current[index] = item)}
|
2024-09-11 17:41:16 +03:00
|
|
|
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-13 06:07:35 +03:00
|
|
|
onClick={() => onSelectSearch(value, index)}
|
2024-09-11 17:41:16 +03:00
|
|
|
/>
|
2024-10-08 05:15:49 +03:00
|
|
|
{(searchTerm ||
|
|
|
|
|
searchFilter?.search_type?.includes("similarity")) && (
|
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" />
|
|
|
|
|
)}
|
2024-10-07 23:30:45 +03:00
|
|
|
{zScoreToConfidence(value.search_distance)}%
|
2024-09-11 17:41:16 +03:00
|
|
|
</Chip>
|
|
|
|
|
</TooltipTrigger>
|
2024-09-26 23:30:56 +03:00
|
|
|
<TooltipPortal>
|
|
|
|
|
<TooltipContent>
|
|
|
|
|
Matched {value.search_source} at{" "}
|
2024-10-07 23:30:45 +03:00
|
|
|
{zScoreToConfidence(value.search_distance)}%
|
2024-09-26 23:30:56 +03:00
|
|
|
</TooltipContent>
|
|
|
|
|
</TooltipPortal>
|
2024-09-11 17:41:16 +03:00
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
2024-09-13 06:07:35 +03:00
|
|
|
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-selected outline-selected` : "outline-transparent duration-500"}`}
|
2024-09-11 17:41:16 +03:00
|
|
|
/>
|
2024-06-23 23:58:00 +03:00
|
|
|
</div>
|
2024-09-11 17:41:16 +03:00
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2024-09-14 16:42:56 +03:00
|
|
|
{uniqueResults && uniqueResults.length > 0 && (
|
|
|
|
|
<>
|
|
|
|
|
<div ref={observerTarget} className="h-10 w-full" />
|
|
|
|
|
<div className="flex h-12 w-full justify-center">
|
|
|
|
|
{hasMore && isLoading && <ActivityIndicator />}
|
|
|
|
|
</div>
|
2024-10-02 15:59:53 +03:00
|
|
|
|
|
|
|
|
{isDesktop && columnCount && (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
|
|
|
|
"fixed bottom-12 right-3 z-50 flex flex-row gap-2 lg:bottom-9",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Popover>
|
|
|
|
|
<Tooltip>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<div className="cursor-pointer rounded-lg bg-secondary text-secondary-foreground opacity-75 transition-all duration-300 hover:bg-muted hover:opacity-100">
|
|
|
|
|
<LuColumns className="size-5 md:m-[6px]" />
|
|
|
|
|
</div>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent>Adjust Grid Columns</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<PopoverContent className="mr-2 w-80">
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="font-medium leading-none">
|
|
|
|
|
Grid Columns
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center space-x-4">
|
|
|
|
|
<Slider
|
|
|
|
|
value={[effectiveColumnCount]}
|
|
|
|
|
onValueChange={([value]) => setColumnCount(value)}
|
|
|
|
|
max={8}
|
|
|
|
|
min={2}
|
|
|
|
|
step={1}
|
|
|
|
|
className="flex-grow"
|
|
|
|
|
/>
|
|
|
|
|
<span className="w-9 text-center text-sm font-medium">
|
|
|
|
|
{effectiveColumnCount}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2024-09-14 16:42:56 +03:00
|
|
|
</>
|
2024-09-11 17:41:16 +03:00
|
|
|
)}
|
2024-06-23 23:58:00 +03:00
|
|
|
</div>
|
2024-09-14 23:08:46 +03:00
|
|
|
{searchFilter &&
|
|
|
|
|
Object.keys(searchFilter).length === 0 &&
|
|
|
|
|
!searchTerm && (
|
|
|
|
|
<div className="scrollbar-container flex size-full flex-col overflow-y-auto">
|
2024-09-24 21:34:29 +03:00
|
|
|
<ExploreView
|
|
|
|
|
searchDetail={searchDetail}
|
|
|
|
|
setSearchDetail={setSearchDetail}
|
|
|
|
|
/>
|
2024-09-14 23:08:46 +03:00
|
|
|
</div>
|
|
|
|
|
)}
|
2024-06-23 23:58:00 +03:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|