diff --git a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useControlNetOrT2IAdapterDefaultSettings.ts b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useControlNetOrT2IAdapterDefaultSettings.ts index 826bec17b1..693cf3cf52 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useControlNetOrT2IAdapterDefaultSettings.ts +++ b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useControlNetOrT2IAdapterDefaultSettings.ts @@ -1,15 +1,10 @@ -import { skipToken } from '@reduxjs/toolkit/query'; import { isNil } from 'lodash-es'; import { useMemo } from 'react'; -import { useGetModelConfigWithTypeGuard } from 'services/api/hooks/useGetModelConfigWithTypeGuard'; -import { isControlNetOrT2IAdapterModelConfig } from 'services/api/types'; - -export const useControlNetOrT2IAdapterDefaultSettings = (modelKey?: string | null) => { - const { modelConfig, isLoading } = useGetModelConfigWithTypeGuard( - modelKey ?? skipToken, - isControlNetOrT2IAdapterModelConfig - ); +import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; +export const useControlNetOrT2IAdapterDefaultSettings = ( + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig +) => { const defaultSettingsDefaults = useMemo(() => { return { preprocessor: { @@ -19,5 +14,5 @@ export const useControlNetOrT2IAdapterDefaultSettings = (modelKey?: string | nul }; }, [modelConfig?.default_settings]); - return { defaultSettingsDefaults, isLoading }; + return defaultSettingsDefaults; }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useMainModelDefaultSettings.ts b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useMainModelDefaultSettings.ts index 6de99673e4..55ee40ada5 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/hooks/useMainModelDefaultSettings.ts +++ b/invokeai/frontend/web/src/features/modelManagerV2/hooks/useMainModelDefaultSettings.ts @@ -1,12 +1,9 @@ -import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { selectConfigSlice } from 'features/system/store/configSlice'; import { isNil } from 'lodash-es'; import { useMemo } from 'react'; -import { useGetModelConfigWithTypeGuard } from 'services/api/hooks/useGetModelConfigWithTypeGuard'; -import { isNonRefinerMainModelConfig } from 'services/api/types'; +import type { MainModelConfig } from 'services/api/types'; const initialStatesSelector = createMemoizedSelector(selectConfigSlice, (config) => { const { steps, guidance, scheduler, cfgRescaleMultiplier, vaePrecision, width, height } = config.sd; @@ -22,9 +19,7 @@ const initialStatesSelector = createMemoizedSelector(selectConfigSlice, (config) }; }); -export const useMainModelDefaultSettings = (modelKey?: string | null) => { - const { modelConfig, isLoading } = useGetModelConfigWithTypeGuard(modelKey ?? skipToken, isNonRefinerMainModelConfig); - +export const useMainModelDefaultSettings = (modelConfig: MainModelConfig) => { const { initialSteps, initialCfg, @@ -81,5 +76,5 @@ export const useMainModelDefaultSettings = (modelKey?: string | null) => { initialHeight, ]); - return { defaultSettingsDefaults, isLoading, optimalDimension: getOptimalDimension(modelConfig) }; + return defaultSettingsDefaults; }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts index c637d30fd8..8a7e3a7aa8 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts +++ b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts @@ -1,6 +1,6 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import type { PersistConfig } from 'app/store/store'; +import type { PersistConfig, RootState } from 'app/store/store'; import type { ModelType } from 'services/api/types'; export type FilterableModelType = Exclude | 'refiner'; @@ -50,6 +50,8 @@ export const modelManagerV2Slice = createSlice({ export const { setSelectedModelKey, setSearchTerm, setFilteredModelType, setSelectedModelMode, setScanPath } = modelManagerV2Slice.actions; +export const selectModelManagerV2Slice = (state: RootState) => state.modelmanagerV2; + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateModelManagerState = (state: any): any => { if (!('_version' in state)) { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx index b82917221e..755a6e21fb 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx @@ -21,7 +21,8 @@ import { FetchingModelsLoader } from './FetchingModelsLoader'; import { ModelListWrapper } from './ModelListWrapper'; const ModelList = () => { - const { searchTerm, filteredModelType } = useAppSelector((s) => s.modelmanagerV2); + const filteredModelType = useAppSelector((s) => s.modelmanagerV2.filteredModelType); + const searchTerm = useAppSelector((s) => s.modelmanagerV2.searchTerm); const { t } = useTranslation(); const [mainModels, { isLoading: isLoadingMainModels }] = useMainModels(); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx index a4a6d5c833..8bfcbd7351 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListItem.tsx @@ -1,7 +1,8 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { ConfirmationAlertDialog, Flex, IconButton, Spacer, Text, useDisclosure } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { selectModelManagerV2Slice, setSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; import ModelBaseBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelBaseBadge'; import ModelFormatBadge from 'features/modelManagerV2/subpanels/ModelManagerPanel/ModelFormatBadge'; import { toast } from 'features/toast/toast'; @@ -23,15 +24,21 @@ const sx: SystemStyleObject = { "&[aria-selected='true']": { bg: 'base.700' }, }; -const ModelListItem = (props: ModelListItemProps) => { +const ModelListItem = ({ model }: ModelListItemProps) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const selectIsSelected = useMemo( + () => + createSelector( + selectModelManagerV2Slice, + (modelManagerV2Slice) => modelManagerV2Slice.selectedModelKey === model.key + ), + [model.key] + ); + const isSelected = useAppSelector(selectIsSelected); const [deleteModel] = useDeleteModelsMutation(); const { isOpen, onOpen, onClose } = useDisclosure(); - const { model } = props; - const handleSelectModel = useCallback(() => { dispatch(setSelectedModelKey(model.key)); }, [model.key, dispatch]); @@ -43,11 +50,6 @@ const ModelListItem = (props: ModelListItemProps) => { }, [onOpen] ); - - const isSelected = useMemo(() => { - return selectedModelKey === model.key; - }, [selectedModelKey, model.key]); - const handleModelDelete = useCallback(() => { deleteModel({ key: model.key }) .unwrap() diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings.tsx index 9a84fbc726..37fb17f8e4 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/ControlNetOrT2IAdapterDefaultSettings.tsx @@ -1,5 +1,4 @@ -import { Button, Flex, Heading, SimpleGrid, Text } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; +import { Button, Flex, Heading, SimpleGrid } from '@invoke-ai/ui-library'; import { useControlNetOrT2IAdapterDefaultSettings } from 'features/modelManagerV2/hooks/useControlNetOrT2IAdapterDefaultSettings'; import { DefaultPreprocessor } from 'features/modelManagerV2/subpanels/ModelPanel/ControlNetOrT2IAdapterDefaultSettings/DefaultPreprocessor'; import type { FormField } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings'; @@ -10,17 +9,20 @@ import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { PiCheckBold } from 'react-icons/pi'; import { useUpdateModelMutation } from 'services/api/endpoints/models'; +import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types'; export type ControlNetOrT2IAdapterDefaultSettingsFormData = { preprocessor: FormField; }; -export const ControlNetOrT2IAdapterDefaultSettings = () => { - const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); +type Props = { + modelConfig: ControlNetModelConfig | T2IAdapterModelConfig; +}; + +export const ControlNetOrT2IAdapterDefaultSettings = ({ modelConfig }: Props) => { const { t } = useTranslation(); - const { defaultSettingsDefaults, isLoading: isLoadingDefaultSettings } = - useControlNetOrT2IAdapterDefaultSettings(selectedModelKey); + const defaultSettingsDefaults = useControlNetOrT2IAdapterDefaultSettings(modelConfig); const [updateModel, { isLoading: isLoadingUpdateModel }] = useUpdateModelMutation(); @@ -30,16 +32,12 @@ export const ControlNetOrT2IAdapterDefaultSettings = () => { const onSubmit = useCallback>( (data) => { - if (!selectedModelKey) { - return; - } - const body = { preprocessor: data.preprocessor.isEnabled ? data.preprocessor.value : null, }; updateModel({ - key: selectedModelKey, + key: modelConfig.key, body: { default_settings: body }, }) .unwrap() @@ -61,13 +59,9 @@ export const ControlNetOrT2IAdapterDefaultSettings = () => { } }); }, - [selectedModelKey, reset, updateModel, t] + [updateModel, modelConfig.key, t, reset] ); - if (isLoadingDefaultSettings) { - return {t('common.loading')}; - } - return ( <> diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx index 233fc7bc6b..3ec7a98a1f 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx @@ -1,16 +1,18 @@ -import { Button, Flex, Heading, SimpleGrid, Text } from '@invoke-ai/ui-library'; +import { Button, Flex, Heading, SimpleGrid } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useMainModelDefaultSettings } from 'features/modelManagerV2/hooks/useMainModelDefaultSettings'; import { DefaultHeight } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight'; import { DefaultWidth } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth'; import type { ParameterScheduler } from 'features/parameters/types/parameterSchemas'; +import { getOptimalDimension } from 'features/parameters/util/optimalDimension'; import { toast } from 'features/toast/toast'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { PiCheckBold } from 'react-icons/pi'; import { useUpdateModelMutation } from 'services/api/endpoints/models'; +import type { MainModelConfig } from 'services/api/types'; import { DefaultCfgRescaleMultiplier } from './DefaultCfgRescaleMultiplier'; import { DefaultCfgScale } from './DefaultCfgScale'; @@ -35,16 +37,16 @@ export type MainModelDefaultSettingsFormData = { height: FormField; }; -export const MainModelDefaultSettings = () => { +type Props = { + modelConfig: MainModelConfig; +}; + +export const MainModelDefaultSettings = ({ modelConfig }: Props) => { const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); const { t } = useTranslation(); - const { - defaultSettingsDefaults, - isLoading: isLoadingDefaultSettings, - optimalDimension, - } = useMainModelDefaultSettings(selectedModelKey); - + const defaultSettingsDefaults = useMainModelDefaultSettings(modelConfig); + const optimalDimension = useMemo(() => getOptimalDimension(modelConfig), [modelConfig]); const [updateModel, { isLoading: isLoadingUpdateModel }] = useUpdateModelMutation(); const { handleSubmit, control, formState, reset } = useForm({ @@ -94,10 +96,6 @@ export const MainModelDefaultSettings = () => { [selectedModelKey, reset, updateModel, t] ); - if (isLoadingDefaultSettings) { - return {t('common.loading')}; - } - return ( <> diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx index fa7ca4c394..c5bbe940e4 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx @@ -1,19 +1,10 @@ -import { Button, Flex, Heading, Spacer, Text } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice'; -import { ModelConvertButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton'; -import { ModelEditButton } from 'features/modelManagerV2/subpanels/ModelPanel/ModelEditButton'; -import { toast } from 'features/toast/toast'; -import { useCallback } from 'react'; -import type { SubmitHandler } from 'react-hook-form'; -import { useForm } from 'react-hook-form'; +import { useAppSelector } from 'app/store/storeHooks'; +import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiCheckBold, PiXBold } from 'react-icons/pi'; -import type { UpdateModelArg } from 'services/api/endpoints/models'; -import { useGetModelConfigQuery, useUpdateModelMutation } from 'services/api/endpoints/models'; +import { PiExclamationMarkBold } from 'react-icons/pi'; +import { modelConfigsAdapterSelectors, useGetModelConfigsQuery } from 'services/api/endpoints/models'; -import ModelImageUpload from './Fields/ModelImageUpload'; import { ModelEdit } from './ModelEdit'; import { ModelView } from './ModelView'; @@ -21,100 +12,34 @@ export const Model = () => { const { t } = useTranslation(); const selectedModelMode = useAppSelector((s) => s.modelmanagerV2.selectedModelMode); const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); - const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken); - const [updateModel, { isLoading: isSubmitting }] = useUpdateModelMutation(); - const dispatch = useAppDispatch(); + const { data: modelConfigs, isLoading } = useGetModelConfigsQuery(); + const modelConfig = useMemo(() => { + if (!modelConfigs) { + return null; + } + if (selectedModelKey === null) { + return null; + } + const modelConfig = modelConfigsAdapterSelectors.selectById(modelConfigs, selectedModelKey); - const form = useForm({ - defaultValues: data, - mode: 'onChange', - }); + if (!modelConfig) { + return null; + } - const onSubmit = useCallback>( - (values) => { - if (!data?.key) { - return; - } - - const responseBody: UpdateModelArg = { - key: data.key, - body: values, - }; - - updateModel(responseBody) - .unwrap() - .then((payload) => { - form.reset(payload, { keepDefaultValues: true }); - dispatch(setSelectedModelMode('view')); - toast({ - id: 'MODEL_UPDATED', - title: t('modelManager.modelUpdated'), - status: 'success', - }); - }) - .catch((_) => { - form.reset(); - toast({ - id: 'MODEL_UPDATE_FAILED', - title: t('modelManager.modelUpdateFailed'), - status: 'error', - }); - }); - }, - [dispatch, data?.key, form, t, updateModel] - ); - - const handleClickCancel = useCallback(() => { - dispatch(setSelectedModelMode('view')); - }, [dispatch]); + return modelConfig; + }, [modelConfigs, selectedModelKey]); if (isLoading) { - return {t('common.loading')}; + return ; } - if (!data) { - return {t('common.somethingWentWrong')}; + if (!modelConfig) { + return ; } - return ( - - - - - - - {data.name} - - - {selectedModelMode === 'view' && } - {selectedModelMode === 'view' && } - {selectedModelMode === 'edit' && ( - - )} - {selectedModelMode === 'edit' && ( - - )} - - {data.source && ( - - {t('modelManager.source')}: {data?.source} - - )} - {data.description} - - - {selectedModelMode === 'view' ? : } - - ); + if (selectedModelMode === 'view') { + return ; + } + + return ; }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx index 40ffca76b4..bb99fcff8b 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelConvertButton.tsx @@ -8,52 +8,46 @@ import { UnorderedList, useDisclosure, } from '@invoke-ai/ui-library'; -import { skipToken } from '@reduxjs/toolkit/query'; import { toast } from 'features/toast/toast'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { useConvertModelMutation, useGetModelConfigQuery } from 'services/api/endpoints/models'; +import { useConvertModelMutation } from 'services/api/endpoints/models'; +import type { CheckpointModelConfig } from 'services/api/types'; interface ModelConvertProps { - modelKey: string | null; + modelConfig: CheckpointModelConfig; } -export const ModelConvertButton = (props: ModelConvertProps) => { - const { modelKey } = props; +export const ModelConvertButton = ({ modelConfig }: ModelConvertProps) => { const { t } = useTranslation(); - const { data } = useGetModelConfigQuery(modelKey ?? skipToken); const [convertModel, { isLoading }] = useConvertModelMutation(); const { isOpen, onOpen, onClose } = useDisclosure(); const modelConvertHandler = useCallback(() => { - if (!data || isLoading) { + if (!modelConfig || isLoading) { return; } - const toastId = `CONVERTING_MODEL_${data.key}`; + const toastId = `CONVERTING_MODEL_${modelConfig.key}`; toast({ id: toastId, - title: `${t('modelManager.convertingModelBegin')}: ${data?.name}`, + title: `${t('modelManager.convertingModelBegin')}: ${modelConfig.name}`, status: 'info', }); - convertModel(data?.key) + convertModel(modelConfig.key) .unwrap() .then(() => { - toast({ id: toastId, title: `${t('modelManager.modelConverted')}: ${data?.name}`, status: 'success' }); + toast({ id: toastId, title: `${t('modelManager.modelConverted')}: ${modelConfig.name}`, status: 'success' }); }) .catch(() => { toast({ id: toastId, - title: `${t('modelManager.modelConversionFailed')}: ${data?.name}`, + title: `${t('modelManager.modelConversionFailed')}: ${modelConfig.name}`, status: 'error', }); }); - }, [data, isLoading, t, convertModel]); - - if (data?.format !== 'checkpoint') { - return; - } + }, [modelConfig, isLoading, t, convertModel]); return ( <> @@ -68,7 +62,7 @@ export const ModelConvertButton = (props: ModelConvertProps) => { 🧨 {t('modelManager.convert')} ; - onSubmit: SubmitHandler; + modelConfig: AnyModelConfig; }; const stringFieldOptions = { validate: (value?: string | null) => (value && value.trim().length > 3) || 'Must be at least 3 characters', }; -export const ModelEdit = ({ form }: Props) => { - const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); - const { data, isLoading } = useGetModelConfigQuery(selectedModelKey ?? skipToken); +export const ModelEdit = ({ modelConfig }: Props) => { const { t } = useTranslation(); + const [updateModel, { isLoading: isSubmitting }] = useUpdateModelMutation(); + const dispatch = useAppDispatch(); - if (isLoading) { - return {t('common.loading')}; - } + const form = useForm({ + defaultValues: modelConfig, + mode: 'onChange', + }); - if (!data) { - return {t('common.somethingWentWrong')}; - } + const onSubmit = useCallback>( + (values) => { + const responseBody: UpdateModelArg = { + key: modelConfig.key, + body: values, + }; + + updateModel(responseBody) + .unwrap() + .then((payload) => { + form.reset(payload, { keepDefaultValues: true }); + dispatch(setSelectedModelMode('view')); + toast({ + id: 'MODEL_UPDATED', + title: t('modelManager.modelUpdated'), + status: 'success', + }); + }) + .catch((_) => { + form.reset(); + toast({ + id: 'MODEL_UPDATE_FAILED', + title: t('modelManager.modelUpdateFailed'), + status: 'error', + }); + }); + }, + [dispatch, modelConfig.key, form, t, updateModel] + ); + + const handleClickCancel = useCallback(() => { + dispatch(setSelectedModelMode('view')); + }, [dispatch]); return ( - -
- - - {t('modelManager.modelName')} - + + + + + + + + + + {t('modelManager.modelName')} + - {form.formState.errors.name?.message && ( - {form.formState.errors.name?.message} - )} - - - - - - - {t('modelManager.description')} -