mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
Add New WebUI and Desktop Mode
Co-Authored-By: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
This commit is contained in:
committed by
Lincoln Stein
parent
40828df663
commit
b8e4c13746
@@ -1,4 +1,3 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@@ -13,8 +12,13 @@ import {
|
||||
} from '../options/optionsSlice';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import SDButton from '../../common/components/SDButton';
|
||||
import IAIButton from '../../common/components/IAIButton';
|
||||
import { runESRGAN, runGFPGAN } from '../../app/socketio/actions';
|
||||
import IAIIconButton from '../../common/components/IAIIconButton';
|
||||
import { MdDelete, MdFace, MdHd, MdImage, MdInfo } from 'react-icons/md';
|
||||
import InvokePopover from './InvokePopover';
|
||||
import UpscaleOptions from '../options/AdvancedOptions/Upscale/UpscaleOptions';
|
||||
import FaceRestoreOptions from '../options/AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
@@ -50,12 +54,16 @@ const CurrentImageButtons = ({
|
||||
}: CurrentImageButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { intermediateImage } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
const intermediateImage = useAppSelector(
|
||||
(state: RootState) => state.gallery.intermediateImage
|
||||
);
|
||||
|
||||
const { upscalingLevel, gfpganStrength } = useAppSelector(
|
||||
(state: RootState) => state.options
|
||||
const upscalingLevel = useAppSelector(
|
||||
(state: RootState) => state.options.upscalingLevel
|
||||
);
|
||||
|
||||
const gfpganStrength = useAppSelector(
|
||||
(state: RootState) => state.options.gfpganStrength
|
||||
);
|
||||
|
||||
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
|
||||
@@ -78,77 +86,83 @@ const CurrentImageButtons = ({
|
||||
setShouldShowImageDetails(!shouldShowImageDetails);
|
||||
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
<SDButton
|
||||
label="Use as initial image"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
<div className="current-image-options">
|
||||
<IAIIconButton
|
||||
icon={<MdImage />}
|
||||
tooltip="Use As Initial Image"
|
||||
aria-label="Use As Initial Image"
|
||||
onClick={handleClickUseAsInitialImage}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label="Use all"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)}
|
||||
<IAIButton
|
||||
label="Use All"
|
||||
isDisabled={
|
||||
!['txt2img', 'img2img'].includes(image?.metadata?.image?.type)
|
||||
}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label="Use seed"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
<IAIButton
|
||||
label="Use Seed"
|
||||
isDisabled={!image?.metadata?.image?.seed}
|
||||
onClick={handleClickUseSeed}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label="Upscale"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={
|
||||
!isESRGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing) ||
|
||||
!upscalingLevel
|
||||
<InvokePopover
|
||||
title="Restore Faces"
|
||||
popoverOptions={<FaceRestoreOptions />}
|
||||
actionButton={
|
||||
<IAIButton
|
||||
label={'Restore Faces'}
|
||||
isDisabled={
|
||||
!isGFPGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing) ||
|
||||
!gfpganStrength
|
||||
}
|
||||
onClick={handleClickFixFaces}
|
||||
/>
|
||||
}
|
||||
onClick={handleClickUpscale}
|
||||
/>
|
||||
<SDButton
|
||||
label="Fix faces"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={
|
||||
!isGFPGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing) ||
|
||||
!gfpganStrength
|
||||
>
|
||||
<IAIIconButton icon={<MdFace />} aria-label="Restore Faces" />
|
||||
</InvokePopover>
|
||||
|
||||
<InvokePopover
|
||||
title="Upscale"
|
||||
styleClass="upscale-popover"
|
||||
popoverOptions={<UpscaleOptions />}
|
||||
actionButton={
|
||||
<IAIButton
|
||||
label={'Upscale Image'}
|
||||
isDisabled={
|
||||
!isESRGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing) ||
|
||||
!upscalingLevel
|
||||
}
|
||||
onClick={handleClickUpscale}
|
||||
/>
|
||||
}
|
||||
onClick={handleClickFixFaces}
|
||||
/>
|
||||
<SDButton
|
||||
label="Details"
|
||||
colorScheme={'gray'}
|
||||
variant={shouldShowImageDetails ? 'solid' : 'outline'}
|
||||
borderWidth={1}
|
||||
flexGrow={1}
|
||||
>
|
||||
<IAIIconButton icon={<MdHd />} aria-label="Upscale" />
|
||||
</InvokePopover>
|
||||
|
||||
<IAIIconButton
|
||||
icon={<MdInfo />}
|
||||
tooltip="Details"
|
||||
aria-label="Details"
|
||||
onClick={handleClickShowImageDetails}
|
||||
/>
|
||||
|
||||
<DeleteImageModal image={image}>
|
||||
<SDButton
|
||||
label="Delete"
|
||||
colorScheme={'red'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
<IAIIconButton
|
||||
icon={<MdDelete />}
|
||||
tooltip="Delete Image"
|
||||
aria-label="Delete Image"
|
||||
isDisabled={Boolean(intermediateImage)}
|
||||
/>
|
||||
</DeleteImageModal>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
91
frontend/src/features/gallery/CurrentImageDisplay.scss
Normal file
91
frontend/src/features/gallery/CurrentImageDisplay.scss
Normal file
@@ -0,0 +1,91 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.current-image-display {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'current-image-tools'
|
||||
'current-image-preview';
|
||||
grid-template-rows: auto 1fr;
|
||||
justify-items: center;
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.current-image-display-placeholder {
|
||||
background-color: var(--background-color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
svg {
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
color: var(--svg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.current-image-tools {
|
||||
grid-area: current-image-tools;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.current-image-options {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
padding: 1rem;
|
||||
height: fit-content;
|
||||
gap: 0.5rem;
|
||||
|
||||
button {
|
||||
@include Button(
|
||||
$btn-width: 3rem,
|
||||
$icon-size: 22px,
|
||||
$btn-color: var(--btn-grey),
|
||||
$btn-color-hover: var(--btn-grey-hover)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.current-image-preview {
|
||||
grid-area: current-image-preview;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
border-radius: 0.5rem;
|
||||
object-fit: contain;
|
||||
width: auto;
|
||||
max-height: $app-gallery-height;
|
||||
}
|
||||
}
|
||||
|
||||
.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,12 +1,10 @@
|
||||
import { Center, Flex, Image, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import { Image } from '@chakra-ui/react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useState } from 'react';
|
||||
import ImageMetadataViewer from './ImageMetadataViewer';
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
|
||||
// TODO: With CSS Grid I had a hard time centering the image in a grid item. This is needed for that.
|
||||
const height = 'calc(100vh - 238px)';
|
||||
import { MdPhoto } from 'react-icons/md';
|
||||
|
||||
/**
|
||||
* Displays the current image if there is one, plus associated actions.
|
||||
@@ -16,24 +14,21 @@ const CurrentImageDisplay = () => {
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
const bgColor = useColorModeValue(
|
||||
'rgba(255, 255, 255, 0.85)',
|
||||
'rgba(0, 0, 0, 0.8)'
|
||||
);
|
||||
|
||||
const [shouldShowImageDetails, setShouldShowImageDetails] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const imageToDisplay = intermediateImage || currentImage;
|
||||
|
||||
return imageToDisplay ? (
|
||||
<Flex direction={'column'} borderWidth={1} rounded={'md'} p={2} gap={2}>
|
||||
<CurrentImageButtons
|
||||
image={imageToDisplay}
|
||||
shouldShowImageDetails={shouldShowImageDetails}
|
||||
setShouldShowImageDetails={setShouldShowImageDetails}
|
||||
/>
|
||||
<Center height={height} position={'relative'}>
|
||||
<div className="current-image-display">
|
||||
<div className="current-image-tools">
|
||||
<CurrentImageButtons
|
||||
image={imageToDisplay}
|
||||
shouldShowImageDetails={shouldShowImageDetails}
|
||||
setShouldShowImageDetails={setShouldShowImageDetails}
|
||||
/>
|
||||
</div>
|
||||
<div className="current-image-preview">
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
fit="contain"
|
||||
@@ -41,26 +36,16 @@ const CurrentImageDisplay = () => {
|
||||
maxHeight={'100%'}
|
||||
/>
|
||||
{shouldShowImageDetails && (
|
||||
<Flex
|
||||
width={'100%'}
|
||||
height={'100%'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
p={3}
|
||||
boxSizing="border-box"
|
||||
backgroundColor={bgColor}
|
||||
overflow="scroll"
|
||||
>
|
||||
<div className="current-image-metadata-viewer">
|
||||
<ImageMetadataViewer image={imageToDisplay} />
|
||||
</Flex>
|
||||
</div>
|
||||
)}
|
||||
</Center>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Center height={'100%'} position={'relative'}>
|
||||
<Text size={'xl'}>No image selected</Text>
|
||||
</Center>
|
||||
<div className="current-image-display-placeholder">
|
||||
<MdPhoto />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{image?.metadata?.image?.seed && (
|
||||
{image?.metadata?.image?.seed !== undefined && (
|
||||
<Tooltip label="Use seed">
|
||||
<IconButton
|
||||
aria-label="Use seed"
|
||||
|
||||
51
frontend/src/features/gallery/ImageGallery.scss
Normal file
51
frontend/src/features/gallery/ImageGallery.scss
Normal file
@@ -0,0 +1,51 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.image-gallery-container {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
grid-auto-rows: max-content;
|
||||
min-width: 16rem;
|
||||
}
|
||||
|
||||
.image-gallery-container-placeholder {
|
||||
display: grid;
|
||||
background-color: var(--background-color-secondary);
|
||||
border-radius: 0.5rem;
|
||||
place-items: center;
|
||||
padding: 2rem 0;
|
||||
|
||||
p {
|
||||
color: var(--subtext-color-bright);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
color: var(--svg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.image-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
gap: 0.6rem;
|
||||
justify-items: center;
|
||||
max-height: $app-gallery-height;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
|
||||
.image-gallery-load-more-btn {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
font-size: 0.85rem !important;
|
||||
|
||||
&:disabled {
|
||||
&:hover {
|
||||
background-color: var(--btn-load-more) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--btn-load-more-hover) !important;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, Center, Flex, Text } from '@chakra-ui/react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { MdPhotoLibrary } from 'react-icons/md';
|
||||
import { requestImages } from '../../app/socketio/actions';
|
||||
import { RootState, useAppDispatch } from '../../app/store';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
@@ -8,7 +9,7 @@ import HoverableImage from './HoverableImage';
|
||||
* Simple image gallery.
|
||||
*/
|
||||
const ImageGallery = () => {
|
||||
const { images, currentImageUuid } = useAppSelector(
|
||||
const { images, currentImageUuid, areMoreImagesAvailable } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -24,23 +25,41 @@ const ImageGallery = () => {
|
||||
dispatch(requestImages());
|
||||
};
|
||||
|
||||
return images.length ? (
|
||||
<Flex direction={'column'} gap={2} pb={2}>
|
||||
<Flex gap={2} wrap="wrap">
|
||||
{images.map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage key={uuid} image={image} isSelected={isSelected} />
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
<Button onClick={handleClickLoadMore}>Load more...</Button>
|
||||
</Flex>
|
||||
) : (
|
||||
<Center height={'100%'} position={'relative'}>
|
||||
<Text size={'xl'}>No images in gallery</Text>
|
||||
</Center>
|
||||
return (
|
||||
<div className="image-gallery-container">
|
||||
{images.length ? (
|
||||
<>
|
||||
<p>
|
||||
<strong>Your Invocations</strong>
|
||||
</p>
|
||||
<div className="image-gallery">
|
||||
{images.map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={uuid}
|
||||
image={image}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="image-gallery-container-placeholder">
|
||||
<MdPhotoLibrary />
|
||||
<p>No Images In Gallery</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleClickLoadMore}
|
||||
isDisabled={!areMoreImagesAvailable}
|
||||
className="image-gallery-load-more-btn"
|
||||
>
|
||||
{areMoreImagesAvailable ? 'Load More' : 'All Images Loaded'}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Text,
|
||||
Tooltip,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { ExternalLinkIcon } from '@chakra-ui/icons';
|
||||
import { memo } from 'react';
|
||||
@@ -39,12 +38,19 @@ type MetadataItemProps = {
|
||||
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 }: MetadataItemProps) => {
|
||||
const MetadataItem = ({
|
||||
label,
|
||||
value,
|
||||
onClick,
|
||||
isLink,
|
||||
labelPosition,
|
||||
}: MetadataItemProps) => {
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
{onClick && (
|
||||
@@ -59,18 +65,20 @@ const MetadataItem = ({ label, value, onClick, isLink }: MetadataItemProps) => {
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Text fontWeight={'semibold'} whiteSpace={'nowrap'}>
|
||||
{label}:
|
||||
</Text>
|
||||
{isLink ? (
|
||||
<Link href={value.toString()} isExternal wordBreak={'break-all'}>
|
||||
{value.toString()} <ExternalLinkIcon mx="2px" />
|
||||
</Link>
|
||||
) : (
|
||||
<Text maxHeight={100} overflowY={'scroll'} wordBreak={'break-all'}>
|
||||
{value.toString()}
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -93,7 +101,7 @@ const memoEqualityCheck = (
|
||||
*/
|
||||
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
|
||||
// const jsonBgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
|
||||
|
||||
const metadata = image?.metadata?.image || {};
|
||||
const {
|
||||
@@ -119,7 +127,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
const metadataJSON = JSON.stringify(metadata, null, 2);
|
||||
|
||||
return (
|
||||
<Flex gap={1} direction={'column'} overflowY={'scroll'} width={'100%'}>
|
||||
<Flex gap={1} direction={'column'} width={'100%'}>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight={'semibold'}>File:</Text>
|
||||
<Link href={image.url} isExternal>
|
||||
@@ -129,25 +137,25 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
</Flex>
|
||||
{Object.keys(metadata).length > 0 ? (
|
||||
<>
|
||||
{type && <MetadataItem label="Type" value={type} />}
|
||||
{type && <MetadataItem label="Generation type" value={type} />}
|
||||
{['esrgan', 'gfpgan'].includes(type) && (
|
||||
<MetadataItem label="Original image" value={orig_path} isLink />
|
||||
<MetadataItem label="Original image" value={orig_path} />
|
||||
)}
|
||||
{type === 'gfpgan' && strength && (
|
||||
{type === 'gfpgan' && strength !== undefined && (
|
||||
<MetadataItem
|
||||
label="Fix faces strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setGfpganStrength(strength))}
|
||||
/>
|
||||
)}
|
||||
{type === 'esrgan' && scale && (
|
||||
{type === 'esrgan' && scale !== undefined && (
|
||||
<MetadataItem
|
||||
label="Upscaling scale"
|
||||
value={scale}
|
||||
onClick={() => dispatch(setUpscalingLevel(scale))}
|
||||
/>
|
||||
)}
|
||||
{type === 'esrgan' && strength && (
|
||||
{type === 'esrgan' && strength !== undefined && (
|
||||
<MetadataItem
|
||||
label="Upscaling strength"
|
||||
value={strength}
|
||||
@@ -157,11 +165,12 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
{prompt && (
|
||||
<MetadataItem
|
||||
label="Prompt"
|
||||
labelPosition="top"
|
||||
value={promptToString(prompt)}
|
||||
onClick={() => dispatch(setPrompt(prompt))}
|
||||
/>
|
||||
)}
|
||||
{seed && (
|
||||
{seed !== undefined && (
|
||||
<MetadataItem
|
||||
label="Seed"
|
||||
value={seed}
|
||||
@@ -182,7 +191,7 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
onClick={() => dispatch(setSteps(steps))}
|
||||
/>
|
||||
)}
|
||||
{cfg_scale && (
|
||||
{cfg_scale !== undefined && (
|
||||
<MetadataItem
|
||||
label="CFG scale"
|
||||
value={cfg_scale}
|
||||
@@ -249,38 +258,53 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
onClick={() => dispatch(setShouldFitToWidthHeight(fit))}
|
||||
/>
|
||||
)}
|
||||
{postprocessing &&
|
||||
postprocessing.length > 0 &&
|
||||
postprocessing.map(
|
||||
(postprocess: InvokeAI.PostProcessedImageMetadata) => {
|
||||
if (postprocess.type === 'esrgan') {
|
||||
const { scale, strength } = postprocess;
|
||||
return (
|
||||
<>
|
||||
<MetadataItem
|
||||
label="Upscaling scale"
|
||||
value={scale}
|
||||
onClick={() => dispatch(setUpscalingLevel(scale))}
|
||||
/>
|
||||
<MetadataItem
|
||||
label="Upscaling strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setUpscalingStrength(strength))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else if (postprocess.type === 'gfpgan') {
|
||||
const { strength } = postprocess;
|
||||
return (
|
||||
<MetadataItem
|
||||
label="Fix faces strength"
|
||||
value={strength}
|
||||
onClick={() => dispatch(setGfpganStrength(strength))}
|
||||
/>
|
||||
);
|
||||
{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`}>
|
||||
@@ -295,16 +319,9 @@ const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
</Tooltip>
|
||||
<Text fontWeight={'semibold'}>Metadata JSON:</Text>
|
||||
</Flex>
|
||||
<Box
|
||||
// maxHeight={200}
|
||||
overflow={'scroll'}
|
||||
flexGrow={3}
|
||||
wordBreak={'break-all'}
|
||||
bgColor={jsonBgColor}
|
||||
padding={2}
|
||||
>
|
||||
<div className={'current-image-json-viewer'}>
|
||||
<pre>{metadataJSON}</pre>
|
||||
</Box>
|
||||
</div>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
|
||||
35
frontend/src/features/gallery/InvokePopover.scss
Normal file
35
frontend/src/features/gallery/InvokePopover.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
.popover-content {
|
||||
background-color: var(--background-color-secondary) !important;
|
||||
border: none !important;
|
||||
border-top: 0px;
|
||||
background-color: var(--tab-hover-color);
|
||||
border-radius: 0 0 0.4rem 0.4rem;
|
||||
}
|
||||
|
||||
.popover-arrow {
|
||||
background: var(--tab-hover-color) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.popover-options {
|
||||
background: var(--tab-panel-bg);
|
||||
border-radius: 0 0 0.4rem 0.4rem;
|
||||
border: 2px solid var(--tab-hover-color);
|
||||
padding: .75rem 1rem .75rem 1rem;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(auto-fill, 1fr);
|
||||
grid-row-gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.popover-header {
|
||||
background: var(--tab-hover-color);
|
||||
border-radius: 0.4rem 0.4rem 0 0;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
padding-left: 1rem !important;
|
||||
}
|
||||
|
||||
.upscale-popover {
|
||||
width: 23rem !important;
|
||||
}
|
||||
45
frontend/src/features/gallery/InvokePopover.tsx
Normal file
45
frontend/src/features/gallery/InvokePopover.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
Box,
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverContent,
|
||||
PopoverHeader,
|
||||
PopoverTrigger,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
type PopoverProps = {
|
||||
title?: string;
|
||||
delay?: number;
|
||||
styleClass?: string;
|
||||
popoverOptions?: ReactNode;
|
||||
actionButton?: ReactNode;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const InvokePopover = ({
|
||||
title = 'Popup',
|
||||
styleClass,
|
||||
delay = 50,
|
||||
popoverOptions,
|
||||
actionButton,
|
||||
children,
|
||||
}: PopoverProps) => {
|
||||
return (
|
||||
<Popover trigger={'hover'} closeDelay={delay}>
|
||||
<PopoverTrigger>
|
||||
<Box>{children}</Box>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className={`popover-content ${styleClass}`}>
|
||||
<PopoverArrow className="popover-arrow" />
|
||||
<PopoverHeader className="popover-header">{title}</PopoverHeader>
|
||||
<div className="popover-options">
|
||||
{popoverOptions ? popoverOptions : null}
|
||||
{actionButton}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default InvokePopover;
|
||||
@@ -8,15 +8,15 @@ export interface GalleryState {
|
||||
currentImageUuid: string;
|
||||
images: Array<InvokeAI.Image>;
|
||||
intermediateImage?: InvokeAI.Image;
|
||||
nextPage: number;
|
||||
offset: number;
|
||||
areMoreImagesAvailable: boolean;
|
||||
latest_mtime?: number;
|
||||
earliest_mtime?: number;
|
||||
}
|
||||
|
||||
const initialState: GalleryState = {
|
||||
currentImageUuid: '',
|
||||
images: [],
|
||||
nextPage: 1,
|
||||
offset: 0,
|
||||
areMoreImagesAvailable: true,
|
||||
};
|
||||
|
||||
export const gallerySlice = createSlice({
|
||||
@@ -71,11 +71,13 @@ export const gallerySlice = createSlice({
|
||||
state.images = newImages;
|
||||
},
|
||||
addImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
state.images.unshift(action.payload);
|
||||
state.currentImageUuid = action.payload.uuid;
|
||||
const newImage = action.payload;
|
||||
const { uuid, mtime } = newImage;
|
||||
state.images.unshift(newImage);
|
||||
state.currentImageUuid = uuid;
|
||||
state.intermediateImage = undefined;
|
||||
state.currentImage = action.payload;
|
||||
state.offset += 1
|
||||
state.currentImage = newImage;
|
||||
state.latest_mtime = mtime;
|
||||
},
|
||||
setIntermediateImage: (state, action: PayloadAction<InvokeAI.Image>) => {
|
||||
state.intermediateImage = action.payload;
|
||||
@@ -87,20 +89,27 @@ export const gallerySlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
images: Array<InvokeAI.Image>;
|
||||
nextPage: number;
|
||||
offset: number;
|
||||
areMoreImagesAvailable: boolean;
|
||||
}>
|
||||
) => {
|
||||
const { images, nextPage, offset } = action.payload;
|
||||
if (images.length) {
|
||||
const newCurrentImage = images[0];
|
||||
const { images, areMoreImagesAvailable } = action.payload;
|
||||
if (images.length > 0) {
|
||||
state.images = state.images
|
||||
.concat(images)
|
||||
.sort((a, b) => b.mtime - a.mtime);
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
state.nextPage = nextPage;
|
||||
state.offset = offset;
|
||||
|
||||
if (!state.currentImage) {
|
||||
const newCurrentImage = images[0];
|
||||
state.currentImage = newCurrentImage;
|
||||
state.currentImageUuid = newCurrentImage.uuid;
|
||||
}
|
||||
|
||||
// keep track of the timestamps of latest and earliest images received
|
||||
state.latest_mtime = images[0].mtime;
|
||||
state.earliest_mtime = images[images.length - 1].mtime;
|
||||
}
|
||||
if (areMoreImagesAvailable !== undefined) {
|
||||
state.areMoreImagesAvailable = areMoreImagesAvailable;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.advanced-settings {
|
||||
display: grid;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.advanced-settings-item {
|
||||
display: grid;
|
||||
max-width: $options-bar-max-width;
|
||||
border: none;
|
||||
border-top: 0px;
|
||||
border-radius: 0.4rem;
|
||||
|
||||
&[aria-expanded='true'] {
|
||||
background-color: var(--tab-hover-color);
|
||||
border-radius: 0 0 0.4rem 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.advanced-settings-panel {
|
||||
background-color: var(--tab-panel-bg);
|
||||
border-radius: 0 0 0.4rem 0.4rem;
|
||||
border: 2px solid var(--tab-hover-color);
|
||||
}
|
||||
|
||||
.advanced-settings-header {
|
||||
border-radius: 0.4rem;
|
||||
|
||||
&[aria-expanded='true'] {
|
||||
background-color: var(--tab-color);
|
||||
border-radius: 0.4rem 0.4rem 0 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tab-hover-color) !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { Feature } from '../../../app/features';
|
||||
import GuideIcon from '../../../common/components/GuideIcon';
|
||||
|
||||
interface InvokeAccordionItemProps {
|
||||
header: ReactElement;
|
||||
feature: Feature;
|
||||
options: ReactElement;
|
||||
}
|
||||
|
||||
export default function InvokeAccordionItem(props: InvokeAccordionItemProps) {
|
||||
const { header, feature, options } = props;
|
||||
|
||||
return (
|
||||
<AccordionItem className="advanced-settings-item">
|
||||
<h2>
|
||||
<AccordionButton className="advanced-settings-header">
|
||||
{header}
|
||||
<GuideIcon feature={feature} />
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel className="advanced-settings-panel">
|
||||
{options}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Flex } from '@chakra-ui/layout';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setShouldRunGFPGAN } from '../../optionsSlice';
|
||||
|
||||
export default function FaceRestore() {
|
||||
const isGFPGANAvailable = useAppSelector(
|
||||
(state: RootState) => state.system.isGFPGANAvailable
|
||||
);
|
||||
|
||||
const shouldRunGFPGAN = useAppSelector(
|
||||
(state: RootState) => state.options.shouldRunGFPGAN
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeShouldRunGFPGAN = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldRunGFPGAN(e.target.checked));
|
||||
|
||||
return (
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<p>Restore Face</p>
|
||||
<IAISwitch
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
isChecked={shouldRunGFPGAN}
|
||||
onChange={handleChangeShouldRunGFPGAN}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
import { OptionsState, setGfpganStrength } from '../options/optionsSlice';
|
||||
import { RootState } from '../../../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../app/store';
|
||||
|
||||
import { OptionsState, setGfpganStrength } from '../../optionsSlice';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||
import { SystemState } from '../../../system/systemSlice';
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
@@ -42,17 +41,16 @@ const systemSelector = createSelector(
|
||||
/**
|
||||
* Displays face-fixing/GFPGAN options (strength).
|
||||
*/
|
||||
const GFPGANOptions = () => {
|
||||
const FaceRestoreOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { gfpganStrength } = useAppSelector(optionsSelector);
|
||||
const { isGFPGANAvailable } = useAppSelector(systemSelector);
|
||||
|
||||
const handleChangeStrength = (v: string | number) =>
|
||||
dispatch(setGfpganStrength(Number(v)));
|
||||
const handleChangeStrength = (v: number) => dispatch(setGfpganStrength(v));
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDNumberInput
|
||||
<IAINumberInput
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
label="Strength"
|
||||
step={0.05}
|
||||
@@ -60,9 +58,11 @@ const GFPGANOptions = () => {
|
||||
max={1}
|
||||
onChange={handleChangeStrength}
|
||||
value={gfpganStrength}
|
||||
width="90px"
|
||||
isInteger={false}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default GFPGANOptions;
|
||||
export default FaceRestoreOptions;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setShouldFitToWidthHeight } from '../../optionsSlice';
|
||||
|
||||
export default function ImageFit() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const shouldFitToWidthHeight = useAppSelector(
|
||||
(state: RootState) => state.options.shouldFitToWidthHeight
|
||||
);
|
||||
|
||||
const handleChangeFit = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldFitToWidthHeight(e.target.checked));
|
||||
|
||||
return (
|
||||
<IAISwitch
|
||||
label="Fit initial image to output size"
|
||||
isChecked={shouldFitToWidthHeight}
|
||||
onChange={handleChangeFit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Flex } from '@chakra-ui/layout';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setShouldUseInitImage } from '../../optionsSlice';
|
||||
|
||||
export default function ImageToImage() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const initialImagePath = useAppSelector(
|
||||
(state: RootState) => state.options.initialImagePath
|
||||
);
|
||||
|
||||
const shouldUseInitImage = useAppSelector(
|
||||
(state: RootState) => state.options.shouldUseInitImage
|
||||
);
|
||||
|
||||
const handleChangeShouldUseInitImage = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldUseInitImage(e.target.checked));
|
||||
return (
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<p>Image to Image</p>
|
||||
<IAISwitch
|
||||
isDisabled={!initialImagePath}
|
||||
isChecked={shouldUseInitImage}
|
||||
onChange={handleChangeShouldUseInitImage}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import InitAndMaskImage from '../../InitAndMaskImage';
|
||||
import ImageFit from './ImageFit';
|
||||
import ImageToImageStrength from './ImageToImageStrength';
|
||||
|
||||
/**
|
||||
* Options for img2img generation (strength, fit, init/mask upload).
|
||||
*/
|
||||
const ImageToImageOptions = () => {
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<ImageToImageStrength />
|
||||
<ImageFit />
|
||||
<InitAndMaskImage />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageToImageOptions;
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import { setImg2imgStrength } from '../../optionsSlice';
|
||||
|
||||
export default function ImageToImageStrength() {
|
||||
const img2imgStrength = useAppSelector(
|
||||
(state: RootState) => state.options.img2imgStrength
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeStrength = (v: number) => dispatch(setImg2imgStrength(v));
|
||||
|
||||
return (
|
||||
<IAINumberInput
|
||||
label="Strength"
|
||||
step={0.01}
|
||||
min={0.01}
|
||||
max={0.99}
|
||||
onChange={handleChangeStrength}
|
||||
value={img2imgStrength}
|
||||
width="90px"
|
||||
isInteger={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setShouldRandomizeSeed } from '../../optionsSlice';
|
||||
|
||||
export default function RandomizeSeed() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const shouldRandomizeSeed = useAppSelector(
|
||||
(state: RootState) => state.options.shouldRandomizeSeed
|
||||
);
|
||||
|
||||
const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldRandomizeSeed(e.target.checked));
|
||||
|
||||
return (
|
||||
<IAISwitch
|
||||
label="Randomize Seed"
|
||||
isChecked={shouldRandomizeSeed}
|
||||
onChange={handleChangeShouldRandomizeSeed}
|
||||
/>
|
||||
);
|
||||
}
|
||||
39
frontend/src/features/options/AdvancedOptions/Seed/Seed.tsx
Normal file
39
frontend/src/features/options/AdvancedOptions/Seed/Seed.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../../../app/constants';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import { setSeed } from '../../optionsSlice';
|
||||
|
||||
export default function Seed() {
|
||||
const seed = useAppSelector((state: RootState) => state.options.seed);
|
||||
const shouldRandomizeSeed = useAppSelector(
|
||||
(state: RootState) => state.options.shouldRandomizeSeed
|
||||
);
|
||||
const shouldGenerateVariations = useAppSelector(
|
||||
(state: RootState) => state.options.shouldGenerateVariations
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeSeed = (v: number) => dispatch(setSeed(v));
|
||||
|
||||
return (
|
||||
<IAINumberInput
|
||||
label="Seed"
|
||||
step={1}
|
||||
precision={0}
|
||||
flexGrow={1}
|
||||
min={NUMPY_RAND_MIN}
|
||||
max={NUMPY_RAND_MAX}
|
||||
isDisabled={shouldRandomizeSeed}
|
||||
isInvalid={seed < 0 && shouldGenerateVariations}
|
||||
onChange={handleChangeSeed}
|
||||
value={seed}
|
||||
width="10rem"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import RandomizeSeed from './RandomizeSeed';
|
||||
import Seed from './Seed';
|
||||
import ShuffleSeed from './ShuffleSeed';
|
||||
|
||||
/**
|
||||
* Seed & variation options. Includes iteration, seed, seed randomization, variation options.
|
||||
*/
|
||||
const SeedOptions = () => {
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<RandomizeSeed />
|
||||
<Flex gap={2}>
|
||||
<Seed />
|
||||
<ShuffleSeed />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeedOptions;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../../../app/constants';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import randomInt from '../../../../common/util/randomInt';
|
||||
import { setSeed } from '../../optionsSlice';
|
||||
|
||||
export default function ShuffleSeed() {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldRandomizeSeed = useAppSelector(
|
||||
(state: RootState) => state.options.shouldRandomizeSeed
|
||||
);
|
||||
|
||||
const handleClickRandomizeSeed = () =>
|
||||
dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)));
|
||||
|
||||
return (
|
||||
<Button
|
||||
size={'sm'}
|
||||
isDisabled={shouldRandomizeSeed}
|
||||
onClick={handleClickRandomizeSeed}
|
||||
>
|
||||
<p>Shuffle</p>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Flex } from '@chakra-ui/layout';
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setShouldRunESRGAN } from '../../optionsSlice';
|
||||
|
||||
export default function Upscale() {
|
||||
const isESRGANAvailable = useAppSelector(
|
||||
(state: RootState) => state.system.isESRGANAvailable
|
||||
);
|
||||
|
||||
const shouldRunESRGAN = useAppSelector(
|
||||
(state: RootState) => state.options.shouldRunESRGAN
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleChangeShouldRunESRGAN = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldRunESRGAN(e.target.checked));
|
||||
return (
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<p>Upscale</p>
|
||||
<IAISwitch
|
||||
isDisabled={!isESRGANAvailable}
|
||||
isChecked={shouldRunESRGAN}
|
||||
onChange={handleChangeShouldRunESRGAN}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.upscale-options {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../../../app/store';
|
||||
|
||||
import {
|
||||
setUpscalingLevel,
|
||||
setUpscalingStrength,
|
||||
UpscalingLevel,
|
||||
OptionsState,
|
||||
} from '../options/optionsSlice';
|
||||
} from '../../optionsSlice';
|
||||
|
||||
|
||||
import { UPSCALING_LEVELS } from '../../app/constants';
|
||||
import { UPSCALING_LEVELS } from '../../../../app/constants';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import { SystemState } from '../../../system/systemSlice';
|
||||
import { ChangeEvent } from 'react';
|
||||
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||
import SDSelect from '../../common/components/SDSelect';
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import IAISelect from '../../../../common/components/IAISelect';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
@@ -51,27 +48,27 @@ const systemSelector = createSelector(
|
||||
/**
|
||||
* Displays upscaling/ESRGAN options (level and strength).
|
||||
*/
|
||||
const ESRGANOptions = () => {
|
||||
const UpscaleOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { upscalingLevel, upscalingStrength } = useAppSelector(optionsSelector);
|
||||
|
||||
const { isESRGANAvailable } = useAppSelector(systemSelector);
|
||||
|
||||
const handleChangeLevel = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setUpscalingLevel(Number(e.target.value) as UpscalingLevel));
|
||||
|
||||
const handleChangeStrength = (v: string | number) =>
|
||||
dispatch(setUpscalingStrength(Number(v)));
|
||||
const handleChangeStrength = (v: number) => dispatch(setUpscalingStrength(v));
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDSelect
|
||||
<div className='upscale-options'>
|
||||
<IAISelect
|
||||
isDisabled={!isESRGANAvailable}
|
||||
label="Scale"
|
||||
value={upscalingLevel}
|
||||
onChange={handleChangeLevel}
|
||||
validValues={UPSCALING_LEVELS}
|
||||
/>
|
||||
<SDNumberInput
|
||||
<IAINumberInput
|
||||
isDisabled={!isESRGANAvailable}
|
||||
label="Strength"
|
||||
step={0.05}
|
||||
@@ -79,9 +76,10 @@ const ESRGANOptions = () => {
|
||||
max={1}
|
||||
onChange={handleChangeStrength}
|
||||
value={upscalingStrength}
|
||||
isInteger={false}
|
||||
/>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ESRGANOptions;
|
||||
export default UpscaleOptions;
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAISwitch from '../../../../common/components/IAISwitch';
|
||||
import { setShouldGenerateVariations } from '../../optionsSlice';
|
||||
|
||||
export default function GenerateVariations() {
|
||||
const shouldGenerateVariations = useAppSelector(
|
||||
(state: RootState) => state.options.shouldGenerateVariations
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeShouldGenerateVariations = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
) => dispatch(setShouldGenerateVariations(e.target.checked));
|
||||
|
||||
return (
|
||||
<IAISwitch
|
||||
isChecked={shouldGenerateVariations}
|
||||
width={'auto'}
|
||||
onChange={handleChangeShouldGenerateVariations}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAIInput from '../../../../common/components/IAIInput';
|
||||
import { validateSeedWeights } from '../../../../common/util/seedWeightPairs';
|
||||
import { setSeedWeights } from '../../optionsSlice';
|
||||
|
||||
export default function SeedWeights() {
|
||||
const seedWeights = useAppSelector(
|
||||
(state: RootState) => state.options.seedWeights
|
||||
);
|
||||
|
||||
const shouldGenerateVariations = useAppSelector(
|
||||
(state: RootState) => state.options.shouldGenerateVariations
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeSeedWeights = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setSeedWeights(e.target.value));
|
||||
|
||||
return (
|
||||
<IAIInput
|
||||
label={'Seed Weights'}
|
||||
value={seedWeights}
|
||||
isInvalid={
|
||||
shouldGenerateVariations &&
|
||||
!(validateSeedWeights(seedWeights) || seedWeights === '')
|
||||
}
|
||||
isDisabled={!shouldGenerateVariations}
|
||||
onChange={handleChangeSeedWeights}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
RootState,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '../../../../app/store';
|
||||
import IAINumberInput from '../../../../common/components/IAINumberInput';
|
||||
import { setVariationAmount } from '../../optionsSlice';
|
||||
|
||||
export default function VariationAmount() {
|
||||
const variationAmount = useAppSelector(
|
||||
(state: RootState) => state.options.variationAmount
|
||||
);
|
||||
|
||||
const shouldGenerateVariations = useAppSelector(
|
||||
(state: RootState) => state.options.shouldGenerateVariations
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleChangevariationAmount = (v: number) =>
|
||||
dispatch(setVariationAmount(v));
|
||||
|
||||
return (
|
||||
<IAINumberInput
|
||||
label="Variation Amount"
|
||||
value={variationAmount}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={1}
|
||||
isDisabled={!shouldGenerateVariations}
|
||||
onChange={handleChangevariationAmount}
|
||||
isInteger={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import GenerateVariations from './GenerateVariations';
|
||||
|
||||
export default function Variations() {
|
||||
return (
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<p>Variations</p>
|
||||
<GenerateVariations />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import SeedWeights from './SeedWeights';
|
||||
import VariationAmount from './VariationAmount';
|
||||
|
||||
/**
|
||||
* Seed & variation options. Includes iteration, seed, seed randomization, variation options.
|
||||
*/
|
||||
const VariationsOptions = () => {
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<VariationAmount />
|
||||
<SeedWeights />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default VariationsOptions;
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||
import SDSwitch from '../../common/components/SDSwitch';
|
||||
import InitAndMaskImage from './InitAndMaskImage';
|
||||
import {
|
||||
OptionsState,
|
||||
setImg2imgStrength,
|
||||
setShouldFitToWidthHeight,
|
||||
} from './optionsSlice';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
img2imgStrength: options.img2imgStrength,
|
||||
shouldFitToWidthHeight: options.shouldFitToWidthHeight,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Options for img2img generation (strength, fit, init/mask upload).
|
||||
*/
|
||||
const ImageToImageOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { img2imgStrength, shouldFitToWidthHeight } =
|
||||
useAppSelector(optionsSelector);
|
||||
|
||||
const handleChangeStrength = (v: string | number) =>
|
||||
dispatch(setImg2imgStrength(Number(v)));
|
||||
|
||||
const handleChangeFit = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldFitToWidthHeight(e.target.checked));
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDNumberInput
|
||||
label="Strength"
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={handleChangeStrength}
|
||||
value={img2imgStrength}
|
||||
/>
|
||||
<SDSwitch
|
||||
label="Fit initial image to output size"
|
||||
isChecked={shouldFitToWidthHeight}
|
||||
onChange={handleChangeFit}
|
||||
/>
|
||||
<InitAndMaskImage />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageToImageOptions;
|
||||
@@ -2,7 +2,7 @@ import { Flex, Image } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { OptionsState } from '../../features/options/optionsSlice';
|
||||
import { OptionsState } from './optionsSlice';
|
||||
import './InitAndMaskImage.css';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
@@ -36,12 +36,14 @@ const InitAndMaskImage = () => {
|
||||
src={initialImagePath}
|
||||
rounded={'md'}
|
||||
className={'checkerboard'}
|
||||
maxWidth={320}
|
||||
/>
|
||||
{shouldShowMask && maskPath && (
|
||||
<Image
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
maxWidth={320}
|
||||
fit={'contain'}
|
||||
src={maskPath}
|
||||
rounded={'md'}
|
||||
|
||||
@@ -3,11 +3,7 @@ import { SyntheticEvent, useCallback } from 'react';
|
||||
import { FaTrash, FaUpload } from 'react-icons/fa';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import {
|
||||
OptionsState,
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
} from '../../features/options/optionsSlice';
|
||||
import { OptionsState, setInitialImagePath, setMaskPath } from './optionsSlice';
|
||||
import {
|
||||
uploadInitialImage,
|
||||
uploadMaskImage,
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import { setShowAdvancedOptions } from '../optionsSlice';
|
||||
|
||||
export default function MainAdvancedOptions() {
|
||||
const showAdvancedOptions = useAppSelector(
|
||||
(state: RootState) => state.options.showAdvancedOptions
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleShowAdvancedOptions = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShowAdvancedOptions(e.target.checked));
|
||||
|
||||
return (
|
||||
<div className="advanced_options_checker">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="advanced_options"
|
||||
id=""
|
||||
onChange={handleShowAdvancedOptions}
|
||||
checked={showAdvancedOptions}
|
||||
/>
|
||||
<label htmlFor="advanced_options">Advanced Options</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
frontend/src/features/options/MainOptions/MainCFGScale.tsx
Normal file
28
frontend/src/features/options/MainOptions/MainCFGScale.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAINumberInput from '../../../common/components/IAINumberInput';
|
||||
import { setCfgScale } from '../optionsSlice';
|
||||
import { fontSize, inputWidth } from './MainOptions';
|
||||
|
||||
export default function MainCFGScale() {
|
||||
const dispatch = useAppDispatch();
|
||||
const cfgScale = useAppSelector((state: RootState) => state.options.cfgScale);
|
||||
|
||||
const handleChangeCfgScale = (v: number) => dispatch(setCfgScale(v));
|
||||
|
||||
return (
|
||||
<IAINumberInput
|
||||
label="CFG Scale"
|
||||
step={0.5}
|
||||
min={1}
|
||||
max={30}
|
||||
onChange={handleChangeCfgScale}
|
||||
value={cfgScale}
|
||||
width={inputWidth}
|
||||
fontSize={fontSize}
|
||||
styleClass="main-option-block"
|
||||
textAlign="center"
|
||||
isInteger={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
frontend/src/features/options/MainOptions/MainHeight.tsx
Normal file
26
frontend/src/features/options/MainOptions/MainHeight.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { HEIGHTS } from '../../../app/constants';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAISelect from '../../../common/components/IAISelect';
|
||||
import { setHeight } from '../optionsSlice';
|
||||
import { fontSize } from './MainOptions';
|
||||
|
||||
export default function MainHeight() {
|
||||
const height = useAppSelector((state: RootState) => state.options.height);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setHeight(Number(e.target.value)));
|
||||
|
||||
return (
|
||||
<IAISelect
|
||||
label="Height"
|
||||
value={height}
|
||||
flexGrow={1}
|
||||
onChange={handleChangeHeight}
|
||||
validValues={HEIGHTS}
|
||||
fontSize={fontSize}
|
||||
styleClass="main-option-block"
|
||||
/>
|
||||
);
|
||||
}
|
||||
29
frontend/src/features/options/MainOptions/MainIterations.tsx
Normal file
29
frontend/src/features/options/MainOptions/MainIterations.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAINumberInput from '../../../common/components/IAINumberInput';
|
||||
import { setIterations } from '../optionsSlice';
|
||||
import { fontSize, inputWidth } from './MainOptions';
|
||||
|
||||
export default function MainIterations() {
|
||||
const dispatch = useAppDispatch();
|
||||
const iterations = useAppSelector(
|
||||
(state: RootState) => state.options.iterations
|
||||
);
|
||||
|
||||
const handleChangeIterations = (v: number) => dispatch(setIterations(v));
|
||||
|
||||
return (
|
||||
<IAINumberInput
|
||||
label="Images"
|
||||
step={1}
|
||||
min={1}
|
||||
max={9999}
|
||||
onChange={handleChangeIterations}
|
||||
value={iterations}
|
||||
width={inputWidth}
|
||||
fontSize={fontSize}
|
||||
styleClass="main-option-block"
|
||||
textAlign="center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
82
frontend/src/features/options/MainOptions/MainOptions.scss
Normal file
82
frontend/src/features/options/MainOptions/MainOptions.scss
Normal file
@@ -0,0 +1,82 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.main-options {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
.main-options-list {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
}
|
||||
|
||||
.main-options-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, auto);
|
||||
column-gap: 1rem;
|
||||
max-width: $options-bar-max-width;
|
||||
}
|
||||
|
||||
.main-option-block {
|
||||
border-radius: 0.5rem;
|
||||
grid-template-columns: auto !important;
|
||||
row-gap: 0.4rem;
|
||||
|
||||
.number-input-label,
|
||||
.iai-select-label {
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.number-input-entry {
|
||||
padding: 0;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.iai-select-picker {
|
||||
height: 2.4rem;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.advanced_options_checker {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
column-gap: 0.5rem;
|
||||
align-items: center;
|
||||
background-color: var(--background-color-secondary);
|
||||
padding: 1rem;
|
||||
font-weight: bold;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
input[type='checkbox'] {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background-color: var(--input-checkbox-bg);
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 0.2rem;
|
||||
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
transform: scale(0);
|
||||
transition: 120ms transform ease-in-out;
|
||||
border-radius: 0.2rem;
|
||||
box-shadow: inset 1rem 1rem var(--input-checkbox-checked-tick);
|
||||
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background-color: var(--input-checkbox-checked-bg);
|
||||
&::before {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
frontend/src/features/options/MainOptions/MainOptions.tsx
Normal file
30
frontend/src/features/options/MainOptions/MainOptions.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import MainAdvancedOptions from './MainAdvancedOptions';
|
||||
import MainCFGScale from './MainCFGScale';
|
||||
import MainHeight from './MainHeight';
|
||||
import MainIterations from './MainIterations';
|
||||
import MainSampler from './MainSampler';
|
||||
import MainSteps from './MainSteps';
|
||||
import MainWidth from './MainWidth';
|
||||
|
||||
export const fontSize = '0.9rem';
|
||||
export const inputWidth = 'auto';
|
||||
|
||||
export default function MainOptions() {
|
||||
return (
|
||||
<div className="main-options">
|
||||
<div className="main-options-list">
|
||||
<div className="main-options-row">
|
||||
<MainIterations />
|
||||
<MainSteps />
|
||||
<MainCFGScale />
|
||||
</div>
|
||||
<div className="main-options-row">
|
||||
<MainWidth />
|
||||
<MainHeight />
|
||||
<MainSampler />
|
||||
</div>
|
||||
<MainAdvancedOptions />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
25
frontend/src/features/options/MainOptions/MainSampler.tsx
Normal file
25
frontend/src/features/options/MainOptions/MainSampler.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { SAMPLERS } from '../../../app/constants';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAISelect from '../../../common/components/IAISelect';
|
||||
import { setSampler } from '../optionsSlice';
|
||||
import { fontSize } from './MainOptions';
|
||||
|
||||
export default function MainSampler() {
|
||||
const sampler = useAppSelector((state: RootState) => state.options.sampler);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setSampler(e.target.value));
|
||||
|
||||
return (
|
||||
<IAISelect
|
||||
label="Sampler"
|
||||
value={sampler}
|
||||
onChange={handleChangeSampler}
|
||||
validValues={SAMPLERS}
|
||||
fontSize={fontSize}
|
||||
styleClass="main-option-block"
|
||||
/>
|
||||
);
|
||||
}
|
||||
27
frontend/src/features/options/MainOptions/MainSteps.tsx
Normal file
27
frontend/src/features/options/MainOptions/MainSteps.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAINumberInput from '../../../common/components/IAINumberInput';
|
||||
import { setSteps } from '../optionsSlice';
|
||||
import { fontSize, inputWidth } from './MainOptions';
|
||||
|
||||
export default function MainSteps() {
|
||||
const dispatch = useAppDispatch();
|
||||
const steps = useAppSelector((state: RootState) => state.options.steps);
|
||||
|
||||
const handleChangeSteps = (v: number) => dispatch(setSteps(v));
|
||||
|
||||
return (
|
||||
<IAINumberInput
|
||||
label="Steps"
|
||||
min={1}
|
||||
max={9999}
|
||||
step={1}
|
||||
onChange={handleChangeSteps}
|
||||
value={steps}
|
||||
width={inputWidth}
|
||||
fontSize={fontSize}
|
||||
styleClass="main-option-block"
|
||||
textAlign="center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
frontend/src/features/options/MainOptions/MainWidth.tsx
Normal file
26
frontend/src/features/options/MainOptions/MainWidth.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { WIDTHS } from '../../../app/constants';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAISelect from '../../../common/components/IAISelect';
|
||||
import { setWidth } from '../optionsSlice';
|
||||
import { fontSize } from './MainOptions';
|
||||
|
||||
export default function MainWidth() {
|
||||
const width = useAppSelector((state: RootState) => state.options.width);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setWidth(Number(e.target.value)));
|
||||
|
||||
return (
|
||||
<IAISelect
|
||||
label="Width"
|
||||
value={width}
|
||||
flexGrow={1}
|
||||
onChange={handleChangeWidth}
|
||||
validValues={WIDTHS}
|
||||
fontSize={fontSize}
|
||||
styleClass="main-option-block"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,85 +1,37 @@
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Text,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionPanel,
|
||||
Switch,
|
||||
ExpandedIndex,
|
||||
// ExpandedIndex,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
// import { RootState } from '../../app/store';
|
||||
// import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
// import { setOpenAccordions } from '../system/systemSlice';
|
||||
|
||||
import {
|
||||
setShouldRunGFPGAN,
|
||||
setShouldRunESRGAN,
|
||||
OptionsState,
|
||||
setShouldUseInitImage,
|
||||
} from '../options/optionsSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { setOpenAccordions, SystemState } from '../system/systemSlice';
|
||||
import SeedVariationOptions from './SeedVariationOptions';
|
||||
import SamplerOptions from './SamplerOptions';
|
||||
import ESRGANOptions from './ESRGANOptions';
|
||||
import GFPGANOptions from './GFPGANOptions';
|
||||
import OutputOptions from './OutputOptions';
|
||||
import ImageToImageOptions from './ImageToImageOptions';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
import GuideIcon from '../../common/components/GuideIcon';
|
||||
import ImageToImageOptions from './AdvancedOptions/ImageToImage/ImageToImageOptions';
|
||||
import { Feature } from '../../app/features';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
initialImagePath: options.initialImagePath,
|
||||
shouldUseInitImage: options.shouldUseInitImage,
|
||||
shouldRunESRGAN: options.shouldRunESRGAN,
|
||||
shouldRunGFPGAN: options.shouldRunGFPGAN,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
openAccordions: system.openAccordions,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
import SeedOptions from './AdvancedOptions/Seed/SeedOptions';
|
||||
import Upscale from './AdvancedOptions/Upscale/Upscale';
|
||||
import UpscaleOptions from './AdvancedOptions/Upscale/UpscaleOptions';
|
||||
import FaceRestore from './AdvancedOptions/FaceRestore/FaceRestore';
|
||||
import FaceRestoreOptions from './AdvancedOptions/FaceRestore/FaceRestoreOptions';
|
||||
import ImageToImage from './AdvancedOptions/ImageToImage/ImageToImage';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { setOpenAccordions } from '../system/systemSlice';
|
||||
import InvokeAccordionItem from './AccordionItems/InvokeAccordionItem';
|
||||
import Variations from './AdvancedOptions/Variations/Variations';
|
||||
import VariationsOptions from './AdvancedOptions/Variations/VariationsOptions';
|
||||
|
||||
/**
|
||||
* Main container for generation and processing parameters.
|
||||
*/
|
||||
const OptionsAccordion = () => {
|
||||
const {
|
||||
shouldRunESRGAN,
|
||||
shouldRunGFPGAN,
|
||||
shouldUseInitImage,
|
||||
initialImagePath,
|
||||
} = useAppSelector(optionsSelector);
|
||||
|
||||
const { isGFPGANAvailable, isESRGANAvailable, openAccordions } =
|
||||
useAppSelector(systemSelector);
|
||||
const openAccordions = useAppSelector(
|
||||
(state: RootState) => state.system.openAccordions
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -89,136 +41,57 @@ const OptionsAccordion = () => {
|
||||
const handleChangeAccordionState = (openAccordions: ExpandedIndex) =>
|
||||
dispatch(setOpenAccordions(openAccordions));
|
||||
|
||||
const handleChangeShouldRunESRGAN = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldRunESRGAN(e.target.checked));
|
||||
|
||||
const handleChangeShouldRunGFPGAN = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldRunGFPGAN(e.target.checked));
|
||||
|
||||
const handleChangeShouldUseInitImage = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldUseInitImage(e.target.checked));
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
defaultIndex={openAccordions}
|
||||
allowMultiple
|
||||
reduceMotion
|
||||
onChange={handleChangeAccordionState}
|
||||
className="advanced-settings"
|
||||
>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Seed & Variation
|
||||
</Box>
|
||||
<GuideIcon feature={Feature.SEED_AND_VARIATION} />
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<SeedVariationOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Sampler
|
||||
</Box>
|
||||
<GuideIcon feature={Feature.SAMPLER} />
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<SamplerOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Upscale (ESRGAN)</Text>
|
||||
<Switch
|
||||
isDisabled={!isESRGANAvailable}
|
||||
isChecked={shouldRunESRGAN}
|
||||
onChange={handleChangeShouldRunESRGAN}
|
||||
/>
|
||||
</Flex>
|
||||
<GuideIcon feature={Feature.ESRGAN} />
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<ESRGANOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Face Correction</Text>
|
||||
<Switch
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
isChecked={shouldRunGFPGAN}
|
||||
onChange={handleChangeShouldRunGFPGAN}
|
||||
/>
|
||||
</Flex>
|
||||
<GuideIcon feature={Feature.FACE_CORRECTION} />
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<GFPGANOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Image to Image</Text>
|
||||
<Switch
|
||||
isDisabled={!initialImagePath}
|
||||
isChecked={shouldUseInitImage}
|
||||
onChange={handleChangeShouldUseInitImage}
|
||||
/>
|
||||
</Flex>
|
||||
<GuideIcon feature={Feature.IMAGE_TO_IMAGE} />
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<ImageToImageOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Output
|
||||
</Box>
|
||||
<GuideIcon feature={Feature.OUTPUT} />
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<OutputOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<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 />}
|
||||
/>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,69 +1,24 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
import { setHeight, setWidth, setSeamless, OptionsState } from '../options/optionsSlice';
|
||||
|
||||
|
||||
import { HEIGHTS, WIDTHS } from '../../app/constants';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { setSeamless } from './optionsSlice';
|
||||
import { ChangeEvent } from 'react';
|
||||
import SDSelect from '../../common/components/SDSelect';
|
||||
import SDSwitch from '../../common/components/SDSwitch';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
height: options.height,
|
||||
width: options.width,
|
||||
seamless: options.seamless,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
import IAISwitch from '../../common/components/IAISwitch';
|
||||
|
||||
/**
|
||||
* Image output options. Includes width, height, seamless tiling.
|
||||
*/
|
||||
const OutputOptions = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { height, width, seamless } = useAppSelector(optionsSelector);
|
||||
|
||||
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setWidth(Number(e.target.value)));
|
||||
|
||||
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setHeight(Number(e.target.value)));
|
||||
const seamless = useAppSelector((state: RootState) => state.options.seamless);
|
||||
|
||||
const handleChangeSeamless = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setSeamless(e.target.checked));
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<Flex gap={2}>
|
||||
<SDSelect
|
||||
label="Width"
|
||||
value={width}
|
||||
flexGrow={1}
|
||||
onChange={handleChangeWidth}
|
||||
validValues={WIDTHS}
|
||||
/>
|
||||
<SDSelect
|
||||
label="Height"
|
||||
value={height}
|
||||
flexGrow={1}
|
||||
onChange={handleChangeHeight}
|
||||
validValues={HEIGHTS}
|
||||
/>
|
||||
</Flex>
|
||||
<SDSwitch
|
||||
<IAISwitch
|
||||
label="Seamless tiling"
|
||||
fontSize={'md'}
|
||||
isChecked={seamless}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { cancelProcessing, generateImage } from '../../app/socketio/actions';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDButton from '../../common/components/SDButton';
|
||||
import useCheckParameters from '../../common/hooks/useCheckParameters';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Buttons to start and cancel image generation.
|
||||
*/
|
||||
const ProcessButtons = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
const isReady = useCheckParameters();
|
||||
|
||||
const handleClickGenerate = () => dispatch(generateImage());
|
||||
|
||||
const handleClickCancel = () => dispatch(cancelProcessing());
|
||||
|
||||
return (
|
||||
<Flex
|
||||
gap={2}
|
||||
direction={'column'}
|
||||
alignItems={'space-between'}
|
||||
height={'100%'}
|
||||
>
|
||||
<SDButton
|
||||
label="Generate"
|
||||
type="submit"
|
||||
colorScheme="green"
|
||||
flexGrow={1}
|
||||
isDisabled={!isReady}
|
||||
fontSize={'md'}
|
||||
size={'md'}
|
||||
onClick={handleClickGenerate}
|
||||
/>
|
||||
<SDButton
|
||||
label="Cancel"
|
||||
colorScheme="red"
|
||||
flexGrow={1}
|
||||
fontSize={'md'}
|
||||
size={'md'}
|
||||
isDisabled={!isConnected || !isProcessing}
|
||||
onClick={handleClickCancel}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessButtons;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { MdCancel } from 'react-icons/md';
|
||||
import { cancelProcessing } from '../../../app/socketio/actions';
|
||||
import { useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||
import { systemSelector } from '../../../common/hooks/useCheckParameters';
|
||||
|
||||
export default function CancelButton() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
const handleClickCancel = () => dispatch(cancelProcessing());
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
icon={<MdCancel />}
|
||||
tooltip="Cancel"
|
||||
aria-label="Cancel"
|
||||
isDisabled={!isConnected || !isProcessing}
|
||||
onClick={handleClickCancel}
|
||||
className="cancel-btn"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { MdAddAPhoto } from 'react-icons/md';
|
||||
import { generateImage } from '../../../app/socketio/actions';
|
||||
import { useAppDispatch } from '../../../app/store';
|
||||
import IAIIconButton from '../../../common/components/IAIIconButton';
|
||||
import useCheckParameters from '../../../common/hooks/useCheckParameters';
|
||||
|
||||
export default function InvokeButton() {
|
||||
const dispatch = useAppDispatch();
|
||||
const isReady = useCheckParameters();
|
||||
|
||||
const handleClickGenerate = () => {
|
||||
dispatch(generateImage());
|
||||
};
|
||||
|
||||
return (
|
||||
<IAIIconButton
|
||||
icon={<MdAddAPhoto />}
|
||||
tooltip="Invoke"
|
||||
aria-label="Invoke"
|
||||
type="submit"
|
||||
isDisabled={!isReady}
|
||||
onClick={handleClickGenerate}
|
||||
className="invoke-btn"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.process-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
.invoke-btn {
|
||||
@include Button(
|
||||
$btn-color: var(--btn-purple),
|
||||
$btn-color-hover: var(--btn-purple-hover),
|
||||
$btn-width: 5rem
|
||||
);
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
@include Button(
|
||||
$btn-color: var(--btn-red),
|
||||
$btn-color-hover: var(--btn-red-hover),
|
||||
$btn-width: 3rem
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import InvokeButton from './InvokeButton';
|
||||
import CancelButton from './CancelButton';
|
||||
|
||||
/**
|
||||
* Buttons to start and cancel image generation.
|
||||
*/
|
||||
const ProcessButtons = () => {
|
||||
return (
|
||||
<div className="process-buttons">
|
||||
<InvokeButton />
|
||||
<CancelButton />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessButtons;
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Textarea } from '@chakra-ui/react';
|
||||
import {
|
||||
ChangeEvent,
|
||||
KeyboardEvent,
|
||||
} from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { generateImage } from '../../app/socketio/actions';
|
||||
import { RootState } from '../../app/store';
|
||||
import { setPrompt } from '../options/optionsSlice';
|
||||
|
||||
/**
|
||||
* Prompt input text area.
|
||||
*/
|
||||
const PromptInput = () => {
|
||||
const { prompt } = useAppSelector((state: RootState) => state.options);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangePrompt = (e: ChangeEvent<HTMLTextAreaElement>) =>
|
||||
dispatch(setPrompt(e.target.value));
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && e.shiftKey === false) {
|
||||
e.preventDefault();
|
||||
dispatch(generateImage())
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
resize="none"
|
||||
size={'lg'}
|
||||
height={'100%'}
|
||||
isInvalid={!prompt.length}
|
||||
onChange={handleChangePrompt}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={prompt}
|
||||
placeholder="I'm dreaming of..."
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptInput;
|
||||
34
frontend/src/features/options/PromptInput/PromptInput.scss
Normal file
34
frontend/src/features/options/PromptInput/PromptInput.scss
Normal file
@@ -0,0 +1,34 @@
|
||||
.prompt-bar {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
|
||||
input,
|
||||
textarea {
|
||||
background-color: var(--prompt-bg-color);
|
||||
font-size: 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
border: 2px solid var(--border-color-light);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border: 2px solid var(--prompt-border-color);
|
||||
box-shadow: 0 0 10px 0 var(--prompt-box-shadow-color);
|
||||
}
|
||||
|
||||
&[aria-invalid='true'] {
|
||||
border: 2px solid var(--border-color-invalid);
|
||||
box-shadow: 0 0 10px 0 var(--box-shadow-color-invalid);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
border: 2px solid var(--border-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 10rem;
|
||||
}
|
||||
}
|
||||
69
frontend/src/features/options/PromptInput/PromptInput.tsx
Normal file
69
frontend/src/features/options/PromptInput/PromptInput.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { FormControl, Textarea } from '@chakra-ui/react';
|
||||
import { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../../app/store';
|
||||
import { generateImage } from '../../../app/socketio/actions';
|
||||
|
||||
import { OptionsState, setPrompt } from '../optionsSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import useCheckParameters, {
|
||||
systemSelector,
|
||||
} from '../../../common/hooks/useCheckParameters';
|
||||
|
||||
export const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
prompt: options.prompt,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Prompt input text area.
|
||||
*/
|
||||
const PromptInput = () => {
|
||||
const { prompt } = useAppSelector(optionsSelector);
|
||||
const { isProcessing } = useAppSelector(systemSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
const isReady = useCheckParameters();
|
||||
|
||||
const handleChangePrompt = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
dispatch(setPrompt(e.target.value));
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && e.shiftKey === false && isReady) {
|
||||
e.preventDefault();
|
||||
dispatch(generateImage());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="prompt-bar">
|
||||
<FormControl
|
||||
isInvalid={prompt.length === 0 || Boolean(prompt.match(/^[\s\r\n]+$/))}
|
||||
isDisabled={isProcessing}
|
||||
>
|
||||
<Textarea
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
placeholder="I'm dreaming of..."
|
||||
size={'lg'}
|
||||
value={prompt}
|
||||
onChange={handleChangePrompt}
|
||||
onKeyDown={handleKeyDown}
|
||||
resize="vertical"
|
||||
height={30}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptInput;
|
||||
@@ -3,15 +3,19 @@ import { Flex } from '@chakra-ui/react';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
|
||||
import { setCfgScale, setSampler, setSteps, OptionsState } from '../options/optionsSlice';
|
||||
|
||||
import {
|
||||
setCfgScale,
|
||||
setSampler,
|
||||
setSteps,
|
||||
OptionsState,
|
||||
} from './optionsSlice';
|
||||
|
||||
import { SAMPLERS } from '../../app/constants';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ChangeEvent } from 'react';
|
||||
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||
import SDSelect from '../../common/components/SDSelect';
|
||||
import IAINumberInput from '../../common/components/IAINumberInput';
|
||||
import IAISelect from '../../common/components/IAISelect';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
@@ -47,21 +51,21 @@ const SamplerOptions = () => {
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<SDNumberInput
|
||||
{/* <IAINumberInput
|
||||
label="Steps"
|
||||
min={1}
|
||||
step={1}
|
||||
precision={0}
|
||||
onChange={handleChangeSteps}
|
||||
value={steps}
|
||||
/>
|
||||
<SDNumberInput
|
||||
/> */}
|
||||
{/* <IAINumberInput
|
||||
label="CFG scale"
|
||||
step={0.5}
|
||||
onChange={handleChangeCfgScale}
|
||||
value={cfgScale}
|
||||
/>
|
||||
<SDSelect
|
||||
/> */}
|
||||
<IAISelect
|
||||
label="Sampler"
|
||||
value={sampler}
|
||||
onChange={handleChangeSampler}
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import {
|
||||
Flex,
|
||||
Input,
|
||||
HStack,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Text,
|
||||
Button,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDNumberInput from '../../common/components/SDNumberInput';
|
||||
import SDSwitch from '../../common/components/SDSwitch';
|
||||
import randomInt from '../../common/util/randomInt';
|
||||
import { validateSeedWeights } from '../../common/util/seedWeightPairs';
|
||||
import {
|
||||
OptionsState,
|
||||
setIterations,
|
||||
setSeed,
|
||||
setSeedWeights,
|
||||
setShouldGenerateVariations,
|
||||
setShouldRandomizeSeed,
|
||||
setVariationAmount,
|
||||
} from './optionsSlice';
|
||||
|
||||
const optionsSelector = createSelector(
|
||||
(state: RootState) => state.options,
|
||||
(options: OptionsState) => {
|
||||
return {
|
||||
variationAmount: options.variationAmount,
|
||||
seedWeights: options.seedWeights,
|
||||
shouldGenerateVariations: options.shouldGenerateVariations,
|
||||
shouldRandomizeSeed: options.shouldRandomizeSeed,
|
||||
seed: options.seed,
|
||||
iterations: options.iterations,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Seed & variation options. Includes iteration, seed, seed randomization, variation options.
|
||||
*/
|
||||
const SeedVariationOptions = () => {
|
||||
const {
|
||||
shouldGenerateVariations,
|
||||
variationAmount,
|
||||
seedWeights,
|
||||
shouldRandomizeSeed,
|
||||
seed,
|
||||
iterations,
|
||||
} = useAppSelector(optionsSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangeIterations = (v: string | number) =>
|
||||
dispatch(setIterations(Number(v)));
|
||||
|
||||
const handleChangeShouldRandomizeSeed = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setShouldRandomizeSeed(e.target.checked));
|
||||
|
||||
const handleChangeSeed = (v: string | number) => dispatch(setSeed(Number(v)));
|
||||
|
||||
const handleClickRandomizeSeed = () =>
|
||||
dispatch(setSeed(randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX)));
|
||||
|
||||
const handleChangeShouldGenerateVariations = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
) => dispatch(setShouldGenerateVariations(e.target.checked));
|
||||
|
||||
const handleChangevariationAmount = (v: string | number) =>
|
||||
dispatch(setVariationAmount(Number(v)));
|
||||
|
||||
const handleChangeSeedWeights = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setSeedWeights(e.target.value));
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<SDNumberInput
|
||||
label="Images to generate"
|
||||
step={1}
|
||||
min={1}
|
||||
precision={0}
|
||||
onChange={handleChangeIterations}
|
||||
value={iterations}
|
||||
/>
|
||||
<SDSwitch
|
||||
label="Randomize seed on generation"
|
||||
isChecked={shouldRandomizeSeed}
|
||||
onChange={handleChangeShouldRandomizeSeed}
|
||||
/>
|
||||
<Flex gap={2}>
|
||||
<SDNumberInput
|
||||
label="Seed"
|
||||
step={1}
|
||||
precision={0}
|
||||
flexGrow={1}
|
||||
min={NUMPY_RAND_MIN}
|
||||
max={NUMPY_RAND_MAX}
|
||||
isDisabled={shouldRandomizeSeed}
|
||||
isInvalid={seed < 0 && shouldGenerateVariations}
|
||||
onChange={handleChangeSeed}
|
||||
value={seed}
|
||||
/>
|
||||
<Button
|
||||
size={'sm'}
|
||||
isDisabled={shouldRandomizeSeed}
|
||||
onClick={handleClickRandomizeSeed}
|
||||
>
|
||||
<Text pl={2} pr={2}>
|
||||
Shuffle
|
||||
</Text>
|
||||
</Button>
|
||||
</Flex>
|
||||
<SDSwitch
|
||||
label="Generate variations"
|
||||
isChecked={shouldGenerateVariations}
|
||||
width={'auto'}
|
||||
onChange={handleChangeShouldGenerateVariations}
|
||||
/>
|
||||
<SDNumberInput
|
||||
label="Variation amount"
|
||||
value={variationAmount}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={1}
|
||||
isDisabled={!shouldGenerateVariations}
|
||||
onChange={handleChangevariationAmount}
|
||||
/>
|
||||
<FormControl
|
||||
isInvalid={
|
||||
shouldGenerateVariations &&
|
||||
!(validateSeedWeights(seedWeights) || seedWeights === '')
|
||||
}
|
||||
flexGrow={1}
|
||||
>
|
||||
<HStack>
|
||||
<FormLabel marginInlineEnd={0} marginBottom={1}>
|
||||
<Text whiteSpace="nowrap">Seed Weights</Text>
|
||||
</FormLabel>
|
||||
<Input
|
||||
size={'sm'}
|
||||
value={seedWeights}
|
||||
onChange={handleChangeSeedWeights}
|
||||
/>
|
||||
</HStack>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default SeedVariationOptions;
|
||||
@@ -30,6 +30,7 @@ export interface OptionsState {
|
||||
shouldRunESRGAN: boolean;
|
||||
shouldRunGFPGAN: boolean;
|
||||
shouldRandomizeSeed: boolean;
|
||||
showAdvancedOptions: boolean;
|
||||
}
|
||||
|
||||
const initialOptionsState: OptionsState = {
|
||||
@@ -56,6 +57,7 @@ const initialOptionsState: OptionsState = {
|
||||
shouldRunGFPGAN: false,
|
||||
gfpganStrength: 0.8,
|
||||
shouldRandomizeSeed: true,
|
||||
showAdvancedOptions: true,
|
||||
};
|
||||
|
||||
const initialState: OptionsState = initialOptionsState;
|
||||
@@ -153,7 +155,6 @@ export const optionsSlice = createSlice({
|
||||
setAllParameters: (state, action: PayloadAction<InvokeAI.Metadata>) => {
|
||||
const {
|
||||
type,
|
||||
postprocessing,
|
||||
sampler,
|
||||
prompt,
|
||||
seed,
|
||||
@@ -191,35 +192,42 @@ export const optionsSlice = createSlice({
|
||||
state.shouldRandomizeSeed = false;
|
||||
}
|
||||
|
||||
let postprocessingNotDone = ['gfpgan', 'esrgan'];
|
||||
if (postprocessing && postprocessing.length > 0) {
|
||||
postprocessing.forEach(
|
||||
(postprocess: InvokeAI.PostProcessedImageMetadata) => {
|
||||
if (postprocess.type === 'gfpgan') {
|
||||
const { strength } = postprocess;
|
||||
if (strength) state.gfpganStrength = strength;
|
||||
state.shouldRunGFPGAN = true;
|
||||
postprocessingNotDone = postprocessingNotDone.filter(
|
||||
(p) => p !== 'gfpgan'
|
||||
);
|
||||
}
|
||||
if (postprocess.type === 'esrgan') {
|
||||
const { scale, strength } = postprocess;
|
||||
if (scale) state.upscalingLevel = scale;
|
||||
if (strength) state.upscalingStrength = strength;
|
||||
state.shouldRunESRGAN = true;
|
||||
postprocessingNotDone = postprocessingNotDone.filter(
|
||||
(p) => p !== 'esrgan'
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* We support arbitrary numbers of postprocessing steps, so it
|
||||
* doesnt make sense to be include postprocessing metadata when
|
||||
* we use all parameters. Because this code needed a bit of braining
|
||||
* to figure out, I am leaving it, in case it is needed again.
|
||||
*/
|
||||
|
||||
postprocessingNotDone.forEach((p) => {
|
||||
if (p === 'esrgan') state.shouldRunESRGAN = false;
|
||||
if (p === 'gfpgan') state.shouldRunGFPGAN = false;
|
||||
});
|
||||
// let postprocessingNotDone = ['gfpgan', 'esrgan'];
|
||||
// if (postprocessing && postprocessing.length > 0) {
|
||||
// postprocessing.forEach(
|
||||
// (postprocess: InvokeAI.PostProcessedImageMetadata) => {
|
||||
// if (postprocess.type === 'gfpgan') {
|
||||
// const { strength } = postprocess;
|
||||
// if (strength) state.gfpganStrength = strength;
|
||||
// state.shouldRunGFPGAN = true;
|
||||
// postprocessingNotDone = postprocessingNotDone.filter(
|
||||
// (p) => p !== 'gfpgan'
|
||||
// );
|
||||
// }
|
||||
// if (postprocess.type === 'esrgan') {
|
||||
// const { scale, strength } = postprocess;
|
||||
// if (scale) state.upscalingLevel = scale;
|
||||
// if (strength) state.upscalingStrength = strength;
|
||||
// state.shouldRunESRGAN = true;
|
||||
// postprocessingNotDone = postprocessingNotDone.filter(
|
||||
// (p) => p !== 'esrgan'
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
|
||||
// postprocessingNotDone.forEach((p) => {
|
||||
// if (p === 'esrgan') state.shouldRunESRGAN = false;
|
||||
// if (p === 'gfpgan') state.shouldRunGFPGAN = false;
|
||||
// });
|
||||
|
||||
if (prompt) state.prompt = promptToString(prompt);
|
||||
if (sampler) state.sampler = sampler;
|
||||
@@ -244,6 +252,9 @@ export const optionsSlice = createSlice({
|
||||
setShouldRandomizeSeed: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldRandomizeSeed = action.payload;
|
||||
},
|
||||
setShowAdvancedOptions: (state, action: PayloadAction<boolean>) => {
|
||||
state.showAdvancedOptions = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -275,6 +286,7 @@ export const {
|
||||
setShouldRunGFPGAN,
|
||||
setShouldRunESRGAN,
|
||||
setShouldRandomizeSeed,
|
||||
setShowAdvancedOptions,
|
||||
} = optionsSlice.actions;
|
||||
|
||||
export default optionsSlice.reducer;
|
||||
|
||||
72
frontend/src/features/system/Console.scss
Normal file
72
frontend/src/features/system/Console.scss
Normal file
@@ -0,0 +1,72 @@
|
||||
.console {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--console-bg-color);
|
||||
overflow: auto;
|
||||
direction: column;
|
||||
font-family: monospace;
|
||||
padding: 0 1rem 1rem 3rem;
|
||||
border-top-width: 0.3rem;
|
||||
border-color: var(--console-border-color);
|
||||
|
||||
.console-info-color {
|
||||
color: var(--error-level-info);
|
||||
}
|
||||
|
||||
.console-warning-color {
|
||||
color: var(--error-level-warning);
|
||||
}
|
||||
|
||||
.console-error-color {
|
||||
color: var(--status-bad-color);
|
||||
}
|
||||
|
||||
.console-entry {
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
|
||||
.console-timestamp {
|
||||
font-weight: semibold;
|
||||
}
|
||||
|
||||
.console-message {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.console-toggle-icon-button {
|
||||
background: var(--console-icon-button-bg-color) !important;
|
||||
position: fixed !important;
|
||||
left: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--console-icon-button-bg-color-hover) !important;
|
||||
}
|
||||
|
||||
&.error-seen {
|
||||
background: var(--status-bad-color) !important;
|
||||
&:hover {
|
||||
background: var(--status-bad-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.console-autoscroll-icon-button {
|
||||
background: var(--console-icon-button-bg-color) !important;
|
||||
position: fixed !important;
|
||||
left: 0.5rem;
|
||||
bottom: 3rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--console-icon-button-bg-color-hover) !important;
|
||||
}
|
||||
|
||||
&.autoscroll-enabled {
|
||||
background: var(--btn-purple) !important;
|
||||
&:hover {
|
||||
background: var(--btn-purple-hover) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
import {
|
||||
IconButton,
|
||||
useColorModeValue,
|
||||
Flex,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { IconButton, Tooltip } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import { setShouldShowLogViewer, SystemState } from './systemSlice';
|
||||
import { errorSeen, setShouldShowLogViewer, SystemState } from './systemSlice';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { FaAngleDoubleDown, FaCode, FaMinus } from 'react-icons/fa';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Resizable } from 're-resizable';
|
||||
|
||||
const logSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
@@ -27,7 +22,11 @@ const logSelector = createSelector(
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return { shouldShowLogViewer: system.shouldShowLogViewer };
|
||||
return {
|
||||
shouldShowLogViewer: system.shouldShowLogViewer,
|
||||
hasError: system.hasError,
|
||||
wasErrorSeen: system.wasErrorSeen,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
@@ -39,26 +38,11 @@ const systemSelector = createSelector(
|
||||
/**
|
||||
* Basic log viewer, floats on bottom of page.
|
||||
*/
|
||||
const LogViewer = () => {
|
||||
const Console = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const log = useAppSelector(logSelector);
|
||||
const { shouldShowLogViewer } = useAppSelector(systemSelector);
|
||||
|
||||
// Set colors based on dark/light mode
|
||||
const bg = useColorModeValue('gray.50', 'gray.900');
|
||||
const borderColor = useColorModeValue('gray.500', 'gray.500');
|
||||
const logTextColors = useColorModeValue(
|
||||
{
|
||||
info: undefined,
|
||||
warning: 'yellow.500',
|
||||
error: 'red.500',
|
||||
},
|
||||
{
|
||||
info: undefined,
|
||||
warning: 'yellow.300',
|
||||
error: 'red.300',
|
||||
}
|
||||
);
|
||||
const { shouldShowLogViewer, hasError, wasErrorSeen } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
// Rudimentary autoscroll
|
||||
const [shouldAutoscroll, setShouldAutoscroll] = useState<boolean>(true);
|
||||
@@ -78,68 +62,58 @@ const LogViewer = () => {
|
||||
}, [shouldAutoscroll, log, shouldShowLogViewer]);
|
||||
|
||||
const handleClickLogViewerToggle = () => {
|
||||
dispatch(errorSeen());
|
||||
dispatch(setShouldShowLogViewer(!shouldShowLogViewer));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldShowLogViewer && (
|
||||
<Flex
|
||||
position={'fixed'}
|
||||
left={0}
|
||||
bottom={0}
|
||||
height="200px" // TODO: Make the log viewer resizeable.
|
||||
width="100vw"
|
||||
overflow="auto"
|
||||
direction="column"
|
||||
fontFamily="monospace"
|
||||
fontSize="sm"
|
||||
pl={12}
|
||||
pr={2}
|
||||
pb={2}
|
||||
borderTopWidth="4px"
|
||||
borderColor={borderColor}
|
||||
background={bg}
|
||||
ref={viewerRef}
|
||||
<Resizable
|
||||
defaultSize={{
|
||||
width: '100%',
|
||||
height: 200,
|
||||
}}
|
||||
style={{ display: 'flex', position: 'fixed', left: 0, bottom: 0 }}
|
||||
maxHeight={'90vh'}
|
||||
>
|
||||
{log.map((entry, i) => {
|
||||
const { timestamp, message, level } = entry;
|
||||
return (
|
||||
<Flex gap={2} key={i} textColor={logTextColors[level]}>
|
||||
<Text fontSize="sm" fontWeight={'semibold'}>
|
||||
{timestamp}:
|
||||
</Text>
|
||||
<Text fontSize="sm" wordBreak={'break-all'}>
|
||||
{message}
|
||||
</Text>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
<div className="console" ref={viewerRef}>
|
||||
{log.map((entry, i) => {
|
||||
const { timestamp, message, level } = entry;
|
||||
return (
|
||||
<div key={i} className={`console-entry console-${level}-color`}>
|
||||
<p className="console-timestamp">{timestamp}:</p>
|
||||
<p className="console-message">{message}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Resizable>
|
||||
)}
|
||||
{shouldShowLogViewer && (
|
||||
<Tooltip label={shouldAutoscroll ? 'Autoscroll on' : 'Autoscroll off'}>
|
||||
<Tooltip label={shouldAutoscroll ? 'Autoscroll On' : 'Autoscroll Off'}>
|
||||
<IconButton
|
||||
className={`console-autoscroll-icon-button ${
|
||||
shouldAutoscroll && 'autoscroll-enabled'
|
||||
}`}
|
||||
size="sm"
|
||||
position={'fixed'}
|
||||
left={2}
|
||||
bottom={12}
|
||||
aria-label="Toggle autoscroll"
|
||||
variant={'solid'}
|
||||
colorScheme={shouldAutoscroll ? 'blue' : 'gray'}
|
||||
icon={<FaAngleDoubleDown />}
|
||||
onClick={() => setShouldAutoscroll(!shouldAutoscroll)}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label={shouldShowLogViewer ? 'Hide logs' : 'Show logs'}>
|
||||
<Tooltip label={shouldShowLogViewer ? 'Hide Console' : 'Show Console'}>
|
||||
<IconButton
|
||||
className={`console-toggle-icon-button ${
|
||||
(hasError || !wasErrorSeen) && 'error-seen'
|
||||
}`}
|
||||
size="sm"
|
||||
position={'fixed'}
|
||||
left={2}
|
||||
bottom={2}
|
||||
variant={'solid'}
|
||||
aria-label="Toggle Log Viewer"
|
||||
// colorScheme={hasError || !wasErrorSeen ? 'red' : 'gray'}
|
||||
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
|
||||
onClick={handleClickLogViewerToggle}
|
||||
/>
|
||||
@@ -148,4 +122,4 @@ const LogViewer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default LogViewer;
|
||||
export default Console;
|
||||
7
frontend/src/features/system/ProgressBar.scss
Normal file
7
frontend/src/features/system/ProgressBar.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.progress-bar {
|
||||
background-color: var(--root-bg-color);
|
||||
|
||||
div {
|
||||
background-color: var(--progress-bar-color);
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,10 @@ const ProgressBar = () => {
|
||||
|
||||
return (
|
||||
<Progress
|
||||
height="10px"
|
||||
height="4px"
|
||||
value={value}
|
||||
isIndeterminate={isProcessing && !currentStatusHasSteps}
|
||||
className="progress-bar"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.settings-modal {
|
||||
background-color: var(--settings-modal-bg) !important;
|
||||
|
||||
.settings-modal-content {
|
||||
display: grid;
|
||||
row-gap: 2rem;
|
||||
}
|
||||
|
||||
.settings-modal-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.settings-modal-items {
|
||||
display: grid;
|
||||
row-gap: 0.5rem;
|
||||
|
||||
.settings-modal-item {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
background-color: var(--background-color);
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-modal-reset {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
|
||||
button {
|
||||
@include Button(
|
||||
$btn-color: var(--btn-red),
|
||||
$btn-color-hover: var(--btn-red-hover)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Heading,
|
||||
HStack,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
@@ -12,22 +9,21 @@ import {
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Switch,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import {
|
||||
setShouldConfirmOnDelete,
|
||||
setShouldDisplayInProgress,
|
||||
setShouldDisplayGuides,
|
||||
SystemState,
|
||||
} from './systemSlice';
|
||||
import { RootState } from '../../app/store';
|
||||
import { persistor } from '../../main';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { cloneElement, ReactElement } from 'react';
|
||||
import { RootState, useAppSelector } from '../../../app/store';
|
||||
import { persistor } from '../../../main';
|
||||
import {
|
||||
setShouldConfirmOnDelete,
|
||||
setShouldDisplayGuides,
|
||||
setShouldDisplayInProgress,
|
||||
SystemState,
|
||||
} from '../systemSlice';
|
||||
import SettingsModalItem from './SettingsModalItem';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
@@ -78,8 +74,6 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||
shouldDisplayGuides,
|
||||
} = useAppSelector(systemSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
/**
|
||||
* Resets localstorage, then opens a secondary modal informing user to
|
||||
* refresh their browser.
|
||||
@@ -99,49 +93,31 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||
|
||||
<Modal isOpen={isSettingsModalOpen} onClose={onSettingsModalClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Settings</ModalHeader>
|
||||
<ModalContent className="settings-modal">
|
||||
<ModalHeader className="settings-modal-header">Settings</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Flex gap={5} direction="column">
|
||||
<FormControl>
|
||||
<HStack>
|
||||
<FormLabel marginBottom={1}>
|
||||
Display in-progress images (slower)
|
||||
</FormLabel>
|
||||
<Switch
|
||||
isChecked={shouldDisplayInProgress}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldDisplayInProgress(e.target.checked))
|
||||
}
|
||||
/>
|
||||
</HStack>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<HStack>
|
||||
<FormLabel marginBottom={1}>Confirm on delete</FormLabel>
|
||||
<Switch
|
||||
isChecked={shouldConfirmOnDelete}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldConfirmOnDelete(e.target.checked))
|
||||
}
|
||||
/>
|
||||
</HStack>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<HStack>
|
||||
<FormLabel marginBottom={1}>
|
||||
Display help guides in configuration menus
|
||||
</FormLabel>
|
||||
<Switch
|
||||
isChecked={shouldDisplayGuides}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldDisplayGuides(e.target.checked))
|
||||
}
|
||||
/>
|
||||
</HStack>
|
||||
</FormControl>
|
||||
<ModalBody className="settings-modal-content">
|
||||
<div className="settings-modal-items">
|
||||
<SettingsModalItem
|
||||
settingTitle="Display In-Progress Images (slower)"
|
||||
isChecked={shouldDisplayInProgress}
|
||||
dispatcher={setShouldDisplayInProgress}
|
||||
/>
|
||||
|
||||
<SettingsModalItem
|
||||
settingTitle="Confirm on Delete"
|
||||
isChecked={shouldConfirmOnDelete}
|
||||
dispatcher={setShouldConfirmOnDelete}
|
||||
/>
|
||||
|
||||
<SettingsModalItem
|
||||
settingTitle="Display Help Icons"
|
||||
isChecked={shouldDisplayGuides}
|
||||
dispatcher={setShouldDisplayGuides}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="settings-modal-reset">
|
||||
<Heading size={'md'}>Reset Web UI</Heading>
|
||||
<Text>
|
||||
Resetting the web UI only resets the browser's local cache of
|
||||
@@ -156,7 +132,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||
<Button colorScheme="red" onClick={handleClickResetWebUI}>
|
||||
Reset Web UI
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { FormControl, FormLabel, Switch } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { useAppDispatch } from '../../../app/store';
|
||||
|
||||
export default function SettingsModalItem({
|
||||
settingTitle,
|
||||
isChecked,
|
||||
dispatcher,
|
||||
}: {
|
||||
settingTitle: string;
|
||||
isChecked: boolean;
|
||||
dispatcher: any;
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
return (
|
||||
<FormControl className="settings-modal-item">
|
||||
<FormLabel marginBottom={1}>{settingTitle}</FormLabel>
|
||||
<Switch
|
||||
isChecked={isChecked}
|
||||
onChange={(e) => dispatch(dispatcher(e.target.checked))}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
27
frontend/src/features/system/SiteHeader.scss
Normal file
27
frontend/src/features/system/SiteHeader.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
.site-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto max-content;
|
||||
}
|
||||
|
||||
.site-header-left-side {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
column-gap: 0.6rem;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.site-header-right-side {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, max-content);
|
||||
align-items: center;
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
@@ -1,119 +1,82 @@
|
||||
import {
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Link,
|
||||
Spacer,
|
||||
Text,
|
||||
useColorMode,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { IconButton, Link, useColorMode } from '@chakra-ui/react';
|
||||
|
||||
import { FaSun, FaMoon, FaGithub } from 'react-icons/fa';
|
||||
import { MdHelp, MdSettings } from 'react-icons/md';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { RootState } from '../../app/store';
|
||||
import SettingsModal from '../system/SettingsModal';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isConnected: system.isConnected,
|
||||
isProcessing: system.isProcessing,
|
||||
currentIteration: system.currentIteration,
|
||||
totalIterations: system.totalIterations,
|
||||
currentStatus: system.currentStatus,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: { resultEqualityCheck: isEqual },
|
||||
}
|
||||
);
|
||||
|
||||
import InvokeAILogo from '../../assets/images/logo.png';
|
||||
import SettingsModal from './SettingsModal/SettingsModal';
|
||||
import StatusIndicator from './StatusIndicator';
|
||||
|
||||
/**
|
||||
* Header, includes color mode toggle, settings button, status message.
|
||||
*/
|
||||
const SiteHeader = () => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const {
|
||||
isConnected,
|
||||
isProcessing,
|
||||
currentIteration,
|
||||
totalIterations,
|
||||
currentStatus,
|
||||
} = useAppSelector(systemSelector);
|
||||
|
||||
const statusMessageTextColor = isConnected ? 'green.500' : 'red.500';
|
||||
|
||||
const colorModeIcon = colorMode == 'light' ? <FaMoon /> : <FaSun />;
|
||||
|
||||
// Make FaMoon and FaSun icon apparent size consistent
|
||||
const colorModeIconFontSize = colorMode == 'light' ? 18 : 20;
|
||||
|
||||
let statusMessage = currentStatus;
|
||||
|
||||
if (isProcessing) {
|
||||
if (totalIterations > 1) {
|
||||
statusMessage += ` [${currentIteration}/${totalIterations}]`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex minWidth="max-content" alignItems="center" gap="1" pl={2} pr={1}>
|
||||
<Heading size={'lg'}>InvokeUI</Heading>
|
||||
<div className="site-header">
|
||||
<div className="site-header-left-side">
|
||||
<img src={InvokeAILogo} alt="invoke-ai-logo" />
|
||||
<h1>
|
||||
invoke <strong>ai</strong>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<Spacer />
|
||||
<div className="site-header-right-side">
|
||||
<StatusIndicator />
|
||||
|
||||
<Text textColor={statusMessageTextColor}>{statusMessage}</Text>
|
||||
<SettingsModal>
|
||||
<IconButton
|
||||
aria-label="Settings"
|
||||
variant="link"
|
||||
fontSize={24}
|
||||
size={'sm'}
|
||||
icon={<MdSettings />}
|
||||
/>
|
||||
</SettingsModal>
|
||||
|
||||
<SettingsModal>
|
||||
<IconButton
|
||||
aria-label="Settings"
|
||||
aria-label="Link to Github Issues"
|
||||
variant="link"
|
||||
fontSize={24}
|
||||
fontSize={23}
|
||||
size={'sm'}
|
||||
icon={<MdSettings />}
|
||||
icon={
|
||||
<Link
|
||||
isExternal
|
||||
href="http://github.com/lstein/stable-diffusion/issues"
|
||||
>
|
||||
<MdHelp />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</SettingsModal>
|
||||
|
||||
<IconButton
|
||||
aria-label="Link to Github Issues"
|
||||
variant="link"
|
||||
fontSize={23}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link
|
||||
isExternal
|
||||
href="http://github.com/lstein/stable-diffusion/issues"
|
||||
>
|
||||
<MdHelp />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Link to Github Repo"
|
||||
variant="link"
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link isExternal href="http://github.com/lstein/stable-diffusion">
|
||||
<FaGithub />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
aria-label="Link to Github Repo"
|
||||
variant="link"
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link isExternal href="http://github.com/lstein/stable-diffusion">
|
||||
<FaGithub />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
aria-label="Toggle Dark Mode"
|
||||
onClick={toggleColorMode}
|
||||
variant="link"
|
||||
size={'sm'}
|
||||
fontSize={colorModeIconFontSize}
|
||||
icon={colorModeIcon}
|
||||
/>
|
||||
</Flex>
|
||||
<IconButton
|
||||
aria-label="Toggle Dark Mode"
|
||||
onClick={toggleColorMode}
|
||||
variant="link"
|
||||
size={'sm'}
|
||||
fontSize={colorModeIconFontSize}
|
||||
icon={colorModeIcon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
19
frontend/src/features/system/StatusIndicator.scss
Normal file
19
frontend/src/features/system/StatusIndicator.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
.status {
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-good {
|
||||
color: var(--status-good-color);
|
||||
text-shadow: 0 0 10px var(--status-good-glow);
|
||||
}
|
||||
|
||||
.status-bad {
|
||||
color: var(--status-bad-color);
|
||||
text-shadow: 0 0 10px var(--status-bad-glow);
|
||||
}
|
||||
|
||||
.status-working {
|
||||
color: var(--status-working-color);
|
||||
text-shadow: 0 0 10px var(--status-working-glow);
|
||||
}
|
||||
95
frontend/src/features/system/StatusIndicator.tsx
Normal file
95
frontend/src/features/system/StatusIndicator.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Text, Tooltip } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { RootState, useAppDispatch, useAppSelector } from '../../app/store';
|
||||
import { errorSeen, SystemState } from './systemSlice';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isConnected: system.isConnected,
|
||||
isProcessing: system.isProcessing,
|
||||
currentIteration: system.currentIteration,
|
||||
totalIterations: system.totalIterations,
|
||||
currentStatus: system.currentStatus,
|
||||
hasError: system.hasError,
|
||||
wasErrorSeen: system.wasErrorSeen,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: { resultEqualityCheck: isEqual },
|
||||
}
|
||||
);
|
||||
|
||||
const StatusIndicator = () => {
|
||||
const {
|
||||
isConnected,
|
||||
isProcessing,
|
||||
currentIteration,
|
||||
totalIterations,
|
||||
currentStatus,
|
||||
hasError,
|
||||
wasErrorSeen,
|
||||
} = useAppSelector(systemSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
// const statusMessageTextColor =
|
||||
// isConnected && !hasError ? 'green.500' : 'red.500';
|
||||
|
||||
let statusStyle;
|
||||
if (isConnected && !hasError) {
|
||||
statusStyle = 'status-good';
|
||||
} else {
|
||||
statusStyle = 'status-bad';
|
||||
}
|
||||
|
||||
let statusMessage = currentStatus;
|
||||
|
||||
const intermediateStatuses = [
|
||||
'generating',
|
||||
'preparing',
|
||||
'saving image',
|
||||
'restoring faces',
|
||||
'upscaling',
|
||||
];
|
||||
|
||||
if (intermediateStatuses.includes(statusMessage.toLowerCase())) {
|
||||
statusStyle = 'status-working';
|
||||
}
|
||||
|
||||
if (statusMessage)
|
||||
if (isProcessing) {
|
||||
if (totalIterations > 1) {
|
||||
statusMessage += ` (${currentIteration}/${totalIterations})`;
|
||||
}
|
||||
}
|
||||
|
||||
const tooltipLabel =
|
||||
hasError && !wasErrorSeen
|
||||
? 'Click to clear, check logs for details'
|
||||
: undefined;
|
||||
|
||||
const statusIndicatorCursor =
|
||||
hasError && !wasErrorSeen ? 'pointer' : 'initial';
|
||||
|
||||
const handleClickStatusIndicator = () => {
|
||||
if (hasError || !wasErrorSeen) {
|
||||
dispatch(errorSeen());
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip label={tooltipLabel}>
|
||||
<Text
|
||||
cursor={statusIndicatorCursor}
|
||||
onClick={handleClickStatusIndicator}
|
||||
className={`status ${statusStyle}`}
|
||||
// textColor={statusMessageTextColor}
|
||||
>
|
||||
{statusMessage}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusIndicator;
|
||||
@@ -34,6 +34,7 @@ export interface SystemState
|
||||
currentStatus: string;
|
||||
currentStatusHasSteps: boolean;
|
||||
shouldDisplayGuides: boolean;
|
||||
wasErrorSeen: boolean;
|
||||
}
|
||||
|
||||
const initialSystemState = {
|
||||
@@ -59,6 +60,8 @@ const initialSystemState = {
|
||||
model_hash: '',
|
||||
app_id: '',
|
||||
app_version: '',
|
||||
hasError: false,
|
||||
wasErrorSeen: true,
|
||||
};
|
||||
|
||||
const initialState: SystemState = initialSystemState;
|
||||
@@ -77,12 +80,23 @@ export const systemSlice = createSlice({
|
||||
state.currentStatus = action.payload;
|
||||
},
|
||||
setSystemStatus: (state, action: PayloadAction<InvokeAI.SystemStatus>) => {
|
||||
const currentStatus =
|
||||
!action.payload.isProcessing && state.isConnected
|
||||
? 'Connected'
|
||||
: action.payload.currentStatus;
|
||||
|
||||
return { ...state, ...action.payload, currentStatus };
|
||||
return { ...state, ...action.payload };
|
||||
},
|
||||
errorOccurred: (state) => {
|
||||
state.hasError = true;
|
||||
state.isProcessing = false;
|
||||
state.currentStep = 0;
|
||||
state.totalSteps = 0;
|
||||
state.currentIteration = 0;
|
||||
state.totalIterations = 0;
|
||||
state.currentStatusHasSteps = false;
|
||||
state.currentStatus = 'Server error';
|
||||
state.wasErrorSeen = false;
|
||||
},
|
||||
errorSeen: (state) => {
|
||||
state.hasError = false;
|
||||
state.wasErrorSeen = true;
|
||||
state.currentStatus = state.isConnected ? 'Connected' : 'Disconnected';
|
||||
},
|
||||
addLogEntry: (
|
||||
state,
|
||||
@@ -114,6 +128,7 @@ export const systemSlice = createSlice({
|
||||
state.currentIteration = 0;
|
||||
state.totalIterations = 0;
|
||||
state.currentStatusHasSteps = false;
|
||||
state.hasError = false;
|
||||
},
|
||||
setSocketId: (state, action: PayloadAction<string>) => {
|
||||
state.socketId = action.payload;
|
||||
@@ -130,6 +145,15 @@ export const systemSlice = createSlice({
|
||||
setShouldDisplayGuides: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldDisplayGuides = action.payload;
|
||||
},
|
||||
processingCanceled: (state) => {
|
||||
state.isProcessing = false;
|
||||
state.currentStep = 0;
|
||||
state.totalSteps = 0;
|
||||
state.currentIteration = 0;
|
||||
state.totalIterations = 0;
|
||||
state.currentStatusHasSteps = false;
|
||||
state.currentStatus = 'Processing canceled';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -146,6 +170,9 @@ export const {
|
||||
setCurrentStatus,
|
||||
setSystemConfig,
|
||||
setShouldDisplayGuides,
|
||||
processingCanceled,
|
||||
errorOccurred,
|
||||
errorSeen,
|
||||
} = systemSlice.actions;
|
||||
|
||||
export default systemSlice.reducer;
|
||||
|
||||
43
frontend/src/features/tabs/InvokeTabs.scss
Normal file
43
frontend/src/features/tabs/InvokeTabs.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
@use '../../styles/Mixins/' as *;
|
||||
|
||||
.app-tabs {
|
||||
display: grid !important;
|
||||
grid-template-columns: min-content auto;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
.app-tabs-list {
|
||||
display: grid;
|
||||
row-gap: 0.3rem;
|
||||
grid-auto-rows: max-content;
|
||||
color: var(--tab-list-text-inactive);
|
||||
|
||||
button {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tab-hover-color);
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
background-color: var(--tab-list-bg);
|
||||
color: var(--tab-list-text);
|
||||
font-weight: bold;
|
||||
border-radius: 0.3rem;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-tabs-panels {
|
||||
.app-tabs-panel {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
84
frontend/src/features/tabs/InvokeTabs.tsx
Normal file
84
frontend/src/features/tabs/InvokeTabs.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Tab, TabPanel, TabPanels, Tabs, Tooltip } from '@chakra-ui/react';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { ImageToImageWIP } from '../../common/components/WorkInProgress/ImageToImageWIP';
|
||||
import InpaintingWIP from '../../common/components/WorkInProgress/InpaintingWIP';
|
||||
import NodesWIP from '../../common/components/WorkInProgress/NodesWIP';
|
||||
import OutpaintingWIP from '../../common/components/WorkInProgress/OutpaintingWIP';
|
||||
import { PostProcessingWIP } from '../../common/components/WorkInProgress/PostProcessingWIP';
|
||||
import ImageToImageIcon from '../../common/icons/ImageToImageIcon';
|
||||
import InpaintIcon from '../../common/icons/InpaintIcon';
|
||||
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 TextToImage from './TextToImage/TextToImage';
|
||||
|
||||
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 renderTabs = () => {
|
||||
const tabsToRender: ReactElement[] = [];
|
||||
Object.keys(tab_dict).forEach((key) => {
|
||||
tabsToRender.push(
|
||||
<Tooltip
|
||||
key={key}
|
||||
label={tab_dict[key as keyof typeof tab_dict].tooltip}
|
||||
placement={'right'}
|
||||
>
|
||||
<Tab>{tab_dict[key as keyof typeof tab_dict].title}</Tab>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
return tabsToRender;
|
||||
};
|
||||
|
||||
const renderTabPanels = () => {
|
||||
const tabPanelsToRender: ReactElement[] = [];
|
||||
Object.keys(tab_dict).forEach((key) => {
|
||||
tabPanelsToRender.push(
|
||||
<TabPanel className="app-tabs-panel" key={key}>
|
||||
{tab_dict[key as keyof typeof tab_dict].panel}
|
||||
</TabPanel>
|
||||
);
|
||||
});
|
||||
return tabPanelsToRender;
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs className="app-tabs" variant={'unstyled'}>
|
||||
<div className="app-tabs-list">{renderTabs()}</div>
|
||||
<TabPanels className="app-tabs-panels">{renderTabPanels()}</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
16
frontend/src/features/tabs/TextToImage/TextToImage.scss
Normal file
16
frontend/src/features/tabs/TextToImage/TextToImage.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
@use '../../../styles/Mixins/' as *;
|
||||
|
||||
.text-to-image-workarea {
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto max-content;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
.text-to-image-panel {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
grid-auto-rows: max-content;
|
||||
height: $app-content-height;
|
||||
overflow-y: scroll;
|
||||
@include HideScrollbar;
|
||||
}
|
||||
14
frontend/src/features/tabs/TextToImage/TextToImage.tsx
Normal file
14
frontend/src/features/tabs/TextToImage/TextToImage.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import CurrentImageDisplay from '../../gallery/CurrentImageDisplay';
|
||||
import ImageGallery from '../../gallery/ImageGallery';
|
||||
import TextToImagePanel from './TextToImagePanel';
|
||||
|
||||
export default function TextToImage() {
|
||||
return (
|
||||
<div className="text-to-image-workarea">
|
||||
<TextToImagePanel />
|
||||
<CurrentImageDisplay />
|
||||
<ImageGallery />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
frontend/src/features/tabs/TextToImage/TextToImagePanel.tsx
Normal file
20
frontend/src/features/tabs/TextToImage/TextToImagePanel.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { RootState, useAppSelector } from '../../../app/store';
|
||||
import MainOptions from '../../options/MainOptions/MainOptions';
|
||||
import OptionsAccordion from '../../options/OptionsAccordion';
|
||||
import ProcessButtons from '../../options/ProcessButtons/ProcessButtons';
|
||||
import PromptInput from '../../options/PromptInput/PromptInput';
|
||||
|
||||
export default function TextToImagePanel() {
|
||||
const showAdvancedOptions = useAppSelector(
|
||||
(state: RootState) => state.options.showAdvancedOptions
|
||||
);
|
||||
return (
|
||||
<div className="text-to-image-panel">
|
||||
<PromptInput />
|
||||
<ProcessButtons />
|
||||
<MainOptions />
|
||||
{showAdvancedOptions ? <OptionsAccordion /> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user