Add New WebUI and Desktop Mode

Co-Authored-By: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
This commit is contained in:
blessedcoolant
2022-10-04 05:15:26 +13:00
committed by Lincoln Stein
parent 40828df663
commit b8e4c13746
157 changed files with 4775 additions and 2622 deletions

View File

@@ -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>
);
};

View 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);
}

View File

@@ -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>
);
};

View File

@@ -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"

View 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;
}
}

View File

@@ -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>
);
};

View File

@@ -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>
</>
) : (

View 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;
}

View 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;

View File

@@ -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;
}
},
},

View File

@@ -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;
}
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View 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"
/>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,5 @@
.upscale-options {
display: grid;
grid-template-columns: auto 1fr;
column-gap: 1rem;
}

View File

@@ -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;

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
);
}

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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'}

View File

@@ -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,

View File

@@ -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>
);
}

View 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}
/>
);
}

View 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"
/>
);
}

View 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"
/>
);
}

View 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);
}
}
}
}

View 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>
);
}

View 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"
/>
);
}

View 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"
/>
);
}

View 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"
/>
);
}

View File

@@ -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>
);
};

View File

@@ -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}

View File

@@ -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;

View File

@@ -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"
/>
);
}

View File

@@ -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"
/>
);
}

View File

@@ -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
);
}
}

View File

@@ -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;

View File

@@ -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;

View 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;
}
}

View 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;

View File

@@ -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}

View File

@@ -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;

View File

@@ -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;

View 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;
}
}
}

View File

@@ -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;

View File

@@ -0,0 +1,7 @@
.progress-bar {
background-color: var(--root-bg-color);
div {
background-color: var(--progress-bar-color);
}
}

View File

@@ -28,9 +28,10 @@ const ProgressBar = () => {
return (
<Progress
height="10px"
height="4px"
value={value}
isIndeterminate={isProcessing && !currentStatusHasSteps}
className="progress-bar"
/>
);
};

View File

@@ -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)
);
}
}
}

View File

@@ -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>

View File

@@ -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>
);
}

View 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;
}

View File

@@ -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>
);
};

View 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);
}

View 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;

View File

@@ -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;

View 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;
}
}

View 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>
);
}

View 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;
}

View 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>
);
}

View 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>
);
}