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] 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 = () => { <> -