diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx index 55c94f009..d134d7079 100644 --- a/web/src/components/overlay/detail/ReviewDetailDialog.tsx +++ b/web/src/components/overlay/detail/ReviewDetailDialog.tsx @@ -25,7 +25,7 @@ import { cn } from "@/lib/utils"; import { FrigatePlusDialog } from "../dialog/FrigatePlusDialog"; import ObjectLifecycle from "./ObjectLifecycle"; import Chip from "@/components/indicators/Chip"; -import { FaDownload } from "react-icons/fa"; +import { FaDownload, FaImages } from "react-icons/fa"; import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon"; import { FaArrowsRotate } from "react-icons/fa6"; import { @@ -33,6 +33,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import { useNavigate } from "react-router-dom"; type ReviewDetailDialogProps = { review?: ReviewSegment; @@ -234,6 +235,8 @@ function EventItem({ const [hovered, setHovered] = useState(isMobile); + const navigate = useNavigate(); + return ( <>
View Object Lifecycle )} + + {event.has_snapshot && config?.semantic_search.enabled && ( + + + { + const similaritySearchParams = new URLSearchParams({ + search_type: "similarity", + event_id: event.id, + }).toString(); + + navigate(`/search?${similaritySearchParams}`); + }} + > + + + + Find Similar + + )}
)} diff --git a/web/src/pages/Search.tsx b/web/src/pages/Search.tsx index 80e9846cb..c66cdc30b 100644 --- a/web/src/pages/Search.tsx +++ b/web/src/pages/Search.tsx @@ -3,7 +3,11 @@ import { useCameraPreviews } from "@/hooks/use-camera-previews"; import { useOverlayState } from "@/hooks/use-overlay-state"; import { FrigateConfig } from "@/types/frigateConfig"; import { RecordingStartingPoint } from "@/types/record"; -import { SearchFilter, SearchResult } from "@/types/search"; +import { + PartialSearchResult, + SearchFilter, + SearchResult, +} from "@/types/search"; import { TimeRange } from "@/types/timeline"; import { RecordingView } from "@/views/recording/RecordingView"; import SearchView from "@/views/search/SearchView"; @@ -38,7 +42,27 @@ export default function Search() { // search api - const [similaritySearch, setSimilaritySearch] = useState(); + const [similaritySearch, setSimilaritySearch] = + useState(); + + useEffect(() => { + if ( + config?.semantic_search.enabled && + searchSearchParams["search_type"] == "similarity" && + searchSearchParams["event_id"]?.length != 0 && + searchFilter + ) { + setSimilaritySearch({ + id: searchSearchParams["event_id"], + }); + + // remove event id from url params + const { event_id: _event_id, ...newFilter } = searchFilter; + setSearchFilter(newFilter); + } + // only run similarity search with event_id in the url when coming from review + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { if (similaritySearch) { diff --git a/web/src/types/search.ts b/web/src/types/search.ts index 673644e1b..af1567605 100644 --- a/web/src/types/search.ts +++ b/web/src/types/search.ts @@ -25,6 +25,9 @@ export type SearchResult = { }; }; + +export type PartialSearchResult = Partial & { id: string }; + export type SearchFilter = { cameras?: string[]; labels?: string[]; @@ -33,4 +36,5 @@ export type SearchFilter = { before?: number; after?: number; search_type?: SearchSource[]; + event_id?: string; }; diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index 06bac13e0..391deceb6 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -13,7 +13,11 @@ import { import { cn } from "@/lib/utils"; import { FrigateConfig } from "@/types/frigateConfig"; import { Preview } from "@/types/preview"; -import { SearchFilter, SearchResult } from "@/types/search"; +import { + PartialSearchResult, + SearchFilter, + SearchResult, +} from "@/types/search"; import { useCallback, useMemo, useState } from "react"; import { isMobileOnly } from "react-device-detect"; import { LuImage, LuSearchX, LuText, LuXCircle } from "react-icons/lu"; @@ -26,7 +30,7 @@ type SearchViewProps = { searchResults?: SearchResult[]; allPreviews?: Preview[]; isLoading: boolean; - similaritySearch?: SearchResult; + similaritySearch?: PartialSearchResult; setSearch: (search: string) => void; setSimilaritySearch: (search: SearchResult) => void; onUpdateFilter: (filter: SearchFilter) => void; @@ -186,7 +190,7 @@ export default function SearchView({ scrollLock={false} onClick={onSelectSearch} /> - {searchTerm && ( + {(searchTerm || similaritySearch) && (