import { baseUrl } from "@/api/baseUrl"; import { CamerasFilterButton, GeneralFilterContent, } from "@/components/filter/ReviewFilterGroup"; import Chip from "@/components/indicators/Chip"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; import { DropdownMenu, DropdownMenuContent, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { DualThumbSlider } from "@/components/ui/slider"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { Event } from "@/types/event"; import { ATTRIBUTE_LABELS, FrigateConfig } from "@/types/frigateConfig"; import { getIconForLabel } from "@/utils/iconUtil"; import { capitalizeFirstLetter } from "@/utils/stringUtil"; import axios from "axios"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { isMobile } from "react-device-detect"; import { FaList, FaSort, FaSortAmountDown, FaSortAmountUp, } from "react-icons/fa"; import { PiSlidersHorizontalFill } from "react-icons/pi"; import useSWR from "swr"; import useSWRInfinite from "swr/infinite"; const API_LIMIT = 100; export default function SubmitPlus() { const { data: config } = useSWR("config"); useEffect(() => { document.title = "Plus - Frigate"; }, []); // filters const [selectedCameras, setSelectedCameras] = useState(); const [selectedLabels, setSelectedLabels] = useState(); const [scoreRange, setScoreRange] = useState(); // sort const [sort, setSort] = useState(); // data const eventFetcher = useCallback((key: string) => { const [path, params] = Array.isArray(key) ? key : [key, undefined]; return axios.get(path, { params }).then((res) => res.data); }, []); const getKey = useCallback( (index: number, prevData: Event[]) => { if (index > 0) { const lastDate = prevData[prevData.length - 1].start_time; return [ "events", { limit: API_LIMIT, in_progress: 0, is_submitted: 0, cameras: selectedCameras ? selectedCameras.join(",") : null, labels: selectedLabels ? selectedLabels.join(",") : null, min_score: scoreRange ? scoreRange[0] : null, max_score: scoreRange ? scoreRange[1] : null, sort: sort ? sort : null, before: lastDate, }, ]; } return [ "events", { limit: 100, in_progress: 0, is_submitted: 0, cameras: selectedCameras ? selectedCameras.join(",") : null, labels: selectedLabels ? selectedLabels.join(",") : null, min_score: scoreRange ? scoreRange[0] : null, max_score: scoreRange ? scoreRange[1] : null, sort: sort ? sort : null, }, ]; }, [scoreRange, selectedCameras, selectedLabels, sort], ); const { data: eventPages, mutate: refresh, size, setSize, isValidating, } = useSWRInfinite(getKey, eventFetcher, { revalidateOnFocus: false, }); const events = useMemo( () => (eventPages ? eventPages.flat() : []), [eventPages], ); const [upload, setUpload] = useState(); // paging const isDone = useMemo( () => (eventPages?.at(-1)?.length ?? 0) < API_LIMIT, [eventPages], ); const pagingObserver = useRef(); const lastEventRef = useCallback( (node: HTMLElement | null) => { if (isValidating) return; if (pagingObserver.current) pagingObserver.current.disconnect(); try { pagingObserver.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && !isDone) { setSize(size + 1); } }); if (node) pagingObserver.current.observe(node); } catch (e) { // no op } }, [isValidating, isDone, size, setSize], ); // layout const grow = useMemo(() => { if (!config || !upload) { return ""; } const camera = config.cameras[upload.camera]; if (!camera) { return ""; } if (camera.detect.width / camera.detect.height < 16 / 9) { return "aspect-video object-contain"; } return ""; }, [config, upload]); const onSubmitToPlus = useCallback( async (falsePositive: boolean) => { if (!upload) { return; } falsePositive ? axios.put(`events/${upload.id}/false_positive`) : axios.post(`events/${upload.id}/plus`, { include_annotation: 1, }); refresh( (data: Event[][] | undefined) => { if (!data) { return data; } let pageIndex = -1; let index = -1; data.forEach((page, pIdx) => { const search = page.findIndex((e) => e.id == upload.id); if (search != -1) { pageIndex = pIdx; index = search; } }); if (index == -1) { return data; } return [ ...data.slice(0, pageIndex), [ ...data[pageIndex].slice(0, index), ...data[pageIndex].slice(index + 1), ], ...data.slice(pageIndex + 1), ]; }, { revalidate: false, populateCache: true }, ); setUpload(undefined); }, [refresh, upload], ); return (
(!open ? setUpload(undefined) : null)} > Submit To Frigate+ Objects in locations you want to avoid are not false positives. Submitting them as false positives will confuse the model. {`${upload?.label}`} {events?.map((event, eIdx) => { if (event.data.type != "object") { return; } const lastRow = eIdx == events.length - 1; return (
setUpload(event)} >
{[event.label].map((object) => { return getIconForLabel( object, "size-3 text-white", ); })}
{[event.label] .map((text) => capitalizeFirstLetter(text)) .sort() .join(", ") .replaceAll("-verified", "")}
); })} {isValidating && }
); } type PlusFilterGroupProps = { selectedCameras: string[] | undefined; selectedLabels: string[] | undefined; selectedScoreRange: number[] | undefined; setSelectedCameras: (cameras: string[] | undefined) => void; setSelectedLabels: (cameras: string[] | undefined) => void; setSelectedScoreRange: (range: number[] | undefined) => void; }; function PlusFilterGroup({ selectedCameras, selectedLabels, selectedScoreRange, setSelectedCameras, setSelectedLabels, setSelectedScoreRange, }: PlusFilterGroupProps) { const { data: config } = useSWR("config"); const allCameras = useMemo(() => { if (!config) { return []; } return Object.keys(config.cameras); }, [config]); const allLabels = useMemo(() => { if (!config) { return []; } const labels = new Set(); const cameras = selectedCameras || Object.keys(config.cameras); cameras.forEach((camera) => { const cameraConfig = config.cameras[camera]; cameraConfig.objects.track.forEach((label) => { if (!ATTRIBUTE_LABELS.includes(label)) { labels.add(label); } }); }); return [...labels].sort(); }, [config, selectedCameras]); const [open, setOpen] = useState<"none" | "camera" | "label" | "score">( "none", ); const [currentLabels, setCurrentLabels] = useState( undefined, ); const [currentScoreRange, setCurrentScoreRange] = useState< number[] | undefined >(undefined); const Menu = isMobile ? Drawer : DropdownMenu; const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger; const Content = isMobile ? DrawerContent : DropdownMenuContent; return (
{ if (!open) { setCurrentLabels(selectedLabels); } setOpen(open ? "label" : "none"); }} > setOpen("none")} /> { setOpen(open ? "score" : "none"); }} >
setCurrentScoreRange([ parseInt(e.target.value) / 100.0, currentScoreRange?.at(1) ?? 1.0, ]) } /> setCurrentScoreRange([ currentScoreRange?.at(0) ?? 0.5, parseInt(e.target.value) / 100.0, ]) } />
); } type PlusSortSelectorProps = { selectedSort?: string; setSelectedSort: (sort: string | undefined) => void; }; function PlusSortSelector({ selectedSort, setSelectedSort, }: PlusSortSelectorProps) { // menu state const [open, setOpen] = useState(false); // sort const [currentSort, setCurrentSort] = useState(); const [currentDir, setCurrentDir] = useState("desc"); // components const Sort = selectedSort ? selectedSort.split("_")[1] == "desc" ? FaSortAmountDown : FaSortAmountUp : FaSort; const Menu = isMobile ? Drawer : DropdownMenu; const Trigger = isMobile ? DrawerTrigger : DropdownMenuTrigger; const Content = isMobile ? DrawerContent : DropdownMenuContent; return (
{ setOpen(open); if (!open) { const parts = selectedSort?.split("_"); if (parts?.length == 2) { setCurrentSort(parts[0]); setCurrentDir(parts[1]); } } }} > setCurrentSort(value)} >
{currentSort == "date" ? ( currentDir == "desc" ? ( setCurrentDir("asc")} /> ) : ( setCurrentDir("desc")} /> ) ) : (
)}
{currentSort == "score" ? ( currentDir == "desc" ? ( setCurrentDir("asc")} /> ) : ( setCurrentDir("desc")} /> ) ) : (
)}
); }