mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): wip img2img ui
This commit is contained in:
parent
568f0aad71
commit
d40d5276dd
@ -98,7 +98,8 @@
|
||||
"pinOptionsPanel": "Pin Options Panel",
|
||||
"loading": "Loading",
|
||||
"loadingInvokeAI": "Loading Invoke AI",
|
||||
"random": "Random"
|
||||
"random": "Random",
|
||||
"generate": "Generate"
|
||||
},
|
||||
"gallery": {
|
||||
"generations": "Generations",
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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) => {
|
||||
|
37
invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
Normal file
37
invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts
Normal file
@ -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]
|
||||
);
|
||||
};
|
@ -59,9 +59,6 @@ const CurrentImageDisplay = () => {
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'absolute', top: 0 }}>
|
||||
<CurrentImageButtons />
|
||||
</Box>
|
||||
<Flex
|
||||
sx={{
|
||||
w: 'full',
|
||||
@ -83,6 +80,9 @@ const CurrentImageDisplay = () => {
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
<Box sx={{ position: 'absolute', top: 0 }}>
|
||||
<CurrentImageButtons />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -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 },
|
||||
|
@ -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<HTMLInputElement>) =>
|
||||
// dispatch(setShouldRandomizeSeed(e.target.checked));
|
||||
|
||||
// return (
|
||||
// <Switch
|
||||
// aria-label={t('parameters.randomizeSeed')}
|
||||
// isChecked={shouldRandomizeSeed}
|
||||
// onChange={handleChangeShouldRandomizeSeed}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
const SeedToggle = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -16,13 +36,15 @@ export default function RandomizeSeed() {
|
||||
);
|
||||
|
||||
const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldRandomizeSeed(e.target.checked));
|
||||
dispatch(setShouldRandomizeSeed(!e.target.checked));
|
||||
|
||||
return (
|
||||
<Switch
|
||||
aria-label={t('parameters.randomizeSeed')}
|
||||
isChecked={shouldRandomizeSeed}
|
||||
isChecked={!shouldRandomizeSeed}
|
||||
onChange={handleChangeShouldRandomizeSeed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(SeedToggle);
|
||||
|
@ -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 (
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
w: size,
|
||||
h: size,
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
bg: 'base.700',
|
||||
color: 'base.400',
|
||||
borderRadius: 'base',
|
||||
aspectRatio: ratioCSSString,
|
||||
objectFit: 'contain',
|
||||
...(orientation === 'landscape' ? { h: 'full' } : { w: 'full' }),
|
||||
}}
|
||||
>
|
||||
<Text sx={{ size: 'xs', userSelect: 'none' }}>
|
||||
{ratioDisplayString}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AspectRatioPreview);
|
@ -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<Orientation>('portrait');
|
||||
|
||||
return (
|
||||
<Flex gap={3}>
|
||||
<Box flexShrink={0}>
|
||||
<AspectRatioPreview
|
||||
ratio={RATIOS[ratioIndex]}
|
||||
orientation={orientation}
|
||||
size="4rem"
|
||||
/>
|
||||
</Box>
|
||||
<FormControl>
|
||||
<FormLabel>Aspect Ratio</FormLabel>
|
||||
<Select
|
||||
onChange={(e) => {
|
||||
setRatioIndex(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{RATIOS.map((r, i) => (
|
||||
<option key={r.join()} value={i}>{`${r[0]}:${r[1]}`}</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<IAISlider
|
||||
label="Size"
|
||||
value={width}
|
||||
min={64}
|
||||
max={2048}
|
||||
step={8}
|
||||
onChange={(v) => {
|
||||
dispatch(setWidth(v));
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DimensionsSettings);
|
@ -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 (
|
||||
<Box {...props}>
|
||||
<IAISlider
|
||||
isDisabled={activeTabName === 'unifiedCanvas'}
|
||||
label={t('parameters.height')}
|
||||
value={height}
|
||||
min={64}
|
||||
step={shift ? 8 : 64}
|
||||
max={2048}
|
||||
onChange={(v) => dispatch(setHeight(v))}
|
||||
handleReset={() => dispatch(setHeight(512))}
|
||||
withInput
|
||||
withReset
|
||||
withSliderMarks
|
||||
sliderNumberInputProps={{ max: 15360 }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(HeightSlider);
|
@ -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))}
|
||||
|
@ -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 (
|
||||
<IAISelect
|
||||
label={t('parameters.sampler')}
|
||||
value={sampler}
|
||||
onChange={handleChangeSampler}
|
||||
validValues={
|
||||
activeModel.format === 'diffusers' ? DIFFUSERS_SAMPLERS : SAMPLERS
|
||||
}
|
||||
minWidth={36}
|
||||
/>
|
||||
<Box {...props}>
|
||||
<IAISelect
|
||||
label={t('parameters.sampler')}
|
||||
value={sampler}
|
||||
onChange={handleChangeSampler}
|
||||
validValues={
|
||||
activeModel.format === 'diffusers' ? DIFFUSERS_SAMPLERS : SAMPLERS
|
||||
}
|
||||
minWidth={36}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -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() {
|
||||
<MainSampler />
|
||||
</VStack>
|
||||
) : (
|
||||
<Flex rowGap={2} flexDirection="column">
|
||||
<Flex columnGap={1}>
|
||||
<Flex gap={3} flexDirection="column">
|
||||
<Flex gap={3}>
|
||||
<MainIterations />
|
||||
<MainSteps />
|
||||
<MainCFGScale />
|
||||
</Flex>
|
||||
<Flex columnGap={1}>
|
||||
<MainWidth />
|
||||
<MainHeight />
|
||||
<MainSampler />
|
||||
<Flex gap={3}>
|
||||
<MainSampler flexGrow={2} />
|
||||
<ModelSelect flexGrow={3} />
|
||||
</Flex>
|
||||
<WidthSlider />
|
||||
<HeightSlider />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -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))}
|
||||
|
@ -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 (
|
||||
<Box {...props}>
|
||||
<IAISlider
|
||||
isDisabled={activeTabName === 'unifiedCanvas'}
|
||||
label={t('parameters.width')}
|
||||
value={width}
|
||||
min={64}
|
||||
step={shift ? 8 : 64}
|
||||
max={2048}
|
||||
onChange={(v) => dispatch(setWidth(v))}
|
||||
handleReset={() => dispatch(setWidth(512))}
|
||||
withInput
|
||||
withReset
|
||||
withSliderMarks
|
||||
sliderNumberInputProps={{ max: 15360 }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WidthSlider);
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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<IAIButtonProps | IAIIconButtonProps, 'aria-label'> {
|
||||
@ -26,7 +26,7 @@ export default function InvokeButton(props: InvokeButton) {
|
||||
|
||||
const handleClickGenerate = () => {
|
||||
// dispatch(generateImage(activeTabName));
|
||||
dispatch(linearGraphBuilt());
|
||||
dispatch(generateGraphBuilt());
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
@ -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 (
|
||||
<Flex
|
||||
style={{
|
||||
paddingInlineStart: 1.5,
|
||||
}}
|
||||
>
|
||||
<Box {...props}>
|
||||
<IAISelect
|
||||
label={t('modelManager.model')}
|
||||
style={{ fontSize: 'sm' }}
|
||||
aria-label={t('accessibility.modelSelect')}
|
||||
tooltip={selectedModel?.description || ''}
|
||||
@ -52,7 +49,7 @@ const ModelSelect = () => {
|
||||
validValues={allModelNames}
|
||||
onChange={handleChangeModel}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -34,8 +34,6 @@ const SiteHeader = () => {
|
||||
>
|
||||
<StatusIndicator />
|
||||
|
||||
<ModelSelect />
|
||||
|
||||
{resolution === 'desktop' ? (
|
||||
<SiteHeaderMenu />
|
||||
) : (
|
||||
|
@ -39,7 +39,7 @@ export const floatingParametersPanelButtonSelector = createSelector(
|
||||
const shouldShowParametersPanelButton =
|
||||
!canvasBetaLayoutCheck &&
|
||||
(!shouldPinParametersPanel || !shouldShowParametersPanel) &&
|
||||
['linear', 'unifiedCanvas'].includes(activeTabName);
|
||||
['generate', 'unifiedCanvas'].includes(activeTabName);
|
||||
|
||||
return {
|
||||
shouldPinParametersPanel,
|
||||
|
@ -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: <Icon as={FaImage} sx={tabIconStyles} />,
|
||||
workarea: <LinearWorkspace />,
|
||||
},
|
||||
{
|
||||
id: 'unifiedCanvas',
|
||||
icon: <Icon as={MdGridOn} sx={tabIconStyles} />,
|
||||
workarea: <UnifiedCanvasWorkarea />,
|
||||
},
|
||||
{
|
||||
id: 'nodes',
|
||||
icon: <Icon as={MdDeviceHub} sx={tabIconStyles} />,
|
||||
workarea: <NodeEditor />,
|
||||
},
|
||||
];
|
||||
return tabs.filter((tab) => !disabledTabs.includes(tab.id));
|
||||
};
|
||||
const tabs: InvokeTabInfo[] = [
|
||||
{
|
||||
id: 'generate',
|
||||
icon: <Icon as={BsLightningChargeFill} sx={{ boxSize: 5 }} />,
|
||||
workarea: <GenerateWorkspace />,
|
||||
},
|
||||
{
|
||||
id: 'unifiedCanvas',
|
||||
icon: <Icon as={MdGridOn} sx={{ boxSize: 6 }} />,
|
||||
workarea: <UnifiedCanvasWorkarea />,
|
||||
},
|
||||
{
|
||||
id: 'nodes',
|
||||
icon: <Icon as={MdDeviceHub} sx={{ boxSize: 6 }} />,
|
||||
workarea: <NodeEditor />,
|
||||
},
|
||||
];
|
||||
|
||||
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) => (
|
||||
<Tooltip
|
||||
key={tab.id}
|
||||
hasArrow
|
||||
@ -126,13 +132,15 @@ export default function InvokeTabs() {
|
||||
</Tab>
|
||||
</Tooltip>
|
||||
)),
|
||||
[t, activeTabs]
|
||||
[t, enabledTabs]
|
||||
);
|
||||
|
||||
const tabPanels = useMemo(
|
||||
() =>
|
||||
activeTabs.map((tab) => <TabPanel key={tab.id}>{tab.workarea}</TabPanel>),
|
||||
[activeTabs]
|
||||
enabledTabs.map((tab) => (
|
||||
<TabPanel key={tab.id}>{tab.workarea}</TabPanel>
|
||||
)),
|
||||
[enabledTabs]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import CurrentImageDisplay from 'features/gallery/components/CurrentImageDisplay';
|
||||
|
||||
const LinearContent = () => {
|
||||
const GenerateContent = () => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -18,4 +18,4 @@ const LinearContent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default LinearContent;
|
||||
export default GenerateContent;
|
@ -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',
|
||||
}}
|
||||
>
|
||||
<MainSettings />
|
||||
<ImageToImageToggle />
|
||||
</Flex>
|
||||
<ParametersAccordion accordionItems={linearAccordions} />
|
||||
<ImageToImageToggle />
|
||||
<ParametersAccordion accordionItems={generateAccordionItems} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LinearParameters);
|
||||
export default memo(GenerateParameters);
|
@ -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 = () => {
|
||||
}}
|
||||
>
|
||||
<Scrollable>
|
||||
<LinearParameters />
|
||||
<GenerateParameters />
|
||||
</Scrollable>
|
||||
<PinParametersPanelButton
|
||||
sx={{ position: 'absolute', top: 0, insetInlineEnd: 0 }}
|
||||
@ -42,12 +42,12 @@ const LinearWorkspace = () => {
|
||||
</Flex>
|
||||
) : (
|
||||
<ParametersSlide>
|
||||
<LinearParameters />
|
||||
<GenerateParameters />
|
||||
</ParametersSlide>
|
||||
)}
|
||||
<LinearContent />
|
||||
<GenerateContent />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LinearWorkspace);
|
||||
export default memo(GenerateWorkspace);
|
26
invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts
Normal file
26
invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts
Normal file
@ -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<boolean>) => {
|
||||
state.shift = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { shiftKeyPressed } = hotkeysSlice.actions;
|
||||
|
||||
export default hotkeysSlice.reducer;
|
@ -1,7 +1,7 @@
|
||||
export const tabMap = [
|
||||
// 'txt2img',
|
||||
// 'img2img',
|
||||
'linear',
|
||||
'generate',
|
||||
'unifiedCanvas',
|
||||
'nodes',
|
||||
// 'postprocessing',
|
||||
|
@ -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<number[]>) => {
|
||||
if (tabMap[state.activeTab] === 'linear') {
|
||||
state.openLinearAccordionItems = action.payload;
|
||||
if (tabMap[state.activeTab] === 'generate') {
|
||||
state.openGenerateAccordionItems = action.payload;
|
||||
}
|
||||
|
||||
if (tabMap[state.activeTab] === 'unifiedCanvas') {
|
||||
|
@ -15,5 +15,8 @@ export interface UIState {
|
||||
shouldPinGallery: boolean;
|
||||
shouldShowGallery: boolean;
|
||||
openLinearAccordionItems: number[];
|
||||
disabledParameterPanels: string[];
|
||||
disabledTabs: InvokeTabName[];
|
||||
openGenerateAccordionItems: number[];
|
||||
openUnifiedCanvasAccordionItems: number[];
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user