Reorganises internal state

`options` slice was huge and managed a mix of generation parameters and general app settings. It has been split up:

- Generation parameters are now in `generationSlice`.
- Postprocessing parameters are now in `postprocessingSlice`
- UI related things are now in `uiSlice`

There is probably more to be done, like `gallerySlice` perhaps should only manage internal gallery state, and not if the gallery is displayed.

Full-slice selectors have been made for each slice.

Other organisational tweaks.
This commit is contained in:
psychedelicious
2023-02-04 11:32:22 +11:00
committed by blessedcoolant
parent ffe0e81ec9
commit d74c4009cb
179 changed files with 7463 additions and 1165 deletions

View File

@@ -0,0 +1,54 @@
@use '../../../../styles/Mixins/' as *;
.advanced-parameters {
padding-top: 0.5rem;
display: grid;
row-gap: 0.5rem;
}
.advanced-parameters-item {
display: grid;
max-width: $options-bar-max-width;
border: none;
border-top: 0px;
border-radius: 0.4rem;
background-color: var(--tab-panel-bg);
&[aria-expanded='true'] {
background-color: var(--tab-hover-color);
border-radius: 0 0 0.4rem 0.4rem;
}
}
.advanced-parameters-panel {
background-color: var(--tab-panel-bg);
border-radius: 0 0 0.4rem 0.4rem;
padding: 1rem;
button {
background-color: var(--btn-base-color);
&:hover {
background-color: var(--btn-base-color-hover);
}
&:disabled {
&:hover {
background-color: var(--btn-base-color);
}
}
}
}
.advanced-parameters-header {
border-radius: 0.4rem;
font-weight: bold;
&[aria-expanded='true'] {
background-color: var(--tab-hover-color);
border-radius: 0.4rem 0.4rem 0 0;
}
&:hover {
background-color: var(--tab-hover-color);
}
}

View File

@@ -0,0 +1,40 @@
import {
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box,
Flex,
} from '@chakra-ui/react';
import { ReactNode } from 'react';
import { Feature } from 'app/features';
import GuideIcon from 'common/components/GuideIcon';
export interface InvokeAccordionItemProps {
header: string;
content: ReactNode;
feature?: Feature;
additionalHeaderComponents?: ReactNode;
}
export default function InvokeAccordionItem(props: InvokeAccordionItemProps) {
const { header, feature, content, additionalHeaderComponents } = props;
return (
<AccordionItem className="advanced-parameters-item">
<AccordionButton className="advanced-parameters-header">
<Flex width={'100%'} gap={'0.5rem'} align={'center'}>
<Box flexGrow={1} textAlign={'left'}>
{header}
</Box>
{additionalHeaderComponents}
{feature && <GuideIcon feature={feature} />}
<AccordionIcon />
</Flex>
</AccordionButton>
<AccordionPanel className="advanced-parameters-panel">
{content}
</AccordionPanel>
</AccordionItem>
);
}

View File

@@ -0,0 +1,53 @@
.inpainting-bounding-box-settings {
display: flex;
flex-direction: column;
border-radius: 0.4rem;
border: 2px solid var(--tab-color);
}
.inpainting-bounding-box-header {
background-color: var(--tab-color);
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0.5rem 1rem;
border-radius: 0.3rem 0.3rem 0 0;
align-items: center;
button {
width: 0.5rem;
height: 1.2rem;
background: none;
&:hover {
background: none;
}
}
p {
// font-weight: bold;
}
}
.inpainting-bounding-box-settings-items {
padding: 1rem;
display: flex;
flex-direction: column;
row-gap: 1rem;
.inpainting-bounding-box-reset-icon-btn {
background-color: var(--btn-base-color);
&:hover {
background-color: var(--btn-base-color-hover);
}
}
}
.inpainting-bounding-box-dimensions-slider-numberinput {
display: grid;
grid-template-columns: repeat(3, auto);
column-gap: 1rem;
}
.inpainting-bounding-box-darken {
width: max-content;
}

View File

