feat(ui): support disabledFeatures, add nicer loading

- `disabledParametersPanels` -> `disabledFeatures`
- handle disabling `faceRestore`, `upscaling`, `lightbox`, `modelManager` and OSS header links/buttons
- wait until models are loaded to hide loading screen
- also wait until schema is parsed if `nodes` is an enabled tab
This commit is contained in:
psychedelicious 2023-04-25 22:10:07 +10:00
parent 82c4dd8b86
commit c1c881ded5
20 changed files with 439 additions and 287 deletions

View File

@ -1,39 +0,0 @@
import { Flex, Spinner, Text } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
interface LoaderProps {
showText?: boolean;
text?: string;
}
// This component loads before the theme so we cannot use theme tokens here
const Loading = (props: LoaderProps) => {
const { t } = useTranslation();
const { showText = false, text = t('common.loadingInvokeAI') } = props;
return (
<Flex
width="100vw"
height="100vh"
alignItems="center"
justifyContent="center"
bg="#121212"
flexDirection="column"
rowGap={4}
>
<Spinner color="grey" w="5rem" h="5rem" />
{showText && (
<Text
color="grey"
fontWeight="semibold"
fontFamily="'Inter', sans-serif"
>
{text}
</Text>
)}
</Flex>
);
};
export default Loading;

View File

