feat(ui): statusindicator changes

We are now using the lefthand vertical strip for the settings menu button. This is a good place for the status indicator.

Really, we only need to display something *if there is a problem*. If the app is processing, the progress bar indicates that.

For the case where the panels are collapsed, I'll add the floating buttons back in some form, and we'll indicate via those if the app is processing something.
This commit is contained in:
psychedelicious 2024-01-04 18:25:12 +11:00 committed by Kent Keirsey
parent d3b6d86e74
commit 7a57bc99cf
10 changed files with 97 additions and 132 deletions
invokeai/frontend/web/src
common/components
InvButton
InvProgress
InvTooltip
features
theme

@ -204,7 +204,7 @@ export const buttonTheme = defineStyleConfig({
_hover: { _hover: {
bg: 'none', bg: 'none',
svg: { svg: {
fill: 'base.500', fill: 'base.400',
}, },
}, },
'&[data-selected="true"]': { '&[data-selected="true"]': {

@ -1,35 +1,57 @@
import { progressAnatomy as parts } from '@chakra-ui/anatomy'; import { progressAnatomy as parts } from '@chakra-ui/anatomy';
import { import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
createMultiStyleConfigHelpers, import { generateStripe, getColorVar } from '@chakra-ui/theme-tools';
defineStyle,
} from '@chakra-ui/styled-system';
const { defineMultiStyleConfig, definePartsStyle } = const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
const invokeAIFilledTrack = defineStyle((_props) => ({
bg: 'blue.500',
}));
const invokeAITrack = defineStyle((_props) => {
return {
bg: 'base.800',
};
});
const invokeAI = definePartsStyle((props) => ({
filledTrack: invokeAIFilledTrack(props),
track: invokeAITrack(props),
}));
export const progressTheme = defineMultiStyleConfig({ export const progressTheme = defineMultiStyleConfig({
baseStyle: { baseStyle: definePartsStyle(
track: { borderRadius: '2px' }, ({ theme: t, colorScheme: c, hasStripe, isIndeterminate }) => {
}, const bgColor = `${c}.300`;
variants: { const addStripe = !isIndeterminate && hasStripe;
invokeAI, const gradient = `linear-gradient(
}, to right,
defaultProps: { transparent 0%,
variant: 'invokeAI', ${getColorVar(t, bgColor)} 50%,
}, transparent 100%
)`;
return {
track: {
borderRadius: '2px',
bg: 'base.800',
},
filledTrack: {
borderRadius: '2px',
...(addStripe && generateStripe()),
...(isIndeterminate ? { bgImage: gradient } : { bgColor }),
},
};
}
),
}); });
// export const progressTheme = defineMultiStyleConfig({
// baseStyle: definePartsStyle(
// ({ theme: t, colorScheme: c, hasStripe, isIndeterminate }) => {
// const bgColor = `${c}.500`;
// const addStripe = !isIndeterminate && hasStripe;
// const gradient = `linear-gradient(
// to right,
// transparent 0%,
// ${getColorVar(t, bgColor)} 50%,
// transparent 100%
// )`;
// return {
// track: {
// borderRadius: '2px',
// bg: 'base.800',
// },
// filledTrack: {
// borderRadius: '2px',
// ...(addStripe && generateStripe("xs")),
// ...(isIndeterminate ? { bgImage: gradient } : { bgColor }),
// },
// };
// }
// ),
// });

@ -12,6 +12,7 @@ export const InvTooltip = memo(
ref={ref} ref={ref}
hasArrow={hasArrow} hasArrow={hasArrow}
placement={placement} placement={placement}
arrowSize={8}
{...rest} {...rest}
> >
{children} {children}

@ -3,14 +3,15 @@ import { cssVar } from '@chakra-ui/theme-tools';
const $arrowBg = cssVar('popper-arrow-bg'); const $arrowBg = cssVar('popper-arrow-bg');
// define the base component styles
const baseStyle = defineStyle(() => ({ const baseStyle = defineStyle(() => ({
borderRadius: 'md', borderRadius: 'md',
shadow: 'dark-lg', shadow: 'dark-lg',
bg: 'base.200', bg: 'base.200',
color: 'base.800', color: 'base.800',
[$arrowBg.variable]: 'colors.base.200', [$arrowBg.variable]: 'colors.base.200',
pb: 1.5, pt: 1,
px: 2,
pb: 1,
})); }));
// export the component theme // export the component theme

@ -28,6 +28,7 @@ const selector = createMemoizedSelector([stateSelector], (state) => {
const { initial, min, sliderMax, inputMax, fineStep, coarseStep } = const { initial, min, sliderMax, inputMax, fineStep, coarseStep } =
state.config.sd.iterations; state.config.sd.iterations;
const { iterations } = state.generation; const { iterations } = state.generation;
const isLoadingDynamicPrompts = state.dynamicPrompts.isLoading;
return { return {
iterations, iterations,
@ -37,15 +38,14 @@ const selector = createMemoizedSelector([stateSelector], (state) => {
inputMax, inputMax,
step: coarseStep, step: coarseStep,
fineStep, fineStep,
isLoadingDynamicPrompts,
}; };
}); });
export const InvokeQueueBackButton = memo(() => { export const InvokeQueueBackButton = memo(() => {
const isLoadingDynamicPrompts = useAppSelector(
(state) => state.dynamicPrompts.isLoading
);
const { queueBack, isLoading, isDisabled } = useQueueBack(); const { queueBack, isLoading, isDisabled } = useQueueBack();
const { iterations, step, fineStep } = useAppSelector(selector); const { iterations, step, fineStep, isLoadingDynamicPrompts } =
useAppSelector(selector);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const handleChange = useCallback( const handleChange = useCallback(
(v: number) => { (v: number) => {

@ -3,7 +3,6 @@ import { InvButtonGroup } from 'common/components/InvButtonGroup/InvButtonGroup'
import ClearQueueButton from 'features/queue/components/ClearQueueButton'; import ClearQueueButton from 'features/queue/components/ClearQueueButton';
import QueueFrontButton from 'features/queue/components/QueueFrontButton'; import QueueFrontButton from 'features/queue/components/QueueFrontButton';
import ProgressBar from 'features/system/components/ProgressBar'; import ProgressBar from 'features/system/components/ProgressBar';
import StatusIndicator from 'features/system/components/StatusIndicator';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react'; import { memo } from 'react';
@ -31,10 +30,7 @@ const QueueControls = () => {
{isPauseEnabled && <PauseProcessorButton asIconButton />} */} {isPauseEnabled && <PauseProcessorButton asIconButton />} */}
<ClearQueueButton asIconButton /> <ClearQueueButton asIconButton />
</InvButtonGroup> </InvButtonGroup>
<Flex h={5} w="full" alignItems="center"> <ProgressBar />
<StatusIndicator />
<ProgressBar height={2} />
</Flex>
</Flex> </Flex>
); );
}; };

@ -16,12 +16,7 @@ const progressBarSelector = createMemoizedSelector(
} }
); );
type ProgressBarProps = { const ProgressBar = () => {
height?: number | string;
};
const ProgressBar = (props: ProgressBarProps) => {
const { height = 'full' } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const { data: queueStatus } = useGetQueueStatusQuery(); const { data: queueStatus } = useGetQueueStatusQuery();
const { hasSteps, value, isConnected } = useAppSelector(progressBarSelector); const { hasSteps, value, isConnected } = useAppSelector(progressBarSelector);
@ -30,13 +25,12 @@ const ProgressBar = (props: ProgressBarProps) => {
<Progress <Progress
value={value} value={value}
aria-label={t('accessibility.invokeProgressBar')} aria-label={t('accessibility.invokeProgressBar')}
hasStripe
isIndeterminate={ isIndeterminate={
isConnected && Boolean(queueStatus?.queue.in_progress) && !hasSteps isConnected && Boolean(queueStatus?.queue.in_progress) && !hasSteps
} }
h={height} h={2}
w="full" w="full"
colorScheme="invokeYellow" colorScheme="blue"
/> />
); );
}; };

@ -1,81 +1,28 @@
import { Flex, Icon } from '@chakra-ui/react'; import { Icon } from '@chakra-ui/react';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { stateSelector } from 'app/store/store';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { InvText } from 'common/components/InvText/wrapper';
import { InvTooltip } from 'common/components/InvTooltip/InvTooltip'; import { InvTooltip } from 'common/components/InvTooltip/InvTooltip';
import { STATUS_TRANSLATION_KEYS } from 'features/system/store/types'; import { memo } from 'react';
import type { ResourceKey } from 'i18next';
import { memo, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FaCircle } from 'react-icons/fa'; import { FaExclamationTriangle } from 'react-icons/fa';
import { useHoverDirty } from 'react-use';
import { useGetQueueStatusQuery } from 'services/api/endpoints/queue';
const statusIndicatorSelector = createMemoizedSelector(
stateSelector,
({ system }) => {
const { isConnected, status } = system;
return {
isConnected,
statusTranslationKey: STATUS_TRANSLATION_KEYS[status],
};
}
);
const COLOR_MAP = {
ok: 'invokeYellow.500',
working: 'blue.500',
error: 'red.500',
};
const StatusIndicator = () => { const StatusIndicator = () => {
const { isConnected, statusTranslationKey } = useAppSelector( const isConnected = useAppSelector((state) => state.system.isConnected);
statusIndicatorSelector
);
const { t } = useTranslation(); const { t } = useTranslation();
const ref = useRef(null);
const { data: queueStatus } = useGetQueueStatusQuery();
const statusColor = useMemo(() => { if (!isConnected) {
if (!isConnected) { return (
return 'error';
}
if (queueStatus?.queue.in_progress) {
return 'working';
}
return 'ok';
}, [queueStatus?.queue.in_progress, isConnected]);
const isHovered = useHoverDirty(ref);
return (
<Flex ref={ref} alignItems="center" p={2} pl={0}>
<InvTooltip <InvTooltip
left={10} label={t('common.statusDisconnected')}
bottom={-24} placement="end"
background="base.800" shouldWrapChildren
label={ gutter={10}
isHovered && (
<InvText
fontSize="sm"
fontWeight="semibold"
pb="1px"
userSelect="none"
color={COLOR_MAP[statusColor]}
>
{t(statusTranslationKey as ResourceKey)}
</InvText>
)
}
> >
<Icon as={FaCircle} boxSize="0.6rem" color={COLOR_MAP[statusColor]} /> <Icon as={FaExclamationTriangle} color="error.300" />
</InvTooltip> </InvTooltip>
</Flex> );
); }
return null;
}; };
export default memo(StatusIndicator); export default memo(StatusIndicator);

@ -15,6 +15,7 @@ import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup'; import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent'; import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu'; import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
import StatusIndicator from 'features/system/components/StatusIndicator';
import type { UsePanelOptions } from 'features/ui/hooks/usePanel'; import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
import { usePanel } from 'features/ui/hooks/usePanel'; import { usePanel } from 'features/ui/hooks/usePanel';
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
@ -125,17 +126,17 @@ const InvokeTabs = () => {
() => () =>
enabledTabs.map((tab) => ( enabledTabs.map((tab) => (
<InvTooltip key={tab.id} label={t(tab.translationKey)} placement="end"> <InvTooltip key={tab.id} label={t(tab.translationKey)} placement="end">
<InvTab <InvTab p={0}>
as={InvIconButton} <InvIconButton
p={0} onClick={handleClickTab}
onClick={handleClickTab} icon={tab.icon}
icon={tab.icon} size="md"
size="md" fontSize="24px"
fontSize="24px" variant="appTab"
variant="appTab" data-selected={activeTabName === tab.id}
data-selected={activeTabName === tab.id} aria-label={t(tab.translationKey)}
aria-label={t(tab.translationKey)} />
></InvTab> </InvTab>
</InvTooltip> </InvTooltip>
)), )),
[enabledTabs, t, handleClickTab, activeTabName] [enabledTabs, t, handleClickTab, activeTabName]
@ -254,13 +255,14 @@ const InvokeTabs = () => {
p={4} p={4}
isLazy isLazy
> >
<Flex flexDir="column" alignItems="center" pt={4}> <Flex flexDir="column" alignItems="center" pt={4} pb={2} gap={4}>
<InvokeAILogoComponent /> <InvokeAILogoComponent />
<InvTabList gap={4} pt={12} pb={4} h="full" flexDir="column"> <InvTabList gap={4} pt={6} h="full" flexDir="column">
{tabs} {tabs}
<Spacer />
<SettingsMenu />
</InvTabList> </InvTabList>
<Spacer />
<StatusIndicator />
<SettingsMenu />
</Flex> </Flex>
<PanelGroup <PanelGroup
ref={panelGroupRef} ref={panelGroupRef}

@ -10,3 +10,5 @@ export const spinKeyframes = keyframes`
`; `;
export const spinAnimation = `${spinKeyframes} 0.45s linear infinite`; export const spinAnimation = `${spinKeyframes} 0.45s linear infinite`;
export const spinAnimationSlow = `${spinKeyframes} 1s linear infinite`;