feat(ui): viewer integrates progress (wip)

This commit is contained in:
psychedelicious
2025-06-23 15:51:37 +10:00
parent f0ba693922
commit d23cdfd0ad
10 changed files with 158 additions and 26 deletions

View File

@@ -1,20 +1,28 @@
import { Box, Flex } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasAlertsInvocationProgress } from 'features/controlLayers/components/CanvasAlerts/CanvasAlertsInvocationProgress';
import { DndImage } from 'features/dnd/DndImage';
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
import type { ProgressImage as ProgressImageType } from 'features/nodes/types/common';
import { selectShouldShowImageDetails } from 'features/ui/store/uiSelectors';
import type { AnimationProps } from 'framer-motion';
import { AnimatePresence, motion } from 'framer-motion';
import { memo, useCallback, useRef, useState } from 'react';
import type { ImageDTO } from 'services/api/types';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import type { ImageDTO, S } from 'services/api/types';
import { $socket } from 'services/events/stores';
import { ImageMetadataMini } from './ImageMetadataMini';
import { NoContentForViewer } from './NoContentForViewer';
import { ProgressImage } from './ProgressImage2';
import { ProgressIndicator } from './ProgressIndicator2';
export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
const socket = useStore($socket);
const [progressEvent, setProgressEvent] = useState<S['InvocationProgressEvent'] | null>(null);
const [progressImage, setProgressImage] = useState<ProgressImageType | null>(null);
// Show and hide the next/prev buttons on mouse move
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
@@ -29,6 +37,34 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO })
}, 500);
}, []);
useEffect(() => {
if (!socket) {
return;
}
const onInvocationProgress = (data: S['InvocationProgressEvent']) => {
setProgressEvent(data);
if (data.image) {
setProgressImage(data.image);
}
};
socket.on('invocation_progress', onInvocationProgress);
return () => {
socket.off('invocation_progress', onInvocationProgress);
};
}, [socket]);
const onLoadImage = useCallback(() => {
if (!progressEvent || !imageDTO) {
return;
}
if (progressEvent.session_id === imageDTO.session_id) {
setProgressImage(null);
}
}, [imageDTO, progressEvent]);
return (
<Flex
onMouseOver={onMouseOver}
@@ -39,7 +75,19 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO })
justifyContent="center"
position="relative"
>
<ImageContent imageDTO={imageDTO} />
{imageDTO ? (
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center">
<DndImage imageDTO={imageDTO} onLoad={onLoadImage} />
</Flex>
) : (
<NoContentForViewer />
)}
{progressEvent && progressImage && (
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center" bg="base.900">
<ProgressImage progressImage={progressImage} />
<ProgressIndicator progressEvent={progressEvent} position="absolute" top={6} right={6} size={8} />
</Flex>
)}
<Flex flexDir="column" gap={2} position="absolute" top={0} insetInlineStart={0} alignItems="flex-start">
<CanvasAlertsInvocationProgress />
{imageDTO && <ImageMetadataMini imageName={imageDTO.image_name} />}
@@ -73,19 +121,6 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO?: ImageDTO })
});
CurrentImagePreview.displayName = 'CurrentImagePreview';
const ImageContent = memo(({ imageDTO }: { imageDTO?: ImageDTO }) => {
if (!imageDTO) {
return <NoContentForViewer />;
}
return (
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center">
<DndImage imageDTO={imageDTO} />
</Flex>
);
});
ImageContent.displayName = 'ImageContent';
const initial: AnimationProps['initial'] = {
opacity: 0,
};

View File

@@ -0,0 +1,22 @@
import { Flex } from '@invoke-ai/ui-library';
import { ProgressImage } from 'features/gallery/components/ImageViewer/ProgressImage2';
import { ProgressIndicator } from 'features/gallery/components/ImageViewer/ProgressIndicator2';
import type { ProgressImage as ProgressImageType } from 'features/nodes/types/common';
import { memo } from 'react';
import type { S } from 'services/api/types';
export const Progress = memo(
({
progressEvent,
progressImage,
}: {
progressEvent: S['InvocationProgressEvent'];
progressImage: ProgressImageType;
}) => (
<Flex position="relative" flexDir="column" w="full" h="full" overflow="hidden" p={2}>
<ProgressImage progressImage={progressImage} />
<ProgressIndicator progressEvent={progressEvent} position="absolute" top={6} right={6} size={8} />
</Flex>
)
);
Progress.displayName = 'Progress';

View File

