diff --git a/web/src/components/overlay/detail/ReviewDetailDialog.tsx b/web/src/components/overlay/detail/ReviewDetailDialog.tsx index fbb4b6db6..7c7888a67 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/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 00e612248..bec7589db 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -20,7 +20,7 @@ import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import { getIconForLabel } from "@/utils/iconUtil"; import { useApiHost } from "@/api"; import { Button } from "../../ui/button"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import axios from "axios"; import { toast } from "sonner"; import { Textarea } from "../../ui/textarea"; @@ -55,6 +55,28 @@ export default function SearchDetailDialog({ : "%b %-d %Y, %I:%M %p", ); + const score = useMemo(() => { + if (!search) { + return 0; + } + + const value = search.score ?? search.data.top_score; + + return Math.round(value * 100); + }, [search]); + + const subLabelScore = useMemo(() => { + if (!search) { + return undefined; + } + + if (search.sub_label) { + return Math.round((search.data?.top_score ?? 0) * 100); + } else { + return undefined; + } + }, [search]); + // api const updateDescription = useCallback(() => { @@ -120,9 +142,7 @@ export default function SearchDetailDialog({
Score
- {Math.round(search.data.top_score * 100)}% - {search.sub_label && - ` (${Math.round((search.data.sub_label_score ?? 0) * 100)}%)`} + {score}%{subLabelScore && ` (${subLabelScore}%)`}
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) && (