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) && (