mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Merge branch 'release-candidate-2' of github.com:invoke-ai/InvokeAI into release-candidate-2
This commit is contained in:
commit
4a7f5c7469
BIN
frontend/dist/assets/image2img.dde6a9f1.png
vendored
BIN
frontend/dist/assets/image2img.dde6a9f1.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 336 KiB |
483
frontend/dist/assets/index.3a9574b7.js
vendored
483
frontend/dist/assets/index.3a9574b7.js
vendored
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.60ca0ee5.css
vendored
1
frontend/dist/assets/index.60ca0ee5.css
vendored
File diff suppressed because one or more lines are too long
483
frontend/dist/assets/index.783ff334.js
vendored
Normal file
483
frontend/dist/assets/index.783ff334.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index.a0250964.css
vendored
Normal file
1
frontend/dist/assets/index.a0250964.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
frontend/dist/index.html
vendored
4
frontend/dist/index.html
vendored
@ -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>
|
||||
|
@ -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',
|
||||
|
65
frontend/src/common/components/InvokeImageUploader.tsx
Normal file
65
frontend/src/common/components/InvokeImageUploader.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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">
|
||||
|
105
frontend/src/features/gallery/CurrentImagePreview.tsx
Normal file
105
frontend/src/features/gallery/CurrentImagePreview.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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(
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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 = () => {
|
||||
|
@ -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
|
||||
|
132
frontend/src/features/tabs/ImageToImage/ImageToImage.scss
Normal file
132
frontend/src/features/tabs/ImageToImage/ImageToImage.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
15
frontend/src/features/tabs/ImageToImage/ImageToImage.tsx
Normal file
15
frontend/src/features/tabs/ImageToImage/ImageToImage.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
37
frontend/src/features/tabs/ImageToImage/InitImagePreview.tsx
Normal file
37
frontend/src/features/tabs/ImageToImage/InitImagePreview.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user