Merge branch 'release-candidate-2' of github.com:invoke-ai/InvokeAI into release-candidate-2

This commit is contained in:
Lincoln Stein 2022-10-08 09:34:11 -04:00
commit 4a7f5c7469
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

483
frontend/dist/assets/index.783ff334.js vendored Normal file

File diff suppressed because one or more lines are too long

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.3a9574b7.js"></script> <script type="module" crossorigin src="/assets/index.783ff334.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;
@ -271,6 +275,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;
},
}, },
}); });
@ -305,6 +315,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';