mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Model Manager Frontend Rebased
This commit is contained in:
parent
3521557541
commit
79d577bff9
@ -27,6 +27,7 @@
|
||||
"@types/uuid": "^8.3.4",
|
||||
"add": "^2.0.6",
|
||||
"dateformat": "^5.0.3",
|
||||
"formik": "^2.2.9",
|
||||
"framer-motion": "^7.2.1",
|
||||
"i18next": "^22.4.5",
|
||||
"i18next-browser-languagedetector": "^7.0.1",
|
||||
|
49
frontend/public/locales/modelmanager/en.json
Normal file
49
frontend/public/locales/modelmanager/en.json
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"modelManager": "Model Manager",
|
||||
"model": "Model",
|
||||
"modelAdded": "Model Added",
|
||||
"modelUpdated": "Model Updated",
|
||||
"modelEntryDeleted": "Model Entry Deleted",
|
||||
"cannotUseSpaces": "Cannot Use Spaces",
|
||||
"addNew": "Add New",
|
||||
"addNewModel": "Add New Model",
|
||||
"addManually": "Add Manually",
|
||||
"manual": "Manual",
|
||||
"name": "Name",
|
||||
"nameValidationMsg": "Enter a name for your model",
|
||||
"description": "description",
|
||||
"descriptionValidationMsg": "Add a description for your model",
|
||||
"config": "Config",
|
||||
"configValidationMsg": "Path to the config file of your model.",
|
||||
"modelLocation": "Model Location",
|
||||
"modelLocationValidationMsg": "Path to where your model is located.",
|
||||
"vaeLocation": "VAE Location",
|
||||
"vaeLocationValidationMsg": "Path to where your VAE is located.",
|
||||
"width": "Width",
|
||||
"widthValidationMsg": "Default width of your model.",
|
||||
"height": "Height",
|
||||
"heightValidationMsg": "Default height of your model.",
|
||||
"addModel": "Add Model",
|
||||
"availableModels": "Available Models",
|
||||
"search": "Search",
|
||||
"load": "Load",
|
||||
"active": "active",
|
||||
"notLoaded": "not loaded",
|
||||
"cached": "cached",
|
||||
"checkpointFolder": "Checkpoint Folder",
|
||||
"clearCheckpointFolder": "Clear Checkpoint Folder",
|
||||
"findModels": "Find Models",
|
||||
"modelsFound": "Models Found",
|
||||
"selectFolder": "Select Folder",
|
||||
"selected": "Selected",
|
||||
"selectAll": "Select All",
|
||||
"deselectAll": "Deselect All",
|
||||
"showExisting": "Show Existing",
|
||||
"addSelected": "Add Selected",
|
||||
"modelExists": "Model Exists",
|
||||
"delete": "Delete",
|
||||
"deleteModel": "Delete Model",
|
||||
"deleteConfig": "Delete Config",
|
||||
"deleteMsg1": "Are you sure you want to delete this model entry from InvokeAI?",
|
||||
"deleteMsg2": "This will not delete the model checkpoint file from your disk. You can readd them if you wish to."
|
||||
}
|
33
frontend/src/app/invokeai.d.ts
vendored
33
frontend/src/app/invokeai.d.ts
vendored
@ -164,10 +164,27 @@ export declare type ModelStatus = 'active' | 'cached' | 'not loaded';
|
||||
export declare type Model = {
|
||||
status: ModelStatus;
|
||||
description: string;
|
||||
weights: string;
|
||||
};
|
||||
|
||||
export declare type ModelList = Record<string, Model>;
|
||||
|
||||
export declare type FoundModel = {
|
||||
name: string;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export declare type InvokeModelConfigProps = {
|
||||
name: string;
|
||||
description: string;
|
||||
config: string;
|
||||
weights: string;
|
||||
vae: string;
|
||||
width: number;
|
||||
height: number;
|
||||
default: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* These types type data received from the server via socketio.
|
||||
*/
|
||||
@ -177,6 +194,22 @@ export declare type ModelChangeResponse = {
|
||||
model_list: ModelList;
|
||||
};
|
||||
|
||||
export declare type ModelAddedResponse = {
|
||||
new_model_name: string;
|
||||
model_list: ModelList;
|
||||
update: boolean;
|
||||
};
|
||||
|
||||
export declare type ModelDeletedResponse = {
|
||||
deleted_model_name: string;
|
||||
model_list: ModelList;
|
||||
};
|
||||
|
||||
export declare type FoundModelResponse = {
|
||||
search_folder: string;
|
||||
found_models: FoundModel[];
|
||||
};
|
||||
|
||||
export declare type SystemStatusResponse = SystemStatus;
|
||||
|
||||
export declare type SystemConfigResponse = SystemConfig;
|
||||
|
@ -30,6 +30,16 @@ export const requestSystemConfig = createAction<undefined>(
|
||||
'socketio/requestSystemConfig'
|
||||
);
|
||||
|
||||
export const searchForModels = createAction<undefined>(
|
||||
'socketio/searchForModels'
|
||||
);
|
||||
|
||||
export const addNewModel = createAction<InvokeAI.InvokeModelConfigProps>(
|
||||
'socketio/addNewModel'
|
||||
);
|
||||
|
||||
export const deleteModel = createAction<string>('socketio/deleteModel');
|
||||
|
||||
export const requestModelChange = createAction<string>(
|
||||
'socketio/requestModelChange'
|
||||
);
|
||||
|
@ -159,6 +159,15 @@ const makeSocketIOEmitters = (
|
||||
emitRequestSystemConfig: () => {
|
||||
socketio.emit('requestSystemConfig');
|
||||
},
|
||||
emitSearchForModels: () => {
|
||||
socketio.emit('searchForModels');
|
||||
},
|
||||
emitAddNewModel: (modelConfig: InvokeAI.InvokeModelConfigProps) => {
|
||||
socketio.emit('addNewModel', modelConfig);
|
||||
},
|
||||
emitDeleteModel: (modelName: string) => {
|
||||
socketio.emit('deleteModel', modelName);
|
||||
},
|
||||
emitRequestModelChange: (modelName: string) => {
|
||||
dispatch(modelChangeRequested());
|
||||
socketio.emit('requestModelChange', modelName);
|
||||
|
@ -17,6 +17,8 @@ import {
|
||||
setModelList,
|
||||
setIsCancelable,
|
||||
addToast,
|
||||
setFoundModels,
|
||||
setSearchFolder,
|
||||
} from 'features/system/store/systemSlice';
|
||||
|
||||
import {
|
||||
@ -351,6 +353,57 @@ const makeSocketIOListeners = (
|
||||
dispatch(setInfillMethod(data.infill_methods[0]));
|
||||
}
|
||||
},
|
||||
onFoundModels: (data: InvokeAI.FoundModelResponse) => {
|
||||
const { search_folder, found_models } = data;
|
||||
dispatch(setSearchFolder(search_folder));
|
||||
dispatch(setFoundModels(found_models));
|
||||
},
|
||||
onNewModelAdded: (data: InvokeAI.ModelAddedResponse) => {
|
||||
const { new_model_name, model_list, update } = data;
|
||||
dispatch(setModelList(model_list));
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `Model Added: ${new_model_name}`,
|
||||
level: 'info',
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: !update
|
||||
? `${i18n.t('modelmanager:modelAdded')}: ${new_model_name}`
|
||||
: `${i18n.t('modelmanager:modelUpdated')}: ${new_model_name}`,
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
onModelDeleted: (data: InvokeAI.ModelDeletedResponse) => {
|
||||
const { deleted_model_name, model_list } = data;
|
||||
dispatch(setModelList(model_list));
|
||||
dispatch(setIsProcessing(false));
|
||||
dispatch(
|
||||
addLogEntry({
|
||||
timestamp: dateFormat(new Date(), 'isoDateTime'),
|
||||
message: `${i18n.t(
|
||||
'modelmanager:modelAdded'
|
||||
)}: ${deleted_model_name}`,
|
||||
level: 'info',
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
addToast({
|
||||
title: `${i18n.t(
|
||||
'modelmanager:modelEntryDeleted'
|
||||
)}: ${deleted_model_name}`,
|
||||
status: 'success',
|
||||
duration: 2500,
|
||||
isClosable: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
onModelChanged: (data: InvokeAI.ModelChangeResponse) => {
|
||||
const { model_name, model_list } = data;
|
||||
dispatch(setModelList(model_list));
|
||||
|
@ -45,6 +45,9 @@ export const socketioMiddleware = () => {
|
||||
onImageDeleted,
|
||||
onSystemConfig,
|
||||
onModelChanged,
|
||||
onFoundModels,
|
||||
onNewModelAdded,
|
||||
onModelDeleted,
|
||||
onModelChangeFailed,
|
||||
onTempFolderEmptied,
|
||||
} = makeSocketIOListeners(store);
|
||||
@ -58,6 +61,9 @@ export const socketioMiddleware = () => {
|
||||
emitRequestNewImages,
|
||||
emitCancelProcessing,
|
||||
emitRequestSystemConfig,
|
||||
emitSearchForModels,
|
||||
emitAddNewModel,
|
||||
emitDeleteModel,
|
||||
emitRequestModelChange,
|
||||
emitSaveStagingAreaImageToGallery,
|
||||
emitRequestEmptyTempFolder,
|
||||
@ -107,6 +113,18 @@ export const socketioMiddleware = () => {
|
||||
onSystemConfig(data);
|
||||
});
|
||||
|
||||
socketio.on('foundModels', (data: InvokeAI.FoundModelResponse) => {
|
||||
onFoundModels(data);
|
||||
});
|
||||
|
||||
socketio.on('newModelAdded', (data: InvokeAI.ModelAddedResponse) => {
|
||||
onNewModelAdded(data);
|
||||
});
|
||||
|
||||
socketio.on('modelDeleted', (data: InvokeAI.ModelDeletedResponse) => {
|
||||
onModelDeleted(data);
|
||||
});
|
||||
|
||||
socketio.on('modelChanged', (data: InvokeAI.ModelChangeResponse) => {
|
||||
onModelChanged(data);
|
||||
});
|
||||
@ -166,6 +184,21 @@ export const socketioMiddleware = () => {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/searchForModels': {
|
||||
emitSearchForModels();
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/addNewModel': {
|
||||
emitAddNewModel(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/deleteModel': {
|
||||
emitDeleteModel(action.payload);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'socketio/requestModelChange': {
|
||||
emitRequestModelChange(action.payload);
|
||||
break;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Checkbox, CheckboxProps } from '@chakra-ui/react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type IAICheckboxProps = CheckboxProps & {
|
||||
label: string;
|
||||
label: string | ReactNode;
|
||||
styleClass?: string;
|
||||
};
|
||||
|
||||
|
@ -5,13 +5,13 @@ interface IAIInputProps extends InputProps {
|
||||
styleClass?: string;
|
||||
label?: string;
|
||||
width?: string | number;
|
||||
value: string;
|
||||
value?: string;
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export default function IAIInput(props: IAIInputProps) {
|
||||
const {
|
||||
label,
|
||||
label = '',
|
||||
styleClass,
|
||||
isDisabled = false,
|
||||
fontSize = 'sm',
|
||||
|
@ -56,6 +56,7 @@ export interface OptionsState {
|
||||
variationAmount: number;
|
||||
width: number;
|
||||
shouldUseCanvasBetaLayout: boolean;
|
||||
shouldShowExistingModelsInSearch: boolean;
|
||||
}
|
||||
|
||||
const initialOptionsState: OptionsState = {
|
||||
@ -103,6 +104,7 @@ const initialOptionsState: OptionsState = {
|
||||
variationAmount: 0.1,
|
||||
width: 512,
|
||||
shouldUseCanvasBetaLayout: false,
|
||||
shouldShowExistingModelsInSearch: false,
|
||||
};
|
||||
|
||||
const initialState: OptionsState = initialOptionsState;
|
||||
@ -404,6 +406,12 @@ export const optionsSlice = createSlice({
|
||||
setShouldUseCanvasBetaLayout: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldUseCanvasBetaLayout = action.payload;
|
||||
},
|
||||
setShouldShowExistingModelsInSearch: (
|
||||
state,
|
||||
action: PayloadAction<boolean>
|
||||
) => {
|
||||
state.shouldShowExistingModelsInSearch = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -460,6 +468,7 @@ export const {
|
||||
setVariationAmount,
|
||||
setWidth,
|
||||
setShouldUseCanvasBetaLayout,
|
||||
setShouldShowExistingModelsInSearch,
|
||||
} = optionsSlice.actions;
|
||||
|
||||
export default optionsSlice.reducer;
|
||||
|
@ -0,0 +1,16 @@
|
||||
.add-model-modal {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.add-model-modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 1rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.add-model-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5rem;
|
||||
}
|
@ -0,0 +1,369 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
Text,
|
||||
useDisclosure,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import React from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { Field, FieldInputProps, Formik, FormikProps } from 'formik';
|
||||
import { RootState } from 'app/store';
|
||||
import { addNewModel } from 'app/socketio/actions';
|
||||
import { InvokeModelConfigProps } from 'app/invokeai';
|
||||
import IAICheckbox from 'common/components/IAICheckbox';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import SearchModels from './SearchModels';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const MIN_MODEL_SIZE = 64;
|
||||
const MAX_MODEL_SIZE = 2048;
|
||||
|
||||
export default function AddModel() {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isProcessing = useAppSelector(
|
||||
(state: RootState) => state.system.isProcessing
|
||||
);
|
||||
|
||||
function hasWhiteSpace(s: string) {
|
||||
return /\\s/g.test(s);
|
||||
}
|
||||
|
||||
function baseValidation(value: string) {
|
||||
let error;
|
||||
if (hasWhiteSpace(value)) error = t('modelmanager:cannotUseSpaces');
|
||||
return error;
|
||||
}
|
||||
|
||||
const addModelFormValues: InvokeModelConfigProps = {
|
||||
name: '',
|
||||
description: '',
|
||||
config: 'configs/stable-diffusion/v1-inference.yaml',
|
||||
weights: '',
|
||||
vae: '',
|
||||
width: 512,
|
||||
height: 512,
|
||||
default: false,
|
||||
};
|
||||
|
||||
const addModelFormSubmitHandler = (values: InvokeModelConfigProps) => {
|
||||
dispatch(addNewModel(values));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const addModelModalClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const [addManually, setAddmanually] = React.useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IAIButton
|
||||
aria-label={t('modelmanager:addNewModel')}
|
||||
tooltip={t('modelmanager:addNewModel')}
|
||||
onClick={onOpen}
|
||||
className="modal-close-btn"
|
||||
size={'sm'}
|
||||
>
|
||||
<Flex columnGap={'0.5rem'} alignItems="center">
|
||||
<FaPlus />
|
||||
{t('modelmanager:addNew')}
|
||||
</Flex>
|
||||
</IAIButton>
|
||||
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={addModelModalClose}
|
||||
size="xl"
|
||||
closeOnOverlayClick={false}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent className="modal add-model-modal">
|
||||
<ModalHeader>{t('modelmanager:addNewModel')}</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody className="add-model-modal-body">
|
||||
<SearchModels />
|
||||
<IAICheckbox
|
||||
label={t('modelmanager:addManually')}
|
||||
isChecked={addManually}
|
||||
onChange={() => setAddmanually(!addManually)}
|
||||
/>
|
||||
|
||||
{addManually && (
|
||||
<Formik
|
||||
initialValues={addModelFormValues}
|
||||
onSubmit={addModelFormSubmitHandler}
|
||||
>
|
||||
{({ handleSubmit, errors, touched }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<VStack rowGap={'0.5rem'}>
|
||||
<Text fontSize={20} fontWeight="bold" alignSelf={'start'}>
|
||||
{t('modelmanager:manual')}
|
||||
</Text>
|
||||
{/* Name */}
|
||||
<FormControl
|
||||
isInvalid={!!errors.name && touched.name}
|
||||
isRequired
|
||||
>
|
||||
<FormLabel htmlFor="name">
|
||||
{t('modelmanager:name')}
|
||||
</FormLabel>
|
||||
<VStack alignItems={'start'}>
|
||||
<Field
|
||||
as={Input}
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
validate={baseValidation}
|
||||
/>
|
||||
{!!errors.name && touched.name ? (
|
||||
<FormErrorMessage>{errors.name}</FormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
{t('modelmanager:nameValidationMsg')}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
|
||||
{/* Description */}
|
||||
<FormControl
|
||||
isInvalid={!!errors.description && touched.description}
|
||||
isRequired
|
||||
>
|
||||
<FormLabel htmlFor="description">
|
||||
{t('modelmanager:description')}
|
||||
</FormLabel>
|
||||
<VStack alignItems={'start'}>
|
||||
<Field
|
||||
as={Input}
|
||||
id="description"
|
||||
name="description"
|
||||
type="text"
|
||||
/>
|
||||
{!!errors.description && touched.description ? (
|
||||
<FormErrorMessage>
|
||||
{errors.description}
|
||||
</FormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
{t('modelmanager:descriptionValidationMsg')}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
|
||||
{/* Config */}
|
||||
<FormControl
|
||||
isInvalid={!!errors.config && touched.config}
|
||||
isRequired
|
||||
>
|
||||
<FormLabel htmlFor="config">
|
||||
{t('modelmanager:config')}
|
||||
</FormLabel>
|
||||
<VStack alignItems={'start'}>
|
||||
<Field
|
||||
as={Input}
|
||||
id="config"
|
||||
name="config"
|
||||
type="text"
|
||||
/>
|
||||
{!!errors.config && touched.config ? (
|
||||
<FormErrorMessage>{errors.config}</FormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
{t('modelmanager:configValidationMsg')}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
|
||||
{/* Weights */}
|
||||
<FormControl
|
||||
isInvalid={!!errors.weights && touched.weights}
|
||||
isRequired
|
||||
>
|
||||
<FormLabel htmlFor="config">
|
||||
{t('modelmanager:modelLocation')}
|
||||
</FormLabel>
|
||||
<VStack alignItems={'start'}>
|
||||
<Field
|
||||
as={Input}
|
||||
id="weights"
|
||||
name="weights"
|
||||
type="text"
|
||||
/>
|
||||
{!!errors.weights && touched.weights ? (
|
||||
<FormErrorMessage>
|
||||
{errors.weights}
|
||||
</FormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
{t('modelmanager:modelLocationValidationMsg')}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
|
||||
{/* VAE */}
|
||||
<FormControl isInvalid={!!errors.vae && touched.vae}>
|
||||
<FormLabel htmlFor="vae">
|
||||
{t('modelmanager:vaeLocation')}
|
||||
</FormLabel>
|
||||
<VStack alignItems={'start'}>
|
||||
<Field as={Input} id="vae" name="vae" type="text" />
|
||||
{!!errors.vae && touched.vae ? (
|
||||
<FormErrorMessage>{errors.vae}</FormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
{t('modelmanager:vaeLocationValidationMsg')}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
|
||||
<HStack width={'100%'}>
|
||||
{/* Width */}
|
||||
<FormControl
|
||||
isInvalid={!!errors.width && touched.width}
|
||||
>
|
||||
<FormLabel htmlFor="width">
|
||||
{t('modelmanager:width')}
|
||||
</FormLabel>
|
||||
<VStack alignItems={'start'}>
|
||||
<Field id="width" name="width">
|
||||
{({
|
||||
field,
|
||||
form,
|
||||
}: {
|
||||
field: FieldInputProps<number>;
|
||||
form: FormikProps<InvokeModelConfigProps>;
|
||||
}) => (
|
||||
<NumberInput
|
||||
{...field}
|
||||
id="width"
|
||||
name="width"
|
||||
min={MIN_MODEL_SIZE}
|
||||
max={MAX_MODEL_SIZE}
|
||||
step={64}
|
||||
onChange={(value) =>
|
||||
form.setFieldValue(
|
||||
field.name,
|
||||
Number(value)
|
||||
)
|
||||
}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{!!errors.width && touched.width ? (
|
||||
<FormErrorMessage>
|
||||
{errors.width}
|
||||
</FormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
{t('modelmanager:widthValidationMsg')}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
|
||||
{/* Height */}
|
||||
<FormControl
|
||||
isInvalid={!!errors.height && touched.height}
|
||||
>
|
||||
<FormLabel htmlFor="height">
|
||||
{t('modelmanager:height')}
|
||||
</FormLabel>
|
||||
<VStack alignItems={'start'}>
|
||||
<Field id="height" name="height">
|
||||
{({
|
||||
field,
|
||||
form,
|
||||
}: {
|
||||
field: FieldInputProps<number>;
|
||||
form: FormikProps<InvokeModelConfigProps>;
|
||||
}) => (
|
||||
<NumberInput
|
||||
{...field}
|
||||
id="height"
|
||||
name="height"
|
||||
min={MIN_MODEL_SIZE}
|
||||
max={MAX_MODEL_SIZE}
|
||||
step={64}
|
||||
onChange={(value) =>
|
||||
form.setFieldValue(
|
||||
field.name,
|
||||
Number(value)
|
||||
)
|
||||
}
|
||||
>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{!!errors.height && touched.height ? (
|
||||
<FormErrorMessage>
|
||||
{errors.height}
|
||||
</FormErrorMessage>
|
||||
) : (
|
||||
<FormHelperText margin={0}>
|
||||
{t('modelmanager:heightValidationMsg')}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</VStack>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="modal-close-btn"
|
||||
isLoading={isProcessing}
|
||||
>
|
||||
{t('modelmanager:addModel')}
|
||||
</Button>
|
||||
</VStack>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function ModelEdit() {
|
||||
return <div>ModelEdit</div>;
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
import { useState, ChangeEvent, ReactNode } from 'react';
|
||||
import { Flex, Text } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from 'app/store';
|
||||
import { SystemState } from 'features/system/store/systemSlice';
|
||||
import AddModel from './AddModel';
|
||||
import ModelListItem from './ModelListItem';
|
||||
import _ from 'lodash';
|
||||
import IAIInput from 'common/components/IAIInput';
|
||||
import { useAppSelector } from 'app/storeHooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const modelListSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
const models = _.map(system.model_list, (model, key) => {
|
||||
return { name: key, ...model };
|
||||
});
|
||||
|
||||
const activeModel = models.find((model) => model.status === 'active');
|
||||
|
||||
return {
|
||||
models,
|
||||
activeModel: activeModel,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const ModelList = () => {
|
||||
const { models } = useAppSelector(modelListSelector);
|
||||
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSearchFilter = _.debounce((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchText(e.target.value);
|
||||
}, 400);
|
||||
|
||||
const renderModelListItems = () => {
|
||||
const modelListItemsToRender: ReactNode[] = [];
|
||||
const filteredModelListItemsToRender: ReactNode[] = [];
|
||||
|
||||
models.forEach((model, i) => {
|
||||
if (model.name.startsWith(searchText)) {
|
||||
filteredModelListItemsToRender.push(
|
||||
<ModelListItem
|
||||
key={i}
|
||||
name={model.name}
|
||||
status={model.status}
|
||||
description={model.description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
modelListItemsToRender.push(
|
||||
<ModelListItem
|
||||
key={i}
|
||||
name={model.name}
|
||||
status={model.status}
|
||||
description={model.description}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return searchText !== ''
|
||||
? filteredModelListItemsToRender
|
||||
: modelListItemsToRender;
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} rowGap="2rem" width={'50%'}>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Text fontSize={'1.4rem'} fontWeight="bold">
|
||||
{t('modelmanager:availableModels')}
|
||||
</Text>
|
||||
<AddModel />
|
||||
</Flex>
|
||||
|
||||
<IAIInput
|
||||
onChange={handleSearchFilter}
|
||||
label={t('modelmanager:search')}
|
||||
/>
|
||||
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
gap={2}
|
||||
maxHeight={window.innerHeight - 360}
|
||||
overflow={'scroll'}
|
||||
paddingRight="1rem"
|
||||
>
|
||||
{renderModelListItems()}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelList;
|
@ -0,0 +1,95 @@
|
||||
import { DeleteIcon } from '@chakra-ui/icons';
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
IconButton,
|
||||
Spacer,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { ModelStatus } from 'app/invokeai';
|
||||
import { deleteModel, requestModelChange } from 'app/socketio/actions';
|
||||
import { RootState } from 'app/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import IAIAlertDialog from 'common/components/IAIAlertDialog';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type ModelListItemProps = {
|
||||
name: string;
|
||||
status: ModelStatus;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export default function ModelListItem(props: ModelListItemProps) {
|
||||
const { isProcessing, isConnected } = useAppSelector(
|
||||
(state: RootState) => state.system
|
||||
);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { name, status, description } = props;
|
||||
|
||||
const handleChangeModel = () => {
|
||||
dispatch(requestModelChange(name));
|
||||
};
|
||||
|
||||
const handleModelDelete = () => {
|
||||
dispatch(deleteModel(name));
|
||||
};
|
||||
|
||||
const statusTextColor = () => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'var(--status-good-color)';
|
||||
case 'cached':
|
||||
return 'var(--status-working-color)';
|
||||
case 'not loaded':
|
||||
return 'var(--text-color-secondary)';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex alignItems={'center'}>
|
||||
<Tooltip label={description} hasArrow placement="bottom">
|
||||
<Text fontWeight={'bold'}>{name}</Text>
|
||||
</Tooltip>
|
||||
<Spacer />
|
||||
|
||||
<Flex gap={4} alignItems="center">
|
||||
<Text color={statusTextColor()}>{status}</Text>
|
||||
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={handleChangeModel}
|
||||
isDisabled={status === 'active' || isProcessing || !isConnected}
|
||||
className="modal-close-btn"
|
||||
>
|
||||
{t('modelmanager:load')}
|
||||
</Button>
|
||||
<IAIAlertDialog
|
||||
title={t('modelmanager:deleteModel')}
|
||||
acceptCallback={handleModelDelete}
|
||||
acceptButtonText={t('modelmanager:delete')}
|
||||
triggerComponent={
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
size={'sm'}
|
||||
aria-label={t('modelmanager:deleteConfig')}
|
||||
isDisabled={status === 'active' || isProcessing || !isConnected}
|
||||
className=" modal-close-btn"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex rowGap={'1rem'} flexDirection="column">
|
||||
<p style={{ fontWeight: 'bold' }}>{t('modelmanager:deleteMsg1')}</p>
|
||||
<p style={{ color: 'var(--text-color-secondary' }}>
|
||||
{t('modelmanager:deleteMsg2')}
|
||||
</p>
|
||||
</Flex>
|
||||
</IAIAlertDialog>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import {
|
||||
Flex,
|
||||
Modal,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { cloneElement, ReactElement } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ModelEdit from './ModelEdit';
|
||||
import ModelList from './ModelList';
|
||||
|
||||
type ModelManagerModalProps = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
export default function ModelManagerModal({
|
||||
children,
|
||||
}: ModelManagerModalProps) {
|
||||
const {
|
||||
isOpen: isModelManagerModalOpen,
|
||||
onOpen: onModelManagerModalOpen,
|
||||
onClose: onModelManagerModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{cloneElement(children, {
|
||||
onClick: onModelManagerModalOpen,
|
||||
})}
|
||||
<Modal
|
||||
isOpen={isModelManagerModalOpen}
|
||||
onClose={onModelManagerModalClose}
|
||||
size="6xl"
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent className=" modal">
|
||||
<ModalCloseButton className="modal-close-btn" />
|
||||
<ModalHeader>{t('modelmanager:modelManager')}</ModalHeader>
|
||||
<Flex padding={'0 2rem 2rem 2rem'} width="100%" columnGap={'2rem'}>
|
||||
<ModelList />
|
||||
<ModelEdit />
|
||||
</Flex>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,315 @@
|
||||
import { Box, Flex, VStack } from '@chakra-ui/react';
|
||||
import { addNewModel, searchForModels } from 'app/socketio/actions';
|
||||
import { RootState } from 'app/store';
|
||||
import IAICheckbox from 'common/components/IAICheckbox';
|
||||
import React, { ReactNode, ChangeEvent } from 'react';
|
||||
import { MdFindInPage } from 'react-icons/md';
|
||||
import _ from 'lodash';
|
||||
import IAIButton from 'common/components/IAIButton';
|
||||
import IAIIconButton from 'common/components/IAIIconButton';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import {
|
||||
setFoundModels,
|
||||
setSearchFolder,
|
||||
} from 'features/system/store/systemSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { systemSelector } from 'features/system/store/systemSelectors';
|
||||
import { setShouldShowExistingModelsInSearch } from 'features/options/store/optionsSlice';
|
||||
import { FoundModel } from 'app/invokeai';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const existingModelsSelector = createSelector([systemSelector], (system) => {
|
||||
const { model_list } = system;
|
||||
|
||||
const existingModels: string[] = [];
|
||||
|
||||
_.forEach(model_list, (value) => {
|
||||
existingModels.push(value.weights);
|
||||
});
|
||||
|
||||
return existingModels;
|
||||
});
|
||||
|
||||
function ModelExistsTag() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
zIndex={2}
|
||||
right={4}
|
||||
top={4}
|
||||
fontSize="0.7rem"
|
||||
fontWeight={'bold'}
|
||||
backgroundColor={'var(--accent-color)'}
|
||||
padding={'0.2rem 0.5rem'}
|
||||
borderRadius="0.2rem"
|
||||
alignItems={'center'}
|
||||
>
|
||||
{t('modelmanager:modelExists')}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface SearchModelEntry {
|
||||
model: FoundModel;
|
||||
modelsToAdd: string[];
|
||||
setModelsToAdd: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
}
|
||||
|
||||
function SearchModelEntry({
|
||||
model,
|
||||
modelsToAdd,
|
||||
setModelsToAdd,
|
||||
}: SearchModelEntry) {
|
||||
const existingModels = useAppSelector(existingModelsSelector);
|
||||
|
||||
const foundModelsChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!modelsToAdd.includes(e.target.value)) {
|
||||
setModelsToAdd([...modelsToAdd, e.target.value]);
|
||||
} else {
|
||||
setModelsToAdd(_.remove(modelsToAdd, (v) => v !== e.target.value));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
{existingModels.includes(model.location) ? <ModelExistsTag /> : null}
|
||||
<IAICheckbox
|
||||
value={model.name}
|
||||
label={
|
||||
<>
|
||||
<VStack alignItems={'start'}>
|
||||
<p style={{ fontWeight: 'bold' }}>{model.name}</p>
|
||||
<p style={{ fontStyle: 'italic' }}>{model.location}</p>
|
||||
</VStack>
|
||||
</>
|
||||
}
|
||||
isChecked={modelsToAdd.includes(model.name)}
|
||||
isDisabled={existingModels.includes(model.location)}
|
||||
onChange={foundModelsChangeHandler}
|
||||
padding={'1rem'}
|
||||
backgroundColor={'var(--background-color)'}
|
||||
borderRadius={'0.5rem'}
|
||||
_checked={{
|
||||
backgroundColor: 'var(--accent-color)',
|
||||
color: 'var(--text-color)',
|
||||
}}
|
||||
_disabled={{
|
||||
backgroundColor: 'var(--background-color-secondary)',
|
||||
}}
|
||||
></IAICheckbox>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SearchModels() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const searchFolder = useAppSelector(
|
||||
(state: RootState) => state.system.searchFolder
|
||||
);
|
||||
|
||||
const foundModels = useAppSelector(
|
||||
(state: RootState) => state.system.foundModels
|
||||
);
|
||||
|
||||
const existingModels = useAppSelector(existingModelsSelector);
|
||||
|
||||
const shouldShowExistingModelsInSearch = useAppSelector(
|
||||
(state: RootState) => state.options.shouldShowExistingModelsInSearch
|
||||
);
|
||||
|
||||
const [modelsToAdd, setModelsToAdd] = React.useState<string[]>([]);
|
||||
|
||||
const resetSearchModelHandler = () => {
|
||||
dispatch(setSearchFolder(null));
|
||||
dispatch(setFoundModels(null));
|
||||
setModelsToAdd([]);
|
||||
};
|
||||
|
||||
const findModelsHandler = () => {
|
||||
dispatch(searchForModels());
|
||||
};
|
||||
|
||||
const addAllToSelected = () => {
|
||||
setModelsToAdd([]);
|
||||
if (foundModels) {
|
||||
foundModels.forEach((model) => {
|
||||
if (!existingModels.includes(model.location)) {
|
||||
setModelsToAdd((currentModels) => {
|
||||
return [...currentModels, model.name];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeAllFromSelected = () => {
|
||||
setModelsToAdd([]);
|
||||
};
|
||||
|
||||
const addSelectedModels = () => {
|
||||
const modelsToBeAdded = foundModels?.filter((foundModel) =>
|
||||
modelsToAdd.includes(foundModel.name)
|
||||
);
|
||||
modelsToBeAdded?.forEach((model) => {
|
||||
const modelFormat = {
|
||||
name: model.name,
|
||||
description: '',
|
||||
config: 'configs/stable-diffusion/v1-inference.yaml',
|
||||
weights: model.location,
|
||||
vae: '',
|
||||
width: 512,
|
||||
height: 512,
|
||||
default: false,
|
||||
};
|
||||
dispatch(addNewModel(modelFormat));
|
||||
});
|
||||
setModelsToAdd([]);
|
||||
};
|
||||
|
||||
const renderFoundModels = () => {
|
||||
const newFoundModels: ReactNode[] = [];
|
||||
const existingFoundModels: ReactNode[] = [];
|
||||
|
||||
if (foundModels) {
|
||||
foundModels.forEach((model, index) => {
|
||||
if (existingModels.includes(model.location)) {
|
||||
existingFoundModels.push(
|
||||
<SearchModelEntry
|
||||
key={index}
|
||||
model={model}
|
||||
modelsToAdd={modelsToAdd}
|
||||
setModelsToAdd={setModelsToAdd}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
newFoundModels.push(
|
||||
<SearchModelEntry
|
||||
key={index}
|
||||
model={model}
|
||||
modelsToAdd={modelsToAdd}
|
||||
setModelsToAdd={setModelsToAdd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{newFoundModels}
|
||||
{shouldShowExistingModelsInSearch && existingFoundModels}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{searchFolder ? (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
padding={'1rem'}
|
||||
backgroundColor={'var(--background-color)'}
|
||||
borderRadius="0.5rem"
|
||||
rowGap={'0.5rem'}
|
||||
position={'relative'}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 'bold',
|
||||
fontSize: '0.8rem',
|
||||
backgroundColor: 'var(--background-color-secondary)',
|
||||
padding: '0.2rem 1rem',
|
||||
width: 'max-content',
|
||||
borderRadius: '0.2rem',
|
||||
}}
|
||||
>
|
||||
{t('modelmanager:checkpointFolder')}
|
||||
</p>
|
||||
<p
|
||||
style={{ fontWeight: 'bold', fontSize: '0.8rem', maxWidth: '80%' }}
|
||||
>
|
||||
{searchFolder}
|
||||
</p>
|
||||
<IAIIconButton
|
||||
aria-label={t('modelmanager:clearCheckpointFolder')}
|
||||
icon={<FaPlus style={{ transform: 'rotate(45deg)' }} />}
|
||||
position={'absolute'}
|
||||
right={5}
|
||||
onClick={resetSearchModelHandler}
|
||||
/>
|
||||
</Flex>
|
||||
) : (
|
||||
<IAIButton
|
||||
aria-label={t('modelmanager:findModels')}
|
||||
onClick={findModelsHandler}
|
||||
>
|
||||
<Flex columnGap={'0.5rem'}>
|
||||
<MdFindInPage fontSize={20} />
|
||||
{t('modelmanager:selectFolder')}
|
||||
</Flex>
|
||||
</IAIButton>
|
||||
)}
|
||||
{foundModels && (
|
||||
<Flex flexDirection={'column'} rowGap={'1rem'}>
|
||||
<Flex justifyContent={'space-between'} alignItems="center">
|
||||
<p>
|
||||
{t('modelmanager:modelsFound')}: {foundModels.length}
|
||||
</p>
|
||||
<p>
|
||||
{t('modelmanager:selected')}: {modelsToAdd.length}
|
||||
</p>
|
||||
</Flex>
|
||||
<Flex columnGap={'0.5rem'} justifyContent={'space-between'}>
|
||||
<Flex columnGap={'0.5rem'}>
|
||||
<IAIButton
|
||||
isDisabled={modelsToAdd.length === foundModels.length}
|
||||
onClick={addAllToSelected}
|
||||
>
|
||||
{t('modelmanager:selectAll')}
|
||||
</IAIButton>
|
||||
<IAIButton
|
||||
isDisabled={modelsToAdd.length === 0}
|
||||
onClick={removeAllFromSelected}
|
||||
>
|
||||
{t('modelmanager:deselectAll')}
|
||||
</IAIButton>
|
||||
<IAICheckbox
|
||||
label={t('modelmanager:showExisting')}
|
||||
isChecked={shouldShowExistingModelsInSearch}
|
||||
onChange={() =>
|
||||
dispatch(
|
||||
setShouldShowExistingModelsInSearch(
|
||||
!shouldShowExistingModelsInSearch
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<IAIButton
|
||||
isDisabled={modelsToAdd.length === 0}
|
||||
onClick={addSelectedModels}
|
||||
>
|
||||
{t('modelmanager:addSelected')}
|
||||
</IAIButton>
|
||||
</Flex>
|
||||
<Flex
|
||||
rowGap={'1rem'}
|
||||
flexDirection="column"
|
||||
maxHeight={'18rem'}
|
||||
overflowY="scroll"
|
||||
paddingRight={'1rem'}
|
||||
paddingLeft={'0.2rem'}
|
||||
>
|
||||
{renderFoundModels()}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
// .chakra-accordion {
|
||||
// display: grid;
|
||||
// row-gap: 0.5rem;
|
||||
// }
|
||||
|
||||
// .chakra-accordion__item {
|
||||
// border: none;
|
||||
// }
|
||||
|
||||
// button {
|
||||
// border-radius: 0.3rem;
|
||||
|
||||
// &[aria-expanded='true'] {
|
||||
// // background-color: var(--tab-hover-color);
|
||||
// border-radius: 0.3rem;
|
||||
// }
|
||||
// }
|
||||
|
||||
.model-list-accordion {
|
||||
outline: none;
|
||||
padding: 0.25rem;
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.model-list-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
row-gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.model-list-header-hint {
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.model-list-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5rem;
|
||||
|
||||
.model-list-item {
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.model-list-item-name {
|
||||
}
|
||||
.model-list-item-description {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.model-list-item-status {
|
||||
&.active {
|
||||
color: var(--status-good-color);
|
||||
}
|
||||
|
||||
&.cached {
|
||||
color: var(--status-working-color);
|
||||
}
|
||||
|
||||
&.not-loaded {
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
}
|
||||
.model-list-item-load-btn {
|
||||
button {
|
||||
padding: 0.5rem;
|
||||
background-color: var(--btn-base-color);
|
||||
color: var(--text-color);
|
||||
border-radius: 0.2rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-base-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Tooltip,
|
||||
Spacer,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionIcon,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import _ from 'lodash';
|
||||
import { ModelStatus } from 'app/invokeai';
|
||||
import { requestModelChange } from 'app/socketio/actions';
|
||||
import { RootState } from 'app/store';
|
||||
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
|
||||
import { SystemState } from 'features/system/store/systemSlice';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type ModelListItemProps = {
|
||||
name: string;
|
||||
status: ModelStatus;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const ModelListItem = (props: ModelListItemProps) => {
|
||||
const { isProcessing, isConnected } = useAppSelector(
|
||||
(state: RootState) => state.system
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { name, status, description } = props;
|
||||
const handleChangeModel = () => {
|
||||
dispatch(requestModelChange(name));
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="model-list-item">
|
||||
<Tooltip label={description} hasArrow placement="bottom">
|
||||
<div className="model-list-item-name">{name}</div>
|
||||
</Tooltip>
|
||||
<Spacer />
|
||||
<div className={`model-list-item-status ${status.split(' ').join('-')}`}>
|
||||
{status}
|
||||
</div>
|
||||
<div className="model-list-item-load-btn">
|
||||
<Button
|
||||
size={'sm'}
|
||||
onClick={handleChangeModel}
|
||||
isDisabled={status === 'active' || isProcessing || !isConnected}
|
||||
>
|
||||
{t('common:load')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const modelListSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
const models = _.map(system.model_list, (model, key) => {
|
||||
return { name: key, ...model };
|
||||
});
|
||||
|
||||
const activeModel = models.find((model) => model.status === 'active');
|
||||
|
||||
return {
|
||||
models,
|
||||
activeModel: activeModel,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const ModelList = () => {
|
||||
const { models } = useAppSelector(modelListSelector);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
allowToggle
|
||||
className="model-list-accordion"
|
||||
variant={'unstyled'}
|
||||
>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<div className="model-list-button">
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="bold"
|
||||
color="var(--text-color-secondary)"
|
||||
>
|
||||
{t('settings:models')}
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</div>
|
||||
</AccordionButton>
|
||||
|
||||
<AccordionPanel>
|
||||
<div className="model-list-list">
|
||||
{models.map((model, i) => (
|
||||
<ModelListItem
|
||||
key={i}
|
||||
name={model.name}
|
||||
status={model.status}
|
||||
description={model.description}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelList;
|
@ -26,7 +26,6 @@ import {
|
||||
setShouldDisplayGuides,
|
||||
setShouldDisplayInProgressType,
|
||||
} from 'features/system/store/systemSlice';
|
||||
import ModelList from './ModelList';
|
||||
import { IN_PROGRESS_IMAGE_TYPES } from 'app/constants';
|
||||
import IAISwitch from 'common/components/IAISwitch';
|
||||
import IAISelect from 'common/components/IAISelect';
|
||||
@ -139,9 +138,6 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||
<ModalCloseButton className="modal-close-btn" />
|
||||
<ModalBody className="settings-modal-content">
|
||||
<div className="settings-modal-items">
|
||||
<div className="settings-modal-item">
|
||||
<ModelList />
|
||||
</div>
|
||||
<div
|
||||
className="settings-modal-item"
|
||||
style={{ gridAutoFlow: 'row', rowGap: '0.5rem' }}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
FaBug,
|
||||
FaKeyboard,
|
||||
FaWrench,
|
||||
FaCube,
|
||||
} from 'react-icons/fa';
|
||||
|
||||
import InvokeAILogo from 'assets/images/logo.png';
|
||||
@ -17,6 +18,8 @@ import SettingsModal from './SettingsModal/SettingsModal';
|
||||
import StatusIndicator from './StatusIndicator';
|
||||
import ThemeChanger from './ThemeChanger';
|
||||
import ModelSelect from './ModelSelect';
|
||||
import ModelManagerModal from './ModelManager/ModelManagerModal';
|
||||
|
||||
import LanguagePicker from './LanguagePicker';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -41,6 +44,18 @@ const SiteHeader = () => {
|
||||
|
||||
<ModelSelect />
|
||||
|
||||
<ModelManagerModal>
|
||||
<IAIIconButton
|
||||
aria-label={t('modelmanager:modelManager')}
|
||||
tooltip={t('modelmanager:modelManager')}
|
||||
size={'sm'}
|
||||
variant="link"
|
||||
data-variant="link"
|
||||
fontSize={20}
|
||||
icon={<FaCube />}
|
||||
/>
|
||||
</ModelManagerModal>
|
||||
|
||||
<HotkeysModal>
|
||||
<IAIIconButton
|
||||
aria-label={t('common:hotkeysLabel')}
|
||||
|
@ -47,6 +47,8 @@ export interface SystemState
|
||||
saveIntermediatesInterval: number;
|
||||
enableImageDebugging: boolean;
|
||||
toastQueue: UseToastOptions[];
|
||||
searchFolder: string | null;
|
||||
foundModels: InvokeAI.FoundModel[] | null;
|
||||
}
|
||||
|
||||
const initialSystemState: SystemState = {
|
||||
@ -82,6 +84,8 @@ const initialSystemState: SystemState = {
|
||||
saveIntermediatesInterval: 5,
|
||||
enableImageDebugging: false,
|
||||
toastQueue: [],
|
||||
searchFolder: null,
|
||||
foundModels: null,
|
||||
};
|
||||
|
||||
export const systemSlice = createSlice({
|
||||
@ -225,6 +229,15 @@ export const systemSlice = createSlice({
|
||||
state.currentStatus = action.payload;
|
||||
state.currentStatusHasSteps = false;
|
||||
},
|
||||
setSearchFolder: (state, action: PayloadAction<string | null>) => {
|
||||
state.searchFolder = action.payload;
|
||||
},
|
||||
setFoundModels: (
|
||||
state,
|
||||
action: PayloadAction<InvokeAI.FoundModel[] | null>
|
||||
) => {
|
||||
state.foundModels = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -253,6 +266,8 @@ export const {
|
||||
addToast,
|
||||
clearToastQueue,
|
||||
setProcessingIndeterminateTask,
|
||||
setSearchFolder,
|
||||
setFoundModels,
|
||||
} = systemSlice.actions;
|
||||
|
||||
export default systemSlice.reducer;
|
||||
|
2
frontend/src/i18.d.ts
vendored
2
frontend/src/i18.d.ts
vendored
@ -8,6 +8,7 @@ import gallery from '../public/locales/gallery/en.json';
|
||||
import toast from '../public/locales/toast/en.json';
|
||||
import hotkeys from '../public/locales/hotkeys/en.json';
|
||||
import settings from '../public/locales/settings/en.json';
|
||||
import modelmanager from '../public/locales/modelmanager/en.json';
|
||||
|
||||
declare module 'i18next' {
|
||||
// Extend CustomTypeOptions
|
||||
@ -23,6 +24,7 @@ declare module 'i18next' {
|
||||
toast: typeof toast;
|
||||
hotkeys: typeof hotkeys;
|
||||
settings: typeof settings;
|
||||
modelmanager: typeof modelmanager;
|
||||
};
|
||||
// Never Return Null
|
||||
returnNull: false;
|
||||
|
@ -18,6 +18,7 @@ i18n
|
||||
'toast',
|
||||
'hotkeys',
|
||||
'settings',
|
||||
'modelmanager',
|
||||
],
|
||||
backend: {
|
||||
loadPath: '/locales/{{ns}}/{{lng}}.json',
|
||||
|
@ -16,7 +16,7 @@
|
||||
@use '../features/system/components/SiteHeader.scss';
|
||||
@use '../features/system/components/StatusIndicator.scss';
|
||||
@use '../features/system/components/SettingsModal/SettingsModal.scss';
|
||||
@use '../features/system/components/SettingsModal/ModelList.scss';
|
||||
@use '../features/system/components/ModelManager/AddModel.scss';
|
||||
@use '../features/system/components/HotkeysModal/HotkeysModal.scss';
|
||||
@use '../features/system/components/Console.scss';
|
||||
|
||||
|
@ -2331,6 +2331,11 @@ deep-is@^0.1.3, deep-is@~0.1.3:
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||
|
||||
deepmerge@^2.1.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
|
||||
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
|
||||
|
||||
defaults@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a"
|
||||
@ -2946,6 +2951,19 @@ focus-lock@^0.11.2:
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
formik@^2.2.9:
|
||||
version "2.2.9"
|
||||
resolved "https://registry.yarnpkg.com/formik/-/formik-2.2.9.tgz#8594ba9c5e2e5cf1f42c5704128e119fc46232d0"
|
||||
integrity sha512-LQLcISMmf1r5at4/gyJigGn0gOwFbeEAlji+N9InZF6LIMXnFNkO42sCI8Jt84YZggpD4cPWObAZaxpEFtSzNA==
|
||||
dependencies:
|
||||
deepmerge "^2.1.1"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
lodash "^4.17.21"
|
||||
lodash-es "^4.17.21"
|
||||
react-fast-compare "^2.0.1"
|
||||
tiny-warning "^1.0.2"
|
||||
tslib "^1.10.0"
|
||||
|
||||
framer-motion@^7.2.1:
|
||||
version "7.6.9"
|
||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-7.6.9.tgz#d2c8ca8b97580aa00f7c6e2b616da241bd2ce8e6"
|
||||
@ -3445,6 +3463,11 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
@ -4021,6 +4044,11 @@ react-fast-compare@3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||
|
||||
react-fast-compare@^2.0.1:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-focus-lock@^2.9.1:
|
||||
version "2.9.2"
|
||||
resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.2.tgz#a57dfd7c493e5a030d87f161c96ffd082bd920f2"
|
||||
@ -4602,6 +4630,11 @@ tiny-invariant@^1.0.6:
|
||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
|
||||
integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
|
||||
|
||||
tiny-warning@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||
|
||||
tmp@^0.0.33:
|
||||
version "0.0.33"
|
||||
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
|
||||
@ -4671,7 +4704,7 @@ tslib@2.4.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
|
||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||
|
||||
tslib@^1.8.1, tslib@^1.9.3:
|
||||
tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.3:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
Loading…
Reference in New Issue
Block a user