From 8c9266359d16c02cd1d02abcca95401f9037e251 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Wed, 5 Jul 2023 23:41:15 +1200 Subject: [PATCH 01/14] feat: Add Embedding Select To Linear UI --- .../components/IAIMantineMultiSelect.tsx | 2 + .../components/ParamEmbeddingCollapse.tsx | 13 ++ .../components/ParamEmbeddingSelect.tsx | 128 ++++++++++++++++++ .../ImageToImageTabParameters.tsx | 2 + .../TextToImage/TextToImageTabParameters.tsx | 2 + .../UnifiedCanvas/UnifiedCanvasParameters.tsx | 2 + 6 files changed, 149 insertions(+) create mode 100644 invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx create mode 100644 invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingSelect.tsx diff --git a/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx index 97e33f300b..7622a604d6 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx @@ -49,6 +49,8 @@ const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { borderWidth: '2px', borderColor: mode(base200, base800)(colorMode), color: mode(base900, base100)(colorMode), + paddingTop: 6, + paddingBottom: 6, paddingRight: 24, fontWeight: 600, '&:hover': { borderColor: mode(base300, base600)(colorMode) }, diff --git a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx new file mode 100644 index 0000000000..cb3a7bc7e0 --- /dev/null +++ b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx @@ -0,0 +1,13 @@ +import { Flex } from '@chakra-ui/react'; +import IAICollapse from 'common/components/IAICollapse'; +import ParamEmbeddingSelect from './ParamEmbeddingSelect'; + +export default function ParamEmbeddingCollapse() { + return ( + + + + + + ); +} diff --git a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingSelect.tsx b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingSelect.tsx new file mode 100644 index 0000000000..5e273b43f3 --- /dev/null +++ b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingSelect.tsx @@ -0,0 +1,128 @@ +import { Flex, Text } from '@chakra-ui/react'; +import { RootState } from 'app/store/store'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIButton from 'common/components/IAIButton'; +import IAIMantineMultiSelect from 'common/components/IAIMantineMultiSelect'; +import { + setNegativePrompt, + setPositivePrompt, +} from 'features/parameters/store/generationSlice'; +import { forEach, join, map } from 'lodash-es'; +import { forwardRef, useMemo, useState } from 'react'; +import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models'; + +type EmbeddingSelectItem = { + label: string; + value: string; + description?: string; +}; + +export default function ParamEmbeddingSelect() { + const { data: embeddingQueryData } = useGetTextualInversionModelsQuery(); + const [selectedEmbeddings, setSelectedEmbeddings] = useState< + string[] | undefined + >(undefined); + + const dispatch = useAppDispatch(); + + const positivePrompt = useAppSelector( + (state: RootState) => state.generation.positivePrompt + ); + + const negativePrompt = useAppSelector( + (state: RootState) => state.generation.negativePrompt + ); + + const data = useMemo(() => { + if (!embeddingQueryData) { + return []; + } + + const data: EmbeddingSelectItem[] = []; + + forEach(embeddingQueryData.entities, (embedding, _) => { + if (!embedding) return; + + data.push({ + value: embedding.name, + label: embedding.name, + description: embedding.description, + }); + }); + + return data; + }, [embeddingQueryData]); + + const handlePositiveAdd = () => { + if (!selectedEmbeddings) return; + const parsedEmbeddings = join( + map(selectedEmbeddings, (embedding) => `<${embedding}>`), + ' ' + ); + dispatch(setPositivePrompt(`${positivePrompt} ${parsedEmbeddings}`)); + setSelectedEmbeddings([]); + }; + + const handleNegativeAdd = () => { + if (!selectedEmbeddings) return; + const parsedEmbeddings = join( + map(selectedEmbeddings, (embedding) => `<${embedding}>`), + ' ' + ); + dispatch(setNegativePrompt(`${negativePrompt} ${parsedEmbeddings}`)); + setSelectedEmbeddings([]); + }; + + return ( + + setSelectedEmbeddings(v)} + data={data} + maxDropdownHeight={400} + nothingFound="No matching Embeddings" + itemComponent={SelectItem} + disabled={data.length === 0} + filter={(value, selected, item: EmbeddingSelectItem) => + item.label.toLowerCase().includes(value.toLowerCase().trim()) || + item.value.toLowerCase().includes(value.toLowerCase().trim()) + } + clearable + /> + + + Add To Positive + + + Add To Negative + + + + ); +} + +interface ItemProps extends React.ComponentPropsWithoutRef<'div'> { + value: string; + label: string; + description?: string; +} + +const SelectItem = forwardRef( + ({ label, description, ...others }: ItemProps, ref) => { + return ( +
+
+ {label} + {description && ( + + {description} + + )} +
+
+ ); + } +); + +SelectItem.displayName = 'SelectItem'; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx index 32b71d6187..00edbe4706 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx @@ -1,4 +1,5 @@ import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/ParamDynamicPromptsCollapse'; +import ParamEmbeddingCollapse from 'features/embedding/components/ParamEmbeddingCollapse'; import ParamLoraCollapse from 'features/lora/components/ParamLoraCollapse'; import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse'; import ParamNegativeConditioning from 'features/parameters/components/Parameters/Core/ParamNegativeConditioning'; @@ -18,6 +19,7 @@ const ImageToImageTabParameters = () => { + diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx index 6291b69a8e..1853679625 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx @@ -1,4 +1,5 @@ import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/ParamDynamicPromptsCollapse'; +import ParamEmbeddingCollapse from 'features/embedding/components/ParamEmbeddingCollapse'; import ParamLoraCollapse from 'features/lora/components/ParamLoraCollapse'; import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse'; import ParamNegativeConditioning from 'features/parameters/components/Parameters/Core/ParamNegativeConditioning'; @@ -19,6 +20,7 @@ const TextToImageTabParameters = () => { + diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx index 63ed4cc1cf..bb6e7f2612 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx @@ -1,4 +1,5 @@ import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/ParamDynamicPromptsCollapse'; +import ParamEmbeddingCollapse from 'features/embedding/components/ParamEmbeddingCollapse'; import ParamLoraCollapse from 'features/lora/components/ParamLoraCollapse'; import ParamInfillAndScalingCollapse from 'features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillAndScalingCollapse'; import ParamSeamCorrectionCollapse from 'features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamCorrectionCollapse'; @@ -18,6 +19,7 @@ const UnifiedCanvasParameters = () => { + From 9204b723834cac6509e9d6ea542b1d7016fbb653 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Thu, 6 Jul 2023 01:45:00 +1200 Subject: [PATCH 02/14] feat: Make Embedding Picker a mini toggle --- .../components/ParamEmbeddingCollapse.tsx | 11 +++++++--- .../Core/ParamPositiveConditioning.tsx | 22 ++++++++++++++++--- .../ImageToImageTabParameters.tsx | 2 +- .../TextToImage/TextToImageTabParameters.tsx | 2 +- .../UnifiedCanvas/UnifiedCanvasParameters.tsx | 2 +- .../web/src/features/ui/store/uiSlice.ts | 7 +++++- .../web/src/features/ui/store/uiTypes.ts | 1 + 7 files changed, 37 insertions(+), 10 deletions(-) diff --git a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx index cb3a7bc7e0..4afce97ee5 100644 --- a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx +++ b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx @@ -1,13 +1,18 @@ import { Flex } from '@chakra-ui/react'; -import IAICollapse from 'common/components/IAICollapse'; +import { RootState } from 'app/store/store'; +import { useAppSelector } from 'app/store/storeHooks'; import ParamEmbeddingSelect from './ParamEmbeddingSelect'; export default function ParamEmbeddingCollapse() { + const shouldShowEmbeddingPicker = useAppSelector( + (state: RootState) => state.ui.shouldShowEmbeddingPicker + ); + return ( - + shouldShowEmbeddingPicker && ( - + ) ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx index f42942a84b..4a03f83c7c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx @@ -11,12 +11,15 @@ import { } from 'features/parameters/store/generationSlice'; import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { userInvoked } from 'app/store/actions'; +import IAIIconButton from 'common/components/IAIIconButton'; +import IAITextarea from 'common/components/IAITextarea'; +import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; +import { toggleEmbeddingPicker } from 'features/ui/store/uiSlice'; import { isEqual } from 'lodash-es'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -import { userInvoked } from 'app/store/actions'; -import IAITextarea from 'common/components/IAITextarea'; -import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; +import { BiCode } from 'react-icons/bi'; const promptInputSelector = createSelector( [(state: RootState) => state.generation, activeTabNameSelector], @@ -68,8 +71,21 @@ const ParamPositiveConditioning = () => { [dispatch, activeTabName, isReady] ); + const shouldShowEmbeddingPicker = useAppSelector( + (state: RootState) => state.ui.shouldShowEmbeddingPicker + ); + return ( + } + sx={{ position: 'absolute', top: 8, right: 2, zIndex: 2 }} + isChecked={shouldShowEmbeddingPicker} + onClick={() => dispatch(toggleEmbeddingPicker())} + > { <> + - diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx index 1853679625..edd5e22d83 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx @@ -18,9 +18,9 @@ const TextToImageTabParameters = () => { <> + - diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx index bb6e7f2612..bfe9584b86 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx @@ -17,9 +17,9 @@ const UnifiedCanvasParameters = () => { <> + - diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 38af668cac..861bf49405 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -1,10 +1,10 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; import { initialImageChanged } from 'features/parameters/store/generationSlice'; +import { SchedulerParam } from 'features/parameters/store/parameterZodSchemas'; import { setActiveTabReducer } from './extraReducers'; import { InvokeTabName } from './tabMap'; import { AddNewModelType, UIState } from './uiTypes'; -import { SchedulerParam } from 'features/parameters/store/parameterZodSchemas'; export const initialUIState: UIState = { activeTab: 0, @@ -19,6 +19,7 @@ export const initialUIState: UIState = { shouldShowGallery: true, shouldHidePreview: false, shouldShowProgressInViewer: true, + shouldShowEmbeddingPicker: false, favoriteSchedulers: [], }; @@ -96,6 +97,9 @@ export const uiSlice = createSlice({ ) => { state.favoriteSchedulers = action.payload; }, + toggleEmbeddingPicker: (state) => { + state.shouldShowEmbeddingPicker = !state.shouldShowEmbeddingPicker; + }, }, extraReducers(builder) { builder.addCase(initialImageChanged, (state) => { @@ -122,6 +126,7 @@ export const { toggleGalleryPanel, setShouldShowProgressInViewer, favoriteSchedulersChanged, + toggleEmbeddingPicker, } = uiSlice.actions; export default uiSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index d55a1d8fcf..ad0250e56d 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -27,5 +27,6 @@ export interface UIState { shouldPinGallery: boolean; shouldShowGallery: boolean; shouldShowProgressInViewer: boolean; + shouldShowEmbeddingPicker: boolean; favoriteSchedulers: SchedulerParam[]; } From e4d92da3a944ea752b4e370a0616127462772155 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Thu, 6 Jul 2023 01:48:50 +1200 Subject: [PATCH 03/14] fix: Make space for icons in prompt box --- .../components/Parameters/Core/ParamPositiveConditioning.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx index 4a03f83c7c..e9c5ae6edd 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx @@ -97,6 +97,7 @@ const ParamPositiveConditioning = () => { resize="vertical" ref={promptRef} minH={32} + paddingRight={8} /> From 90ae8ce26a3f82c0924aef5df4a031e3b1c328ce Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 5 Jul 2023 15:38:07 -0400 Subject: [PATCH 04/14] prevent model install crash "torch needs to be restarted with spawn" --- invokeai/backend/model_management/model_probe.py | 3 +-- invokeai/frontend/install/model_install.py | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/invokeai/backend/model_management/model_probe.py b/invokeai/backend/model_management/model_probe.py index 2828cc7ab1..938868e714 100644 --- a/invokeai/backend/model_management/model_probe.py +++ b/invokeai/backend/model_management/model_probe.py @@ -78,7 +78,6 @@ class ModelProbe(object): format_type = 'diffusers' if model_path.is_dir() else 'checkpoint' else: format_type = 'diffusers' if isinstance(model,(ConfigMixin,ModelMixin)) else 'checkpoint' - model_info = None try: model_type = cls.get_model_type_from_folder(model_path, model) \ @@ -105,7 +104,7 @@ class ModelProbe(object): ) else 512, ) except Exception: - return None + raise return model_info diff --git a/invokeai/frontend/install/model_install.py b/invokeai/frontend/install/model_install.py index 33ef114912..f3ebcb22be 100644 --- a/invokeai/frontend/install/model_install.py +++ b/invokeai/frontend/install/model_install.py @@ -678,9 +678,8 @@ def select_and_download_models(opt: Namespace): # this is where the TUI is called else: - # needed because the torch library is loaded, even though we don't use it - # currently commented out because it has started generating errors (?) - # torch.multiprocessing.set_start_method("spawn") + # needed to support the probe() method running under a subprocess + torch.multiprocessing.set_start_method("spawn") # the third argument is needed in the Windows 11 environment in # order to launch and resize a console window running this program From 863336acbb34e7b6aaefa6bdeeca78a4e5358138 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 5 Jul 2023 16:18:53 -0400 Subject: [PATCH 05/14] Recognize and load diffusers-style LoRAs (.bin) Prevent double-reporting of autoimported models - closes #3636 Allow autoimport of diffusers-style LoRA models - closes #3637 --- invokeai/backend/install/model_install_backend.py | 5 ++++- invokeai/backend/model_management/model_manager.py | 5 +++-- invokeai/backend/model_management/model_probe.py | 6 ++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/invokeai/backend/install/model_install_backend.py b/invokeai/backend/install/model_install_backend.py index 00646e70e3..86a922c05a 100644 --- a/invokeai/backend/install/model_install_backend.py +++ b/invokeai/backend/install/model_install_backend.py @@ -193,7 +193,10 @@ class ModelInstall(object): models_installed.update(self._install_path(path)) # folders style or similar - elif path.is_dir() and any([(path/x).exists() for x in {'config.json','model_index.json','learned_embeds.bin'}]): + elif path.is_dir() and any([(path/x).exists() for x in \ + {'config.json','model_index.json','learned_embeds.bin','pytorch_lora_weights.bin'} + ] + ): models_installed.update(self._install_path(path)) # recursive scan diff --git a/invokeai/backend/model_management/model_manager.py b/invokeai/backend/model_management/model_manager.py index f15dcfac3c..db8a691d29 100644 --- a/invokeai/backend/model_management/model_manager.py +++ b/invokeai/backend/model_management/model_manager.py @@ -785,7 +785,7 @@ class ModelManager(object): if path in known_paths or path.parent in scanned_dirs: scanned_dirs.add(path) continue - if any([(path/x).exists() for x in {'config.json','model_index.json','learned_embeds.bin'}]): + if any([(path/x).exists() for x in {'config.json','model_index.json','learned_embeds.bin','pytorch_lora_weights.bin'}]): new_models_found.update(installer.heuristic_import(path)) scanned_dirs.add(path) @@ -794,7 +794,8 @@ class ModelManager(object): if path in known_paths or path.parent in scanned_dirs: continue if path.suffix in {'.ckpt','.bin','.pth','.safetensors','.pt'}: - new_models_found.update(installer.heuristic_import(path)) + import_result = installer.heuristic_import(path) + new_models_found.update(import_result) self.logger.info(f'Scanned {items_scanned} files and directories, imported {len(new_models_found)} models') installed.update(new_models_found) diff --git a/invokeai/backend/model_management/model_probe.py b/invokeai/backend/model_management/model_probe.py index 938868e714..eef3292d6d 100644 --- a/invokeai/backend/model_management/model_probe.py +++ b/invokeai/backend/model_management/model_probe.py @@ -126,6 +126,8 @@ class ModelProbe(object): return ModelType.Vae elif any(key.startswith(v) for v in {"lora_te_", "lora_unet_"}): return ModelType.Lora + elif any(key.endswith(v) for v in {"to_k_lora.up.weight", "to_q_lora.down.weight"}): + return ModelType.Lora elif any(key.startswith(v) for v in {"control_model", "input_blocks"}): return ModelType.ControlNet elif key in {"emb_params", "string_to_param"}: @@ -136,7 +138,7 @@ class ModelProbe(object): if len(ckpt) < 10 and all(isinstance(v, torch.Tensor) for v in ckpt.values()): return ModelType.TextualInversion - raise ValueError("Unable to determine model type") + raise ValueError(f"Unable to determine model type for {model_path}") @classmethod def get_model_type_from_folder(cls, folder_path: Path, model: ModelMixin)->ModelType: @@ -166,7 +168,7 @@ class ModelProbe(object): return type # give up - raise ValueError("Unable to determine model type") + raise ValueError("Unable to determine model type for {folder_path}") @classmethod def _scan_and_load_checkpoint(cls,model_path: Path)->dict: From 685a47cc7de17a762873a3f393e8013af4158ee5 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 5 Jul 2023 16:40:47 -0400 Subject: [PATCH 06/14] fix crash during lora application --- invokeai/backend/model_management/lora.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/invokeai/backend/model_management/lora.py b/invokeai/backend/model_management/lora.py index 5d27555ab3..864905d6cf 100644 --- a/invokeai/backend/model_management/lora.py +++ b/invokeai/backend/model_management/lora.py @@ -3,15 +3,13 @@ from __future__ import annotations import copy from contextlib import contextmanager from pathlib import Path -from typing import Any, Dict, Optional, Tuple +from typing import Any, Dict, Optional, Tuple, Union, List import torch from compel.embeddings_provider import BaseTextualInversionManager from diffusers.models import UNet2DConditionModel from safetensors.torch import load_file -from torch.utils.hooks import RemovableHandle -from transformers import CLIPTextModel - +from transformers import CLIPTextModel, CLIPTokenizer class LoRALayerBase: #rank: Optional[int] @@ -123,8 +121,8 @@ class LoRALayer(LoRALayerBase): def get_weight(self): if self.mid is not None: - up = self.up.reshape(up.shape[0], up.shape[1]) - down = self.down.reshape(up.shape[0], up.shape[1]) + up = self.up.reshape(self.up.shape[0], self.up.shape[1]) + down = self.down.reshape(self.down.shape[0], self.down.shape[1]) weight = torch.einsum("m n w h, i m, n j -> i j w h", self.mid, up, down) else: weight = self.up.reshape(self.up.shape[0], -1) @ self.down.reshape(self.down.shape[0], -1) @@ -410,7 +408,7 @@ class LoRAModel: #(torch.nn.Module): else: # TODO: diff/ia3/... format print( - f">> Encountered unknown lora layer module in {self.name}: {layer_key}" + f">> Encountered unknown lora layer module in {model.name}: {layer_key}" ) return From c21bd806f0767280d79dfa89d833204ff298eff2 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 5 Jul 2023 16:54:23 -0400 Subject: [PATCH 07/14] default LoRA weight to 0.75 --- .../frontend/web/src/features/lora/components/ParamLora.tsx | 2 +- invokeai/frontend/web/src/features/lora/store/loraSlice.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx b/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx index 23459e9410..a32fee5f2c 100644 --- a/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx +++ b/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx @@ -22,7 +22,7 @@ const ParamLora = (props: Props) => { ); const handleReset = useCallback(() => { - dispatch(loraWeightChanged({ id: lora.id, weight: 1 })); + dispatch(loraWeightChanged({ id: lora.id, weight: 0.75 })); }, [dispatch, lora.id]); const handleRemoveLora = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts index c9b290eb2d..1469207077 100644 --- a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts +++ b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts @@ -8,7 +8,7 @@ export type Lora = { }; export const defaultLoRAConfig: Omit = { - weight: 1, + weight: 0.75, }; export type LoraState = { From 2415dc1235d7146c518c93293f1fa6e2ccb2eccc Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jul 2023 13:40:13 +1000 Subject: [PATCH 08/14] feat(ui): refactor embedding ui; now is autocomplete --- .../components/IAIMantineMultiSelect.tsx | 8 +- .../components/AddEmbeddingButton.tsx | 33 +++++ .../components/ParamEmbeddingCollapse.tsx | 18 --- .../components/ParamEmbeddingPopover.tsx | 129 ++++++++++++++++++ .../components/ParamEmbeddingSelect.tsx | 128 ----------------- .../embedding/store/embeddingSlice.ts | 0 .../features/lora/components/ParamLora.tsx | 9 +- .../web/src/features/lora/store/loraSlice.ts | 7 +- .../Core/ParamNegativeConditioning.tsx | 83 +++++++++-- .../Core/ParamPositiveConditioning.tsx | 95 ++++++++----- .../components/PinParametersPanelButton.tsx | 40 +++--- .../ImageToImageTabParameters.tsx | 2 - .../TextToImage/TextToImageTabParameters.tsx | 2 - .../UnifiedCanvas/UnifiedCanvasParameters.tsx | 2 - 14 files changed, 332 insertions(+), 224 deletions(-) create mode 100644 invokeai/frontend/web/src/features/embedding/components/AddEmbeddingButton.tsx delete mode 100644 invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx create mode 100644 invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx delete mode 100644 invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingSelect.tsx create mode 100644 invokeai/frontend/web/src/features/embedding/store/embeddingSlice.ts diff --git a/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx b/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx index 7622a604d6..9a0bc865a4 100644 --- a/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx +++ b/invokeai/frontend/web/src/common/components/IAIMantineMultiSelect.tsx @@ -1,15 +1,16 @@ import { Tooltip, useColorMode, useToken } from '@chakra-ui/react'; import { MultiSelect, MultiSelectProps } from '@mantine/core'; import { useChakraThemeTokens } from 'common/hooks/useChakraThemeTokens'; -import { memo } from 'react'; +import { RefObject, memo } from 'react'; import { mode } from 'theme/util/mode'; type IAIMultiSelectProps = MultiSelectProps & { tooltip?: string; + inputRef?: RefObject; }; const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { - const { searchable = true, tooltip, ...rest } = props; + const { searchable = true, tooltip, inputRef, ...rest } = props; const { base50, base100, @@ -33,6 +34,7 @@ const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { return ( ({ label: { @@ -49,8 +51,6 @@ const IAIMantineMultiSelect = (props: IAIMultiSelectProps) => { borderWidth: '2px', borderColor: mode(base200, base800)(colorMode), color: mode(base900, base100)(colorMode), - paddingTop: 6, - paddingBottom: 6, paddingRight: 24, fontWeight: 600, '&:hover': { borderColor: mode(base300, base600)(colorMode) }, diff --git a/invokeai/frontend/web/src/features/embedding/components/AddEmbeddingButton.tsx b/invokeai/frontend/web/src/features/embedding/components/AddEmbeddingButton.tsx new file mode 100644 index 0000000000..1dae6f56e6 --- /dev/null +++ b/invokeai/frontend/web/src/features/embedding/components/AddEmbeddingButton.tsx @@ -0,0 +1,33 @@ +import IAIIconButton from 'common/components/IAIIconButton'; +import { memo } from 'react'; +import { BiCode } from 'react-icons/bi'; + +type Props = { + onClick: () => void; +}; + +const AddEmbeddingButton = (props: Props) => { + const { onClick } = props; + return ( + } + sx={{ + p: 2, + color: 'base.700', + _hover: { + color: 'base.550', + }, + _active: { + color: 'base.500', + }, + }} + variant="link" + onClick={onClick} + /> + ); +}; + +export default memo(AddEmbeddingButton); diff --git a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx deleted file mode 100644 index 4afce97ee5..0000000000 --- a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingCollapse.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Flex } from '@chakra-ui/react'; -import { RootState } from 'app/store/store'; -import { useAppSelector } from 'app/store/storeHooks'; -import ParamEmbeddingSelect from './ParamEmbeddingSelect'; - -export default function ParamEmbeddingCollapse() { - const shouldShowEmbeddingPicker = useAppSelector( - (state: RootState) => state.ui.shouldShowEmbeddingPicker - ); - - return ( - shouldShowEmbeddingPicker && ( - - - - ) - ); -} diff --git a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx new file mode 100644 index 0000000000..df89e0b686 --- /dev/null +++ b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx @@ -0,0 +1,129 @@ +import { + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, + Text, +} from '@chakra-ui/react'; +import IAIMantineMultiSelect from 'common/components/IAIMantineMultiSelect'; +import { forEach } from 'lodash-es'; +import { + PropsWithChildren, + forwardRef, + useCallback, + useMemo, + useRef, +} from 'react'; +import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models'; +import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; + +type EmbeddingSelectItem = { + label: string; + value: string; + description?: string; +}; + +type Props = PropsWithChildren & { + onSelect: (v: string) => void; + isOpen: boolean; + onClose: () => void; +}; + +const ParamEmbeddingPopover = (props: Props) => { + const { onSelect, isOpen, onClose, children } = props; + const { data: embeddingQueryData } = useGetTextualInversionModelsQuery(); + const inputRef = useRef(null); + + const data = useMemo(() => { + if (!embeddingQueryData) { + return []; + } + + const data: EmbeddingSelectItem[] = []; + + forEach(embeddingQueryData.entities, (embedding, _) => { + if (!embedding) return; + + data.push({ + value: embedding.name, + label: embedding.name, + description: embedding.description, + }); + }); + + return data; + }, [embeddingQueryData]); + + const handleChange = useCallback( + (v: string[]) => { + if (v.length === 0) { + return; + } + + onSelect(v[0]); + }, + [onSelect] + ); + + return ( + + {children} + + + + item.label.toLowerCase().includes(value.toLowerCase().trim()) || + item.value.toLowerCase().includes(value.toLowerCase().trim()) + } + onChange={handleChange} + /> + + + + ); +}; + +export default ParamEmbeddingPopover; + +interface ItemProps extends React.ComponentPropsWithoutRef<'div'> { + value: string; + label: string; + description?: string; +} + +const SelectItem = forwardRef( + ({ label, description, ...others }: ItemProps, ref) => { + return ( +
+
+ {label} + {description && ( + + {description} + + )} +
+
+ ); + } +); + +SelectItem.displayName = 'SelectItem'; diff --git a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingSelect.tsx b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingSelect.tsx deleted file mode 100644 index 5e273b43f3..0000000000 --- a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingSelect.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Flex, Text } from '@chakra-ui/react'; -import { RootState } from 'app/store/store'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import IAIButton from 'common/components/IAIButton'; -import IAIMantineMultiSelect from 'common/components/IAIMantineMultiSelect'; -import { - setNegativePrompt, - setPositivePrompt, -} from 'features/parameters/store/generationSlice'; -import { forEach, join, map } from 'lodash-es'; -import { forwardRef, useMemo, useState } from 'react'; -import { useGetTextualInversionModelsQuery } from 'services/api/endpoints/models'; - -type EmbeddingSelectItem = { - label: string; - value: string; - description?: string; -}; - -export default function ParamEmbeddingSelect() { - const { data: embeddingQueryData } = useGetTextualInversionModelsQuery(); - const [selectedEmbeddings, setSelectedEmbeddings] = useState< - string[] | undefined - >(undefined); - - const dispatch = useAppDispatch(); - - const positivePrompt = useAppSelector( - (state: RootState) => state.generation.positivePrompt - ); - - const negativePrompt = useAppSelector( - (state: RootState) => state.generation.negativePrompt - ); - - const data = useMemo(() => { - if (!embeddingQueryData) { - return []; - } - - const data: EmbeddingSelectItem[] = []; - - forEach(embeddingQueryData.entities, (embedding, _) => { - if (!embedding) return; - - data.push({ - value: embedding.name, - label: embedding.name, - description: embedding.description, - }); - }); - - return data; - }, [embeddingQueryData]); - - const handlePositiveAdd = () => { - if (!selectedEmbeddings) return; - const parsedEmbeddings = join( - map(selectedEmbeddings, (embedding) => `<${embedding}>`), - ' ' - ); - dispatch(setPositivePrompt(`${positivePrompt} ${parsedEmbeddings}`)); - setSelectedEmbeddings([]); - }; - - const handleNegativeAdd = () => { - if (!selectedEmbeddings) return; - const parsedEmbeddings = join( - map(selectedEmbeddings, (embedding) => `<${embedding}>`), - ' ' - ); - dispatch(setNegativePrompt(`${negativePrompt} ${parsedEmbeddings}`)); - setSelectedEmbeddings([]); - }; - - return ( - - setSelectedEmbeddings(v)} - data={data} - maxDropdownHeight={400} - nothingFound="No matching Embeddings" - itemComponent={SelectItem} - disabled={data.length === 0} - filter={(value, selected, item: EmbeddingSelectItem) => - item.label.toLowerCase().includes(value.toLowerCase().trim()) || - item.value.toLowerCase().includes(value.toLowerCase().trim()) - } - clearable - /> - - - Add To Positive - - - Add To Negative - - - - ); -} - -interface ItemProps extends React.ComponentPropsWithoutRef<'div'> { - value: string; - label: string; - description?: string; -} - -const SelectItem = forwardRef( - ({ label, description, ...others }: ItemProps, ref) => { - return ( -
-
- {label} - {description && ( - - {description} - - )} -
-
- ); - } -); - -SelectItem.displayName = 'SelectItem'; diff --git a/invokeai/frontend/web/src/features/embedding/store/embeddingSlice.ts b/invokeai/frontend/web/src/features/embedding/store/embeddingSlice.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx b/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx index a32fee5f2c..4ca9700a8c 100644 --- a/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx +++ b/invokeai/frontend/web/src/features/lora/components/ParamLora.tsx @@ -4,7 +4,12 @@ import IAIIconButton from 'common/components/IAIIconButton'; import IAISlider from 'common/components/IAISlider'; import { memo, useCallback } from 'react'; import { FaTrash } from 'react-icons/fa'; -import { Lora, loraRemoved, loraWeightChanged } from '../store/loraSlice'; +import { + Lora, + loraRemoved, + loraWeightChanged, + loraWeightReset, +} from '../store/loraSlice'; type Props = { lora: Lora; @@ -22,7 +27,7 @@ const ParamLora = (props: Props) => { ); const handleReset = useCallback(() => { - dispatch(loraWeightChanged({ id: lora.id, weight: 0.75 })); + dispatch(loraWeightReset(lora.id)); }, [dispatch, lora.id]); const handleRemoveLora = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts index 1469207077..7da6018e58 100644 --- a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts +++ b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts @@ -38,9 +38,14 @@ export const loraSlice = createSlice({ const { id, weight } = action.payload; state.loras[id].weight = weight; }, + loraWeightReset: (state, action: PayloadAction) => { + const id = action.payload; + state.loras[id].weight = defaultLoRAConfig.weight; + }, }, }); -export const { loraAdded, loraRemoved, loraWeightChanged } = loraSlice.actions; +export const { loraAdded, loraRemoved, loraWeightChanged, loraWeightReset } = + loraSlice.actions; export default loraSlice.reducer; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx index 589b751d6b..ec3dd416d0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamNegativeConditioning.tsx @@ -1,29 +1,90 @@ -import { FormControl } from '@chakra-ui/react'; +import { Box, FormControl, useDisclosure } from '@chakra-ui/react'; import type { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAITextarea from 'common/components/IAITextarea'; +import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton'; +import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover'; import { setNegativePrompt } from 'features/parameters/store/generationSlice'; +import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; const ParamNegativeConditioning = () => { const negativePrompt = useAppSelector( (state: RootState) => state.generation.negativePrompt ); - + const promptRef = useRef(null); + const { isOpen, onClose, onOpen } = useDisclosure(); const dispatch = useAppDispatch(); const { t } = useTranslation(); + const handleChangePrompt = useCallback( + (e: ChangeEvent) => { + dispatch(setNegativePrompt(e.target.value)); + }, + [dispatch] + ); + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === '<') { + onOpen(); + } + }, + [onOpen] + ); + + const handleSelect = useCallback( + (v: string) => { + const caret = promptRef.current?.selectionStart; + + if (caret === undefined) { + return; + } + + let newPrompt = negativePrompt.slice(0, caret); + + if (newPrompt[newPrompt.length - 1] !== '<') { + newPrompt += '<'; + } + + newPrompt += `${v}>`; + newPrompt += negativePrompt.slice(caret); + + dispatch(setNegativePrompt(newPrompt)); + }, + [dispatch, negativePrompt] + ); + return ( - dispatch(setNegativePrompt(e.target.value))} - placeholder={t('parameters.negativePromptPlaceholder')} - fontSize="sm" - minH={16} - /> + + + + {!isOpen && ( + + + + )} ); }; diff --git a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx index e9c5ae6edd..ab52264fdf 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Parameters/Core/ParamPositiveConditioning.tsx @@ -1,4 +1,4 @@ -import { Box, FormControl } from '@chakra-ui/react'; +import { Box, FormControl, useDisclosure } from '@chakra-ui/react'; import { RootState } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { ChangeEvent, KeyboardEvent, useCallback, useRef } from 'react'; @@ -12,14 +12,13 @@ import { import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { userInvoked } from 'app/store/actions'; -import IAIIconButton from 'common/components/IAIIconButton'; import IAITextarea from 'common/components/IAITextarea'; import { useIsReadyToInvoke } from 'common/hooks/useIsReadyToInvoke'; -import { toggleEmbeddingPicker } from 'features/ui/store/uiSlice'; +import AddEmbeddingButton from 'features/embedding/components/AddEmbeddingButton'; +import ParamEmbeddingPopover from 'features/embedding/components/ParamEmbeddingPopover'; import { isEqual } from 'lodash-es'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -import { BiCode } from 'react-icons/bi'; const promptInputSelector = createSelector( [(state: RootState) => state.generation, activeTabNameSelector], @@ -43,14 +42,16 @@ const ParamPositiveConditioning = () => { const dispatch = useAppDispatch(); const { prompt, activeTabName } = useAppSelector(promptInputSelector); const isReady = useIsReadyToInvoke(); - const promptRef = useRef(null); - + const { isOpen, onClose, onOpen } = useDisclosure(); const { t } = useTranslation(); - const handleChangePrompt = (e: ChangeEvent) => { - dispatch(setPositivePrompt(e.target.value)); - }; + const handleChangePrompt = useCallback( + (e: ChangeEvent) => { + dispatch(setPositivePrompt(e.target.value)); + }, + [dispatch] + ); useHotkeys( 'alt+a', @@ -67,39 +68,67 @@ const ParamPositiveConditioning = () => { dispatch(clampSymmetrySteps()); dispatch(userInvoked(activeTabName)); } + if (e.key === '<') { + onOpen(); + } }, - [dispatch, activeTabName, isReady] + [isReady, dispatch, activeTabName, onOpen] ); - const shouldShowEmbeddingPicker = useAppSelector( - (state: RootState) => state.ui.shouldShowEmbeddingPicker + const handleSelect = useCallback( + (v: string) => { + const caret = promptRef.current?.selectionStart; + + if (caret === undefined) { + return; + } + + let newPrompt = prompt.slice(0, caret); + + if (newPrompt[newPrompt.length - 1] !== '<') { + newPrompt += '<'; + } + + newPrompt += `${v}>`; + newPrompt += prompt.slice(caret); + + dispatch(setPositivePrompt(newPrompt)); + }, + [dispatch, prompt] ); return ( - } - sx={{ position: 'absolute', top: 8, right: 2, zIndex: 2 }} - isChecked={shouldShowEmbeddingPicker} - onClick={() => dispatch(toggleEmbeddingPicker())} - > - + + + + {!isOpen && ( + + + + )} ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx b/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx index a742e2a587..30cc1d2158 100644 --- a/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx +++ b/invokeai/frontend/web/src/features/ui/components/PinParametersPanelButton.tsx @@ -1,4 +1,3 @@ -import { Tooltip } from '@chakra-ui/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIIconButton, { IAIIconButtonProps, @@ -25,26 +24,25 @@ const PinParametersPanelButton = (props: PinParametersPanelButtonProps) => { }; return ( - - : } - variant="ghost" - size="sm" - sx={{ - color: 'base.700', - _hover: { - color: 'base.550', - }, - _active: { - color: 'base.500', - }, - ...sx, - }} - /> - + : } + variant="ghost" + size="sm" + sx={{ + color: 'base.700', + _hover: { + color: 'base.550', + }, + _active: { + color: 'base.500', + }, + ...sx, + }} + /> ); }; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx index 1207853fe8..32b71d6187 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/ImageToImage/ImageToImageTabParameters.tsx @@ -1,5 +1,4 @@ import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/ParamDynamicPromptsCollapse'; -import ParamEmbeddingCollapse from 'features/embedding/components/ParamEmbeddingCollapse'; import ParamLoraCollapse from 'features/lora/components/ParamLoraCollapse'; import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse'; import ParamNegativeConditioning from 'features/parameters/components/Parameters/Core/ParamNegativeConditioning'; @@ -17,7 +16,6 @@ const ImageToImageTabParameters = () => { <> - diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx index edd5e22d83..6291b69a8e 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/TextToImage/TextToImageTabParameters.tsx @@ -1,5 +1,4 @@ import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/ParamDynamicPromptsCollapse'; -import ParamEmbeddingCollapse from 'features/embedding/components/ParamEmbeddingCollapse'; import ParamLoraCollapse from 'features/lora/components/ParamLoraCollapse'; import ParamControlNetCollapse from 'features/parameters/components/Parameters/ControlNet/ParamControlNetCollapse'; import ParamNegativeConditioning from 'features/parameters/components/Parameters/Core/ParamNegativeConditioning'; @@ -18,7 +17,6 @@ const TextToImageTabParameters = () => { <> - diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx index bfe9584b86..63ed4cc1cf 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/UnifiedCanvas/UnifiedCanvasParameters.tsx @@ -1,5 +1,4 @@ import ParamDynamicPromptsCollapse from 'features/dynamicPrompts/components/ParamDynamicPromptsCollapse'; -import ParamEmbeddingCollapse from 'features/embedding/components/ParamEmbeddingCollapse'; import ParamLoraCollapse from 'features/lora/components/ParamLoraCollapse'; import ParamInfillAndScalingCollapse from 'features/parameters/components/Parameters/Canvas/InfillAndScaling/ParamInfillAndScalingCollapse'; import ParamSeamCorrectionCollapse from 'features/parameters/components/Parameters/Canvas/SeamCorrection/ParamSeamCorrectionCollapse'; @@ -17,7 +16,6 @@ const UnifiedCanvasParameters = () => { <> - From fbd6b25b4dd2edd2a18fa2a11983eead77be7f05 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jul 2023 14:56:37 +1000 Subject: [PATCH 09/14] feat(ui): improve ux on TI autcomplete - cursor reinserts at the end of the trigger - `enter` closes the select - popover styling --- .../components/ParamEmbeddingPopover.tsx | 15 +++- .../Core/ParamNegativeConditioning.tsx | 27 ++++++-- .../Core/ParamPositiveConditioning.tsx | 68 ++++++++++++------- 3 files changed, 79 insertions(+), 31 deletions(-) diff --git a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx index df89e0b686..5ed5315a4b 100644 --- a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx +++ b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx @@ -68,17 +68,26 @@ const ParamEmbeddingPopover = (props: Props) => { return ( {children} - + { @@ -32,9 +33,14 @@ const ParamNegativeConditioning = () => { [onOpen] ); - const handleSelect = useCallback( + const handleSelectEmbedding = useCallback( (v: string) => { - const caret = promptRef.current?.selectionStart; + if (!promptRef.current) { + return; + } + + // this is where we insert the TI trigger + const caret = promptRef.current.selectionStart; if (caret === undefined) { return; @@ -47,11 +53,22 @@ const ParamNegativeConditioning = () => { } newPrompt += `${v}>`; + + // we insert the cursor after the `>` + const finalCaretPos = newPrompt.length; + newPrompt += negativePrompt.slice(caret); - dispatch(setNegativePrompt(newPrompt)); + // must flush dom updates else selection gets reset + flushSync(() => { + dispatch(setNegativePrompt(newPrompt)); + }); + + // set the caret position to just after the TI trigger promptRef.current.selectionStart = finalCaretPos; + promptRef.current.selectionEnd = finalCaretPos; + onClose(); }, - [dispatch, negativePrompt] + [dispatch, onClose, negativePrompt] ); return ( @@ -59,7 +76,7 @@ const ParamNegativeConditioning = () => { { const promptRef = useRef(null); const { isOpen, onClose, onOpen } = useDisclosure(); const { t } = useTranslation(); - const handleChangePrompt = useCallback( (e: ChangeEvent) => { dispatch(setPositivePrompt(e.target.value)); @@ -61,6 +61,45 @@ const ParamPositiveConditioning = () => { [] ); + const handleSelectEmbedding = useCallback( + (v: string) => { + if (!promptRef.current) { + return; + } + + // this is where we insert the TI trigger + const caret = promptRef.current.selectionStart; + + if (caret === undefined) { + return; + } + + let newPrompt = prompt.slice(0, caret); + + if (newPrompt[newPrompt.length - 1] !== '<') { + newPrompt += '<'; + } + + newPrompt += `${v}>`; + + // we insert the cursor after the `>` + const finalCaretPos = newPrompt.length; + + newPrompt += prompt.slice(caret); + + // must flush dom updates else selection gets reset + flushSync(() => { + dispatch(setPositivePrompt(newPrompt)); + }); + + // set the caret position to just after the TI trigger + promptRef.current.selectionStart = finalCaretPos; + promptRef.current.selectionEnd = finalCaretPos; + onClose(); + }, + [dispatch, onClose, prompt] + ); + const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Enter' && e.shiftKey === false && isReady) { @@ -75,27 +114,10 @@ const ParamPositiveConditioning = () => { [isReady, dispatch, activeTabName, onOpen] ); - const handleSelect = useCallback( - (v: string) => { - const caret = promptRef.current?.selectionStart; - - if (caret === undefined) { - return; - } - - let newPrompt = prompt.slice(0, caret); - - if (newPrompt[newPrompt.length - 1] !== '<') { - newPrompt += '<'; - } - - newPrompt += `${v}>`; - newPrompt += prompt.slice(caret); - - dispatch(setPositivePrompt(newPrompt)); - }, - [dispatch, prompt] - ); + // const handleSelect = (e: MouseEvent) => { + // const target = e.target as HTMLTextAreaElement; + // setCaret({ start: target.selectionStart, end: target.selectionEnd }); + // }; return ( @@ -103,7 +125,7 @@ const ParamPositiveConditioning = () => { Date: Thu, 6 Jul 2023 22:24:50 +1000 Subject: [PATCH 10/14] feat(ui): improve no loaded embeddings UI --- .../components/ParamEmbeddingPopover.tsx | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx index 5ed5315a4b..3c2ded0166 100644 --- a/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx +++ b/invokeai/frontend/web/src/features/embedding/components/ParamEmbeddingPopover.tsx @@ -1,4 +1,5 @@ import { + Flex, Popover, PopoverBody, PopoverContent, @@ -82,28 +83,40 @@ const ParamEmbeddingPopover = (props: Props) => { p: 0, top: -1, shadow: 'dark-lg', - bg: 'accent.300', - _dark: { bg: 'accent.400' }, + borderColor: 'accent.300', + borderWidth: '2px', + borderStyle: 'solid', + _dark: { borderColor: 'accent.400' }, }} > - - item.label.toLowerCase().includes(value.toLowerCase().trim()) || - item.value.toLowerCase().includes(value.toLowerCase().trim()) - } - onChange={handleChange} - /> + {data.length === 0 ? ( + + + No Embeddings Loaded + + + ) : ( + + item.label.toLowerCase().includes(value.toLowerCase().trim()) || + item.value.toLowerCase().includes(value.toLowerCase().trim()) + } + onChange={handleChange} + /> + )} From e09c07a97d7243f2819172eb242a2d558b1c896d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jul 2023 22:25:05 +1000 Subject: [PATCH 11/14] fix(ui): fix board auto-add --- .../frontend/web/src/services/events/middleware.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/invokeai/frontend/web/src/services/events/middleware.ts b/invokeai/frontend/web/src/services/events/middleware.ts index 85641b88a0..665761a626 100644 --- a/invokeai/frontend/web/src/services/events/middleware.ts +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -1,18 +1,18 @@ import { Middleware, MiddlewareAPI } from '@reduxjs/toolkit'; -import { io, Socket } from 'socket.io-client'; +import { Socket, io } from 'socket.io-client'; +import { AppThunkDispatch, RootState } from 'app/store/store'; +import { getTimestamp } from 'common/util/getTimestamp'; +import { sessionCreated } from 'services/api/thunks/session'; import { ClientToServerEvents, ServerToClientEvents, } from 'services/events/types'; import { socketSubscribed, socketUnsubscribed } from './actions'; -import { AppThunkDispatch, RootState } from 'app/store/store'; -import { getTimestamp } from 'common/util/getTimestamp'; -import { sessionCreated } from 'services/api/thunks/session'; // import { OpenAPI } from 'services/api/types'; -import { setEventListeners } from 'services/events/util/setEventListeners'; import { log } from 'app/logging/useLogger'; import { $authToken, $baseUrl } from 'services/api/client'; +import { setEventListeners } from 'services/events/util/setEventListeners'; const socketioLog = log.child({ namespace: 'socketio' }); @@ -88,7 +88,7 @@ export const socketMiddleware = () => { socketSubscribed({ sessionId: sessionId, timestamp: getTimestamp(), - boardId: getState().boards.selectedBoardId, + boardId: getState().gallery.selectedBoardId, }) ); } From a901a37433dc155f70db460ab83aad7e3eb6e00b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jul 2023 22:26:54 +1000 Subject: [PATCH 12/14] feat(ui): improve no loaded loras UI --- .../src/features/lora/components/ParamLoraSelect.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx b/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx index 54ac3d615d..9168814f35 100644 --- a/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx +++ b/invokeai/frontend/web/src/features/lora/components/ParamLoraSelect.tsx @@ -1,4 +1,4 @@ -import { Text } from '@chakra-ui/react'; +import { Flex, Text } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { stateSelector } from 'app/store/store'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; @@ -61,6 +61,16 @@ const ParamLoraSelect = () => { [dispatch, lorasQueryData?.entities] ); + if (lorasQueryData?.ids.length === 0) { + return ( + + + No LoRAs Loaded + + + ); + } + return ( Date: Wed, 5 Jul 2023 10:47:20 -0400 Subject: [PATCH 13/14] only show delete icon if big enough --- .../web/src/features/gallery/components/GalleryImage.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx index ea0b3b0fd8..41c536759a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx @@ -23,9 +23,11 @@ export const makeSelector = (image_name: string) => ({ gallery }) => { const isSelected = gallery.selection.includes(image_name); const selectionCount = gallery.selection.length; + const galleryImageMinimumWidth = gallery.galleryImageMinimumWidth; return { isSelected, selectionCount, + galleryImageMinimumWidth, }; }, defaultSelectorOptions @@ -44,7 +46,8 @@ const GalleryImage = (props: HoverableImageProps) => { const localSelector = useMemo(() => makeSelector(image_name), [image_name]); - const { isSelected, selectionCount } = useAppSelector(localSelector); + const { isSelected, selectionCount, galleryImageMinimumWidth } = + useAppSelector(localSelector); const dispatch = useAppDispatch(); @@ -113,7 +116,9 @@ const GalleryImage = (props: HoverableImageProps) => { draggableData={draggableData} isSelected={isSelected} minSize={0} - onClickReset={handleDelete} + onClickReset={ + galleryImageMinimumWidth > 60 ? handleDelete : undefined + } resetIcon={} resetTooltip="Delete image" imageSx={{ w: 'full', h: 'full' }} From 94e38e976975b664da946af2710055ea9ac1a372 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 6 Jul 2023 22:34:11 +1000 Subject: [PATCH 14/14] feat(ui): remove delete image button in gallery it was really easy to accidentally click, just commented out, easy to add back or add a setting for it in the future --- .../src/features/gallery/components/GalleryImage.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx index 41c536759a..a8d4c84adc 100644 --- a/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/GalleryImage.tsx @@ -23,11 +23,10 @@ export const makeSelector = (image_name: string) => ({ gallery }) => { const isSelected = gallery.selection.includes(image_name); const selectionCount = gallery.selection.length; - const galleryImageMinimumWidth = gallery.galleryImageMinimumWidth; + return { isSelected, selectionCount, - galleryImageMinimumWidth, }; }, defaultSelectorOptions @@ -46,8 +45,7 @@ const GalleryImage = (props: HoverableImageProps) => { const localSelector = useMemo(() => makeSelector(image_name), [image_name]); - const { isSelected, selectionCount, galleryImageMinimumWidth } = - useAppSelector(localSelector); + const { isSelected, selectionCount } = useAppSelector(localSelector); const dispatch = useAppDispatch(); @@ -116,13 +114,11 @@ const GalleryImage = (props: HoverableImageProps) => { draggableData={draggableData} isSelected={isSelected} minSize={0} - onClickReset={ - galleryImageMinimumWidth > 60 ? handleDelete : undefined - } + onClickReset={handleDelete} resetIcon={} resetTooltip="Delete image" imageSx={{ w: 'full', h: 'full' }} - withResetIcon + // withResetIcon // removed bc it's too easy to accidentally delete images isDropDisabled={true} isUploadDisabled={true} />