2024-09-10 18:23:20 +02:00
|
|
|
import { useApiFilterArgs } from "@/hooks/use-api-filter";
|
2024-06-23 22:58:00 +02:00
|
|
|
import { useCameraPreviews } from "@/hooks/use-camera-previews";
|
2024-09-11 16:41:16 +02:00
|
|
|
import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
|
2024-06-23 22:58:00 +02:00
|
|
|
import { FrigateConfig } from "@/types/frigateConfig";
|
|
|
|
import { RecordingStartingPoint } from "@/types/record";
|
2024-09-14 15:42:56 +02:00
|
|
|
import { SearchFilter, SearchQuery, SearchResult } from "@/types/search";
|
2024-06-23 22:58:00 +02:00
|
|
|
import { TimeRange } from "@/types/timeline";
|
|
|
|
import { RecordingView } from "@/views/recording/RecordingView";
|
|
|
|
import SearchView from "@/views/search/SearchView";
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
|
import useSWR from "swr";
|
2024-09-14 15:42:56 +02:00
|
|
|
import useSWRInfinite from "swr/infinite";
|
|
|
|
|
|
|
|
const API_LIMIT = 25;
|
2024-06-23 22:58:00 +02:00
|
|
|
|
2024-09-11 16:41:16 +02:00
|
|
|
export default function Explore() {
|
2024-06-23 22:58:00 +02:00
|
|
|
const { data: config } = useSWR<FrigateConfig>("config", {
|
|
|
|
revalidateOnFocus: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
// search field handler
|
|
|
|
|
|
|
|
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout>();
|
|
|
|
const [search, setSearch] = useState("");
|
|
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
|
|
|
|
|
|
const [recording, setRecording] =
|
|
|
|
useOverlayState<RecordingStartingPoint>("recording");
|
|
|
|
|
|
|
|
// search filter
|
|
|
|
|
2024-09-11 21:20:41 +02:00
|
|
|
const similaritySearch = useMemo(() => {
|
|
|
|
if (!searchTerm.includes("similarity:")) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return searchTerm.split(":")[1];
|
|
|
|
}, [searchTerm]);
|
2024-09-11 16:41:16 +02:00
|
|
|
|
2024-06-23 22:58:00 +02:00
|
|
|
const [searchFilter, setSearchFilter, searchSearchParams] =
|
2024-09-10 18:23:20 +02:00
|
|
|
useApiFilterArgs<SearchFilter>();
|
2024-06-23 22:58:00 +02:00
|
|
|
|
|
|
|
// search api
|
|
|
|
|
2024-09-11 16:41:16 +02:00
|
|
|
useSearchEffect("similarity_search_id", (similarityId) => {
|
2024-09-11 21:20:41 +02:00
|
|
|
setSearch(`similarity:${similarityId}`);
|
|
|
|
// @ts-expect-error we want to clear this
|
|
|
|
setSearchFilter({ ...searchFilter, similarity_search_id: undefined });
|
2024-09-12 16:46:29 +02:00
|
|
|
return false;
|
2024-09-11 16:41:16 +02:00
|
|
|
});
|
2024-06-23 22:58:00 +02:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (searchTimeout) {
|
|
|
|
clearTimeout(searchTimeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
setSearchTimeout(
|
|
|
|
setTimeout(() => {
|
|
|
|
setSearchTimeout(undefined);
|
|
|
|
setSearchTerm(search);
|
2024-09-09 16:33:38 +02:00
|
|
|
}, 750),
|
2024-06-23 22:58:00 +02:00
|
|
|
);
|
|
|
|
// we only want to update the searchTerm when search changes
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [search]);
|
|
|
|
|
2024-09-14 15:42:56 +02:00
|
|
|
const searchQuery: SearchQuery = useMemo(() => {
|
2024-06-23 22:58:00 +02:00
|
|
|
if (similaritySearch) {
|
|
|
|
return [
|
|
|
|
"events/search",
|
|
|
|
{
|
2024-09-11 21:20:41 +02:00
|
|
|
query: similaritySearch,
|
2024-06-23 22:58:00 +02:00
|
|
|
cameras: searchSearchParams["cameras"],
|
|
|
|
labels: searchSearchParams["labels"],
|
2024-09-10 18:23:20 +02:00
|
|
|
sub_labels: searchSearchParams["subLabels"],
|
2024-06-23 22:58:00 +02:00
|
|
|
zones: searchSearchParams["zones"],
|
|
|
|
before: searchSearchParams["before"],
|
|
|
|
after: searchSearchParams["after"],
|
|
|
|
include_thumbnails: 0,
|
2024-09-09 21:45:19 +02:00
|
|
|
search_type: "similarity",
|
2024-06-23 22:58:00 +02:00
|
|
|
},
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2024-09-10 18:23:20 +02:00
|
|
|
if (searchTerm) {
|
|
|
|
return [
|
|
|
|
"events/search",
|
|
|
|
{
|
|
|
|
query: searchTerm,
|
|
|
|
cameras: searchSearchParams["cameras"],
|
|
|
|
labels: searchSearchParams["labels"],
|
|
|
|
sub_labels: searchSearchParams["subLabels"],
|
|
|
|
zones: searchSearchParams["zones"],
|
|
|
|
before: searchSearchParams["before"],
|
|
|
|
after: searchSearchParams["after"],
|
|
|
|
search_type: searchSearchParams["search_type"],
|
|
|
|
include_thumbnails: 0,
|
|
|
|
},
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2024-09-11 16:41:16 +02:00
|
|
|
if (searchSearchParams && Object.keys(searchSearchParams).length !== 0) {
|
|
|
|
return [
|
|
|
|
"events",
|
|
|
|
{
|
|
|
|
cameras: searchSearchParams["cameras"],
|
|
|
|
labels: searchSearchParams["labels"],
|
|
|
|
sub_labels: searchSearchParams["subLabels"],
|
|
|
|
zones: searchSearchParams["zones"],
|
|
|
|
before: searchSearchParams["before"],
|
|
|
|
after: searchSearchParams["after"],
|
|
|
|
search_type: searchSearchParams["search_type"],
|
2024-09-14 15:42:56 +02:00
|
|
|
limit:
|
|
|
|
Object.keys(searchSearchParams).length == 0 ? API_LIMIT : undefined,
|
2024-09-11 16:41:16 +02:00
|
|
|
in_progress: 0,
|
|
|
|
include_thumbnails: 0,
|
|
|
|
},
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
2024-06-23 22:58:00 +02:00
|
|
|
}, [searchTerm, searchSearchParams, similaritySearch]);
|
|
|
|
|
2024-09-14 15:42:56 +02:00
|
|
|
// paging
|
|
|
|
|
|
|
|
const getKey = (
|
|
|
|
pageIndex: number,
|
|
|
|
previousPageData: SearchResult[] | null,
|
|
|
|
): SearchQuery => {
|
|
|
|
if (previousPageData && !previousPageData.length) return null; // reached the end
|
|
|
|
if (!searchQuery) return null;
|
|
|
|
|
|
|
|
const [url, params] = searchQuery;
|
|
|
|
|
|
|
|
// If it's not the first page, use the last item's start_time as the 'before' parameter
|
|
|
|
if (pageIndex > 0 && previousPageData) {
|
|
|
|
const lastDate = previousPageData[previousPageData.length - 1].start_time;
|
|
|
|
return [
|
|
|
|
url,
|
|
|
|
{ ...params, before: lastDate.toString(), limit: API_LIMIT },
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
// For the first page, use the original params
|
|
|
|
return [url, { ...params, limit: API_LIMIT }];
|
|
|
|
};
|
|
|
|
|
|
|
|
const { data, size, setSize, isValidating } = useSWRInfinite<SearchResult[]>(
|
|
|
|
getKey,
|
|
|
|
{
|
|
|
|
revalidateFirstPage: false,
|
|
|
|
revalidateAll: false,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
const searchResults = useMemo(
|
|
|
|
() => (data ? ([] as SearchResult[]).concat(...data) : []),
|
|
|
|
[data],
|
|
|
|
);
|
|
|
|
const isLoadingInitialData = !data && !isValidating;
|
|
|
|
const isLoadingMore =
|
|
|
|
isLoadingInitialData ||
|
|
|
|
(size > 0 && data && typeof data[size - 1] === "undefined");
|
|
|
|
const isEmpty = data?.[0]?.length === 0;
|
|
|
|
const isReachingEnd =
|
|
|
|
isEmpty || (data && data[data.length - 1]?.length < API_LIMIT);
|
|
|
|
|
|
|
|
const loadMore = useCallback(() => {
|
|
|
|
if (!isReachingEnd && !isLoadingMore) {
|
|
|
|
if (searchQuery) {
|
|
|
|
const [url] = searchQuery;
|
|
|
|
|
|
|
|
// for chroma, only load 100 results for description and similarity
|
|
|
|
if (url === "events/search" && searchResults.length >= 100) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setSize(size + 1);
|
|
|
|
}
|
|
|
|
}, [isReachingEnd, isLoadingMore, setSize, size, searchResults, searchQuery]);
|
|
|
|
|
|
|
|
// previews
|
2024-06-23 22:58:00 +02:00
|
|
|
|
|
|
|
const previewTimeRange = useMemo<TimeRange>(() => {
|
|
|
|
if (!searchResults) {
|
|
|
|
return { after: 0, before: 0 };
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
after: Math.min(...searchResults.map((res) => res.start_time)),
|
|
|
|
before: Math.max(
|
|
|
|
...searchResults.map((res) => res.end_time ?? Date.now() / 1000),
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}, [searchResults]);
|
|
|
|
|
|
|
|
const allPreviews = useCameraPreviews(previewTimeRange, {
|
|
|
|
autoRefresh: false,
|
|
|
|
fetchPreviews: searchResults != undefined,
|
|
|
|
});
|
|
|
|
|
|
|
|
// selection
|
|
|
|
|
|
|
|
const onOpenSearch = useCallback(
|
|
|
|
(item: SearchResult) => {
|
|
|
|
setRecording({
|
|
|
|
camera: item.camera,
|
|
|
|
startTime: item.start_time,
|
|
|
|
severity: "alert",
|
|
|
|
});
|
|
|
|
},
|
|
|
|
[setRecording],
|
|
|
|
);
|
|
|
|
|
|
|
|
const selectedReviewData = useMemo(() => {
|
|
|
|
if (!recording) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!config) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!searchResults) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const allCameras = searchFilter?.cameras ?? Object.keys(config.cameras);
|
|
|
|
|
|
|
|
return {
|
|
|
|
camera: recording.camera,
|
|
|
|
start_time: recording.startTime,
|
|
|
|
allCameras: allCameras,
|
|
|
|
};
|
|
|
|
|
|
|
|
// previews will not update after item is selected
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [recording, searchResults]);
|
|
|
|
|
|
|
|
const selectedTimeRange = useMemo(() => {
|
|
|
|
if (!recording) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
const time = new Date(recording.startTime * 1000);
|
|
|
|
time.setUTCMinutes(0, 0, 0);
|
|
|
|
const start = time.getTime() / 1000;
|
|
|
|
time.setHours(time.getHours() + 2);
|
|
|
|
const end = time.getTime() / 1000;
|
|
|
|
return {
|
|
|
|
after: start,
|
|
|
|
before: end,
|
|
|
|
};
|
|
|
|
}, [recording]);
|
|
|
|
|
|
|
|
if (recording) {
|
|
|
|
if (selectedReviewData && selectedTimeRange) {
|
|
|
|
return (
|
|
|
|
<RecordingView
|
|
|
|
startCamera={selectedReviewData.camera}
|
|
|
|
startTime={selectedReviewData.start_time}
|
|
|
|
allCameras={selectedReviewData.allCameras}
|
|
|
|
allPreviews={allPreviews}
|
|
|
|
timeRange={selectedTimeRange}
|
2024-09-11 21:20:41 +02:00
|
|
|
updateFilter={setSearchFilter}
|
2024-06-23 22:58:00 +02:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return (
|
|
|
|
<SearchView
|
|
|
|
search={search}
|
|
|
|
searchTerm={searchTerm}
|
|
|
|
searchFilter={searchFilter}
|
|
|
|
searchResults={searchResults}
|
2024-09-14 15:42:56 +02:00
|
|
|
isLoading={(isLoadingInitialData || isLoadingMore) ?? true}
|
2024-06-23 22:58:00 +02:00
|
|
|
setSearch={setSearch}
|
2024-09-11 21:20:41 +02:00
|
|
|
setSimilaritySearch={(search) => setSearch(`similarity:${search.id}`)}
|
|
|
|
onUpdateFilter={setSearchFilter}
|
2024-06-23 22:58:00 +02:00
|
|
|
onOpenSearch={onOpenSearch}
|
2024-09-14 15:42:56 +02:00
|
|
|
loadMore={loadMore}
|
|
|
|
hasMore={!isReachingEnd}
|
2024-06-23 22:58:00 +02:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|