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 (
isESRGANAvailable &&
!shouldDisableToolbarButtons &&
isConnected &&
!isProcessing &&
upscalingLevel
) {
handleClickUpscale(); handleClickUpscale();
} else { },
toast({ {
title: t('toast.upscalingFailed'), enabled: () =>
status: 'error', disabledFeatures.includes('upscaling') ||
duration: 2500, !isESRGANAvailable ||
isClosable: true, shouldDisableToolbarButtons ||
}); !isConnected ||
} isProcessing ||
!upscalingLevel,
}, },
[ [
disabledFeatures,
selectedImage, selectedImage,
isESRGANAvailable, isESRGANAvailable,
shouldDisableToolbarButtons, shouldDisableToolbarButtons,
@ -362,24 +362,20 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
useHotkeys( useHotkeys(
'Shift+R', 'Shift+R',
() => { () => {
if (
isGFPGANAvailable &&
!shouldDisableToolbarButtons &&
isConnected &&
!isProcessing &&
facetoolStrength
) {
handleClickFixFaces(); 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,6 +505,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
isChecked={shouldHidePreview} isChecked={shouldHidePreview}
onClick={handlePreviewVisibility} onClick={handlePreviewVisibility}
/> />
{!disabledFeatures.includes('lightbox') && (
<IAIIconButton <IAIIconButton
icon={<FaExpand />} icon={<FaExpand />}
tooltip={ tooltip={
@ -524,6 +521,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
isChecked={isLightboxOpen} isChecked={isLightboxOpen}
onClick={handleLightBox} onClick={handleLightBox}
/> />
)}
</ButtonGroup> </ButtonGroup>
<ButtonGroup isAttached={true}> <ButtonGroup isAttached={true}>
@ -556,7 +554,12 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
/> />
</ButtonGroup> </ButtonGroup>
{!(
disabledFeatures.includes('faceRestore') &&
disabledFeatures.includes('upscaling')
) && (
<ButtonGroup isAttached={true}> <ButtonGroup isAttached={true}>
{!disabledFeatures.includes('faceRestore') && (
<IAIPopover <IAIPopover
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
@ -585,7 +588,9 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
</IAIButton> </IAIButton>
</Flex> </Flex>
</IAIPopover> </IAIPopover>
)}
{!disabledFeatures.includes('upscaling') && (
<IAIPopover <IAIPopover
triggerComponent={ triggerComponent={
<IAIIconButton <IAIIconButton
@ -614,7 +619,9 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => {
</IAIButton> </IAIButton>
</Flex> </Flex>
</IAIPopover> </IAIPopover>
)}
</ButtonGroup> </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>
{!disabledFeatures.includes('lightbox') && (
<MenuItem onClickCapture={handleLightBox}> <MenuItem onClickCapture={handleLightBox}>
{t('parameters.openInViewer')} {t('parameters.openInViewer')}
</MenuItem> </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,21 +2,25 @@ 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(
[uiSelector, systemSelector],
(uiSlice, system) => {
const { const {
activeTab, activeTab,
openLinearAccordionItems, openLinearAccordionItems,
openUnifiedCanvasAccordionItems, openUnifiedCanvasAccordionItems,
disabledParameterPanels,
} = uiSlice; } = uiSlice;
const { disabledFeatures } = system;
let openAccordions: number[] = []; let openAccordions: number[] = [];
if (tabMap[activeTab] === 'linear') { if (tabMap[activeTab] === 'linear') {
@ -29,9 +33,10 @@ const parametersAccordionSelector = createSelector([uiSelector], (uiSlice) => {
return { return {
openAccordions, openAccordions,
disabledParameterPanels, disabledFeatures,
}; };
}); }
);
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,
(item) => disabledParameterPanels.indexOf(item.name) === -1
);
return filteredAccordionItems.map((accordionItem) => (
<InvokeAccordionItem <InvokeAccordionItem
key={accordionItem.name} key={accordionItem.name}
accordionItem={accordionItem} accordionItem={accordionItem}
/> />
)); )),
}, [disabledParameterPanels, accordionItems]); [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,6 +23,7 @@ const SiteHeaderMenu = () => {
flexDirection={{ base: 'column', xl: 'row' }} flexDirection={{ base: 'column', xl: 'row' }}
gap={{ base: 4, xl: 1 }} gap={{ base: 4, xl: 1 }}
> >
{!disabledFeatures.includes('modelManager') && (
<ModelManagerModal> <ModelManagerModal>
<IAIIconButton <IAIIconButton
aria-label={t('modelManager.modelManager')} aria-label={t('modelManager.modelManager')}
@ -29,6 +35,7 @@ const SiteHeaderMenu = () => {
icon={<FaCube />} icon={<FaCube />}
/> />
</ModelManagerModal> </ModelManagerModal>
)}
<HotkeysModal> <HotkeysModal>
<IAIIconButton <IAIIconButton
@ -44,8 +51,9 @@ const SiteHeaderMenu = () => {
<ThemeChanger /> <ThemeChanger />
<LanguagePicker /> {!disabledFeatures.includes('localization') && <LanguagePicker />}
{!disabledFeatures.includes('bugLink') && (
<Link <Link
isExternal isExternal
href="http://github.com/invoke-ai/InvokeAI/issues" href="http://github.com/invoke-ai/InvokeAI/issues"
@ -61,7 +69,9 @@ const SiteHeaderMenu = () => {
icon={<FaBug />} icon={<FaBug />}
/> />
</Link> </Link>
)}
{!disabledFeatures.includes('githubLink') && (
<Link <Link
isExternal isExternal
href="http://github.com/invoke-ai/InvokeAI" href="http://github.com/invoke-ai/InvokeAI"
@ -77,7 +87,9 @@ const SiteHeaderMenu = () => {
icon={<FaGithub />} icon={<FaGithub />}
/> />
</Link> </Link>
)}
{!disabledFeatures.includes('discordLink') && (
<Link <Link
isExternal isExternal
href="https://discord.gg/ZmtBAhwWhy" href="https://discord.gg/ZmtBAhwWhy"
@ -93,6 +105,7 @@ const SiteHeaderMenu = () => {
icon={<FaDiscord />} 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;
} }
); );