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" />
<title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="shortcut icon" type="icon" href="/assets/favicon.0d253ced.ico" />
<script type="module" crossorigin src="/assets/index.3a9574b7.js"></script>
<link rel="stylesheet" href="/assets/index.60ca0ee5.css">
<script type="module" crossorigin src="/assets/index.783ff334.js"></script>
<link rel="stylesheet" href="/assets/index.a0250964.css">
</head>
<body>

View File

@ -6,6 +6,7 @@ import {
addLogEntry,
setIsProcessing,
} from '../../features/system/systemSlice';
import { tabMap, tab_dict } from '../../features/tabs/InvokeTabs';
import * as InvokeAI from '../invokeai';
/**
@ -23,8 +24,14 @@ const makeSocketIOEmitters = (
emitGenerateImage: () => {
dispatch(setIsProcessing(true));
const options = { ...getState().options };
if (tabMap[options.activeTab] === 'txt2img') {
options.shouldUseInitImage = false;
}
const { generationParameters, esrganParameters, gfpganParameters } =
frontendToBackendParameters(getState().options, getState().system);
frontendToBackendParameters(options, getState().system);
socketio.emit(
'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,
initialImagePath: options.initialImagePath,
seed: options.seed,
activeTab: options.activeTab,
};
},
{
@ -55,6 +56,7 @@ const useCheckParameters = (): boolean => {
maskPath,
initialImagePath,
seed,
activeTab,
} = useAppSelector(optionsSelector);
const { isProcessing, isConnected } = useAppSelector(systemSelector);
@ -65,6 +67,10 @@ const useCheckParameters = (): boolean => {
return false;
}
if (prompt && !initialImagePath && activeTab === 1) {
return false;
}
// Cannot generate with a mask without img2img
if (maskPath && !initialImagePath) {
return false;

View File

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

View File

@ -27,7 +27,6 @@
}
.current-image-tools {
grid-area: current-image-tools;
width: 100%;
height: 100%;
display: grid;
@ -58,34 +57,37 @@
align-items: center;
display: grid;
width: 100%;
grid-template-areas: 'current-image-content';
img {
grid-area: current-image-content;
background-color: var(--img2img-img-bg-color);
border-radius: 0.5rem;
object-fit: contain;
width: auto;
height: $app-gallery-height;
max-height: $app-gallery-height;
}
}
.current-image-metadata {
grid-area: current-image-preview;
}
.current-image-next-prev-buttons {
position: absolute;
top: 0;
left: 0;
grid-area: current-image-content;
display: flex;
align-items: center;
justify-content: space-between;
width: calc(100% - 2rem);
padding: 0.5rem;
margin-left: 1rem;
z-index: 1;
height: calc($app-metadata-height - 1rem);
height: 100%;
pointer-events: none;
}
.next-prev-button-trigger-area {
width: 7rem;
height: 100%;
display: flex;
width: 100%;
display: grid;
align-items: center;
pointer-events: auto;
@ -99,31 +101,8 @@
}
.next-prev-button {
font-size: 5rem;
fill: var(--text-color-secondary);
font-size: 4rem;
fill: var(--white);
filter: drop-shadow(0 0 1rem var(--text-color-secondary));
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 { useAppDispatch, useAppSelector } from '../../app/store';
import { RootState } from '../../app/store';
import { useState } from 'react';
import ImageMetadataViewer from './ImageMetadataViewer';
import { RootState, useAppSelector } from '../../app/store';
import CurrentImageButtons from './CurrentImageButtons';
import { MdPhoto } from 'react-icons/md';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
import { selectNextImage, selectPrevImage } from './gallerySlice';
import CurrentImagePreview from './CurrentImagePreview';
import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer';
/**
* Displays the current image if there is one, plus associated actions.
*/
const CurrentImageDisplay = () => {
const dispatch = useAppDispatch();
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] =
useState<boolean>(false);
const { currentImage, intermediateImage } = useAppSelector(
(state: RootState) => state.gallery
);
const [shouldShowImageDetails, setShouldShowImageDetails] =
useState<boolean>(false);
const shouldShowImageDetails = useAppSelector(
(state: RootState) => state.options.shouldShowImageDetails
);
const imageToDisplay = intermediateImage || currentImage;
const handleCurrentImagePreviewMouseOver = () => {
setShouldShowNextPrevButtons(true);
};
const handleCurrentImagePreviewMouseOut = () => {
setShouldShowNextPrevButtons(false);
};
const handleClickPrevButton = () => {
dispatch(selectPrevImage());
};
const handleClickNextButton = () => {
dispatch(selectNextImage());
};
return imageToDisplay ? (
<div className="current-image-display">
<div className="current-image-tools">
<CurrentImageButtons
<CurrentImageButtons image={imageToDisplay} />
</div>
<CurrentImagePreview imageToDisplay={imageToDisplay} />
{shouldShowImageDetails && (
<ImageMetadataViewer
image={imageToDisplay}
shouldShowImageDetails={shouldShowImageDetails}
setShouldShowImageDetails={setShouldShowImageDetails}
styleClass="current-image-metadata"
/>
</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 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,
useColorModeValue,
} from '@chakra-ui/react';
import { useAppDispatch } from '../../app/store';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
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 { 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 { IoArrowUndoCircleOutline } from 'react-icons/io5';
@ -33,6 +38,10 @@ const HoverableImage = memo((props: HoverableImageProps) => {
const [isHovered, setIsHovered] = useState<boolean>(false);
const dispatch = useAppDispatch();
const activeTab = useAppSelector(
(state: RootState) => state.options.activeTab
);
const checkColor = useColorModeValue('green.600', 'green.300');
const bgColor = useColorModeValue('gray.200', 'gray.700');
const bgGradient = useColorModeValue(
@ -56,6 +65,14 @@ const HoverableImage = memo((props: HoverableImageProps) => {
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));
return (
@ -131,6 +148,16 @@ const HoverableImage = memo((props: HoverableImageProps) => {
/>
</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>

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 GuideIcon from '../../../common/components/GuideIcon';
interface InvokeAccordionItemProps {
export interface InvokeAccordionItemProps {
header: ReactElement;
feature: Feature;
options: ReactElement;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,34 +1,25 @@
import {
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 { Accordion, ExpandedIndex } from '@chakra-ui/react';
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
import { setOpenAccordions } from '../system/systemSlice';
import InvokeAccordionItem from './AccordionItems/InvokeAccordionItem';
import Variations from './AdvancedOptions/Variations/Variations';
import VariationsOptions from './AdvancedOptions/Variations/VariationsOptions';
import InvokeAccordionItem, {
InvokeAccordionItemProps,
} from './AccordionItems/InvokeAccordionItem';
import { ReactElement } from 'react';
type OptionsAccordionType = {
[optionAccordionKey: string]: InvokeAccordionItemProps;
};
type OptionAccordionsType = {
accordionInfo: OptionsAccordionType;
};
/**
* Main container for generation and processing parameters.
*/
const OptionsAccordion = () => {
const OptionsAccordion = (props: OptionAccordionsType) => {
const { accordionInfo } = props;
const openAccordions = useAppSelector(
(state: RootState) => state.system.openAccordions
);
@ -41,6 +32,23 @@ const OptionsAccordion = () => {
const handleChangeAccordionState = (openAccordions: ExpandedIndex) =>
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 (
<Accordion
defaultIndex={openAccordions}
@ -49,49 +57,7 @@ const OptionsAccordion = () => {
onChange={handleChangeAccordionState}
className="advanced-settings"
>
<InvokeAccordionItem
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 />}
/>
{renderAccordions()}
</Accordion>
);
};

View File

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

View File

@ -61,6 +61,16 @@ export default function HotkeysModal({ children }: HotkeysModalProps) {
desc: 'Display the next image in the gallery',
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 = () => {

View File

@ -1,4 +1,5 @@
import { IconButton, Link, useColorMode } from '@chakra-ui/react';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaSun, FaMoon, FaGithub } from 'react-icons/fa';
import { MdHelp, MdKeyboard, MdSettings } from 'react-icons/md';
@ -15,6 +16,14 @@ import StatusIndicator from './StatusIndicator';
const SiteHeader = () => {
const { colorMode, toggleColorMode } = useColorMode();
useHotkeys(
'shift+d',
() => {
toggleColorMode();
},
[colorMode, toggleColorMode]
);
const colorModeIcon = colorMode == 'light' ? <FaMoon /> : <FaSun />;
// 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 _ from 'lodash';
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 NodesWIP from '../../common/components/WorkInProgress/NodesWIP';
import OutpaintingWIP from '../../common/components/WorkInProgress/OutpaintingWIP';
@ -11,41 +13,74 @@ import NodesIcon from '../../common/icons/NodesIcon';
import OutpaintIcon from '../../common/icons/OutpaintIcon';
import PostprocessingIcon from '../../common/icons/PostprocessingIcon';
import TextToImageIcon from '../../common/icons/TextToImageIcon';
import { setActiveTab } from '../options/optionsSlice';
import ImageToImage from './ImageToImage/ImageToImage';
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() {
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: <ImageToImageWIP />,
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',
},
};
const activeTab = useAppSelector(
(state: RootState) => state.options.activeTab
);
const dispatch = useAppDispatch();
useHotkeys('1', () => {
dispatch(setActiveTab(0));
});
useHotkeys('2', () => {
dispatch(setActiveTab(1));
});
useHotkeys('3', () => {
dispatch(setActiveTab(2));
});
useHotkeys('4', () => {
dispatch(setActiveTab(3));
});
useHotkeys('5', () => {
dispatch(setActiveTab(4));
});
useHotkeys('6', () => {
dispatch(setActiveTab(5));
});
const renderTabs = () => {
const tabsToRender: ReactElement[] = [];
@ -76,7 +111,16 @@ export default function InvokeTabs() {
};
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>
<TabPanels className="app-tabs-panels">{renderTabPanels()}</TabPanels>
</Tabs>

View File

@ -1,7 +1,20 @@
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 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 OptionsAccordion from '../../options/OptionsAccordion';
import OutputOptions from '../../options/OutputOptions';
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
import PromptInput from '../../options/PromptInput/PromptInput';
@ -9,12 +22,57 @@ export default function TextToImagePanel() {
const showAdvancedOptions = useAppSelector(
(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 (
<div className="text-to-image-panel">
<PromptInput />
<ProcessButtons />
<MainOptions />
{showAdvancedOptions ? <OptionsAccordion /> : null}
<MainAdvancedOptions />
{showAdvancedOptions ? (
<OptionsAccordion accordionInfo={textToImageAccordions} />
) : null}
</div>
);
}

View File

@ -89,4 +89,7 @@
--console-icon-button-bg-color: rgb(50, 53, 64);
--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-icon-button-bg-color: var(--switch-bg-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/ImageGallery.scss';
@use '../features/gallery/InvokePopover.scss';
@use '../features/gallery/ImageMetaDataViewer/ImageMetadataViewer.scss';
// Tabs
@use '../features/tabs/InvokeTabs.scss';
@use '../features/tabs/TextToImage/TextToImage.scss';
@use '../features/tabs/ImageToImage/ImageToImage.scss';
// Component Shared
@use '../common/components/IAINumberInput.scss';