diff --git a/web/src/components/overlay/FaceSelectionDialog.tsx b/web/src/components/overlay/FaceSelectionDialog.tsx
new file mode 100644
index 0000000000..4e9c644b83
--- /dev/null
+++ b/web/src/components/overlay/FaceSelectionDialog.tsx
@@ -0,0 +1,108 @@
+import {
+ Drawer,
+ DrawerClose,
+ DrawerContent,
+ DrawerTrigger,
+} from "@/components/ui/drawer";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { isDesktop, isMobile } from "react-device-detect";
+import { LuPlus, LuScanFace } from "react-icons/lu";
+import { useTranslation } from "react-i18next";
+import { cn } from "@/lib/utils";
+import React, { ReactNode, useMemo, useState } from "react";
+import TextEntryDialog from "./dialog/TextEntryDialog";
+import { Button } from "../ui/button";
+
+type FaceSelectionDialogProps = {
+ className?: string;
+ faceNames: string[];
+ onTrainAttempt: (name: string) => void;
+ children: ReactNode;
+};
+export default function FaceSelectionDialog({
+ className,
+ faceNames,
+ onTrainAttempt,
+ children,
+}: FaceSelectionDialogProps) {
+ const { t } = useTranslation(["views/faceLibrary"]);
+
+ const isChildButton = useMemo(
+ () => React.isValidElement(children) && children.type === Button,
+ [children],
+ );
+
+ // control
+ const [newFace, setNewFace] = useState(false);
+
+ // components
+ const Selector = isDesktop ? DropdownMenu : Drawer;
+ const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
+ const SelectorContent = isDesktop ? DropdownMenuContent : DrawerContent;
+ const SelectorItem = isDesktop ? DropdownMenuItem : DrawerClose;
+
+ return (
+
+ {newFace && (
+
onTrainAttempt(newName)}
+ />
+ )}
+
+
+
+
+ {children}
+
+
+ {t("trainFaceAs")}
+
+ setNewFace(true)}
+ >
+
+ {t("createFaceLibrary.new")}
+
+ {faceNames.map((faceName) => (
+ onTrainAttempt(faceName)}
+ >
+
+ {faceName}
+
+ ))}
+
+
+
+ {t("trainFace")}
+
+
+ );
+}
diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx
index 6762890d4a..afd52b6296 100644
--- a/web/src/components/overlay/detail/SearchDetailDialog.tsx
+++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx
@@ -57,7 +57,6 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
- DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
@@ -77,6 +76,7 @@ import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
import { useTranslation } from "react-i18next";
import { TbFaceId } from "react-icons/tb";
import { useIsAdmin } from "@/hooks/use-is-admin";
+import FaceSelectionDialog from "../FaceSelectionDialog";
const SEARCH_TABS = [
"details",
@@ -844,30 +844,18 @@ function ObjectDetailsTab({
)}
{hasFace && (
-
-
-
-
-
-
- {t("trainFaceAs", { ns: "views/faceLibrary" })}
-
- {faceNames.map((faceName) => (
- onTrainFace(faceName)}
- >
- {faceName}
-
- ))}
-
-
+
+
+
)}
diff --git a/web/src/pages/FaceLibrary.tsx b/web/src/pages/FaceLibrary.tsx
index 4c7f59b83c..0057db094a 100644
--- a/web/src/pages/FaceLibrary.tsx
+++ b/web/src/pages/FaceLibrary.tsx
@@ -5,6 +5,7 @@ import ActivityIndicator from "@/components/indicators/activity-indicator";
import CreateFaceWizardDialog from "@/components/overlay/detail/FaceCreateWizardDialog";
import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog";
import UploadImageDialog from "@/components/overlay/dialog/UploadImageDialog";
+import FaceSelectionDialog from "@/components/overlay/FaceSelectionDialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -13,17 +14,10 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerTrigger,
-} from "@/components/ui/drawer";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
- DropdownMenuLabel,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
@@ -48,7 +42,6 @@ import { isDesktop, isMobile } from "react-device-detect";
import { useTranslation } from "react-i18next";
import {
LuImagePlus,
- LuPlus,
LuRefreshCw,
LuScanFace,
LuSearch,
@@ -789,8 +782,6 @@ function FaceAttempt({
// interaction
- const [newFace, setNewFace] = useState(false);
-
const imgRef = useRef(null);
useContextMenu(imgRef, () => {
@@ -848,22 +839,8 @@ function FaceAttempt({
});
}, [data, onRefresh, t]);
- const Selector = isDesktop ? DropdownMenu : Drawer;
- const SelectorTrigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
- const SelectorContent = isDesktop ? DropdownMenuContent : DrawerContent;
- const SelectorItem = isDesktop ? DropdownMenuItem : DrawerClose;
-
return (
<>
- {newFace && (
- onTrainAttempt(newName)}
- />
- )}
-
-
-
-
-
-
-
-
-
- {t("trainFaceAs")}
-
- setNewFace(true)}
- >
-
- {t("createFaceLibrary.new")}
-
- {faceNames.map((faceName) => (
- onTrainAttempt(faceName)}
- >
-
- {faceName}
-
- ))}
-
-
-
- {t("trainFace")}
-
+
+
+