feat(ui): add config slice, configuration default values

This commit is contained in:
psychedelicious 2023-04-26 20:27:15 +10:00
parent 55e33eaf4c
commit 0a936696c3
39 changed files with 810 additions and 633 deletions

View File

@ -15,62 +15,39 @@ import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel';
import Lightbox from 'features/lightbox/components/Lightbox'; import Lightbox from 'features/lightbox/components/Lightbox';
import { useAppDispatch, useAppSelector } from './storeHooks'; import { useAppDispatch, useAppSelector } from './storeHooks';
import { PropsWithChildren, useCallback, useEffect, useState } from 'react'; import { PropsWithChildren, useCallback, useEffect, useState } from 'react';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { shouldTransformUrlsChanged } from 'features/system/store/systemSlice';
import { setShouldFetchImages } from 'features/gallery/store/resultsSlice';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import Loading from 'common/components/Loading/Loading'; import Loading from 'common/components/Loading/Loading';
import {
disabledFeaturesChanged,
disabledTabsChanged,
} from 'features/system/store/systemSlice';
import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady'; import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
import { ApplicationFeature } from './invokeai'; import { AppConfig } from './invokeai';
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
import { configChanged } from 'features/system/store/configSlice';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
keepGUIAlive(); keepGUIAlive();
interface Props extends PropsWithChildren { interface Props extends PropsWithChildren {
options: { config?: Partial<AppConfig>;
disabledTabs: InvokeTabName[];
disabledFeatures: ApplicationFeature[];
shouldTransformUrls?: boolean;
shouldFetchImages: boolean;
};
} }
const App = (props: Props) => { const App = ({ config = {}, children }: Props) => {
useToastWatcher(); useToastWatcher();
useGlobalHotkeys(); useGlobalHotkeys();
const currentTheme = useAppSelector((state) => state.ui.currentTheme); const currentTheme = useAppSelector((state) => state.ui.currentTheme);
const disabledFeatures = useAppSelector(
(state) => state.system.disabledFeatures const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
);
const isApplicationReady = useIsApplicationReady(); const isApplicationReady = useIsApplicationReady();
const [loadingOverridden, setLoadingOverridden] = useState(false); const [loadingOverridden, setLoadingOverridden] = useState(false);
const { setColorMode } = useColorMode(); const { setColorMode } = useColorMode();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
dispatch(disabledFeaturesChanged(props.options.disabledFeatures)); console.log('Received config: ', config);
}, [dispatch, props.options.disabledFeatures]); dispatch(configChanged(config));
}, [dispatch, config]);
useEffect(() => {
dispatch(disabledTabsChanged(props.options.disabledTabs));
}, [dispatch, props.options.disabledTabs]);
useEffect(() => {
dispatch(
shouldTransformUrlsChanged(Boolean(props.options.shouldTransformUrls))
);
}, [dispatch, props.options.shouldTransformUrls]);
useEffect(() => {
dispatch(setShouldFetchImages(props.options.shouldFetchImages));
}, [dispatch, props.options.shouldFetchImages]);
useEffect(() => { useEffect(() => {
setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark'); setColorMode(['light'].includes(currentTheme) ? 'light' : 'dark');
@ -82,7 +59,7 @@ const App = (props: Props) => {
return ( return (
<Grid w="100vw" h="100vh" position="relative"> <Grid w="100vw" h="100vh" position="relative">
{!disabledFeatures.includes('lightbox') && <Lightbox />} {isLightboxEnabled && <Lightbox />}
<ImageUploader> <ImageUploader>
<ProgressBar /> <ProgressBar />
<Grid <Grid
@ -92,7 +69,7 @@ const App = (props: Props) => {
w={APP_WIDTH} w={APP_WIDTH}
h={APP_HEIGHT} h={APP_HEIGHT}
> >
{props.children || <SiteHeader />} {children || <SiteHeader />}
<Flex <Flex
gap={4} gap={4}
w={{ base: '100vw', xl: 'full' }} w={{ base: '100vw', xl: 'full' }}

View File

@ -12,24 +12,12 @@
* 'gfpgan'. * 'gfpgan'.
*/ */
import { FacetoolType } from 'features/parameters/store/postprocessingSlice';
import { InvokeTabName } from 'features/ui/store/tabMap'; import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types'; import { IRect } from 'konva/lib/types';
import { ImageMetadata, ImageType } from 'services/api'; import { ImageMetadata, ImageType } from 'services/api';
import { AnyInvocation } from 'services/events/types'; import { AnyInvocation } from 'services/events/types';
/**
* A disable-able application feature
*/
export declare type ApplicationFeature =
| 'faceRestore'
| 'upscaling'
| 'lightbox'
| 'modelManager'
| 'githubLink'
| 'discordLink'
| 'bugLink'
| 'localization';
/** /**
* TODO: * TODO:
* Once an image has been generated, if it is postprocessed again, * Once an image has been generated, if it is postprocessed again,
@ -347,3 +335,93 @@ export declare type UploadOutpaintingMergeImagePayload = {
dataURL: string; dataURL: string;
name: string; name: string;
}; };
/**
* A disable-able application feature
*/
export declare type AppFeature =
| 'faceRestore'
| 'upscaling'
| 'lightbox'
| 'modelManager'
| 'githubLink'
| 'discordLink'
| 'bugLink'
| 'localization';
/**
* A disable-able Stable Diffusion feature
*/
export declare type StableDiffusionFeature =
| 'noiseConfig'
| 'variations'
| 'symmetry'
| 'tiling'
| 'hires';
/**
* Configuration options for the InvokeAI UI.
* Distinct from system settings which may be changed inside the app.
*/
export declare type AppConfig = {
/**
* Whether or not URLs should be transformed to use a different host
*/
shouldTransformUrls: boolean;
/**
* Whether or not we need to re-fetch images
*/
shouldFetchImages: boolean;
disabledTabs: InvokeTabName[];
disabledFeatures: AppFeature[];
sd: {
iterations: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
width: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
height: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
steps: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
guidance: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
img2imgStrength: {
initial: number;
min: number;
sliderMax: number;
inputMax: number;
fineStep: number;
coarseStep: number;
};
};
};

View File

@ -13,6 +13,7 @@ import lightboxReducer from 'features/lightbox/store/lightboxSlice';
import generationReducer from 'features/parameters/store/generationSlice'; import generationReducer from 'features/parameters/store/generationSlice';
import postprocessingReducer from 'features/parameters/store/postprocessingSlice'; import postprocessingReducer from 'features/parameters/store/postprocessingSlice';
import systemReducer from 'features/system/store/systemSlice'; import systemReducer from 'features/system/store/systemSlice';
import configReducer from 'features/system/store/configSlice';
import uiReducer from 'features/ui/store/uiSlice'; import uiReducer from 'features/ui/store/uiSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice'; import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import modelsReducer from 'features/system/store/modelSlice'; import modelsReducer from 'features/system/store/modelSlice';
@ -54,6 +55,7 @@ const rootReducer = combineReducers({
postprocessing: postprocessingReducer, postprocessing: postprocessingReducer,
results: resultsReducer, results: resultsReducer,
system: systemReducer, system: systemReducer,
config: configReducer,
ui: uiReducer, ui: uiReducer,
uploads: uploadsReducer, uploads: uploadsReducer,
hotkeys: hotkeysReducer, hotkeys: hotkeysReducer,
@ -78,6 +80,7 @@ const rootPersistConfig = getPersistConfig({
// ...uploadsBlacklist, // ...uploadsBlacklist,
'uploads', 'uploads',
'hotkeys', 'hotkeys',
'config',
], ],
debounce: 300, debounce: 300,
}); });

View File

@ -26,7 +26,15 @@ import {
import { clamp } from 'lodash'; import { clamp } from 'lodash';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FocusEvent, memo, useEffect, useMemo, useState } from 'react'; import {
FocusEvent,
memo,
MouseEvent,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { BiReset } from 'react-icons/bi'; import { BiReset } from 'react-icons/bi';
import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton'; import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
import { roundDownToMultiple } from 'common/util/roundDownToMultiple'; import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
@ -109,33 +117,52 @@ const IAISlider = (props: IAIFullSliderProps) => {
[max, sliderNumberInputProps?.max] [max, sliderNumberInputProps?.max]
); );
const handleSliderChange = (v: number) => { const handleSliderChange = useCallback(
onChange(v); (v: number) => {
}; onChange(v);
},
[onChange]
);
const handleInputBlur = (e: FocusEvent<HTMLInputElement>) => { const handleInputBlur = useCallback(
if (e.target.value === '') e.target.value = String(min); (e: FocusEvent<HTMLInputElement>) => {
const clamped = clamp( if (e.target.value === '') {
isInteger ? Math.floor(Number(e.target.value)) : Number(localInputValue), e.target.value = String(min);
min, }
numberInputMax const clamped = clamp(
); isInteger
const quantized = roundDownToMultiple(clamped, step); ? Math.floor(Number(e.target.value))
onChange(quantized); : Number(localInputValue),
setLocalInputValue(quantized); min,
}; numberInputMax
);
const quantized = roundDownToMultiple(clamped, step);
onChange(quantized);
setLocalInputValue(quantized);
},
[isInteger, localInputValue, min, numberInputMax, onChange, step]
);
const handleInputChange = (v: number | string) => { const handleInputChange = useCallback((v: number | string) => {
setLocalInputValue(v); setLocalInputValue(v);
}; }, []);
const handleResetDisable = () => { const handleResetDisable = useCallback(() => {
if (!handleReset) return; if (!handleReset) {
return;
}
handleReset(); handleReset();
}; }, [handleReset]);
const forceInputBlur = useCallback((e: MouseEvent) => {
if (e.target instanceof HTMLDivElement) {
e.target.focus();
}
}, []);
return ( return (
<FormControl <FormControl
onClick={forceInputBlur}
sx={ sx={
isCompact isCompact
? { ? {
@ -218,6 +245,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
value={localInputValue} value={localInputValue}
onChange={handleInputChange} onChange={handleInputChange}
onBlur={handleInputBlur} onBlur={handleInputBlur}
focusInputOnChange={false}
{...sliderNumberInputProps} {...sliderNumberInputProps}
> >
<NumberInputField <NumberInputField
@ -240,7 +268,7 @@ const IAISlider = (props: IAIFullSliderProps) => {
<IAIIconButton <IAIIconButton
size="sm" size="sm"
aria-label={t('accessibility.reset')} aria-label={t('accessibility.reset')}
tooltip="Reset" tooltip={t('accessibility.reset')}
icon={<BiReset />} icon={<BiReset />}
isDisabled={isDisabled} isDisabled={isDisabled}
onClick={handleResetDisable} onClick={handleResetDisable}

View File

@ -1,160 +0,0 @@
// import WorkInProgress from './WorkInProgress';
// import ReactFlow, {
// applyEdgeChanges,
// applyNodeChanges,
// Background,
// Controls,
// Edge,
// Handle,
// Node,
// NodeTypes,
// OnEdgesChange,
// OnNodesChange,
// Position,
// } from 'reactflow';
// import 'reactflow/dist/style.css';
// import {
// Fragment,
// FunctionComponent,
// ReactNode,
// useCallback,
// useMemo,
// useState,
// } from 'react';
// import { OpenAPIV3 } from 'openapi-types';
// import { filter, map, reduce } from 'lodash';
// import {
// Box,
// Flex,
// FormControl,
// FormLabel,
// Input,
// Select,
// Switch,
// Text,
// NumberInput,
// NumberInputField,
// NumberInputStepper,
// NumberIncrementStepper,
// NumberDecrementStepper,
// Tooltip,
// chakra,
// Badge,
// Heading,
// VStack,
// HStack,
// Menu,
// MenuButton,
// MenuList,
// MenuItem,
// MenuItemOption,
// MenuGroup,
// MenuOptionGroup,
// MenuDivider,
// IconButton,
// } from '@chakra-ui/react';
// import { FaPlus } from 'react-icons/fa';
// import {
// FIELD_NAMES as FIELD_NAMES,
// FIELDS,
// INVOCATION_NAMES as INVOCATION_NAMES,
// INVOCATIONS,
// } from 'features/nodeEditor/constants';
// console.log('invocations', INVOCATIONS);
// const nodeTypes = reduce(
// INVOCATIONS,
// (acc, val, key) => {
// acc[key] = val.component;
// return acc;
// },
// {} as NodeTypes
// );
// console.log('nodeTypes', nodeTypes);
// // make initial nodes one of every node for now
// let n = 0;
// const initialNodes = map(INVOCATIONS, (i) => ({
// id: i.type,
// type: i.title,
// position: { x: (n += 20), y: (n += 20) },
// data: {},
// }));
// console.log('initialNodes', initialNodes);
// export default function NodesWIP() {
// const [nodes, setNodes] = useState<Node[]>([]);
// const [edges, setEdges] = useState<Edge[]>([]);
// const onNodesChange: OnNodesChange = useCallback(
// (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
// []
// );
// const onEdgesChange: OnEdgesChange = useCallback(
// (changes) => setEdges((eds: Edge[]) => applyEdgeChanges(changes, eds)),
// []
// );
// return (
// <Box
// sx={{
// position: 'relative',
// width: 'full',
// height: 'full',
// borderRadius: 'md',
// }}
// >
// <ReactFlow
// nodeTypes={nodeTypes}
// nodes={nodes}
// edges={edges}
// onNodesChange={onNodesChange}
// onEdgesChange={onEdgesChange}
// >
// <Background />
// <Controls />
// </ReactFlow>
// <HStack sx={{ position: 'absolute', top: 2, right: 2 }}>
// {FIELD_NAMES.map((field) => (
// <Badge
// key={field}
// colorScheme={FIELDS[field].color}
// sx={{ userSelect: 'none' }}
// >
// {field}
// </Badge>
// ))}
// </HStack>
// <Menu>
// <MenuButton
// as={IconButton}
// aria-label="Options"
// icon={<FaPlus />}
// sx={{ position: 'absolute', top: 2, left: 2 }}
// />
// <MenuList>
// {INVOCATION_NAMES.map((name) => {
// const invocation = INVOCATIONS[name];
// return (
// <Tooltip
// key={name}
// label={invocation.description}
// placement="end"
// hasArrow
// >
// <MenuItem>{invocation.title}</MenuItem>
// </Tooltip>
// );
// })}
// </MenuList>
// </Menu>
// </Box>
// );
// }
export default {};

View File

@ -18,6 +18,8 @@ const globalHotkeysSelector = createSelector(
} }
); );
// TODO: Does not catch keypresses while focused in an input. Maybe there is a way?
export const useGlobalHotkeys = () => { export const useGlobalHotkeys = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { shift } = useAppSelector(globalHotkeysSelector); const { shift } = useAppSelector(globalHotkeysSelector);

View File

@ -12,7 +12,7 @@ export const getUrlAlt = (url: string, shouldTransformUrls: boolean) => {
export const useGetUrl = () => { export const useGetUrl = () => {
const shouldTransformUrls = useAppSelector( const shouldTransformUrls = useAppSelector(
(state: RootState) => state.system.shouldTransformUrls (state: RootState) => state.config.shouldTransformUrls
); );
return { return {

View File

@ -1,10 +1,9 @@
import React, { lazy, PropsWithChildren, useEffect, useState } from 'react'; import React, { lazy, PropsWithChildren, useEffect } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react'; import { PersistGate } from 'redux-persist/integration/react';
import { buildMiddleware, store } from './app/store'; import { buildMiddleware, store } from './app/store';
import { persistor } from './persistor'; import { persistor } from './persistor';
import { OpenAPI } from 'services/api'; import { OpenAPI } from 'services/api';
import { InvokeTabName } from 'features/ui/store/tabMap';
import '@fontsource/inter/100.css'; import '@fontsource/inter/100.css';
import '@fontsource/inter/200.css'; import '@fontsource/inter/200.css';
import '@fontsource/inter/300.css'; import '@fontsource/inter/300.css';
@ -16,41 +15,21 @@ import '@fontsource/inter/800.css';
import '@fontsource/inter/900.css'; import '@fontsource/inter/900.css';
import Loading from './common/components/Loading/Loading'; import Loading from './common/components/Loading/Loading';
// Localization
import './i18n';
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares'; import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
import { ApplicationFeature } from 'app/invokeai'; import { AppConfig } from 'app/invokeai';
import './i18n';
const App = lazy(() => import('./app/App')); const App = lazy(() => import('./app/App'));
const ThemeLocaleProvider = lazy(() => import('./app/ThemeLocaleProvider')); const ThemeLocaleProvider = lazy(() => import('./app/ThemeLocaleProvider'));
interface Props extends PropsWithChildren { interface Props extends PropsWithChildren {
apiUrl?: string; apiUrl?: string;
disabledPanels?: string[];
disabledTabs?: InvokeTabName[];
disabledFeatures?: ApplicationFeature[];
token?: string; token?: string;
shouldTransformUrls?: boolean; config?: Partial<AppConfig>;
shouldFetchImages?: boolean;
} }
export default function Component({ export default function Component({ apiUrl, token, config, children }: Props) {
apiUrl,
disabledTabs = [],
disabledFeatures = [
'lightbox',
'bugLink',
'discordLink',
'githubLink',
'localization',
'modelManager',
],
token,
children,
shouldTransformUrls,
shouldFetchImages = false,
}: Props) {
useEffect(() => { useEffect(() => {
// configure API client token // configure API client token
if (token) { if (token) {
@ -80,16 +59,7 @@ export default function Component({
<PersistGate loading={<Loading />} persistor={persistor}> <PersistGate loading={<Loading />} persistor={persistor}>
<React.Suspense fallback={<Loading />}> <React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider> <ThemeLocaleProvider>
<App <App config={config}>{children}</App>
options={{
disabledTabs,
disabledFeatures,
shouldTransformUrls,
shouldFetchImages,
}}
>
{children}
</App>
</ThemeLocaleProvider> </ThemeLocaleProvider>
</React.Suspense> </React.Suspense>
</PersistGate> </PersistGate>

View File

@ -65,6 +65,7 @@ import { useCallback } from 'react';
import useSetBothPrompts from 'features/parameters/hooks/usePrompt'; import useSetBothPrompts from 'features/parameters/hooks/usePrompt';
import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale'; import { requestCanvasRescale } from 'features/canvas/store/thunks/requestCanvasScale';
import { useGetUrl } from 'common/util/getUrl'; import { useGetUrl } from 'common/util/getUrl';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
const currentImageButtonsSelector = createSelector( const currentImageButtonsSelector = createSelector(
[ [
@ -88,8 +89,6 @@ const currentImageButtonsSelector = createSelector(
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } = const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
system; system;
const { disabledFeatures } = system;
const { upscalingLevel, facetoolStrength } = postprocessing; const { upscalingLevel, facetoolStrength } = postprocessing;
const { isLightboxOpen } = lightbox; const { isLightboxOpen } = lightbox;
@ -99,7 +98,6 @@ const currentImageButtonsSelector = createSelector(
const { intermediateImage, currentImage } = gallery; const { intermediateImage, currentImage } = gallery;
return { return {
disabledFeatures,
isProcessing, isProcessing,
isConnected, isConnected,
isGFPGANAvailable, isGFPGANAvailable,
@ -144,9 +142,12 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
activeTabName, activeTabName,
shouldHidePreview, shouldHidePreview,
selectedImage, selectedImage,
disabledFeatures,
} = useAppSelector(currentImageButtonsSelector); } = useAppSelector(currentImageButtonsSelector);
const isLightboxEnabled = useFeatureStatus('lightbox').isFeatureEnabled;
const isUpscalingEnabled = useFeatureStatus('upscaling').isFeatureEnabled;
const isFaceRestoreEnabled = useFeatureStatus('faceRestore').isFeatureEnabled;
const { getUrl, shouldTransformUrls } = useGetUrl(); const { getUrl, shouldTransformUrls } = useGetUrl();
const toast = useToast(); const toast = useToast();
@ -345,7 +346,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
{ {
enabled: () => enabled: () =>
Boolean( Boolean(
!disabledFeatures.includes('upscaling') && isUpscalingEnabled &&
isESRGANAvailable && isESRGANAvailable &&
!shouldDisableToolbarButtons && !shouldDisableToolbarButtons &&
isConnected && isConnected &&
@ -354,7 +355,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
), ),
}, },
[ [
disabledFeatures, isUpscalingEnabled,
selectedImage, selectedImage,
isESRGANAvailable, isESRGANAvailable,
shouldDisableToolbarButtons, shouldDisableToolbarButtons,
@ -376,7 +377,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
{ {
enabled: () => enabled: () =>
Boolean( Boolean(
!disabledFeatures.includes('faceRestore') && isFaceRestoreEnabled &&
isGFPGANAvailable && isGFPGANAvailable &&
!shouldDisableToolbarButtons && !shouldDisableToolbarButtons &&
isConnected && isConnected &&
@ -386,7 +387,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
}, },
[ [
disabledFeatures, isFaceRestoreEnabled,
selectedImage, selectedImage,
isGFPGANAvailable, isGFPGANAvailable,
shouldDisableToolbarButtons, shouldDisableToolbarButtons,
@ -517,7 +518,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
isChecked={shouldHidePreview} isChecked={shouldHidePreview}
onClick={handlePreviewVisibility} onClick={handlePreviewVisibility}
/> />
{!disabledFeatures.includes('lightbox') && ( {isLightboxEnabled && (
<IAIIconButton <IAIIconButton
icon={<FaExpand />} icon={<FaExpand />}
tooltip={ tooltip={
@ -566,12 +567,9 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
/> />
</ButtonGroup> </ButtonGroup>
{!( {(isUpscalingEnabled || isFaceRestoreEnabled) && (
disabledFeatures.includes('faceRestore') &&
disabledFeatures.includes('upscaling')
) && (
<ButtonGroup isAttached={true}> <ButtonGroup isAttached={true}>
{!disabledFeatures.includes('faceRestore') && ( {isFaceRestoreEnabled && (
<IAIPopover <IAIPopover
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
@ -602,7 +600,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
</IAIPopover> </IAIPopover>
)} )}
{!disabledFeatures.includes('upscaling') && ( {isUpscalingEnabled && (
<IAIPopover <IAIPopover
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton

View File

@ -158,7 +158,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
}; };
const handleDragStart = (e: DragEvent<HTMLDivElement>) => { const handleDragStart = (e: DragEvent<HTMLDivElement>) => {
console.log('drag started');
e.dataTransfer.setData('invokeai/imageName', image.name); e.dataTransfer.setData('invokeai/imageName', image.name);
e.dataTransfer.setData('invokeai/imageType', image.type); e.dataTransfer.setData('invokeai/imageType', image.type);
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';

View File

@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors'; import { lightboxSelector } from 'features/lightbox/store/lightboxSelectors';
import { configSelector } from 'features/system/store/configSelectors';
import { systemSelector } from 'features/system/store/systemSelectors'; import { systemSelector } from 'features/system/store/systemSelectors';
import { import {
activeTabNameSelector, activeTabNameSelector,
@ -68,8 +69,14 @@ export const imageGallerySelector = createSelector(
); );
export const hoverableImageSelector = createSelector( export const hoverableImageSelector = createSelector(
[gallerySelector, systemSelector, lightboxSelector, activeTabNameSelector], [
(gallery, system, lightbox, activeTabName) => { gallerySelector,
systemSelector,
configSelector,
lightboxSelector,
activeTabNameSelector,
],
(gallery, system, config, lightbox, activeTabName) => {
return { return {
mayDeleteImage: system.isConnected && !system.isProcessing, mayDeleteImage: system.isConnected && !system.isProcessing,
galleryImageObjectFit: gallery.galleryImageObjectFit, galleryImageObjectFit: gallery.galleryImageObjectFit,
@ -77,7 +84,7 @@ export const hoverableImageSelector = createSelector(
shouldUseSingleGalleryColumn: gallery.shouldUseSingleGalleryColumn, shouldUseSingleGalleryColumn: gallery.shouldUseSingleGalleryColumn,
activeTabName, activeTabName,
isLightboxOpen: lightbox.isLightboxOpen, isLightboxOpen: lightbox.isLightboxOpen,
disabledFeatures: system.disabledFeatures, disabledFeatures: config.disabledFeatures,
}; };
}, },
{ {

View File

@ -1,8 +1,4 @@
import { import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
PayloadAction,
createEntityAdapter,
createSlice,
} from '@reduxjs/toolkit';
import { Image } from 'app/invokeai'; import { Image } from 'app/invokeai';
import { invocationComplete } from 'services/events/actions'; import { invocationComplete } from 'services/events/actions';
@ -19,37 +15,24 @@ import {
import { deserializeImageResponse } from 'services/util/deserializeImageResponse'; import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
import { imageReceived, thumbnailReceived } from 'services/thunks/image'; import { imageReceived, thumbnailReceived } from 'services/thunks/image';
// use `createEntityAdapter` to create a slice for results images
// https://redux-toolkit.js.org/api/createEntityAdapter#overview
// the "Entity" is InvokeAI.ResultImage, while the "entities" are instances of that type
export const resultsAdapter = createEntityAdapter<Image>({ export const resultsAdapter = createEntityAdapter<Image>({
// Provide a callback to get a stable, unique identifier for each entity. This defaults to
// `(item) => item.id`, but for our result images, the `name` is the unique identifier.
selectId: (image) => image.name, selectId: (image) => image.name,
// Order all images by their time (in descending order)
sortComparer: (a, b) => b.metadata.created - a.metadata.created, sortComparer: (a, b) => b.metadata.created - a.metadata.created,
}); });
// This type is intersected with the Entity type to create the shape of the state
type AdditionalResultsState = { type AdditionalResultsState = {
// these are a bit misleading; they refer to sessions, not results, but we don't have a route page: number;
// to list all images directly at this time... pages: number;
page: number; // current page we are on isLoading: boolean;
pages: number; // the total number of pages available nextPage: number;
isLoading: boolean; // whether we are loading more images or not, mostly a placeholder
nextPage: number; // the next page to request
shouldFetchImages: boolean; // whether we need to re-fetch images or not
}; };
export const initialResultsState = export const initialResultsState =
resultsAdapter.getInitialState<AdditionalResultsState>({ resultsAdapter.getInitialState<AdditionalResultsState>({
// provide the additional initial state
page: 0, page: 0,
pages: 0, pages: 0,
isLoading: false, isLoading: false,
nextPage: 0, nextPage: 0,
shouldFetchImages: false,
}); });
export type ResultsState = typeof initialResultsState; export type ResultsState = typeof initialResultsState;
@ -58,21 +41,9 @@ const resultsSlice = createSlice({
name: 'results', name: 'results',
initialState: initialResultsState, initialState: initialResultsState,
reducers: { reducers: {
// the adapter provides some helper reducers; see the docs for all of them
// can use them as helper functions within a reducer, or use the function itself as a reducer
// here we just use the function itself as the reducer. we'll call this on `invocation_complete`
// to add a single result
resultAdded: resultsAdapter.upsertOne, resultAdded: resultsAdapter.upsertOne,
setShouldFetchImages: (state, action: PayloadAction<boolean>) => {
state.shouldFetchImages = action.payload;
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
// here we can respond to a fulfilled call of the `getNextResultsPage` thunk
// because we pass in the fulfilled thunk action creator, everything is typed
/** /**
* Received Result Images Page - PENDING * Received Result Images Page - PENDING
*/ */
@ -90,7 +61,6 @@ const resultsSlice = createSlice({
deserializeImageResponse(image) deserializeImageResponse(image)
); );
// use the adapter reducer to append all the results to state
resultsAdapter.addMany(state, resultImages); resultsAdapter.addMany(state, resultImages);
state.page = page; state.page = page;
@ -103,14 +73,15 @@ const resultsSlice = createSlice({
* Invocation Complete * Invocation Complete
*/ */
builder.addCase(invocationComplete, (state, action) => { builder.addCase(invocationComplete, (state, action) => {
const { data } = action.payload; const { data, shouldFetchImages } = action.payload;
const { result, node, graph_execution_state_id } = data; const { result, node, graph_execution_state_id } = data;
if (isImageOutput(result)) { if (isImageOutput(result)) {
const name = result.image.image_name; const name = result.image.image_name;
const type = result.image.image_type; const type = result.image.image_type;
// if we need to refetch, set URLs to placeholder for now // if we need to refetch, set URLs to placeholder for now
const { url, thumbnail } = state.shouldFetchImages const { url, thumbnail } = shouldFetchImages
? { url: '', thumbnail: '' } ? { url: '', thumbnail: '' }
: buildImageUrls(type, name); : buildImageUrls(type, name);
@ -123,7 +94,7 @@ const resultsSlice = createSlice({
thumbnail, thumbnail,
metadata: { metadata: {
created: timestamp, created: timestamp,
width: result.width, // TODO: add tese dimensions width: result.width,
height: result.height, height: result.height,
invokeai: { invokeai: {
session_id: graph_execution_state_id, session_id: graph_execution_state_id,
@ -162,8 +133,6 @@ const resultsSlice = createSlice({
}, },
}); });
// Create a set of memoized selectors based on the location of this entity state
// to be used as selectors in a `useAppSelector()` call
export const { export const {
selectAll: selectResultsAll, selectAll: selectResultsAll,
selectById: selectResultsById, selectById: selectResultsById,
@ -172,6 +141,6 @@ export const {
selectTotal: selectResultsTotal, selectTotal: selectResultsTotal,
} = resultsAdapter.getSelectors<RootState>((state) => state.results); } = resultsAdapter.getSelectors<RootState>((state) => state.results);
export const { resultAdded, setShouldFetchImages } = resultsSlice.actions; export const { resultAdded } = resultsSlice.actions;
export default resultsSlice.reducer; export default resultsSlice.reducer;

View File

@ -1,4 +1,7 @@
import { InputFieldTemplate, InputFieldValue } from 'features/nodes/types'; import {
InputFieldTemplate,
InputFieldValue,
} from 'features/nodes/types/types';
export type FieldComponentProps< export type FieldComponentProps<
V extends InputFieldValue, V extends InputFieldValue,

View File

@ -3,7 +3,10 @@ import { NodesState } from './nodesSlice';
/** /**
* Nodes slice persist blacklist * Nodes slice persist blacklist
*/ */
const itemsToBlacklist: (keyof NodesState)[] = ['schema', 'invocations']; const itemsToBlacklist: (keyof NodesState)[] = [
'schema',
'invocationTemplates',
];
export const nodesBlacklist = itemsToBlacklist.map( export const nodesBlacklist = itemsToBlacklist.map(
(blacklistItem) => `nodes.${blacklistItem}` (blacklistItem) => `nodes.${blacklistItem}`

View File

@ -1,46 +1,73 @@
import { RootState } from 'app/store'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setImg2imgStrength } from 'features/parameters/store/generationSlice'; import { setImg2imgStrength } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
interface ImageToImageStrengthProps { const selector = createSelector(
label?: string; [generationSelector, hotkeysSelector, configSelector],
} (generation, hotkeys, config) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.img2imgStrength;
const { img2imgStrength, isImageToImageEnabled } = generation;
export default function ImageToImageStrength(props: ImageToImageStrengthProps) { const step = hotkeys.shift ? fineStep : coarseStep;
const { t } = useTranslation();
const { label = `${t('parameters.strength')}` } = props;
const img2imgStrength = useAppSelector(
(state: RootState) => state.generation.img2imgStrength
);
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
return {
img2imgStrength,
isImageToImageEnabled,
initial,
min,
sliderMax,
inputMax,
step,
};
}
);
const ImageToImageStrength = () => {
const {
img2imgStrength,
isImageToImageEnabled,
initial,
min,
sliderMax,
inputMax,
step,
} = useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeStrength = (v: number) => dispatch(setImg2imgStrength(v)); const handleChange = useCallback(
(v: number) => dispatch(setImg2imgStrength(v)),
[dispatch]
);
const handleImg2ImgStrengthReset = () => { const handleReset = useCallback(() => {
dispatch(setImg2imgStrength(0.75)); dispatch(setImg2imgStrength(initial));
}; }, [dispatch, initial]);
return ( return (
<IAISlider <IAISlider
label={label} label={`${t('parameters.strength')}`}
step={0.01} step={step}
min={0.01} min={min}
max={1} max={sliderMax}
onChange={handleChangeStrength} onChange={handleChange}
handleReset={handleReset}
value={img2imgStrength} value={img2imgStrength}
isInteger={false} isInteger={false}
withInput withInput
withSliderMarks withSliderMarks
inputWidth={22}
withReset withReset
handleReset={handleImg2ImgStrengthReset}
isDisabled={!isImageToImageEnabled} isDisabled={!isImageToImageEnabled}
sliderNumberInputProps={{ max: inputMax }}
/> />
); );
} };
export default memo(ImageToImageStrength);

View File

@ -1,37 +1,57 @@
import { Box, BoxProps } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setHeight } from 'features/parameters/store/generationSlice'; import { setHeight } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { configSelector } from 'features/system/store/configSelectors';
import { memo } from 'react'; import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const HeightSlider = (props: BoxProps) => { const selector = createSelector(
const height = useAppSelector((state: RootState) => state.generation.height); [generationSelector, hotkeysSelector, configSelector],
const shift = useAppSelector((state: RootState) => state.hotkeys.shift); (generation, hotkeys, config) => {
const activeTabName = useAppSelector(activeTabNameSelector); const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.height;
const { height } = generation;
const step = hotkeys.shift ? fineStep : coarseStep;
return { height, initial, min, sliderMax, inputMax, step };
}
);
const HeightSlider = () => {
const { height, initial, min, sliderMax, inputMax, step } =
useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const handleChange = useCallback(
(v: number) => {
dispatch(setHeight(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setHeight(initial));
}, [dispatch, initial]);
return ( return (
<Box {...props}> <IAISlider
<IAISlider label={t('parameters.height')}
isDisabled={activeTabName === 'unifiedCanvas'} value={height}
label={t('parameters.height')} min={min}
value={height} step={step}
min={64} max={sliderMax}
step={shift ? 8 : 64} onChange={handleChange}
max={2048} handleReset={handleReset}
onChange={(v) => dispatch(setHeight(v))} withInput
handleReset={() => dispatch(setHeight(512))} withReset
withInput withSliderMarks
withReset sliderNumberInputProps={{ max: inputMax }}
withSliderMarks />
sliderNumberInputProps={{ max: 15360 }}
/>
</Box>
); );
}; };

View File

@ -1,46 +1,85 @@
import { RootState } from 'app/store'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput'; import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setCfgScale } from 'features/parameters/store/generationSlice'; import { setCfgScale } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export default function MainCFGScale() { const selector = createSelector(
[generationSelector, configSelector, uiSelector, hotkeysSelector],
(generation, config, ui, hotkeys) => {
const { initial, min, sliderMax, inputMax } = config.sd.guidance;
const { cfgScale } = generation;
const { shouldUseSliders } = ui;
const { shift } = hotkeys;
return {
cfgScale,
initial,
min,
sliderMax,
inputMax,
shouldUseSliders,
shift,
};
}
);
const GuidanceScale = () => {
const {
cfgScale,
initial,
min,
sliderMax,
inputMax,
shouldUseSliders,
shift,
} = useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const cfgScale = useAppSelector(
(state: RootState) => state.generation.cfgScale
);
const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders
);
const { t } = useTranslation(); const { t } = useTranslation();
const handleChangeCfgScale = (v: number) => dispatch(setCfgScale(v)); const handleChange = useCallback(
(v: number) => dispatch(setCfgScale(v)),
[dispatch]
);
const handleReset = useCallback(
() => dispatch(setCfgScale(initial)),
[dispatch, initial]
);
return shouldUseSliders ? ( return shouldUseSliders ? (
<IAISlider <IAISlider
label={t('parameters.cfgScale')} label={t('parameters.cfgScale')}
step={0.5} step={shift ? 0.1 : 0.5}
min={1.01} min={min}
max={30} max={sliderMax}
onChange={handleChangeCfgScale} onChange={handleChange}
handleReset={() => dispatch(setCfgScale(7.5))} handleReset={handleReset}
value={cfgScale} value={cfgScale}
sliderNumberInputProps={{ max: 200 }} sliderNumberInputProps={{ max: inputMax }}
withInput withInput
withReset withReset
withSliderMarks withSliderMarks
isInteger={false}
/> />
) : ( ) : (
<IAINumberInput <IAINumberInput
label={t('parameters.cfgScale')} label={t('parameters.cfgScale')}
step={0.5} step={0.5}
min={1.01} min={min}
max={200} max={inputMax}
onChange={handleChangeCfgScale} onChange={handleChange}
value={cfgScale} value={cfgScale}
isInteger={false} isInteger={false}
numberInputFieldProps={{ textAlign: 'center' }} numberInputFieldProps={{ textAlign: 'center' }}
/> />
); );
} };
export default memo(GuidanceScale);

View File

@ -1,48 +1,86 @@
import type { RootState } from 'app/store'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput'; import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setIterations } from 'features/parameters/store/generationSlice'; import { setIterations } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export default function MainIterations() { const selector = createSelector(
const iterations = useAppSelector( [generationSelector, configSelector, uiSelector, hotkeysSelector],
(state: RootState) => state.generation.iterations (generation, config, ui, hotkeys) => {
); const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.iterations;
const { iterations } = generation;
const { shouldUseSliders } = ui;
const shouldUseSliders = useAppSelector( const step = hotkeys.shift ? fineStep : coarseStep;
(state: RootState) => state.ui.shouldUseSliders
);
return {
iterations,
initial,
min,
sliderMax,
inputMax,
step,
shouldUseSliders,
};
}
);
const MainIterations = () => {
const {
iterations,
initial,
min,
sliderMax,
inputMax,
step,
shouldUseSliders,
} = useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const handleChangeIterations = (v: number) => dispatch(setIterations(v)); const handleChange = useCallback(
(v: number) => {
dispatch(setIterations(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setIterations(initial));
}, [dispatch, initial]);
return shouldUseSliders ? ( return shouldUseSliders ? (
<IAISlider <IAISlider
label={t('parameters.images')} label={t('parameters.images')}
step={1} step={step}
min={1} min={min}
max={16} max={sliderMax}
onChange={handleChangeIterations} onChange={handleChange}
handleReset={() => dispatch(setIterations(1))} handleReset={handleReset}
value={iterations} value={iterations}
withInput withInput
withReset withReset
withSliderMarks withSliderMarks
sliderNumberInputProps={{ max: 9999 }} sliderNumberInputProps={{ max: inputMax }}
/> />
) : ( ) : (
<IAINumberInput <IAINumberInput
label={t('parameters.images')} label={t('parameters.images')}
step={1} step={step}
min={1} min={min}
max={9999} max={inputMax}
onChange={handleChangeIterations} onChange={handleChange}
value={iterations} value={iterations}
numberInputFieldProps={{ textAlign: 'center' }} numberInputFieldProps={{ textAlign: 'center' }}
/> />
); );
} };
export default memo(MainIterations);

View File

@ -1,35 +1,32 @@
import { Box, BoxProps } from '@chakra-ui/react'; import { DIFFUSERS_SAMPLERS } from 'app/constants';
import { DIFFUSERS_SAMPLERS, SAMPLERS } from 'app/constants';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect'; import IAISelect from 'common/components/IAISelect';
import { setSampler } from 'features/parameters/store/generationSlice'; import { setSampler } from 'features/parameters/store/generationSlice';
import { activeModelSelector } from 'features/system/store/systemSelectors'; import { ChangeEvent, memo, useCallback } from 'react';
import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export default function MainSampler(props: BoxProps) { const Scheduler = () => {
const sampler = useAppSelector( const sampler = useAppSelector(
(state: RootState) => state.generation.sampler (state: RootState) => state.generation.sampler
); );
const activeModel = useAppSelector(activeModelSelector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) => const handleChange = useCallback(
dispatch(setSampler(e.target.value)); (e: ChangeEvent<HTMLSelectElement>) => dispatch(setSampler(e.target.value)),
[dispatch]
);
return ( return (
<Box {...props}> <IAISelect
<IAISelect label={t('parameters.sampler')}
label={t('parameters.sampler')} value={sampler}
value={sampler} onChange={handleChange}
onChange={handleChangeSampler} validValues={DIFFUSERS_SAMPLERS}
validValues={ minWidth={36}
activeModel.format === 'diffusers' ? DIFFUSERS_SAMPLERS : SAMPLERS />
}
minWidth={36}
/>
</Box>
); );
} };
export default memo(Scheduler);

View File

@ -1,17 +1,16 @@
import { Divider, Flex, VStack } from '@chakra-ui/react'; import { Box, Flex, VStack } from '@chakra-ui/react';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
import { ModelSelect } from 'exports'; import { ModelSelect } from 'exports';
import { memo } from 'react';
import HeightSlider from './HeightSlider'; import HeightSlider from './HeightSlider';
import MainCFGScale from './MainCFGScale'; import MainCFGScale from './MainCFGScale';
import MainHeight from './MainHeight';
import MainIterations from './MainIterations'; import MainIterations from './MainIterations';
import MainSampler from './MainSampler'; import MainSampler from './MainSampler';
import MainSteps from './MainSteps'; import MainSteps from './MainSteps';
import MainWidth from './MainWidth';
import WidthSlider from './WidthSlider'; import WidthSlider from './WidthSlider';
export default function MainSettings() { const MainSettings = () => {
const shouldUseSliders = useAppSelector( const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders (state: RootState) => state.ui.shouldUseSliders
); );
@ -21,9 +20,16 @@ export default function MainSettings() {
<MainIterations /> <MainIterations />
<MainSteps /> <MainSteps />
<MainCFGScale /> <MainCFGScale />
<MainWidth /> <WidthSlider />
<MainHeight /> <HeightSlider />
<MainSampler /> <Flex gap={3} w="full">
<Box flexGrow={2}>
<MainSampler />
</Box>
<Box flexGrow={3}>
<ModelSelect />
</Box>
</Flex>
</VStack> </VStack>
) : ( ) : (
<Flex gap={3} flexDirection="column"> <Flex gap={3} flexDirection="column">
@ -32,12 +38,18 @@ export default function MainSettings() {
<MainSteps /> <MainSteps />
<MainCFGScale /> <MainCFGScale />
</Flex> </Flex>
<Flex gap={3}>
<MainSampler flexGrow={2} />
<ModelSelect flexGrow={3} />
</Flex>
<WidthSlider /> <WidthSlider />
<HeightSlider /> <HeightSlider />
<Flex gap={3} w="full">
<Box flexGrow={2}>
<MainSampler />
</Box>
<Box flexGrow={3}>
<ModelSelect />
</Box>
</Flex>
</Flex> </Flex>
); );
} };
export default memo(MainSettings);

View File

@ -1,53 +1,87 @@
import { RootState } from 'app/store'; import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput'; import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { import {
clampSymmetrySteps, clampSymmetrySteps,
setSteps, setSteps,
} from 'features/parameters/store/generationSlice'; } from 'features/parameters/store/generationSlice';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { uiSelector } from 'features/ui/store/uiSelectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export default function MainSteps() { const selector = createSelector(
[generationSelector, configSelector, uiSelector, hotkeysSelector],
(generation, config, ui, hotkeys) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.steps;
const { steps } = generation;
const { shouldUseSliders } = ui;
const step = hotkeys.shift ? fineStep : coarseStep;
return {
steps,
initial,
min,
sliderMax,
inputMax,
step,
shouldUseSliders,
};
}
);
const MainSteps = () => {
const { steps, initial, min, sliderMax, inputMax, step, shouldUseSliders } =
useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const steps = useAppSelector((state: RootState) => state.generation.steps);
const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders
);
const { t } = useTranslation(); const { t } = useTranslation();
const handleChangeSteps = (v: number) => { const handleChange = useCallback(
dispatch(setSteps(v)); (v: number) => {
}; dispatch(setSteps(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setSteps(initial));
}, [dispatch, initial]);
const handleBlur = () => { const handleBlur = useCallback(() => {
dispatch(clampSymmetrySteps()); dispatch(clampSymmetrySteps());
}; }, [dispatch]);
return shouldUseSliders ? ( return shouldUseSliders ? (
<IAISlider <IAISlider
label={t('parameters.steps')} label={t('parameters.steps')}
min={1} min={min}
step={1} max={sliderMax}
onChange={handleChangeSteps} step={step}
handleReset={() => dispatch(setSteps(20))} onChange={handleChange}
handleReset={handleReset}
value={steps} value={steps}
withInput withInput
withReset withReset
withSliderMarks withSliderMarks
sliderNumberInputProps={{ max: 9999 }} sliderNumberInputProps={{ max: inputMax }}
/> />
) : ( ) : (
<IAINumberInput <IAINumberInput
label={t('parameters.steps')} label={t('parameters.steps')}
min={1} min={min}
max={9999} max={inputMax}
step={1} step={step}
onChange={handleChangeSteps} onChange={handleChange}
value={steps} value={steps}
numberInputFieldProps={{ textAlign: 'center' }} numberInputFieldProps={{ textAlign: 'center' }}
onBlur={handleBlur} onBlur={handleBlur}
/> />
); );
} };
export default memo(MainSteps);

View File

@ -1,36 +1,57 @@
import { Box, BoxProps } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider'; import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setWidth } from 'features/parameters/store/generationSlice'; import { setWidth } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { configSelector } from 'features/system/store/configSelectors';
import { memo } from 'react'; import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
const WidthSlider = (props: BoxProps) => { const selector = createSelector(
const width = useAppSelector((state: RootState) => state.generation.width); [generationSelector, hotkeysSelector, configSelector],
const shift = useAppSelector((state: RootState) => state.hotkeys.shift); (generation, hotkeys, config) => {
const activeTabName = useAppSelector(activeTabNameSelector); const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
const { t } = useTranslation(); config.sd.width;
const { width } = generation;
const step = hotkeys.shift ? fineStep : coarseStep;
return { width, initial, min, sliderMax, inputMax, step };
}
);
const WidthSlider = () => {
const { width, initial, min, sliderMax, inputMax, step } =
useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChange = useCallback(
(v: number) => {
dispatch(setWidth(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setWidth(initial));
}, [dispatch, initial]);
return ( return (
<Box {...props}> <IAISlider
<IAISlider label={t('parameters.width')}
isDisabled={activeTabName === 'unifiedCanvas'} value={width}
label={t('parameters.width')} min={min}
value={width} step={step}
min={64} max={sliderMax}
step={shift ? 8 : 64} onChange={handleChange}
max={2048} handleReset={handleReset}
onChange={(v) => dispatch(setWidth(v))} withInput
handleReset={() => dispatch(setWidth(512))} withReset
withInput withSliderMarks
withReset sliderNumberInputProps={{ max: inputMax }}
withSliderMarks />
sliderNumberInputProps={{ max: 15360 }}
/>
</Box>
); );
}; };

View File

@ -2,44 +2,34 @@ import { Accordion } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { Feature } from 'app/features'; import { Feature } from 'app/features';
import { useAppDispatch, useAppSelector } from 'app/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import { systemSelector } from 'features/system/store/systemSelectors';
import { tabMap } from 'features/ui/store/tabMap'; import { tabMap } from 'features/ui/store/tabMap';
import { import { uiSelector } from 'features/ui/store/uiSelectors';
activeTabNameSelector,
uiSelector,
} from 'features/ui/store/uiSelectors';
import { openAccordionItemsChanged } from 'features/ui/store/uiSlice'; import { openAccordionItemsChanged } from 'features/ui/store/uiSlice';
import { filter, map } from 'lodash'; import { map } from 'lodash';
import { ReactNode, useCallback } from 'react'; import { ReactNode, useCallback } from 'react';
import InvokeAccordionItem from './AccordionItems/InvokeAccordionItem'; import InvokeAccordionItem from './AccordionItems/InvokeAccordionItem';
const parametersAccordionSelector = createSelector( const parametersAccordionSelector = createSelector([uiSelector], (uiSlice) => {
[uiSelector, systemSelector], const {
(uiSlice, system) => { activeTab,
const { openLinearAccordionItems,
activeTab, openUnifiedCanvasAccordionItems,
openLinearAccordionItems, } = uiSlice;
openUnifiedCanvasAccordionItems,
} = uiSlice;
const { disabledFeatures } = system; let openAccordions: number[] = [];
let openAccordions: number[] = []; if (tabMap[activeTab] === 'generate') {
openAccordions = openLinearAccordionItems;
if (tabMap[activeTab] === 'generate') {
openAccordions = openLinearAccordionItems;
}
if (tabMap[activeTab] === 'unifiedCanvas') {
openAccordions = openUnifiedCanvasAccordionItems;
}
return {
openAccordions,
disabledFeatures,
};
} }
);
if (tabMap[activeTab] === 'unifiedCanvas') {
openAccordions = openUnifiedCanvasAccordionItems;
}
return {
openAccordions,
};
});
export type ParametersAccordionItem = { export type ParametersAccordionItem = {
name: string; name: string;
@ -61,9 +51,7 @@ type ParametersAccordionProps = {
* Main container for generation and processing parameters. * Main container for generation and processing parameters.
*/ */
const ParametersAccordion = ({ accordionItems }: ParametersAccordionProps) => { const ParametersAccordion = ({ accordionItems }: ParametersAccordionProps) => {
const { openAccordions, disabledFeatures } = useAppSelector( const { openAccordions } = useAppSelector(parametersAccordionSelector);
parametersAccordionSelector
);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();

View File

@ -1,6 +1,5 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { ChangeEvent } from 'react'; import { ChangeEvent, memo } from 'react';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -30,7 +29,7 @@ const selector = createSelector(
} }
); );
const ModelSelect = (props: BoxProps) => { const ModelSelect = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const { allModelNames, selectedModel } = useAppSelector(selector); const { allModelNames, selectedModel } = useAppSelector(selector);
@ -39,18 +38,16 @@ const ModelSelect = (props: BoxProps) => {
}; };
return ( return (
<Box {...props}> <IAISelect
<IAISelect label={t('modelManager.model')}
label={t('modelManager.model')} style={{ fontSize: 'sm' }}
style={{ fontSize: 'sm' }} aria-label={t('accessibility.modelSelect')}
aria-label={t('accessibility.modelSelect')} tooltip={selectedModel?.description || ''}
tooltip={selectedModel?.description || ''} value={selectedModel?.name || undefined}
value={selectedModel?.name || undefined} validValues={allModelNames}
validValues={allModelNames} onChange={handleChangeModel}
onChange={handleChangeModel} />
/>
</Box>
); );
}; };
export default ModelSelect; export default memo(ModelSelect);

View File

@ -8,22 +8,26 @@ import ModelManagerModal from './ModelManager/ModelManagerModal';
import SettingsModal from './SettingsModal/SettingsModal'; import SettingsModal from './SettingsModal/SettingsModal';
import ThemeChanger from './ThemeChanger'; import ThemeChanger from './ThemeChanger';
import IAIIconButton from 'common/components/IAIIconButton'; import IAIIconButton from 'common/components/IAIIconButton';
import { useAppSelector } from 'app/storeHooks'; import { useFeatureStatus } from '../hooks/useFeatureStatus';
import { RootState } from 'app/store';
const SiteHeaderMenu = () => { const SiteHeaderMenu = () => {
const disabledFeatures = useAppSelector(
(state: RootState) => state.system.disabledFeatures
);
const { t } = useTranslation(); const { t } = useTranslation();
const isModelManagerEnabled =
useFeatureStatus('modelManager').isFeatureEnabled;
const isLocalizationEnabled =
useFeatureStatus('localization').isFeatureEnabled;
const isBugLinkEnabled = useFeatureStatus('bugLink').isFeatureEnabled;
const isDiscordLinkEnabled = useFeatureStatus('discordLink').isFeatureEnabled;
const isGithubLinkEnabled = useFeatureStatus('githubLink').isFeatureEnabled;
return ( return (
<Flex <Flex
alignItems="center" alignItems="center"
flexDirection={{ base: 'column', xl: 'row' }} flexDirection={{ base: 'column', xl: 'row' }}
gap={{ base: 4, xl: 1 }} gap={{ base: 4, xl: 1 }}
> >
{!disabledFeatures.includes('modelManager') && ( {isModelManagerEnabled && (
<ModelManagerModal> <ModelManagerModal>
<IAIIconButton <IAIIconButton
aria-label={t('modelManager.modelManager')} aria-label={t('modelManager.modelManager')}
@ -51,9 +55,9 @@ const SiteHeaderMenu = () => {
<ThemeChanger /> <ThemeChanger />
{!disabledFeatures.includes('localization') && <LanguagePicker />} {isLocalizationEnabled && <LanguagePicker />}
{!disabledFeatures.includes('bugLink') && ( {isBugLinkEnabled && (
<Link <Link
isExternal isExternal
href="http://github.com/invoke-ai/InvokeAI/issues" href="http://github.com/invoke-ai/InvokeAI/issues"
@ -71,7 +75,7 @@ const SiteHeaderMenu = () => {
</Link> </Link>
)} )}
{!disabledFeatures.includes('githubLink') && ( {isGithubLinkEnabled && (
<Link <Link
isExternal isExternal
href="http://github.com/invoke-ai/InvokeAI" href="http://github.com/invoke-ai/InvokeAI"
@ -89,7 +93,7 @@ const SiteHeaderMenu = () => {
</Link> </Link>
)} )}
{!disabledFeatures.includes('discordLink') && ( {isDiscordLinkEnabled && (
<Link <Link
isExternal isExternal
href="https://discord.gg/ZmtBAhwWhy" href="https://discord.gg/ZmtBAhwWhy"

View File

@ -0,0 +1,22 @@
import { AppFeature } from 'app/invokeai';
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import { useMemo } from 'react';
export const useFeatureStatus = (feature: AppFeature) => {
const disabledFeatures = useAppSelector(
(state: RootState) => state.config.disabledFeatures
);
const isFeatureDisabled = useMemo(
() => disabledFeatures.includes(feature),
[disabledFeatures, feature]
);
const isFeatureEnabled = useMemo(
() => !disabledFeatures.includes(feature),
[disabledFeatures, feature]
);
return { isFeatureDisabled, isFeatureEnabled };
};

View File

@ -2,20 +2,18 @@ import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store'; import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks'; import { useAppSelector } from 'app/storeHooks';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { configSelector } from '../store/configSelectors';
import { systemSelector } from '../store/systemSelectors';
const isApplicationReadySelector = createSelector( const isApplicationReadySelector = createSelector(
[(state: RootState) => state.system], [systemSelector, configSelector],
(system) => { (system, config) => {
const { const { wereModelsReceived, wasSchemaParsed } = system;
disabledFeatures,
disabledTabs, const { disabledTabs } = config;
wereModelsReceived,
wasSchemaParsed,
} = system;
return { return {
disabledTabs, disabledTabs,
disabledFeatures,
wereModelsReceived, wereModelsReceived,
wasSchemaParsed, wasSchemaParsed,
}; };
@ -23,12 +21,9 @@ const isApplicationReadySelector = createSelector(
); );
export const useIsApplicationReady = () => { export const useIsApplicationReady = () => {
const { const { disabledTabs, wereModelsReceived, wasSchemaParsed } = useAppSelector(
disabledTabs, isApplicationReadySelector
disabledFeatures, );
wereModelsReceived,
wasSchemaParsed,
} = useAppSelector(isApplicationReadySelector);
const isApplicationReady = useMemo(() => { const isApplicationReady = useMemo(() => {
if (!wereModelsReceived) { if (!wereModelsReceived) {

View File

@ -0,0 +1,17 @@
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { useCallback } from 'react';
export const useIsTabDisabled = () => {
const disabledTabs = useAppSelector(
(state: RootState) => state.config.disabledTabs
);
const isTabDisabled = useCallback(
(tab: InvokeTabName) => disabledTabs.includes(tab),
[disabledTabs]
);
return isTabDisabled;
};

View File

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

View File

@ -0,0 +1,75 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { AppConfig } from 'app/invokeai';
import { cloneDeep, defaultsDeep } from 'lodash';
const initialConfigState: AppConfig = {
shouldTransformUrls: false,
shouldFetchImages: false,
disabledTabs: [],
disabledFeatures: [],
sd: {
iterations: {
initial: 1,
min: 1,
sliderMax: 20,
inputMax: 9999,
fineStep: 1,
coarseStep: 1,
},
width: {
initial: 512,
min: 64,
sliderMax: 1536,
inputMax: 4096,
fineStep: 8,
coarseStep: 64,
},
height: {
initial: 512,
min: 64,
sliderMax: 1536,
inputMax: 4096,
fineStep: 8,
coarseStep: 64,
},
steps: {
initial: 30,
min: 1,
sliderMax: 100,
inputMax: 500,
fineStep: 1,
coarseStep: 1,
},
guidance: {
initial: 7,
min: 1,
sliderMax: 20,
inputMax: 200,
fineStep: 0.1,
coarseStep: 0.5,
},
img2imgStrength: {
initial: 0.7,
min: 0,
sliderMax: 1,
inputMax: 1,
fineStep: 0.01,
coarseStep: 0.05,
},
},
};
export const configSlice = createSlice({
name: 'config',
initialState: initialConfigState,
reducers: {
configChanged: (state, action: PayloadAction<Partial<AppConfig>>) => {
defaultsDeep(state, cloneDeep(action.payload));
},
},
});
export const { configChanged } = configSlice.actions;
export default configSlice.reducer;

View File

@ -90,18 +90,18 @@ export interface SystemState
* Array of node IDs that we want to handle when events received * Array of node IDs that we want to handle when events received
*/ */
subscribedNodeIds: string[]; subscribedNodeIds: string[];
/** // /**
* Whether or not URLs should be transformed to use a different host // * Whether or not URLs should be transformed to use a different host
*/ // */
shouldTransformUrls: boolean; // shouldTransformUrls: boolean;
/** // /**
* Array of disabled tabs // * Array of disabled tabs
*/ // */
disabledTabs: InvokeTabName[]; // disabledTabs: InvokeTabName[];
/** // /**
* Array of disabled features // * Array of disabled features
*/ // */
disabledFeatures: InvokeAI.ApplicationFeature[]; // disabledFeatures: InvokeAI.AppFeature[];
/** /**
* Whether or not the available models were received * Whether or not the available models were received
*/ */
@ -157,9 +157,9 @@ const initialSystemState: SystemState = {
cancelType: 'immediate', cancelType: 'immediate',
isCancelScheduled: false, isCancelScheduled: false,
subscribedNodeIds: [], subscribedNodeIds: [],
shouldTransformUrls: false, // shouldTransformUrls: false,
disabledTabs: [], // disabledTabs: [],
disabledFeatures: [], // disabledFeatures: [],
wereModelsReceived: false, wereModelsReceived: false,
wasSchemaParsed: false, wasSchemaParsed: false,
}; };
@ -359,27 +359,27 @@ export const systemSlice = createSlice({
subscribedNodeIdsSet: (state, action: PayloadAction<string[]>) => { subscribedNodeIdsSet: (state, action: PayloadAction<string[]>) => {
state.subscribedNodeIds = action.payload; state.subscribedNodeIds = action.payload;
}, },
/** // /**
* `shouldTransformUrls` was changed // * `shouldTransformUrls` was changed
*/ // */
shouldTransformUrlsChanged: (state, action: PayloadAction<boolean>) => { // shouldTransformUrlsChanged: (state, action: PayloadAction<boolean>) => {
state.shouldTransformUrls = action.payload; // state.shouldTransformUrls = action.payload;
}, // },
/** // /**
* `disabledTabs` was changed // * `disabledTabs` was changed
*/ // */
disabledTabsChanged: (state, action: PayloadAction<InvokeTabName[]>) => { // disabledTabsChanged: (state, action: PayloadAction<InvokeTabName[]>) => {
state.disabledTabs = action.payload; // state.disabledTabs = action.payload;
}, // },
/** // /**
* `disabledFeatures` was changed // * `disabledFeatures` was changed
*/ // */
disabledFeaturesChanged: ( // disabledFeaturesChanged: (
state, // state,
action: PayloadAction<InvokeAI.ApplicationFeature[]> // action: PayloadAction<InvokeAI.AppFeature[]>
) => { // ) => {
state.disabledFeatures = action.payload; // state.disabledFeatures = action.payload;
}, // },
}, },
extraReducers(builder) { extraReducers(builder) {
/** /**
@ -601,9 +601,9 @@ export const {
scheduledCancelAborted, scheduledCancelAborted,
cancelTypeChanged, cancelTypeChanged,
subscribedNodeIdsSet, subscribedNodeIdsSet,
shouldTransformUrlsChanged, // shouldTransformUrlsChanged,
disabledTabsChanged, // disabledTabsChanged,
disabledFeaturesChanged, // disabledFeaturesChanged,
} = systemSlice.actions; } = systemSlice.actions;
export default systemSlice.reducer; export default systemSlice.reducer;

View File

@ -27,6 +27,7 @@ import GenerateWorkspace from './tabs/Generate/GenerateWorkspace';
import { FaImage } from 'react-icons/fa'; import { FaImage } from 'react-icons/fa';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { BsLightningChargeFill, BsLightningFill } from 'react-icons/bs'; import { BsLightningChargeFill, BsLightningFill } from 'react-icons/bs';
import { configSelector } from 'features/system/store/configSelectors';
export interface InvokeTabInfo { export interface InvokeTabInfo {
id: InvokeTabName; id: InvokeTabName;
@ -56,14 +57,11 @@ const tabs: InvokeTabInfo[] = [
}, },
]; ];
const enabledTabsSelector = createSelector( const enabledTabsSelector = createSelector(configSelector, (config) => {
(state: RootState) => state.ui, const { disabledTabs } = config;
(ui) => {
const { disabledTabs } = ui;
return tabs.filter((tab) => !disabledTabs.includes(tab.id)); return tabs.filter((tab) => !disabledTabs.includes(tab.id));
} });
);
export default function InvokeTabs() { export default function InvokeTabs() {
const activeTab = useAppSelector(activeTabIndexSelector); const activeTab = useAppSelector(activeTabIndexSelector);
@ -76,10 +74,6 @@ export default function InvokeTabs() {
(state: RootState) => state.ui (state: RootState) => state.ui
); );
const disabledTabs = useAppSelector(
(state: RootState) => state.system.disabledTabs
);
const { t } = useTranslation(); const { t } = useTranslation();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();

View File

@ -32,7 +32,7 @@ export default function UnifiedCanvasParameters() {
name: 'unifiedCanvasImg2Img', name: 'unifiedCanvasImg2Img',
header: `${t('parameters.imageToImage')}`, header: `${t('parameters.imageToImage')}`,
feature: undefined, feature: undefined,
content: <ImageToImageStrength label={t('parameters.img2imgStrength')} />, content: <ImageToImageStrength />,
}, },
seed: { seed: {
name: 'seed', name: 'seed',

View File

@ -1,5 +1,6 @@
import type { PayloadAction } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
type HotkeysState = { type HotkeysState = {
shift: boolean; shift: boolean;
@ -24,3 +25,5 @@ export const hotkeysSlice = createSlice({
export const { shiftKeyPressed } = hotkeysSlice.actions; export const { shiftKeyPressed } = hotkeysSlice.actions;
export default hotkeysSlice.reducer; export default hotkeysSlice.reducer;
export const hotkeysSelector = (state: RootState) => state.hotkeys;

View File

@ -19,8 +19,6 @@ const initialUIState: UIState = {
shouldShowGallery: true, shouldShowGallery: true,
shouldHidePreview: false, shouldHidePreview: false,
openLinearAccordionItems: [], openLinearAccordionItems: [],
disabledParameterPanels: [],
disabledTabs: [],
openGenerateAccordionItems: [], openGenerateAccordionItems: [],
openUnifiedCanvasAccordionItems: [], openUnifiedCanvasAccordionItems: [],
}; };

View File

@ -1,3 +1,5 @@
import { InvokeTabName } from './tabMap';
export type AddNewModelType = 'ckpt' | 'diffusers' | null; export type AddNewModelType = 'ckpt' | 'diffusers' | null;
export interface UIState { export interface UIState {
@ -15,8 +17,6 @@ export interface UIState {
shouldPinGallery: boolean; shouldPinGallery: boolean;
shouldShowGallery: boolean; shouldShowGallery: boolean;
openLinearAccordionItems: number[]; openLinearAccordionItems: number[];
disabledParameterPanels: string[];
disabledTabs: InvokeTabName[];
openGenerateAccordionItems: number[]; openGenerateAccordionItems: number[];
openUnifiedCanvasAccordionItems: number[]; openUnifiedCanvasAccordionItems: number[];
} }

View File

@ -1,7 +1,13 @@
import { AppConfig } from 'app/invokeai';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import Component from './component'; import Component from './component';
const testConfig: Partial<AppConfig> = {
disabledTabs: ['nodes'],
disabledFeatures: ['upscaling'],
};
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<Component /> <Component config={testConfig} />
); );

View File

@ -35,7 +35,10 @@ export const invocationStarted = createAction<
>('socket/invocationStarted'); >('socket/invocationStarted');
export const invocationComplete = createAction< export const invocationComplete = createAction<
BaseSocketPayload & { data: InvocationCompleteEvent } BaseSocketPayload & {
data: InvocationCompleteEvent;
shouldFetchImages: boolean;
}
>('socket/invocationComplete'); >('socket/invocationComplete');
export const invocationError = createAction< export const invocationError = createAction<

View File

@ -92,9 +92,9 @@ export const socketMiddleware = () => {
socket.on('connect', () => { socket.on('connect', () => {
dispatch(socketConnected({ timestamp: getTimestamp() })); dispatch(socketConnected({ timestamp: getTimestamp() }));
const { results, uploads, models, nodes, system } = getState(); const { results, uploads, models, nodes, config } = getState();
const { disabledTabs } = system; const { disabledTabs } = config;
// These thunks need to be dispatch in middleware; cannot handle in a reducer // These thunks need to be dispatch in middleware; cannot handle in a reducer
if (!results.ids.length) { if (!results.ids.length) {
@ -203,13 +203,20 @@ export const socketMiddleware = () => {
const sessionId = data.graph_execution_state_id; const sessionId = data.graph_execution_state_id;
const { cancelType, isCancelScheduled } = getState().system; const { cancelType, isCancelScheduled } = getState().system;
const { shouldFetchImages } = getState().config;
// Handle scheduled cancelation // Handle scheduled cancelation
if (cancelType === 'scheduled' && isCancelScheduled) { if (cancelType === 'scheduled' && isCancelScheduled) {
dispatch(sessionCanceled({ sessionId })); dispatch(sessionCanceled({ sessionId }));
} }
dispatch(invocationComplete({ data, timestamp: getTimestamp() })); dispatch(
invocationComplete({
data,
timestamp: getTimestamp(),
shouldFetchImages,
})
);
} }
}); });
@ -218,9 +225,9 @@ export const socketMiddleware = () => {
} }
if (invocationComplete.match(action)) { if (invocationComplete.match(action)) {
const { results } = getState(); const { config } = getState();
if (results.shouldFetchImages) { if (config.shouldFetchImages) {
const { result } = action.payload.data; const { result } = action.payload.data;
if (isImageOutput(result)) { if (isImageOutput(result)) {
const imageName = result.image.image_name; const imageName = result.image.image_name;