@@ -0,0 +1,112 @@
import { Box, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
canvasSelector,
(canvas) => {
const { boundingBoxDimensions, boundingBoxScaleMethod: boundingBoxScale } =
canvas;
return {
boundingBoxDimensions,
boundingBoxScale,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
const BoundingBoxSettings = () => {
const dispatch = useAppDispatch();
const { boundingBoxDimensions } = useAppSelector(selector);
const { t } = useTranslation();
const handleChangeWidth = (v: number) => {
dispatch(
setBoundingBoxDimensions({
...boundingBoxDimensions,
width: Math.floor(v),
})
);
};
const handleChangeHeight = (v: number) => {
dispatch(
setBoundingBoxDimensions({
...boundingBoxDimensions,
height: Math.floor(v),
})
);
};
const handleResetWidth = () => {
dispatch(
setBoundingBoxDimensions({
...boundingBoxDimensions,
width: Math.floor(512),
})
);
};
const handleResetHeight = () => {
dispatch(
setBoundingBoxDimensions({
...boundingBoxDimensions,
height: Math.floor(512),
})
);
};
return (
<Flex direction="column" gap="1rem">
<IAISlider
label={t('parameters:width')}
min={64}
max={1024}
step={64}
value={boundingBoxDimensions.width}
onChange={handleChangeWidth}
sliderNumberInputProps={{ max: 4096 }}
withSliderMarks
withInput
inputReadOnly
withReset
handleReset={handleResetWidth}
/>
<IAISlider
label={t('parameters:height')}
min={64}
max={1024}
step={64}
value={boundingBoxDimensions.height}
onChange={handleChangeHeight}
sliderNumberInputProps={{ max: 4096 }}
withSliderMarks
withInput
inputReadOnly
withReset
handleReset={handleResetHeight}
/>
</Flex>
);
};
export default BoundingBoxSettings;
export const BoundingBoxSettingsHeader = () => {
const { t } = useTranslation();
return (
<Box flex="1" textAlign="left">
{t('parameters:boundingBoxHeader')}
</Box>
);
};

View File

@@ -0,0 +1,180 @@
import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect';
import IAISlider from 'common/components/IAISlider';
import { canvasSelector } from 'features/canvas/store/canvasSelectors';
import {
setBoundingBoxScaleMethod,
setScaledBoundingBoxDimensions,
} from 'features/canvas/store/canvasSlice';
import {
BoundingBoxScale,
BOUNDING_BOX_SCALES_DICT,
} from 'features/canvas/store/canvasTypes';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import {
setInfillMethod,
setTileSize,
} from 'features/parameters/store/generationSlice';
import { systemSelector } from 'features/system/store/systemSelectors';
import _ from 'lodash';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[generationSelector, systemSelector, canvasSelector],
(parameters, system, canvas) => {
const { tileSize, infillMethod } = parameters;
const { infill_methods: availableInfillMethods } = system;
const {
boundingBoxScaleMethod: boundingBoxScale,
scaledBoundingBoxDimensions,
} = canvas;
return {
boundingBoxScale,
scaledBoundingBoxDimensions,
tileSize,
infillMethod,
availableInfillMethods,
isManual: boundingBoxScale === 'manual',
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
const InfillAndScalingSettings = () => {
const dispatch = useAppDispatch();
const {
tileSize,
infillMethod,
availableInfillMethods,
boundingBoxScale,
isManual,
scaledBoundingBoxDimensions,
} = useAppSelector(selector);
const { t } = useTranslation();
const handleChangeScaledWidth = (v: number) => {
dispatch(
setScaledBoundingBoxDimensions({
...scaledBoundingBoxDimensions,
width: Math.floor(v),
})
);
};
const handleChangeScaledHeight = (v: number) => {
dispatch(
setScaledBoundingBoxDimensions({
...scaledBoundingBoxDimensions,
height: Math.floor(v),
})
);
};
const handleResetScaledWidth = () => {
dispatch(
setScaledBoundingBoxDimensions({
...scaledBoundingBoxDimensions,
width: Math.floor(512),
})
);
};
const handleResetScaledHeight = () => {
dispatch(
setScaledBoundingBoxDimensions({
...scaledBoundingBoxDimensions,
height: Math.floor(512),
})
);
};
const handleChangeBoundingBoxScaleMethod = (
e: ChangeEvent<HTMLSelectElement>
) => {
dispatch(setBoundingBoxScaleMethod(e.target.value as BoundingBoxScale));
};
return (
<Flex direction="column" gap="1rem">
<IAISelect
label={t('parameters:scaleBeforeProcessing')}
validValues={BOUNDING_BOX_SCALES_DICT}
value={boundingBoxScale}
onChange={handleChangeBoundingBoxScaleMethod}
/>
<IAISlider
isInputDisabled={!isManual}
isResetDisabled={!isManual}
isSliderDisabled={!isManual}
label={t('parameters:scaledWidth')}
min={64}
max={1024}
step={64}
value={scaledBoundingBoxDimensions.width}
onChange={handleChangeScaledWidth}
sliderNumberInputProps={{ max: 4096 }}
withSliderMarks
withInput
inputReadOnly
withReset
handleReset={handleResetScaledWidth}
/>
<IAISlider
isInputDisabled={!isManual}
isResetDisabled={!isManual}
isSliderDisabled={!isManual}
label={t('parameters:scaledHeight')}
min={64}
max={1024}
step={64}
value={scaledBoundingBoxDimensions.height}
onChange={handleChangeScaledHeight}
sliderNumberInputProps={{ max: 4096 }}
withSliderMarks
withInput
inputReadOnly
withReset
handleReset={handleResetScaledHeight}
/>
<IAISelect
label={t('parameters:infillMethod')}
value={infillMethod}
validValues={availableInfillMethods}
onChange={(e) => dispatch(setInfillMethod(e.target.value))}
/>
<IAISlider
isInputDisabled={infillMethod !== 'tile'}
isResetDisabled={infillMethod !== 'tile'}
isSliderDisabled={infillMethod !== 'tile'}
sliderMarkRightOffset={-4}
label={t('parameters:tileSize')}
min={16}
max={64}
sliderNumberInputProps={{ max: 256 }}
value={tileSize}
onChange={(v) => {
dispatch(setTileSize(v));
}}
withInput
withSliderMarks
withReset
handleReset={() => {
dispatch(setTileSize(32));
}}
/>
</Flex>
);
};
export default InfillAndScalingSettings;

View File

@@ -0,0 +1,33 @@
import type { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setSeamBlur } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function SeamBlur() {
const dispatch = useAppDispatch();
const seamBlur = useAppSelector(
(state: RootState) => state.generation.seamBlur
);
const { t } = useTranslation();
return (
<IAISlider
sliderMarkRightOffset={-4}
label={t('parameters:seamBlur')}
min={0}
max={64}
sliderNumberInputProps={{ max: 512 }}
value={seamBlur}
onChange={(v) => {
dispatch(setSeamBlur(v));
}}
withInput
withSliderMarks
withReset
handleReset={() => {
dispatch(setSeamBlur(16));
}}
/>
);
}

View File

@@ -0,0 +1,18 @@
import { Flex } from '@chakra-ui/react';
import SeamBlur from './SeamBlur';
import SeamSize from './SeamSize';
import SeamSteps from './SeamSteps';
import SeamStrength from './SeamStrength';
const SeamCorrectionSettings = () => {
return (
<Flex direction="column" gap="1rem">
<SeamSize />
<SeamBlur />
<SeamStrength />
<SeamSteps />
</Flex>
);
};
export default SeamCorrectionSettings;

View File

@@ -0,0 +1,31 @@
import type { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setSeamSize } from 'features/parameters/store/generationSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function SeamSize() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const seamSize = useAppSelector((state: RootState) => state.generation.seamSize);
return (
<IAISlider
sliderMarkRightOffset={-6}
label={t('parameters:seamSize')}
min={1}
max={256}
sliderNumberInputProps={{ max: 512 }}
value={seamSize}
onChange={(v) => {
dispatch(setSeamSize(v));
}}
withInput
withSliderMarks
withReset
handleReset={() => dispatch(setSeamSize(96))}
/>
);
}

View File

@@ -0,0 +1,34 @@
import type { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setSeamSteps } from 'features/parameters/store/generationSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function SeamSteps() {
const { t } = useTranslation();
const seamSteps = useAppSelector(
(state: RootState) => state.generation.seamSteps
);
const dispatch = useAppDispatch();
return (
<IAISlider
sliderMarkRightOffset={-4}
label={t('parameters:seamSteps')}
min={1}
max={100}
sliderNumberInputProps={{ max: 999 }}
value={seamSteps}
onChange={(v) => {
dispatch(setSeamSteps(v));
}}
withInput
withSliderMarks
withReset
handleReset={() => {
dispatch(setSeamSteps(30));
}}
/>
);
}

View File

@@ -0,0 +1,34 @@
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setSeamStrength } from 'features/parameters/store/generationSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function SeamStrength() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const seamStrength = useAppSelector(
(state: RootState) => state.generation.seamStrength
);
return (
<IAISlider
sliderMarkRightOffset={-7}
label={t('parameters:seamStrength')}
min={0.01}
max={0.99}
step={0.01}
value={seamStrength}
onChange={(v) => {
dispatch(setSeamStrength(v));
}}
withInput
withSliderMarks
withReset
handleReset={() => {
dispatch(setSeamStrength(0.7));
}}
/>
);
}

View File

