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 Lightbox from 'features/lightbox/components/Lightbox';
import { useAppDispatch, useAppSelector } from './storeHooks'; import { useAppDispatch, useAppSelector } from './storeHooks';
import { PropsWithChildren, useEffect } from 'react'; import { PropsWithChildren, useEffect } from 'react';
import { setDisabledPanels, setDisabledTabs } from 'features/ui/store/uiSlice';
import { InvokeTabName } from 'features/ui/store/tabMap'; import { InvokeTabName } from 'features/ui/store/tabMap';
import { shouldTransformUrlsChanged } from 'features/system/store/systemSlice'; import { shouldTransformUrlsChanged } from 'features/system/store/systemSlice';
import { setShouldFetchImages } from 'features/gallery/store/resultsSlice'; 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(); keepGUIAlive();
interface Props extends PropsWithChildren { interface Props extends PropsWithChildren {
options: { options: {
disabledPanels: string[];
disabledTabs: InvokeTabName[]; disabledTabs: InvokeTabName[];
disabledFeatures: ApplicationFeature[];
shouldTransformUrls?: boolean; shouldTransformUrls?: boolean;
shouldFetchImages: boolean; shouldFetchImages: boolean;
}; };
@ -35,15 +42,21 @@ const App = (props: Props) => {
useToastWatcher(); useToastWatcher();
const currentTheme = useAppSelector((state) => state.ui.currentTheme); const currentTheme = useAppSelector((state) => state.ui.currentTheme);
const disabledFeatures = useAppSelector(
(state) => state.system.disabledFeatures
);
const isApplicationReady = useIsApplicationReady();
const { setColorMode } = useColorMode(); const { setColorMode } = useColorMode();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
dispatch(setDisabledPanels(props.options.disabledPanels)); dispatch(disabledFeaturesChanged(props.options.disabledFeatures));
}, [dispatch, props.options.disabledPanels]); }, [dispatch, props.options.disabledFeatures]);
useEffect(() => { useEffect(() => {
dispatch(setDisabledTabs(props.options.disabledTabs)); dispatch(disabledTabsChanged(props.options.disabledTabs));
}, [dispatch, props.options.disabledTabs]); }, [dispatch, props.options.disabledTabs]);
useEffect(() => { useEffect(() => {
@ -61,8 +74,8 @@ const App = (props: Props) => {
}, [setColorMode, currentTheme]); }, [setColorMode, currentTheme]);
return ( return (
<Grid w="100vw" h="100vh"> <Grid w="100vw" h="100vh" position="relative">
<Lightbox /> {!disabledFeatures.includes('lightbox') && <Lightbox />}
<ImageUploader> <ImageUploader>
<ProgressBar /> <ProgressBar />
<Grid <Grid
@ -83,16 +96,34 @@ const App = (props: Props) => {
<ImageGalleryPanel /> <ImageGalleryPanel />
</Flex> </Flex>
</Grid> </Grid>
<Box>
<Console />
</Box>
</ImageUploader> </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> <Portal>
<FloatingParametersPanelButtons /> <FloatingParametersPanelButtons />
</Portal> </Portal>
<Portal> <Portal>
<FloatingGalleryButton /> <FloatingGalleryButton />
</Portal> </Portal>
<Portal>
<Console />
</Portal>
</Grid> </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/800.css';
import '@fontsource/inter/900.css'; import '@fontsource/inter/900.css';
import Loading from './Loading'; import Loading from './common/components/Loading/Loading';
// Localization // Localization
import './i18n'; import './i18n';
import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares'; import { addMiddleware, resetMiddlewares } from 'redux-dynamic-middlewares';
import { ApplicationFeature } from 'features/system/store/systemSlice';
const App = lazy(() => import('./app/App')); const App = lazy(() => import('./app/App'));
const ThemeLocaleProvider = lazy(() => import('./app/ThemeLocaleProvider')); const ThemeLocaleProvider = lazy(() => import('./app/ThemeLocaleProvider'));
@ -28,6 +29,7 @@ interface Props extends PropsWithChildren {
apiUrl?: string; apiUrl?: string;
disabledPanels?: string[]; disabledPanels?: string[];
disabledTabs?: InvokeTabName[]; disabledTabs?: InvokeTabName[];
disabledFeatures?: ApplicationFeature[];
token?: string; token?: string;
shouldTransformUrls?: boolean; shouldTransformUrls?: boolean;
shouldFetchImages?: boolean; shouldFetchImages?: boolean;
@ -35,8 +37,15 @@ interface Props extends PropsWithChildren {
export default function Component({ export default function Component({
apiUrl, apiUrl,
disabledPanels = [],
disabledTabs = [], disabledTabs = [],
disabledFeatures = [
'lightbox',
'bugLink',
'discordLink',
'githubLink',
'localization',
'modelManager',
],
token, token,
children, children,
shouldTransformUrls, shouldTransformUrls,
@ -69,12 +78,12 @@ export default function Component({
<React.StrictMode> <React.StrictMode>
<Provider store={store}> <Provider store={store}>
<PersistGate loading={<Loading />} persistor={persistor}> <PersistGate loading={<Loading />} persistor={persistor}>
<React.Suspense fallback={<Loading showText />}> <React.Suspense fallback={<Loading />}>
<ThemeLocaleProvider> <ThemeLocaleProvider>
<App <App
options={{ options={{
disabledPanels,
disabledTabs, disabledTabs,
disabledFeatures,
shouldTransformUrls, shouldTransformUrls,
shouldFetchImages, shouldFetchImages,
}} }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,8 +8,13 @@ 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 { RootState } from 'app/store';
const SiteHeaderMenu = () => { const SiteHeaderMenu = () => {
const disabledFeatures = useAppSelector(
(state: RootState) => state.system.disabledFeatures
);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@ -18,17 +23,19 @@ const SiteHeaderMenu = () => {
flexDirection={{ base: 'column', xl: 'row' }} flexDirection={{ base: 'column', xl: 'row' }}
gap={{ base: 4, xl: 1 }} gap={{ base: 4, xl: 1 }}
> >
<ModelManagerModal> {!disabledFeatures.includes('modelManager') && (
<IAIIconButton <ModelManagerModal>
aria-label={t('modelManager.modelManager')} <IAIIconButton
tooltip={t('modelManager.modelManager')} aria-label={t('modelManager.modelManager')}
size="sm" tooltip={t('modelManager.modelManager')}
variant="link" size="sm"
data-variant="link" variant="link"
fontSize={20} data-variant="link"
icon={<FaCube />} fontSize={20}
/> icon={<FaCube />}
</ModelManagerModal> />
</ModelManagerModal>
)}
<HotkeysModal> <HotkeysModal>
<IAIIconButton <IAIIconButton
@ -44,55 +51,61 @@ const SiteHeaderMenu = () => {
<ThemeChanger /> <ThemeChanger />
<LanguagePicker /> {!disabledFeatures.includes('localization') && <LanguagePicker />}
<Link {!disabledFeatures.includes('bugLink') && (
isExternal <Link
href="http://github.com/invoke-ai/InvokeAI/issues" isExternal
marginBottom="-0.25rem" href="http://github.com/invoke-ai/InvokeAI/issues"
> marginBottom="-0.25rem"
<IAIIconButton >
aria-label={t('common.reportBugLabel')} <IAIIconButton
tooltip={t('common.reportBugLabel')} aria-label={t('common.reportBugLabel')}
variant="link" tooltip={t('common.reportBugLabel')}
data-variant="link" variant="link"
fontSize={20} data-variant="link"
size="sm" fontSize={20}
icon={<FaBug />} size="sm"
/> icon={<FaBug />}
</Link> />
</Link>
)}
<Link {!disabledFeatures.includes('githubLink') && (
isExternal <Link
href="http://github.com/invoke-ai/InvokeAI" isExternal
marginBottom="-0.25rem" href="http://github.com/invoke-ai/InvokeAI"
> marginBottom="-0.25rem"
<IAIIconButton >
aria-label={t('common.githubLabel')} <IAIIconButton
tooltip={t('common.githubLabel')} aria-label={t('common.githubLabel')}
variant="link" tooltip={t('common.githubLabel')}
data-variant="link" variant="link"
fontSize={20} data-variant="link"
size="sm" fontSize={20}
icon={<FaGithub />} size="sm"
/> icon={<FaGithub />}
</Link> />
</Link>
)}
<Link {!disabledFeatures.includes('discordLink') && (
isExternal <Link
href="https://discord.gg/ZmtBAhwWhy" isExternal
marginBottom="-0.25rem" href="https://discord.gg/ZmtBAhwWhy"
> marginBottom="-0.25rem"
<IAIIconButton >
aria-label={t('common.discordLabel')} <IAIIconButton
tooltip={t('common.discordLabel')} aria-label={t('common.discordLabel')}
variant="link" tooltip={t('common.discordLabel')}
data-variant="link" variant="link"
fontSize={20} data-variant="link"
size="sm" fontSize={20}
icon={<FaDiscord />} size="sm"
/> icon={<FaDiscord />}
</Link> />
</Link>
)}
<SettingsModal> <SettingsModal>
<IAIIconButton <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', 'isCancelScheduled',
'sessionId', 'sessionId',
'progressImage', 'progressImage',
'wereModelsReceived',
'wasSchemaParsed',
]; ];
export const systemBlacklist = itemsToBlacklist.map( 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 { initialImageSelected } from 'features/parameters/store/generationSlice';
import { makeToast } from '../hooks/useToastWatcher'; import { makeToast } from '../hooks/useToastWatcher';
import { sessionCanceled, sessionInvoked } from 'services/thunks/session'; 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'; export type LogLevel = 'info' | 'warning' | 'error';
@ -32,10 +35,18 @@ export interface Log {
[index: number]: LogEntry; [index: number]: LogEntry;
} }
export type ReadinessPayload = { /**
isReady: boolean; * A disable-able application feature
reasonsWhyNotReady: string[]; */
}; export type ApplicationFeature =
| 'faceRestore'
| 'upscaling'
| 'lightbox'
| 'modelManager'
| 'githubLink'
| 'discordLink'
| 'bugLink'
| 'localization';
export type InProgressImageType = 'none' | 'full-res' | 'latents'; 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 * Whether or not URLs should be transformed to use a different host
*/ */
shouldTransformUrls: boolean; 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 = { const initialSystemState: SystemState = {
@ -144,6 +171,10 @@ const initialSystemState: SystemState = {
isCancelScheduled: false, isCancelScheduled: false,
subscribedNodeIds: [], subscribedNodeIds: [],
shouldTransformUrls: false, shouldTransformUrls: false,
disabledTabs: [],
disabledFeatures: [],
wereModelsReceived: false,
wasSchemaParsed: false,
}; };
export const systemSlice = createSlice({ export const systemSlice = createSlice({
@ -347,6 +378,21 @@ export const systemSlice = createSlice({
shouldTransformUrlsChanged: (state, action: PayloadAction<boolean>) => { shouldTransformUrlsChanged: (state, action: PayloadAction<boolean>) => {
state.shouldTransformUrls = action.payload; 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) { extraReducers(builder) {
/** /**
@ -417,7 +463,8 @@ export const systemSlice = createSlice({
step, step,
total_steps, total_steps,
progress_image, progress_image,
invocation, node,
source_node_id,
graph_execution_state_id, graph_execution_state_id,
} = action.payload.data; } = action.payload.data;
@ -514,6 +561,20 @@ export const systemSlice = createSlice({
builder.addCase(initialImageSelected, (state) => { builder.addCase(initialImageSelected, (state) => {
state.toastQueue.push(makeToast(i18n.t('toast.sentToImageToImage'))); 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, cancelTypeChanged,
subscribedNodeIdsSet, subscribedNodeIdsSet,
shouldTransformUrlsChanged, shouldTransformUrlsChanged,
disabledTabsChanged,
disabledFeaturesChanged,
} = systemSlice.actions; } = systemSlice.actions;
export default systemSlice.reducer; export default systemSlice.reducer;

View File

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

View File

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

View File

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

View File

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

View File

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