From 1d725162966b01bef417bde938f43ce149257505 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 17 Mar 2025 09:42:05 -0600 Subject: [PATCH] Add wizard for adding new face to library --- .../overlay/detail/FaceCreateWizardDialog.tsx | 160 ++++++++++++++++++ web/src/pages/FaceLibrary.tsx | 46 ++--- 2 files changed, 170 insertions(+), 36 deletions(-) create mode 100644 web/src/components/overlay/detail/FaceCreateWizardDialog.tsx diff --git a/web/src/components/overlay/detail/FaceCreateWizardDialog.tsx b/web/src/components/overlay/detail/FaceCreateWizardDialog.tsx new file mode 100644 index 000000000..ebc62c1d3 --- /dev/null +++ b/web/src/components/overlay/detail/FaceCreateWizardDialog.tsx @@ -0,0 +1,160 @@ +import StepIndicator from "@/components/indicators/StepIndicator"; +import ImageEntry from "@/components/input/ImageEntry"; +import TextEntry from "@/components/input/TextEntry"; +import { + MobilePage, + MobilePageContent, + MobilePageDescription, + MobilePageHeader, + MobilePageTitle, +} from "@/components/mobile/MobilePage"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import axios from "axios"; +import { useCallback, useState } from "react"; +import { isDesktop } from "react-device-detect"; +import { useTranslation } from "react-i18next"; +import { LuExternalLink } from "react-icons/lu"; +import { Link } from "react-router-dom"; +import { toast } from "sonner"; + +const STEPS = ["Enter Face Name", "Upload Face Image", "Next Steps"]; + +type CreateFaceWizardDialogProps = { + open: boolean; + setOpen: (open: boolean) => void; + onFinish: () => void; +}; +export default function CreateFaceWizardDialog({ + open, + setOpen, +}: CreateFaceWizardDialogProps) { + const { t } = useTranslation("views/faceLibrary"); + + // wizard + + const [step, setStep] = useState(0); + const [name, setName] = useState(""); + + const handleReset = useCallback(() => { + setStep(0); + setName(""); + setOpen(false); + }, [setOpen]); + + // data handling + + const onUploadImage = useCallback( + (file: File) => { + const formData = new FormData(); + formData.append("file", file); + axios + .post(`faces/${name}/register`, formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then((resp) => { + if (resp.status == 200) { + setStep(2); + toast.success(t("toast.success.uploadedImage"), { + position: "top-center", + }); + } + }) + .catch((error) => { + const errorMessage = + error.response?.data?.message || + error.response?.data?.detail || + "Unknown error"; + toast.error(t("toast.error.uploadingImageFailed", { errorMessage }), { + position: "top-center", + }); + }); + }, + [name, t], + ); + + // layout + + const Overlay = isDesktop ? Dialog : MobilePage; + const Content = isDesktop ? DialogContent : MobilePageContent; + const Header = isDesktop ? DialogHeader : MobilePageHeader; + const Title = isDesktop ? DialogTitle : MobilePageTitle; + const Description = isDesktop ? DialogDescription : MobilePageDescription; + + return ( + { + if (!open) { + handleReset(); + } + }} + > + +
+ Add New Face + + Walk through adding a new face to the Face Library. + +
+ + {step == 0 && ( + { + setName(name); + setStep(1); + }} + > +
+ +
+
+ )} + {step == 1 && ( + +
+ +
+
+ )} + {step == 2 && ( +
+ {name} has successfully been added to the Face Library! +
+ + {t("classification.faceRecognition.readTheDocumentation", { + ns: "views/settings", + })}{" "} + to view more details on refining images for the Face Library + + +
+
+ +
+
+ )} +
+
+ ); +} diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx index 2ec769668..f1309ea5d 100644 --- a/web/src/pages/FaceLibrary.tsx +++ b/web/src/pages/FaceLibrary.tsx @@ -2,7 +2,7 @@ import { baseUrl } from "@/api/baseUrl"; import TimeAgo from "@/components/dynamic/TimeAgo"; import AddFaceIcon from "@/components/icons/AddFaceIcon"; import ActivityIndicator from "@/components/indicators/activity-indicator"; -import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; +import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog"; import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog"; import { Button } from "@/components/ui/button"; import { @@ -116,36 +116,6 @@ export default function FaceLibrary() { [pageToggle, refreshFaces, t], ); - const onAddName = useCallback( - (name: string) => { - axios - .post(`faces/${name}/create`, { - headers: { - "Content-Type": "multipart/form-data", - }, - }) - .then((resp) => { - if (resp.status == 200) { - setAddFace(false); - refreshFaces(); - toast.success(t("toast.success.addFaceLibrary"), { - position: "top-center", - }); - } - }) - .catch((error) => { - const errorMessage = - error.response?.data?.message || - error.response?.data?.detail || - "Unknown error"; - toast.error(t("toast.error.addFaceLibraryFailed", { errorMessage }), { - position: "top-center", - }); - }); - }, - [refreshFaces, t], - ); - // face multiselect const [selectedFaces, setSelectedFaces] = useState([]); @@ -183,6 +153,12 @@ export default function FaceLibrary() { toast.success(t("toast.success.deletedFace"), { position: "top-center", }); + + if (faceImages.length == 1) { + // face has been deleted + setPageToggle(""); + } + refreshFaces(); } }) @@ -195,7 +171,7 @@ export default function FaceLibrary() { position: "top-center", }); }); - }, [selectedFaces, refreshFaces, t]); + }, [faceImages, selectedFaces, refreshFaces, setPageToggle, t]); // keyboard @@ -229,12 +205,10 @@ export default function FaceLibrary() { onSave={onUploadImage} /> -