Add wizard for adding new face to library

This commit is contained in:
Nicolas Mowen 2025-03-17 09:42:05 -06:00
parent b40421c1fb
commit 1d72516296
2 changed files with 170 additions and 36 deletions

View File

@ -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 (
<Overlay
open={open}
onOpenChange={(open) => {
if (!open) {
handleReset();
}
}}
>
<Content>
<Header>
<Title>Add New Face</Title>
<Description>
Walk through adding a new face to the Face Library.
</Description>
</Header>
<StepIndicator steps={STEPS} currentStep={step} />
{step == 0 && (
<TextEntry
placeholder="Enter Face Name"
onSave={(name) => {
setName(name);
setStep(1);
}}
>
<div className="flex justify-end py-2">
<Button variant="select" type="submit">
{t("button.save", { ns: "common" })}
</Button>
</div>
</TextEntry>
)}
{step == 1 && (
<ImageEntry onSave={onUploadImage}>
<div className="flex justify-end py-2">
<Button variant="select" type="submit">
{t("button.save", { ns: "common" })}
</Button>
</div>
</ImageEntry>
)}
{step == 2 && (
<div>
{name} has successfully been added to the Face Library!
<div className="text-s my-4 flex items-center text-secondary-foreground">
<Link
to="https://docs.frigate.video/configuration/face_recognition"
target="_blank"
rel="noopener noreferrer"
className="inline"
>
{t("classification.faceRecognition.readTheDocumentation", {
ns: "views/settings",
})}{" "}
to view more details on refining images for the Face Library
<LuExternalLink className="ml-2 inline-flex size-3" />
</Link>
</div>
<div className="flex justify-end">
<Button variant="select" onClick={() => handleReset()}>
Done
</Button>
</div>
</div>
)}
</Content>
</Overlay>
);
}

View File

@ -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<string[]>([]);
@ -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}
/>
<TextEntryDialog
title={t("createFaceLibrary.title")}
description={t("createFaceLibrary.desc")}
<CreateFaceWizardDialog
open={addFace}
setOpen={setAddFace}
onSave={onAddName}
onFinish={refreshFaces}
/>
<div className="relative mb-2 flex h-11 w-full items-center justify-between">