@@ -0,0 +1,44 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Flex, Image } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import type { ProgressImage as ProgressImageType } from 'features/nodes/types/common';
import { selectSystemSlice } from 'features/system/store/systemSlice';
import { memo, useMemo } from 'react';
const selectShouldAntialiasProgressImage = createSelector(
selectSystemSlice,
(system) => system.shouldAntialiasProgressImage
);
export const ProgressImage = memo(({ progressImage }: { progressImage: ProgressImageType }) => {
const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage);
const sx = useMemo<SystemStyleObject>(
() => ({
imageRendering: shouldAntialiasProgressImage ? 'auto' : 'pixelated',
}),
[shouldAntialiasProgressImage]
);
return (
<Flex width="full" height="full" alignItems="center" justifyContent="center" minW={0} minH={0}>
<Image
src={progressImage.dataURL}
width={progressImage.width}
height={progressImage.height}
draggable={false}
data-testid="progress-image"
objectFit="contain"
maxWidth="full"
maxHeight="full"
borderRadius="base"
sx={sx}
minH={0}
minW={0}
/>
</Flex>
);
});
ProgressImage.displayName = 'ProgressImage';

View File

@@ -0,0 +1,31 @@
import type { CircularProgressProps, SystemStyleObject } from '@invoke-ai/ui-library';
import { CircularProgress, Tooltip } from '@invoke-ai/ui-library';
import { memo } from 'react';
import type { S } from 'services/api/types';
import { formatProgressMessage } from 'services/events/stores';
const circleStyles: SystemStyleObject = {
circle: {
transitionProperty: 'none',
transitionDuration: '0s',
},
};
export const ProgressIndicator = memo(
({ progressEvent, ...rest }: { progressEvent: S['InvocationProgressEvent'] } & CircularProgressProps) => {
return (
<Tooltip label={formatProgressMessage(progressEvent)}>
<CircularProgress
size="14px"
color="invokeBlue.500"
thickness={14}
isIndeterminate={!progressEvent || progressEvent.percentage === null}
value={progressEvent?.percentage ? progressEvent.percentage * 100 : undefined}
sx={circleStyles}
{...rest}
/>
</Tooltip>
);
}
);
ProgressIndicator.displayName = 'ProgressMessage';

View File

@@ -1,9 +1,9 @@
import { Flex, Text } from '@invoke-ai/ui-library';
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import type { IDockviewPanelHeaderProps } from 'dockview';
import { useCallback, useRef } from 'react';
import { memo, useCallback, useRef } from 'react';
export const TabWithoutCloseButton = (props: IDockviewPanelHeaderProps) => {
export const TabWithoutCloseButton = memo((props: IDockviewPanelHeaderProps) => {
const ref = useRef<HTMLDivElement>(null);
const setActive = useCallback(() => {
if (!props.api.isActive) {
@@ -20,5 +20,5 @@ export const TabWithoutCloseButton = (props: IDockviewPanelHeaderProps) => {
</Text>
</Flex>
);
};
});
TabWithoutCloseButton.displayName = 'TabWithoutCloseButton';

View File

@@ -2,10 +2,10 @@ import { Flex, Text } from '@invoke-ai/ui-library';
import { useCallbackOnDragEnter } from 'common/hooks/useCallbackOnDragEnter';
import type { IDockviewPanelHeaderProps } from 'dockview';
import ProgressBar from 'features/system/components/ProgressBar';
import { useCallback, useRef } from 'react';
import { memo, useCallback, useRef } from 'react';
import { useIsGenerationInProgress } from 'services/api/endpoints/queue';
export const TabWithoutCloseButtonAndWithProgressIndicator = (props: IDockviewPanelHeaderProps) => {
export const TabWithoutCloseButtonAndWithProgressIndicator = memo((props: IDockviewPanelHeaderProps) => {
const isGenerationInProgress = useIsGenerationInProgress();
const ref = useRef<HTMLDivElement>(null);
@@ -27,5 +27,5 @@ export const TabWithoutCloseButtonAndWithProgressIndicator = (props: IDockviewPa
)}
</Flex>
);
};
});
TabWithoutCloseButtonAndWithProgressIndicator.displayName = 'TabWithoutCloseButtonAndWithProgressIndicator';

View File

@@ -69,7 +69,7 @@ const initializeCenterPanelLayout = (api: DockviewApi) => {
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
tabComponent: DEFAULT_TAB_ID,
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
position: {
direction: 'within',
referencePanel: LAUNCHPAD_PANEL_ID,

View File

@@ -54,7 +54,7 @@ const initializeCenterPanelLayout = (api: DockviewApi) => {
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
tabComponent: DEFAULT_TAB_ID,
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
position: {
direction: 'within',
referencePanel: LAUNCHPAD_PANEL_ID,

View File

@@ -54,7 +54,7 @@ const initializeCenterLayout = (api: DockviewApi) => {
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
tabComponent: DEFAULT_TAB_ID,
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
position: {
direction: 'within',
referencePanel: LAUNCHPAD_PANEL_ID,

View File

@@ -67,7 +67,7 @@ const initializeCenterPanelLayout = (api: DockviewApi) => {
id: VIEWER_PANEL_ID,
component: VIEWER_PANEL_ID,
title: 'Image Viewer',
tabComponent: DEFAULT_TAB_ID,
tabComponent: TAB_WITH_PROGRESS_INDICATOR_ID,
position: {
direction: 'within',
referencePanel: LAUNCHPAD_PANEL_ID,