From fc46437ad608a806555bcf30d73a4ab531ce104b Mon Sep 17 00:00:00 2001 From: jeafreezy Date: Sat, 16 Nov 2024 03:04:36 +0000 Subject: [PATCH] chore: added validation to training settings input --- .../src/app/providers/models-provider.tsx | 59 +++++++++++++- .../src/app/routes/models/confirmation.tsx | 76 +++++++++++-------- .../src/app/routes/models/model-details.tsx | 1 + .../src/app/routes/models/models-list.tsx | 14 +++- .../components/progress-buttons.tsx | 14 ++-- .../training-settings-form.tsx | 33 ++++++-- .../dialogs/model-enhancement-dialog.tsx | 5 +- .../dialogs/training-settings-dialog.tsx | 48 ++++++++---- .../src/features/models/components/header.tsx | 28 ++++--- .../models/components/model-details-info.tsx | 4 +- frontend/src/utils/content.ts | 4 +- 11 files changed, 199 insertions(+), 87 deletions(-) diff --git a/frontend/src/app/providers/models-provider.tsx b/frontend/src/app/providers/models-provider.tsx index a3ab69f6..cb37ff3a 100644 --- a/frontend/src/app/providers/models-provider.tsx +++ b/frontend/src/app/providers/models-provider.tsx @@ -5,6 +5,7 @@ import { useSessionStorage } from "@/hooks/use-storage"; import { APPLICATION_ROUTES, HOT_FAIR_MODEL_CREATION_LOCAL_STORAGE_KEY, + MODEL_CREATION_CONTENT, showErrorToast, showSuccessToast, TMS_URL_REGEX_PATTERN, @@ -51,6 +52,9 @@ export enum MODEL_CREATION_FORM_NAME { OAM_TIME_NAME = "oamTileName", OAM_BOUNDS = "oamBounds", TRAINING_AREAS = "trainingAreas", + TRAINING_REQUEST_SUCCESS = 'trainingRequestIsSuccessful', + TRAINING_REQUEST_MESSAGE = 'trainingRequestMessage', + TRAINING_SETTINGS_IS_VALID = 'trainingSettingsIsValid' } export const FORM_VALIDATION_CONFIG = { @@ -150,6 +154,9 @@ type FormData = { batchSize: number; boundaryWidth: number; zoomLevels: number[]; + trainingRequestIsSuccessful: boolean + trainingRequestMessage: string + trainingSettingsIsValid: boolean }; const initialFormState: FormData = { @@ -177,6 +184,10 @@ const initialFormState: FormData = { batchSize: 8, boundaryWidth: 3, zoomLevels: [19, 20, 21], + trainingSettingsIsValid: true, + // Training requests response + trainingRequestIsSuccessful: true, + trainingRequestMessage: "" }; const ModelsContext = createContext<{ @@ -206,6 +217,7 @@ const ModelsContext = createContext<{ >; hasLabeledTrainingAreas: boolean; hasAOIsWithGeometry: boolean; + resetState: () => void }>({ formData: initialFormState, setFormData: () => { }, @@ -224,6 +236,7 @@ const ModelsContext = createContext<{ >, hasLabeledTrainingAreas: false, hasAOIsWithGeometry: false, + resetState: () => { } }); export const ModelsProvider: React.FC<{ @@ -265,13 +278,47 @@ export const ModelsProvider: React.FC<{ mutationConfig: { onSuccess: () => { showSuccessToast(TOAST_NOTIFICATIONS.trainingRequestSubmittedSuccess); + handleChange( + MODEL_CREATION_FORM_NAME.TRAINING_REQUEST_SUCCESS, + true + ); + handleChange( + MODEL_CREATION_FORM_NAME.TRAINING_REQUEST_MESSAGE, + MODEL_CREATION_CONTENT.confirmation.trainingRequestSuccess + ); // delay for a few seconds before resetting the state timeOutRef.current = setTimeout(() => { - setFormData(initialFormState); - }, 3000); + setFormData((prevFormData) => ({ + ...initialFormState, + // Preserve the training requests information because it's needed in the confirmation page. + trainingRequestMessage: prevFormData.trainingRequestMessage, + trainingRequestIsSuccessful: prevFormData.trainingRequestIsSuccessful, + })); + }, 2000); }, + onError: (error) => { showErrorToast(error); + // delay for a few seconds before resetting the state, but keep the data that will be needed for submitting training + // request incase the user wants to do that. + timeOutRef.current = setTimeout(() => { + setFormData((prevFormData) => ({ + ...initialFormState, + // Preserve the training requests information because it's needed in the confirmation page. + trainingRequestMessage: prevFormData.trainingRequestMessage, + trainingRequestIsSuccessful: prevFormData.trainingRequestIsSuccessful, + })); + }, 2000); + + handleChange( + MODEL_CREATION_FORM_NAME.TRAINING_REQUEST_SUCCESS, + false + ); + handleChange( + MODEL_CREATION_FORM_NAME.TRAINING_REQUEST_MESSAGE, + // @ts-expect-error bad type definition + `Your created model could not be trained because ${String(error?.response?.data[0]).toLocaleLowerCase()}. Click on the enhance button below to retrain your model.` + ); }, }, }); @@ -341,7 +388,9 @@ export const ModelsProvider: React.FC<{ .length === 0 ); }, [formData]); - + const resetState = () => { + setFormData(initialFormState) + } const memoizedValues = useMemo( () => ({ setFormData, @@ -350,7 +399,8 @@ export const ModelsProvider: React.FC<{ createNewModelMutation, hasLabeledTrainingAreas, hasAOIsWithGeometry, - formData + formData, + resetState }), [ setFormData, @@ -360,6 +410,7 @@ export const ModelsProvider: React.FC<{ createNewModelMutation, hasLabeledTrainingAreas, hasAOIsWithGeometry, + resetState ], ); diff --git a/frontend/src/app/routes/models/confirmation.tsx b/frontend/src/app/routes/models/confirmation.tsx index 1c94fc6c..f44a6c5c 100644 --- a/frontend/src/app/routes/models/confirmation.tsx +++ b/frontend/src/app/routes/models/confirmation.tsx @@ -1,49 +1,59 @@ +import { useModelsContext } from "@/app/providers/models-provider"; import ModelFormConfirmation from "@/assets/images/model_creation_success.png"; import { Button } from "@/components/ui/button"; import { Image } from "@/components/ui/image"; import { Link } from "@/components/ui/link"; +import ModelEnhancementDialog from "@/features/models/components/dialogs/model-enhancement-dialog"; +import { useDialog } from "@/hooks/use-dialog"; import { APPLICATION_ROUTES, MODEL_CREATION_CONTENT } from "@/utils"; import ConfettiExplosion from "react-confetti-explosion"; -import { useSearchParams } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; export const ModelConfirmationPage = () => { const [searchParams] = useSearchParams(); - const modelId = searchParams.get("id"); + const { formData } = useModelsContext(); + const { isOpened, openDialog, closeDialog } = useDialog(); + const navigate = useNavigate(); + + const handleClick = () => { + if (formData.trainingRequestIsSuccessful) { + navigate(`${APPLICATION_ROUTES.MODELS}/${modelId}`) + } else { + openDialog() + } + } return ( -
-
- - Model Creation Success Icon -

Model {modelId} is Created!

-

- {MODEL_CREATION_CONTENT.confirmation.description} -

-
- - - - - - + + + +
- + ); }; diff --git a/frontend/src/app/routes/models/model-details.tsx b/frontend/src/app/routes/models/model-details.tsx index dc413eaf..1b704eb3 100644 --- a/frontend/src/app/routes/models/model-details.tsx +++ b/frontend/src/app/routes/models/model-details.tsx @@ -57,6 +57,7 @@ export const ModelDetailsPage = () => { { const canClearAllFilters = Boolean( query[SEARCH_PARAMS.searchQuery] || - query[SEARCH_PARAMS.startDate] || - query[SEARCH_PARAMS.endDate] || - query[SEARCH_PARAMS.id], + query[SEARCH_PARAMS.startDate] || + query[SEARCH_PARAMS.endDate] || + query[SEARCH_PARAMS.id], ); return ( @@ -334,7 +335,12 @@ export const ModelsPage = () => { disabled={isPending} />
- + {/* + Providing access to the models context, so that the 'create model' button can reset the store before going to the model creation form. + */} + + +
diff --git a/frontend/src/features/model-creation/components/progress-buttons.tsx b/frontend/src/features/model-creation/components/progress-buttons.tsx index 32a69940..37f5aa9b 100644 --- a/frontend/src/features/model-creation/components/progress-buttons.tsx +++ b/frontend/src/features/model-creation/components/progress-buttons.tsx @@ -102,11 +102,11 @@ const ProgressButtons: React.FC = ({ case APPLICATION_ROUTES.CREATE_NEW_MODEL: return ( formData.modelName.length >= - FORM_VALIDATION_CONFIG[MODEL_CREATION_FORM_NAME.MODEL_NAME] - .minLength && + FORM_VALIDATION_CONFIG[MODEL_CREATION_FORM_NAME.MODEL_NAME] + .minLength && formData.modelDescription.length >= - FORM_VALIDATION_CONFIG[MODEL_CREATION_FORM_NAME.MODEL_DESCRIPTION] - .minLength + FORM_VALIDATION_CONFIG[MODEL_CREATION_FORM_NAME.MODEL_DESCRIPTION] + .minLength ); case APPLICATION_ROUTES.CREATE_NEW_MODEL_TRAINING_DATASET: @@ -126,8 +126,8 @@ const ProgressButtons: React.FC = ({ return ( formData.tmsURLValidation.valid && formData.datasetName.length >= - FORM_VALIDATION_CONFIG[MODEL_CREATION_FORM_NAME.DATASET_NAME] - .minLength + FORM_VALIDATION_CONFIG[MODEL_CREATION_FORM_NAME.DATASET_NAME] + .minLength ); } else if ( formData.trainingDatasetOption === TrainingDatasetOption.USE_EXISTING @@ -139,7 +139,7 @@ const ProgressButtons: React.FC = ({ } case APPLICATION_ROUTES.CREATE_NEW_MODEL_TRAINING_SETTINGS: // confirm that the user has selected at least an option - return formData.zoomLevels.length > 0; + return formData.zoomLevels.length > 0 && formData.trainingSettingsIsValid; case APPLICATION_ROUTES.CREATE_NEW_MODEL_TRAINING_AREA: return ( hasLabeledTrainingAreas && hasAOIsWithGeometry && formData.oamBounds diff --git a/frontend/src/features/model-creation/components/training-settings/training-settings-form.tsx b/frontend/src/features/model-creation/components/training-settings/training-settings-form.tsx index 49a6cd19..44cab9b1 100644 --- a/frontend/src/features/model-creation/components/training-settings/training-settings-form.tsx +++ b/frontend/src/features/model-creation/components/training-settings/training-settings-form.tsx @@ -27,7 +27,7 @@ const trainingTypes = [ const TrainingSettingsForm = () => { const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); - + const [validationMessage, setValidationMessage] = useState('Hellow') const { formData, handleChange } = useModelsContext(); const advancedSettings = [ @@ -182,7 +182,7 @@ const TrainingSettingsForm = () => { />
- {showAdvancedSettings && ( + {showAdvancedSettings && (<>
{advancedSettings.filter(setting => setting.enabled).map((setting, id) => (
@@ -202,15 +202,38 @@ const TrainingSettingsForm = () => { // @ts-expect-error bad type definition FORM_VALIDATION_CONFIG[formData.baseModel][setting.value].max } - handleInput={(e) => - handleChange(setting.value, Number(e.target.value)) - } + handleInput={(e) => { + const inputValue = Number(e.target.value); + + const min = + // @ts-expect-error bad type definition + FORM_VALIDATION_CONFIG[formData.baseModel][setting.value].min; + const max = + // @ts-expect-error bad type definition + FORM_VALIDATION_CONFIG[formData.baseModel][setting.value].max; + handleChange(setting.value, inputValue); + if (inputValue < min || inputValue > max) { + // Set validation message for out-of-range values + setValidationMessage( + `${setting.label} must be between ${min} and ${max}.` + ); + handleChange(MODEL_CREATION_FORM_NAME.TRAINING_SETTINGS_IS_VALID, false) + } else { + // Clear the validation message if the value is valid + setValidationMessage(""); + handleChange(setting.value, inputValue); + handleChange(MODEL_CREATION_FORM_NAME.TRAINING_SETTINGS_IS_VALID, true) + } + }} toolTipContent={setting.toolTip} />
))}
+

{validationMessage}

+ )} +
); diff --git a/frontend/src/features/models/components/dialogs/model-enhancement-dialog.tsx b/frontend/src/features/models/components/dialogs/model-enhancement-dialog.tsx index 464740ea..b2ea83d1 100644 --- a/frontend/src/features/models/components/dialogs/model-enhancement-dialog.tsx +++ b/frontend/src/features/models/components/dialogs/model-enhancement-dialog.tsx @@ -7,10 +7,11 @@ import ModelTrainingSettingsDialog from "./training-settings-dialog"; type ModelEnhancementDialogProps = { isOpened: boolean; closeDialog: () => void; + modelId: string }; const ModelEnhancementDialog: React.FC = ({ isOpened, - closeDialog, + closeDialog, modelId }) => { const { isOpened: isTrainingSettingsDialogOpened, openDialog, closeDialog: closeTrainingSettingsDialog } = useDialog(); @@ -43,7 +44,7 @@ const ModelEnhancementDialog: React.FC = ({ closeDialog={closeDialog} label={APP_CONTENT.models.modelsDetailsCard.modelUpdate.dialogHeading} > - +
    {options.map((option, id) => (
  • void; + modelId: string }; const ModelTrainingSettingsDialog: React.FC = ({ isOpened, closeDialog, + modelId }) => { - const { isLaptop, isMobile, isTablet } = useScreenSize() + const { isLaptop, isMobile, isTablet } = useScreenSize(); + // get model details with the id + const { data, isPending, isError } = useModelDetails(modelId); + + // Training enhancement can only happen in two places + // When the user is on the confirmation page or on the model card page. + // In either page, after successful enhancement, redirect the user to the model card page to see the training settings. + // If they're on the confirmation page, the redirection will be obvious, but it won't on the model card, since they're on the page already. + // If the training is not successful, they'll remain on their current page, so they can try again. return ( = ({ label={APP_CONTENT.models.modelsDetailsCard.trainingSettings.dialogHeading} size={isMobile || isTablet ? SHOELACE_SIZES.EXTRA_LARGE : isLaptop ? SHOELACE_SIZES.LARGE : SHOELACE_SIZES.MEDIUM} > -
    -

    {APP_CONTENT.models.modelsDetailsCard.trainingSettings.description}

    -

    Model Name O

    - -
    - -
    -
    + { + isError ?

    Error retrieving model details

    : + isPending ?
    : +
    +

    {APP_CONTENT.models.modelsDetailsCard.trainingSettings.description}

    +

    {data.name}

    + +
    + +
    +
    + } +
    ); diff --git a/frontend/src/features/models/components/header.tsx b/frontend/src/features/models/components/header.tsx index b3de688e..a279c3b3 100644 --- a/frontend/src/features/models/components/header.tsx +++ b/frontend/src/features/models/components/header.tsx @@ -1,9 +1,18 @@ +import { useModelsContext } from "@/app/providers/models-provider"; import { ButtonWithIcon } from "@/components/ui/button"; import { AddIcon } from "@/components/ui/icons"; -import { Link } from "@/components/ui/link"; import { APP_CONTENT, APPLICATION_ROUTES } from "@/utils"; +import { useNavigate } from "react-router-dom"; const PageHeader = () => { + const { resetState } = useModelsContext(); + + const navigate = useNavigate() + const handleClick = () => { + resetState() + navigate(APPLICATION_ROUTES.CREATE_NEW_MODEL) + } + return (
    @@ -16,17 +25,12 @@ const PageHeader = () => { {APP_CONTENT.models.modelsList.description}

    - - - +
    diff --git a/frontend/src/features/models/components/model-details-info.tsx b/frontend/src/features/models/components/model-details-info.tsx index 14cbe058..c111b76a 100644 --- a/frontend/src/features/models/components/model-details-info.tsx +++ b/frontend/src/features/models/components/model-details-info.tsx @@ -99,12 +99,12 @@ const ModelDetailsInfo = ({ {isPending ?

    : isError ? Error retrieving dataset info :

    {truncateString(trainingDataset?.name, 30)}

    } -

    +

    {APP_CONTENT.models.modelsDetailsCard.datasetId}

    {data?.dataset}

    -

    +