feat(ui): simple session initial state

This commit is contained in:
psychedelicious
2025-06-06 20:05:02 +10:00
parent 9df69496e4
commit 2531366386
16 changed files with 291 additions and 242 deletions

View File

@@ -1,6 +1,5 @@
import { useAppSelector } from 'app/store/storeHooks';
import { AdvancedSession } from 'features/controlLayers/components/AdvancedSession/AdvancedSession';
import { NoSession } from 'features/controlLayers/components/NoSession/NoSession';
import { SimpleSession } from 'features/controlLayers/components/SimpleSession/SimpleSession';
import { selectCanvasSessionId, selectCanvasSessionType } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo } from 'react';
@@ -12,11 +11,7 @@ export const CanvasMainPanelContent = memo(() => {
const id = useAppSelector(selectCanvasSessionId);
if (type === 'simple') {
if (id === null) {
return <NoSession />;
} else {
return <SimpleSession id={id} />;
}
return <SimpleSession id={id} />;
}
if (type === 'advanced') {

View File

@@ -1,68 +0,0 @@
/* eslint-disable i18next/no-literal-string */
import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { memo, useMemo } from 'react';
import { Trans } from 'react-i18next';
import { PiUploadBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
import type { Param0 } from 'tsafe';
const generateWithControlImageDndTargetData = newCanvasFromImageDndTarget.getData({
type: 'control_layer',
withResize: true,
});
export const GenerateWithControlImage = memo(() => {
const { getState, dispatch } = useAppStore();
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
() => ({
onUpload: (imageDTO: ImageDTO) => {
newCanvasFromImage({ imageDTO, type: 'control_layer', withResize: true, getState, dispatch });
},
allowMultiple: false,
}),
[dispatch, getState]
);
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
const components = useMemo(
() => ({
UploadButton: (
<Button
size="sm"
variant="link"
color="base.300"
{...uploadApi.getUploadButtonProps()}
rightIcon={<PiUploadBold />}
/>
),
}),
[uploadApi]
);
return (
<Flex position="relative" flexDir="column">
<Text fontSize="lg" fontWeight="semibold">
Generate with a Control Image
</Text>
<Text color="base.300">
Generate a new image using the control image to guide the structure and composition (Text to Image with
Control).
</Text>
<Text color="base.300">
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
<input {...uploadApi.getUploadInputProps()} />
</Text>
<DndDropTarget
dndTarget={newCanvasFromImageDndTarget}
dndTargetData={generateWithControlImageDndTargetData}
label="Drop"
/>
</Flex>
);
});
GenerateWithControlImage.displayName = 'GenerateWithControlImage';

View File

@@ -1,65 +0,0 @@
/* eslint-disable i18next/no-literal-string */
import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { memo, useMemo } from 'react';
import { Trans } from 'react-i18next';
import { PiUploadBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
import type { Param0 } from 'tsafe';
const generateWithStartingImageDndTargetData = newCanvasFromImageDndTarget.getData({
type: 'raster_layer',
withResize: true,
});
export const GenerateWithStartingImage = memo(() => {
const { getState, dispatch } = useAppStore();
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
() => ({
onUpload: (imageDTO: ImageDTO) => {
newCanvasFromImage({ imageDTO, type: 'raster_layer', withResize: true, getState, dispatch });
},
allowMultiple: false,
}),
[dispatch, getState]
);
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
const components = useMemo(
() => ({
UploadButton: (
<Button
size="sm"
variant="link"
color="base.300"
{...uploadApi.getUploadButtonProps()}
rightIcon={<PiUploadBold />}
/>
),
}),
[uploadApi]
);
return (
<Flex position="relative" flexDir="column">
<Text fontSize="lg" fontWeight="semibold">
Generate with a Starting Image
</Text>
<Text color="base.300">Regenerate the starting image using the model (Image to Image).</Text>
<Text color="base.300">
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
<input {...uploadApi.getUploadInputProps()} />
</Text>
<DndDropTarget
dndTarget={newCanvasFromImageDndTarget}
dndTargetData={generateWithStartingImageDndTargetData}
label="Drop"
/>
</Flex>
);
});
GenerateWithStartingImage.displayName = 'GenerateWithStartingImage';

View File

@@ -1,65 +0,0 @@
/* eslint-disable i18next/no-literal-string */
import { Button, Flex, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { memo, useMemo } from 'react';
import { Trans } from 'react-i18next';
import { PiUploadBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
import type { Param0 } from 'tsafe';
const generateWithStartingImageAndInpaintMaskDndTargetData = newCanvasFromImageDndTarget.getData({
type: 'raster_layer',
withInpaintMask: true,
});
export const GenerateWithStartingImageAndInpaintMask = memo(() => {
const { getState, dispatch } = useAppStore();
const useImageUploadButtonOptions = useMemo<Param0<typeof useImageUploadButton>>(
() => ({
onUpload: (imageDTO: ImageDTO) => {
newCanvasFromImage({ imageDTO, type: 'raster_layer', withInpaintMask: true, getState, dispatch });
},
allowMultiple: false,
}),
[dispatch, getState]
);
const uploadApi = useImageUploadButton(useImageUploadButtonOptions);
const components = useMemo(
() => ({
UploadButton: (
<Button
size="sm"
variant="link"
color="base.300"
{...uploadApi.getUploadButtonProps()}
rightIcon={<PiUploadBold />}
/>
),
}),
[uploadApi]
);
return (
<Flex position="relative" flexDir="column">
<Text fontSize="lg" fontWeight="semibold">
Edit Image
</Text>
<Text color="base.300">Edit the image by regenerating parts of it (Inpaint).</Text>
<Text color="base.300">
<Trans i18nKey="controlLayers.uploadOrDragAnImage" components={components} />
<input {...uploadApi.getUploadInputProps()} />
</Text>
<DndDropTarget
dndTarget={newCanvasFromImageDndTarget}
dndTargetData={generateWithStartingImageAndInpaintMaskDndTargetData}
label="Drop"
/>
</Flex>
);
});
GenerateWithStartingImageAndInpaintMask.displayName = 'GenerateWithStartingImageAndInpaintMask';

View File

@@ -1,32 +0,0 @@
/* eslint-disable i18next/no-literal-string */
import { Button, Flex, Heading, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { GenerateWithControlImage } from 'features/controlLayers/components/NoSession/GenerateWithControlImage';
import { GenerateWithStartingImage } from 'features/controlLayers/components/NoSession/GenerateWithStartingImage';
import { GenerateWithStartingImageAndInpaintMask } from 'features/controlLayers/components/NoSession/GenerateWithStartingImageAndInpaintMask';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
export const NoSession = memo(() => {
const dispatch = useAppDispatch();
const newSesh = useCallback(() => {
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
}, [dispatch]);
return (
<Flex flexDir="column" w="full" h="full" alignItems="center" justifyContent="center">
<Heading>Get Started with Invoke</Heading>
<Button variant="ghost" onClick={newSesh}>
Start a new Canvas Session
</Button>
<Text>or</Text>
<Flex flexDir="column" maxW={512}>
<GenerateWithStartingImage />
<GenerateWithControlImage />
<GenerateWithStartingImageAndInpaintMask />
</Flex>
</Flex>
);
});
NoSession.displayName = 'NoSession';

View File

@@ -0,0 +1,50 @@
/* eslint-disable i18next/no-literal-string */
import { Button, Flex, Grid, Heading, Text } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { InitialStateAddAStyleReference } from 'features/controlLayers/components/SimpleSession/InitialStateAddAStyleReference';
import { InitialStateCardGridItem } from 'features/controlLayers/components/SimpleSession/InitialStateCardGridItem';
import { InitialStateEditImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateEditImageCard';
import { InitialStateGenerateFromText } from 'features/controlLayers/components/SimpleSession/InitialStateGenerateFromText';
import { InitialStateUseALayoutImageCard } from 'features/controlLayers/components/SimpleSession/InitialStateUseALayoutImageCard';
import { canvasSessionTypeChanged } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { memo, useCallback } from 'react';
export const InitialState = memo(() => {
const dispatch = useAppDispatch();
const newCanvasSession = useCallback(() => {
dispatch(canvasSessionTypeChanged({ type: 'advanced' }));
}, [dispatch]);
return (
<Flex flexDir="column" h="full" justifyContent="center" mx={16}>
<Heading mb={4}>Choose a starting method.</Heading>
<Text fontSize="md" fontStyle="italic" mb={6}>
Drag an image onto a card or click the upload icon.
</Text>
<Grid gridTemplateColumns="1fr 1fr" gridTemplateRows="1fr 1fr" gap={4}>
<InitialStateCardGridItem>
<InitialStateGenerateFromText />
</InitialStateCardGridItem>
<InitialStateCardGridItem>
<InitialStateAddAStyleReference />
</InitialStateCardGridItem>
<InitialStateCardGridItem>
<InitialStateUseALayoutImageCard />
</InitialStateCardGridItem>
<InitialStateCardGridItem>
<InitialStateEditImageCard />
</InitialStateCardGridItem>
</Grid>
<Text fontSize="md" color="base.300" alignSelf="center" mt={6}>
or{' '}
<Button variant="link" onClick={newCanvasSession}>
start from a blank canvas.
</Button>
</Text>
</Flex>
);
});
InitialState.displayName = 'InitialState';

View File

@@ -0,0 +1,39 @@
/* eslint-disable i18next/no-literal-string */
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { memo, useCallback } from 'react';
import { PiUserCircleGearBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
const NEW_CANVAS_OPTIONS = { type: 'reference_image' } as const;
const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
export const InitialStateAddAStyleReference = memo(() => {
const { getState, dispatch } = useAppStore();
const onUpload = useCallback(
(imageDTO: ImageDTO) => {
newCanvasFromImage({ imageDTO, getState, dispatch, ...NEW_CANVAS_OPTIONS });
},
[dispatch, getState]
);
return (
<>
<Icon as={PiUserCircleGearBold} boxSize={8} color="base.500" />
<Heading size="sm">Add a Style Reference</Heading>
<Text color="base.300">Add an image to transfer its look.</Text>
<Flex w="full" justifyContent="flex-end">
<UploadImageIconButton onUpload={onUpload} variant="link" h={8} />
</Flex>
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
</>
);
});
InitialStateAddAStyleReference.displayName = 'InitialStateAddAStyleReference';

View File

@@ -0,0 +1,24 @@
import { GridItem } from '@invoke-ai/ui-library';
import { memo, type PropsWithChildren } from 'react';
export const InitialStateCardGridItem = memo((props: PropsWithChildren) => {
return (
<GridItem
display="flex"
position="relative"
flexDir="column"
alignItems="center"
borderWidth={1}
borderRadius="base"
p={2}
pt={6}
gap={2}
w="full"
h="full"
>
{props.children}
</GridItem>
);
});
InitialStateCardGridItem.displayName = 'InitialStateCardGridItem';

View File

@@ -0,0 +1,39 @@
/* eslint-disable i18next/no-literal-string */
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { memo, useCallback } from 'react';
import { PiPencilBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
const NEW_CANVAS_OPTIONS = { type: 'raster_layer', withInpaintMask: true } as const;
const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
export const InitialStateEditImageCard = memo(() => {
const { getState, dispatch } = useAppStore();
const onUpload = useCallback(
(imageDTO: ImageDTO) => {
newCanvasFromImage({ imageDTO, getState, dispatch, ...NEW_CANVAS_OPTIONS });
},
[dispatch, getState]
);
return (
<>
<Icon as={PiPencilBold} boxSize={8} color="base.500" />
<Heading size="sm">Edit Image</Heading>
<Text color="base.300">Add an image to refine.</Text>
<Flex w="full" justifyContent="flex-end">
<UploadImageIconButton onUpload={onUpload} variant="link" h={8} />
</Flex>
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
</>
);
});
InitialStateEditImageCard.displayName = 'InitialStateEditImageCard';

View File

@@ -0,0 +1,33 @@
/* eslint-disable i18next/no-literal-string */
import { Flex, Heading, Icon, IconButton, Text } from '@invoke-ai/ui-library';
import { memo } from 'react';
import { PiCursorTextBold, PiTextAaBold } from 'react-icons/pi';
const focusOnPrompt = () => {
const promptElement = document.getElementById('prompt');
if (promptElement instanceof HTMLTextAreaElement) {
promptElement.focus();
promptElement.select();
}
};
export const InitialStateGenerateFromText = memo(() => {
return (
<>
<Icon as={PiTextAaBold} boxSize={8} color="base.500" />
<Heading size="sm">Generate from Text</Heading>
<Text color="base.300">Enter a prompt and Invoke.</Text>
<Flex w="full" justifyContent="flex-end">
<IconButton
onClick={focusOnPrompt}
aria-label="Focus on prompt"
icon={<PiCursorTextBold />}
variant="link"
h={8}
/>
</Flex>
</>
);
});
InitialStateGenerateFromText.displayName = 'InitialStateGenerateFromText';

View File

@@ -0,0 +1,39 @@
/* eslint-disable i18next/no-literal-string */
import { Flex, Heading, Icon, Text } from '@invoke-ai/ui-library';
import { useAppStore } from 'app/store/nanostores/store';
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
import { newCanvasFromImageDndTarget } from 'features/dnd/dnd';
import { DndDropTarget } from 'features/dnd/DndDropTarget';
import { newCanvasFromImage } from 'features/imageActions/actions';
import { memo, useCallback } from 'react';
import { PiRectangleDashedBold } from 'react-icons/pi';
import type { ImageDTO } from 'services/api/types';
const NEW_CANVAS_OPTIONS = { type: 'control_layer', withResize: true } as const;
const dndTargetData = newCanvasFromImageDndTarget.getData(NEW_CANVAS_OPTIONS);
export const InitialStateUseALayoutImageCard = memo(() => {
const { getState, dispatch } = useAppStore();
const onUpload = useCallback(
(imageDTO: ImageDTO) => {
newCanvasFromImage({ imageDTO, getState, dispatch, ...NEW_CANVAS_OPTIONS });
},
[dispatch, getState]
);
return (
<>
<Icon as={PiRectangleDashedBold} boxSize={8} color="base.500" />
<Heading size="sm">Use a Layout Image</Heading>
<Text color="base.300">Add an image to control composition.</Text>
<Flex w="full" justifyContent="flex-end">
<UploadImageIconButton onUpload={onUpload} variant="link" h={8} />
</Flex>
<DndDropTarget dndTarget={newCanvasFromImageDndTarget} dndTargetData={dndTargetData} label="Drop" />
</>
);
});
InitialStateUseALayoutImageCard.displayName = 'InitialStateUseALayoutImageCard';

View File

@@ -1,8 +1,12 @@
import { CanvasSessionContextProvider } from 'features/controlLayers/components/SimpleSession/context';
import { InitialState } from 'features/controlLayers/components/SimpleSession/InitialState';
import { StagingArea } from 'features/controlLayers/components/SimpleSession/StagingArea';
import { memo } from 'react';
export const SimpleSession = memo(({ id }: { id: string }) => {
export const SimpleSession = memo(({ id }: { id: string | null }) => {
if (id === null) {
return <InitialState />;
}
return (
<CanvasSessionContextProvider type="simple" id={id}>
<StagingArea />

View File

@@ -0,0 +1,32 @@
import type { ButtonProps } from '@invoke-ai/ui-library';
import { Button } from '@invoke-ai/ui-library';
import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXCircle } from 'react-icons/pi';
type Props = ButtonProps;
export const DeleteAllExceptCurrentButton = memo((props: Props) => {
const { t } = useTranslation();
const deleteAllExceptCurrent = useDeleteAllExceptCurrentQueueItemDialog();
return (
<>
<Button
onClick={deleteAllExceptCurrent.openDialog}
isLoading={deleteAllExceptCurrent.isLoading}
isDisabled={deleteAllExceptCurrent.isDisabled}
tooltip={t('queue.cancelAllExceptCurrentTooltip')}
leftIcon={<PiXCircle />}
colorScheme="error"
data-testid={t('queue.clear')}
{...props}
>
{t('queue.clear')}
</Button>
</>
);
});
DeleteAllExceptCurrentButton.displayName = 'DeleteAllExceptCurrentButton';

View File

@@ -0,0 +1,25 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useDeleteAllExceptCurrentQueueItemDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiXCircle } from 'react-icons/pi';
export const DeleteAllExceptCurrentIconButton = memo(() => {
const { t } = useTranslation();
const deleteAllExceptCurrent = useDeleteAllExceptCurrentQueueItemDialog();
return (
<IconButton
size="lg"
isDisabled={deleteAllExceptCurrent.isDisabled}
isLoading={deleteAllExceptCurrent.isLoading}
aria-label={t('queue.clear')}
tooltip={t('queue.cancelAllExceptCurrentTooltip')}
icon={<PiXCircle />}
colorScheme="error"
onClick={deleteAllExceptCurrent.openDialog}
/>
);
});
DeleteAllExceptCurrentIconButton.displayName = 'DeleteAllExceptCurrentIconButton';

View File

@@ -1,5 +1,5 @@
import { Flex, Spacer, useShiftModifier } from '@invoke-ai/ui-library';
import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { DeleteAllExceptCurrentIconButton } from 'features/queue/components/DeleteAllExceptCurrentIconButton';
import { DeleteCurrentQueueItemIconButton } from 'features/queue/components/DeleteCurrentQueueItemIconButton';
import { QueueActionsMenuButton } from 'features/queue/components/QueueActionsMenuButton';
import ProgressBar from 'features/system/components/ProgressBar';
@@ -30,7 +30,7 @@ export const DeleteIconButton = memo(() => {
return <DeleteCurrentQueueItemIconButton />;
}
return <DeleteAllExceptCurrentQueueItemConfirmationAlertDialog />;
return <DeleteAllExceptCurrentIconButton />;
});
DeleteIconButton.displayName = 'DeleteIconButton';

View File

@@ -1,6 +1,5 @@
/* eslint-disable i18next/no-literal-string */
import { ButtonGroup, Flex } from '@invoke-ai/ui-library';
import { DeleteAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/DeleteAllExceptCurrentQueueItemConfirmationAlertDialog';
import { DeleteAllExceptCurrentButton } from 'features/queue/components/DeleteAllExceptCurrentButton';
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
import { memo } from 'react';
@@ -24,7 +23,7 @@ const QueueTabQueueControls = () => {
)}
<ButtonGroup w={28} orientation="vertical" size="sm">
<PruneQueueButton />
<DeleteAllExceptCurrentQueueItemConfirmationAlertDialog />
<DeleteAllExceptCurrentButton />
</ButtonGroup>
</Flex>
<ClearModelCacheButton />