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

View File

@ -12,24 +12,12 @@
* 'gfpgan'.
*/
import { FacetoolType } from 'features/parameters/store/postprocessingSlice';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { IRect } from 'konva/lib/types';
import { ImageMetadata, ImageType } from 'services/api';
import { AnyInvocation } from 'services/events/types';
/**
* A disable-able application feature
*/
export declare type ApplicationFeature =
| 'faceRestore'
| 'upscaling'
| 'lightbox'
| 'modelManager'
| 'githubLink'
| 'discordLink'
| 'bugLink'
| 'localization';
/**
* TODO:
* Once an image has been generated, if it is postprocessed again,
@ -347,3 +335,93 @@ export declare type UploadOutpaintingMergeImagePayload = {
dataURL: 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 postprocessingReducer from 'features/parameters/store/postprocessingSlice';
import systemReducer from 'features/system/store/systemSlice';
import configReducer from 'features/system/store/configSlice';
import uiReducer from 'features/ui/store/uiSlice';
import hotkeysReducer from 'features/ui/store/hotkeysSlice';
import modelsReducer from 'features/system/store/modelSlice';
@ -54,6 +55,7 @@ const rootReducer = combineReducers({
postprocessing: postprocessingReducer,
results: resultsReducer,
system: systemReducer,
config: configReducer,
ui: uiReducer,
uploads: uploadsReducer,
hotkeys: hotkeysReducer,
@ -78,6 +80,7 @@ const rootPersistConfig = getPersistConfig({
// ...uploadsBlacklist,
'uploads',
'hotkeys',
'config',
],
debounce: 300,
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,4 @@
import {
PayloadAction,
createEntityAdapter,
createSlice,
} from '@reduxjs/toolkit';
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
import { Image } from 'app/invokeai';
import { invocationComplete } from 'services/events/actions';
@ -19,37 +15,24 @@ import {
import { deserializeImageResponse } from 'services/util/deserializeImageResponse';
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>({
// 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,
// Order all images by their time (in descending order)
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 = {
// these are a bit misleading; they refer to sessions, not results, but we don't have a route
// to list all images directly at this time...
page: number; // current page we are on
pages: number; // the total number of pages available
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
page: number;
pages: number;
isLoading: boolean;
nextPage: number;
};
export const initialResultsState =
resultsAdapter.getInitialState<AdditionalResultsState>({
// provide the additional initial state
page: 0,
pages: 0,
isLoading: false,
nextPage: 0,
shouldFetchImages: false,
});
export type ResultsState = typeof initialResultsState;
@ -58,21 +41,9 @@ const resultsSlice = createSlice({
name: 'results',
initialState: initialResultsState,
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,
setShouldFetchImages: (state, action: PayloadAction<boolean>) => {
state.shouldFetchImages = action.payload;
},
},
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
*/
@ -90,7 +61,6 @@ const resultsSlice = createSlice({
deserializeImageResponse(image)
);
// use the adapter reducer to append all the results to state
resultsAdapter.addMany(state, resultImages);
state.page = page;
@ -103,14 +73,15 @@ const resultsSlice = createSlice({
* Invocation Complete
*/
builder.addCase(invocationComplete, (state, action) => {
const { data } = action.payload;
const { data, shouldFetchImages } = action.payload;
const { result, node, graph_execution_state_id } = data;
if (isImageOutput(result)) {
const name = result.image.image_name;
const type = result.image.image_type;
// if we need to refetch, set URLs to placeholder for now
const { url, thumbnail } = state.shouldFetchImages
const { url, thumbnail } = shouldFetchImages
? { url: '', thumbnail: '' }
: buildImageUrls(type, name);
@ -123,7 +94,7 @@ const resultsSlice = createSlice({
thumbnail,
metadata: {
created: timestamp,
width: result.width, // TODO: add tese dimensions
width: result.width,
height: result.height,
invokeai: {
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 {
selectAll: selectResultsAll,
selectById: selectResultsById,
@ -172,6 +141,6 @@ export const {
selectTotal: selectResultsTotal,
} = resultsAdapter.getSelectors<RootState>((state) => state.results);
export const { resultAdded, setShouldFetchImages } = resultsSlice.actions;
export const { resultAdded } = resultsSlice.actions;
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<
V extends InputFieldValue,

View File

@ -3,7 +3,10 @@ import { NodesState } from './nodesSlice';
/**
* Nodes slice persist blacklist
*/
const itemsToBlacklist: (keyof NodesState)[] = ['schema', 'invocations'];
const itemsToBlacklist: (keyof NodesState)[] = [
'schema',
'invocationTemplates',
];
export const nodesBlacklist = itemsToBlacklist.map(
(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 IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
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';
interface ImageToImageStrengthProps {
label?: string;
}
const selector = createSelector(
[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 { t } = useTranslation();
const { label = `${t('parameters.strength')}` } = props;
const img2imgStrength = useAppSelector(
(state: RootState) => state.generation.img2imgStrength
);
const isImageToImageEnabled = useAppSelector(
(state: RootState) => state.generation.isImageToImageEnabled
);
const step = hotkeys.shift ? fineStep : coarseStep;
return {
img2imgStrength,
isImageToImageEnabled,
initial,
min,
sliderMax,
inputMax,
step,
};
}
);
const ImageToImageStrength = () => {
const {
img2imgStrength,
isImageToImageEnabled,
initial,
min,
sliderMax,
inputMax,
step,
} = useAppSelector(selector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeStrength = (v: number) => dispatch(setImg2imgStrength(v));
const handleChange = useCallback(
(v: number) => dispatch(setImg2imgStrength(v)),
[dispatch]
);
const handleImg2ImgStrengthReset = () => {
dispatch(setImg2imgStrength(0.75));
};
const handleReset = useCallback(() => {
dispatch(setImg2imgStrength(initial));
}, [dispatch, initial]);
return (
<IAISlider
label={label}
step={0.01}
min={0.01}
max={1}
onChange={handleChangeStrength}
label={`${t('parameters.strength')}`}
step={step}
min={min}
max={sliderMax}
onChange={handleChange}
handleReset={handleReset}
value={img2imgStrength}
isInteger={false}
withInput
withSliderMarks
inputWidth={22}
withReset
handleReset={handleImg2ImgStrengthReset}
isDisabled={!isImageToImageEnabled}
sliderNumberInputProps={{ max: inputMax }}
/>
);
}
};
export default memo(ImageToImageStrength);

View File

@ -1,37 +1,57 @@
import { Box, BoxProps } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setHeight } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { memo, useCallback } 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 selector = createSelector(
[generationSelector, hotkeysSelector, configSelector],
(generation, hotkeys, config) => {
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 { t } = useTranslation();
const handleChange = useCallback(
(v: number) => {
dispatch(setHeight(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setHeight(initial));
}, [dispatch, initial]);
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>
<IAISlider
label={t('parameters.height')}
value={height}
min={min}
step={step}
max={sliderMax}
onChange={handleChange}
handleReset={handleReset}
withInput
withReset
withSliderMarks
sliderNumberInputProps={{ max: inputMax }}
/>
);
};

View File

@ -1,46 +1,85 @@
import { RootState } from 'app/store';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
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';
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 cfgScale = useAppSelector(
(state: RootState) => state.generation.cfgScale
);
const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders
);
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 ? (
<IAISlider
label={t('parameters.cfgScale')}
step={0.5}
min={1.01}
max={30}
onChange={handleChangeCfgScale}
handleReset={() => dispatch(setCfgScale(7.5))}
step={shift ? 0.1 : 0.5}
min={min}
max={sliderMax}
onChange={handleChange}
handleReset={handleReset}
value={cfgScale}
sliderNumberInputProps={{ max: 200 }}
sliderNumberInputProps={{ max: inputMax }}
withInput
withReset
withSliderMarks
isInteger={false}
/>
) : (
<IAINumberInput
label={t('parameters.cfgScale')}
step={0.5}
min={1.01}
max={200}
onChange={handleChangeCfgScale}
min={min}
max={inputMax}
onChange={handleChange}
value={cfgScale}
isInteger={false}
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 IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
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';
export default function MainIterations() {
const iterations = useAppSelector(
(state: RootState) => state.generation.iterations
);
const selector = createSelector(
[generationSelector, configSelector, uiSelector, hotkeysSelector],
(generation, config, ui, hotkeys) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
config.sd.iterations;
const { iterations } = generation;
const { shouldUseSliders } = ui;
const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders
);
const step = hotkeys.shift ? fineStep : coarseStep;
return {
iterations,
initial,
min,
sliderMax,
inputMax,
step,
shouldUseSliders,
};
}
);
const MainIterations = () => {
const {
iterations,
initial,
min,
sliderMax,
inputMax,
step,
shouldUseSliders,
} = useAppSelector(selector);
const dispatch = useAppDispatch();
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 ? (
<IAISlider
label={t('parameters.images')}
step={1}
min={1}
max={16}
onChange={handleChangeIterations}
handleReset={() => dispatch(setIterations(1))}
step={step}
min={min}
max={sliderMax}
onChange={handleChange}
handleReset={handleReset}
value={iterations}
withInput
withReset
withSliderMarks
sliderNumberInputProps={{ max: 9999 }}
sliderNumberInputProps={{ max: inputMax }}
/>
) : (
<IAINumberInput
label={t('parameters.images')}
step={1}
min={1}
max={9999}
onChange={handleChangeIterations}
step={step}
min={min}
max={inputMax}
onChange={handleChange}
value={iterations}
numberInputFieldProps={{ textAlign: 'center' }}
/>
);
}
};
export default memo(MainIterations);

View File

@ -1,35 +1,32 @@
import { Box, BoxProps } from '@chakra-ui/react';
import { DIFFUSERS_SAMPLERS, SAMPLERS } from 'app/constants';
import { DIFFUSERS_SAMPLERS } from 'app/constants';
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISelect from 'common/components/IAISelect';
import { setSampler } from 'features/parameters/store/generationSlice';
import { activeModelSelector } from 'features/system/store/systemSelectors';
import { ChangeEvent } from 'react';
import { ChangeEvent, memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export default function MainSampler(props: BoxProps) {
const Scheduler = () => {
const sampler = useAppSelector(
(state: RootState) => state.generation.sampler
);
const activeModel = useAppSelector(activeModelSelector);
const dispatch = useAppDispatch();
const { t } = useTranslation();
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) =>
dispatch(setSampler(e.target.value));
const handleChange = useCallback(
(e: ChangeEvent<HTMLSelectElement>) => dispatch(setSampler(e.target.value)),
[dispatch]
);
return (
<Box {...props}>
<IAISelect
label={t('parameters.sampler')}
value={sampler}
onChange={handleChangeSampler}
validValues={
activeModel.format === 'diffusers' ? DIFFUSERS_SAMPLERS : SAMPLERS
}
minWidth={36}
/>
</Box>
<IAISelect
label={t('parameters.sampler')}
value={sampler}
onChange={handleChange}
validValues={DIFFUSERS_SAMPLERS}
minWidth={36}
/>
);
}
};
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 { useAppSelector } from 'app/storeHooks';
import { ModelSelect } from 'exports';
import { memo } from 'react';
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 MainSettings = () => {
const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders
);
@ -21,9 +20,16 @@ export default function MainSettings() {
<MainIterations />
<MainSteps />
<MainCFGScale />
<MainWidth />
<MainHeight />
<MainSampler />
<WidthSlider />
<HeightSlider />
<Flex gap={3} w="full">
<Box flexGrow={2}>
<MainSampler />
</Box>
<Box flexGrow={3}>
<ModelSelect />
</Box>
</Flex>
</VStack>
) : (
<Flex gap={3} flexDirection="column">
@ -32,12 +38,18 @@ export default function MainSettings() {
<MainSteps />
<MainCFGScale />
</Flex>
<Flex gap={3}>
<MainSampler flexGrow={2} />
<ModelSelect flexGrow={3} />
</Flex>
<WidthSlider />
<HeightSlider />
<Flex gap={3} w="full">
<Box flexGrow={2}>
<MainSampler />
</Box>
<Box flexGrow={3}>
<ModelSelect />
</Box>
</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 IAINumberInput from 'common/components/IAINumberInput';
import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import {
clampSymmetrySteps,
setSteps,
} 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';
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 steps = useAppSelector((state: RootState) => state.generation.steps);
const shouldUseSliders = useAppSelector(
(state: RootState) => state.ui.shouldUseSliders
);
const { t } = useTranslation();
const handleChangeSteps = (v: number) => {
dispatch(setSteps(v));
};
const handleChange = useCallback(
(v: number) => {
dispatch(setSteps(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setSteps(initial));
}, [dispatch, initial]);
const handleBlur = () => {
const handleBlur = useCallback(() => {
dispatch(clampSymmetrySteps());
};
}, [dispatch]);
return shouldUseSliders ? (
<IAISlider
label={t('parameters.steps')}
min={1}
step={1}
onChange={handleChangeSteps}
handleReset={() => dispatch(setSteps(20))}
min={min}
max={sliderMax}
step={step}
onChange={handleChange}
handleReset={handleReset}
value={steps}
withInput
withReset
withSliderMarks
sliderNumberInputProps={{ max: 9999 }}
sliderNumberInputProps={{ max: inputMax }}
/>
) : (
<IAINumberInput
label={t('parameters.steps')}
min={1}
max={9999}
step={1}
onChange={handleChangeSteps}
min={min}
max={inputMax}
step={step}
onChange={handleChange}
value={steps}
numberInputFieldProps={{ textAlign: 'center' }}
onBlur={handleBlur}
/>
);
}
};
export default memo(MainSteps);

View File

@ -1,36 +1,57 @@
import { Box, BoxProps } from '@chakra-ui/react';
import { RootState } from 'app/store';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { generationSelector } from 'features/parameters/store/generationSelectors';
import { setWidth } from 'features/parameters/store/generationSlice';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { memo } from 'react';
import { configSelector } from 'features/system/store/configSelectors';
import { hotkeysSelector } from 'features/ui/store/hotkeysSlice';
import { memo, useCallback } 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 selector = createSelector(
[generationSelector, hotkeysSelector, configSelector],
(generation, hotkeys, config) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
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 { t } = useTranslation();
const handleChange = useCallback(
(v: number) => {
dispatch(setWidth(v));
},
[dispatch]
);
const handleReset = useCallback(() => {
dispatch(setWidth(initial));
}, [dispatch, initial]);
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>
<IAISlider
label={t('parameters.width')}
value={width}
min={min}
step={step}
max={sliderMax}
onChange={handleChange}
handleReset={handleReset}
withInput
withReset
withSliderMarks
sliderNumberInputProps={{ max: inputMax }}
/>
);
};

View File

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

View File

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

View File

@ -8,22 +8,26 @@ import ModelManagerModal from './ModelManager/ModelManagerModal';
import SettingsModal from './SettingsModal/SettingsModal';
import ThemeChanger from './ThemeChanger';
import IAIIconButton from 'common/components/IAIIconButton';
import { useAppSelector } from 'app/storeHooks';
import { RootState } from 'app/store';
import { useFeatureStatus } from '../hooks/useFeatureStatus';
const SiteHeaderMenu = () => {
const disabledFeatures = useAppSelector(
(state: RootState) => state.system.disabledFeatures
);
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 (
<Flex
alignItems="center"
flexDirection={{ base: 'column', xl: 'row' }}
gap={{ base: 4, xl: 1 }}
>
{!disabledFeatures.includes('modelManager') && (
{isModelManagerEnabled && (
<ModelManagerModal>
<IAIIconButton
aria-label={t('modelManager.modelManager')}
@ -51,9 +55,9 @@ const SiteHeaderMenu = () => {
<ThemeChanger />
{!disabledFeatures.includes('localization') && <LanguagePicker />}
{isLocalizationEnabled && <LanguagePicker />}
{!disabledFeatures.includes('bugLink') && (
{isBugLinkEnabled && (
<Link
isExternal
href="http://github.com/invoke-ai/InvokeAI/issues"
@ -71,7 +75,7 @@ const SiteHeaderMenu = () => {
</Link>
)}
{!disabledFeatures.includes('githubLink') && (
{isGithubLinkEnabled && (
<Link
isExternal
href="http://github.com/invoke-ai/InvokeAI"
@ -89,7 +93,7 @@ const SiteHeaderMenu = () => {
</Link>
)}
{!disabledFeatures.includes('discordLink') && (
{isDiscordLinkEnabled && (
<Link
isExternal
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 { useAppSelector } from 'app/storeHooks';
import { useMemo } from 'react';
import { configSelector } from '../store/configSelectors';
import { systemSelector } from '../store/systemSelectors';
const isApplicationReadySelector = createSelector(
[(state: RootState) => state.system],
(system) => {
const {
disabledFeatures,
disabledTabs,
wereModelsReceived,
wasSchemaParsed,
} = system;
[systemSelector, configSelector],
(system, config) => {
const { wereModelsReceived, wasSchemaParsed } = system;
const { disabledTabs } = config;
return {
disabledTabs,
disabledFeatures,
wereModelsReceived,
wasSchemaParsed,
};
@ -23,12 +21,9 @@ const isApplicationReadySelector = createSelector(
);
export const useIsApplicationReady = () => {
const {
disabledTabs,
disabledFeatures,
wereModelsReceived,
wasSchemaParsed,
} = useAppSelector(isApplicationReadySelector);
const { disabledTabs, wereModelsReceived, wasSchemaParsed } = useAppSelector(
isApplicationReadySelector
);
const isApplicationReady = useMemo(() => {
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
*/
subscribedNodeIds: string[];
/**
* Whether or not URLs should be transformed to use a different host
*/
shouldTransformUrls: boolean;
/**
* Array of disabled tabs
*/
disabledTabs: InvokeTabName[];
/**
* Array of disabled features
*/
disabledFeatures: InvokeAI.ApplicationFeature[];
// /**
// * Whether or not URLs should be transformed to use a different host
// */
// shouldTransformUrls: boolean;
// /**
// * Array of disabled tabs
// */
// disabledTabs: InvokeTabName[];
// /**
// * Array of disabled features
// */
// disabledFeatures: InvokeAI.AppFeature[];
/**
* Whether or not the available models were received
*/
@ -157,9 +157,9 @@ const initialSystemState: SystemState = {
cancelType: 'immediate',
isCancelScheduled: false,
subscribedNodeIds: [],
shouldTransformUrls: false,
disabledTabs: [],
disabledFeatures: [],
// shouldTransformUrls: false,
// disabledTabs: [],
// disabledFeatures: [],
wereModelsReceived: false,
wasSchemaParsed: false,
};
@ -359,27 +359,27 @@ export const systemSlice = createSlice({
subscribedNodeIdsSet: (state, action: PayloadAction<string[]>) => {
state.subscribedNodeIds = action.payload;
},
/**
* `shouldTransformUrls` was changed
*/
shouldTransformUrlsChanged: (state, action: PayloadAction<boolean>) => {
state.shouldTransformUrls = action.payload;
},
/**
* `disabledTabs` was changed
*/
disabledTabsChanged: (state, action: PayloadAction<InvokeTabName[]>) => {
state.disabledTabs = action.payload;
},
/**
* `disabledFeatures` was changed
*/
disabledFeaturesChanged: (
state,
action: PayloadAction<InvokeAI.ApplicationFeature[]>
) => {
state.disabledFeatures = action.payload;
},
// /**
// * `shouldTransformUrls` was changed
// */
// shouldTransformUrlsChanged: (state, action: PayloadAction<boolean>) => {
// state.shouldTransformUrls = action.payload;
// },
// /**
// * `disabledTabs` was changed
// */
// disabledTabsChanged: (state, action: PayloadAction<InvokeTabName[]>) => {
// state.disabledTabs = action.payload;
// },
// /**
// * `disabledFeatures` was changed
// */
// disabledFeaturesChanged: (
// state,
// action: PayloadAction<InvokeAI.AppFeature[]>
// ) => {
// state.disabledFeatures = action.payload;
// },
},
extraReducers(builder) {
/**
@ -601,9 +601,9 @@ export const {
scheduledCancelAborted,
cancelTypeChanged,
subscribedNodeIdsSet,
shouldTransformUrlsChanged,
disabledTabsChanged,
disabledFeaturesChanged,
// shouldTransformUrlsChanged,
// disabledTabsChanged,
// disabledFeaturesChanged,
} = systemSlice.actions;
export default systemSlice.reducer;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -92,9 +92,9 @@ export const socketMiddleware = () => {
socket.on('connect', () => {
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
if (!results.ids.length) {
@ -203,13 +203,20 @@ export const socketMiddleware = () => {
const sessionId = data.graph_execution_state_id;
const { cancelType, isCancelScheduled } = getState().system;
const { shouldFetchImages } = getState().config;
// Handle scheduled cancelation
if (cancelType === 'scheduled' && isCancelScheduled) {
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)) {
const { results } = getState();
const { config } = getState();
if (results.shouldFetchImages) {
if (config.shouldFetchImages) {
const { result } = action.payload.data;
if (isImageOutput(result)) {
const imageName = result.image.image_name;