[WebUI] Add Image To Image UI

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View File

@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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.a0198006.js"></script>
<link rel="stylesheet" href="/assets/index.60ca0ee5.css">
<script type="module" crossorigin src="/assets/index.dd3155db.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;
@ -269,6 +273,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;
},
},
});
@ -303,6 +313,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';