From 7d44970f78e653ee1e70c8600363511aaf59e4bf Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 10 Mar 2025 09:01:52 -0600 Subject: [PATCH] Face multi select (#17068) * Implement multi select for face library * Clear list of selected * Add keyboard shortcut --- web/src/pages/FaceLibrary.tsx | 154 +++++++++++++++++++++-------- web/src/views/events/EventView.tsx | 7 +- 2 files changed, 117 insertions(+), 44 deletions(-) diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index b9d3ee71a..fbb75c681 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -19,6 +19,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; import useOptimisticState from "@/hooks/use-optimistic-state"; import { cn } from "@/lib/utils"; import { FrigateConfig } from "@/types/frigateConfig"; @@ -141,6 +142,73 @@ export default function FaceLibrary() { [refreshFaces], ); + // face multiselect + + const [selectedFaces, setSelectedFaces] = useState([]); + + const onClickFace = useCallback( + (imageId: string) => { + const index = selectedFaces.indexOf(imageId); + + if (index != -1) { + if (selectedFaces.length == 1) { + setSelectedFaces([]); + } else { + const copy = [ + ...selectedFaces.slice(0, index), + ...selectedFaces.slice(index + 1), + ]; + setSelectedFaces(copy); + } + } else { + const copy = [...selectedFaces]; + copy.push(imageId); + setSelectedFaces(copy); + } + }, + [selectedFaces, setSelectedFaces], + ); + + const onDelete = useCallback(() => { + axios + .post(`/faces/train/delete`, { ids: selectedFaces }) + .then((resp) => { + setSelectedFaces([]); + + if (resp.status == 200) { + toast.success(`Successfully deleted face.`, { + position: "top-center", + }); + refreshFaces(); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(`Failed to delete: ${errorMessage}`, { + position: "top-center", + }); + }); + }, [selectedFaces, refreshFaces]); + + // keyboard + + useKeyboardListener(["a"], (key, modifiers) => { + if (modifiers.repeat || !modifiers.down) { + return; + } + + switch (key) { + case "a": + if (modifiers.ctrl) { + setSelectedFaces([...trainImages]); + } + break; + } + }); + if (!config) { return ; } @@ -210,16 +278,27 @@ export default function FaceLibrary() { -
- - -
+ {selectedFaces?.length > 0 ? ( +
+ +
+ ) : ( +
+ + {pageToggle != "train" && ( + + )} +
+ )} {pageToggle && (pageToggle == "train" ? ( @@ -227,6 +306,8 @@ export default function FaceLibrary() { config={config} attemptImages={trainImages} faceNames={faces} + selectedFaces={selectedFaces} + onClickFace={onClickFace} onRefresh={refreshFaces} /> ) : ( @@ -244,22 +325,28 @@ type TrainingGridProps = { config: FrigateConfig; attemptImages: string[]; faceNames: string[]; + selectedFaces: string[]; + onClickFace: (image: string) => void; onRefresh: () => void; }; function TrainingGrid({ config, attemptImages, faceNames, + selectedFaces, + onClickFace, onRefresh, }: TrainingGridProps) { return ( -
+
{attemptImages.map((image: string) => ( onClickFace(image)} onRefresh={onRefresh} /> ))} @@ -271,12 +358,16 @@ type FaceAttemptProps = { image: string; faceNames: string[]; threshold: number; + selected: boolean; + onClick: () => void; onRefresh: () => void; }; function FaceAttempt({ image, faceNames, threshold, + selected, + onClick, onRefresh, }: FaceAttemptProps) { const data = useMemo(() => { @@ -336,30 +427,16 @@ function FaceAttempt({ }); }, [image, onRefresh]); - const onDelete = useCallback(() => { - axios - .post(`/faces/train/delete`, { ids: [image] }) - .then((resp) => { - if (resp.status == 200) { - toast.success(`Successfully deleted face.`, { - position: "top-center", - }); - onRefresh(); - } - }) - .catch((error) => { - const errorMessage = - error.response?.data?.message || - error.response?.data?.detail || - "Unknown error"; - toast.error(`Failed to delete: ${errorMessage}`, { - position: "top-center", - }); - }); - }, [image, onRefresh]); - return ( -
+
@@ -409,15 +486,6 @@ function FaceAttempt({ Reprocess Face - - - - - Delete Face Attempt -
diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index 5ffeef8af..0c0cc2571 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -757,7 +757,12 @@ function DetectionReview({ />
);