[WebUI] Add Image To Image UI

This commit is contained in:
blessedcoolant 2022-10-06 18:42:26 +13:00 committed by Lincoln Stein
parent 5157cbeda1
commit 3b0c4b74b6
38 changed files with 1705 additions and 1066 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

483
frontend/dist/assets/index.dd3155db.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>InvokeAI - A Stable Diffusion Toolkit</title> <title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="shortcut icon" type="icon" href="/assets/favicon.0d253ced.ico" /> <link rel="shortcut icon" type="icon" href="/assets/favicon.0d253ced.ico" />
<script type="module" crossorigin src="/assets/index.a0198006.js"></script> <script type="module" crossorigin src="/assets/index.dd3155db.js"></script>
<link rel="stylesheet" href="/assets/index.60ca0ee5.css"> <link rel="stylesheet" href="/assets/index.a0250964.css">
</head> </head>
<body> <body>

View File

@ -6,6 +6,7 @@ import {
addLogEntry, addLogEntry,
setIsProcessing, setIsProcessing,
} from '../../features/system/systemSlice'; } from '../../features/system/systemSlice';
import { tabMap, tab_dict } from '../../features/tabs/InvokeTabs';
import * as InvokeAI from '../invokeai'; import * as InvokeAI from '../invokeai';
/** /**
@ -23,8 +24,14 @@ const makeSocketIOEmitters = (
emitGenerateImage: () => { emitGenerateImage: () => {
dispatch(setIsProcessing(true)); dispatch(setIsProcessing(true));
const options = { ...getState().options };
if (tabMap[options.activeTab] === 'txt2img') {
options.shouldUseInitImage = false;
}
const { generationParameters, esrganParameters, gfpganParameters } = const { generationParameters, esrganParameters, gfpganParameters } =
frontendToBackendParameters(getState().options, getState().system); frontendToBackendParameters(options, getState().system);
socketio.emit( socketio.emit(
'generateImage', 'generateImage',

View File

@ -0,0 +1,65 @@
import { Button, useToast } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import { FileRejection } from 'react-dropzone';
import { useAppDispatch } from '../../app/store';
import ImageUploader from '../../features/options/ImageUploader';
interface InvokeImageUploaderProps {
label?: string;
icon?: any;
onMouseOver?: any;
OnMouseout?: any;
dispatcher: any;
styleClass?: string;
}
export default function InvokeImageUploader(props: InvokeImageUploaderProps) {
const { label, icon, dispatcher, styleClass, onMouseOver, OnMouseout } =
props;
const toast = useToast();
const dispatch = useAppDispatch();
// Callbacks to for handling file upload attempts
const fileAcceptedCallback = useCallback(
(file: File) => dispatch(dispatcher(file)),
[dispatch, dispatcher]
);
const fileRejectionCallback = useCallback(
(rejection: FileRejection) => {
const msg = rejection.errors.reduce(
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
''
);
toast({
title: 'Upload failed',
description: msg,
status: 'error',
isClosable: true,
});
},
[toast]
);
return (
<ImageUploader
fileAcceptedCallback={fileAcceptedCallback}
fileRejectionCallback={fileRejectionCallback}
styleClass={styleClass}
>
<Button
size={'sm'}
fontSize={'md'}
fontWeight={'normal'}
onMouseOver={onMouseOver}
onMouseOut={OnMouseout}
leftIcon={icon}
width={'100%'}
>
{label ? label : null}
</Button>
</ImageUploader>
);
}

View File

@ -18,6 +18,7 @@ export const optionsSelector = createSelector(
maskPath: options.maskPath, maskPath: options.maskPath,
initialImagePath: options.initialImagePath, initialImagePath: options.initialImagePath,
seed: options.seed, seed: options.seed,
activeTab: options.activeTab,
}; };
}, },
{ {
@ -55,6 +56,7 @@ const useCheckParameters = (): boolean => {
maskPath, maskPath,
initialImagePath, initialImagePath,
seed, seed,
activeTab,
} = useAppSelector(optionsSelector); } = useAppSelector(optionsSelector);
const { isProcessing, isConnected } = useAppSelector(systemSelector); const { isProcessing, isConnected } = useAppSelector(systemSelector);
@ -65,6 +67,10 @@ const useCheckParameters = (): boolean => {
return false; return false;
} }
if (prompt && !initialImagePath && activeTab === 1) {
return false;
}
// Cannot generate with a mask without img2img // Cannot generate with a mask without img2img
if (maskPath && !initialImagePath) { if (maskPath && !initialImagePath) {
return false; return false;

View File

@ -6,9 +6,11 @@ import * as InvokeAI from '../../app/invokeai';
import { useAppDispatch, useAppSelector } from '../../app/store'; import { useAppDispatch, useAppSelector } from '../../app/store';
import { RootState } from '../../app/store'; import { RootState } from '../../app/store';
import { import {
setActiveTab,
setAllParameters, setAllParameters,
setInitialImagePath, setInitialImagePath,
setSeed, setSeed,
setShouldShowImageDetails,
} from '../options/optionsSlice'; } from '../options/optionsSlice';
import DeleteImageModal from './DeleteImageModal'; import DeleteImageModal from './DeleteImageModal';
import { SystemState } from '../system/systemSlice'; import { SystemState } from '../system/systemSlice';
@ -41,21 +43,19 @@ const systemSelector = createSelector(
type CurrentImageButtonsProps = { type CurrentImageButtonsProps = {
image: InvokeAI.Image; image: InvokeAI.Image;
shouldShowImageDetails: boolean;
setShouldShowImageDetails: (b: boolean) => void;
}; };
/** /**
* Row of buttons for common actions: * Row of buttons for common actions:
* Use as init image, use all params, use seed, upscale, fix faces, details, delete. * Use as init image, use all params, use seed, upscale, fix faces, details, delete.
*/ */
const CurrentImageButtons = ({ const CurrentImageButtons = ({ image }: CurrentImageButtonsProps) => {
image,
shouldShowImageDetails,
setShouldShowImageDetails,
}: CurrentImageButtonsProps) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const shouldShowImageDetails = useAppSelector(
(state: RootState) => state.options.shouldShowImageDetails
);
const toast = useToast(); const toast = useToast();
const intermediateImage = useAppSelector( const intermediateImage = useAppSelector(
@ -73,8 +73,11 @@ const CurrentImageButtons = ({
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } = const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
useAppSelector(systemSelector); useAppSelector(systemSelector);
const handleClickUseAsInitialImage = () => const handleClickUseAsInitialImage = () => {
dispatch(setInitialImagePath(image.url)); dispatch(setInitialImagePath(image.url));
dispatch(setActiveTab(1));
};
useHotkeys( useHotkeys(
'shift+i', 'shift+i',
() => { () => {
@ -215,7 +218,8 @@ const CurrentImageButtons = ({
); );
const handleClickShowImageDetails = () => const handleClickShowImageDetails = () =>
setShouldShowImageDetails(!shouldShowImageDetails); dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
useHotkeys( useHotkeys(
'i', 'i',
() => { () => {
@ -237,8 +241,8 @@ const CurrentImageButtons = ({
<div className="current-image-options"> <div className="current-image-options">
<IAIIconButton <IAIIconButton
icon={<MdImage />} icon={<MdImage />}
tooltip="Use As Initial Image" tooltip="Send To Image To Image"
aria-label="Use As Initial Image" aria-label="Send To Image To Image"
onClick={handleClickUseAsInitialImage} onClick={handleClickUseAsInitialImage}
/> />

View File

@ -27,7 +27,6 @@
} }
.current-image-tools { .current-image-tools {
grid-area: current-image-tools;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: grid; display: grid;
@ -58,34 +57,37 @@
align-items: center; align-items: center;
display: grid; display: grid;
width: 100%; width: 100%;
grid-template-areas: 'current-image-content';
img { img {
grid-area: current-image-content;
background-color: var(--img2img-img-bg-color);
border-radius: 0.5rem; border-radius: 0.5rem;
object-fit: contain; object-fit: contain;
width: auto; width: auto;
height: $app-gallery-height;
max-height: $app-gallery-height; max-height: $app-gallery-height;
} }
} }
.current-image-metadata {
grid-area: current-image-preview;
}
.current-image-next-prev-buttons { .current-image-next-prev-buttons {
position: absolute; grid-area: current-image-content;
top: 0;
left: 0;
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
width: calc(100% - 2rem);
padding: 0.5rem;
margin-left: 1rem;
z-index: 1; z-index: 1;
height: calc($app-metadata-height - 1rem); height: 100%;
pointer-events: none; pointer-events: none;
} }
.next-prev-button-trigger-area { .next-prev-button-trigger-area {
width: 7rem; width: 7rem;
height: 100%; height: 100%;
display: flex; width: 100%;
display: grid;
align-items: center; align-items: center;
pointer-events: auto; pointer-events: auto;
@ -99,31 +101,8 @@
} }
.next-prev-button { .next-prev-button {
font-size: 5rem; font-size: 4rem;
fill: var(--text-color-secondary); fill: var(--white);
filter: drop-shadow(0 0 1rem var(--text-color-secondary)); filter: drop-shadow(0 0 1rem var(--text-color-secondary));
opacity: 70%; opacity: 70%;
} }
.current-image-metadata-viewer {
border-radius: 0.5rem;
position: absolute;
top: 0;
left: 0;
width: calc(100% - 2rem);
padding: 0.5rem;
margin-left: 1rem;
background-color: var(--metadata-bg-color);
z-index: 1;
overflow: scroll;
height: calc($app-metadata-height - 1rem);
}
.current-image-json-viewer {
border-radius: 0.5rem;
margin: 0 0.5rem 1rem 0.5rem;
padding: 1rem;
overflow-x: scroll;
word-break: break-all;
background-color: var(--metadata-json-bg-color);
}

View File

@ -1,100 +1,35 @@
import { IconButton, Image } from '@chakra-ui/react'; import { RootState, useAppSelector } from '../../app/store';
import { useAppDispatch, useAppSelector } from '../../app/store';
import { RootState } from '../../app/store';
import { useState } from 'react';
import ImageMetadataViewer from './ImageMetadataViewer';
import CurrentImageButtons from './CurrentImageButtons'; import CurrentImageButtons from './CurrentImageButtons';
import { MdPhoto } from 'react-icons/md'; import { MdPhoto } from 'react-icons/md';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa'; import CurrentImagePreview from './CurrentImagePreview';
import { selectNextImage, selectPrevImage } from './gallerySlice'; import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
/** /**
* Displays the current image if there is one, plus associated actions. * Displays the current image if there is one, plus associated actions.
*/ */
const CurrentImageDisplay = () => { const CurrentImageDisplay = () => {
const dispatch = useAppDispatch();
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState<boolean>(false);
const { currentImage, intermediateImage } = useAppSelector( const { currentImage, intermediateImage } = useAppSelector(
(state: RootState) => state.gallery (state: RootState) => state.gallery
); );
const [shouldShowImageDetails, setShouldShowImageDetails] = const shouldShowImageDetails = useAppSelector(
useState<boolean>(false); (state: RootState) => state.options.shouldShowImageDetails
);
const imageToDisplay = intermediateImage || currentImage; const imageToDisplay = intermediateImage || currentImage;
const handleCurrentImagePreviewMouseOver = () => {
setShouldShowNextPrevButtons(true);
};
const handleCurrentImagePreviewMouseOut = () => {
setShouldShowNextPrevButtons(false);
};
const handleClickPrevButton = () => {
dispatch(selectPrevImage());
};
const handleClickNextButton = () => {
dispatch(selectNextImage());
};
return imageToDisplay ? ( return imageToDisplay ? (
<div className="current-image-display"> <div className="current-image-display">
<div className="current-image-tools"> <div className="current-image-tools">
<CurrentImageButtons <CurrentImageButtons image={imageToDisplay} />
</div>
<CurrentImagePreview imageToDisplay={imageToDisplay} />
{shouldShowImageDetails && (
<ImageMetadataViewer
image={imageToDisplay} image={imageToDisplay}
shouldShowImageDetails={shouldShowImageDetails} styleClass="current-image-metadata"
setShouldShowImageDetails={setShouldShowImageDetails}
/> />
</div> )}
<div className="current-image-preview">
<Image
src={imageToDisplay.url}
fit="contain"
maxWidth={'100%'}
maxHeight={'100%'}
/>
{shouldShowImageDetails && (
<div className="current-image-metadata-viewer">
<ImageMetadataViewer image={imageToDisplay} />
</div>
)}
{!shouldShowImageDetails && (
<div className="current-image-next-prev-buttons">
<div
className="next-prev-button-trigger-area prev-button-trigger-area"
onMouseOver={handleCurrentImagePreviewMouseOver}
onMouseOut={handleCurrentImagePreviewMouseOut}
>
{shouldShowNextPrevButtons && (
<IconButton
aria-label="Previous image"
icon={<FaAngleLeft className="next-prev-button" />}
variant="unstyled"
onClick={handleClickPrevButton}
/>
)}
</div>
<div
className="next-prev-button-trigger-area next-button-trigger-area"
onMouseOver={handleCurrentImagePreviewMouseOver}
onMouseOut={handleCurrentImagePreviewMouseOut}
>
{shouldShowNextPrevButtons && (
<IconButton
aria-label="Next image"
icon={<FaAngleRight className="next-prev-button" />}
variant="unstyled"
onClick={handleClickNextButton}
/>
)}
</div>
</div>
)}
</div>
</div> </div>
) : ( ) : (
<div className="current-image-display-placeholder"> <div className="current-image-display-placeholder">

View File

@ -0,0 +1,105 @@
import { IconButton, Image } from '@chakra-ui/react';
import React, { useState } from 'react';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
import { GalleryState, selectNextImage, selectPrevImage } from './gallerySlice';
import * as InvokeAI from '../../app/invokeai';
import { createSelector } from '@reduxjs/toolkit';
import _ from 'lodash';
const imagesSelector = createSelector(
(state: RootState) => state.gallery,
(gallery: GalleryState) => {
const currentImageIndex = gallery.images.findIndex(
(i) => i.uuid === gallery?.currentImage?.uuid
);
const imagesLength = gallery.images.length;
return {
isOnFirstImage: currentImageIndex === 0,
isOnLastImage:
!isNaN(currentImageIndex) && currentImageIndex === imagesLength - 1,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
interface CurrentImagePreviewProps {
imageToDisplay: InvokeAI.Image;
}
export default function CurrentImagePreview(props: CurrentImagePreviewProps) {
const { imageToDisplay } = props;
const dispatch = useAppDispatch();
const { isOnFirstImage, isOnLastImage } = useAppSelector(imagesSelector);
const shouldShowImageDetails = useAppSelector(
(state: RootState) => state.options.shouldShowImageDetails
);
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState<boolean>(false);
const handleCurrentImagePreviewMouseOver = () => {
setShouldShowNextPrevButtons(true);
};
const handleCurrentImagePreviewMouseOut = () => {
setShouldShowNextPrevButtons(false);
};
const handleClickPrevButton = () => {
dispatch(selectPrevImage());
};
const handleClickNextButton = () => {
dispatch(selectNextImage());
};
return (
<div className="current-image-preview">
<Image
src={imageToDisplay.url}
fit="contain"
maxWidth={'100%'}
maxHeight={'100%'}
/>
{!shouldShowImageDetails && (
<div className="current-image-next-prev-buttons">
<div
className="next-prev-button-trigger-area prev-button-trigger-area"
onMouseOver={handleCurrentImagePreviewMouseOver}
onMouseOut={handleCurrentImagePreviewMouseOut}
>
{shouldShowNextPrevButtons && !isOnFirstImage && (
<IconButton
aria-label="Previous image"
icon={<FaAngleLeft className="next-prev-button" />}
variant="unstyled"
onClick={handleClickPrevButton}
/>
)}
</div>
<div
className="next-prev-button-trigger-area next-button-trigger-area"
onMouseOver={handleCurrentImagePreviewMouseOver}
onMouseOut={handleCurrentImagePreviewMouseOut}
>
{shouldShowNextPrevButtons && !isOnLastImage && (
<IconButton
aria-label="Next image"
icon={<FaAngleRight className="next-prev-button" />}
variant="unstyled"
onClick={handleClickNextButton}
/>
)}
</div>
</div>
)}
</div>
);
}

View File

@ -7,12 +7,17 @@ import {
Tooltip, Tooltip,
useColorModeValue, useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useAppDispatch } from '../../app/store'; import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
import { setCurrentImage } from './gallerySlice'; import { setCurrentImage } from './gallerySlice';
import { FaCheck, FaSeedling, FaTrashAlt } from 'react-icons/fa'; import { FaCheck, FaImage, FaSeedling, FaTrashAlt } from 'react-icons/fa';
import DeleteImageModal from './DeleteImageModal'; import DeleteImageModal from './DeleteImageModal';
import { memo, SyntheticEvent, useState } from 'react'; import { memo, SyntheticEvent, useState } from 'react';
import { setAllParameters, setSeed } from '../options/optionsSlice'; import {
setActiveTab,
setAllParameters,
setInitialImagePath,
setSeed,
} from '../options/optionsSlice';
import * as InvokeAI from '../../app/invokeai'; import * as InvokeAI from '../../app/invokeai';
import { IoArrowUndoCircleOutline } from 'react-icons/io5'; import { IoArrowUndoCircleOutline } from 'react-icons/io5';
@ -33,6 +38,10 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const [isHovered, setIsHovered] = useState<boolean>(false); const [isHovered, setIsHovered] = useState<boolean>(false);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const activeTab = useAppSelector(
(state: RootState) => state.options.activeTab
);
const checkColor = useColorModeValue('green.600', 'green.300'); const checkColor = useColorModeValue('green.600', 'green.300');
const bgColor = useColorModeValue('gray.200', 'gray.700'); const bgColor = useColorModeValue('gray.200', 'gray.700');
const bgGradient = useColorModeValue( const bgGradient = useColorModeValue(
@ -56,6 +65,14 @@ const HoverableImage = memo((props: HoverableImageProps) => {
dispatch(setSeed(image.metadata.image.seed)); dispatch(setSeed(image.metadata.image.seed));
}; };
const handleSetInitImage = (e: SyntheticEvent) => {
e.stopPropagation();
dispatch(setInitialImagePath(image.url));
if (activeTab !== 1) {
dispatch(setActiveTab(1));
}
};
const handleClickImage = () => dispatch(setCurrentImage(image)); const handleClickImage = () => dispatch(setCurrentImage(image));
return ( return (
@ -131,6 +148,16 @@ const HoverableImage = memo((props: HoverableImageProps) => {
/> />
</Tooltip> </Tooltip>
)} )}
<Tooltip label="Send To Image To Image">
<IconButton
aria-label="Send To Image To Image"
icon={<FaImage />}
size="xs"
fontSize={16}
variant={'imageHoverIconButton'}
onClickCapture={handleSetInitImage}
/>
</Tooltip>
</Flex> </Flex>
)} )}
</Flex> </Flex>

View File

@ -0,0 +1,20 @@
@use '../../../styles/Mixins/' as *;
.image-metadata-viewer {
width: 100%;
border-radius: 0.5rem;
padding: 1rem;
background-color: var(--metadata-bg-color);
overflow: scroll;
max-height: calc($app-content-height - 4rem);
z-index: 1;
}
.image-json-viewer {
border-radius: 0.5rem;
margin: 0 0.5rem 1rem 0.5rem;
padding: 1rem;
overflow-x: scroll;
word-break: break-all;
background-color: var(--metadata-json-bg-color);
}

View File

@ -0,0 +1,360 @@
import {
Center,
Flex,
Heading,
IconButton,
Link,
Text,
Tooltip,
} from '@chakra-ui/react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { memo } from 'react';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { useAppDispatch } from '../../../app/store';
import * as InvokeAI from '../../../app/invokeai';
import {
setCfgScale,
setGfpganStrength,
setHeight,
setImg2imgStrength,
setInitialImagePath,
setMaskPath,
setPrompt,
setSampler,
setSeed,
setSeedWeights,
setShouldFitToWidthHeight,
setSteps,
setUpscalingLevel,
setUpscalingStrength,
setWidth,
} from '../../options/optionsSlice';
import promptToString from '../../../common/util/promptToString';
import { seedWeightsToString } from '../../../common/util/seedWeightPairs';
import { FaCopy } from 'react-icons/fa';
type MetadataItemProps = {
isLink?: boolean;
label: string;
onClick?: () => void;
value: number | string | boolean;
labelPosition?: string;
};
/**
* Component to display an individual metadata item or parameter.
*/
const MetadataItem = ({
label,
value,
onClick,
isLink,
labelPosition,
}: MetadataItemProps) => {
return (
<Flex gap={2}>
{onClick && (
<Tooltip label={`Recall ${label}`}>
<IconButton
aria-label="Use this parameter"
icon={<IoArrowUndoCircleOutline />}
size={'xs'}
variant={'ghost'}
fontSize={20}
onClick={onClick}
/>
</Tooltip>
)}
<Flex direction={labelPosition ? 'column' : 'row'}>
<Text fontWeight={'semibold'} whiteSpace={'pre-wrap'} pr={2}>
{label}:
</Text>
{isLink ? (
<Link href={value.toString()} isExternal wordBreak={'break-all'}>
{value.toString()} <ExternalLinkIcon mx="2px" />
</Link>
) : (
<Text overflowY={'scroll'} wordBreak={'break-all'}>
{value.toString()}
</Text>
)}
</Flex>
</Flex>
);
};
type ImageMetadataViewerProps = {
image: InvokeAI.Image;
styleClass?: string;
};
// TODO: I don't know if this is needed.
const memoEqualityCheck = (
prev: ImageMetadataViewerProps,
next: ImageMetadataViewerProps
) => prev.image.uuid === next.image.uuid;
// TODO: Show more interesting information in this component.
/**
* Image metadata viewer overlays currently selected image and provides
* access to any of its metadata for use in processing.
*/
const ImageMetadataViewer = memo(
({ image, styleClass }: ImageMetadataViewerProps) => {
const dispatch = useAppDispatch();
// const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
const metadata = image?.metadata?.image || {};
const {
type,
postprocessing,
sampler,
prompt,
seed,
variations,
steps,
cfg_scale,
seamless,
width,
height,
strength,
fit,
init_image_path,
mask_image_path,
orig_path,
scale,
} = metadata;
const metadataJSON = JSON.stringify(metadata, null, 2);
return (
<div className={`image-metadata-viewer ${styleClass}`}>
<Flex gap={1} direction={'column'} width={'100%'}>
<Flex gap={2}>
<Text fontWeight={'semibold'}>File:</Text>
<Link href={image.url} isExternal>
{image.url}
<ExternalLinkIcon mx="2px" />
</Link>
</Flex>
{Object.keys(metadata).length > 0 ? (
<>
{type && <MetadataItem label="Generation type" value={type} />}
{['esrgan', 'gfpgan'].includes(type) && (
<MetadataItem label="Original image" value={orig_path} />
)}
{type === 'gfpgan' && strength !== undefined && (
<MetadataItem
label="Fix faces strength"
value={strength}
onClick={() => dispatch(setGfpganStrength(strength))}
/>
)}
{type === 'esrgan' && scale !== undefined && (
<MetadataItem
label="Upscaling scale"
value={scale}
onClick={() => dispatch(setUpscalingLevel(scale))}
/>
)}
{type === 'esrgan' && strength !== undefined && (
<MetadataItem
label="Upscaling strength"
value={strength}
onClick={() => dispatch(setUpscalingStrength(strength))}
/>
)}
{prompt && (
<MetadataItem
label="Prompt"
labelPosition="top"
value={promptToString(prompt)}
onClick={() => dispatch(setPrompt(prompt))}
/>
)}
{seed !== undefined && (
<MetadataItem
label="Seed"
value={seed}
onClick={() => dispatch(setSeed(seed))}
/>
)}
{sampler && (
<MetadataItem
label="Sampler"
value={sampler}
onClick={() => dispatch(setSampler(sampler))}
/>
)}
{steps && (
<MetadataItem
label="Steps"
value={steps}
onClick={() => dispatch(setSteps(steps))}
/>
)}
{cfg_scale !== undefined && (
<MetadataItem
label="CFG scale"
value={cfg_scale}
onClick={() => dispatch(setCfgScale(cfg_scale))}
/>
)}
{variations && variations.length > 0 && (
<MetadataItem
label="Seed-weight pairs"
value={seedWeightsToString(variations)}
onClick={() =>
dispatch(setSeedWeights(seedWeightsToString(variations)))
}
/>
)}
{seamless && (
<MetadataItem
label="Seamless"
value={seamless}
onClick={() => dispatch(setWidth(seamless))}
/>
)}
{width && (
<MetadataItem
label="Width"
value={width}
onClick={() => dispatch(setWidth(width))}
/>
)}
{height && (
<MetadataItem
label="Height"
value={height}
onClick={() => dispatch(setHeight(height))}
/>
)}
{init_image_path && (
<MetadataItem
label="Initial image"
value={init_image_path}
isLink
onClick={() => dispatch(setInitialImagePath(init_image_path))}
/>
)}
{mask_image_path && (
<MetadataItem
label="Mask image"
value={mask_image_path}
isLink
onClick={() => dispatch(setMaskPath(mask_image_path))}
/>
)}
{type === 'img2img' && strength && (
<MetadataItem
label="Image to image strength"
value={strength}
onClick={() => dispatch(setImg2imgStrength(strength))}
/>
)}
{fit && (
<MetadataItem
label="Image to image fit"
value={fit}
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
/>
)}
{postprocessing && postprocessing.length > 0 && (
<>
<Heading size={'sm'}>Postprocessing</Heading>
{postprocessing.map(
(
postprocess: InvokeAI.PostProcessedImageMetadata,
i: number
) => {
if (postprocess.type === 'esrgan') {
const { scale, strength } = postprocess;
return (
<Flex
key={i}
pl={'2rem'}
gap={1}
direction={'column'}
>
<Text size={'md'}>{`${
i + 1
}: Upscale (ESRGAN)`}</Text>
<MetadataItem
label="Scale"
value={scale}
onClick={() => dispatch(setUpscalingLevel(scale))}
/>
<MetadataItem
label="Strength"
value={strength}
onClick={() =>
dispatch(setUpscalingStrength(strength))
}
/>
</Flex>
);
} else if (postprocess.type === 'gfpgan') {
const { strength } = postprocess;
return (
<Flex
key={i}
pl={'2rem'}
gap={1}
direction={'column'}
>
<Text size={'md'}>{`${
i + 1
}: Face restoration (GFPGAN)`}</Text>
<MetadataItem
label="Strength"
value={strength}
onClick={() =>
dispatch(setGfpganStrength(strength))
}
/>
</Flex>
);
}
}
)}
</>
)}
<Flex gap={2} direction={'column'}>
<Flex gap={2}>
<Tooltip label={`Copy metadata JSON`}>
<IconButton
aria-label="Copy metadata JSON"
icon={<FaCopy />}
size={'xs'}
variant={'ghost'}
fontSize={14}
onClick={() =>
navigator.clipboard.writeText(metadataJSON)
}
/>
</Tooltip>
<Text fontWeight={'semibold'}>Metadata JSON:</Text>
</Flex>
<div className={'image-json-viewer'}>
<pre>{metadataJSON}</pre>
</div>
</Flex>
</>
) : (
<Center width={'100%'} pt={10}>
<Text fontSize={'lg'} fontWeight="semibold">
No metadata available
</Text>
</Center>
)}
</Flex>
</div>
);
},
memoEqualityCheck
);
export default ImageMetadataViewer;

View File

@ -1,338 +0,0 @@
import {
Center,
Flex,
Heading,
IconButton,
Link,
Text,
Tooltip,
} from '@chakra-ui/react';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import { memo } from 'react';
import { IoArrowUndoCircleOutline } from 'react-icons/io5';
import { useAppDispatch } from '../../app/store';
import * as InvokeAI from '../../app/invokeai';
import {
setCfgScale,
setGfpganStrength,
setHeight,
setImg2imgStrength,
setInitialImagePath,
setMaskPath,
setPrompt,
setSampler,
setSeed,
setSeedWeights,
setShouldFitToWidthHeight,
setSteps,
setUpscalingLevel,
setUpscalingStrength,
setWidth,
} from '../options/optionsSlice';
import promptToString from '../../common/util/promptToString';
import { seedWeightsToString } from '../../common/util/seedWeightPairs';
import { FaCopy } from 'react-icons/fa';
type MetadataItemProps = {
isLink?: boolean;
label: string;
onClick?: () => void;
value: number | string | boolean;
labelPosition?: string;
};
/**
* Component to display an individual metadata item or parameter.
*/
const MetadataItem = ({
label,
value,
onClick,
isLink,
labelPosition,
}: MetadataItemProps) => {
return (
<Flex gap={2}>
{onClick && (
<Tooltip label={`Recall ${label}`}>
<IconButton
aria-label="Use this parameter"
icon={<IoArrowUndoCircleOutline />}
size={'xs'}
variant={'ghost'}
fontSize={20}
onClick={onClick}
/>
</Tooltip>
)}
<Flex direction={labelPosition ? 'column' : 'row'}>
<Text fontWeight={'semibold'} whiteSpace={'nowrap'} pr={2}>
{label}:
</Text>
{isLink ? (
<Link href={value.toString()} isExternal wordBreak={'break-all'}>
{value.toString()} <ExternalLinkIcon mx="2px" />
</Link>
) : (
<Text overflowY={'scroll'} wordBreak={'break-all'}>
{value.toString()}
</Text>
)}
</Flex>
</Flex>
);
};
type ImageMetadataViewerProps = {
image: InvokeAI.Image;
};
// TODO: I don't know if this is needed.
const memoEqualityCheck = (
prev: ImageMetadataViewerProps,
next: ImageMetadataViewerProps
) => prev.image.uuid === next.image.uuid;
// TODO: Show more interesting information in this component.
/**
* Image metadata viewer overlays currently selected image and provides
* access to any of its metadata for use in processing.
*/
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
const dispatch = useAppDispatch();
// const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
const metadata = image?.metadata?.image || {};
const {
type,
postprocessing,
sampler,
prompt,
seed,
variations,
steps,
cfg_scale,
seamless,
width,
height,
strength,
fit,
init_image_path,
mask_image_path,
orig_path,
scale,
} = metadata;
const metadataJSON = JSON.stringify(metadata, null, 2);
return (
<Flex gap={1} direction={'column'} width={'100%'}>
<Flex gap={2}>
<Text fontWeight={'semibold'}>File:</Text>
<Link href={image.url} isExternal>
{image.url}
<ExternalLinkIcon mx="2px" />
</Link>
</Flex>
{Object.keys(metadata).length > 0 ? (
<>
{type && <MetadataItem label="Generation type" value={type} />}
{['esrgan', 'gfpgan'].includes(type) && (
<MetadataItem label="Original image" value={orig_path} />
)}
{type === 'gfpgan' && strength !== undefined && (
<MetadataItem
label="Fix faces strength"
value={strength}
onClick={() => dispatch(setGfpganStrength(strength))}
/>
)}
{type === 'esrgan' && scale !== undefined && (
<MetadataItem
label="Upscaling scale"
value={scale}
onClick={() => dispatch(setUpscalingLevel(scale))}
/>
)}
{type === 'esrgan' && strength !== undefined && (
<MetadataItem
label="Upscaling strength"
value={strength}
onClick={() => dispatch(setUpscalingStrength(strength))}
/>
)}
{prompt && (
<MetadataItem
label="Prompt"
labelPosition="top"
value={promptToString(prompt)}
onClick={() => dispatch(setPrompt(prompt))}
/>
)}
{seed !== undefined && (
<MetadataItem
label="Seed"
value={seed}
onClick={() => dispatch(setSeed(seed))}
/>
)}
{sampler && (
<MetadataItem
label="Sampler"
value={sampler}
onClick={() => dispatch(setSampler(sampler))}
/>
)}
{steps && (
<MetadataItem
label="Steps"
value={steps}
onClick={() => dispatch(setSteps(steps))}
/>
)}
{cfg_scale !== undefined && (
<MetadataItem
label="CFG scale"
value={cfg_scale}
onClick={() => dispatch(setCfgScale(cfg_scale))}
/>
)}
{variations && variations.length > 0 && (
<MetadataItem
label="Seed-weight pairs"
value={seedWeightsToString(variations)}
onClick={() =>
dispatch(setSeedWeights(seedWeightsToString(variations)))
}
/>
)}
{seamless && (
<MetadataItem
label="Seamless"
value={seamless}
onClick={() => dispatch(setWidth(seamless))}
/>
)}
{width && (
<MetadataItem
label="Width"
value={width}
onClick={() => dispatch(setWidth(width))}
/>
)}
{height && (
<MetadataItem
label="Height"
value={height}
onClick={() => dispatch(setHeight(height))}
/>
)}
{init_image_path && (
<MetadataItem
label="Initial image"
value={init_image_path}
isLink
onClick={() => dispatch(setInitialImagePath(init_image_path))}
/>
)}
{mask_image_path && (
<MetadataItem
label="Mask image"
value={mask_image_path}
isLink
onClick={() => dispatch(setMaskPath(mask_image_path))}
/>
)}
{type === 'img2img' && strength && (
<MetadataItem
label="Image to image strength"
value={strength}
onClick={() => dispatch(setImg2imgStrength(strength))}
/>
)}
{fit && (
<MetadataItem
label="Image to image fit"
value={fit}
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
/>
)}
{postprocessing && postprocessing.length > 0 && (
<>
<Heading size={'sm'}>Postprocessing</Heading>
{postprocessing.map(
(
postprocess: InvokeAI.PostProcessedImageMetadata,
i: number
) => {
if (postprocess.type === 'esrgan') {
const { scale, strength } = postprocess;
return (
<Flex key={i} pl={'2rem'} gap={1} direction={'column'}>
<Text size={'md'}>{`${i + 1}: Upscale (ESRGAN)`}</Text>
<MetadataItem
label="Scale"
value={scale}
onClick={() => dispatch(setUpscalingLevel(scale))}
/>
<MetadataItem
label="Strength"
value={strength}
onClick={() =>
dispatch(setUpscalingStrength(strength))
}
/>
</Flex>
);
} else if (postprocess.type === 'gfpgan') {
const { strength } = postprocess;
return (
<Flex key={i} pl={'2rem'} gap={1} direction={'column'}>
<Text size={'md'}>{`${
i + 1
}: Face restoration (GFPGAN)`}</Text>
<MetadataItem
label="Strength"
value={strength}
onClick={() => dispatch(setGfpganStrength(strength))}
/>
</Flex>
);
}
}
)}
</>
)}
<Flex gap={2} direction={'column'}>
<Flex gap={2}>
<Tooltip label={`Copy metadata JSON`}>
<IconButton
aria-label="Copy metadata JSON"
icon={<FaCopy />}
size={'xs'}
variant={'ghost'}
fontSize={14}
onClick={() => navigator.clipboard.writeText(metadataJSON)}
/>
</Tooltip>
<Text fontWeight={'semibold'}>Metadata JSON:</Text>
</Flex>
<div className={'current-image-json-viewer'}>
<pre>{metadataJSON}</pre>
</div>
</Flex>
</>
) : (
<Center width={'100%'} pt={10}>
<Text fontSize={'lg'} fontWeight="semibold">
No metadata available
</Text>
</Center>
)}
</Flex>
);
}, memoEqualityCheck);
export default ImageMetadataViewer;

View File

@ -8,7 +8,7 @@ import React, { ReactElement } from 'react';
import { Feature } from '../../../app/features'; import { Feature } from '../../../app/features';
import GuideIcon from '../../../common/components/GuideIcon'; import GuideIcon from '../../../common/components/GuideIcon';
interface InvokeAccordionItemProps { export interface InvokeAccordionItemProps {
header: ReactElement; header: ReactElement;
feature: Feature; feature: Feature;
options: ReactElement; options: ReactElement;

View File

@ -19,7 +19,7 @@ export default function ImageFit() {
return ( return (
<IAISwitch <IAISwitch
label="Fit initial image to output size" label="Fit Initial Image To Output Size"
isChecked={shouldFitToWidthHeight} isChecked={shouldFitToWidthHeight}
onChange={handleChangeFit} onChange={handleChangeFit}
/> />

View File

@ -8,7 +8,7 @@ import {
import IAISwitch from '../../../../common/components/IAISwitch'; import IAISwitch from '../../../../common/components/IAISwitch';
import { setShouldUseInitImage } from '../../optionsSlice'; import { setShouldUseInitImage } from '../../optionsSlice';
export default function ImageToImage() { export default function ImageToImageAccordion() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const initialImagePath = useAppSelector( const initialImagePath = useAppSelector(

View File

@ -7,7 +7,13 @@ import {
import IAINumberInput from '../../../../common/components/IAINumberInput'; import IAINumberInput from '../../../../common/components/IAINumberInput';
import { setImg2imgStrength } from '../../optionsSlice'; import { setImg2imgStrength } from '../../optionsSlice';
export default function ImageToImageStrength() { interface ImageToImageStrengthProps {
label?: string;
styleClass?: string;
}
export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
const { label = 'Strength', styleClass } = props;
const img2imgStrength = useAppSelector( const img2imgStrength = useAppSelector(
(state: RootState) => state.options.img2imgStrength (state: RootState) => state.options.img2imgStrength
); );
@ -18,7 +24,7 @@ export default function ImageToImageStrength() {
return ( return (
<IAINumberInput <IAINumberInput
label="Strength" label={label}
step={0.01} step={0.01}
min={0.01} min={0.01}
max={0.99} max={0.99}
@ -26,6 +32,7 @@ export default function ImageToImageStrength() {
value={img2imgStrength} value={img2imgStrength}
width="90px" width="90px"
isInteger={false} isInteger={false}
styleClass={styleClass}
/> />
); );
} }

View File

@ -15,6 +15,8 @@ type ImageUploaderProps = {
* Callback to handle a file being rejected. * Callback to handle a file being rejected.
*/ */
fileRejectionCallback: (rejection: FileRejection) => void; fileRejectionCallback: (rejection: FileRejection) => void;
// Styling
styleClass?: string;
}; };
/** /**
@ -25,6 +27,7 @@ const ImageUploader = ({
children, children,
fileAcceptedCallback, fileAcceptedCallback,
fileRejectionCallback, fileRejectionCallback,
styleClass,
}: ImageUploaderProps) => { }: ImageUploaderProps) => {
const onDrop = useCallback( const onDrop = useCallback(
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => { (acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
@ -52,7 +55,7 @@ const ImageUploader = ({
}; };
return ( return (
<Box {...getRootProps()} flexGrow={3}> <Box {...getRootProps()} flexGrow={3} className={`${styleClass}`}>
<input {...getInputProps({ multiple: false })} /> <input {...getInputProps({ multiple: false })} />
{cloneElement(children, { {cloneElement(children, {
onClick: handleClickUploadIcon, onClick: handleClickUploadIcon,

View File

@ -1,4 +1,3 @@
import MainAdvancedOptions from './MainAdvancedOptions';
import MainCFGScale from './MainCFGScale'; import MainCFGScale from './MainCFGScale';
import MainHeight from './MainHeight'; import MainHeight from './MainHeight';
import MainIterations from './MainIterations'; import MainIterations from './MainIterations';
@ -23,7 +22,6 @@ export default function MainOptions() {
<MainHeight /> <MainHeight />
<MainSampler /> <MainSampler />
</div> </div>
<MainAdvancedOptions />
</div> </div>
</div> </div>
); );

View File

@ -1,34 +1,25 @@
import { import { Accordion, ExpandedIndex } from '@chakra-ui/react';
Box,
Accordion,
ExpandedIndex,
// ExpandedIndex,
} from '@chakra-ui/react';
// import { RootState } from '../../app/store';
// import { useAppDispatch, useAppSelector } from '../../app/store';
// import { setOpenAccordions } from '../system/systemSlice';
import OutputOptions from './OutputOptions';
import ImageToImageOptions from './AdvancedOptions/ImageToImage/ImageToImageOptions';
import { Feature } from '../../app/features';
import SeedOptions from './AdvancedOptions/Seed/SeedOptions';
import Upscale from './AdvancedOptions/Upscale/Upscale';
import UpscaleOptions from './AdvancedOptions/Upscale/UpscaleOptions';
import FaceRestore from './AdvancedOptions/FaceRestore/FaceRestore';
import FaceRestoreOptions from './AdvancedOptions/FaceRestore/FaceRestoreOptions';
import ImageToImage from './AdvancedOptions/ImageToImage/ImageToImage';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store'; import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
import { setOpenAccordions } from '../system/systemSlice'; import { setOpenAccordions } from '../system/systemSlice';
import InvokeAccordionItem from './AccordionItems/InvokeAccordionItem'; import InvokeAccordionItem, {
import Variations from './AdvancedOptions/Variations/Variations'; InvokeAccordionItemProps,
import VariationsOptions from './AdvancedOptions/Variations/VariationsOptions'; } from './AccordionItems/InvokeAccordionItem';
import { ReactElement } from 'react';
type OptionsAccordionType = {
[optionAccordionKey: string]: InvokeAccordionItemProps;
};
type OptionAccordionsType = {
accordionInfo: OptionsAccordionType;
};
/** /**
* Main container for generation and processing parameters. * Main container for generation and processing parameters.
*/ */
const OptionsAccordion = () => { const OptionsAccordion = (props: OptionAccordionsType) => {
const { accordionInfo } = props;
const openAccordions = useAppSelector( const openAccordions = useAppSelector(
(state: RootState) => state.system.openAccordions (state: RootState) => state.system.openAccordions
); );
@ -41,6 +32,23 @@ const OptionsAccordion = () => {
const handleChangeAccordionState = (openAccordions: ExpandedIndex) => const handleChangeAccordionState = (openAccordions: ExpandedIndex) =>
dispatch(setOpenAccordions(openAccordions)); dispatch(setOpenAccordions(openAccordions));
const renderAccordions = () => {
const accordionsToRender: ReactElement[] = [];
if (accordionInfo) {
Object.keys(accordionInfo).forEach((key) => {
accordionsToRender.push(
<InvokeAccordionItem
key={key}
header={accordionInfo[key as keyof typeof accordionInfo].header}
feature={accordionInfo[key as keyof typeof accordionInfo].feature}
options={accordionInfo[key as keyof typeof accordionInfo].options}
/>
);
});
}
return accordionsToRender;
};
return ( return (
<Accordion <Accordion
defaultIndex={openAccordions} defaultIndex={openAccordions}
@ -49,49 +57,7 @@ const OptionsAccordion = () => {
onChange={handleChangeAccordionState} onChange={handleChangeAccordionState}
className="advanced-settings" className="advanced-settings"
> >
<InvokeAccordionItem {renderAccordions()}
header={
<Box flex="1" textAlign="left">
Seed
</Box>
}
feature={Feature.SEED}
options={<SeedOptions />}
/>
<InvokeAccordionItem
header={<Variations />}
feature={Feature.VARIATIONS}
options={<VariationsOptions />}
/>
<InvokeAccordionItem
header={<FaceRestore />}
feature={Feature.FACE_CORRECTION}
options={<FaceRestoreOptions />}
/>
<InvokeAccordionItem
header={<Upscale />}
feature={Feature.UPSCALE}
options={<UpscaleOptions />}
/>
<InvokeAccordionItem
header={<ImageToImage />}
feature={Feature.IMAGE_TO_IMAGE}
options={<ImageToImageOptions />}
/>
<InvokeAccordionItem
header={
<Box flex="1" textAlign="left">
Other
</Box>
}
feature={Feature.OTHER}
options={<OutputOptions />}
/>
</Accordion> </Accordion>
); );
}; };

View File

@ -22,7 +22,7 @@ export interface OptionsState {
upscalingLevel: UpscalingLevel; upscalingLevel: UpscalingLevel;
upscalingStrength: number; upscalingStrength: number;
shouldUseInitImage: boolean; shouldUseInitImage: boolean;
initialImagePath: string; initialImagePath: string | null;
maskPath: string; maskPath: string;
seamless: boolean; seamless: boolean;
shouldFitToWidthHeight: boolean; shouldFitToWidthHeight: boolean;
@ -33,6 +33,8 @@ export interface OptionsState {
shouldRunGFPGAN: boolean; shouldRunGFPGAN: boolean;
shouldRandomizeSeed: boolean; shouldRandomizeSeed: boolean;
showAdvancedOptions: boolean; showAdvancedOptions: boolean;
activeTab: number;
shouldShowImageDetails: boolean;
} }
const initialOptionsState: OptionsState = { const initialOptionsState: OptionsState = {
@ -49,7 +51,7 @@ const initialOptionsState: OptionsState = {
seamless: false, seamless: false,
shouldUseInitImage: false, shouldUseInitImage: false,
img2imgStrength: 0.75, img2imgStrength: 0.75,
initialImagePath: '', initialImagePath: null,
maskPath: '', maskPath: '',
shouldFitToWidthHeight: true, shouldFitToWidthHeight: true,
shouldGenerateVariations: false, shouldGenerateVariations: false,
@ -62,6 +64,8 @@ const initialOptionsState: OptionsState = {
gfpganStrength: 0.8, gfpganStrength: 0.8,
shouldRandomizeSeed: true, shouldRandomizeSeed: true,
showAdvancedOptions: true, showAdvancedOptions: true,
activeTab: 0,
shouldShowImageDetails: false,
}; };
const initialState: OptionsState = initialOptionsState; const initialState: OptionsState = initialOptionsState;
@ -121,7 +125,7 @@ export const optionsSlice = createSlice({
setShouldUseInitImage: (state, action: PayloadAction<boolean>) => { setShouldUseInitImage: (state, action: PayloadAction<boolean>) => {
state.shouldUseInitImage = action.payload; state.shouldUseInitImage = action.payload;
}, },
setInitialImagePath: (state, action: PayloadAction<string>) => { setInitialImagePath: (state, action: PayloadAction<string | null>) => {
const newInitialImagePath = action.payload; const newInitialImagePath = action.payload;
state.shouldUseInitImage = newInitialImagePath ? true : false; state.shouldUseInitImage = newInitialImagePath ? true : false;
state.initialImagePath = newInitialImagePath; state.initialImagePath = newInitialImagePath;
@ -269,6 +273,12 @@ export const optionsSlice = createSlice({
setShowAdvancedOptions: (state, action: PayloadAction<boolean>) => { setShowAdvancedOptions: (state, action: PayloadAction<boolean>) => {
state.showAdvancedOptions = action.payload; state.showAdvancedOptions = action.payload;
}, },
setActiveTab: (state, action: PayloadAction<number>) => {
state.activeTab = action.payload;
},
setShouldShowImageDetails: (state, action: PayloadAction<boolean>) => {
state.shouldShowImageDetails = action.payload;
},
}, },
}); });
@ -303,6 +313,8 @@ export const {
setShouldRunESRGAN, setShouldRunESRGAN,
setShouldRandomizeSeed, setShouldRandomizeSeed,
setShowAdvancedOptions, setShowAdvancedOptions,
setActiveTab,
setShouldShowImageDetails,
} = optionsSlice.actions; } = optionsSlice.actions;
export default optionsSlice.reducer; export default optionsSlice.reducer;

View File

@ -61,6 +61,16 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
desc: 'Display the next image in the gallery', desc: 'Display the next image in the gallery',
hotkey: 'Arrow right', hotkey: 'Arrow right',
}, },
{
title: 'Change Tabs',
desc: 'Switch to another workspace',
hotkey: '1-6',
},
{
title: 'Theme Toggle',
desc: 'Switch between dark and light modes',
hotkey: 'Shift+D',
},
]; ];
const renderHotkeyModalItems = () => { const renderHotkeyModalItems = () => {

View File

@ -1,4 +1,5 @@
import { IconButton, Link, useColorMode } from '@chakra-ui/react'; import { IconButton, Link, useColorMode } from '@chakra-ui/react';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaSun, FaMoon, FaGithub } from 'react-icons/fa'; import { FaSun, FaMoon, FaGithub } from 'react-icons/fa';
import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md'; import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md';
@ -15,6 +16,14 @@ import StatusIndicator from './StatusIndicator';
const SiteHeader = () => { const SiteHeader = () => {
const { colorMode, toggleColorMode } = useColorMode(); const { colorMode, toggleColorMode } = useColorMode();
useHotkeys(
'shift+d',
() => {
toggleColorMode();
},
[colorMode, toggleColorMode]
);
const colorModeIcon = colorMode == 'light' ? <FaMoon /> : <FaSun />; const colorModeIcon = colorMode == 'light' ? <FaMoon /> : <FaSun />;
// Make FaMoon and FaSun icon apparent size consistent // Make FaMoon and FaSun icon apparent size consistent

View File

@ -0,0 +1,132 @@
@use '../../../styles/Mixins/' as *;
.image-to-image-workarea {
display: grid;
grid-template-columns: max-content auto max-content;
column-gap: 1rem;
}
.image-to-image-panel {
display: grid;
row-gap: 1rem;
grid-auto-rows: max-content;
width: $options-bar-max-width;
height: $app-content-height;
overflow-y: scroll;
@include HideScrollbar;
}
.image-to-image-strength-main-option {
display: grid;
grid-template-columns: none !important;
.number-input-entry {
padding: 0 1rem;
}
}
.image-to-image-display {
border-radius: 0.5rem;
background-color: var(--background-color-secondary);
display: grid;
.current-image-options {
grid-auto-columns: max-content;
justify-self: center;
align-self: start;
}
}
.image-to-image-single-preview {
display: grid;
column-gap: 0.5rem;
padding: 0 1rem;
place-content: center;
}
.image-to-image-dual-preview-container {
display: grid;
grid-template-areas: 'img2img-preview';
}
.image-to-image-dual-preview {
grid-area: img2img-preview;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 0.5rem;
padding: 0 1rem;
place-content: center;
.current-image-preview {
img {
height: calc($app-gallery-height - 2rem);
max-height: calc($app-gallery-height - 2rem);
}
}
}
.img2img-metadata {
grid-area: img2img-preview;
z-index: 3;
}
.init-image-preview {
display: grid;
grid-template-areas: 'init-image-content';
justify-content: center;
align-items: center;
border-radius: 0.5rem;
.init-image-preview-header {
grid-area: init-image-content;
z-index: 2;
display: grid;
grid-template-columns: auto max-content;
height: max-content;
align-items: center;
align-self: start;
padding: 1rem;
border-radius: 0.5rem;
h1 {
padding: 0.2rem 0.6rem;
border-radius: 0.4rem;
background-color: var(--tab-hover-color);
width: max-content;
font-weight: bold;
font-size: 0.85rem;
}
}
.init-image-image {
grid-area: init-image-content;
img {
border-radius: 0.5rem;
object-fit: contain;
background-color: var(--img2img-img-bg-color);
width: auto;
height: calc($app-gallery-height - 2rem);
max-height: calc($app-gallery-height - 2rem);
}
}
}
.image-to-image-upload-btn {
display: grid;
width: 100%;
height: $app-content-height;
button {
overflow: hidden;
width: 100%;
height: 100%;
font-size: 1.5rem;
color: var(--text-color-secondary);
background-color: var(--background-color-secondary);
&:hover {
background-color: var(--img2img-img-bg-color);
}
}
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import ImageGallery from '../../gallery/ImageGallery';
import ImageToImageDisplay from './ImageToImageDisplay';
import ImageToImagePanel from './ImageToImagePanel';
export default function ImageToImage() {
return (
<div className="image-to-image-workarea">
<ImageToImagePanel />
<ImageToImageDisplay />
<ImageGallery />
</div>
);
}

View File

@ -0,0 +1,74 @@
import React from 'react';
import { FaUpload } from 'react-icons/fa';
import { uploadInitialImage } from '../../../app/socketio/actions';
import { RootState, useAppSelector } from '../../../app/store';
import InvokeImageUploader from '../../../common/components/InvokeImageUploader';
import CurrentImageButtons from '../../gallery/CurrentImageButtons';
import CurrentImagePreview from '../../gallery/CurrentImagePreview';
import ImageMetadataViewer from '../../gallery/ImageMetaDataViewer/ImageMetadataViewer';
import InitImagePreview from './InitImagePreview';
export default function ImageToImageDisplay() {
const initialImagePath = useAppSelector(
(state: RootState) => state.options.initialImagePath
);
const { currentImage, intermediateImage } = useAppSelector(
(state: RootState) => state.gallery
);
const shouldShowImageDetails = useAppSelector(
(state: RootState) => state.options.shouldShowImageDetails
);
const imageToDisplay = intermediateImage || currentImage;
return (
<div
className="image-to-image-display"
style={
imageToDisplay
? { gridAutoRows: 'max-content auto' }
: { gridAutoRows: 'auto' }
}
>
{initialImagePath ? (
<>
{imageToDisplay ? (
<>
<CurrentImageButtons image={imageToDisplay} />
<div className="image-to-image-dual-preview-container">
<div className="image-to-image-dual-preview">
<InitImagePreview />
<div className="image-to-image-current-image-display">
<CurrentImagePreview imageToDisplay={imageToDisplay} />
</div>
</div>
{shouldShowImageDetails && (
<ImageMetadataViewer
image={imageToDisplay}
styleClass="img2img-metadata"
/>
)}
</div>
</>
) : (
<div className="image-to-image-single-preview">
<InitImagePreview />
</div>
)}
</>
) : (
<div className="upload-image">
<InvokeImageUploader
label="Upload or Drop Image Here"
icon={<FaUpload />}
styleClass="image-to-image-upload-btn"
dispatcher={uploadInitialImage}
/>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,78 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
import { Feature } from '../../../app/features';
import { RootState, useAppSelector } from '../../../app/store';
import FaceRestore from '../../options/AdvancedOptions/FaceRestore/FaceRestore';
import FaceRestoreOptions from '../../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
import ImageFit from '../../options/AdvancedOptions/ImageToImage/ImageFit';
import ImageToImageStrength from '../../options/AdvancedOptions/ImageToImage/ImageToImageStrength';
import SeedOptions from '../../options/AdvancedOptions/Seed/SeedOptions';
import Upscale from '../../options/AdvancedOptions/Upscale/Upscale';
import UpscaleOptions from '../../options/AdvancedOptions/Upscale/UpscaleOptions';
import Variations from '../../options/AdvancedOptions/Variations/Variations';
import VariationsOptions from '../../options/AdvancedOptions/Variations/VariationsOptions';
import MainAdvancedOptions from '../../options/MainOptions/MainAdvancedOptions';
import MainOptions from '../../options/MainOptions/MainOptions';
import OptionsAccordion from '../../options/OptionsAccordion';
import OutputOptions from '../../options/OutputOptions';
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
import PromptInput from '../../options/PromptInput/PromptInput';
export default function ImageToImagePanel() {
const showAdvancedOptions = useAppSelector(
(state: RootState) => state.options.showAdvancedOptions
);
const imageToImageAccordions = {
seed: {
header: (
<Box flex="1" textAlign="left">
Seed
</Box>
),
feature: Feature.SEED,
options: <SeedOptions />,
},
variations: {
header: <Variations />,
feature: Feature.VARIATIONS,
options: <VariationsOptions />,
},
face_restore: {
header: <FaceRestore />,
feature: Feature.FACE_CORRECTION,
options: <FaceRestoreOptions />,
},
upscale: {
header: <Upscale />,
feature: Feature.UPSCALE,
options: <UpscaleOptions />,
},
other: {
header: (
<Box flex="1" textAlign="left">
Other
</Box>
),
feature: Feature.OTHER,
options: <OutputOptions />,
},
};
return (
<div className="image-to-image-panel">
<PromptInput />
<ProcessButtons />
<MainOptions />
<ImageToImageStrength
label="Image To Image Strength"
styleClass="main-option-block image-to-image-strength-main-option"
/>
<ImageFit />
<MainAdvancedOptions />
{showAdvancedOptions ? (
<OptionsAccordion accordionInfo={imageToImageAccordions} />
) : null}
</div>
);
}

View File

@ -0,0 +1,37 @@
import { IconButton, Image } from '@chakra-ui/react';
import React, { SyntheticEvent } from 'react';
import { MdClear } from 'react-icons/md';
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
import { setInitialImagePath } from '../../options/optionsSlice';
export default function InitImagePreview() {
const initialImagePath = useAppSelector(
(state: RootState) => state.options.initialImagePath
);
const dispatch = useAppDispatch();
const handleClickResetInitialImage = (e: SyntheticEvent) => {
e.stopPropagation();
dispatch(setInitialImagePath(null));
};
return (
<div className="init-image-preview">
<div className="init-image-preview-header">
<h1>Initial Image</h1>
<IconButton
isDisabled={!initialImagePath}
size={'sm'}
aria-label={'Reset Initial Image'}
onClick={handleClickResetInitialImage}
icon={<MdClear />}
/>
</div>
{initialImagePath && (
<div className="init-image-image">
<Image fit={'contain'} src={initialImagePath} rounded={'md'} />
</div>
)}
</div>
);
}

View File

@ -0,0 +1,18 @@
import { Image } from '@chakra-ui/react';
import React from 'react';
import { RootState, useAppSelector } from '../../../app/store';
export default function InitialImageOverlay() {
const initialImagePath = useAppSelector(
(state: RootState) => state.options.initialImagePath
);
return initialImagePath ? (
<Image
fit={'contain'}
src={initialImagePath}
rounded={'md'}
className={'checkerboard'}
/>
) : null;
}

View File

@ -1,6 +1,8 @@
import { Tab, TabPanel, TabPanels, Tabs, Tooltip } from '@chakra-ui/react'; import { Tab, TabPanel, TabPanels, Tabs, Tooltip } from '@chakra-ui/react';
import _ from 'lodash';
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { ImageToImageWIP } from '../../common/components/WorkInProgress/ImageToImageWIP'; import { useHotkeys } from 'react-hotkeys-hook';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
import InpaintingWIP from '../../common/components/WorkInProgress/InpaintingWIP'; import InpaintingWIP from '../../common/components/WorkInProgress/InpaintingWIP';
import NodesWIP from '../../common/components/WorkInProgress/NodesWIP'; import NodesWIP from '../../common/components/WorkInProgress/NodesWIP';
import OutpaintingWIP from '../../common/components/WorkInProgress/OutpaintingWIP'; import OutpaintingWIP from '../../common/components/WorkInProgress/OutpaintingWIP';
@ -11,41 +13,74 @@ import NodesIcon from '../../common/icons/NodesIcon';
import OutpaintIcon from '../../common/icons/OutpaintIcon'; import OutpaintIcon from '../../common/icons/OutpaintIcon';
import PostprocessingIcon from '../../common/icons/PostprocessingIcon'; import PostprocessingIcon from '../../common/icons/PostprocessingIcon';
import TextToImageIcon from '../../common/icons/TextToImageIcon'; import TextToImageIcon from '../../common/icons/TextToImageIcon';
import { setActiveTab } from '../options/optionsSlice';
import ImageToImage from './ImageToImage/ImageToImage';
import TextToImage from './TextToImage/TextToImage'; import TextToImage from './TextToImage/TextToImage';
export const tab_dict = {
txt2img: {
title: <TextToImageIcon fill={'black'} boxSize={'2.5rem'} />,
panel: <TextToImage />,
tooltip: 'Text To Image',
},
img2img: {
title: <ImageToImageIcon fill={'black'} boxSize={'2.5rem'} />,
panel: <ImageToImage />,
tooltip: 'Image To Image',
},
inpainting: {
title: <InpaintIcon fill={'black'} boxSize={'2.5rem'} />,
panel: <InpaintingWIP />,
tooltip: 'Inpainting',
},
outpainting: {
title: <OutpaintIcon fill={'black'} boxSize={'2.5rem'} />,
panel: <OutpaintingWIP />,
tooltip: 'Outpainting',
},
nodes: {
title: <NodesIcon fill={'black'} boxSize={'2.5rem'} />,
panel: <NodesWIP />,
tooltip: 'Nodes',
},
postprocess: {
title: <PostprocessingIcon fill={'black'} boxSize={'2.5rem'} />,
panel: <PostProcessingWIP />,
tooltip: 'Post Processing',
},
};
export const tabMap = _.map(tab_dict, (tab, key) => key);
export default function InvokeTabs() { export default function InvokeTabs() {
const tab_dict = { const activeTab = useAppSelector(
txt2img: { (state: RootState) => state.options.activeTab
title: <TextToImageIcon fill={'black'} boxSize={'2.5rem'} />, );
panel: <TextToImage />, const dispatch = useAppDispatch();
tooltip: 'Text To Image',
}, useHotkeys('1', () => {
img2img: { dispatch(setActiveTab(0));
title: <ImageToImageIcon fill={'black'} boxSize={'2.5rem'} />, });
panel: <ImageToImageWIP />,
tooltip: 'Image To Image', useHotkeys('2', () => {
}, dispatch(setActiveTab(1));
inpainting: { });
title: <InpaintIcon fill={'black'} boxSize={'2.5rem'} />,
panel: <InpaintingWIP />, useHotkeys('3', () => {
tooltip: 'Inpainting', dispatch(setActiveTab(2));
}, });
outpainting: {
title: <OutpaintIcon fill={'black'} boxSize={'2.5rem'} />, useHotkeys('4', () => {
panel: <OutpaintingWIP />, dispatch(setActiveTab(3));
tooltip: 'Outpainting', });
},
nodes: { useHotkeys('5', () => {
title: <NodesIcon fill={'black'} boxSize={'2.5rem'} />, dispatch(setActiveTab(4));
panel: <NodesWIP />, });
tooltip: 'Nodes',
}, useHotkeys('6', () => {
postprocess: { dispatch(setActiveTab(5));
title: <PostprocessingIcon fill={'black'} boxSize={'2.5rem'} />, });
panel: <PostProcessingWIP />,
tooltip: 'Post Processing',
},
};
const renderTabs = () => { const renderTabs = () => {
const tabsToRender: ReactElement[] = []; const tabsToRender: ReactElement[] = [];
@ -76,7 +111,16 @@ export default function InvokeTabs() {
}; };
return ( return (
<Tabs className="app-tabs" variant={'unstyled'}> <Tabs
isLazy
className="app-tabs"
variant={'unstyled'}
defaultIndex={activeTab}
index={activeTab}
onChange={(index: number) => {
dispatch(setActiveTab(index));
}}
>
<div className="app-tabs-list">{renderTabs()}</div> <div className="app-tabs-list">{renderTabs()}</div>
<TabPanels className="app-tabs-panels">{renderTabPanels()}</TabPanels> <TabPanels className="app-tabs-panels">{renderTabPanels()}</TabPanels>
</Tabs> </Tabs>

View File

@ -1,7 +1,20 @@
import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { Feature } from '../../../app/features';
import { RootState, useAppSelector } from '../../../app/store'; import { RootState, useAppSelector } from '../../../app/store';
import FaceRestore from '../../options/AdvancedOptions/FaceRestore/FaceRestore';
import FaceRestoreOptions from '../../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
import ImageToImageAccordion from '../../options/AdvancedOptions/ImageToImage/ImageToImageAccordion';
import ImageToImageOptions from '../../options/AdvancedOptions/ImageToImage/ImageToImageOptions';
import SeedOptions from '../../options/AdvancedOptions/Seed/SeedOptions';
import Upscale from '../../options/AdvancedOptions/Upscale/Upscale';
import UpscaleOptions from '../../options/AdvancedOptions/Upscale/UpscaleOptions';
import Variations from '../../options/AdvancedOptions/Variations/Variations';
import VariationsOptions from '../../options/AdvancedOptions/Variations/VariationsOptions';
import MainAdvancedOptions from '../../options/MainOptions/MainAdvancedOptions';
import MainOptions from '../../options/MainOptions/MainOptions'; import MainOptions from '../../options/MainOptions/MainOptions';
import OptionsAccordion from '../../options/OptionsAccordion'; import OptionsAccordion from '../../options/OptionsAccordion';
import OutputOptions from '../../options/OutputOptions';
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons'; import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
import PromptInput from '../../options/PromptInput/PromptInput'; import PromptInput from '../../options/PromptInput/PromptInput';
@ -9,12 +22,57 @@ export default function TextToImagePanel() {
const showAdvancedOptions = useAppSelector( const showAdvancedOptions = useAppSelector(
(state: RootState) => state.options.showAdvancedOptions (state: RootState) => state.options.showAdvancedOptions
); );
const textToImageAccordions = {
seed: {
header: (
<Box flex="1" textAlign="left">
Seed
</Box>
),
feature: Feature.SEED,
options: <SeedOptions />,
},
variations: {
header: <Variations />,
feature: Feature.VARIATIONS,
options: <VariationsOptions />,
},
face_restore: {
header: <FaceRestore />,
feature: Feature.FACE_CORRECTION,
options: <FaceRestoreOptions />,
},
upscale: {
header: <Upscale />,
feature: Feature.UPSCALE,
options: <UpscaleOptions />,
},
// img2img: {
// header: <ImageToImageAccordion />,
// feature: Feature.IMAGE_TO_IMAGE,
// options: <ImageToImageOptions />,
// },
other: {
header: (
<Box flex="1" textAlign="left">
Other
</Box>
),
feature: Feature.OTHER,
options: <OutputOptions />,
},
};
return ( return (
<div className="text-to-image-panel"> <div className="text-to-image-panel">
<PromptInput /> <PromptInput />
<ProcessButtons /> <ProcessButtons />
<MainOptions /> <MainOptions />
{showAdvancedOptions ? <OptionsAccordion /> : null} <MainAdvancedOptions />
{showAdvancedOptions ? (
<OptionsAccordion accordionInfo={textToImageAccordions} />
) : null}
</div> </div>
); );
} }

View File

@ -89,4 +89,7 @@
--console-icon-button-bg-color: rgb(50, 53, 64); --console-icon-button-bg-color: rgb(50, 53, 64);
--console-icon-button-bg-color-hover: rgb(70, 73, 84); --console-icon-button-bg-color-hover: rgb(70, 73, 84);
// Img2Img
--img2img-img-bg-color: rgb(30, 32, 42);
} }

View File

@ -88,4 +88,7 @@
--console-border-color: rgb(160, 162, 164); --console-border-color: rgb(160, 162, 164);
--console-icon-button-bg-color: var(--switch-bg-color); --console-icon-button-bg-color: var(--switch-bg-color);
--console-icon-button-bg-color-hover: var(--console-border-color); --console-icon-button-bg-color-hover: var(--console-border-color);
// Img2Img
--img2img-img-bg-color: rgb(180, 182, 184);
} }

View File

@ -26,10 +26,12 @@
@use '../features/gallery/CurrentImageDisplay.scss'; @use '../features/gallery/CurrentImageDisplay.scss';
@use '../features/gallery/ImageGallery.scss'; @use '../features/gallery/ImageGallery.scss';
@use '../features/gallery/InvokePopover.scss'; @use '../features/gallery/InvokePopover.scss';
@use '../features/gallery/ImageMetaDataViewer/ImageMetadataViewer.scss';
// Tabs // Tabs
@use '../features/tabs/InvokeTabs.scss'; @use '../features/tabs/InvokeTabs.scss';
@use '../features/tabs/TextToImage/TextToImage.scss'; @use '../features/tabs/TextToImage/TextToImage.scss';
@use '../features/tabs/ImageToImage/ImageToImage.scss';
// Component Shared // Component Shared
@use '../common/components/IAINumberInput.scss'; @use '../common/components/IAINumberInput.scss';