From 397d4e5b49f3cf9321cce23ec7947c9ee340a5f8 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 26 Nov 2025 08:47:09 -0700 Subject: [PATCH] Handle case where user doesn't have images that represent all states If a user selects all imags and can't proceed we show a warning that they can still proceed but the model won't be trained until they get at least one image for every state. --- .../locales/en/views/classificationModel.json | 6 +- .../wizard/Step3ChooseExamples.tsx | 136 +++++++++++++----- 2 files changed, 105 insertions(+), 37 deletions(-) diff --git a/web/public/locales/en/views/classificationModel.json b/web/public/locales/en/views/classificationModel.json index f8aef1b8f..6641b607e 100644 --- a/web/public/locales/en/views/classificationModel.json +++ b/web/public/locales/en/views/classificationModel.json @@ -173,7 +173,11 @@ "generationFailed": "Generation failed. Please try again.", "classifyFailed": "Failed to classify images: {{error}}" }, - "generateSuccess": "Successfully generated sample images" + "generateSuccess": "Successfully generated sample images", + "missingStatesWarning": { + "title": "Missing State Examples", + "description": "You haven't selected examples for all states. The model will not be trained until all states have images. After continuing, use the Recent Classifications view to classify images for the missing states, then train the model." + } } } } diff --git a/web/src/components/classification/wizard/Step3ChooseExamples.tsx b/web/src/components/classification/wizard/Step3ChooseExamples.tsx index 6e4311cec..bae697f6e 100644 --- a/web/src/components/classification/wizard/Step3ChooseExamples.tsx +++ b/web/src/components/classification/wizard/Step3ChooseExamples.tsx @@ -10,12 +10,8 @@ import useSWR from "swr"; import { baseUrl } from "@/api/baseUrl"; import { isMobile } from "react-device-detect"; import { cn } from "@/lib/utils"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { TooltipPortal } from "@radix-ui/react-tooltip"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { IoIosWarning } from "react-icons/io"; export type Step3FormData = { examplesGenerated: boolean; @@ -159,6 +155,19 @@ export default function Step3ChooseExamples({ const handleContinueClassification = useCallback(async () => { // Mark selected images with current class const newClassifications = { ...imageClassifications }; + + // Handle user going back and de-selecting images + const imagesToCheck = unknownImages.slice(0, 24); + imagesToCheck.forEach((imageName) => { + if ( + newClassifications[imageName] === currentClass && + !selectedImages.has(imageName) + ) { + delete newClassifications[imageName]; + } + }); + + // Then, add all currently selected images to the current class selectedImages.forEach((imageName) => { newClassifications[imageName] = currentClass; }); @@ -329,8 +338,43 @@ export default function Step3ChooseExamples({ return unclassifiedImages.length === 0; }, [unclassifiedImages]); - // For state models on the last class, require all images to be classified const isLastClass = currentClassIndex === allClasses.length - 1; + const statesWithExamples = useMemo(() => { + if (step1Data.modelType !== "state") return new Set(); + + const states = new Set(); + const allImages = unknownImages.slice(0, 24); + + // Check which states have at least one image classified + allImages.forEach((img) => { + let className: string | undefined; + if (selectedImages.has(img)) { + className = currentClass; + } else { + className = imageClassifications[img]; + } + if (className && allClasses.includes(className)) { + states.add(className); + } + }); + + return states; + }, [ + step1Data.modelType, + unknownImages, + imageClassifications, + selectedImages, + currentClass, + allClasses, + ]); + + const allStatesHaveExamples = useMemo(() => { + if (step1Data.modelType !== "state") return true; + return allClasses.every((className) => statesWithExamples.has(className)); + }, [step1Data.modelType, allClasses, statesWithExamples]); + + // For state models on the last class, require all images to be classified + // But allow proceeding even if not all states have examples (with warning) const canProceed = useMemo(() => { if (step1Data.modelType === "state" && isLastClass) { // Check if all 24 images will be classified after current selections are applied @@ -353,6 +397,28 @@ export default function Step3ChooseExamples({ selectedImages, ]); + const hasUnclassifiedImages = useMemo(() => { + if (!unknownImages) return false; + const allImages = unknownImages.slice(0, 24); + return allImages.some((img) => !imageClassifications[img]); + }, [unknownImages, imageClassifications]); + + const showMissingStatesWarning = useMemo(() => { + return ( + step1Data.modelType === "state" && + isLastClass && + !allStatesHaveExamples && + !hasUnclassifiedImages && + hasGenerated + ); + }, [ + step1Data.modelType, + isLastClass, + allStatesHaveExamples, + hasUnclassifiedImages, + hasGenerated, + ]); + const handleBack = useCallback(() => { if (currentClassIndex > 0) { const previousClass = allClasses[currentClassIndex - 1]; @@ -399,6 +465,17 @@ export default function Step3ChooseExamples({ ) : hasGenerated ? (
+ {showMissingStatesWarning && ( + + + + {t("wizard.step3.missingStatesWarning.title")} + + + {t("wizard.step3.missingStatesWarning.description")} + + + )} {!allImagesClassified && (

@@ -474,35 +551,22 @@ export default function Step3ChooseExamples({ - - - - - {!canProceed && ( - - - {t("wizard.step3.allImagesRequired", { - count: unclassifiedImages.length, - })} - - - )} - +

)}