Reclassification (#22603)

* add ability to reclassify images

* add ability to reclassify faces

* work around radix pointer events issue again
This commit is contained in:
Josh Hawkins
2026-03-24 07:18:06 -06:00
committed by GitHub
parent 91ef3b2ceb
commit 854ef320de
7 changed files with 324 additions and 13 deletions
@@ -34,7 +34,11 @@ type ClassificationSelectionDialogProps = {
classes: string[];
modelName: string;
image: string;
onRefresh: () => void;
onRefresh?: () => void;
onCategorize?: (category: string) => void;
excludeCategory?: string;
dialogLabel?: string;
tooltipLabel?: string;
children: ReactNode;
};
export default function ClassificationSelectionDialog({
@@ -43,12 +47,21 @@ export default function ClassificationSelectionDialog({
modelName,
image,
onRefresh,
onCategorize,
excludeCategory,
dialogLabel,
tooltipLabel,
children,
}: ClassificationSelectionDialogProps) {
const { t } = useTranslation(["views/classificationModel"]);
const onCategorizeImage = useCallback(
(category: string) => {
if (onCategorize) {
onCategorize(category);
return;
}
axios
.post(`/classification/${modelName}/dataset/categorize`, {
category,
@@ -59,7 +72,7 @@ export default function ClassificationSelectionDialog({
toast.success(t("toast.success.categorizedImage"), {
position: "top-center",
});
onRefresh();
onRefresh?.();
}
})
.catch((error) => {
@@ -72,7 +85,13 @@ export default function ClassificationSelectionDialog({
});
});
},
[modelName, image, onRefresh, t],
[modelName, image, onRefresh, onCategorize, t],
);
const filteredClasses = useMemo(
() =>
excludeCategory ? classes.filter((c) => c !== excludeCategory) : classes,
[classes, excludeCategory],
);
const isChildButton = useMemo(
@@ -111,6 +130,7 @@ export default function ClassificationSelectionDialog({
</SelectorTrigger>
<SelectorContent
className={cn("", isMobile && "mx-1 gap-2 rounded-t-2xl px-4")}
onCloseAutoFocus={(e) => e.preventDefault()}
>
{isMobile && (
<DrawerHeader className="sr-only">
@@ -118,14 +138,16 @@ export default function ClassificationSelectionDialog({
<DrawerDescription>Details</DrawerDescription>
</DrawerHeader>
)}
<DropdownMenuLabel>{t("categorizeImageAs")}</DropdownMenuLabel>
<DropdownMenuLabel>
{dialogLabel ?? t("categorizeImageAs")}
</DropdownMenuLabel>
<div
className={cn(
"flex max-h-[40dvh] flex-col overflow-y-auto",
isMobile && "gap-2 pb-4",
)}
>
{classes
{filteredClasses
.sort((a, b) => {
if (a === "none") return 1;
if (b === "none") return -1;
@@ -152,7 +174,7 @@ export default function ClassificationSelectionDialog({
</div>
</SelectorContent>
</Selector>
<TooltipContent>{t("categorizeImage")}</TooltipContent>
<TooltipContent>{tooltipLabel ?? t("categorizeImage")}</TooltipContent>
</Tooltip>
</div>
);
@@ -30,17 +30,29 @@ import { Button } from "../ui/button";
type FaceSelectionDialogProps = {
className?: string;
faceNames: string[];
excludeName?: string;
dialogLabel?: string;
tooltipLabel?: string;
onTrainAttempt: (name: string) => void;
children: ReactNode;
};
export default function FaceSelectionDialog({
className,
faceNames,
excludeName,
dialogLabel,
tooltipLabel,
onTrainAttempt,
children,
}: FaceSelectionDialogProps) {
const { t } = useTranslation(["views/faceLibrary"]);
const filteredNames = useMemo(
() =>
excludeName ? faceNames.filter((n) => n !== excludeName) : faceNames,
[faceNames, excludeName],
);
const isChildButton = useMemo(
() => React.isValidElement(children) && children.type === Button,
[children],
@@ -79,6 +91,7 @@ export default function FaceSelectionDialog({
</SelectorTrigger>
<SelectorContent
className={cn("", isMobile && "mx-1 gap-2 rounded-t-2xl px-4")}
onCloseAutoFocus={(e) => e.preventDefault()}
>
{isMobile && (
<DrawerHeader className="sr-only">
@@ -86,14 +99,16 @@ export default function FaceSelectionDialog({
<DrawerDescription>Details</DrawerDescription>
</DrawerHeader>
)}
<DropdownMenuLabel>{t("trainFaceAs")}</DropdownMenuLabel>
<DropdownMenuLabel>
{dialogLabel ?? t("trainFaceAs")}
</DropdownMenuLabel>
<div
className={cn(
"flex max-h-[40dvh] flex-col overflow-y-auto overflow-x-hidden",
isMobile && "gap-2 pb-4",
)}
>
{faceNames.sort().map((faceName) => (
{filteredNames.sort().map((faceName) => (
<SelectorItem
key={faceName}
className="flex cursor-pointer gap-2 smart-capitalize"
@@ -112,7 +127,7 @@ export default function FaceSelectionDialog({
</div>
</SelectorContent>
</Selector>
<TooltipContent>{t("trainFace")}</TooltipContent>
<TooltipContent>{tooltipLabel ?? t("trainFace")}</TooltipContent>
</Tooltip>
</div>
);