@@ -0,0 +1,101 @@
import { Flex } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { FacetoolType } from 'features/parameters/store/postprocessingSlice';
import {
setCodeformerFidelity,
setFacetoolStrength,
setFacetoolType,
} from 'features/parameters/store/postprocessingSlice';
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import IAINumberInput from 'common/components/IAINumberInput';
import IAISelect from 'common/components/IAISelect';
import { FACETOOL_TYPES } from 'app/constants';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
import { systemSelector } from 'features/system/store/systemSelectors';
const optionsSelector = createSelector(
[postprocessingSelector, systemSelector],
(
{ facetoolStrength, facetoolType, codeformerFidelity },
{ isGFPGANAvailable }
) => {
return {
facetoolStrength,
facetoolType,
codeformerFidelity,
isGFPGANAvailable,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
/**
* Displays face-fixing/GFPGAN options (strength).
*/
const FaceRestoreSettings = () => {
const dispatch = useAppDispatch();
const {
facetoolStrength,
facetoolType,
codeformerFidelity,
isGFPGANAvailable,
} = useAppSelector(optionsSelector);
const handleChangeStrength = (v: number) => dispatch(setFacetoolStrength(v));
const handleChangeCodeformerFidelity = (v: number) =>
dispatch(setCodeformerFidelity(v));
const handleChangeFacetoolType = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setFacetoolType(e.target.value as FacetoolType));
const { t } = useTranslation();
return (
<Flex direction={'column'} gap={2}>
<IAISelect
label={t('parameters:type')}
validValues={FACETOOL_TYPES.concat()}
value={facetoolType}
onChange={handleChangeFacetoolType}
/>
<IAINumberInput
isDisabled={!isGFPGANAvailable}
label={t('parameters:strength')}
step={0.05}
min={0}
max={1}
onChange={handleChangeStrength}
value={facetoolStrength}
width="90px"
isInteger={false}
/>
{facetoolType === 'codeformer' && (
<IAINumberInput
isDisabled={!isGFPGANAvailable}
label={t('parameters:codeformerFidelity')}
step={0.05}
min={0}
max={1}
onChange={handleChangeCodeformerFidelity}
value={codeformerFidelity}
width="90px"
isInteger={false}
/>
)}
</Flex>
);
};
export default FaceRestoreSettings;

View File

@@ -0,0 +1,28 @@
import { ChangeEvent } from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { setShouldRunFacetool } from 'features/parameters/store/postprocessingSlice';
export default function FaceRestoreToggle() {
const isGFPGANAvailable = useAppSelector(
(state: RootState) => state.system.isGFPGANAvailable
);
const shouldRunFacetool = useAppSelector(
(state: RootState) => state.postprocessing.shouldRunFacetool
);
const dispatch = useAppDispatch();
const handleChangeShouldRunFacetool = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldRunFacetool(e.target.checked));
return (
<IAISwitch
isDisabled={!isGFPGANAvailable}
isChecked={shouldRunFacetool}
onChange={handleChangeShouldRunFacetool}
/>
);
}

View File

@@ -0,0 +1,27 @@
import React, { ChangeEvent } from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { setShouldFitToWidthHeight } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function ImageFit() {
const dispatch = useAppDispatch();
const shouldFitToWidthHeight = useAppSelector(
(state: RootState) => state.generation.shouldFitToWidthHeight
);
const handleChangeFit = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldFitToWidthHeight(e.target.checked));
const { t } = useTranslation();
return (
<IAISwitch
label={t('parameters:imageFit')}
isChecked={shouldFitToWidthHeight}
onChange={handleChangeFit}
/>
);
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setImg2imgStrength } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
interface ImageToImageStrengthProps {
label?: string;
styleClass?: string;
}
export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
const { t } = useTranslation();
const { label = `${t('parameters:strength')}`, styleClass } = props;
const img2imgStrength = useAppSelector(
(state: RootState) => state.generation.img2imgStrength
);
const dispatch = useAppDispatch();
const handleChangeStrength = (v: number) => dispatch(setImg2imgStrength(v));
const handleImg2ImgStrengthReset = () => {
dispatch(setImg2imgStrength(0.75));
};
return (
<IAISlider
label={label}
step={0.01}
min={0.01}
max={0.99}
onChange={handleChangeStrength}
value={img2imgStrength}
isInteger={false}
styleClass={styleClass}
withInput
withSliderMarks
inputWidth={'5.5rem'}
withReset
handleReset={handleImg2ImgStrengthReset}
/>
);
}

View File

@@ -0,0 +1,90 @@
import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import IAISwitch from 'common/components/IAISwitch';
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
import {
setHiresFix,
setHiresStrength,
} from 'features/parameters/store/postprocessingSlice';
import { isEqual } from 'lodash';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
const hiresStrengthSelector = createSelector(
[postprocessingSelector],
({ hiresFix, hiresStrength }) => ({ hiresFix, hiresStrength }),
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
const HiresStrength = () => {
const { hiresFix, hiresStrength } = useAppSelector(hiresStrengthSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleHiresStrength = (v: number) => {
dispatch(setHiresStrength(v));
};
const handleHiResStrengthReset = () => {
dispatch(setHiresStrength(0.75));
};
return (
<IAISlider
label={t('parameters:hiresStrength')}
step={0.01}
min={0.01}
max={0.99}
onChange={handleHiresStrength}
value={hiresStrength}
isInteger={false}
withInput
withSliderMarks
inputWidth={'5.5rem'}
withReset
handleReset={handleHiResStrengthReset}
isSliderDisabled={!hiresFix}
isInputDisabled={!hiresFix}
isResetDisabled={!hiresFix}
/>
);
};
/**
* Hires Fix Toggle
*/
const HiresSettings = () => {
const dispatch = useAppDispatch();
const hiresFix = useAppSelector(
(state: RootState) => state.postprocessing.hiresFix
);
const { t } = useTranslation();
const handleChangeHiresFix = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setHiresFix(e.target.checked));
return (
<Flex gap={2} direction={'column'}>
<IAISwitch
label={t('parameters:hiresOptim')}
fontSize={'md'}
isChecked={hiresFix}
onChange={handleChangeHiresFix}
/>
<HiresStrength />
</Flex>
);
};
export default HiresSettings;

View File

@@ -0,0 +1,12 @@
import { Flex } from '@chakra-ui/react';
import SeamlessSettings from './SeamlessSettings';
const ImageToImageOutputSettings = () => {
return (
<Flex gap={2} direction={'column'}>
<SeamlessSettings />
</Flex>
);
};
export default ImageToImageOutputSettings;

View File

@@ -0,0 +1,14 @@
import { Flex } from '@chakra-ui/react';
import HiresSettings from './HiresSettings';
import SeamlessSettings from './SeamlessSettings';
const OutputSettings = () => {
return (
<Flex gap={2} direction={'column'}>
<SeamlessSettings />
<HiresSettings />
</Flex>
);
};
export default OutputSettings;

View File

@@ -0,0 +1,34 @@
import { Flex } from '@chakra-ui/react';
import { ChangeEvent } from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { setSeamless } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
/**
* Seamless tiling toggle
*/
const SeamlessSettings = () => {
const dispatch = useAppDispatch();
const seamless = useAppSelector((state: RootState) => state.generation.seamless);
const handleChangeSeamless = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setSeamless(e.target.checked));
const { t } = useTranslation();
return (
<Flex gap={2} direction={'column'}>
<IAISwitch
label={t('parameters:seamlessTiling')}
fontSize={'md'}
isChecked={seamless}
onChange={handleChangeSeamless}
/>
</Flex>
);
};
export default SeamlessSettings;

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setPerlin } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function Perlin() {
const dispatch = useAppDispatch();
const perlin = useAppSelector((state: RootState) => state.generation.perlin);
const { t } = useTranslation();
const handleChangePerlin = (v: number) => dispatch(setPerlin(v));
return (
<IAINumberInput
label={t('parameters:perlinNoise')}
min={0}
max={1}
step={0.05}
onChange={handleChangePerlin}
value={perlin}
isInteger={false}
/>
);
}

View File

@@ -0,0 +1,28 @@
import { ChangeEvent } from 'react';
import React from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { setShouldRandomizeSeed } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function RandomizeSeed() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const shouldRandomizeSeed = useAppSelector(
(state: RootState) => state.generation.shouldRandomizeSeed
);
const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldRandomizeSeed(e.target.checked));
return (
<IAISwitch
label={t('parameters:randomizeSeed')}
isChecked={shouldRandomizeSeed}
onChange={handleChangeShouldRandomizeSeed}
/>
);
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setSeed } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function Seed() {
const seed = useAppSelector((state: RootState) => state.generation.seed);
const shouldRandomizeSeed = useAppSelector(
(state: RootState) => state.generation.shouldRandomizeSeed
);
const shouldGenerateVariations = useAppSelector(
(state: RootState) => state.generation.shouldGenerateVariations
);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const handleChangeSeed = (v: number) => dispatch(setSeed(v));
return (
<IAINumberInput
label={t('parameters:seed')}
step={1}
precision={0}
flexGrow={1}
min={NUMPY_RAND_MIN}
max={NUMPY_RAND_MAX}
isDisabled={shouldRandomizeSeed}
isInvalid={seed < 0 && shouldGenerateVariations}
onChange={handleChangeSeed}
value={seed}
width="auto"
/>
);
}

