From b8b58e980b66915a06954cbd6970a32dce65dc1e Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 3 Apr 2025 09:17:29 -0500 Subject: [PATCH] Use dropdown for collection selection in Face Library (#17521) * Use dropdown for name selection in face library * change verbiage --- web/public/locales/en/views/faceLibrary.json | 12 +- web/src/pages/FaceLibrary.tsx | 195 ++++++++++++------- 2 files changed, 137 insertions(+), 70 deletions(-) diff --git a/web/public/locales/en/views/faceLibrary.json b/web/public/locales/en/views/faceLibrary.json index baabed69a..0a4444ae5 100644 --- a/web/public/locales/en/views/faceLibrary.json +++ b/web/public/locales/en/views/faceLibrary.json @@ -25,6 +25,11 @@ "aria": "Select train" }, "selectItem": "Select {{item}}", + "selectFace": "Select Face", + "deleteFaceLibrary": { + "title": "Delete Name", + "desc": "Are you sure you want to delete {{name}}? This will permanently delete all associated faces." + }, "button": { "deleteFaceAttempts": "Delete Face Attempts", "addFace": "Add Face", @@ -38,7 +43,11 @@ "success": { "uploadedImage": "Successfully uploaded image.", "addFaceLibrary": "{{name}} has successfully been added to the Face Library!", - "deletedFace": "Successfully deleted face.", + "deletedFace_one": "Successfully deleted {{count}} face.", + "deletedFace_other": "Successfully deleted {{count}} faces.", + "deletedName_zero": "Empty collection deleted successfully.", + "deletedName_one": "{{count}} face has been successfully deleted.", + "deletedName_other": "{{count}} faces have been successfully deleted.", "trainedFace": "Successfully trained face.", "updatedFaceScore": "Successfully updated face score." }, @@ -46,6 +55,7 @@ "uploadingImageFailed": "Failed to upload image: {{errorMessage}}", "addFaceLibraryFailed": "Failed to set face name: {{errorMessage}}", "deleteFaceFailed": "Failed to delete: {{errorMessage}}", + "deleteNameFailed": "Failed to delete name: {{errorMessage}}", "trainFailed": "Failed to train: {{errorMessage}}", "updateFaceScoreFailed": "Failed to update face score: {{errorMessage}}" } diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index 65c9ca8f1..30606808e 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -19,10 +19,9 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, + DropdownMenuSeparator, } from "@/components/ui/dropdown-menu"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { Toaster } from "@/components/ui/sonner"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { Tooltip, TooltipContent, @@ -174,16 +173,28 @@ export default function FaceLibrary() { ); const onDelete = useCallback( - (name: string, ids: string[]) => { + (name: string, ids: string[], isName: boolean = false) => { axios .post(`/faces/${name}/delete`, { ids }) .then((resp) => { setSelectedFaces([]); if (resp.status == 200) { - toast.success(t("toast.success.deletedFace"), { - position: "top-center", - }); + if (isName) { + toast.success( + t("toast.success.deletedName", { count: ids.length }), + { + position: "top-center", + }, + ); + } else { + toast.success( + t("toast.success.deletedFace", { count: ids.length }), + { + position: "top-center", + }, + ); + } if (faceImages.length == 1) { // face has been deleted @@ -198,9 +209,15 @@ export default function FaceLibrary() { error.response?.data?.message || error.response?.data?.detail || "Unknown error"; - toast.error(t("toast.error.deleteFaceFailed", { errorMessage }), { - position: "top-center", - }); + if (isName) { + toast.error(t("toast.error.deleteNameFailed", { errorMessage }), { + position: "top-center", + }); + } else { + toast.error(t("toast.error.deleteFaceFailed", { errorMessage }), { + position: "top-center", + }); + } }); }, [faceImages, refreshFaces, setPageToggle, t], @@ -258,6 +275,7 @@ export default function FaceLibrary() { faces={faces} trainImages={trainImages} setPageToggle={setPageToggle} + onDelete={onDelete} /> {selectedFaces?.length > 0 ? (
@@ -321,6 +339,7 @@ type LibrarySelectorProps = { faces: string[]; trainImages: string[]; setPageToggle: (toggle: string | undefined) => void; + onDelete: (name: string, ids: string[], isName: boolean) => void; }; function LibrarySelector({ pageToggle, @@ -328,79 +347,117 @@ function LibrarySelector({ faces, trainImages, setPageToggle, + onDelete, }: LibrarySelectorProps) { const { t } = useTranslation(["views/faceLibrary"]); + const [confirmDelete, setConfirmDelete] = useState(null); - return isDesktop ? ( - -
- { - if (value) { - setPageToggle(value); - } - }} + const handleDeleteFace = useCallback( + (faceName: string) => { + // Get all image IDs for this face + const imageIds = faceData?.[faceName] || []; + + onDelete(faceName, imageIds, true); + setPageToggle("train"); + }, + [faceData, onDelete, setPageToggle], + ); + + return ( + <> + !open && setConfirmDelete(null)} + > + + + {t("deleteFaceLibrary.title")} + + {t("deleteFaceLibrary.desc", { name: confirmDelete })} + + +
+ + +
+
+
+ + + + + + {trainImages.length > 0 && ( + setPageToggle("train")} + > +
{t("train.title")}
+
+ ({trainImages.length}) +
+
+ )} + {trainImages.length > 0 && faces.length > 0 && ( <> - -
{t("train.title")}
-
-
|
+ +
+ Collections +
)} - {Object.values(faces).map((face) => ( - -
- {face} ({faceData?.[face].length}) +
setPageToggle(face)} + > + {face} + + ({faceData?.[face].length}) +
- + + ))} - - -
- - ) : ( - - - - - - {trainImages.length > 0 && ( - setPageToggle("train")} - > -
{t("train.title")}
-
- )} - {Object.values(faces).map((face) => ( - setPageToggle(face)} - > - {face} ({faceData?.[face].length}) - - ))} -
-
+
+
+ ); }