@ -15,17 +15,24 @@ import ImageGalleryPanel from 'features/gallery/components/ImageGalleryPanel';
import Lightbox from 'features/lightbox/components/Lightbox';
import { useAppDispatch, useAppSelector } from './storeHooks';
import { PropsWithChildren, useEffect } from 'react';
import { setDisabledPanels, setDisabledTabs } from 'features/ui/store/uiSlice';
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 {
ApplicationFeature,
disabledFeaturesChanged,
disabledTabsChanged,
} from 'features/system/store/systemSlice';
import { useIsApplicationReady } from 'features/system/hooks/useIsApplicationReady';
keepGUIAlive();
interface Props extends PropsWithChildren {
options: {
disabledPanels: string[];
disabledTabs: InvokeTabName[];
disabledFeatures: ApplicationFeature[];
shouldTransformUrls?: boolean;
shouldFetchImages: boolean;
};
@ -35,15 +42,21 @@ const App = (props: Props) => {
useToastWatcher();
const currentTheme = useAppSelector((state) => state.ui.currentTheme);
const disabledFeatures = useAppSelector(
(state) => state.system.disabledFeatures
);
const isApplicationReady = useIsApplicationReady();
const { setColorMode } = useColorMode();
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(setDisabledPanels(props.options.disabledPanels));
}, [dispatch, props.options.disabledPanels]);
dispatch(disabledFeaturesChanged(props.options.disabledFeatures));
}, [dispatch, props.options.disabledFeatures]);
useEffect(() => {
dispatch(setDisabledTabs(props.options.disabledTabs));
dispatch(disabledTabsChanged(props.options.disabledTabs));
}, [dispatch, props.options.disabledTabs]);
useEffect(() => {
@ -61,8 +74,8 @@ const App = (props: Props) => {
}, [setColorMode, currentTheme]);
return (
<Grid w="100vw" h="100vh">
<Lightbox />
<Grid w="100vw" h="100vh" position="relative">
{!disabledFeatures.includes('lightbox') && <Lightbox />}
<ImageUploader>
<ProgressBar />
<Grid
@ -83,16 +96,34 @@ const App = (props: Props) => {
<ImageGalleryPanel />
</Flex>
</Grid>
<Box>
<Console />
</Box>
</ImageUploader>
<AnimatePresence>
{!isApplicationReady && (
<motion.div
key="loading"
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
style={{ zIndex: 3 }}
>
<Box position="absolute" top={0} left={0} w="100vw" h="100vh">
<Loading />
</Box>
</motion.div>
)}
</AnimatePresence>
<Portal>
<FloatingParametersPanelButtons />
</Portal>
<Portal>
<FloatingGalleryButton />
</Portal>
<Portal>
<Console />
</Portal>
</Grid>
);
};

View File

@ -0,0 +1,21 @@
import { Flex, Image } from '@chakra-ui/react';
import InvokeAILogoImage from 'assets/images/logo.png';
// This component loads before the theme so we cannot use theme tokens here
const Loading = () => {
return (
<Flex
position="relative"
width="100vw"
height="100vh"
alignItems="center"
justifyContent="center"
bg="#151519"
>
<Image src={InvokeAILogoImage} w="8rem" h="8rem" />
</Flex>
);
};
export default Loading;

View File

@ -15,11 +15,12 @@ import '@fontsource/inter/700.css';
import '@fontsource/inter/800.css';
import '@fontsource/inter/900.css';
import Loading from './Loading';
import Loading from './common/components/Loading/Loading';
// Localization
import './i18n';
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
import { ApplicationFeature } from 'features/system/store/systemSlice';
const App = lazy(() => import('./app/App'));
const ThemeLocaleProvider = lazy(() => import('./app/ThemeLocaleProvider'));
@ -28,6 +29,7 @@ interface Props extends PropsWithChildren {
apiUrl?: string;
disabledPanels?: string[];
disabledTabs?: InvokeTabName[];
disabledFeatures?: ApplicationFeature[];
token?: string;
shouldTransformUrls?: boolean;
shouldFetchImages?: boolean;
@ -35,8 +37,15 @@ interface Props extends PropsWithChildren {
export default function Component({
apiUrl,
disabledPanels = [],
disabledTabs = [],
disabledFeatures = [
'lightbox',
'bugLink',
'discordLink',
'githubLink',
'localization',
'modelManager',
],
token,
children,
shouldTransformUrls,
@ -69,12 +78,12 @@ export default function Component({
<React.StrictMode>
<Provider store={store}>
<PersistGate loading={<Loading />} persistor={persistor}>
<React.Suspense fallback={<Loading showText />}>
<React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider>
<App
options={{
disabledPanels,
disabledTabs,
disabledFeatures,
shouldTransformUrls,
shouldFetchImages,
}}

View File

@ -70,8 +70,8 @@ const currentImageButtonsSelector = createSelector(
selectedImageSelector,
],
(
system: SystemState,
gallery: GalleryState,
system,
gallery,
postprocessing,
ui,
lightbox,
@ -81,6 +81,8 @@ const currentImageButtonsSelector = createSelector(
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
system;
const { disabledFeatures } = system;
const { upscalingLevel, facetoolStrength } = postprocessing;
const { isLightboxOpen } = lightbox;
@ -90,6 +92,7 @@ const currentImageButtonsSelector = createSelector(
const { intermediateImage, currentImage } = gallery;
return {
disabledFeatures,
isProcessing,
isConnected,
isGFPGANAvailable,
@ -134,7 +137,9 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
activeTabName,
shouldHidePreview,
selectedImage,
disabledFeatures,
} = useAppSelector(currentImageButtonsSelector);
const { getUrl, shouldTransformUrls } = useGetUrl();
const toast = useToast();
@ -328,24 +333,19 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys(
'Shift+U',
() => {
if (
isESRGANAvailable &&
!shouldDisableToolbarButtons &&
isConnected &&
!isProcessing &&
upscalingLevel
) {
handleClickUpscale();
} else {
toast({
title: t('toast.upscalingFailed'),
status: 'error',
duration: 2500,
isClosable: true,
});
}
handleClickUpscale();
},
{
enabled: () =>
disabledFeatures.includes('upscaling') ||
!isESRGANAvailable ||
shouldDisableToolbarButtons ||
!isConnected ||
isProcessing ||
!upscalingLevel,
},
[
disabledFeatures,
selectedImage,
isESRGANAvailable,
shouldDisableToolbarButtons,
@ -362,24 +362,20 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys(
'Shift+R',
() => {
if (
isGFPGANAvailable &&
!shouldDisableToolbarButtons &&
isConnected &&
!isProcessing &&
facetoolStrength
) {
handleClickFixFaces();
} else {
toast({
title: t('toast.faceRestoreFailed'),
status: 'error',
duration: 2500,
isClosable: true,
});
}
handleClickFixFaces();
},
{
enabled: () =>
disabledFeatures.includes('faceRestore') ||
!isGFPGANAvailable ||
shouldDisableToolbarButtons ||
!isConnected ||
isProcessing ||
!facetoolStrength,
},
[
disabledFeatures,
selectedImage,
isGFPGANAvailable,
shouldDisableToolbarButtons,
@ -509,21 +505,23 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
isChecked={shouldHidePreview}
onClick={handlePreviewVisibility}
/>
<IAIIconButton
icon={<FaExpand />}
tooltip={
!isLightboxOpen
? `${t('parameters.openInViewer')} (Z)`
: `${t('parameters.closeViewer')} (Z)`
}
aria-label={
!isLightboxOpen
? `${t('parameters.openInViewer')} (Z)`
: `${t('parameters.closeViewer')} (Z)`
}
isChecked={isLightboxOpen}
onClick={handleLightBox}
/>
{!disabledFeatures.includes('lightbox') && (
<IAIIconButton
icon={<FaExpand />}
tooltip={
!isLightboxOpen
? `${t('parameters.openInViewer')} (Z)`
: `${t('parameters.closeViewer')} (Z)`
}
aria-label={
!isLightboxOpen
? `${t('parameters.openInViewer')} (Z)`
: `${t('parameters.closeViewer')} (Z)`
}
isChecked={isLightboxOpen}
onClick={handleLightBox}
/>
)}
</ButtonGroup>
<ButtonGroup isAttached={true}>
@ -556,65 +554,74 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
/>
</ButtonGroup>
<ButtonGroup isAttached={true}>
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaGrinStars />}
aria-label={t('parameters.restoreFaces')}
/>
}
>
<Flex
sx={{
flexDirection: 'column',
rowGap: 4,
}}
>
<FaceRestoreSettings />
<IAIButton
isDisabled={
!isGFPGANAvailable ||
!selectedImage ||
!(isConnected && !isProcessing) ||
!facetoolStrength
{!(
disabledFeatures.includes('faceRestore') &&
disabledFeatures.includes('upscaling')
) && (
<ButtonGroup isAttached={true}>
{!disabledFeatures.includes('faceRestore') && (
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaGrinStars />}
aria-label={t('parameters.restoreFaces')}
/>
}
onClick={handleClickFixFaces}
>
{t('parameters.restoreFaces')}
</IAIButton>
</Flex>
</IAIPopover>
<Flex
sx={{
flexDirection: 'column',
rowGap: 4,
}}
>
<FaceRestoreSettings />
<IAIButton
isDisabled={
!isGFPGANAvailable ||
!selectedImage ||
!(isConnected && !isProcessing) ||
!facetoolStrength
}
onClick={handleClickFixFaces}
>
{t('parameters.restoreFaces')}
</IAIButton>
</Flex>
</IAIPopover>
)}
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaExpandArrowsAlt />}
aria-label={t('parameters.upscale')}
/>
}
>
<Flex
sx={{
flexDirection: 'column',
gap: 4,
}}
>
<UpscaleSettings />
<IAIButton
isDisabled={
!isESRGANAvailable ||
!selectedImage ||
!(isConnected && !isProcessing) ||
!upscalingLevel
{!disabledFeatures.includes('upscaling') && (
<IAIPopover
triggerComponent={
<IAIIconButton
icon={<FaExpandArrowsAlt />}
aria-label={t('parameters.upscale')}
/>
}
onClick={handleClickUpscale}
>
{t('parameters.upscaleImage')}
</IAIButton>
</Flex>
</IAIPopover>
</ButtonGroup>
<Flex
sx={{
flexDirection: 'column',
gap: 4,
}}
>
<UpscaleSettings />
<IAIButton
isDisabled={
!isESRGANAvailable ||
!selectedImage ||
!(isConnected && !isProcessing) ||
!upscalingLevel
}
onClick={handleClickUpscale}
>
{t('parameters.upscaleImage')}
</IAIButton>
</Flex>
</IAIPopover>
)}
</ButtonGroup>
)}
<ButtonGroup isAttached={true}>
<IAIIconButton

View File

@ -57,6 +57,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
galleryImageMinimumWidth,
mayDeleteImage,
shouldUseSingleGalleryColumn,
disabledFeatures,
} = useAppSelector(hoverableImageSelector);
const { image, isSelected } = props;
const { url, thumbnail, name, metadata } = image;
@ -133,7 +134,6 @@ const HoverableImage = memo((props: HoverableImageProps) => {
metadata.sd_metadata?.image?.init_image_path
);
if (response.ok) {
dispatch(setActiveTab('img2img'));
dispatch(setAllImageToImageParameters(metadata?.sd_metadata));
toast({
title: t('toast.initialImageSet'),
@ -174,9 +174,11 @@ const HoverableImage = memo((props: HoverableImageProps) => {
menuProps={{ size: 'sm', isLazy: true }}
renderMenu={() => (
<MenuList>
<MenuItem onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</MenuItem>
{!disabledFeatures.includes('lightbox') && (
<MenuItem onClickCapture={handleLightBox}>
{t('parameters.openInViewer')}
</MenuItem>
)}
<MenuItem
onClickCapture={handleUsePrompt}
isDisabled={image?.metadata?.sd_metadata?.prompt === undefined}

View File

@ -77,6 +77,7 @@ export const hoverableImageSelector = createSelector(
shouldUseSingleGalleryColumn: gallery.shouldUseSingleGalleryColumn,
activeTabName,
isLightboxOpen: lightbox.isLightboxOpen,
disabledFeatures: system.disabledFeatures,
};
},
{

View File

@ -85,8 +85,7 @@ const nodesSlice = createSlice({
},
extraReducers(builder) {
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
state.schema = action.payload;
state.invocationTemplates = parseSchema(action.payload);
state.invocationTemplates = action.payload;
});
builder.addMatcher(isFulfilledAnyGraphBuilt, (state, action) => {

View File

@ -114,7 +114,5 @@ export const parseSchema = (openAPI: OpenAPIV3.Document) => {
return acc;
}, {});
console.debug('Generated invocations: ', invocations);
return invocations;
};

View File

@ -2,36 +2,41 @@ 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 { uiSelector } from 'features/ui/store/uiSelectors';
import { openAccordionItemsChanged } from 'features/ui/store/uiSlice';
import { filter } from 'lodash';
import { filter, map } from 'lodash';
import { ReactNode, useCallback } from 'react';
import InvokeAccordionItem from './AccordionItems/InvokeAccordionItem';
const parametersAccordionSelector = createSelector([uiSelector], (uiSlice) => {
const {
activeTab,
openLinearAccordionItems,
openUnifiedCanvasAccordionItems,
disabledParameterPanels,
} = uiSlice;
const parametersAccordionSelector = createSelector(
[uiSelector, systemSelector],
(uiSlice, system) => {
const {
activeTab,
openLinearAccordionItems,
openUnifiedCanvasAccordionItems,
} = uiSlice;
let openAccordions: number[] = [];
const { disabledFeatures } = system;
if (tabMap[activeTab] === 'linear') {
openAccordions = openLinearAccordionItems;
let openAccordions: number[] = [];
if (tabMap[activeTab] === 'linear') {
openAccordions = openLinearAccordionItems;
}
if (tabMap[activeTab] === 'unifiedCanvas') {
openAccordions = openUnifiedCanvasAccordionItems;
}
return {
openAccordions,
disabledFeatures,
};
}
if (tabMap[activeTab] === 'unifiedCanvas') {
openAccordions = openUnifiedCanvasAccordionItems;
}
return {
openAccordions,
disabledParameterPanels,
};
});
);
export type ParametersAccordionItem = {
name: string;
@ -53,7 +58,7 @@ type ParametersAccordionProps = {
* Main container for generation and processing parameters.
*/
const ParametersAccordion = ({ accordionItems }: ParametersAccordionProps) => {
const { openAccordions, disabledParameterPanels } = useAppSelector(
const { openAccordions, disabledFeatures } = useAppSelector(
parametersAccordionSelector
);
@ -68,20 +73,16 @@ const ParametersAccordion = ({ accordionItems }: ParametersAccordionProps) => {
};
// Render function for accordion items
const renderAccordionItems = useCallback(() => {
// Filter out disabled accordions
const filteredAccordionItems = filter(
accordionItems,
(item) => disabledParameterPanels.indexOf(item.name) === -1
);
return filteredAccordionItems.map((accordionItem) => (
<InvokeAccordionItem
key={accordionItem.name}
accordionItem={accordionItem}
/>
));
}, [disabledParameterPanels, accordionItems]);
const renderAccordionItems = useCallback(
() =>
map(accordionItems, (accordionItem) => (
<InvokeAccordionItem
key={accordionItem.name}
accordionItem={accordionItem}
/>
)),
[accordionItems]
);
return (
<Accordion

View File

@ -110,7 +110,7 @@ const Console = () => {
position: 'fixed',
insetInlineStart: 0,
bottom: 0,
zIndex: 9999,
zIndex: 1,
}}
maxHeight="90vh"
>
@ -128,6 +128,7 @@ const Console = () => {
borderTopWidth: 5,
bg: 'base.850',
borderColor: 'base.700',
zIndex: 2,
}}
ref={viewerRef}
onScroll={handleOnScroll}
@ -166,7 +167,7 @@ const Console = () => {
position: 'fixed',
insetInlineStart: 2,
bottom: 12,
zIndex: '10000',
zIndex: 1,
}}
/>
</Tooltip>
@ -184,7 +185,7 @@ const Console = () => {
position: 'fixed',
insetInlineStart: 2,
bottom: 2,
zIndex: '10000',
zIndex: 1,
}}
colorScheme={hasError || !wasErrorSeen ? 'error' : 'base'}
/>

View File

@ -8,8 +8,13 @@ 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';
const SiteHeaderMenu = () => {
const disabledFeatures = useAppSelector(
(state: RootState) => state.system.disabledFeatures
);
const { t } = useTranslation();
return (
@ -18,17 +23,19 @@ const SiteHeaderMenu = () => {
flexDirection={{ base: 'column', xl: 'row' }}
gap={{ base: 4, xl: 1 }}
>
<ModelManagerModal>
<IAIIconButton
aria-label={t('modelManager.modelManager')}
tooltip={t('modelManager.modelManager')}
size="sm"
variant="link"
data-variant="link"
fontSize={20}
icon={<FaCube />}
/>
</ModelManagerModal>
{!disabledFeatures.includes('modelManager') && (
<ModelManagerModal>
<IAIIconButton
aria-label={t('modelManager.modelManager')}
tooltip={t('modelManager.modelManager')}
size="sm"
variant="link"
data-variant="link"
fontSize={20}
icon={<FaCube />}
/>
</ModelManagerModal>
)}
<HotkeysModal>
<IAIIconButton
@ -44,55 +51,61 @@ const SiteHeaderMenu = () => {
<ThemeChanger />
<LanguagePicker />
{!disabledFeatures.includes('localization') && <LanguagePicker />}
<Link
isExternal
href="http://github.com/invoke-ai/InvokeAI/issues"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.reportBugLabel')}
tooltip={t('common.reportBugLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaBug />}
/>
</Link>
{!disabledFeatures.includes('bugLink') && (
<Link
isExternal
href="http://github.com/invoke-ai/InvokeAI/issues"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.reportBugLabel')}
tooltip={t('common.reportBugLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaBug />}
/>
</Link>
)}
<Link
isExternal
href="http://github.com/invoke-ai/InvokeAI"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.githubLabel')}
tooltip={t('common.githubLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaGithub />}
/>
</Link>
{!disabledFeatures.includes('githubLink') && (
<Link
isExternal
href="http://github.com/invoke-ai/InvokeAI"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.githubLabel')}
tooltip={t('common.githubLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaGithub />}
/>
</Link>
)}
<Link
isExternal
href="https://discord.gg/ZmtBAhwWhy"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.discordLabel')}
tooltip={t('common.discordLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaDiscord />}
/>
</Link>
{!disabledFeatures.includes('discordLink') && (
<Link
isExternal
href="https://discord.gg/ZmtBAhwWhy"
marginBottom="-0.25rem"
>
<IAIIconButton
aria-label={t('common.discordLabel')}
tooltip={t('common.discordLabel')}
variant="link"
data-variant="link"
fontSize={20}
size="sm"
icon={<FaDiscord />}
/>
</Link>
)}
<SettingsModal>
<IAIIconButton

View File

@ -0,0 +1,46 @@
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from 'app/store';
import { useAppSelector } from 'app/storeHooks';
import { useMemo } from 'react';
const isApplicationReadySelector = createSelector(
[(state: RootState) => state.system],
(system) => {
const {
disabledFeatures,
disabledTabs,
wereModelsReceived,
wasSchemaParsed,
} = system;
return {
disabledTabs,
disabledFeatures,
wereModelsReceived,
wasSchemaParsed,
};
}
);
export const useIsApplicationReady = () => {
const {
disabledTabs,
disabledFeatures,
wereModelsReceived,
wasSchemaParsed,
} = useAppSelector(isApplicationReadySelector);
const isApplicationReady = useMemo(() => {
if (!wereModelsReceived) {
return false;
}
if (!disabledTabs.includes('nodes') && !wasSchemaParsed) {
return false;
}
return true;
}, [disabledTabs, wereModelsReceived, wasSchemaParsed]);
return isApplicationReady;
};

View File

@ -19,6 +19,8 @@ const itemsToBlacklist: (keyof SystemState)[] = [
'isCancelScheduled',
'sessionId',
'progressImage',
'wereModelsReceived',
'wasSchemaParsed',
];
export const systemBlacklist = itemsToBlacklist.map(

View File

@ -19,6 +19,9 @@ import { ProgressImage } from 'services/events/types';
import { initialImageSelected } from 'features/parameters/store/generationSlice';
import { makeToast } from '../hooks/useToastWatcher';
import { sessionCanceled, sessionInvoked } from 'services/thunks/session';
import { InvokeTabName } from 'features/ui/store/tabMap';
import { receivedModels } from 'services/thunks/model';
import { receivedOpenAPISchema } from 'services/thunks/schema';
export type LogLevel = 'info' | 'warning' | 'error';
@ -32,10 +35,18 @@ export interface Log {
[index: number]: LogEntry;
}
export type ReadinessPayload = {
isReady: boolean;
reasonsWhyNotReady: string[];
};
/**
* A disable-able application feature
*/
export type ApplicationFeature =
| 'faceRestore'
| 'upscaling'
| 'lightbox'
| 'modelManager'
| 'githubLink'
| 'discordLink'
| 'bugLink'
| 'localization';
export type InProgressImageType = 'none' | 'full-res' | 'latents';
@ -96,6 +107,22 @@ export interface SystemState
* 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: ApplicationFeature[];
/**
* Whether or not the available models were received
*/
wereModelsReceived: boolean;
/**
* Whether or not the OpenAPI schema was received and parsed
*/
wasSchemaParsed: boolean;
}
const initialSystemState: SystemState = {
@ -144,6 +171,10 @@ const initialSystemState: SystemState = {
isCancelScheduled: false,
subscribedNodeIds: [],
shouldTransformUrls: false,
disabledTabs: [],
disabledFeatures: [],
wereModelsReceived: false,
wasSchemaParsed: false,
};
export const systemSlice = createSlice({
@ -347,6 +378,21 @@ export const systemSlice = createSlice({
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<ApplicationFeature[]>
) => {
state.disabledFeatures = action.payload;
},
},
extraReducers(builder) {
/**
@ -417,7 +463,8 @@ export const systemSlice = createSlice({
step,
total_steps,
progress_image,
invocation,
node,
source_node_id,
graph_execution_state_id,
} = action.payload.data;
@ -514,6 +561,20 @@ export const systemSlice = createSlice({
builder.addCase(initialImageSelected, (state) => {
state.toastQueue.push(makeToast(i18n.t('toast.sentToImageToImage')));
});
/**
* Received available models from the backend
*/
builder.addCase(receivedModels.fulfilled, (state, action) => {
state.wereModelsReceived = true;
});
/**
* OpenAPI schema was received and parsed
*/
builder.addCase(receivedOpenAPISchema.fulfilled, (state, action) => {
state.wasSchemaParsed = true;
});
},
});
@ -554,6 +615,8 @@ export const {
cancelTypeChanged,
subscribedNodeIdsSet,
shouldTransformUrlsChanged,
disabledTabsChanged,
disabledFeaturesChanged,
} = systemSlice.actions;
export default systemSlice.reducer;

View File

@ -64,8 +64,13 @@ export default function InvokeTabs() {
(state: RootState) => state.lightbox.isLightboxOpen
);
const { shouldPinGallery, disabledTabs, shouldPinParametersPanel } =
useAppSelector((state: RootState) => state.ui);
const { shouldPinGallery, shouldPinParametersPanel } = useAppSelector(
(state: RootState) => state.ui
);
const disabledTabs = useAppSelector(
(state: RootState) => state.system.disabledTabs
);
const activeTabs = buildTabs(disabledTabs);

View File

@ -1,11 +1,10 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { initialImageSelected } from 'features/parameters/store/generationSlice';
import { setActiveTabReducer } from './extraReducers';
import { InvokeTabName, tabMap } from './tabMap';
import { AddNewModelType, UIState } from './uiTypes';
const initialtabsState: UIState = {
const initialUIState: UIState = {
activeTab: 0,
currentTheme: 'dark',
parametersPanelScrollPosition: 0,
@ -19,13 +18,11 @@ const initialtabsState: UIState = {
shouldPinGallery: true,
shouldShowGallery: true,
shouldHidePreview: false,
disabledParameterPanels: [],
disabledTabs: [],
openLinearAccordionItems: [],
openUnifiedCanvasAccordionItems: [],
};
const initialState: UIState = initialtabsState;
const initialState: UIState = initialUIState;
export const uiSlice = createSlice({
name: 'ui',
@ -98,12 +95,6 @@ export const uiSlice = createSlice({
state.shouldShowParametersPanel = true;
}
},
setDisabledPanels: (state, action: PayloadAction<string[]>) => {
state.disabledParameterPanels = action.payload;
},
setDisabledTabs: (state, action: PayloadAction<InvokeTabName[]>) => {
state.disabledTabs = action.payload;
},
openAccordionItemsChanged: (state, action: PayloadAction<number[]>) => {
if (tabMap[state.activeTab] === 'linear') {
state.openLinearAccordionItems = action.payload;
@ -135,8 +126,6 @@ export const {
togglePinParametersPanel,
toggleParametersPanel,
toggleGalleryPanel,
setDisabledPanels,
setDisabledTabs,
openAccordionItemsChanged,
} = uiSlice.actions;

View File

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

View File

@ -92,7 +92,9 @@ export const socketMiddleware = () => {
socket.on('connect', () => {
dispatch(socketConnected({ timestamp: getTimestamp() }));
const { results, uploads, models, nodes } = getState();
const { results, uploads, models, nodes, system } = getState();
const { disabledTabs } = system;
// These thunks need to be dispatch in middleware; cannot handle in a reducer
if (!results.ids.length) {
@ -107,7 +109,7 @@ export const socketMiddleware = () => {
dispatch(receivedModels());
}
if (!nodes.schema) {
if (!nodes.schema && !disabledTabs.includes('nodes')) {
dispatch(receivedOpenAPISchema());
}
});

View File

@ -1,4 +1,5 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { parseSchema } from 'features/nodes/util/parseSchema';
import { OpenAPIV3 } from 'openapi-types';
export const receivedOpenAPISchema = createAsyncThunk(
@ -9,6 +10,10 @@ export const receivedOpenAPISchema = createAsyncThunk(
console.debug('OpenAPI schema: ', jsonData);
return jsonData;
const parsedSchema = parseSchema(jsonData);
console.debug('Parsed schema: ', parsedSchema);
return parsedSchema;
}
);