View File

@@ -0,0 +1,29 @@
import { Flex } from '@chakra-ui/react';
import RandomizeSeed from './RandomizeSeed';
import Seed from './Seed';
import ShuffleSeed from './ShuffleSeed';
import Threshold from './Threshold';
import Perlin from './Perlin';
/**
* Seed & variation options. Includes iteration, seed, seed randomization, variation options.
*/
const SeedSettings = () => {
return (
<Flex gap={2} direction={'column'}>
<RandomizeSeed />
<Flex gap={2}>
<Seed />
<ShuffleSeed />
</Flex>
<Flex gap={2}>
<Threshold />
</Flex>
<Flex gap={2}>
<Perlin />
</Flex>
</Flex>
);
};
export default SeedSettings;

View File

@@ -0,0 +1,30 @@
import { Button } from '@chakra-ui/react';
import React from 'react';
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import randomInt from 'common/util/randomInt';
import { setSeed } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function ShuffleSeed() {
const dispatch = useAppDispatch();
const shouldRandomizeSeed = useAppSelector(
(state: RootState) => state.generation.shouldRandomizeSeed
);
const { t } = useTranslation();
const handleClickRandomizeSeed = () =>
dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)));
return (
<Button
size={'sm'}
isDisabled={shouldRandomizeSeed}
onClick={handleClickRandomizeSeed}
padding="0 1.5rem"
>
<p>{t('parameters:shuffle')}</p>
</Button>
);
}

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setThreshold } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function Threshold() {
const dispatch = useAppDispatch();
const threshold = useAppSelector(
(state: RootState) => state.generation.threshold
);
const { t } = useTranslation();
const handleChangeThreshold = (v: number) => dispatch(setThreshold(v));
return (
<IAINumberInput
label={t('parameters:noiseThreshold')}
min={0}
max={1000}
step={0.1}
onChange={handleChangeThreshold}
value={threshold}
isInteger={false}
/>
);
}

View File

@@ -0,0 +1,5 @@
.upscale-settings {
display: grid;
grid-template-columns: auto 1fr;
column-gap: 1rem;
}

View File

@@ -0,0 +1,74 @@
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { UpscalingLevel } from 'features/parameters/store/postprocessingSlice';
import {
setUpscalingLevel,
setUpscalingStrength,
} from 'features/parameters/store/postprocessingSlice';
import { UPSCALING_LEVELS } from 'app/constants';
import { createSelector } from '@reduxjs/toolkit';
import { isEqual } from 'lodash';
import { ChangeEvent } from 'react';
import IAINumberInput from 'common/components/IAINumberInput';
import IAISelect from 'common/components/IAISelect';
import { useTranslation } from 'react-i18next';
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
import { systemSelector } from 'features/system/store/systemSelectors';
const parametersSelector = createSelector(
[postprocessingSelector, systemSelector],
({ upscalingLevel, upscalingStrength }, { isESRGANAvailable }) => {
return {
upscalingLevel,
upscalingStrength,
isESRGANAvailable,
};
},
{
memoizeOptions: {
resultEqualityCheck: isEqual,
},
}
);
/**
* Displays upscaling/ESRGAN options (level and strength).
*/
const UpscaleSettings = () => {
const dispatch = useAppDispatch();
const { upscalingLevel, upscalingStrength, isESRGANAvailable } =
useAppSelector(parametersSelector);
const { t } = useTranslation();
const handleChangeLevel = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setUpscalingLevel(Number(e.target.value) as UpscalingLevel));
const handleChangeStrength = (v: number) => dispatch(setUpscalingStrength(v));
return (
<div className="upscale-settings">
<IAISelect
isDisabled={!isESRGANAvailable}
label={t('parameters:scale')}
value={upscalingLevel}
onChange={handleChangeLevel}
validValues={UPSCALING_LEVELS}
/>
<IAINumberInput
isDisabled={!isESRGANAvailable}
label={t('parameters:strength')}
step={0.05}
min={0}
max={1}
onChange={handleChangeStrength}
value={upscalingStrength}
isInteger={false}
/>
</div>
);
};
export default UpscaleSettings;

View File

@@ -0,0 +1,26 @@
import { ChangeEvent } from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { setShouldRunESRGAN } from 'features/parameters/store/postprocessingSlice';
export default function UpscaleToggle() {
const isESRGANAvailable = useAppSelector(
(state: RootState) => state.system.isESRGANAvailable
);
const shouldRunESRGAN = useAppSelector(
(state: RootState) => state.postprocessing.shouldRunESRGAN
);
const dispatch = useAppDispatch();
const handleChangeShouldRunESRGAN = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setShouldRunESRGAN(e.target.checked));
return (
<IAISwitch
isDisabled={!isESRGANAvailable}
isChecked={shouldRunESRGAN}
onChange={handleChangeShouldRunESRGAN}
/>
);
}

View File

@@ -0,0 +1,25 @@
import React, { ChangeEvent } from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISwitch from 'common/components/IAISwitch';
import { setShouldGenerateVariations } from 'features/parameters/store/generationSlice';
export default function GenerateVariationsToggle() {
const shouldGenerateVariations = useAppSelector(
(state: RootState) => state.generation.shouldGenerateVariations
);
const dispatch = useAppDispatch();
const handleChangeShouldGenerateVariations = (
e: ChangeEvent<HTMLInputElement>
) => dispatch(setShouldGenerateVariations(e.target.checked));
return (
<IAISwitch
isChecked={shouldGenerateVariations}
width={'auto'}
onChange={handleChangeShouldGenerateVariations}
/>
);
}

View File

@@ -0,0 +1,37 @@
import React, { ChangeEvent } from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIInput from 'common/components/IAIInput';
import { validateSeedWeights } from 'common/util/seedWeightPairs';
import { setSeedWeights } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function SeedWeights() {
const seedWeights = useAppSelector(
(state: RootState) => state.generation.seedWeights
);
const shouldGenerateVariations = useAppSelector(
(state: RootState) => state.generation.shouldGenerateVariations
);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const handleChangeSeedWeights = (e: ChangeEvent<HTMLInputElement>) =>
dispatch(setSeedWeights(e.target.value));
return (
<IAIInput
label={t('parameters:seedWeights')}
value={seedWeights}
isInvalid={
shouldGenerateVariations &&
!(validateSeedWeights(seedWeights) || seedWeights === '')
}
isDisabled={!shouldGenerateVariations}
onChange={handleChangeSeedWeights}
/>
);
}

View File

@@ -0,0 +1,35 @@
import React from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setVariationAmount } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function VariationAmount() {
const variationAmount = useAppSelector(
(state: RootState) => state.generation.variationAmount
);
const shouldGenerateVariations = useAppSelector(
(state: RootState) => state.generation.shouldGenerateVariations
);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const handleChangevariationAmount = (v: number) =>
dispatch(setVariationAmount(v));
return (
<IAINumberInput
label={t('parameters:variationAmount')}
value={variationAmount}
step={0.01}
min={0}
max={1}
isDisabled={!shouldGenerateVariations}
onChange={handleChangevariationAmount}
isInteger={false}
/>
);
}

View File

