Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): new user experience #6892

Merged
merged 10 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions invokeai/frontend/web/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
13 changes: 13 additions & 0 deletions invokeai/frontend/web/src/common/components/InvokeLogoIcon.tsx
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -72,7 +69,7 @@ const CurrentImagePreview = () => {
isUploadDisabled={true}
fitContainer
useThumbailFallback
noContentFallback={<IAINoContentFallback icon={PiImageBold} label={t('gallery.noImageSelected')} />}
noContentFallback={<NoContentForViewer />}
dataTestId="image-preview"
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
@@ -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;
};
Loading