diff --git a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts index 29b071e9b1..83bd0cf8d5 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts +++ b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts @@ -6,6 +6,7 @@ import type { PersistConfig, RootState } from 'app/store/store'; type ModelManagerState = { _version: 1; selectedModelKey: string | null; + selectedModelMode: "edit" | "view", searchTerm: string; filteredModelType: string | null; }; @@ -13,6 +14,7 @@ type ModelManagerState = { export const initialModelManagerState: ModelManagerState = { _version: 1, selectedModelKey: null, + selectedModelMode: "view", filteredModelType: null, searchTerm: "" }; @@ -22,8 +24,12 @@ export const modelManagerV2Slice = createSlice({ initialState: initialModelManagerState, reducers: { setSelectedModelKey: (state, action: PayloadAction) => { + state.selectedModelMode = "view" state.selectedModelKey = action.payload; }, + setSelectedModelMode: (state, action: PayloadAction<"view" | "edit">) => { + state.selectedModelMode = action.payload; + }, setSearchTerm: (state, action: PayloadAction) => { state.searchTerm = action.payload; }, @@ -34,7 +40,7 @@ export const modelManagerV2Slice = createSlice({ }, }); -export const { setSelectedModelKey, setSearchTerm, setFilteredModelType } = modelManagerV2Slice.actions; +export const { setSelectedModelKey, setSearchTerm, setFilteredModelType, setSelectedModelMode } = modelManagerV2Slice.actions; export const selectModelManagerSlice = (state: RootState) => state.modelmanager; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx index 9756b357b3..7658e741d3 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx @@ -1,13 +1,13 @@ import { Box } from '@invoke-ai/ui-library'; import { useAppSelector } from '../../../app/store/storeHooks'; import { ImportModels } from './ImportModels'; -import { ModelView } from './ModelPanel/ModelView'; +import { Model } from './ModelPanel/Model'; export const ModelPane = () => { const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); return ( - {selectedModelKey ? : } + {selectedModelKey ? : } ); }; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx new file mode 100644 index 0000000000..da7333c2a8 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BaseModelSelect.tsx @@ -0,0 +1,29 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'sd-1', label: MODEL_TYPE_MAP['sd-1'] }, + { value: 'sd-2', label: MODEL_TYPE_MAP['sd-2'] }, + { value: 'sdxl', label: MODEL_TYPE_MAP['sdxl'] }, + { value: 'sdxl-refiner', label: MODEL_TYPE_MAP['sdxl-refiner'] }, +]; + +const BaseModelSelect = (props: UseControllerProps) => { + 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 typedMemo(BaseModelSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BooleanSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BooleanSelect.tsx new file mode 100644 index 0000000000..d21ee89531 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/BooleanSelect.tsx @@ -0,0 +1,27 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox } from '@invoke-ai/ui-library'; +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 type { AnyModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'none', label: '-' }, + { value: true as any, label: 'True' }, + { value: false as any, label: 'False' }, +]; + +const BooleanSelect = (props: UseControllerProps) => { + const { field } = useController(props); + const value = useMemo(() => options.find((o) => o.value === field.value), [field.value]); + const onChange = useCallback( + (v) => { + v?.value === 'none' ? field.onChange(undefined) : field.onChange(v?.value); + }, + [field] + ); + return ; +}; + +export default typedMemo(BooleanSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx new file mode 100644 index 0000000000..0552789a86 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelFormatSelect.tsx @@ -0,0 +1,53 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { LORA_MODEL_FORMAT_MAP, MODEL_TYPE_MAP } from 'features/parameters/types/constants'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'sd-1', label: MODEL_TYPE_MAP['sd-1'] }, + { value: 'sd-2', label: MODEL_TYPE_MAP['sd-2'] }, + { value: 'sdxl', label: MODEL_TYPE_MAP['sdxl'] }, + { value: 'sdxl-refiner', label: MODEL_TYPE_MAP['sdxl-refiner'] }, +]; + +const ModelFormatSelect = (props: UseControllerProps) => { + const { field, formState } = useController(props); + + const onChange = useCallback( + (v) => { + field.onChange(v?.value); + }, + [field] + ); + + const options: ComboboxOption[] = useMemo(() => { + if (formState.defaultValues?.type === 'lora') { + return Object.keys(LORA_MODEL_FORMAT_MAP).map((format) => ({ + value: format, + label: LORA_MODEL_FORMAT_MAP[format], + })) as ComboboxOption[]; + } else if (formState.defaultValues?.type === 'embedding') { + return [ + { value: 'embedding_file', label: 'Embedding File' }, + { value: 'embedding_folder', label: 'Embedding Folder' }, + ]; + } else if (formState.defaultValues?.type === 'ip_adapter') { + return [{ value: 'invokeai', label: 'invokeai' }]; + } else { + return [ + { value: 'diffusers', label: 'Diffusers' }, + { value: 'checkpoint', label: 'Checkpoint' }, + ]; + } + }, [formState.defaultValues?.type]); + + const value = useMemo(() => options.find((o) => o.value === field.value), [options, field.value]); + + return ; +}; + +export default typedMemo(ModelFormatSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx new file mode 100644 index 0000000000..140bfa9fe0 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelTypeSelect.tsx @@ -0,0 +1,33 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +import { typedMemo } from 'common/util/typedMemo'; +import { MODEL_TYPE_MAP } from 'features/parameters/types/constants'; +import { useCallback, useMemo } from 'react'; +import type { UseControllerProps } from 'react-hook-form'; +import { useController } from 'react-hook-form'; +import type { AnyModelConfig } from 'services/api/types'; +import { MODEL_TYPE_LABELS } from '../../ModelManagerPanel/ModelTypeFilter'; + +const options: ComboboxOption[] = [ + { value: 'main', label: MODEL_TYPE_LABELS['main'] as string }, + { value: 'lora', label: MODEL_TYPE_LABELS['lora'] as string }, + { value: 'embedding', label: MODEL_TYPE_LABELS['embedding'] as string }, + { value: 'vae', label: MODEL_TYPE_LABELS['vae'] as string }, + { value: 'controlnet', label: MODEL_TYPE_LABELS['controlnet'] as string }, + { value: 'ip_adapter', label: MODEL_TYPE_LABELS['ip_adapter'] as string }, + { value: 't2i_adapater', label: MODEL_TYPE_LABELS['t2i_adapter'] as string }, +]; + +const ModelTypeSelect = (props: UseControllerProps) => { + 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 typedMemo(ModelTypeSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx new file mode 100644 index 0000000000..7fb74b0bd9 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/ModelVariantSelect.tsx @@ -0,0 +1,27 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; +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 type { AnyModelConfig, CheckpointModelConfig, DiffusersModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'normal', label: 'Normal' }, + { value: 'inpaint', label: 'Inpaint' }, + { value: 'depth', label: 'Depth' }, +]; + +const ModelVariantSelect = (props: UseControllerProps) => { + 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 typedMemo(ModelVariantSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx new file mode 100644 index 0000000000..20667ab5bc --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/PredictionTypeSelect.tsx @@ -0,0 +1,28 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox } from '@invoke-ai/ui-library'; +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 type { AnyModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'none', label: '-' }, + { value: 'epsilon', label: 'epsilon' }, + { value: 'v_prediction', label: 'v_prediction' }, + { value: 'sample', label: 'sample' }, +]; + +const PredictionTypeSelect = (props: UseControllerProps) => { + const { field } = useController(props); + const value = useMemo(() => options.find((o) => o.value === field.value), [field.value]); + const onChange = useCallback( + (v) => { + v?.value === 'none' ? field.onChange(undefined) : field.onChange(v?.value); + }, + [field] + ); + return ; +}; + +export default typedMemo(PredictionTypeSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/RepoVariantSelect.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/RepoVariantSelect.tsx new file mode 100644 index 0000000000..74793be789 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Fields/RepoVariantSelect.tsx @@ -0,0 +1,30 @@ +import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library'; +import { Combobox } from '@invoke-ai/ui-library'; +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 type { AnyModelConfig } from 'services/api/types'; + +const options: ComboboxOption[] = [ + { value: 'none', label: '-' }, + { value: 'fp16', label: 'fp16' }, + { value: 'fp32', label: 'fp32' }, + { value: 'onnx', label: 'onnx' }, + { value: 'openvino', label: 'openvino' }, + { value: 'flax', label: 'flax' }, +]; + +const RepoVariantSelect = (props: UseControllerProps) => { + const { field } = useController(props); + const value = useMemo(() => options.find((o) => o.value === field.value), [field.value]); + const onChange = useCallback( + (v) => { + v?.value === 'none' ? field.onChange(undefined) : field.onChange(v?.value); + }, + [field] + ); + return ; +}; + +export default typedMemo(RepoVariantSelect); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx new file mode 100644 index 0000000000..8a6f7ddee4 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx @@ -0,0 +1,8 @@ +import { useAppSelector } from '../../../../app/store/storeHooks'; +import { ModelEdit } from './ModelEdit'; +import { ModelView } from './ModelView'; + +export const Model = () => { + const selectedModelMode = useAppSelector((s) => s.modelmanagerV2.selectedModelMode); + return selectedModelMode === 'view' ? : ; +}; diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx new file mode 100644 index 0000000000..70d0596cd8 --- /dev/null +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/ModelEdit.tsx @@ -0,0 +1,196 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { useAppDispatch, useAppSelector } from '../../../../app/store/storeHooks'; +import { useGetModelQuery } from '../../../../services/api/endpoints/models'; +import { Flex, Text, Heading, Button, Input, FormControl, FormLabel, Textarea } from '@invoke-ai/ui-library'; +import { useCallback, useMemo } from 'react'; +import { + AnyModelConfig, + CheckpointModelConfig, + ControlNetConfig, + DiffusersModelConfig, + IPAdapterConfig, + LoRAConfig, + T2IAdapterConfig, + TextualInversionConfig, + VAEConfig, +} from '../../../../services/api/types'; +import { setSelectedModelMode } from '../../store/modelManagerV2Slice'; +import BaseModelSelect from './Fields/BaseModelSelect'; +import { useForm } from 'react-hook-form'; +import ModelTypeSelect from './Fields/ModelTypeSelect'; +import ModelVariantSelect from './Fields/ModelVariantSelect'; +import RepoVariantSelect from './Fields/RepoVariantSelect'; +import PredictionTypeSelect from './Fields/PredictionTypeSelect'; +import BooleanSelect from './Fields/BooleanSelect'; +import ModelFormatSelect from './Fields/ModelFormatSelect'; + +export const ModelEdit = () => { + const dispatch = useAppDispatch(); + const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const { data, isLoading } = useGetModelQuery(selectedModelKey ?? skipToken); + + const modelData = useMemo(() => { + if (!data) { + return null; + } + const modelFormat = data.format; + const modelType = data.type; + + if (modelType === 'main') { + if (modelFormat === 'diffusers') { + return data as DiffusersModelConfig; + } else if (modelFormat === 'checkpoint') { + return data as CheckpointModelConfig; + } + } + + switch (modelType) { + case 'lora': + return data as LoRAConfig; + case 'embedding': + return data as TextualInversionConfig; + case 't2i_adapter': + return data as T2IAdapterConfig; + case 'ip_adapter': + return data as IPAdapterConfig; + case 'controlnet': + return data as ControlNetConfig; + case 'vae': + return data as VAEConfig; + default: + return data as DiffusersModelConfig; + } + }, [data]); + + const { + register, + handleSubmit, + control, + formState: { errors }, + reset, + } = useForm({ + defaultValues: { + ...modelData, + }, + mode: 'onChange', + }); + + const handleClickCancel = useCallback(() => { + dispatch(setSelectedModelMode('view')); + }, [dispatch]); + + if (isLoading) { + return Loading; + } + + if (!modelData) { + return Something went wrong; + } + return ( + + + value.trim().length > 3 || 'Must be at least 3 characters', + })} + size="lg" + /> + + + + + + + + + + Description +