@@ -0,0 +1,17 @@
import { Flex } from '@chakra-ui/react';
import SeedWeights from './SeedWeights';
import VariationAmount from './VariationAmount';
/**
* Seed & variation options. Includes iteration, seed, seed randomization, variation options.
*/
const VariationsSettings = () => {
return (
<Flex gap={2} direction={'column'}>
<VariationAmount />
<SeedWeights />
</Flex>
);
};
export default VariationsSettings;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setCfgScale } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function MainCFGScale() {
const dispatch = useAppDispatch();
const cfgScale = useAppSelector((state: RootState) => state.generation.cfgScale);
const { t } = useTranslation();
const handleChangeCfgScale = (v: number) => dispatch(setCfgScale(v));
return (
<IAINumberInput
label={t('parameters:cfgScale')}
step={0.5}
min={1.01}
max={200}
onChange={handleChangeCfgScale}
value={cfgScale}
width="auto"
styleClass="main-settings-block"
textAlign="center"
isInteger={false}
/>
);
}

View File

@@ -0,0 +1,31 @@
import { ChangeEvent } from 'react';
import React from 'react';
import { HEIGHTS } from 'app/constants';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { setHeight } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function MainHeight() {
const height = useAppSelector((state: RootState) => state.generation.height);
const activeTabName = useAppSelector(activeTabNameSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setHeight(Number(e.target.value)));
return (
<IAISelect
isDisabled={activeTabName === 'unifiedCanvas'}
label={t('parameters:height')}
value={height}
flexGrow={1}
onChange={handleChangeHeight}
validValues={HEIGHTS}
styleClass="main-settings-block"
/>
);
}

View File

