From df858eb3f969ab1a072b8962f683e96568dc8c7a Mon Sep 17 00:00:00 2001
From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com>
Date: Mon, 26 Dec 2022 23:47:49 +1300
Subject: [PATCH] Add Edit Model Functionality

---
 frontend/public/locales/modelmanager/en.json  |   3 +-
 frontend/src/app/invokeai.d.ts                |  16 +-
 .../components/ModelManager/ModelEdit.tsx     | 290 ++++++++++++++++--
 3 files changed, 281 insertions(+), 28 deletions(-)

diff --git a/frontend/public/locales/modelmanager/en.json b/frontend/public/locales/modelmanager/en.json
index 48b2821ec7..aa824590af 100644
--- a/frontend/public/locales/modelmanager/en.json
+++ b/frontend/public/locales/modelmanager/en.json
@@ -11,7 +11,7 @@
   "manual": "Manual",
   "name": "Name",
   "nameValidationMsg": "Enter a name for your model",
-  "description": "description",
+  "description": "Description",
   "descriptionValidationMsg": "Add a description for your model",
   "config": "Config",
   "configValidationMsg": "Path to the config file of your model.",
@@ -24,6 +24,7 @@
   "height": "Height",
   "heightValidationMsg": "Default height of your model.",
   "addModel": "Add Model",
+  "updateModel": "Update Model",
   "availableModels": "Available Models",
   "search": "Search",
   "load": "Load",
