From d40d5276ddb3c26088fc7dcaa20432e23d6f2655 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Mon, 24 Apr 2023 20:34:24 +1000 Subject: [PATCH] feat(ui): wip img2img ui --- invokeai/frontend/web/public/locales/en.json | 3 +- invokeai/frontend/web/src/app/App.tsx | 2 + invokeai/frontend/web/src/app/store.ts | 3 + .../web/src/common/components/IAISlider.tsx | 5 +- .../web/src/common/hooks/useGlobalHotkeys.ts | 37 +++++++++ .../components/CurrentImageDisplay.tsx | 6 +- .../gallery/components/ImageGalleryPanel.tsx | 2 +- .../AdvancedParameters/Seed/RandomizeSeed.tsx | 32 ++++++-- .../ImageDimensions/AspectRatioPreview.tsx | 75 ++++++++++++++++++ .../ImageDimensions/DimensionsSettings.tsx | 76 +++++++++++++++++++ .../MainParameters/HeightSlider.tsx | 38 ++++++++++ .../components/MainParameters/MainHeight.tsx | 2 +- .../components/MainParameters/MainSampler.tsx | 23 +++--- .../MainParameters/MainSettings.tsx | 18 +++-- .../components/MainParameters/MainWidth.tsx | 2 +- .../components/MainParameters/WidthSlider.tsx | 37 +++++++++ .../components/ParametersAccordion.tsx | 7 +- .../ProcessButtons/InvokeButton.tsx | 4 +- .../system/components/ModelSelect.tsx | 13 ++-- .../features/system/components/SiteHeader.tsx | 2 - .../FloatingParametersPanelButtons.tsx | 2 +- .../src/features/ui/components/InvokeTabs.tsx | 70 +++++++++-------- .../GenerateContent.tsx} | 4 +- .../GenerateParameters.tsx} | 28 +++++-- .../GenerateWorkspace.tsx} | 14 ++-- .../web/src/features/ui/store/hotkeysSlice.ts | 26 +++++++ .../web/src/features/ui/store/tabMap.ts | 2 +- .../web/src/features/ui/store/uiSlice.ts | 7 +- .../web/src/features/ui/store/uiTypes.ts | 3 + .../web/src/services/thunks/session.ts | 10 +-- 30 files changed, 453 insertions(+), 100 deletions(-) create mode 100644 invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts create mode 100644 invokeai/frontend/web/src/features/parameters/components/ImageDimensions/AspectRatioPreview.tsx create mode 100644 invokeai/frontend/web/src/features/parameters/components/ImageDimensions/DimensionsSettings.tsx create mode 100644 invokeai/frontend/web/src/features/parameters/components/MainParameters/HeightSlider.tsx create mode 100644 invokeai/frontend/web/src/features/parameters/components/MainParameters/WidthSlider.tsx rename invokeai/frontend/web/src/features/ui/components/tabs/{Linear/LinearContent.tsx => Generate/GenerateContent.tsx} (86%) rename invokeai/frontend/web/src/features/ui/components/tabs/{Linear/LinearParameters.tsx => Generate/GenerateParameters.tsx} (83%) rename invokeai/frontend/web/src/features/ui/components/tabs/{Linear/LinearWorkspace.tsx => Generate/GenerateWorkspace.tsx} (82%) create mode 100644 invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 6996119de3..68978baf3b 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -98,7 +98,8 @@ "pinOptionsPanel": "Pin Options Panel", "loading": "Loading", "loadingInvokeAI": "Loading Invoke AI", - "random": "Random" + "random": "Random", + "generate": "Generate" }, "gallery": { "generations": "Generations", diff --git a/invokeai/frontend/web/src/app/App.tsx b/invokeai/frontend/web/src/app/App.tsx index d2f96b0746..e1078ac411 100644 --- a/invokeai/frontend/web/src/app/App.tsx +++ b/invokeai/frontend/web/src/app/App.tsx @@ -26,6 +26,7 @@ import { } from 'features/system/store/systemSlice'; import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady'; import { ApplicationFeature } from './invokeai'; +import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; keepGUIAlive(); @@ -40,6 +41,7 @@ interface Props extends PropsWithChildren { const App = (props: Props) => { useToastWatcher(); + useGlobalHotkeys(); const currentTheme = useAppSelector((state) => state.ui.currentTheme); const disabledFeatures = useAppSelector( diff --git a/invokeai/frontend/web/src/app/store.ts b/invokeai/frontend/web/src/app/store.ts index 3e046d8ed9..05154c6987 100644 --- a/invokeai/frontend/web/src/app/store.ts +++ b/invokeai/frontend/web/src/app/store.ts @@ -14,6 +14,7 @@ import generationReducer from 'features/parameters/store/generationSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; import systemReducer from 'features/system/store/systemSlice'; import uiReducer from 'features/ui/store/uiSlice'; +import hotkeysReducer from 'features/ui/store/hotkeysSlice'; import modelsReducer from 'features/system/store/modelSlice'; import nodesReducer from 'features/nodes/store/nodesSlice'; @@ -55,6 +56,7 @@ const rootReducer = combineReducers({ system: systemReducer, ui: uiReducer, uploads: uploadsReducer, + hotkeys: hotkeysReducer, }); const rootPersistConfig = getPersistConfig({ @@ -75,6 +77,7 @@ const rootPersistConfig = getPersistConfig({ ...uiBlacklist, // ...uploadsBlacklist, 'uploads', + 'hotkeys', ], debounce: 300, }); diff --git a/invokeai/frontend/web/src/common/components/IAISlider.tsx b/invokeai/frontend/web/src/common/components/IAISlider.tsx index 189ef4f5ad..614bebe807 100644 --- a/invokeai/frontend/web/src/common/components/IAISlider.tsx +++ b/invokeai/frontend/web/src/common/components/IAISlider.tsx @@ -29,6 +29,7 @@ import { useTranslation } from 'react-i18next'; import { FocusEvent, memo, useEffect, useMemo, useState } from 'react'; import { BiReset } from 'react-icons/bi'; import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton'; +import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; export type IAIFullSliderProps = { label: string; @@ -119,7 +120,9 @@ const IAISlider = (props: IAIFullSliderProps) => { min, numberInputMax ); - onChange(clamped); + const quantized = roundDownToMultiple(clamped, step); + onChange(quantized); + setLocalInputValue(quantized); }; const handleInputChange = (v: number | string) => { diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts new file mode 100644 index 0000000000..79fcf6446a --- /dev/null +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -0,0 +1,37 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import { shiftKeyPressed } from 'features/ui/store/hotkeysSlice'; +import { isEqual } from 'lodash'; +import { isHotkeyPressed, useHotkeys } from 'react-hotkeys-hook'; + +const globalHotkeysSelector = createSelector( + (state: RootState) => state.hotkeys, + (hotkeys) => { + const { shift } = hotkeys; + return { shift }; + }, + { + memoizeOptions: { + resultEqualityCheck: isEqual, + }, + } +); + +export const useGlobalHotkeys = () => { + const dispatch = useAppDispatch(); + const { shift } = useAppSelector(globalHotkeysSelector); + + useHotkeys( + '*', + () => { + if (isHotkeyPressed('shift')) { + !shift && dispatch(shiftKeyPressed(true)); + } else { + shift && dispatch(shiftKeyPressed(false)); + } + }, + { keyup: true, keydown: true }, + [shift] + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx index 0364f879cc..1730b41095 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageDisplay.tsx @@ -59,9 +59,6 @@ const CurrentImageDisplay = () => { justifyContent: 'center', }} > - - - { /> )} + + + ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx index ea19408c65..2a557240ef 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryPanel.tsx @@ -35,7 +35,7 @@ const GALLERY_TAB_WIDTHS: Record< > = { // txt2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, // img2img: { galleryMinWidth: 200, galleryMaxWidth: 500 }, - linear: { galleryMinWidth: 200, galleryMaxWidth: 500 }, + generate: { galleryMinWidth: 200, galleryMaxWidth: 500 }, unifiedCanvas: { galleryMinWidth: 200, galleryMaxWidth: 200 }, nodes: { galleryMinWidth: 200, galleryMaxWidth: 500 }, // postprocessing: { galleryMinWidth: 200, galleryMaxWidth: 500 }, diff --git a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/RandomizeSeed.tsx b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/RandomizeSeed.tsx index ebb6196c23..576ac61aba 100644 --- a/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/RandomizeSeed.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/AdvancedParameters/Seed/RandomizeSeed.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent } from 'react'; +import { ChangeEvent, memo } from 'react'; import { RootState } from 'app/store'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; @@ -7,7 +7,27 @@ import { setShouldRandomizeSeed } from 'features/parameters/store/generationSlic import { useTranslation } from 'react-i18next'; import { Switch } from '@chakra-ui/react'; -export default function RandomizeSeed() { +// export default function RandomizeSeed() { +// const dispatch = useAppDispatch(); +// const { t } = useTranslation(); + +// const shouldRandomizeSeed = useAppSelector( +// (state: RootState) => state.generation.shouldRandomizeSeed +// ); + +// const handleChangeShouldRandomizeSeed = (e: ChangeEvent) => +// dispatch(setShouldRandomizeSeed(e.target.checked)); + +// return ( +// +// ); +// } + +const SeedToggle = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -16,13 +36,15 @@ export default function RandomizeSeed() { ); const handleChangeShouldRandomizeSeed = (e: ChangeEvent) => - dispatch(setShouldRandomizeSeed(e.target.checked)); + dispatch(setShouldRandomizeSeed(!e.target.checked)); return ( ); -} +}; + +export default memo(SeedToggle); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageDimensions/AspectRatioPreview.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageDimensions/AspectRatioPreview.tsx new file mode 100644 index 0000000000..ecf4a6713e --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/ImageDimensions/AspectRatioPreview.tsx @@ -0,0 +1,75 @@ +import { Flex, Text } from '@chakra-ui/react'; +import { memo, useMemo } from 'react'; + +export const ratioToCSSString = ( + ratio: AspectRatio, + orientation: Orientation +) => { + if (orientation === 'portrait') { + return `${ratio[0]}/${ratio[1]}`; + } + return `${ratio[1]}/${ratio[0]}`; +}; + +export const ratioToDisplayString = ( + ratio: AspectRatio, + orientation: Orientation +) => { + if (orientation === 'portrait') { + return `${ratio[0]}:${ratio[1]}`; + } + return `${ratio[1]}:${ratio[0]}`; +}; + +type AspectRatioPreviewProps = { + ratio: AspectRatio; + orientation: Orientation; + size: string; +}; + +export type AspectRatio = [number, number]; + +export type Orientation = 'portrait' | 'landscape'; + +const AspectRatioPreview = (props: AspectRatioPreviewProps) => { + const { ratio, size, orientation } = props; + + const ratioCSSString = useMemo(() => { + if (orientation === 'portrait') { + return `${ratio[0]}/${ratio[1]}`; + } + return `${ratio[1]}/${ratio[0]}`; + }, [ratio, orientation]); + + const ratioDisplayString = useMemo(() => `${ratio[0]}:${ratio[1]}`, [ratio]); + + return ( + + + + {ratioDisplayString} + + + + ); +}; + +export default memo(AspectRatioPreview); diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageDimensions/DimensionsSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageDimensions/DimensionsSettings.tsx new file mode 100644 index 0000000000..b6b1e206b6 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/ImageDimensions/DimensionsSettings.tsx @@ -0,0 +1,76 @@ +import { Box, Flex, FormControl, FormLabel, Select } from '@chakra-ui/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import IAISlider from 'common/components/IAISlider'; +import { setWidth } from 'features/parameters/store/generationSlice'; +import { memo, useState } from 'react'; +import AspectRatioPreview, { + AspectRatio, + Orientation, +} from './AspectRatioPreview'; + +const RATIOS: AspectRatio[] = [ + [1, 1], + [5, 4], + [3, 2], + [16, 10], + [16, 9], +]; + +RATIOS.forEach((r) => { + const float = r[0] / r[1]; + console.log((512 * float) / 8); +}); + +const dimensionsSettingsSelector = createSelector( + (state: RootState) => state.generation, + (generation) => { + const { width, height } = generation; + + return { width, height }; + } +); + +const DimensionsSettings = () => { + const { width, height } = useAppSelector(dimensionsSettingsSelector); + const dispatch = useAppDispatch(); + const [ratioIndex, setRatioIndex] = useState(4); + const [orientation, setOrientation] = useState('portrait'); + + return ( + + + + + + Aspect Ratio + + + { + dispatch(setWidth(v)); + }} + /> + + ); +}; + +export default memo(DimensionsSettings); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/HeightSlider.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/HeightSlider.tsx new file mode 100644 index 0000000000..87f1842f78 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/MainParameters/HeightSlider.tsx @@ -0,0 +1,38 @@ +import { Box, BoxProps } from '@chakra-ui/react'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import IAISlider from 'common/components/IAISlider'; +import { setHeight } from 'features/parameters/store/generationSlice'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { memo } from 'react'; + +import { useTranslation } from 'react-i18next'; + +const HeightSlider = (props: BoxProps) => { + const height = useAppSelector((state: RootState) => state.generation.height); + const shift = useAppSelector((state: RootState) => state.hotkeys.shift); + const activeTabName = useAppSelector(activeTabNameSelector); + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + return ( + + dispatch(setHeight(v))} + handleReset={() => dispatch(setHeight(512))} + withInput + withReset + withSliderMarks + sliderNumberInputProps={{ max: 15360 }} + /> + + ); +}; + +export default memo(HeightSlider); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx index 8dbf70eab5..744e3a0967 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainHeight.tsx @@ -23,7 +23,7 @@ export default function MainHeight() { label={t('parameters.height')} value={height} min={64} - step={64} + step={8} max={2048} onChange={(v) => dispatch(setHeight(v))} handleReset={() => dispatch(setHeight(512))} diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSampler.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSampler.tsx index 0f55cca12a..5e010d7912 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSampler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSampler.tsx @@ -1,3 +1,4 @@ +import { Box, BoxProps } from '@chakra-ui/react'; import { DIFFUSERS_SAMPLERS, SAMPLERS } from 'app/constants'; import { RootState } from 'app/store'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; @@ -7,7 +8,7 @@ import { activeModelSelector } from 'features/system/store/systemSelectors'; import { ChangeEvent } from 'react'; import { useTranslation } from 'react-i18next'; -export default function MainSampler() { +export default function MainSampler(props: BoxProps) { const sampler = useAppSelector( (state: RootState) => state.generation.sampler ); @@ -19,14 +20,16 @@ export default function MainSampler() { dispatch(setSampler(e.target.value)); return ( - + + + ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSettings.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSettings.tsx index cad30e58e6..5ed0076153 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSettings.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainSettings.tsx @@ -1,12 +1,15 @@ -import { Flex, VStack } from '@chakra-ui/react'; +import { Divider, Flex, VStack } from '@chakra-ui/react'; import { RootState } from 'app/store'; import { useAppSelector } from 'app/storeHooks'; +import { ModelSelect } from 'exports'; +import HeightSlider from './HeightSlider'; import MainCFGScale from './MainCFGScale'; import MainHeight from './MainHeight'; import MainIterations from './MainIterations'; import MainSampler from './MainSampler'; import MainSteps from './MainSteps'; import MainWidth from './MainWidth'; +import WidthSlider from './WidthSlider'; export default function MainSettings() { const shouldUseSliders = useAppSelector( @@ -23,17 +26,18 @@ export default function MainSettings() { ) : ( - - + + - - - - + + + + + ); } diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainWidth.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainWidth.tsx index f6045be4e4..81942b83f9 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainWidth.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainParameters/MainWidth.tsx @@ -22,7 +22,7 @@ export default function MainWidth() { isDisabled={activeTabName === 'unifiedCanvas'} label={t('parameters.width')} value={width} - min={64} + min={8} step={64} max={2048} onChange={(v) => dispatch(setWidth(v))} diff --git a/invokeai/frontend/web/src/features/parameters/components/MainParameters/WidthSlider.tsx b/invokeai/frontend/web/src/features/parameters/components/MainParameters/WidthSlider.tsx new file mode 100644 index 0000000000..d450b217f9 --- /dev/null +++ b/invokeai/frontend/web/src/features/parameters/components/MainParameters/WidthSlider.tsx @@ -0,0 +1,37 @@ +import { Box, BoxProps } from '@chakra-ui/react'; +import { RootState } from 'app/store'; +import { useAppDispatch, useAppSelector } from 'app/storeHooks'; +import IAISlider from 'common/components/IAISlider'; +import { setWidth } from 'features/parameters/store/generationSlice'; +import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +const WidthSlider = (props: BoxProps) => { + const width = useAppSelector((state: RootState) => state.generation.width); + const shift = useAppSelector((state: RootState) => state.hotkeys.shift); + const activeTabName = useAppSelector(activeTabNameSelector); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + + return ( + + dispatch(setWidth(v))} + handleReset={() => dispatch(setWidth(512))} + withInput + withReset + withSliderMarks + sliderNumberInputProps={{ max: 15360 }} + /> + + ); +}; + +export default memo(WidthSlider); diff --git a/invokeai/frontend/web/src/features/parameters/components/ParametersAccordion.tsx b/invokeai/frontend/web/src/features/parameters/components/ParametersAccordion.tsx index a2d12f96ff..726e3fb1fc 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ParametersAccordion.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ParametersAccordion.tsx @@ -4,7 +4,10 @@ import { Feature } from 'app/features'; import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { systemSelector } from 'features/system/store/systemSelectors'; import { tabMap } from 'features/ui/store/tabMap'; -import { uiSelector } from 'features/ui/store/uiSelectors'; +import { + activeTabNameSelector, + uiSelector, +} from 'features/ui/store/uiSelectors'; import { openAccordionItemsChanged } from 'features/ui/store/uiSlice'; import { filter, map } from 'lodash'; import { ReactNode, useCallback } from 'react'; @@ -23,7 +26,7 @@ const parametersAccordionSelector = createSelector( let openAccordions: number[] = []; - if (tabMap[activeTab] === 'linear') { + if (tabMap[activeTab] === 'generate') { openAccordions = openLinearAccordionItems; } diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index d4293c0938..ab1953dcc6 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -11,7 +11,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import { FaPlay } from 'react-icons/fa'; -import { linearGraphBuilt, sessionCreated } from 'services/thunks/session'; +import { generateGraphBuilt, sessionCreated } from 'services/thunks/session'; interface InvokeButton extends Omit { @@ -26,7 +26,7 @@ export default function InvokeButton(props: InvokeButton) { const handleClickGenerate = () => { // dispatch(generateImage(activeTabName)); - dispatch(linearGraphBuilt()); + dispatch(generateGraphBuilt()); }; const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx index 31d228df35..20fe231fec 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx @@ -1,4 +1,4 @@ -import { Flex } from '@chakra-ui/react'; +import { Box, BoxProps, Flex } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { ChangeEvent } from 'react'; import { isEqual } from 'lodash'; @@ -30,7 +30,7 @@ const selector = createSelector( } ); -const ModelSelect = () => { +const ModelSelect = (props: BoxProps) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const { allModelNames, selectedModel } = useAppSelector(selector); @@ -39,12 +39,9 @@ const ModelSelect = () => { }; return ( - + { validValues={allModelNames} onChange={handleChangeModel} /> - + ); }; diff --git a/invokeai/frontend/web/src/features/system/components/SiteHeader.tsx b/invokeai/frontend/web/src/features/system/components/SiteHeader.tsx index f407206b67..af7a4ce33f 100644 --- a/invokeai/frontend/web/src/features/system/components/SiteHeader.tsx +++ b/invokeai/frontend/web/src/features/system/components/SiteHeader.tsx @@ -34,8 +34,6 @@ const SiteHeader = () => { > - - {resolution === 'desktop' ? ( ) : ( diff --git a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx index 06ac904bb1..77855cd05f 100644 --- a/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx +++ b/invokeai/frontend/web/src/features/ui/components/FloatingParametersPanelButtons.tsx @@ -39,7 +39,7 @@ export const floatingParametersPanelButtonSelector = createSelector( const shouldShowParametersPanelButton = !canvasBetaLayoutCheck && (!shouldPinParametersPanel || !shouldShowParametersPanel) && - ['linear', 'unifiedCanvas'].includes(activeTabName); + ['generate', 'unifiedCanvas'].includes(activeTabName); return { shouldPinParametersPanel, diff --git a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx index 11dddc48bf..96545c2b9b 100644 --- a/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx +++ b/invokeai/frontend/web/src/features/ui/components/InvokeTabs.tsx @@ -23,8 +23,10 @@ import { useTranslation } from 'react-i18next'; import { ResourceKey } from 'i18next'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import NodeEditor from 'features/nodes/components/NodeEditor'; -import LinearWorkspace from './tabs/Linear/LinearWorkspace'; +import GenerateWorkspace from './tabs/Generate/GenerateWorkspace'; import { FaImage } from 'react-icons/fa'; +import { createSelector } from '@reduxjs/toolkit'; +import { BsLightningChargeFill, BsLightningFill } from 'react-icons/bs'; export interface InvokeTabInfo { id: InvokeTabName; @@ -36,30 +38,36 @@ const tabIconStyles: ChakraProps['sx'] = { boxSize: 6, }; -const buildTabs = (disabledTabs: InvokeTabName[]): InvokeTabInfo[] => { - const tabs: InvokeTabInfo[] = [ - { - id: 'linear', - icon: , - workarea: , - }, - { - id: 'unifiedCanvas', - icon: , - workarea: , - }, - { - id: 'nodes', - icon: , - workarea: , - }, - ]; - return tabs.filter((tab) => !disabledTabs.includes(tab.id)); -}; +const tabs: InvokeTabInfo[] = [ + { + id: 'generate', + icon: , + workarea: , + }, + { + id: 'unifiedCanvas', + icon: , + workarea: , + }, + { + id: 'nodes', + icon: , + workarea: , + }, +]; + +const enabledTabsSelector = createSelector( + (state: RootState) => state.ui, + (ui) => { + const { disabledTabs } = ui; + + return tabs.filter((tab) => !disabledTabs.includes(tab.id)); + } +); export default function InvokeTabs() { const activeTab = useAppSelector(activeTabIndexSelector); - + const enabledTabs = useAppSelector(enabledTabsSelector); const isLightBoxOpen = useAppSelector( (state: RootState) => state.lightbox.isLightboxOpen ); @@ -72,22 +80,20 @@ export default function InvokeTabs() { (state: RootState) => state.system.disabledTabs ); - const activeTabs = buildTabs(disabledTabs); - const { t } = useTranslation(); const dispatch = useAppDispatch(); useHotkeys('1', () => { - dispatch(setActiveTab(0)); + dispatch(setActiveTab('generate')); }); useHotkeys('2', () => { - dispatch(setActiveTab(1)); + dispatch(setActiveTab('unifiedCanvas')); }); useHotkeys('3', () => { - dispatch(setActiveTab(2)); + dispatch(setActiveTab('nodes')); }); // Lightbox Hotkey @@ -111,7 +117,7 @@ export default function InvokeTabs() { const tabs = useMemo( () => - activeTabs.map((tab) => ( + enabledTabs.map((tab) => ( )), - [t, activeTabs] + [t, enabledTabs] ); const tabPanels = useMemo( () => - activeTabs.map((tab) => {tab.workarea}), - [activeTabs] + enabledTabs.map((tab) => ( + {tab.workarea} + )), + [enabledTabs] ); return ( diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearContent.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx similarity index 86% rename from invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearContent.tsx rename to invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx index 8860956aeb..53fdcb4a49 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearContent.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateContent.tsx @@ -1,7 +1,7 @@ import { Box, Flex } from '@chakra-ui/react'; import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay'; -const LinearContent = () => { +const GenerateContent = () => { return ( { ); }; -export default LinearContent; +export default GenerateContent; diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearParameters.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateParameters.tsx similarity index 83% rename from invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearParameters.tsx rename to invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateParameters.tsx index 5e6c9f82f3..46cf07d088 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearParameters.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateParameters.tsx @@ -1,5 +1,16 @@ -import { Flex } from '@chakra-ui/react'; +import { + AspectRatio, + Box, + Flex, + Select, + Slider, + SliderFilledTrack, + SliderThumb, + SliderTrack, + Text, +} from '@chakra-ui/react'; import { Feature } from 'app/features'; +import IAISlider from 'common/components/IAISlider'; import IAISwitch from 'common/components/IAISwitch'; import ImageToImageSettings from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageSettings'; import ImageToImageToggle from 'features/parameters/components/AdvancedParameters/ImageToImage/ImageToImageToggle'; @@ -10,6 +21,7 @@ import RandomizeSeed from 'features/parameters/components/AdvancedParameters/See import SeedSettings from 'features/parameters/components/AdvancedParameters/Seed/SeedSettings'; import GenerateVariationsToggle from 'features/parameters/components/AdvancedParameters/Variations/GenerateVariations'; import VariationsSettings from 'features/parameters/components/AdvancedParameters/Variations/VariationsSettings'; +import DimensionsSettings from 'features/parameters/components/ImageDimensions/DimensionsSettings'; import MainSettings from 'features/parameters/components/MainParameters/MainSettings'; import ParametersAccordion, { ParametersAccordionItems, @@ -17,14 +29,15 @@ import ParametersAccordion, { import ProcessButtons from 'features/parameters/components/ProcessButtons/ProcessButtons'; import NegativePromptInput from 'features/parameters/components/PromptInput/NegativePromptInput'; import PromptInput from 'features/parameters/components/PromptInput/PromptInput'; -import { memo, useMemo } from 'react'; +import { findIndex } from 'lodash'; +import { memo, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PARAMETERS_PANEL_WIDTH } from 'theme/util/constants'; -const LinearParameters = () => { +const GenerateParameters = () => { const { t } = useTranslation(); - const linearAccordions: ParametersAccordionItems = useMemo( + const generateAccordionItems: ParametersAccordionItems = useMemo( () => ({ // general: { // name: 'general', @@ -80,15 +93,16 @@ const LinearParameters = () => { gap: 2, bg: 'base.800', p: 4, + pb: 6, borderRadius: 'base', }} > - - + + ); }; -export default memo(LinearParameters); +export default memo(GenerateParameters); diff --git a/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkspace.tsx b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateWorkspace.tsx similarity index 82% rename from invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkspace.tsx rename to invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateWorkspace.tsx index 706cee8247..e6c0c71ae1 100644 --- a/invokeai/frontend/web/src/features/ui/components/tabs/Linear/LinearWorkspace.tsx +++ b/invokeai/frontend/web/src/features/ui/components/tabs/Generate/GenerateWorkspace.tsx @@ -1,15 +1,15 @@ import { Box, Flex } from '@chakra-ui/react'; import { useAppSelector } from 'app/storeHooks'; import { memo } from 'react'; -import LinearContent from './LinearContent'; -import LinearParameters from './LinearParameters'; +import GenerateContent from './GenerateContent'; +import GenerateParameters from './GenerateParameters'; import PinParametersPanelButton from '../../PinParametersPanelButton'; import { RootState } from 'app/store'; import Scrollable from '../../common/Scrollable'; import ParametersSlide from '../../common/ParametersSlide'; import AnimatedImageToImagePanel from 'features/parameters/components/AnimatedImageToImagePanel'; -const LinearWorkspace = () => { +const GenerateWorkspace = () => { const shouldPinParametersPanel = useAppSelector( (state: RootState) => state.ui.shouldPinParametersPanel ); @@ -33,7 +33,7 @@ const LinearWorkspace = () => { }} > - + { ) : ( - + )} - + ); }; -export default memo(LinearWorkspace); +export default memo(GenerateWorkspace); diff --git a/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts b/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts new file mode 100644 index 0000000000..65f2011841 --- /dev/null +++ b/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts @@ -0,0 +1,26 @@ +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; + +type HotkeysState = { + shift: boolean; +}; + +const initialHotkeysState: HotkeysState = { + shift: false, +}; + +const initialState: HotkeysState = initialHotkeysState; + +export const hotkeysSlice = createSlice({ + name: 'hotkeys', + initialState, + reducers: { + shiftKeyPressed: (state, action: PayloadAction) => { + state.shift = action.payload; + }, + }, +}); + +export const { shiftKeyPressed } = hotkeysSlice.actions; + +export default hotkeysSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/tabMap.ts b/invokeai/frontend/web/src/features/ui/store/tabMap.ts index 7584878e02..fe6e2d033a 100644 --- a/invokeai/frontend/web/src/features/ui/store/tabMap.ts +++ b/invokeai/frontend/web/src/features/ui/store/tabMap.ts @@ -1,7 +1,7 @@ export const tabMap = [ // 'txt2img', // 'img2img', - 'linear', + 'generate', 'unifiedCanvas', 'nodes', // 'postprocessing', diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index fd8ba78f01..c2795ed648 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -19,6 +19,9 @@ const initialUIState: UIState = { shouldShowGallery: true, shouldHidePreview: false, openLinearAccordionItems: [], + disabledParameterPanels: [], + disabledTabs: [], + openGenerateAccordionItems: [], openUnifiedCanvasAccordionItems: [], }; @@ -96,8 +99,8 @@ export const uiSlice = createSlice({ } }, openAccordionItemsChanged: (state, action: PayloadAction) => { - if (tabMap[state.activeTab] === 'linear') { - state.openLinearAccordionItems = action.payload; + if (tabMap[state.activeTab] === 'generate') { + state.openGenerateAccordionItems = action.payload; } if (tabMap[state.activeTab] === 'unifiedCanvas') { diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index a222aaee88..f519eaa6ec 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -15,5 +15,8 @@ export interface UIState { shouldPinGallery: boolean; shouldShowGallery: boolean; openLinearAccordionItems: number[]; + disabledParameterPanels: string[]; + disabledTabs: InvokeTabName[]; + openGenerateAccordionItems: number[]; openUnifiedCanvasAccordionItems: number[]; } diff --git a/invokeai/frontend/web/src/services/thunks/session.ts b/invokeai/frontend/web/src/services/thunks/session.ts index a1213ffcc2..6267f66ac2 100644 --- a/invokeai/frontend/web/src/services/thunks/session.ts +++ b/invokeai/frontend/web/src/services/thunks/session.ts @@ -1,13 +1,13 @@ import { createAppAsyncThunk } from 'app/storeUtils'; import { SessionsService } from 'services/api'; -import { buildLinearGraph } from 'features/nodes/util/linearGraphBuilder/buildLinearGraph'; +import { buildLinearGraph as buildGenerateGraph } from 'features/nodes/util/linearGraphBuilder/buildLinearGraph'; import { isAnyOf, isFulfilled } from '@reduxjs/toolkit'; import { buildNodesGraph } from 'features/nodes/util/nodesGraphBuilder/buildNodesGraph'; -export const linearGraphBuilt = createAppAsyncThunk( - 'api/linearGraphBuilt', +export const generateGraphBuilt = createAppAsyncThunk( + 'api/generateGraphBuilt', async (_, { dispatch, getState }) => { - const graph = buildLinearGraph(getState()); + const graph = buildGenerateGraph(getState()); dispatch(sessionCreated({ graph })); @@ -27,7 +27,7 @@ export const nodesGraphBuilt = createAppAsyncThunk( ); export const isFulfilledAnyGraphBuilt = isAnyOf( - linearGraphBuilt.fulfilled, + generateGraphBuilt.fulfilled, nodesGraphBuilt.fulfilled );