@@ -0,0 +1,50 @@
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import React from 'react';
import type { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import {
GenerationState,
setIterations,
} from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
const mainIterationsSelector = createSelector(
[(state: RootState) => state.generation],
(parameters: GenerationState) => {
const { iterations } = parameters;
return {
iterations,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
export default function MainIterations() {
const dispatch = useAppDispatch();
const { iterations } = useAppSelector(mainIterationsSelector);
const { t } = useTranslation();
const handleChangeIterations = (v: number) => dispatch(setIterations(v));
return (
<IAINumberInput
label={t('parameters:images')}
step={1}
min={1}
max={9999}
onChange={handleChangeIterations}
value={iterations}
width="auto"
labelFontSize={0.5}
styleClass="main-settings-block"
textAlign="center"
/>
);
}

View File

@@ -0,0 +1,35 @@
@use '../../../../styles/Mixins/' as *;
.main-settings {
display: grid;
row-gap: 1rem;
}
.main-settings-list {
display: grid;
row-gap: 1rem;
}
.main-settings-row {
display: grid;
grid-template-columns: repeat(3, auto);
column-gap: 0.5rem;
max-width: $options-bar-max-width;
}
.main-settings-block {
border-radius: 0.5rem;
display: grid !important;
grid-template-columns: auto !important;
row-gap: 0.5rem;
.invokeai__number-input-form-label,
.invokeai__select-label {
font-weight: bold;
font-size: 0.9rem !important;
}
.invokeai__select-label {
margin: 0;
}
}

View File

@@ -0,0 +1,27 @@
import MainCFGScale from './MainCFGScale';
import MainHeight from './MainHeight';
import MainIterations from './MainIterations';
import MainSampler from './MainSampler';
import MainSteps from './MainSteps';
import MainWidth from './MainWidth';
export const inputWidth = 'auto';
export default function MainSettings() {
return (
<div className="main-settings">
<div className="main-settings-list">
<div className="main-settings-row">
<MainIterations />
<MainSteps />
<MainCFGScale />
</div>
<div className="main-settings-row">
<MainWidth />
<MainHeight />
<MainSampler />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import { DIFFUSERS_SAMPLERS, SAMPLERS } from 'app/constants';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect';
import { setSampler } from 'features/parameters/store/generationSlice';
import { activeModelSelector } from 'features/system/store/systemSelectors';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next';
export default function MainSampler() {
const sampler = useAppSelector(
(state: RootState) => state.generation.sampler
);
const activeModel = useAppSelector(activeModelSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setSampler(e.target.value));
return (
<IAISelect
label={t('parameters:sampler')}
value={sampler}
onChange={handleChangeSampler}
validValues={
activeModel.format === 'diffusers' ? DIFFUSERS_SAMPLERS : SAMPLERS
}
styleClass="main-option-block"
/>
);
}

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import { setSteps } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function MainSteps() {
const dispatch = useAppDispatch();
const steps = useAppSelector((state: RootState) => state.generation.steps);
const { t } = useTranslation();
const handleChangeSteps = (v: number) => dispatch(setSteps(v));
return (
<IAINumberInput
label={t('parameters:steps')}
min={1}
max={9999}
step={1}
onChange={handleChangeSteps}
value={steps}
width="auto"
styleClass="main-settings-block"
textAlign="center"
/>
);
}

View File

@@ -0,0 +1,31 @@
import React, { ChangeEvent } from 'react';
import { WIDTHS } from 'app/constants';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { setWidth } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
export default function MainWidth() {
const width = useAppSelector((state: RootState) => state.generation.width);
const activeTabName = useAppSelector(activeTabNameSelector);
const { t } = useTranslation();
const dispatch = useAppDispatch();
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setWidth(Number(e.target.value)));
return (
<IAISelect
isDisabled={activeTabName === 'unifiedCanvas'}
label={t('parameters:width')}
value={width}
flexGrow={1}
onChange={handleChangeWidth}
validValues={WIDTHS}
styleClass="main-settings-block"
/>
);
}

View File

@@ -0,0 +1,69 @@
import { Accordion, ExpandedIndex } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { setOpenAccordions } from 'features/system/store/systemSlice';
import InvokeAccordionItem, {
InvokeAccordionItemProps,
} from './AccordionItems/InvokeAccordionItem';
import { ReactElement } from 'react';
type ParametersAccordionType = {
[parametersAccordionKey: string]: InvokeAccordionItemProps;
};
type ParametersAccordionsType = {
accordionInfo: ParametersAccordionType;
};
/**
* Main container for generation and processing parameters.
*/
const ParametersAccordion = (props: ParametersAccordionsType) => {
const { accordionInfo } = props;
const openAccordions = useAppSelector(
(state: RootState) => state.system.openAccordions
);
const dispatch = useAppDispatch();
/**
* Stores accordion state in redux so preferred UI setup is retained.
*/
const handleChangeAccordionState = (openAccordions: ExpandedIndex) =>
dispatch(setOpenAccordions(openAccordions));
const renderAccordions = () => {
const accordionsToRender: ReactElement[] = [];
if (accordionInfo) {
Object.keys(accordionInfo).forEach((key) => {
const { header, feature, content, additionalHeaderComponents } =
accordionInfo[key];
accordionsToRender.push(
<InvokeAccordionItem
key={key}
header={header}
feature={feature}
content={content}
additionalHeaderComponents={additionalHeaderComponents}
/>
);
});
}
return accordionsToRender;
};
return (
<Accordion
defaultIndex={openAccordions}
allowMultiple
reduceMotion
onChange={handleChangeAccordionState}
className="advanced-parameters"
>
{renderAccordions()}
</Accordion>
);
};
export default ParametersAccordion;

View File

@@ -0,0 +1,62 @@
import { MdCancel } from 'react-icons/md';
import { cancelProcessing } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIIconButton, {
IAIIconButtonProps,
} from 'common/components/IAIIconButton';
import { useHotkeys } from 'react-hotkeys-hook';
import { createSelector } from '@reduxjs/toolkit';
import { SystemState } from 'features/system/store/systemSlice';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { systemSelector } from 'features/system/store/systemSelectors';
const cancelButtonSelector = createSelector(
systemSelector,
(system: SystemState) => {
return {
isProcessing: system.isProcessing,
isConnected: system.isConnected,
isCancelable: system.isCancelable,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
export default function CancelButton(
props: Omit<IAIIconButtonProps, 'aria-label'>
) {
const { ...rest } = props;
const dispatch = useAppDispatch();
const { isProcessing, isConnected, isCancelable } =
useAppSelector(cancelButtonSelector);
const handleClickCancel = () => dispatch(cancelProcessing());
const { t } = useTranslation();
useHotkeys(
'shift+x',
() => {
if ((isConnected || isProcessing) && isCancelable) {
handleClickCancel();
}
},
[isConnected, isProcessing, isCancelable]
);
return (
<IAIIconButton
icon={<MdCancel />}
tooltip={t('parameters:cancel')}
aria-label={t('parameters:cancel')}
isDisabled={!isConnected || !isProcessing || !isCancelable}
onClick={handleClickCancel}
styleClass="cancel-btn"
{...rest}
/>
);
}

View File

@@ -0,0 +1,71 @@
import { useHotkeys } from 'react-hotkeys-hook';
import { FaPlay } from 'react-icons/fa';
import { readinessSelector } from 'app/selectors/readinessSelector';
import { generateImage } from 'app/socketio/actions';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIButton, { IAIButtonProps } from 'common/components/IAIButton';
import IAIIconButton, {
IAIIconButtonProps,
} from 'common/components/IAIIconButton';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useTranslation } from 'react-i18next';
interface InvokeButton
extends Omit<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
iconButton?: boolean;
}
export default function InvokeButton(props: InvokeButton) {
const { iconButton = false, ...rest } = props;
const dispatch = useAppDispatch();
const { isReady } = useAppSelector(readinessSelector);
const activeTabName = useAppSelector(activeTabNameSelector);
const handleClickGenerate = () => {
dispatch(generateImage(activeTabName));
};
const { t } = useTranslation();
useHotkeys(
['ctrl+enter', 'meta+enter'],
() => {
dispatch(generateImage(activeTabName));
},
{
enabled: () => isReady,
preventDefault: true,
enableOnFormTags: ['input', 'textarea', 'select'],
},
[isReady, activeTabName]
);
return (
<div style={{ flexGrow: 4 }}>
{iconButton ? (
<IAIIconButton
aria-label={t('parameters:invoke')}
type="submit"
icon={<FaPlay />}
isDisabled={!isReady}
onClick={handleClickGenerate}
className="invoke-btn"
tooltip={t('parameters:invoke')}
tooltipProps={{ placement: 'bottom' }}
{...rest}
/>
) : (
<IAIButton
aria-label={t('parameters:invoke')}
type="submit"
isDisabled={!isReady}
onClick={handleClickGenerate}
className="invoke-btn"
{...rest}
>
Invoke
</IAIButton>
)}
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { createSelector } from '@reduxjs/toolkit';
import { FaRecycle } from 'react-icons/fa';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAIIconButton from 'common/components/IAIIconButton';
import { setShouldLoopback } from 'features/parameters/store/postprocessingSlice';
import { useTranslation } from 'react-i18next';
import { postprocessingSelector } from 'features/parameters/store/postprocessingSelectors';
const loopbackSelector = createSelector(
postprocessingSelector,
({ shouldLoopback }) => shouldLoopback
);
const LoopbackButton = () => {
const dispatch = useAppDispatch();
const shouldLoopback = useAppSelector(loopbackSelector);
const { t } = useTranslation();
return (
<IAIIconButton
aria-label={t('parameters:toggleLoopback')}
tooltip={t('parameters:toggleLoopback')}
styleClass="loopback-btn"
asCheckbox={true}
isChecked={shouldLoopback}
icon={<FaRecycle />}
onClick={() => {
dispatch(setShouldLoopback(!shouldLoopback));
}}
/>
);
};
export default LoopbackButton;

View File

@@ -0,0 +1,56 @@
@use '../../../../styles/Mixins/' as *;
.process-buttons {
display: flex;
column-gap: 0.5rem;
}
.invoke-btn {
flex-grow: 1;
width: 100%;
@include Button(
$btn-color: var(--accent-color),
$btn-color-hover: var(--accent-color-hover),
$icon-size: 16px
);
}
.cancel-btn {
@include Button(
$btn-color: var(--destructive-color),
$btn-color-hover: var(--destructive-color-hover),
$btn-width: 3rem
);
}
.loopback-btn {
&[data-as-checkbox='true'] {
background-color: var(--btn-btn-base-color);
border: 3px solid var(--btn-btn-base-color);
svg {
fill: var(--text-color);
}
&:hover {
background-color: var(--btn-btn-base-color);
border-color: var(--btn-checkbox-border-hover);
svg {
fill: var(--text-color);
}
}
&[data-selected='true'] {
border-color: var(--accent-color);
background-color: var(--btn-btn-base-color);
svg {
fill: var(--text-color);
}
&:hover {
border-color: var(--accent-color);
background-color: var(--btn-btn-base-color);
svg {
fill: var(--text-color);
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
import InvokeButton from './InvokeButton';
import CancelButton from './CancelButton';
import LoopbackButton from './Loopback';
import { useAppSelector } from 'app/storeHooks';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
/**
* Buttons to start and cancel image generation.
*/
const ProcessButtons = () => {
const activeTabName = useAppSelector(activeTabNameSelector);
return (
<div className="process-buttons">
<InvokeButton />
{activeTabName === 'img2img' && <LoopbackButton />}
<CancelButton />
</div>
);
};
export default ProcessButtons;

View File

@@ -0,0 +1,40 @@
import { FormControl, Textarea } from '@chakra-ui/react';
import type { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { setNegativePrompt } from 'features/parameters/store/generationSlice';
import { useTranslation } from 'react-i18next';
const NegativePromptInput = () => {
const negativePrompt = useAppSelector(
(state: RootState) => state.generation.negativePrompt
);
const dispatch = useAppDispatch();
const { t } = useTranslation();
return (
<FormControl>
<Textarea
id="negativePrompt"
name="negativePrompt"
value={negativePrompt}
onChange={(e) => dispatch(setNegativePrompt(e.target.value))}
background="var(--prompt-bg-color)"
placeholder={t('parameters:negativePrompts')}
_placeholder={{ fontSize: '0.8rem' }}
borderColor="var(--border-color)"
_hover={{
borderColor: 'var(--border-color-light)',
}}
_focusVisible={{
borderColor: 'var(--border-color-invalid)',
boxShadow: '0 0 10px var(--box-shadow-color-invalid)',
}}
fontSize="0.9rem"
color="var(--text-color-secondary)"
/>
</FormControl>
);
};
export default NegativePromptInput;

View File

@@ -0,0 +1,34 @@
.prompt-bar {
display: grid;
row-gap: 1rem;
input,
textarea {
background-color: var(--prompt-bg-color);
font-size: 1rem;
border: 2px solid var(--border-color);
&:hover {
border: 2px solid var(--border-color-light);
}
&:focus-visible {
border: 2px solid var(--input-border-color);
box-shadow: 0 0 10px 0 var(--input-box-shadow-color);
}
&[aria-invalid='true'] {
border: 2px solid var(--border-color-invalid);
box-shadow: 0 0 10px 0 var(--box-shadow-color-invalid);
}
&:disabled {
border: 2px solid var(--border-color);
box-shadow: none;
}
}
textarea {
min-height: 10rem;
}
}

View File

@@ -0,0 +1,86 @@
import { FormControl, Textarea } from '@chakra-ui/react';
import { ChangeEvent, KeyboardEvent, useRef } from 'react';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { generateImage } from 'app/socketio/actions';
import { GenerationState, setPrompt } from 'features/parameters/store/generationSlice';
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { useHotkeys } from 'react-hotkeys-hook';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { readinessSelector } from 'app/selectors/readinessSelector';
import { useTranslation } from 'react-i18next';
const promptInputSelector = createSelector(
[(state: RootState) => state.generation, activeTabNameSelector],
(parameters: GenerationState, activeTabName) => {
return {
prompt: parameters.prompt,
activeTabName,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
/**
* Prompt input text area.
*/
const PromptInput = () => {
const dispatch = useAppDispatch();
const { prompt, activeTabName } = useAppSelector(promptInputSelector);
const { isReady } = useAppSelector(readinessSelector);
const promptRef = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();
const handleChangePrompt = (e: ChangeEvent<HTMLTextAreaElement>) => {
dispatch(setPrompt(e.target.value));
};
useHotkeys(
'alt+a',
() => {
promptRef.current?.focus();
},
[]
);
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && e.shiftKey === false && isReady) {
e.preventDefault();
dispatch(generateImage(activeTabName));
}
};
return (
<div className="prompt-bar">
<FormControl
isInvalid={prompt.length === 0 || Boolean(prompt.match(/^[\s\r\n]+$/))}
>
<Textarea
id="prompt"
name="prompt"
placeholder={t('parameters:promptPlaceholder')}
size={'lg'}
value={prompt}
onChange={handleChangePrompt}
onKeyDown={handleKeyDown}
resize="vertical"
height={30}
ref={promptRef}
_placeholder={{
color: 'var(--text-color-secondary)',
}}
/>
</FormControl>
</div>
);
};
export default PromptInput;

View File

@@ -0,0 +1,17 @@
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
import { RootState } from 'app/store';
export const generationSelector = (state: RootState) => state.generation;
export const mayGenerateMultipleImagesSelector = createSelector(
generationSelector,
({ shouldRandomizeSeed, shouldGenerateVariations }) => {
return shouldRandomizeSeed || shouldGenerateVariations;
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);

View File

@@ -0,0 +1,358 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import * as InvokeAI from 'app/invokeai';
import promptToString from 'common/util/promptToString';
import { seedWeightsToString } from 'common/util/seedWeightPairs';
import { getPromptAndNegative } from 'common/util/getPromptAndNegative';
export interface GenerationState {
cfgScale: number;
height: number;
img2imgStrength: number;
infillMethod: string;
initialImage?: InvokeAI.Image | string; // can be an Image or url
iterations: number;
maskPath: string;
perlin: number;
prompt: string;
negativePrompt: string;
sampler: string;
seamBlur: number;
seamless: boolean;
seamSize: number;
seamSteps: number;
seamStrength: number;
seed: number;
seedWeights: string;
shouldFitToWidthHeight: boolean;
shouldGenerateVariations: boolean;
shouldRandomizeSeed: boolean;
steps: number;
threshold: number;
tileSize: number;
variationAmount: number;
width: number;
}
const initialGenerationState: GenerationState = {
cfgScale: 7.5,
height: 512,
img2imgStrength: 0.75,
infillMethod: 'patchmatch',
iterations: 1,
maskPath: '',
perlin: 0,
prompt: '',
negativePrompt: '',
sampler: 'k_lms',
seamBlur: 16,
seamless: false,
seamSize: 96,
seamSteps: 30,
seamStrength: 0.7,
seed: 0,
seedWeights: '',
shouldFitToWidthHeight: true,
shouldGenerateVariations: false,
shouldRandomizeSeed: true,
steps: 50,
threshold: 0,
tileSize: 32,
variationAmount: 0.1,
width: 512,
};
const initialState: GenerationState = initialGenerationState;
export const generationSlice = createSlice({
name: 'generation',
initialState,
reducers: {
setPrompt: (state, action: PayloadAction<string | InvokeAI.Prompt>) => {
const newPrompt = action.payload;
if (typeof newPrompt === 'string') {
state.prompt = newPrompt;
} else {
state.prompt = promptToString(newPrompt);
}
},
setNegativePrompt: (
state,
action: PayloadAction<string | InvokeAI.Prompt>
) => {
const newPrompt = action.payload;
if (typeof newPrompt === 'string') {
state.negativePrompt = newPrompt;
} else {
state.negativePrompt = promptToString(newPrompt);
}
},
setIterations: (state, action: PayloadAction<number>) => {
state.iterations = action.payload;
},
setSteps: (state, action: PayloadAction<number>) => {
state.steps = action.payload;
},
setCfgScale: (state, action: PayloadAction<number>) => {
state.cfgScale = action.payload;
},
setThreshold: (state, action: PayloadAction<number>) => {
state.threshold = action.payload;
},
setPerlin: (state, action: PayloadAction<number>) => {
state.perlin = action.payload;
},
setHeight: (state, action: PayloadAction<number>) => {
state.height = action.payload;
},
setWidth: (state, action: PayloadAction<number>) => {
state.width = action.payload;
},
setSampler: (state, action: PayloadAction<string>) => {
state.sampler = action.payload;
},
setSeed: (state, action: PayloadAction<number>) => {
state.seed = action.payload;
state.shouldRandomizeSeed = false;
},
setImg2imgStrength: (state, action: PayloadAction<number>) => {
state.img2imgStrength = action.payload;
},
setMaskPath: (state, action: PayloadAction<string>) => {
state.maskPath = action.payload;
},
setSeamless: (state, action: PayloadAction<boolean>) => {
state.seamless = action.payload;
},
setShouldFitToWidthHeight: (state, action: PayloadAction<boolean>) => {
state.shouldFitToWidthHeight = action.payload;
},
resetSeed: (state) => {
state.seed = -1;
},
setParameter: (
state,
action: PayloadAction<{ key: string; value: string | number | boolean }>
) => {
// TODO: This probably needs to be refactored.
// TODO: This probably also needs to be fixed after the reorg.
const { key, value } = action.payload;
const temp = { ...state, [key]: value };
if (key === 'seed') {
temp.shouldRandomizeSeed = false;
}
return temp;
},
setShouldGenerateVariations: (state, action: PayloadAction<boolean>) => {
state.shouldGenerateVariations = action.payload;
},
setVariationAmount: (state, action: PayloadAction<number>) => {
state.variationAmount = action.payload;
},
setSeedWeights: (state, action: PayloadAction<string>) => {
state.seedWeights = action.payload;
state.shouldGenerateVariations = true;
state.variationAmount = 0;
},
setAllTextToImageParameters: (
state,
action: PayloadAction<InvokeAI.Metadata>
) => {
const {
sampler,
prompt,
seed,
variations,
steps,
cfg_scale,
threshold,
perlin,
seamless,
hires_fix,
width,
height,
} = action.payload.image;
if (variations && variations.length > 0) {
state.seedWeights = seedWeightsToString(variations);
state.shouldGenerateVariations = true;
state.variationAmount = 0;
} else {
state.shouldGenerateVariations = false;
}
if (seed) {
state.seed = seed;
state.shouldRandomizeSeed = false;
}
if (prompt) state.prompt = promptToString(prompt);
if (sampler) state.sampler = sampler;
if (steps) state.steps = steps;
if (cfg_scale) state.cfgScale = cfg_scale;
if (typeof threshold === 'undefined') {
state.threshold = 0;
} else {
state.threshold = threshold;
}
if (perlin) state.perlin = perlin;
if (typeof perlin === 'undefined') state.perlin = 0;
if (typeof seamless === 'boolean') state.seamless = seamless;
// if (typeof hires_fix === 'boolean') state.hiresFix = hires_fix; // TODO: Needs to be fixed after reorg
if (width) state.width = width;
if (height) state.height = height;
},
setAllImageToImageParameters: (
state,
action: PayloadAction<InvokeAI.Metadata>
) => {
const { type, strength, fit, init_image_path, mask_image_path } =
action.payload.image;
if (type === 'img2img') {
if (init_image_path) state.initialImage = init_image_path;
if (mask_image_path) state.maskPath = mask_image_path;
if (strength) state.img2imgStrength = strength;
if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit;
}
},
setAllParameters: (state, action: PayloadAction<InvokeAI.Metadata>) => {
const {
type,
sampler,
prompt,
seed,
variations,
steps,
cfg_scale,
threshold,
perlin,
seamless,
hires_fix,
width,
height,
strength,
fit,
init_image_path,
mask_image_path,
} = action.payload.image;
if (type === 'img2img') {
if (init_image_path) state.initialImage = init_image_path;
if (mask_image_path) state.maskPath = mask_image_path;
if (strength) state.img2imgStrength = strength;
if (typeof fit === 'boolean') state.shouldFitToWidthHeight = fit;
}
if (variations && variations.length > 0) {
state.seedWeights = seedWeightsToString(variations);
state.shouldGenerateVariations = true;
state.variationAmount = 0;
} else {
state.shouldGenerateVariations = false;
}
if (seed) {
state.seed = seed;
state.shouldRandomizeSeed = false;
}
if (prompt) {
const [promptOnly, negativePrompt] = getPromptAndNegative(prompt);
if (promptOnly) state.prompt = promptOnly;
negativePrompt
? (state.negativePrompt = negativePrompt)
: (state.negativePrompt = '');
}
if (sampler) state.sampler = sampler;
if (steps) state.steps = steps;
if (cfg_scale) state.cfgScale = cfg_scale;
if (threshold) state.threshold = threshold;
if (typeof threshold === 'undefined') state.threshold = 0;
if (perlin) state.perlin = perlin;
if (typeof perlin === 'undefined') state.perlin = 0;
if (typeof seamless === 'boolean') state.seamless = seamless;
// if (typeof hires_fix === 'boolean') state.hiresFix = hires_fix; // TODO: Needs to be fixed after reorg
if (width) state.width = width;
if (height) state.height = height;
// state.shouldRunESRGAN = false; // TODO: Needs to be fixed after reorg
// state.shouldRunFacetool = false; // TODO: Needs to be fixed after reorg
},
resetParametersState: (state) => {
return {
...state,
...initialGenerationState,
};
},
setShouldRandomizeSeed: (state, action: PayloadAction<boolean>) => {
state.shouldRandomizeSeed = action.payload;
},
setInitialImage: (
state,
action: PayloadAction<InvokeAI.Image | string>
) => {
state.initialImage = action.payload;
},
clearInitialImage: (state) => {
state.initialImage = undefined;
},
setSeamSize: (state, action: PayloadAction<number>) => {
state.seamSize = action.payload;
},
setSeamBlur: (state, action: PayloadAction<number>) => {
state.seamBlur = action.payload;
},
setSeamStrength: (state, action: PayloadAction<number>) => {
state.seamStrength = action.payload;
},
setSeamSteps: (state, action: PayloadAction<number>) => {
state.seamSteps = action.payload;
},
setTileSize: (state, action: PayloadAction<number>) => {
state.tileSize = action.payload;
},
setInfillMethod: (state, action: PayloadAction<string>) => {
state.infillMethod = action.payload;
},
},
});
export const {
clearInitialImage,
resetParametersState,
resetSeed,
setAllImageToImageParameters,
setAllParameters,
setAllTextToImageParameters,
setCfgScale,
setHeight,
setImg2imgStrength,
setInfillMethod,
setInitialImage,
setIterations,
setMaskPath,
setParameter,
setPerlin,
setPrompt,
setNegativePrompt,
setSampler,
setSeamBlur,
setSeamless,
setSeamSize,
setSeamSteps,
setSeamStrength,
setSeed,
setSeedWeights,
setShouldFitToWidthHeight,
setShouldGenerateVariations,
setShouldRandomizeSeed,
setSteps,
setThreshold,
setTileSize,
setVariationAmount,
setWidth,
} = generationSlice.actions;
export default generationSlice.reducer;

View File

@@ -0,0 +1,3 @@
import { RootState } from 'app/store';
export const postprocessingSelector = (state: RootState) => state.postprocessing;

View File

@@ -0,0 +1,94 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { FACETOOL_TYPES } from 'app/constants';
export type UpscalingLevel = 2 | 4;
export type FacetoolType = typeof FACETOOL_TYPES[number];
export interface PostprocessingState {
codeformerFidelity: number;
facetoolStrength: number;
facetoolType: FacetoolType;
hiresFix: boolean;
hiresStrength: number;
shouldLoopback: boolean;
shouldRunESRGAN: boolean;
shouldRunFacetool: boolean;
upscalingLevel: UpscalingLevel;
upscalingStrength: number;
}
const initialPostprocessingState: PostprocessingState = {
codeformerFidelity: 0.75,
facetoolStrength: 0.8,
facetoolType: 'gfpgan',
hiresFix: false,
hiresStrength: 0.75,
shouldLoopback: false,
shouldRunESRGAN: false,
shouldRunFacetool: false,
upscalingLevel: 4,
upscalingStrength: 0.75,
};
const initialState: PostprocessingState = initialPostprocessingState;
export const postprocessingSlice = createSlice({
name: 'postprocessing',
initialState,
reducers: {
setFacetoolStrength: (state, action: PayloadAction<number>) => {
state.facetoolStrength = action.payload;
},
setCodeformerFidelity: (state, action: PayloadAction<number>) => {
state.codeformerFidelity = action.payload;
},
setUpscalingLevel: (state, action: PayloadAction<UpscalingLevel>) => {
state.upscalingLevel = action.payload;
},
setUpscalingStrength: (state, action: PayloadAction<number>) => {
state.upscalingStrength = action.payload;
},
setHiresFix: (state, action: PayloadAction<boolean>) => {
state.hiresFix = action.payload;
},
setHiresStrength: (state, action: PayloadAction<number>) => {
state.hiresStrength = action.payload;
},
resetPostprocessingState: (state) => {
return {
...state,
...initialPostprocessingState,
};
},
setShouldRunFacetool: (state, action: PayloadAction<boolean>) => {
state.shouldRunFacetool = action.payload;
},
setFacetoolType: (state, action: PayloadAction<FacetoolType>) => {
state.facetoolType = action.payload;
},
setShouldRunESRGAN: (state, action: PayloadAction<boolean>) => {
state.shouldRunESRGAN = action.payload;
},
setShouldLoopback: (state, action: PayloadAction<boolean>) => {
state.shouldLoopback = action.payload;
},
},
});
export const {
resetPostprocessingState,
setCodeformerFidelity,
setFacetoolStrength,
setFacetoolType,
setHiresFix,
setHiresStrength,
setShouldLoopback,
setShouldRunESRGAN,
setShouldRunFacetool,
setUpscalingLevel,
setUpscalingStrength,
} = postprocessingSlice.actions;
export default postprocessingSlice.reducer;