mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): new user experience (#6892)
* wip * more updates for new user experience * pull whats new out * use loading state * lint * fix(ui): translation missing period * feat(ui): create icon component for invoke logo * feat(ui): tweaked invoke logo colors --------- Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local> Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
This commit is contained in:
@@ -2074,6 +2074,10 @@
|
||||
},
|
||||
"showSendingToAlerts": "Alert When Sending to Different View"
|
||||
},
|
||||
"newUserExperience": {
|
||||
"toGetStarted": "To get started, enter a prompt in the box and click <StrongComponent>Invoke</StrongComponent> to generate your first image. You can choose to save your images directly to the <StrongComponent>Gallery</StrongComponent> or edit them to the <StrongComponent>Canvas</StrongComponent>.",
|
||||
"gettingStartedSeries": "Want more guidance? Check out our <LinkComponent>Getting Started Series</LinkComponent> for tips on unlocking the full potential of the Invoke Studio."
|
||||
},
|
||||
"whatsNew": {
|
||||
"whatsNewInInvoke": "What's New in Invoke",
|
||||
"canvasV2Announcement": {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { IconProps } from '@invoke-ai/ui-library';
|
||||
import { Icon } from '@invoke-ai/ui-library';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const InvokeLogoIcon = memo((props: IconProps) => {
|
||||
return (
|
||||
<Icon boxSize={8} opacity={1} stroke="base.500" viewBox="0 0 66 66" fill="none" {...props}>
|
||||
<path d="M43.9137 16H63.1211V3H3.12109V16H22.3285L43.9137 50H63.1211V63H3.12109V50H22.3285" strokeWidth="5" />
|
||||
</Icon>
|
||||
);
|
||||
});
|
||||
|
||||
InvokeLogoIcon.displayName = 'InvokeLogoIcon';
|
||||
@@ -3,7 +3,6 @@ import { useStore } from '@nanostores/react';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import type { TypesafeDraggableData } from 'features/dnd/types';
|
||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
||||
@@ -12,15 +11,13 @@ import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from '
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImageBold } from 'react-icons/pi';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { $hasProgress, $isProgressFromCanvas } from 'services/events/stores';
|
||||
|
||||
import { NoContentForViewer } from './NoContentForViewer';
|
||||
import ProgressImage from './ProgressImage';
|
||||
|
||||
const CurrentImagePreview = () => {
|
||||
const { t } = useTranslation();
|
||||
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
|
||||
const imageName = useAppSelector(selectLastSelectedImageName);
|
||||
const hasDenoiseProgress = useStore($hasProgress);
|
||||
@@ -72,7 +69,7 @@ const CurrentImagePreview = () => {
|
||||
isUploadDisabled={true}
|
||||
fitContainer
|
||||
useThumbailFallback
|
||||
noContentFallback={<IAINoContentFallback icon={PiImageBold} label={t('gallery.noImageSelected')} />}
|
||||
noContentFallback={<NoContentForViewer />}
|
||||
dataTestId="image-preview"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Flex, Spinner, Text } from '@invoke-ai/ui-library';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { InvokeLogoIcon } from 'common/components/InvokeLogoIcon';
|
||||
import { LOADING_SYMBOL, useHasImages } from 'features/gallery/hooks/useHasImages';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { PiImageBold } from 'react-icons/pi';
|
||||
|
||||
export const NoContentForViewer = () => {
|
||||
const hasImages = useHasImages();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (hasImages === LOADING_SYMBOL) {
|
||||
return (
|
||||
// Blank bg w/ a spinner. The new user experience components below have an invoke logo, but it's not centered.
|
||||
// If we show the logo while loading, there is an awkward layout shift where the invoke logo moves a bit. Less
|
||||
// jarring to show a blank bg with a spinner - it will only be shown for a moment as we do the initial images
|
||||
// fetching.
|
||||
<Flex position="relative" width="full" height="full" alignItems="center" justifyContent="center">
|
||||
<Spinner label="Loading" color="grey" position="absolute" size="sm" width={8} height={8} right={4} bottom={4} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasImages) {
|
||||
return <IAINoContentFallback icon={PiImageBold} label={t('gallery.noImageSelected')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" gap={4} alignItems="center" textAlign="center" maxW="600px">
|
||||
<InvokeLogoIcon w={40} h={40} />
|
||||
<Text fontSize="md" color="base.200" pt={16}>
|
||||
<Trans
|
||||
i18nKey="newUserExperience.toGetStarted"
|
||||
components={{
|
||||
StrongComponent: <Text as="span" color="white" fontSize="md" fontWeight="semibold" />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Text fontSize="md" color="base.200">
|
||||
<Trans
|
||||
i18nKey="newUserExperience.gettingStartedSeries"
|
||||
components={{
|
||||
LinkComponent: (
|
||||
<Text
|
||||
as="a"
|
||||
color="white"
|
||||
fontSize="md"
|
||||
fontWeight="semibold"
|
||||
href="https://www.youtube.com/@invokeai/videos"
|
||||
target="_blank"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||
|
||||
export const LOADING_SYMBOL = Symbol('LOADING');
|
||||
|
||||
export const useHasImages = () => {
|
||||
const { data: boardList, isLoading: loadingBoards } = useListAllBoardsQuery({ include_archived: true });
|
||||
const { data: uncategorizedImages, isLoading: loadingImages } = useListImagesQuery({
|
||||
board_id: 'none',
|
||||
offset: 0,
|
||||
limit: 0,
|
||||
is_intermediate: false,
|
||||
});
|
||||
|
||||
const hasImages = useMemo(() => {
|
||||
// default to true
|
||||
if (loadingBoards || loadingImages) {
|
||||
return LOADING_SYMBOL;
|
||||
}
|
||||
|
||||
const hasBoards = boardList && boardList.length > 0;
|
||||
|
||||
if (hasBoards) {
|
||||
if (boardList.filter((board) => board.image_count > 0).length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return uncategorizedImages ? uncategorizedImages.total > 0 : true;
|
||||
}, [boardList, uncategorizedImages, loadingBoards, loadingImages]);
|
||||
|
||||
return hasImages;
|
||||
};
|
||||
Reference in New Issue
Block a user