diff --git a/frontend/src/app/invokeai.d.ts b/frontend/src/app/invokeai.d.ts
index af292058c2..d5ee6adbbc 100644
--- a/frontend/src/app/invokeai.d.ts
+++ b/frontend/src/app/invokeai.d.ts
@@ -180,14 +180,14 @@ export declare type FoundModel = {
 };
 
 export declare type InvokeModelConfigProps = {
-  name: string;
-  description: string;
-  config: string;
-  weights: string;
-  vae: string;
-  width: number;
-  height: number;
-  default: boolean;
+  name: string | undefined;
+  description: string | undefined;
+  config: string | undefined;
+  weights: string | undefined;
+  vae: string | undefined;
+  width: number | undefined;
+  height: number | undefined;
+  default: boolean | undefined;
 };
 
 /**
diff --git a/frontend/src/features/system/components/ModelManager/ModelEdit.tsx b/frontend/src/features/system/components/ModelManager/ModelEdit.tsx
index 4738b0a2a7..11cbf8e2fe 100644
--- a/frontend/src/features/system/components/ModelManager/ModelEdit.tsx
+++ b/frontend/src/features/system/components/ModelManager/ModelEdit.tsx
@@ -1,11 +1,30 @@
-import { createSelector, Dictionary } from '@reduxjs/toolkit';
+import { createSelector } from '@reduxjs/toolkit';
 import type { RootState } from 'app/store';
-import { useAppSelector } from 'app/storeHooks';
+import { useAppDispatch, useAppSelector } from 'app/storeHooks';
 import { systemSelector } from 'features/system/store/systemSelectors';
 import React, { useEffect, useState } from 'react';
 import _ from 'lodash';
-import { Model } from 'app/invokeai';
-import { Flex, Spacer, Text } from '@chakra-ui/react';
+import type { InvokeModelConfigProps } from 'app/invokeai';
+import {
+  Button,
+  Flex,
+  FormControl,
+  FormErrorMessage,
+  FormHelperText,
+  FormLabel,
+  Input,
+  NumberDecrementStepper,
+  NumberIncrementStepper,
+  NumberInput,
+  NumberInputField,
+  NumberInputStepper,
+  Text,
+  VStack,
+} from '@chakra-ui/react';
+import { Field, Formik } from 'formik';
+import type { FieldInputProps, FormikProps } from 'formik';
+import { useTranslation } from 'react-i18next';
+import { addNewModel } from 'app/socketio/actions';
 
 const selector = createSelector(
   [systemSelector],
@@ -23,38 +42,271 @@ const selector = createSelector(
   }
 );
 
+const MIN_MODEL_SIZE = 64;
+const MAX_MODEL_SIZE = 2048;
+
 export default function ModelEdit() {
   const { openModel, model_list } = useAppSelector(selector);
-  const [openedModel, setOpenedModel] = useState<Model>();
+  const isProcessing = useAppSelector(
+    (state: RootState) => state.system.isProcessing
+  );
+
+  const dispatch = useAppDispatch();
+
+  const { t } = useTranslation();
+
+  const [editModelFormValues, setEditModelFormValues] =
+    useState<InvokeModelConfigProps>({
+      name: '',
+      description: '',
+      config: 'configs/stable-diffusion/v1-inference.yaml',
+      weights: '',
+      vae: '',
+      width: 512,
+      height: 512,
+      default: false,
+    });
 
   useEffect(() => {
     if (openModel) {
       const retrievedModel = _.pickBy(model_list, (val, key) => {
         return _.isEqual(key, openModel);
       });
-      setOpenedModel(retrievedModel[openModel]);
+      setEditModelFormValues({
+        name: openModel,
+        description: retrievedModel[openModel]?.description,
+        config: retrievedModel[openModel]?.config,
+        weights: retrievedModel[openModel]?.weights,
+        vae: retrievedModel[openModel]?.vae,
+        width: retrievedModel[openModel]?.width,
+        height: retrievedModel[openModel]?.height,
+        default: retrievedModel[openModel]?.default,
+      });
     }
   }, [model_list, openModel]);
 
-  console.log(openedModel);
+  const editModelFormSubmitHandler = (values: InvokeModelConfigProps) => {
+    dispatch(addNewModel(values));
+  };
 
-  return (
-    <Flex flexDirection="column" rowGap="1rem">
-      <Flex columnGap="1rem" alignItems="center">
+  return openModel ? (
+    <Flex flexDirection="column" rowGap="1rem" width="100%">
+      <Flex alignItems="center">
         <Text fontSize="lg" fontWeight="bold">
           {openModel}
         </Text>
-        <Text>{openedModel?.status}</Text>
       </Flex>
-      <Flex flexDirection="column">
-        <Text>{openedModel?.config}</Text>
-        <Text>{openedModel?.description}</Text>
-        <Text>{openedModel?.height}</Text>
-        <Text>{openedModel?.width}</Text>
-        <Text>{openedModel?.default}</Text>
-        <Text>{openedModel?.vae}</Text>
-        <Text>{openedModel?.weights}</Text>
+      <Flex
+        flexDirection="column"
+        maxHeight={window.innerHeight - 270}
+        overflowY="scroll"
+        paddingRight="2rem"
+      >
+        <Formik
+          enableReinitialize={true}
+          initialValues={editModelFormValues}
+          onSubmit={editModelFormSubmitHandler}
+        >
+          {({ handleSubmit, errors, touched }) => (
+            <form onSubmit={handleSubmit}>
+              <VStack rowGap={'0.5rem'} alignItems="start">
+                {/* 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>
+
+                <VStack 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>
+                </VStack>
+
+                <Button
+                  type="submit"
+                  className="modal-close-btn"
+                  isLoading={isProcessing}
+                >
+                  {t('modelmanager:updateModel')}
+                </Button>
+              </VStack>
+            </form>
+          )}
+        </Formik>
       </Flex>
     </Flex>
+  ) : (
+    <Flex
+      width="100%"
+      height="250px"
+      justifyContent="center"
+      alignItems="center"
+      backgroundColor="var(--background-color)"
+      borderRadius="0.5rem"
+    >
+      <Text fontWeight="bold" color="var(--subtext-color-bright)">
+        Pick A Model To Edit
+      </Text>
+    </Flex>
   );
 }