From 52f9749bf5ad11b251f78c51d5817994e72391d4 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 29 Dec 2023 20:43:20 +1100 Subject: [PATCH] feat(ui): partial rebuild of model manager internal logic --- invokeai/frontend/web/package.json | 3 +- invokeai/frontend/web/pnpm-lock.yaml | 14 +- .../components/InvControl/InvControl.tsx | 51 +++--- .../src/common/components/InvControl/theme.ts | 28 ++- .../src/common/components/InvControl/types.ts | 1 + .../AddModelsPanel/AdvancedAddCheckpoint.tsx | 166 +++++++++--------- .../AddModelsPanel/AdvancedAddDiffusers.tsx | 155 ++++++++-------- .../subpanels/AddModelsPanel/ScanModels.tsx | 9 +- .../subpanels/ImportModelsPanel.tsx | 2 +- .../ModelManagerPanel/CheckpointModelEdit.tsx | 90 +++++----- .../ModelManagerPanel/DiffusersModelEdit.tsx | 83 +++++---- .../ModelManagerPanel/LoRAModelEdit.tsx | 75 ++++---- .../subpanels/shared/BaseModelSelect.tsx | 29 ++- .../shared/CheckpointConfigsSelect.tsx | 27 ++- .../subpanels/shared/ModelVariantSelect.tsx | 34 +++- invokeai/frontend/web/src/theme/theme.ts | 7 +- 16 files changed, 457 insertions(+), 317 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 7b9446b2f3..3fda030089 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -66,7 +66,7 @@ "@emotion/react": "^11.11.3", "@emotion/styled": "^11.11.0", "@fontsource-variable/inter": "^5.0.16", - "@mantine/form": "^6.0.21", + "@mantine/form": "6.0.21", "@nanostores/react": "^0.7.1", "@reduxjs/toolkit": "^2.0.1", "@roarr/browser-log-writer": "^1.3.0", @@ -89,6 +89,7 @@ "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", "react-error-boundary": "^4.0.12", + "react-hook-form": "^7.49.2", "react-hotkeys-hook": "4.4.1", "react-i18next": "^13.5.0", "react-icons": "^4.12.0", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 8997336b36..573f921ade 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -48,7 +48,7 @@ dependencies: specifier: ^5.0.16 version: 5.0.16 '@mantine/form': - specifier: ^6.0.21 + specifier: 6.0.21 version: 6.0.21(react@18.2.0) '@nanostores/react': specifier: ^0.7.1 @@ -116,6 +116,9 @@ dependencies: react-error-boundary: specifier: ^4.0.12 version: 4.0.12(react@18.2.0) + react-hook-form: + specifier: ^7.49.2 + version: 7.49.2(react@18.2.0) react-hotkeys-hook: specifier: 4.4.1 version: 4.4.1(react-dom@18.2.0)(react@18.2.0) @@ -11049,6 +11052,15 @@ packages: use-sidecar: 1.1.2(@types/react@18.2.46)(react@18.2.0) dev: false + /react-hook-form@7.49.2(react@18.2.0): + resolution: {integrity: sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==} + engines: {node: '>=18', pnpm: '8'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + react: 18.2.0 + dev: false + /react-hotkeys-hook@4.4.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-sClBMBioFEgFGYLTWWRKvhxcCx1DRznd+wkFHwQZspnRBkHTgruKIHptlK/U/2DPX8BhHoRGzpMVWUXMmdZlmw==} peerDependencies: diff --git a/invokeai/frontend/web/src/common/components/InvControl/InvControl.tsx b/invokeai/frontend/web/src/common/components/InvControl/InvControl.tsx index 4223c147ef..499b1b25ae 100644 --- a/invokeai/frontend/web/src/common/components/InvControl/InvControl.tsx +++ b/invokeai/frontend/web/src/common/components/InvControl/InvControl.tsx @@ -1,6 +1,7 @@ import { Flex, FormControl as ChakraFormControl, + FormErrorMessage as ChakraFormErrorMessage, FormHelperText as ChakraFormHelperText, forwardRef, } from '@chakra-ui/react'; @@ -22,36 +23,38 @@ export const InvControl = memo( isDisabled, labelProps, label, + error, ...formControlProps } = props; const ctx = useContext(InvControlGroupContext); - if (helperText) { - return ( - - - {label && ( - - {label} - - )} - {children} - + return ( + + + {label && ( + + {label} + + )} + {children} + + {helperText && ( {helperText} - - ); - } + )} + {error && {error}} + + ); return ( { +const formBaseStyle = defineFormPartsStyle((props) => { return { container: { display: 'flex', @@ -19,7 +24,7 @@ const formBaseStyle = definePartsStyle((props) => { }; }); -const withHelperText = definePartsStyle(() => ({ +const withHelperText = defineFormPartsStyle(() => ({ container: { flexDirection: 'column', gap: 0, @@ -41,7 +46,7 @@ const withHelperText = definePartsStyle(() => ({ }, })); -export const formTheme = defineMultiStyleConfig({ +export const formTheme = defineFormMultiStyleConfig({ baseStyle: formBaseStyle, variants: { withHelperText, @@ -73,3 +78,14 @@ const formLabelBaseStyle = defineStyle(() => { export const formLabelTheme = defineStyleConfig({ baseStyle: formLabelBaseStyle, }); + +const { defineMultiStyleConfig: defineFormErrorMultiStyleConfig } = + createMultiStyleConfigHelpers(formErrorParts.keys); + +export const formErrorTheme = defineFormErrorMultiStyleConfig({ + baseStyle: { + text: { + color: 'error.300', + }, + }, +}); diff --git a/invokeai/frontend/web/src/common/components/InvControl/types.ts b/invokeai/frontend/web/src/common/components/InvControl/types.ts index ffe35f24cd..56cd8e6f1a 100644 --- a/invokeai/frontend/web/src/common/components/InvControl/types.ts +++ b/invokeai/frontend/web/src/common/components/InvControl/types.ts @@ -7,6 +7,7 @@ import type { Feature } from 'common/components/IAIInformationalPopover/constant export type InvControlProps = ChakraFormControlProps & { label?: string; helperText?: string; + error?: string; feature?: Feature; renderInfoPopoverInPortal?: boolean; labelProps?: Omit< diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AdvancedAddCheckpoint.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AdvancedAddCheckpoint.tsx index c199c7fb1c..26798c7574 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AdvancedAddCheckpoint.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AdvancedAddCheckpoint.tsx @@ -1,5 +1,4 @@ import { Flex } from '@chakra-ui/react'; -import { useForm } from '@mantine/form'; import { useAppDispatch } from 'app/store/storeHooks'; import { InvButton } from 'common/components/InvButton/InvButton'; import { InvCheckbox } from 'common/components/InvCheckbox/wrapper'; @@ -13,6 +12,8 @@ import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; import type { CSSProperties, FocusEventHandler } from 'react'; import { memo, useCallback, useState } from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useAddMainModelsMutation } from 'services/api/endpoints/models'; import type { CheckpointModelConfig } from 'services/api/types'; @@ -28,8 +29,16 @@ const AdvancedAddCheckpoint = (props: AdvancedAddCheckpointProps) => { const dispatch = useAppDispatch(); const { model_path } = props; - const advancedAddCheckpointForm = useForm({ - initialValues: { + const { + register, + handleSubmit, + control, + getValues, + setValue, + formState: { errors }, + reset, + } = useForm({ + defaultValues: { model_name: model_path ? getModelName(model_path) : '', base_model: 'sd-1', model_type: 'main', @@ -41,64 +50,64 @@ const AdvancedAddCheckpoint = (props: AdvancedAddCheckpointProps) => { variant: 'normal', config: 'configs\\stable-diffusion\\v1-inference.yaml', }, + mode: 'onChange', }); const [addMainModel] = useAddMainModelsMutation(); const [useCustomConfig, setUseCustomConfig] = useState(false); - const advancedAddCheckpointFormHandler = (values: CheckpointModelConfig) => { - addMainModel({ - body: values, - }) - .unwrap() - .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('modelManager.modelAdded', { - modelName: values.model_name, - }), - status: 'success', - }) - ) - ); - advancedAddCheckpointForm.reset(); - - // Close Advanced Panel in Scan Models tab - if (model_path) { - dispatch(setAdvancedAddScanModel(null)); - } + const onSubmit = useCallback>( + (values) => { + addMainModel({ + body: values, }) - .catch((error) => { - if (error) { + .unwrap() + .then((_) => { dispatch( addToast( makeToast({ - title: t('toast.modelAddFailed'), - status: 'error', + title: t('modelManager.modelAdded', { + modelName: values.model_name, + }), + status: 'success', }) ) ); - } - }); - }; + reset(); - const handleBlurModelLocation: FocusEventHandler = - useCallback( - (e) => { - if (advancedAddCheckpointForm.values['model_name'] === '') { - const modelName = getModelName(e.currentTarget.value); - if (modelName) { - advancedAddCheckpointForm.setFieldValue( - 'model_name', - modelName as string + // Close Advanced Panel in Scan Models tab + if (model_path) { + dispatch(setAdvancedAddScanModel(null)); + } + }) + .catch((error) => { + if (error) { + dispatch( + addToast( + makeToast({ + title: t('toast.modelAddFailed'), + status: 'error', + }) + ) ); } + }); + }, + [addMainModel, dispatch, model_path, reset, t] + ); + + const onBlur: FocusEventHandler = useCallback( + (e) => { + if (getValues().model_name === '') { + const modelName = getModelName(e.currentTarget.value); + if (modelName) { + setValue('model_name', modelName as string); } - }, - [advancedAddCheckpointForm] - ); + } + }, + [getValues, setValue] + ); const handleChangeUseCustomConfig = useCallback( () => setUseCustomConfig((prev) => !prev), @@ -106,56 +115,53 @@ const AdvancedAddCheckpoint = (props: AdvancedAddCheckpointProps) => { ); return ( -
- advancedAddCheckpointFormHandler(v) - )} - style={formStyles} - > + - + + value.trim().length > 3 || 'Must be at least 3 characters', + })} /> - - - - + + control={control} + name="base_model" + /> + + value.trim().length > 0 || 'Must provide a path', + onBlur, + })} /> - + - - - - + + + control={control} + name="variant" + /> {!useCustomConfig ? ( - + ) : ( - - + + )} diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AdvancedAddDiffusers.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AdvancedAddDiffusers.tsx index e7d23b81c5..0797a3e9da 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AdvancedAddDiffusers.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/AdvancedAddDiffusers.tsx @@ -1,5 +1,4 @@ import { Flex } from '@chakra-ui/react'; -import { useForm } from '@mantine/form'; import { useAppDispatch } from 'app/store/storeHooks'; import { InvButton } from 'common/components/InvButton/InvButton'; import { InvControl } from 'common/components/InvControl/InvControl'; @@ -11,6 +10,8 @@ import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; import type { CSSProperties, FocusEventHandler } from 'react'; import { memo, useCallback } from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useAddMainModelsMutation } from 'services/api/endpoints/models'; import type { DiffusersModelConfig } from 'services/api/types'; @@ -28,8 +29,16 @@ const AdvancedAddDiffusers = (props: AdvancedAddDiffusersProps) => { const [addMainModel] = useAddMainModelsMutation(); - const advancedAddDiffusersForm = useForm({ - initialValues: { + const { + register, + handleSubmit, + control, + getValues, + setValue, + formState: { errors }, + reset, + } = useForm({ + defaultValues: { model_name: model_path ? getModelName(model_path, false) : '', base_model: 'sd-1', model_type: 'main', @@ -40,96 +49,104 @@ const AdvancedAddDiffusers = (props: AdvancedAddDiffusersProps) => { vae: '', variant: 'normal', }, + mode: 'onChange', }); - const advancedAddDiffusersFormHandler = (values: DiffusersModelConfig) => { - addMainModel({ - body: values, - }) - .unwrap() - .then((_) => { - dispatch( - addToast( - makeToast({ - title: t('modelManager.modelAdded', { - modelName: values.model_name, - }), - status: 'success', - }) - ) - ); - advancedAddDiffusersForm.reset(); - // Close Advanced Panel in Scan Models tab - if (model_path) { - dispatch(setAdvancedAddScanModel(null)); - } + const onSubmit = useCallback>( + (values) => { + addMainModel({ + body: values, }) - .catch((error) => { - if (error) { + .unwrap() + .then((_) => { dispatch( addToast( makeToast({ - title: t('toast.modelAddFailed'), - status: 'error', + title: t('modelManager.modelAdded', { + modelName: values.model_name, + }), + status: 'success', }) ) ); - } - }); - }; - - const handleBlurModelLocation: FocusEventHandler = - useCallback( - (e) => { - if (advancedAddDiffusersForm.values['model_name'] === '') { - const modelName = getModelName(e.currentTarget.value, false); - if (modelName) { - advancedAddDiffusersForm.setFieldValue( - 'model_name', - modelName as string + reset(); + // Close Advanced Panel in Scan Models tab + if (model_path) { + dispatch(setAdvancedAddScanModel(null)); + } + }) + .catch((error) => { + if (error) { + dispatch( + addToast( + makeToast({ + title: t('toast.modelAddFailed'), + status: 'error', + }) + ) ); } + }); + }, + [addMainModel, dispatch, model_path, reset, t] + ); + + const onBlur: FocusEventHandler = useCallback( + (e) => { + if (getValues().model_name === '') { + const modelName = getModelName(e.currentTarget.value, false); + if (modelName) { + setValue('model_name', modelName as string); } - }, - [advancedAddDiffusersForm] - ); + } + }, + [getValues, setValue] + ); return ( - - advancedAddDiffusersFormHandler(v) - )} - style={formStyles} - > + - - - - - + + value.trim().length > 3 || 'Must be at least 3 characters', + })} /> - + + + control={control} + name="base_model" + /> + + + value.trim().length > 0 || 'Must provide a path', + onBlur, + })} /> - + - - - - + + + control={control} + name="variant" + /> {t('modelManager.addModel')} diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/ScanModels.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/ScanModels.tsx index c492b74be3..d6160915c2 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/ScanModels.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/AddModelsPanel/ScanModels.tsx @@ -7,15 +7,10 @@ import SearchFolderForm from './SearchFolderForm'; const ScanModels = () => { return ( - + - + diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ImportModelsPanel.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ImportModelsPanel.tsx index ed6b421565..a9dd71c3b5 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ImportModelsPanel.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ImportModelsPanel.tsx @@ -17,7 +17,7 @@ const ImportModelsPanel = () => { const handleClickScanTab = useCallback(() => setAddModelTab('scan'), []); return ( - + { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const checkpointEditForm = useForm({ - initialValues: { + const { + register, + handleSubmit, + control, + formState: { errors }, + reset, + } = useForm({ + defaultValues: { model_name: model.model_name ? model.model_name : '', base_model: model.base_model, model_type: 'main', @@ -56,10 +63,7 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => { config: model.config ? model.config : '', variant: model.variant, }, - validate: { - path: (value) => - value.trim().length === 0 ? 'Must provide a path' : null, - }, + mode: 'onChange', }); const handleChangeUseCustomConfig = useCallback( @@ -67,8 +71,8 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => { [] ); - const editModelFormSubmitHandler = useCallback( - (values: CheckpointModelConfig) => { + const onSubmit = useCallback>( + (values) => { const responseBody = { base_model: model.base_model, model_name: model.model_name, @@ -77,7 +81,7 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => { updateMainModel(responseBody) .unwrap() .then((payload) => { - checkpointEditForm.setValues(payload as CheckpointModelConfig); + reset(payload as CheckpointModelConfig, { keepDefaultValues: true }); dispatch( addToast( makeToast({ @@ -88,7 +92,7 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => { ); }) .catch((_) => { - checkpointEditForm.reset(); + reset(); dispatch( addToast( makeToast({ @@ -99,14 +103,7 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => { ); }); }, - [ - checkpointEditForm, - dispatch, - model.base_model, - model.model_name, - t, - updateMainModel, - ] + [dispatch, model.base_model, model.model_name, reset, t, updateMainModel] ); return ( @@ -135,42 +132,53 @@ const CheckpointModelEdit = (props: CheckpointModelEditProps) => { maxHeight={window.innerHeight - 270} overflowY="scroll" > - - editModelFormSubmitHandler(values) - )} - > + - - + + + value.trim().length > 3 || 'Must be at least 3 characters', + })} + /> - + - + control={control} + name="base_model" /> - + control={control} + name="variant" /> - - + + + value.trim().length > 0 || 'Must provide a path', + })} + /> - + {!useCustomConfig ? ( - + ) : ( - + )} diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx index 7545fcb315..aa8032d055 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/DiffusersModelEdit.tsx @@ -1,5 +1,4 @@ import { Divider, Flex } from '@chakra-ui/react'; -import { useForm } from '@mantine/form'; import { useAppDispatch } from 'app/store/storeHooks'; import { InvButton } from 'common/components/InvButton/InvButton'; import { InvControl } from 'common/components/InvControl/InvControl'; @@ -11,6 +10,8 @@ import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; import { memo, useCallback } from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import type { DiffusersModelConfigEntity } from 'services/api/endpoints/models'; import { useUpdateMainModelsMutation } from 'services/api/endpoints/models'; @@ -28,8 +29,14 @@ const DiffusersModelEdit = (props: DiffusersModelEditProps) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const diffusersEditForm = useForm({ - initialValues: { + const { + register, + handleSubmit, + control, + formState: { errors }, + reset, + } = useForm({ + defaultValues: { model_name: model.model_name ? model.model_name : '', base_model: model.base_model, model_type: 'main', @@ -39,14 +46,11 @@ const DiffusersModelEdit = (props: DiffusersModelEditProps) => { vae: model.vae ? model.vae : '', variant: model.variant, }, - validate: { - path: (value) => - value.trim().length === 0 ? 'Must provide a path' : null, - }, + mode: 'onChange', }); - const editModelFormSubmitHandler = useCallback( - (values: DiffusersModelConfig) => { + const onSubmit = useCallback>( + (values) => { const responseBody = { base_model: model.base_model, model_name: model.model_name, @@ -56,7 +60,7 @@ const DiffusersModelEdit = (props: DiffusersModelEditProps) => { updateMainModel(responseBody) .unwrap() .then((payload) => { - diffusersEditForm.setValues(payload as DiffusersModelConfig); + reset(payload as DiffusersModelConfig, { keepDefaultValues: true }); dispatch( addToast( makeToast({ @@ -67,7 +71,7 @@ const DiffusersModelEdit = (props: DiffusersModelEditProps) => { ); }) .catch((_) => { - diffusersEditForm.reset(); + reset(); dispatch( addToast( makeToast({ @@ -78,14 +82,7 @@ const DiffusersModelEdit = (props: DiffusersModelEditProps) => { ); }); }, - [ - diffusersEditForm, - dispatch, - model.base_model, - model.model_name, - t, - updateMainModel, - ] + [dispatch, model.base_model, model.model_name, reset, t, updateMainModel] ); return ( @@ -100,31 +97,45 @@ const DiffusersModelEdit = (props: DiffusersModelEditProps) => { - - editModelFormSubmitHandler(values) - )} - > + - - + + + value.trim().length > 3 || 'Must be at least 3 characters', + })} + /> - + - + control={control} + name="base_model" /> - + control={control} + name="variant" /> - - + + + value.trim().length > 0 || 'Must provide a path', + })} + /> - + {t('modelManager.updateModel')} diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx index 1c7698769e..b8be206498 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/ModelManagerPanel/LoRAModelEdit.tsx @@ -1,5 +1,4 @@ import { Divider, Flex } from '@chakra-ui/react'; -import { useForm } from '@mantine/form'; import { useAppDispatch } from 'app/store/storeHooks'; import { InvButton } from 'common/components/InvButton/InvButton'; import { InvControl } from 'common/components/InvControl/InvControl'; @@ -13,6 +12,8 @@ import { import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; import { memo, useCallback } from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import type { LoRAModelConfigEntity } from 'services/api/endpoints/models'; import { useUpdateLoRAModelsMutation } from 'services/api/endpoints/models'; @@ -30,8 +31,14 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const loraEditForm = useForm({ - initialValues: { + const { + register, + handleSubmit, + control, + formState: { errors }, + reset, + } = useForm({ + defaultValues: { model_name: model.model_name ? model.model_name : '', base_model: model.base_model, model_type: 'lora', @@ -39,14 +46,11 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { description: model.description ? model.description : '', model_format: model.model_format, }, - validate: { - path: (value) => - value.trim().length === 0 ? 'Must provide a path' : null, - }, + mode: 'onChange', }); - const editModelFormSubmitHandler = useCallback( - (values: LoRAModelConfig) => { + const onSubmit = useCallback>( + (values) => { const responseBody = { base_model: model.base_model, model_name: model.model_name, @@ -56,7 +60,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { updateLoRAModel(responseBody) .unwrap() .then((payload) => { - loraEditForm.setValues(payload as LoRAModelConfig); + reset(payload as LoRAModelConfig, { keepDefaultValues: true }); dispatch( addToast( makeToast({ @@ -67,7 +71,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { ); }) .catch((_) => { - loraEditForm.reset(); + reset(); dispatch( addToast( makeToast({ @@ -78,14 +82,7 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { ); }); }, - [ - dispatch, - loraEditForm, - model.base_model, - model.model_name, - t, - updateLoRAModel, - ] + [dispatch, model.base_model, model.model_name, reset, t, updateLoRAModel] ); return ( @@ -101,21 +98,39 @@ const LoRAModelEdit = (props: LoRAModelEditProps) => { - - editModelFormSubmitHandler(values) - )} - > + - - + + + value.trim().length > 3 || 'Must be at least 3 characters', + })} + /> - + - - - + + control={control} + name="base_model" + /> + + + + value.trim().length > 0 || 'Must provide a path', + })} + /> {t('modelManager.updateModel')} diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/shared/BaseModelSelect.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/shared/BaseModelSelect.tsx index ea46cf029e..772d6b9d40 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/shared/BaseModelSelect.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/shared/BaseModelSelect.tsx @@ -1,12 +1,16 @@ import { InvControl } from 'common/components/InvControl/InvControl'; import { InvSelect } from 'common/components/InvSelect/InvSelect'; import type { + InvSelectOnChange, InvSelectOption, - InvSelectProps, } from 'common/components/InvSelect/types'; +import { typedMemo } from 'common/util/typedMemo'; import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; -import { memo } from 'react'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import type { AnyModelConfig } from 'services/api/types'; const options: InvSelectOption[] = [ { value: 'sd-1', label: MODEL_TYPE_MAP['sd-1'] }, @@ -15,15 +19,26 @@ const options: InvSelectOption[] = [ { value: 'sdxl-refiner', label: MODEL_TYPE_MAP['sdxl-refiner'] }, ]; -type BaseModelSelectProps = Omit; - -const BaseModelSelect = (props: BaseModelSelectProps) => { +const BaseModelSelect = ( + props: UseControllerProps +) => { const { t } = useTranslation(); + const { field } = useController(props); + const value = useMemo( + () => options.find((o) => o.value === field.value), + [field.value] + ); + const onChange = useCallback( + (v) => { + field.onChange(v?.value); + }, + [field] + ); return ( - + ); }; -export default memo(BaseModelSelect); +export default typedMemo(BaseModelSelect); diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/shared/CheckpointConfigsSelect.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/shared/CheckpointConfigsSelect.tsx index 0dd099b407..572ab6367e 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/shared/CheckpointConfigsSelect.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/shared/CheckpointConfigsSelect.tsx @@ -2,29 +2,44 @@ import type { ChakraProps } from '@chakra-ui/react'; import { InvControl } from 'common/components/InvControl/InvControl'; import { InvSelect } from 'common/components/InvSelect/InvSelect'; import type { + InvSelectOnChange, InvSelectOption, - InvSelectProps, } from 'common/components/InvSelect/types'; -import { memo, useMemo } from 'react'; +import { memo, useCallback, useMemo } from 'react'; +import { useController, type UseControllerProps } from 'react-hook-form'; import { useGetCheckpointConfigsQuery } from 'services/api/endpoints/models'; - -type CheckpointConfigSelectProps = Omit; +import type { CheckpointModelConfig } from 'services/api/types'; const sx: ChakraProps['sx'] = { w: 'full' }; -const CheckpointConfigsSelect = (props: CheckpointConfigSelectProps) => { +const CheckpointConfigsSelect = ( + props: UseControllerProps +) => { const { data } = useGetCheckpointConfigsQuery(); const options = useMemo( () => (data ? data.map((i) => ({ label: i, value: i })) : []), [data] ); + const { field } = useController(props); + const value = useMemo( + () => options.find((o) => o.value === field.value), + [field.value, options] + ); + const onChange = useCallback( + (v) => { + field.onChange(v?.value); + }, + [field] + ); + return ( ); diff --git a/invokeai/frontend/web/src/features/modelManager/subpanels/shared/ModelVariantSelect.tsx b/invokeai/frontend/web/src/features/modelManager/subpanels/shared/ModelVariantSelect.tsx index 2207ae427f..3e137cf495 100644 --- a/invokeai/frontend/web/src/features/modelManager/subpanels/shared/ModelVariantSelect.tsx +++ b/invokeai/frontend/web/src/features/modelManager/subpanels/shared/ModelVariantSelect.tsx @@ -1,11 +1,18 @@ import { InvControl } from 'common/components/InvControl/InvControl'; import { InvSelect } from 'common/components/InvSelect/InvSelect'; import type { + InvSelectOnChange, InvSelectOption, - InvSelectProps, } from 'common/components/InvSelect/types'; -import { memo } from 'react'; +import { typedMemo } from 'common/util/typedMemo'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import type { + CheckpointModelConfig, + DiffusersModelConfig, +} from 'services/api/types'; const options: InvSelectOption[] = [ { value: 'normal', label: 'Normal' }, @@ -13,15 +20,28 @@ const options: InvSelectOption[] = [ { value: 'depth', label: 'Depth' }, ]; -type VariantSelectProps = Omit; - -const ModelVariantSelect = (props: VariantSelectProps) => { +const ModelVariantSelect = < + T extends CheckpointModelConfig | DiffusersModelConfig, +>( + props: UseControllerProps +) => { const { t } = useTranslation(); + const { field } = useController(props); + const value = useMemo( + () => options.find((o) => o.value === field.value), + [field.value] + ); + const onChange = useCallback( + (v) => { + field.onChange(v?.value); + }, + [field] + ); return ( - + ); }; -export default memo(ModelVariantSelect); +export default typedMemo(ModelVariantSelect); diff --git a/invokeai/frontend/web/src/theme/theme.ts b/invokeai/frontend/web/src/theme/theme.ts index fe47399aba..ee6d42eda4 100644 --- a/invokeai/frontend/web/src/theme/theme.ts +++ b/invokeai/frontend/web/src/theme/theme.ts @@ -4,7 +4,11 @@ import { badgeTheme } from 'common/components/InvBadge/theme'; import { buttonTheme } from 'common/components/InvButton/theme'; import { cardTheme } from 'common/components/InvCard/theme'; import { checkboxTheme } from 'common/components/InvCheckbox/theme'; -import { formLabelTheme, formTheme } from 'common/components/InvControl/theme'; +import { + formErrorTheme, + formLabelTheme, + formTheme, +} from 'common/components/InvControl/theme'; import { editableTheme } from 'common/components/InvEditable/theme'; import { headingTheme } from 'common/components/InvHeading/theme'; import { inputTheme } from 'common/components/InvInput/theme'; @@ -112,6 +116,7 @@ export const theme: ThemeOverride = { Text: textTheme, Textarea: textareaTheme, Tooltip: tooltipTheme, + FormError: formErrorTheme, }, space: space, sizes: space,