diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 36cf1d7af6..ed2899cfcb 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -454,7 +454,8 @@ "none": "none", "addDifference": "Add Difference", "pickModelType": "Pick Model Type", - "selectModel": "Select Model" + "selectModel": "Select Model", + "importModels": "Import Models" }, "parameters": { "general": "General", diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/ModelManagerTab.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/ModelManagerTab.tsx index 9aced0dda8..6f9a836902 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/ModelManagerTab.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/ModelManagerTab.tsx @@ -5,7 +5,7 @@ import AddModelsPanel from './subpanels/AddModelsPanel'; import MergeModelsPanel from './subpanels/MergeModelsPanel'; import ModelManagerPanel from './subpanels/ModelManagerPanel'; -type ModelManagerTabName = 'modelManager' | 'addModels' | 'mergeModels'; +type ModelManagerTabName = 'modelManager' | 'importModels' | 'mergeModels'; type ModelManagerTabInfo = { id: ModelManagerTabName; @@ -20,8 +20,8 @@ const tabs: ModelManagerTabInfo[] = [ content: , }, { - id: 'addModels', - label: i18n.t('modelManager.addModel'), + id: 'importModels', + label: i18n.t('modelManager.importModels'), content: , }, { @@ -46,7 +46,7 @@ const ModelManagerTab = () => { ))} - + {tabs.map((tab) => ( {tab.content} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel.tsx index 32fafe9ae1..d868dc6c80 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel.tsx @@ -1,43 +1,39 @@ -import { Divider, Flex, useColorMode } from '@chakra-ui/react'; -import { RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { ButtonGroup, Divider, Flex } from '@chakra-ui/react'; import IAIButton from 'common/components/IAIButton'; -import { setAddNewModelUIOption } from 'features/ui/store/uiSlice'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import AddCheckpointModel from './AddModelsPanel/AddCheckpointModel'; -import AddDiffusersModel from './AddModelsPanel/AddDiffusersModel'; +import AddModels from './AddModelsPanel/AddModels'; +import ScanModels from './AddModelsPanel/ScanModels'; + +type AddModelTabs = 'add' | 'scan'; export default function AddModelsPanel() { - const addNewModelUIOption = useAppSelector( - (state: RootState) => state.ui.addNewModelUIOption - ); - - const { colorMode } = useColorMode(); - - const dispatch = useAppDispatch(); + const [addModelTab, setAddModelTab] = useState('add'); const { t } = useTranslation(); return ( - + dispatch(setAddNewModelUIOption('ckpt'))} - isChecked={addNewModelUIOption == 'ckpt'} + onClick={() => setAddModelTab('add')} + isChecked={addModelTab == 'add'} + size="sm" > - {t('modelManager.addCheckpointModel')} + {t('modelManager.addModel')} dispatch(setAddNewModelUIOption('diffusers'))} - isChecked={addNewModelUIOption == 'diffusers'} + onClick={() => setAddModelTab('scan')} + isChecked={addModelTab == 'scan'} + size="sm" > - {t('modelManager.addDiffuserModel')} + {t('modelManager.scanForModels')} - + - {addNewModelUIOption == 'ckpt' && } - {addNewModelUIOption == 'diffusers' && } + {addModelTab == 'add' && } + {addModelTab == 'scan' && } ); } diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/AddDiffusersModel.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/AddDiffusersModel.tsx deleted file mode 100644 index c871a0ede5..0000000000 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/AddDiffusersModel.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { - Flex, - FormControl, - FormErrorMessage, - FormHelperText, - FormLabel, - Text, - VStack, -} from '@chakra-ui/react'; -import { InvokeDiffusersModelConfigProps } from 'app/types/invokeai'; -// import { addNewModel } from 'app/socketio/actions'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIButton from 'common/components/IAIButton'; -import IAIInput from 'common/components/IAIInput'; -import { setAddNewModelUIOption } from 'features/ui/store/uiSlice'; -import { Field, Formik } from 'formik'; -import { useTranslation } from 'react-i18next'; - -import type { RootState } from 'app/store/store'; -import IAIForm from 'common/components/IAIForm'; -import { IAIFormItemWrapper } from 'common/components/IAIForms/IAIFormItemWrapper'; - -export default function AddDiffusersModel() { - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - - const isProcessing = useAppSelector( - (state: RootState) => state.system.isProcessing - ); - - function hasWhiteSpace(s: string) { - return /\s/.test(s); - } - - function baseValidation(value: string) { - let error; - if (hasWhiteSpace(value)) error = t('modelManager.cannotUseSpaces'); - return error; - } - - const addModelFormValues: InvokeDiffusersModelConfigProps = { - name: '', - description: '', - repo_id: '', - path: '', - format: 'diffusers', - default: false, - vae: { - repo_id: '', - path: '', - }, - }; - - const addModelFormSubmitHandler = ( - values: InvokeDiffusersModelConfigProps - ) => { - const diffusersModelToAdd = values; - - if (values.path === '') delete diffusersModelToAdd.path; - if (values.repo_id === '') delete diffusersModelToAdd.repo_id; - if (values.vae.path === '') delete diffusersModelToAdd.vae.path; - if (values.vae.repo_id === '') delete diffusersModelToAdd.vae.repo_id; - - dispatch(addNewModel(diffusersModelToAdd)); - dispatch(setAddNewModelUIOption(null)); - }; - - return ( - - - {({ handleSubmit, errors, touched }) => ( - - - - {/* Name */} - - - {t('modelManager.name')} - - - - {!!errors.name && touched.name ? ( - {errors.name} - ) : ( - - {t('modelManager.nameValidationMsg')} - - )} - - - - - - {/* Description */} - - - {t('modelManager.description')} - - - - {!!errors.description && touched.description ? ( - {errors.description} - ) : ( - - {t('modelManager.descriptionValidationMsg')} - - )} - - - - - - - {t('modelManager.formMessageDiffusersModelLocation')} - - - {t('modelManager.formMessageDiffusersModelLocationDesc')} - - - {/* Path */} - - - {t('modelManager.modelLocation')} - - - - {!!errors.path && touched.path ? ( - {errors.path} - ) : ( - - {t('modelManager.modelLocationValidationMsg')} - - )} - - - - {/* Repo ID */} - - - {t('modelManager.repo_id')} - - - - {!!errors.repo_id && touched.repo_id ? ( - {errors.repo_id} - ) : ( - - {t('modelManager.repoIDValidationMsg')} - - )} - - - - - - {/* VAE Path */} - - {t('modelManager.formMessageDiffusersVAELocation')} - - - {t('modelManager.formMessageDiffusersVAELocationDesc')} - - - - {t('modelManager.vaeLocation')} - - - - {!!errors.vae?.path && touched.vae?.path ? ( - {errors.vae?.path} - ) : ( - - {t('modelManager.vaeLocationValidationMsg')} - - )} - - - - {/* VAE Repo ID */} - - - {t('modelManager.vaeRepoID')} - - - - {!!errors.vae?.repo_id && touched.vae?.repo_id ? ( - {errors.vae?.repo_id} - ) : ( - - {t('modelManager.vaeRepoIDValidationMsg')} - - )} - - - - - - {t('modelManager.addModel')} - - - - )} - - - ); -} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/AddModels.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/AddModels.tsx new file mode 100644 index 0000000000..c6517d38a7 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/AddModels.tsx @@ -0,0 +1,110 @@ +import { Flex } from '@chakra-ui/react'; +// import { addNewModel } from 'app/socketio/actions'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useTranslation } from 'react-i18next'; + +import { SelectItem } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { makeToast } from 'app/components/Toaster'; +import { RootState } from 'app/store/store'; +import IAIButton from 'common/components/IAIButton'; +import IAIMantineTextInput from 'common/components/IAIMantineInput'; +import IAIMantineSelect from 'common/components/IAIMantineSelect'; +import { addToast } from 'features/system/store/systemSlice'; +import { useImportMainModelsMutation } from 'services/api/endpoints/models'; + +const predictionSelectData: SelectItem[] = [ + { label: 'None', value: 'none' }, + { label: 'v_prediction', value: 'v_prediction' }, + { label: 'epsilon', value: 'epsilon' }, + { label: 'sample', value: 'sample' }, +]; + +type ExtendedImportModelConfig = { + location: string; + prediction_type?: 'v_prediction' | 'epsilon' | 'sample' | 'none' | undefined; +}; + +export default function AddModels() { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + const isProcessing = useAppSelector( + (state: RootState) => state.system.isProcessing + ); + + const [importMainModel, { isLoading }] = useImportMainModelsMutation(); + + const addModelForm = useForm({ + initialValues: { + location: '', + prediction_type: undefined, + }, + }); + + const handleAddModelSubmit = (values: ExtendedImportModelConfig) => { + const importModelResponseBody = { + location: values.location, + prediction_type: + values.prediction_type === 'none' ? undefined : values.prediction_type, + }; + + importMainModel({ body: importModelResponseBody }) + .unwrap() + .then((_) => { + dispatch( + addToast( + makeToast({ + title: 'Model Added', + status: 'success', + }) + ) + ); + addModelForm.reset(); + }) + .catch((error) => { + if (error) { + console.log(error); + dispatch( + addToast( + makeToast({ + title: `${error.data.detail} `, + status: 'error', + }) + ) + ); + } + }); + }; + + return ( +
handleAddModelSubmit(v))}> + + + + + {t('modelManager.addModel')} + + +
+ ); +} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/FoundModelsList.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/FoundModelsList.tsx index af862d005d..69dbd20746 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/FoundModelsList.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/FoundModelsList.tsx @@ -22,7 +22,11 @@ export default function FoundModelsList() { } return ( - + {foundModels.map((model) => ( {model} ))} diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/AddCheckpointModel.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/ScanModels.tsx similarity index 99% rename from invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/AddCheckpointModel.tsx rename to invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/ScanModels.tsx index 75e2017bb8..20f4330658 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/AddCheckpointModel.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/ScanModels.tsx @@ -33,7 +33,7 @@ import SearchModels from './SearchModels'; const MIN_MODEL_SIZE = 64; const MAX_MODEL_SIZE = 2048; -export default function AddCheckpointModel() { +export default function ScanModels() { const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/SearchModels.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/SearchModels.tsx index e3e48c7e6b..be2d9ea20d 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/SearchModels.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ModelManager/subpanels/AddModelsPanel/SearchModels.tsx @@ -11,7 +11,9 @@ export default function SearchModels() { return ( - + + + ); } diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 4c72bd6239..325e8e898f 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,7 +1,5 @@ import { SchedulerParam } from 'features/parameters/types/parameterSchemas'; -export type AddNewModelType = 'ckpt' | 'diffusers' | null; - export type Coordinates = { x: number; y: number; @@ -22,7 +20,6 @@ export interface UIState { shouldUseCanvasBetaLayout: boolean; shouldShowExistingModelsInSearch: boolean; shouldUseSliders: boolean; - addNewModelUIOption: AddNewModelType; shouldHidePreview: boolean; shouldPinGallery: boolean; shouldShowGallery: boolean; diff --git a/invokeai/frontend/web/src/services/api/endpoints/models.ts b/invokeai/frontend/web/src/services/api/endpoints/models.ts index a838a82f46..2a5e5547b3 100644 --- a/invokeai/frontend/web/src/services/api/endpoints/models.ts +++ b/invokeai/frontend/web/src/services/api/endpoints/models.ts @@ -7,6 +7,7 @@ import { ControlNetModelConfig, ConvertModelConfig, DiffusersModelConfig, + ImportModelConfig, LoRAModelConfig, MainModelConfig, MergeModelConfig, @@ -78,6 +79,13 @@ type MergeMainModelArg = { type MergeMainModelResponse = paths['/api/v1/models/merge/{base_model}']['put']['responses']['200']['content']['application/json']; +type ImportMainModelArg = { + body: ImportModelConfig; +}; + +type ImportMainModelResponse = + paths['/api/v1/models/import']['post']['responses']['201']['content']['application/json']; + type SearchFolderResponse = paths['/api/v1/models/search']['get']['responses']['200']['content']['application/json']; @@ -168,6 +176,19 @@ export const modelsApi = api.injectEndpoints({ }, invalidatesTags: [{ type: 'MainModel', id: LIST_TAG }], }), + importMainModels: build.mutation< + ImportMainModelResponse, + ImportMainModelArg + >({ + query: ({ body }) => { + return { + url: `models/import`, + method: 'POST', + body: body, + }; + }, + invalidatesTags: [{ type: 'MainModel', id: LIST_TAG }], + }), deleteMainModels: build.mutation< DeleteMainModelResponse, DeleteMainModelArg @@ -356,6 +377,7 @@ export const { useGetVaeModelsQuery, useUpdateMainModelsMutation, useDeleteMainModelsMutation, + useImportMainModelsMutation, useConvertMainModelsMutation, useMergeMainModelsMutation, useGetModelsInFolderQuery, diff --git a/invokeai/frontend/web/src/services/api/types.d.ts b/invokeai/frontend/web/src/services/api/types.d.ts index 8519228c5e..6812a62925 100644 --- a/invokeai/frontend/web/src/services/api/types.d.ts +++ b/invokeai/frontend/web/src/services/api/types.d.ts @@ -60,7 +60,7 @@ export type AnyModelConfig = export type MergeModelConfig = components['schemas']['Body_merge_models']; export type ConvertModelConfig = components['schemas']['Body_convert_model']; -export type SearchFolderConfig = components['schemas']; +export type ImportModelConfig = components['schemas']['Body_import_model']; // Graphs export type Graph = components['schemas']['Graph'];