mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
Improves code structure, comments, formatting, linting
This commit is contained in:
parent
1799bf5e42
commit
e45f46d673
@ -1,85 +1,25 @@
|
||||
# Stable Diffusion Web UI
|
||||
|
||||
Demo at https://peaceful-otter-7a427f.netlify.app/ (not connected to back end)
|
||||
|
||||
much of this readme is just notes for myself during dev work
|
||||
|
||||
numpy rand: 0 to 4294967295
|
||||
|
||||
## Test and Build
|
||||
## Build
|
||||
|
||||
from `frontend/`:
|
||||
|
||||
- `yarn dev` runs `tsc-watch`, which runs `vite build` on successful `tsc` transpilation
|
||||
- `yarn dev` runs vite dev server
|
||||
- `yarn build-dev` builds dev
|
||||
- `yarn build` builds prod
|
||||
|
||||
from `.`:
|
||||
|
||||
- `python backend/server.py` serves both frontend and backend at http://localhost:9090
|
||||
|
||||
## API
|
||||
|
||||
`backend/server.py` serves the UI and provides a [socket.io](https://github.com/socketio/socket.io) API via [flask-socketio](https://github.com/miguelgrinberg/flask-socketio).
|
||||
|
||||
### Server Listeners
|
||||
|
||||
The server listens for these socket.io events:
|
||||
|
||||
`cancel`
|
||||
|
||||
- Cancels in-progress image generation
|
||||
- Returns ack only
|
||||
|
||||
`generateImage`
|
||||
|
||||
- Accepts object of image parameters
|
||||
- Generates an image
|
||||
- Returns ack only (image generation function sends progress and result via separate events)
|
||||
|
||||
`deleteImage`
|
||||
|
||||
- Accepts file path to image
|
||||
- Deletes image
|
||||
- Returns ack only
|
||||
|
||||
`deleteAllImages` WIP
|
||||
|
||||
- Deletes all images in `outputs/`
|
||||
- Returns ack only
|
||||
|
||||
`requestAllImages`
|
||||
|
||||
- Returns array of all images in `outputs/`
|
||||
|
||||
`requestCapabilities` WIP
|
||||
|
||||
- Returns capabilities of server (torch device, GFPGAN and ESRGAN availability, ???)
|
||||
|
||||
`sendImage` WIP
|
||||
|
||||
- Accepts a File and attributes
|
||||
- Saves image
|
||||
- Used to save init images which are not generated images
|
||||
|
||||
### Server Emitters
|
||||
|
||||
`progress`
|
||||
|
||||
- Emitted during each step in generation
|
||||
- Sends a number from 0 to 1 representing percentage of steps completed
|
||||
|
||||
`result` WIP
|
||||
|
||||
- Emitted when an image generation has completed
|
||||
- Sends a object:
|
||||
|
||||
```
|
||||
{
|
||||
url: relative_file_path,
|
||||
metadata: image_metadata_object
|
||||
}
|
||||
```
|
||||
- `python backend/server.py` serves both frontend and backend at http://localhost:9090
|
||||
|
||||
## TODO
|
||||
|
||||
- Search repo for "TODO"
|
||||
- My one gripe with Chakra: no way to disable all animations right now and drop the dependence on `framer-motion`. I would prefer to save the ~30kb on bundle and have zero animations. This is on the Chakra roadmap. See https://github.com/chakra-ui/chakra-ui/pull/6368 for last discussion on this. Need to check in on this issue periodically.
|
||||
- Search repo for "TODO"
|
||||
- My one gripe with Chakra: no way to disable all animations right now and drop the dependence on
|
||||
`framer-motion`. I would prefer to save the ~30kb on bundle and have zero animations. This is on
|
||||
the Chakra roadmap. See https://github.com/chakra-ui/chakra-ui/pull/6368 for last discussion on
|
||||
this. Need to check in on this issue periodically.
|
||||
- More status info e.g. phase of processing, image we are on of the total count, etc
|
||||
- Mobile friendly layout
|
||||
- Proper image gallery/viewer/manager
|
||||
- Instead of deleting images directly, use something like [send2trash](https://pypi.org/project/Send2Trash/)
|
||||
|
694
frontend/dist/assets/index.3d2e59c5.js
vendored
Normal file
694
frontend/dist/assets/index.3d2e59c5.js
vendored
Normal file
File diff suppressed because one or more lines are too long
695
frontend/dist/assets/index.cc5cde43.js
vendored
695
frontend/dist/assets/index.cc5cde43.js
vendored
File diff suppressed because one or more lines are too long
2
frontend/dist/index.html
vendored
2
frontend/dist/index.html
vendored
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Stable Diffusion Dream Server</title>
|
||||
<script type="module" crossorigin src="/assets/index.cc5cde43.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index.3d2e59c5.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index.447eb2a9.css">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -4,8 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsc-watch --onSuccess 'yarn run vite build -m development'",
|
||||
"hmr": "vite dev",
|
||||
"dev": "vite dev",
|
||||
"build": "tsc && vite build",
|
||||
"build-dev": "tsc && vite build -m development",
|
||||
"preview": "vite preview"
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Grid, GridItem } from '@chakra-ui/react';
|
||||
import CurrentImage from './features/gallery/CurrentImage';
|
||||
import CurrentImageDisplay from './features/gallery/CurrentImageDisplay';
|
||||
import LogViewer from './features/system/LogViewer';
|
||||
import PromptInput from './features/sd/PromptInput';
|
||||
import ProgressBar from './features/header/ProgressBar';
|
||||
@ -7,54 +7,57 @@ import { useEffect } from 'react';
|
||||
import { useAppDispatch } from './app/hooks';
|
||||
import { requestAllImages } from './app/socketio';
|
||||
import ProcessButtons from './features/sd/ProcessButtons';
|
||||
import ImageRoll from './features/gallery/ImageRoll';
|
||||
import ImageGallery from './features/gallery/ImageGallery';
|
||||
import SiteHeader from './features/header/SiteHeader';
|
||||
import OptionsAccordion from './features/sd/OptionsAccordion';
|
||||
|
||||
const App = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(requestAllImages());
|
||||
}, [dispatch]);
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
width='100vw'
|
||||
height='100vh'
|
||||
templateAreas={`
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Load images from the gallery once
|
||||
useEffect(() => {
|
||||
dispatch(requestAllImages());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
width="100vw"
|
||||
height="100vh"
|
||||
templateAreas={`
|
||||
"header header header header"
|
||||
"progressBar progressBar progressBar progressBar"
|
||||
"menu prompt processButtons imageRoll"
|
||||
"menu currentImage currentImage imageRoll"`}
|
||||
gridTemplateRows={'36px 10px 100px auto'}
|
||||
gridTemplateColumns={'350px auto 100px 388px'}
|
||||
gap={2}
|
||||
>
|
||||
<GridItem area={'header'} pt={1}>
|
||||
<SiteHeader />
|
||||
</GridItem>
|
||||
<GridItem area={'progressBar'}>
|
||||
<ProgressBar />
|
||||
</GridItem>
|
||||
<GridItem pl='2' area={'menu'} overflowY='scroll'>
|
||||
<OptionsAccordion />
|
||||
</GridItem>
|
||||
<GridItem area={'prompt'}>
|
||||
<PromptInput />
|
||||
</GridItem>
|
||||
<GridItem area={'processButtons'}>
|
||||
<ProcessButtons />
|
||||
</GridItem>
|
||||
<GridItem area={'currentImage'}>
|
||||
<CurrentImage />
|
||||
</GridItem>
|
||||
<GridItem pr='2' area={'imageRoll'} overflowY='scroll'>
|
||||
<ImageRoll />
|
||||
</GridItem>
|
||||
</Grid>
|
||||
<LogViewer />
|
||||
</>
|
||||
);
|
||||
gridTemplateRows={'36px 10px 100px auto'}
|
||||
gridTemplateColumns={'350px auto 100px 388px'}
|
||||
gap={2}
|
||||
>
|
||||
<GridItem area={'header'} pt={1}>
|
||||
<SiteHeader />
|
||||
</GridItem>
|
||||
<GridItem area={'progressBar'}>
|
||||
<ProgressBar />
|
||||
</GridItem>
|
||||
<GridItem pl="2" area={'menu'} overflowY="scroll">
|
||||
<OptionsAccordion />
|
||||
</GridItem>
|
||||
<GridItem area={'prompt'}>
|
||||
<PromptInput />
|
||||
</GridItem>
|
||||
<GridItem area={'processButtons'}>
|
||||
<ProcessButtons />
|
||||
</GridItem>
|
||||
<GridItem area={'currentImage'}>
|
||||
<CurrentImageDisplay />
|
||||
</GridItem>
|
||||
<GridItem pr="2" area={'imageRoll'} overflowY="scroll">
|
||||
<ImageGallery />
|
||||
</GridItem>
|
||||
</Grid>
|
||||
<LogViewer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
@ -32,7 +32,7 @@ export const frontendToBackendParameters = (
|
||||
maskPath,
|
||||
shouldFitToWidthHeight,
|
||||
shouldGenerateVariations,
|
||||
variantAmount,
|
||||
variationAmount,
|
||||
seedWeights,
|
||||
shouldRunESRGAN,
|
||||
upscalingLevel,
|
||||
@ -71,13 +71,13 @@ export const frontendToBackendParameters = (
|
||||
}
|
||||
|
||||
if (shouldGenerateVariations) {
|
||||
generationParameters.variation_amount = variantAmount;
|
||||
generationParameters.variation_amount = variationAmount;
|
||||
if (seedWeights) {
|
||||
generationParameters.with_variations =
|
||||
stringToSeedWeights(seedWeights);
|
||||
}
|
||||
} else {
|
||||
generationParameters.variation_amount = 0;
|
||||
generationParameters.variation_amount = 0.1;
|
||||
}
|
||||
|
||||
let esrganParameters: false | { [k: string]: any } = false;
|
||||
@ -138,7 +138,7 @@ export const backendToFrontendParameters = (parameters: {
|
||||
|
||||
if (variation_amount > 0) {
|
||||
sd.shouldGenerateVariations = true;
|
||||
sd.variantAmount = variation_amount;
|
||||
sd.variationAmount = variation_amount;
|
||||
if (with_variations) {
|
||||
sd.seedWeights = seedWeightsToString(with_variations);
|
||||
}
|
||||
|
@ -1,16 +1,21 @@
|
||||
import { Button, ButtonProps } from '@chakra-ui/react';
|
||||
|
||||
interface Props extends ButtonProps {
|
||||
label: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable customized button component. Originally was more customized - now probably unecessary.
|
||||
*
|
||||
* TODO: Get rid of this.
|
||||
*/
|
||||
const SDButton = (props: Props) => {
|
||||
const { label, size = 'sm', ...rest } = props;
|
||||
return (
|
||||
<Button size={size} {...rest}>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
const { label, size = 'sm', ...rest } = props;
|
||||
return (
|
||||
<Button size={size} {...rest}>
|
||||
{label}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SDButton;
|
||||
|
@ -16,6 +16,9 @@ interface Props extends NumberInputProps {
|
||||
width?: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customized Chakra FormControl + NumberInput multi-part component.
|
||||
*/
|
||||
const SDNumberInput = (props: Props) => {
|
||||
const {
|
||||
label,
|
||||
@ -31,7 +34,7 @@ const SDNumberInput = (props: Props) => {
|
||||
<Flex gap={2} justifyContent={'space-between'} alignItems={'center'}>
|
||||
{label && (
|
||||
<FormLabel marginBottom={1}>
|
||||
<Text fontSize={fontSize} whiteSpace='nowrap'>
|
||||
<Text fontSize={fontSize} whiteSpace="nowrap">
|
||||
{label}
|
||||
</Text>
|
||||
</FormLabel>
|
||||
@ -42,7 +45,7 @@ const SDNumberInput = (props: Props) => {
|
||||
keepWithinRange={false}
|
||||
clampValueOnBlur={true}
|
||||
>
|
||||
<NumberInputField fontSize={'md'}/>
|
||||
<NumberInputField fontSize={'md'} />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
|
@ -1,57 +1,56 @@
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Select,
|
||||
SelectProps,
|
||||
Text,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Select,
|
||||
SelectProps,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface Props extends SelectProps {
|
||||
label: string;
|
||||
validValues:
|
||||
| Array<number | string>
|
||||
| Array<{ key: string; value: string | number }>;
|
||||
label: string;
|
||||
validValues:
|
||||
| Array<number | string>
|
||||
| Array<{ key: string; value: string | number }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customized Chakra FormControl + Select multi-part component.
|
||||
*/
|
||||
const SDSelect = (props: Props) => {
|
||||
const {
|
||||
label,
|
||||
isDisabled,
|
||||
validValues,
|
||||
size = 'sm',
|
||||
fontSize = 'md',
|
||||
marginBottom = 1,
|
||||
whiteSpace = 'nowrap',
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl isDisabled={isDisabled}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<FormLabel
|
||||
marginBottom={marginBottom}
|
||||
>
|
||||
<Text fontSize={fontSize} whiteSpace={whiteSpace}>
|
||||
{label}
|
||||
</Text>
|
||||
</FormLabel>
|
||||
<Select fontSize={fontSize} size={size} {...rest}>
|
||||
{validValues.map((opt) => {
|
||||
return typeof opt === 'string' ||
|
||||
typeof opt === 'number' ? (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
) : (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.key}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
);
|
||||
const {
|
||||
label,
|
||||
isDisabled,
|
||||
validValues,
|
||||
size = 'sm',
|
||||
fontSize = 'md',
|
||||
marginBottom = 1,
|
||||
whiteSpace = 'nowrap',
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<FormControl isDisabled={isDisabled}>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<FormLabel marginBottom={marginBottom}>
|
||||
<Text fontSize={fontSize} whiteSpace={whiteSpace}>
|
||||
{label}
|
||||
</Text>
|
||||
</FormLabel>
|
||||
<Select fontSize={fontSize} size={size} {...rest}>
|
||||
{validValues.map((opt) => {
|
||||
return typeof opt === 'string' || typeof opt === 'number' ? (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
) : (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.key}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default SDSelect;
|
||||
|
@ -11,6 +11,9 @@ interface Props extends SwitchProps {
|
||||
width?: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customized Chakra FormControl + Switch multi-part component.
|
||||
*/
|
||||
const SDSwitch = (props: Props) => {
|
||||
const {
|
||||
label,
|
||||
@ -28,7 +31,7 @@ const SDSwitch = (props: Props) => {
|
||||
fontSize={fontSize}
|
||||
marginBottom={1}
|
||||
flexGrow={2}
|
||||
whiteSpace='nowrap'
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
|
@ -1,161 +0,0 @@
|
||||
import { Center, Flex, Image, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import { setAllParameters, setInitialImagePath, setSeed } from '../sd/sdSlice';
|
||||
import { useState } from 'react';
|
||||
import ImageMetadataViewer from './ImageMetadataViewer';
|
||||
import DeleteImageModalButton from './DeleteImageModalButton';
|
||||
import SDButton from '../../components/SDButton';
|
||||
import { runESRGAN, runGFPGAN } from '../../app/socketio';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const height = 'calc(100vh - 238px)';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const CurrentImage = () => {
|
||||
const { currentImage, intermediateImage } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
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 (
|
||||
<Flex direction={'column'} rounded={'md'} borderWidth={1} p={2} gap={2}>
|
||||
{imageToDisplay && (
|
||||
<Flex gap={2}>
|
||||
<SDButton
|
||||
label='Use as initial image'
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
onClick={() =>
|
||||
dispatch(setInitialImagePath(imageToDisplay.url))
|
||||
}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label='Use all'
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
onClick={() =>
|
||||
dispatch(setAllParameters(imageToDisplay.metadata))
|
||||
}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label='Use seed'
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={!imageToDisplay.metadata.seed}
|
||||
onClick={() =>
|
||||
dispatch(setSeed(imageToDisplay.metadata.seed!))
|
||||
}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label='Upscale'
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={
|
||||
!isESRGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing)
|
||||
}
|
||||
onClick={() => dispatch(runESRGAN(imageToDisplay))}
|
||||
/>
|
||||
<SDButton
|
||||
label='Fix faces'
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={
|
||||
!isGFPGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing)
|
||||
}
|
||||
onClick={() => dispatch(runGFPGAN(imageToDisplay))}
|
||||
/>
|
||||
<SDButton
|
||||
label='Details'
|
||||
colorScheme={'gray'}
|
||||
variant={shouldShowImageDetails ? 'solid' : 'outline'}
|
||||
borderWidth={1}
|
||||
flexGrow={1}
|
||||
onClick={() =>
|
||||
setShouldShowImageDetails(!shouldShowImageDetails)
|
||||
}
|
||||
/>
|
||||
<DeleteImageModalButton image={imageToDisplay}>
|
||||
<SDButton
|
||||
label='Delete'
|
||||
colorScheme={'red'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={Boolean(intermediateImage)}
|
||||
/>
|
||||
</DeleteImageModalButton>
|
||||
</Flex>
|
||||
)}
|
||||
<Center height={height} position={'relative'}>
|
||||
{imageToDisplay && (
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
fit='contain'
|
||||
maxWidth={'100%'}
|
||||
maxHeight={'100%'}
|
||||
/>
|
||||
)}
|
||||
{imageToDisplay && shouldShowImageDetails && (
|
||||
<Flex
|
||||
width={'100%'}
|
||||
height={'100%'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
p={3}
|
||||
boxSizing='border-box'
|
||||
backgroundColor={bgColor}
|
||||
overflow='scroll'
|
||||
>
|
||||
<ImageMetadataViewer image={imageToDisplay} />
|
||||
</Flex>
|
||||
)}
|
||||
</Center>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentImage;
|
141
frontend/src/features/gallery/CurrentImageButtons.tsx
Normal file
141
frontend/src/features/gallery/CurrentImageButtons.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import { setAllParameters, setInitialImagePath, setSeed } from '../sd/sdSlice';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import SDButton from '../../components/SDButton';
|
||||
import { runESRGAN, runGFPGAN } from '../../app/socketio';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import { isEqual } from 'lodash';
|
||||
import { SDImage } from './gallerySlice';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type CurrentImageButtonsProps = {
|
||||
image: SDImage;
|
||||
shouldShowImageDetails: boolean;
|
||||
setShouldShowImageDetails: (b: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Row of buttons for common actions:
|
||||
* Use as init image, use all params, use seed, upscale, fix faces, details, delete.
|
||||
*/
|
||||
const CurrentImageButtons = ({
|
||||
image,
|
||||
shouldShowImageDetails,
|
||||
setShouldShowImageDetails,
|
||||
}: CurrentImageButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { intermediateImage } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
const { isProcessing, isConnected, isGFPGANAvailable, isESRGANAvailable } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const handleClickUseAsInitialImage = () =>
|
||||
dispatch(setInitialImagePath(image.url));
|
||||
|
||||
const handleClickUseAllParameters = () =>
|
||||
dispatch(setAllParameters(image.metadata));
|
||||
|
||||
// Non-null assertion: this button is disabled if there is no seed.
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const handleClickUseSeed = () => dispatch(setSeed(image.metadata.seed!));
|
||||
|
||||
const handleClickUpscale = () => dispatch(runESRGAN(image));
|
||||
|
||||
const handleClickFixFaces = () => dispatch(runGFPGAN(image));
|
||||
|
||||
const handleClickShowImageDetails = () =>
|
||||
setShouldShowImageDetails(!shouldShowImageDetails);
|
||||
|
||||
return (
|
||||
<Flex gap={2}>
|
||||
<SDButton
|
||||
label="Use as initial image"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
onClick={handleClickUseAsInitialImage}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label="Use all"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
onClick={handleClickUseAllParameters}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label="Use seed"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={!image.metadata.seed}
|
||||
onClick={handleClickUseSeed}
|
||||
/>
|
||||
|
||||
<SDButton
|
||||
label="Upscale"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={
|
||||
!isESRGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing)
|
||||
}
|
||||
onClick={handleClickUpscale}
|
||||
/>
|
||||
<SDButton
|
||||
label="Fix faces"
|
||||
colorScheme={'gray'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={
|
||||
!isGFPGANAvailable ||
|
||||
Boolean(intermediateImage) ||
|
||||
!(isConnected && !isProcessing)
|
||||
}
|
||||
onClick={handleClickFixFaces}
|
||||
/>
|
||||
<SDButton
|
||||
label="Details"
|
||||
colorScheme={'gray'}
|
||||
variant={shouldShowImageDetails ? 'solid' : 'outline'}
|
||||
borderWidth={1}
|
||||
flexGrow={1}
|
||||
onClick={handleClickShowImageDetails}
|
||||
/>
|
||||
<DeleteImageModal image={image}>
|
||||
<SDButton
|
||||
label="Delete"
|
||||
colorScheme={'red'}
|
||||
flexGrow={1}
|
||||
variant={'outline'}
|
||||
isDisabled={Boolean(intermediateImage)}
|
||||
/>
|
||||
</DeleteImageModal>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentImageButtons;
|
67
frontend/src/features/gallery/CurrentImageDisplay.tsx
Normal file
67
frontend/src/features/gallery/CurrentImageDisplay.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { Center, Flex, Image, Text, useColorModeValue } from '@chakra-ui/react';
|
||||
import { useAppSelector } from '../../app/hooks';
|
||||
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)';
|
||||
|
||||
/**
|
||||
* Displays the current image if there is one, plus associated actions.
|
||||
*/
|
||||
const CurrentImageDisplay = () => {
|
||||
const { currentImage, intermediateImage } = useAppSelector(
|
||||
(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'}>
|
||||
<Image
|
||||
src={imageToDisplay.url}
|
||||
fit="contain"
|
||||
maxWidth={'100%'}
|
||||
maxHeight={'100%'}
|
||||
/>
|
||||
{shouldShowImageDetails && (
|
||||
<Flex
|
||||
width={'100%'}
|
||||
height={'100%'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
p={3}
|
||||
boxSizing="border-box"
|
||||
backgroundColor={bgColor}
|
||||
overflow="scroll"
|
||||
>
|
||||
<ImageMetadataViewer image={imageToDisplay} />
|
||||
</Flex>
|
||||
)}
|
||||
</Center>
|
||||
</Flex>
|
||||
) : (
|
||||
<Center height={'100%'} position={'relative'}>
|
||||
<Text size={'xl'}>No image selected</Text>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentImageDisplay;
|
121
frontend/src/features/gallery/DeleteImageModal.tsx
Normal file
121
frontend/src/features/gallery/DeleteImageModal.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import {
|
||||
Text,
|
||||
AlertDialog,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogContent,
|
||||
AlertDialogOverlay,
|
||||
useDisclosure,
|
||||
Button,
|
||||
Switch,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Flex,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
ChangeEvent,
|
||||
cloneElement,
|
||||
ReactElement,
|
||||
SyntheticEvent,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { deleteImage } from '../../app/socketio';
|
||||
import { RootState } from '../../app/store';
|
||||
import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
||||
import { SDImage } from './gallerySlice';
|
||||
|
||||
interface DeleteImageModalProps {
|
||||
/**
|
||||
* Component which, on click, should delete the image/open the modal.
|
||||
*/
|
||||
children: ReactElement;
|
||||
/**
|
||||
* The image to delete.
|
||||
*/
|
||||
image: SDImage;
|
||||
}
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => system.shouldConfirmOnDelete
|
||||
);
|
||||
|
||||
/**
|
||||
* Needs a child, which will act as the button to delete an image.
|
||||
* If system.shouldConfirmOnDelete is true, a confirmation modal is displayed.
|
||||
* If it is false, the image is deleted immediately.
|
||||
* The confirmation modal has a "Don't ask me again" switch to set the boolean.
|
||||
*/
|
||||
const DeleteImageModal = ({ image, children }: DeleteImageModalProps) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldConfirmOnDelete = useAppSelector(systemSelector);
|
||||
const cancelRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleClickDelete = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
shouldConfirmOnDelete ? onOpen() : handleDelete();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
dispatch(deleteImage(image));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleChangeShouldConfirmOnDelete = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
) => dispatch(setShouldConfirmOnDelete(!e.target.checked));
|
||||
|
||||
return (
|
||||
<>
|
||||
{cloneElement(children, {
|
||||
// TODO: This feels wrong.
|
||||
onClick: handleClickDelete,
|
||||
})}
|
||||
|
||||
<AlertDialog
|
||||
isOpen={isOpen}
|
||||
leastDestructiveRef={cancelRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
Delete image
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>
|
||||
<Flex direction={'column'} gap={5}>
|
||||
<Text>
|
||||
Are you sure? You can't undo this action afterwards.
|
||||
</Text>
|
||||
<FormControl>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel mb={0}>Don't ask me again</FormLabel>
|
||||
<Switch
|
||||
checked={!shouldConfirmOnDelete}
|
||||
onChange={handleChangeShouldConfirmOnDelete}
|
||||
/>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
</AlertDialogBody>
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelRef} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="red" onClick={handleDelete} ml={3}>
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteImageModal;
|
@ -1,94 +0,0 @@
|
||||
import {
|
||||
IconButtonProps,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import {
|
||||
cloneElement,
|
||||
ReactElement,
|
||||
SyntheticEvent,
|
||||
} from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { deleteImage } from '../../app/socketio';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDButton from '../../components/SDButton';
|
||||
import { setShouldConfirmOnDelete, SystemState } from '../system/systemSlice';
|
||||
import { SDImage } from './gallerySlice';
|
||||
|
||||
interface Props extends IconButtonProps {
|
||||
image: SDImage;
|
||||
'aria-label': string;
|
||||
children: ReactElement;
|
||||
}
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => system.shouldConfirmOnDelete
|
||||
);
|
||||
|
||||
/*
|
||||
TODO: The modal and button to open it should be two different components,
|
||||
but their state is closely related and I'm not sure how best to accomplish it.
|
||||
*/
|
||||
const DeleteImageModalButton = (props: Omit<Props, 'aria-label'>) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldConfirmOnDelete = useAppSelector(systemSelector);
|
||||
|
||||
const handleClickDelete = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
shouldConfirmOnDelete ? onOpen() : handleDelete();
|
||||
};
|
||||
|
||||
const { image, children } = props;
|
||||
|
||||
const handleDelete = () => {
|
||||
dispatch(deleteImage(image));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleDeleteAndDontAsk = () => {
|
||||
dispatch(deleteImage(image));
|
||||
dispatch(setShouldConfirmOnDelete(false));
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{cloneElement(children, {
|
||||
onClick: handleClickDelete,
|
||||
})}
|
||||
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Are you sure you want to delete this image?</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Text>It will be deleted forever!</Text>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter justifyContent={'space-between'}>
|
||||
<SDButton label={'Yes'} colorScheme='red' onClick={handleDelete} />
|
||||
<SDButton
|
||||
label={"Yes, and don't ask me again"}
|
||||
colorScheme='red'
|
||||
onClick={handleDeleteAndDontAsk}
|
||||
/>
|
||||
<SDButton label='Cancel' colorScheme='blue' onClick={onClose} />
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteImageModalButton;
|
131
frontend/src/features/gallery/HoverableImage.tsx
Normal file
131
frontend/src/features/gallery/HoverableImage.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Icon,
|
||||
IconButton,
|
||||
Image,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch } from '../../app/hooks';
|
||||
import { SDImage, setCurrentImage } from './gallerySlice';
|
||||
import { FaCheck, FaCopy, FaSeedling, FaTrash } from 'react-icons/fa';
|
||||
import DeleteImageModal from './DeleteImageModal';
|
||||
import { memo, SyntheticEvent, useState } from 'react';
|
||||
import { setAllParameters, setSeed } from '../sd/sdSlice';
|
||||
|
||||
interface HoverableImageProps {
|
||||
image: SDImage;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const memoEqualityCheck = (
|
||||
prev: HoverableImageProps,
|
||||
next: HoverableImageProps
|
||||
) => prev.image.uuid === next.image.uuid && prev.isSelected === next.isSelected;
|
||||
|
||||
/**
|
||||
* Gallery image component with delete/use all/use seed buttons on hover.
|
||||
*/
|
||||
const HoverableImage = memo((props: HoverableImageProps) => {
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const checkColor = useColorModeValue('green.600', 'green.300');
|
||||
const bgColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const bgGradient = useColorModeValue(
|
||||
'radial-gradient(circle, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0.7) 20%, rgba(0,0,0,0) 100%)',
|
||||
'radial-gradient(circle, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.7) 20%, rgba(0,0,0,0) 100%)'
|
||||
);
|
||||
|
||||
const { image, isSelected } = props;
|
||||
const { url, uuid, metadata } = image;
|
||||
|
||||
const handleMouseOver = () => setIsHovered(true);
|
||||
const handleMouseOut = () => setIsHovered(false);
|
||||
|
||||
const handleClickSetAllParameters = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setAllParameters(metadata));
|
||||
};
|
||||
|
||||
const handleClickSetSeed = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
// Non-null assertion: this button is not rendered unless this exists
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
dispatch(setSeed(image.metadata.seed!));
|
||||
};
|
||||
|
||||
const handleClickImage = () => dispatch(setCurrentImage(image));
|
||||
|
||||
return (
|
||||
<Box position={'relative'} key={uuid}>
|
||||
<Image
|
||||
width={120}
|
||||
height={120}
|
||||
objectFit="cover"
|
||||
rounded={'md'}
|
||||
src={url}
|
||||
loading={'lazy'}
|
||||
backgroundColor={bgColor}
|
||||
/>
|
||||
<Flex
|
||||
cursor={'pointer'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
rounded={'md'}
|
||||
width="100%"
|
||||
height="100%"
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
background={isSelected ? bgGradient : undefined}
|
||||
onClick={handleClickImage}
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
>
|
||||
{isSelected && (
|
||||
<Icon fill={checkColor} width={'50%'} height={'50%'} as={FaCheck} />
|
||||
)}
|
||||
{isHovered && (
|
||||
<Flex
|
||||
direction={'column'}
|
||||
gap={1}
|
||||
position={'absolute'}
|
||||
top={1}
|
||||
right={1}
|
||||
>
|
||||
<DeleteImageModal image={image}>
|
||||
<IconButton
|
||||
colorScheme="red"
|
||||
aria-label="Delete image"
|
||||
icon={<FaTrash />}
|
||||
size="xs"
|
||||
fontSize={15}
|
||||
/>
|
||||
</DeleteImageModal>
|
||||
<IconButton
|
||||
aria-label="Use all parameters"
|
||||
colorScheme={'blue'}
|
||||
icon={<FaCopy />}
|
||||
size="xs"
|
||||
fontSize={15}
|
||||
onClickCapture={handleClickSetAllParameters}
|
||||
/>
|
||||
{image.metadata.seed && (
|
||||
<IconButton
|
||||
aria-label="Use seed"
|
||||
colorScheme={'blue'}
|
||||
icon={<FaSeedling />}
|
||||
size="xs"
|
||||
fontSize={16}
|
||||
onClickCapture={handleClickSetSeed}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}, memoEqualityCheck);
|
||||
|
||||
export default HoverableImage;
|
35
frontend/src/features/gallery/ImageGallery.tsx
Normal file
35
frontend/src/features/gallery/ImageGallery.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppSelector } from '../../app/hooks';
|
||||
import HoverableImage from './HoverableImage';
|
||||
|
||||
/**
|
||||
* Simple image gallery.
|
||||
*/
|
||||
const ImageGallery = () => {
|
||||
const { images, currentImageUuid } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
/**
|
||||
* I don't like that this needs to rerender whenever the current image is changed.
|
||||
* What if we have a large number of images? I suppose pagination (planned) will
|
||||
* mitigate this issue.
|
||||
*
|
||||
* TODO: Refactor if performance complaints, or after migrating to new API which supports pagination.
|
||||
*/
|
||||
|
||||
return (
|
||||
<Flex gap={2} wrap="wrap" pb={2}>
|
||||
{[...images].reverse().map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage key={uuid} image={image} isSelected={isSelected} />
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageGallery;
|
@ -1,12 +1,13 @@
|
||||
import {
|
||||
Center,
|
||||
Flex,
|
||||
IconButton,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
Text,
|
||||
Center,
|
||||
Flex,
|
||||
IconButton,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { memo } from 'react';
|
||||
import { FaPlus } from 'react-icons/fa';
|
||||
import { PARAMETERS } from '../../app/constants';
|
||||
import { useAppDispatch } from '../../app/hooks';
|
||||
@ -14,111 +15,120 @@ import SDButton from '../../components/SDButton';
|
||||
import { setAllParameters, setParameter } from '../sd/sdSlice';
|
||||
import { SDImage, SDMetadata } from './gallerySlice';
|
||||
|
||||
type Props = {
|
||||
image: SDImage;
|
||||
type ImageMetadataViewerProps = {
|
||||
image: SDImage;
|
||||
};
|
||||
|
||||
const ImageMetadataViewer = ({ image }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
// TODO: I don't know if this is needed.
|
||||
const memoEqualityCheck = (
|
||||
prev: ImageMetadataViewerProps,
|
||||
next: ImageMetadataViewerProps
|
||||
) => prev.image.uuid === next.image.uuid;
|
||||
|
||||
const keys = Object.keys(PARAMETERS);
|
||||
// TODO: Show more interesting information in this component.
|
||||
|
||||
const metadata: Array<{
|
||||
label: string;
|
||||
key: string;
|
||||
value: string | number | boolean;
|
||||
}> = [];
|
||||
/**
|
||||
* Image metadata viewer overlays currently selected image and provides
|
||||
* access to any of its metadata for use in processing.
|
||||
*/
|
||||
const ImageMetadataViewer = memo(({ image }: ImageMetadataViewerProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
keys.forEach((key) => {
|
||||
const value = image.metadata[key as keyof SDMetadata];
|
||||
if (value !== undefined) {
|
||||
metadata.push({ label: PARAMETERS[key], key, value });
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Build an array representing each item of metadata and a human-readable
|
||||
* label for it e.g. "cfgScale" > "CFG Scale".
|
||||
*
|
||||
* This array is then used to render each item with a button to use that
|
||||
* parameter in the processing settings.
|
||||
*
|
||||
* TODO: All this logic feels sloppy.
|
||||
*/
|
||||
const keys = Object.keys(PARAMETERS);
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'} overflowY={'scroll'} width={'100%'}>
|
||||
<SDButton
|
||||
label='Use all parameters'
|
||||
colorScheme={'gray'}
|
||||
padding={2}
|
||||
isDisabled={metadata.length === 0}
|
||||
onClick={() => dispatch(setAllParameters(image.metadata))}
|
||||
/>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight={'semibold'}>File:</Text>
|
||||
<Link href={image.url} isExternal>
|
||||
<Text>{image.url}</Text>
|
||||
</Link>
|
||||
</Flex>
|
||||
{metadata.length ? (
|
||||
<>
|
||||
<List>
|
||||
{metadata.map((parameter, i) => {
|
||||
const { label, key, value } = parameter;
|
||||
return (
|
||||
<ListItem key={i} pb={1}>
|
||||
<Flex gap={2}>
|
||||
<IconButton
|
||||
aria-label='Use this parameter'
|
||||
icon={<FaPlus />}
|
||||
size={'xs'}
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
setParameter({
|
||||
key,
|
||||
value,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Text fontWeight={'semibold'}>
|
||||
{label}:
|
||||
</Text>
|
||||
const metadata: Array<{
|
||||
label: string;
|
||||
key: string;
|
||||
value: string | number | boolean;
|
||||
}> = [];
|
||||
|
||||
{value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
value === 0 ? (
|
||||
<Text
|
||||
maxHeight={100}
|
||||
fontStyle={'italic'}
|
||||
>
|
||||
None
|
||||
</Text>
|
||||
) : (
|
||||
<Text
|
||||
maxHeight={100}
|
||||
overflowY={'scroll'}
|
||||
>
|
||||
{value.toString()}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight={'semibold'}>Raw:</Text>
|
||||
<Text
|
||||
maxHeight={100}
|
||||
overflowY={'scroll'}
|
||||
wordBreak={'break-all'}
|
||||
>
|
||||
{JSON.stringify(image.metadata)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<Center width={'100%'} pt={10}>
|
||||
<Text fontSize={'lg'} fontWeight='semibold'>
|
||||
No metadata available
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
keys.forEach((key) => {
|
||||
const value = image.metadata[key as keyof SDMetadata];
|
||||
if (value !== undefined) {
|
||||
metadata.push({ label: PARAMETERS[key], key, value });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'} overflowY={'scroll'} width={'100%'}>
|
||||
<SDButton
|
||||
label="Use all parameters"
|
||||
colorScheme={'gray'}
|
||||
padding={2}
|
||||
isDisabled={metadata.length === 0}
|
||||
onClick={() => dispatch(setAllParameters(image.metadata))}
|
||||
/>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight={'semibold'}>File:</Text>
|
||||
<Link href={image.url} isExternal>
|
||||
<Text>{image.url}</Text>
|
||||
</Link>
|
||||
</Flex>
|
||||
{metadata.length ? (
|
||||
<>
|
||||
<List>
|
||||
{metadata.map((parameter, i) => {
|
||||
const { label, key, value } = parameter;
|
||||
return (
|
||||
<ListItem key={i} pb={1}>
|
||||
<Flex gap={2}>
|
||||
<IconButton
|
||||
aria-label="Use this parameter"
|
||||
icon={<FaPlus />}
|
||||
size={'xs'}
|
||||
onClick={() =>
|
||||
dispatch(
|
||||
setParameter({
|
||||
key,
|
||||
value,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Text fontWeight={'semibold'}>{label}:</Text>
|
||||
|
||||
{value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
value === 0 ? (
|
||||
<Text maxHeight={100} fontStyle={'italic'}>
|
||||
None
|
||||
</Text>
|
||||
) : (
|
||||
<Text maxHeight={100} overflowY={'scroll'}>
|
||||
{value.toString()}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Flex gap={2}>
|
||||
<Text fontWeight={'semibold'}>Raw:</Text>
|
||||
<Text maxHeight={100} overflowY={'scroll'} wordBreak={'break-all'}>
|
||||
{JSON.stringify(image.metadata)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<Center width={'100%'} pt={10}>
|
||||
<Text fontSize={'lg'} fontWeight="semibold">
|
||||
No metadata available
|
||||
</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}, memoEqualityCheck);
|
||||
|
||||
export default ImageMetadataViewer;
|
||||
|
@ -1,150 +0,0 @@
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Icon,
|
||||
IconButton,
|
||||
Image,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { SDImage, setCurrentImage } from './gallerySlice';
|
||||
import { FaCheck, FaCopy, FaSeedling, FaTrash } from 'react-icons/fa';
|
||||
import DeleteImageModalButton from './DeleteImageModalButton';
|
||||
import { memo, SyntheticEvent, useState } from 'react';
|
||||
import { setAllParameters, setSeed } from '../sd/sdSlice';
|
||||
|
||||
interface HoverableImageProps {
|
||||
image: SDImage;
|
||||
isSelected: boolean;
|
||||
}
|
||||
|
||||
const HoverableImage = memo(
|
||||
(props: HoverableImageProps) => {
|
||||
const [isHovered, setIsHovered] = useState<boolean>(false);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const checkColor = useColorModeValue('green.600', 'green.300');
|
||||
const bgColor = useColorModeValue('gray.200', 'gray.700');
|
||||
const bgGradient = useColorModeValue(
|
||||
'radial-gradient(circle, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0.7) 20%, rgba(0,0,0,0) 100%)',
|
||||
'radial-gradient(circle, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.7) 20%, rgba(0,0,0,0) 100%)'
|
||||
);
|
||||
|
||||
const { image, isSelected } = props;
|
||||
const { url, uuid, metadata } = image;
|
||||
|
||||
const handleMouseOver = () => setIsHovered(true);
|
||||
const handleMouseOut = () => setIsHovered(false);
|
||||
const handleClickSetAllParameters = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setAllParameters(metadata));
|
||||
};
|
||||
const handleClickSetSeed = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setSeed(image.metadata.seed!)); // component not rendered unless this exists
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position={'relative'} key={uuid}>
|
||||
<Image
|
||||
width={120}
|
||||
height={120}
|
||||
objectFit='cover'
|
||||
rounded={'md'}
|
||||
src={url}
|
||||
loading={'lazy'}
|
||||
backgroundColor={bgColor}
|
||||
/>
|
||||
<Flex
|
||||
cursor={'pointer'}
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
rounded={'md'}
|
||||
width='100%'
|
||||
height='100%'
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
background={isSelected ? bgGradient : undefined}
|
||||
onClick={() => dispatch(setCurrentImage(image))}
|
||||
onMouseOver={handleMouseOver}
|
||||
onMouseOut={handleMouseOut}
|
||||
>
|
||||
{isSelected && (
|
||||
<Icon
|
||||
fill={checkColor}
|
||||
width={'50%'}
|
||||
height={'50%'}
|
||||
as={FaCheck}
|
||||
/>
|
||||
)}
|
||||
{isHovered && (
|
||||
<Flex
|
||||
direction={'column'}
|
||||
gap={1}
|
||||
position={'absolute'}
|
||||
top={1}
|
||||
right={1}
|
||||
>
|
||||
<DeleteImageModalButton image={image}>
|
||||
<IconButton
|
||||
colorScheme='red'
|
||||
aria-label='Delete image'
|
||||
icon={<FaTrash />}
|
||||
size='xs'
|
||||
fontSize={15}
|
||||
/>
|
||||
</DeleteImageModalButton>
|
||||
<IconButton
|
||||
aria-label='Use all parameters'
|
||||
colorScheme={'blue'}
|
||||
icon={<FaCopy />}
|
||||
size='xs'
|
||||
fontSize={15}
|
||||
onClickCapture={handleClickSetAllParameters}
|
||||
/>
|
||||
{image.metadata.seed && (
|
||||
<IconButton
|
||||
aria-label='Use seed'
|
||||
colorScheme={'blue'}
|
||||
icon={<FaSeedling />}
|
||||
size='xs'
|
||||
fontSize={16}
|
||||
onClickCapture={handleClickSetSeed}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.image.uuid === next.image.uuid &&
|
||||
prev.isSelected === next.isSelected
|
||||
);
|
||||
|
||||
const ImageRoll = () => {
|
||||
const { images, currentImageUuid } = useAppSelector(
|
||||
(state: RootState) => state.gallery
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex gap={2} wrap='wrap' pb={2}>
|
||||
{[...images].reverse().map((image) => {
|
||||
const { uuid } = image;
|
||||
const isSelected = currentImageUuid === uuid;
|
||||
return (
|
||||
<HoverableImage
|
||||
key={uuid}
|
||||
image={image}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageRoll;
|
@ -27,24 +27,36 @@ const systemSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Header, includes color mode toggle, settings button, status message.
|
||||
*/
|
||||
const SiteHeader = () => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { isConnected } = useAppSelector(systemSelector);
|
||||
|
||||
const statusMessage = isConnected
|
||||
? `Connected to server`
|
||||
: 'No connection to server';
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<Flex minWidth='max-content' alignItems='center' gap='1' pl={2} pr={1}>
|
||||
<Flex minWidth="max-content" alignItems="center" gap="1" pl={2} pr={1}>
|
||||
<Heading size={'lg'}>Stable Diffusion Dream Server</Heading>
|
||||
|
||||
<Spacer />
|
||||
|
||||
<Text textColor={isConnected ? 'green.500' : 'red.500'}>
|
||||
{isConnected ? `Connected to server` : 'No connection to server'}
|
||||
</Text>
|
||||
<Text textColor={statusMessageTextColor}>{statusMessage}</Text>
|
||||
|
||||
<SettingsModal>
|
||||
<IconButton
|
||||
aria-label='Settings'
|
||||
variant='link'
|
||||
aria-label="Settings"
|
||||
variant="link"
|
||||
fontSize={24}
|
||||
size={'sm'}
|
||||
icon={<MdSettings />}
|
||||
@ -52,14 +64,14 @@ const SiteHeader = () => {
|
||||
</SettingsModal>
|
||||
|
||||
<IconButton
|
||||
aria-label='Link to Github Issues'
|
||||
variant='link'
|
||||
aria-label="Link to Github Issues"
|
||||
variant="link"
|
||||
fontSize={23}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link
|
||||
isExternal
|
||||
href='http://github.com/lstein/stable-diffusion/issues'
|
||||
href="http://github.com/lstein/stable-diffusion/issues"
|
||||
>
|
||||
<MdHelp />
|
||||
</Link>
|
||||
@ -67,24 +79,24 @@ const SiteHeader = () => {
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
aria-label='Link to Github Repo'
|
||||
variant='link'
|
||||
aria-label="Link to Github Repo"
|
||||
variant="link"
|
||||
fontSize={20}
|
||||
size={'sm'}
|
||||
icon={
|
||||
<Link isExternal href='http://github.com/lstein/stable-diffusion'>
|
||||
<Link isExternal href="http://github.com/lstein/stable-diffusion">
|
||||
<FaGithub />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
aria-label='Toggle Dark Mode'
|
||||
aria-label="Toggle Dark Mode"
|
||||
onClick={toggleColorMode}
|
||||
variant='link'
|
||||
variant="link"
|
||||
size={'sm'}
|
||||
fontSize={colorMode == 'light' ? 18 : 20}
|
||||
icon={colorMode == 'light' ? <FaMoon /> : <FaSun />}
|
||||
fontSize={colorModeIconFontSize}
|
||||
icon={colorModeIcon}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
|
@ -4,10 +4,10 @@ import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
|
||||
import {
|
||||
setUpscalingLevel,
|
||||
setUpscalingStrength,
|
||||
UpscalingLevel,
|
||||
SDState,
|
||||
setUpscalingLevel,
|
||||
setUpscalingStrength,
|
||||
UpscalingLevel,
|
||||
SDState,
|
||||
} from '../sd/sdSlice';
|
||||
|
||||
import SDNumberInput from '../../components/SDNumberInput';
|
||||
@ -17,68 +17,71 @@ import { UPSCALING_LEVELS } from '../../app/constants';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
upscalingLevel: sd.upscalingLevel,
|
||||
upscalingStrength: sd.upscalingStrength,
|
||||
};
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
upscalingLevel: sd.upscalingLevel,
|
||||
upscalingStrength: sd.upscalingStrength,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
};
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays upscaling/ESRGAN options (level and strength).
|
||||
*/
|
||||
const ESRGANOptions = () => {
|
||||
const { upscalingLevel, upscalingStrength } = useAppSelector(sdSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
const { upscalingLevel, upscalingStrength } = useAppSelector(sdSelector);
|
||||
const { isESRGANAvailable } = useAppSelector(systemSelector);
|
||||
|
||||
const { isESRGANAvailable } = useAppSelector(systemSelector);
|
||||
const handleChangeLevel = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setUpscalingLevel(Number(e.target.value) as UpscalingLevel));
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleChangeStrength = (v: string | number) =>
|
||||
dispatch(setUpscalingStrength(Number(v)));
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDSelect
|
||||
isDisabled={!isESRGANAvailable}
|
||||
label='Scale'
|
||||
value={upscalingLevel}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setUpscalingLevel(
|
||||
Number(e.target.value) as UpscalingLevel
|
||||
)
|
||||
)
|
||||
}
|
||||
validValues={UPSCALING_LEVELS}
|
||||
/>
|
||||
<SDNumberInput
|
||||
isDisabled={!isESRGANAvailable}
|
||||
label='Strength'
|
||||
step={0.05}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={(v) => dispatch(setUpscalingStrength(Number(v)))}
|
||||
value={upscalingStrength}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDSelect
|
||||
isDisabled={!isESRGANAvailable}
|
||||
label="Scale"
|
||||
value={upscalingLevel}
|
||||
onChange={handleChangeLevel}
|
||||
validValues={UPSCALING_LEVELS}
|
||||
/>
|
||||
<SDNumberInput
|
||||
isDisabled={!isESRGANAvailable}
|
||||
label="Strength"
|
||||
step={0.05}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={handleChangeStrength}
|
||||
value={upscalingStrength}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ESRGANOptions;
|
||||
|
@ -12,52 +12,57 @@ import { isEqual } from 'lodash';
|
||||
import { SystemState } from '../system/systemSlice';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
gfpganStrength: sd.gfpganStrength,
|
||||
};
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
gfpganStrength: sd.gfpganStrength,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
};
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays face-fixing/GFPGAN options (strength).
|
||||
*/
|
||||
const GFPGANOptions = () => {
|
||||
const { gfpganStrength } = useAppSelector(sdSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
const { gfpganStrength } = useAppSelector(sdSelector);
|
||||
const { isGFPGANAvailable } = useAppSelector(systemSelector);
|
||||
|
||||
const { isGFPGANAvailable } = useAppSelector(systemSelector);
|
||||
const handleChangeStrength = (v: string | number) =>
|
||||
dispatch(setGfpganStrength(Number(v)));
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDNumberInput
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
label='Strength'
|
||||
step={0.05}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={(v) => dispatch(setGfpganStrength(Number(v)))}
|
||||
value={gfpganStrength}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDNumberInput
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
label="Strength"
|
||||
step={0.05}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={handleChangeStrength}
|
||||
value={gfpganStrength}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default GFPGANOptions;
|
||||
|
@ -1,54 +1,59 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDNumberInput from '../../components/SDNumberInput';
|
||||
import SDSwitch from '../../components/SDSwitch';
|
||||
import InitImage from './InitImage';
|
||||
import InitAndMaskImage from './InitAndMaskImage';
|
||||
import {
|
||||
SDState,
|
||||
setImg2imgStrength,
|
||||
setShouldFitToWidthHeight,
|
||||
SDState,
|
||||
setImg2imgStrength,
|
||||
setShouldFitToWidthHeight,
|
||||
} from './sdSlice';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
initialImagePath: sd.initialImagePath,
|
||||
img2imgStrength: sd.img2imgStrength,
|
||||
shouldFitToWidthHeight: sd.shouldFitToWidthHeight,
|
||||
};
|
||||
}
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
img2imgStrength: sd.img2imgStrength,
|
||||
shouldFitToWidthHeight: sd.shouldFitToWidthHeight,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Options for img2img generation (strength, fit, init/mask upload).
|
||||
*/
|
||||
const ImageToImageOptions = () => {
|
||||
const { initialImagePath, img2imgStrength, shouldFitToWidthHeight } =
|
||||
useAppSelector(sdSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
const { img2imgStrength, shouldFitToWidthHeight } =
|
||||
useAppSelector(sdSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
return (
|
||||
<Flex direction={'column'} gap={2}>
|
||||
<SDNumberInput
|
||||
isDisabled={!initialImagePath}
|
||||
label='Strength'
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={1}
|
||||
onChange={(v) => dispatch(setImg2imgStrength(Number(v)))}
|
||||
value={img2imgStrength}
|
||||
/>
|
||||
<SDSwitch
|
||||
isDisabled={!initialImagePath}
|
||||
label='Fit initial image to output size'
|
||||
isChecked={shouldFitToWidthHeight}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldFitToWidthHeight(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<InitImage />
|
||||
</Flex>
|
||||
);
|
||||
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;
|
||||
|
63
frontend/src/features/sd/ImageUploader.tsx
Normal file
63
frontend/src/features/sd/ImageUploader.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { cloneElement, ReactElement, SyntheticEvent, useCallback } from 'react';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
|
||||
type ImageUploaderProps = {
|
||||
/**
|
||||
* Component which, on click, should open the upload interface.
|
||||
*/
|
||||
children: ReactElement;
|
||||
/**
|
||||
* Callback to handle uploading the selected file.
|
||||
*/
|
||||
fileAcceptedCallback: (file: File) => void;
|
||||
/**
|
||||
* Callback to handle a file being rejected.
|
||||
*/
|
||||
fileRejectionCallback: (rejection: FileRejection) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* File upload using react-dropzone.
|
||||
* Needs a child to be the button to activate the upload interface.
|
||||
*/
|
||||
const ImageUploader = ({
|
||||
children,
|
||||
fileAcceptedCallback,
|
||||
fileRejectionCallback,
|
||||
}: ImageUploaderProps) => {
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||
fileRejections.forEach((rejection: FileRejection) => {
|
||||
fileRejectionCallback(rejection);
|
||||
});
|
||||
|
||||
acceptedFiles.forEach((file: File) => {
|
||||
fileAcceptedCallback(file);
|
||||
});
|
||||
},
|
||||
[fileAcceptedCallback, fileRejectionCallback]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, open } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/jpeg': ['.jpg', '.jpeg', '.png'],
|
||||
},
|
||||
});
|
||||
|
||||
const handleClickUploadIcon = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
open();
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...getRootProps()}>
|
||||
<input {...getInputProps({ multiple: false })} />
|
||||
{cloneElement(children, {
|
||||
onClick: handleClickUploadIcon,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUploader;
|
57
frontend/src/features/sd/InitAndMaskImage.tsx
Normal file
57
frontend/src/features/sd/InitAndMaskImage.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { Flex, Image } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import { SDState } from '../../features/sd/sdSlice';
|
||||
import './InitAndMaskImage.css';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import InitAndMaskUploadButtons from './InitAndMaskUploadButtons';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
initialImagePath: sd.initialImagePath,
|
||||
maskPath: sd.maskPath,
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||
);
|
||||
|
||||
/**
|
||||
* Displays init and mask images and buttons to upload/delete them.
|
||||
*/
|
||||
const InitAndMaskImage = () => {
|
||||
const { initialImagePath, maskPath } = useAppSelector(sdSelector);
|
||||
const [shouldShowMask, setShouldShowMask] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<Flex direction={'column'} alignItems={'center'} gap={2}>
|
||||
<InitAndMaskUploadButtons setShouldShowMask={setShouldShowMask} />
|
||||
{initialImagePath && (
|
||||
<Flex position={'relative'} width={'100%'}>
|
||||
<Image
|
||||
fit={'contain'}
|
||||
src={initialImagePath}
|
||||
rounded={'md'}
|
||||
className={'checkerboard'}
|
||||
/>
|
||||
{shouldShowMask && maskPath && (
|
||||
<Image
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
fit={'contain'}
|
||||
src={maskPath}
|
||||
rounded={'md'}
|
||||
zIndex={1}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitAndMaskImage;
|
131
frontend/src/features/sd/InitAndMaskUploadButtons.tsx
Normal file
131
frontend/src/features/sd/InitAndMaskUploadButtons.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { Button, Flex, IconButton, useToast } from '@chakra-ui/react';
|
||||
import { SyntheticEvent, useCallback } from 'react';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import {
|
||||
SDState,
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
} from '../../features/sd/sdSlice';
|
||||
import { uploadInitialImage, uploadMaskImage } from '../../app/socketio';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import ImageUploader from './ImageUploader';
|
||||
import { FileRejection } from 'react-dropzone';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
initialImagePath: sd.initialImagePath,
|
||||
maskPath: sd.maskPath,
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||
);
|
||||
|
||||
type InitAndMaskUploadButtonsProps = {
|
||||
setShouldShowMask: (b: boolean) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Init and mask image upload buttons.
|
||||
*/
|
||||
const InitAndMaskUploadButtons = ({
|
||||
setShouldShowMask,
|
||||
}: InitAndMaskUploadButtonsProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { initialImagePath } = useAppSelector(sdSelector);
|
||||
|
||||
// Use a toast to alert user when a file upload is rejected
|
||||
const toast = useToast();
|
||||
|
||||
// Clear the init and mask images
|
||||
const handleClickResetInitialImageAndMask = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setInitialImagePath(''));
|
||||
dispatch(setMaskPath(''));
|
||||
};
|
||||
|
||||
// Handle hover to view initial image and mask image
|
||||
const handleMouseOverInitialImageUploadButton = () =>
|
||||
setShouldShowMask(false);
|
||||
const handleMouseOutInitialImageUploadButton = () => setShouldShowMask(true);
|
||||
|
||||
const handleMouseOverMaskUploadButton = () => setShouldShowMask(true);
|
||||
const handleMouseOutMaskUploadButton = () => setShouldShowMask(true);
|
||||
|
||||
// Callbacks to for handling file upload attempts
|
||||
const initImageFileAcceptedCallback = useCallback(
|
||||
(file: File) => dispatch(uploadInitialImage(file)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const maskImageFileAcceptedCallback = useCallback(
|
||||
(file: File) => dispatch(uploadMaskImage(file)),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const fileRejectionCallback = useCallback(
|
||||
(rejection: FileRejection) => {
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
||||
''
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Upload failed',
|
||||
description: msg,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex gap={2} justifyContent={'space-between'} width={'100%'}>
|
||||
<ImageUploader
|
||||
fileAcceptedCallback={initImageFileAcceptedCallback}
|
||||
fileRejectionCallback={fileRejectionCallback}
|
||||
>
|
||||
<Button
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onMouseOver={handleMouseOverInitialImageUploadButton}
|
||||
onMouseOut={handleMouseOutInitialImageUploadButton}
|
||||
>
|
||||
Upload Image
|
||||
</Button>
|
||||
</ImageUploader>
|
||||
|
||||
<ImageUploader
|
||||
fileAcceptedCallback={maskImageFileAcceptedCallback}
|
||||
fileRejectionCallback={fileRejectionCallback}
|
||||
>
|
||||
<Button
|
||||
isDisabled={!initialImagePath}
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onMouseOver={handleMouseOverMaskUploadButton}
|
||||
onMouseOut={handleMouseOutMaskUploadButton}
|
||||
>
|
||||
Upload Mask
|
||||
</Button>
|
||||
</ImageUploader>
|
||||
|
||||
<IconButton
|
||||
isDisabled={!initialImagePath}
|
||||
size={'sm'}
|
||||
aria-label={'Reset initial image and mask'}
|
||||
onClick={handleClickResetInitialImageAndMask}
|
||||
icon={<FaTrash />}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitAndMaskUploadButtons;
|
@ -1,155 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
IconButton,
|
||||
Image,
|
||||
useToast,
|
||||
} from '@chakra-ui/react';
|
||||
import { SyntheticEvent, useCallback, useState } from 'react';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { FaTrash } from 'react-icons/fa';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import {
|
||||
SDState,
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
} from '../../features/sd/sdSlice';
|
||||
import MaskUploader from './MaskUploader';
|
||||
import './InitImage.css';
|
||||
import { uploadInitialImage } from '../../app/socketio';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
initialImagePath: sd.initialImagePath,
|
||||
maskPath: sd.maskPath,
|
||||
};
|
||||
},
|
||||
{ memoizeOptions: { resultEqualityCheck: isEqual } }
|
||||
);
|
||||
|
||||
const InitImage = () => {
|
||||
const toast = useToast();
|
||||
const dispatch = useAppDispatch();
|
||||
const { initialImagePath, maskPath } = useAppSelector(sdSelector);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||
fileRejections.forEach((rejection: FileRejection) => {
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) => acc + '\n' + cur.message,
|
||||
''
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Upload failed',
|
||||
description: msg,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
});
|
||||
|
||||
acceptedFiles.forEach((file: File) => {
|
||||
dispatch(uploadInitialImage(file));
|
||||
});
|
||||
},
|
||||
[dispatch, toast]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, open } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/jpeg': ['.jpg', '.jpeg', '.png'],
|
||||
},
|
||||
});
|
||||
|
||||
const [shouldShowMask, setShouldShowMask] = useState<boolean>(false);
|
||||
const handleClickUploadIcon = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
open();
|
||||
};
|
||||
const handleClickResetInitialImageAndMask = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
dispatch(setInitialImagePath(''));
|
||||
dispatch(setMaskPath(''));
|
||||
};
|
||||
|
||||
const handleMouseOverInitialImageUploadButton = () =>
|
||||
setShouldShowMask(false);
|
||||
const handleMouseOutInitialImageUploadButton = () => setShouldShowMask(true);
|
||||
|
||||
const handleMouseOverMaskUploadButton = () => setShouldShowMask(true);
|
||||
const handleMouseOutMaskUploadButton = () => setShouldShowMask(true);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
{...getRootProps({
|
||||
onClick: initialImagePath ? (e) => e.stopPropagation() : undefined,
|
||||
})}
|
||||
direction={'column'}
|
||||
alignItems={'center'}
|
||||
gap={2}
|
||||
>
|
||||
<input {...getInputProps({ multiple: false })} />
|
||||
<Flex gap={2} justifyContent={'space-between'} width={'100%'}>
|
||||
<Button
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onClick={handleClickUploadIcon}
|
||||
onMouseOver={handleMouseOverInitialImageUploadButton}
|
||||
onMouseOut={handleMouseOutInitialImageUploadButton}
|
||||
>
|
||||
Upload Image
|
||||
</Button>
|
||||
|
||||
<MaskUploader>
|
||||
<Button
|
||||
size={'sm'}
|
||||
fontSize={'md'}
|
||||
fontWeight={'normal'}
|
||||
onClick={handleClickUploadIcon}
|
||||
onMouseOver={handleMouseOverMaskUploadButton}
|
||||
onMouseOut={handleMouseOutMaskUploadButton}
|
||||
>
|
||||
Upload Mask
|
||||
</Button>
|
||||
</MaskUploader>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
aria-label={'Reset initial image and mask'}
|
||||
onClick={handleClickResetInitialImageAndMask}
|
||||
icon={<FaTrash />}
|
||||
/>
|
||||
</Flex>
|
||||
{initialImagePath && (
|
||||
<Flex position={'relative'} width={'100%'}>
|
||||
<Image
|
||||
fit={'contain'}
|
||||
src={initialImagePath}
|
||||
rounded={'md'}
|
||||
className={'checkerboard'}
|
||||
/>
|
||||
{shouldShowMask && maskPath && (
|
||||
<Image
|
||||
position={'absolute'}
|
||||
top={0}
|
||||
left={0}
|
||||
fit={'contain'}
|
||||
src={maskPath}
|
||||
rounded={'md'}
|
||||
zIndex={1}
|
||||
className={'checkerboard'}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default InitImage;
|
@ -1,61 +0,0 @@
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { cloneElement, ReactElement, SyntheticEvent, useCallback } from 'react';
|
||||
import { FileRejection, useDropzone } from 'react-dropzone';
|
||||
import { useAppDispatch } from '../../app/hooks';
|
||||
import { uploadMaskImage } from '../../app/socketio';
|
||||
|
||||
type Props = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
const MaskUploader = ({ children }: Props) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const toast = useToast();
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: Array<File>, fileRejections: Array<FileRejection>) => {
|
||||
fileRejections.forEach((rejection: FileRejection) => {
|
||||
const msg = rejection.errors.reduce(
|
||||
(acc: string, cur: { message: string }) =>
|
||||
acc + '\n' + cur.message,
|
||||
''
|
||||
);
|
||||
|
||||
toast({
|
||||
title: 'Upload failed',
|
||||
description: msg,
|
||||
status: 'error',
|
||||
isClosable: true,
|
||||
});
|
||||
});
|
||||
|
||||
acceptedFiles.forEach((file: File) => {
|
||||
dispatch(uploadMaskImage(file));
|
||||
});
|
||||
},
|
||||
[dispatch, toast]
|
||||
);
|
||||
|
||||
const { getRootProps, getInputProps, open } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/jpeg': ['.jpg', '.jpeg', '.png'],
|
||||
},
|
||||
});
|
||||
|
||||
const handleClickUploadIcon = (e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
open();
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...getRootProps()}>
|
||||
<input {...getInputProps({ multiple: false })} />
|
||||
{cloneElement(children, {
|
||||
onClick: handleClickUploadIcon,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MaskUploader;
|
@ -1,23 +1,24 @@
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Text,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionPanel,
|
||||
Switch,
|
||||
Flex,
|
||||
Box,
|
||||
Text,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionPanel,
|
||||
Switch,
|
||||
ExpandedIndex,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { RootState } from '../../app/store';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
|
||||
import {
|
||||
setShouldRunGFPGAN,
|
||||
setShouldRunESRGAN,
|
||||
SDState,
|
||||
setShouldUseInitImage,
|
||||
setShouldRunGFPGAN,
|
||||
setShouldRunESRGAN,
|
||||
SDState,
|
||||
setShouldUseInitImage,
|
||||
} from '../sd/sdSlice';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
@ -28,184 +29,189 @@ import ESRGANOptions from './ESRGANOptions';
|
||||
import GFPGANOptions from './GFPGANOptions';
|
||||
import OutputOptions from './OutputOptions';
|
||||
import ImageToImageOptions from './ImageToImageOptions';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
initialImagePath: sd.initialImagePath,
|
||||
shouldUseInitImage: sd.shouldUseInitImage,
|
||||
shouldRunESRGAN: sd.shouldRunESRGAN,
|
||||
shouldRunGFPGAN: sd.shouldRunGFPGAN,
|
||||
};
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
initialImagePath: sd.initialImagePath,
|
||||
shouldUseInitImage: sd.shouldUseInitImage,
|
||||
shouldRunESRGAN: sd.shouldRunESRGAN,
|
||||
shouldRunGFPGAN: sd.shouldRunGFPGAN,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
openAccordions: system.openAccordions,
|
||||
};
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isGFPGANAvailable: system.isGFPGANAvailable,
|
||||
isESRGANAvailable: system.isESRGANAvailable,
|
||||
openAccordions: system.openAccordions,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Main container for generation and processing parameters.
|
||||
*/
|
||||
const OptionsAccordion = () => {
|
||||
const {
|
||||
shouldRunESRGAN,
|
||||
shouldRunGFPGAN,
|
||||
shouldUseInitImage,
|
||||
initialImagePath,
|
||||
} = useAppSelector(sdSelector);
|
||||
const {
|
||||
shouldRunESRGAN,
|
||||
shouldRunGFPGAN,
|
||||
shouldUseInitImage,
|
||||
initialImagePath,
|
||||
} = useAppSelector(sdSelector);
|
||||
|
||||
const { isGFPGANAvailable, isESRGANAvailable, openAccordions } =
|
||||
useAppSelector(systemSelector);
|
||||
const { isGFPGANAvailable, isESRGANAvailable, openAccordions } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
defaultIndex={openAccordions}
|
||||
allowMultiple
|
||||
reduceMotion
|
||||
onChange={(openAccordions) =>
|
||||
dispatch(setOpenAccordions(openAccordions))
|
||||
}
|
||||
>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex='1' textAlign='left'>
|
||||
Seed & Variation
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<SeedVariationOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex='1' textAlign='left'>
|
||||
Sampler
|
||||
</Box>
|
||||
<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={(e) =>
|
||||
dispatch(
|
||||
setShouldRunESRGAN(e.target.checked)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<ESRGANOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Fix Faces (GFPGAN)</Text>
|
||||
<Switch
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
isChecked={shouldRunGFPGAN}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setShouldRunGFPGAN(e.target.checked)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<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={(e) =>
|
||||
dispatch(
|
||||
setShouldUseInitImage(e.target.checked)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<ImageToImageOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex='1' textAlign='left'>
|
||||
Output
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<OutputOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
/**
|
||||
* Stores accordion state in redux so preferred UI setup is retained.
|
||||
*/
|
||||
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}
|
||||
>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Seed & Variation
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<SeedVariationOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Sampler
|
||||
</Box>
|
||||
<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>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<ESRGANOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Flex
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
mr={2}
|
||||
>
|
||||
<Text>Fix Faces (GFPGAN)</Text>
|
||||
<Switch
|
||||
isDisabled={!isGFPGANAvailable}
|
||||
isChecked={shouldRunGFPGAN}
|
||||
onChange={handleChangeShouldRunGFPGAN}
|
||||
/>
|
||||
</Flex>
|
||||
<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>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<ImageToImageOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box flex="1" textAlign="left">
|
||||
Output
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<OutputOptions />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
|
||||
export default OptionsAccordion;
|
||||
|
@ -11,56 +11,66 @@ import { HEIGHTS, WIDTHS } from '../../app/constants';
|
||||
import SDSwitch from '../../components/SDSwitch';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
height: sd.height,
|
||||
width: sd.width,
|
||||
seamless: sd.seamless,
|
||||
};
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
height: sd.height,
|
||||
width: sd.width,
|
||||
seamless: sd.seamless,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Image output options. Includes width, height, seamless tiling.
|
||||
*/
|
||||
const OutputOptions = () => {
|
||||
const { height, width, seamless } = useAppSelector(sdSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
const { height, width, seamless } = useAppSelector(sdSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleChangeWidth = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setWidth(Number(e.target.value)));
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<Flex gap={2}>
|
||||
<SDSelect
|
||||
label='Width'
|
||||
value={width}
|
||||
flexGrow={1}
|
||||
onChange={(e) => dispatch(setWidth(Number(e.target.value)))}
|
||||
validValues={WIDTHS}
|
||||
/>
|
||||
<SDSelect
|
||||
label='Height'
|
||||
value={height}
|
||||
flexGrow={1}
|
||||
onChange={(e) =>
|
||||
dispatch(setHeight(Number(e.target.value)))
|
||||
}
|
||||
validValues={HEIGHTS}
|
||||
/>
|
||||
</Flex>
|
||||
<SDSwitch
|
||||
label='Seamless tiling'
|
||||
fontSize={'md'}
|
||||
isChecked={seamless}
|
||||
onChange={(e) => dispatch(setSeamless(e.target.checked))}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
const handleChangeHeight = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setHeight(Number(e.target.value)));
|
||||
|
||||
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
|
||||
label="Seamless tiling"
|
||||
fontSize={'md'}
|
||||
isChecked={seamless}
|
||||
onChange={handleChangeSeamless}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutputOptions;
|
||||
|
@ -9,50 +9,60 @@ import { SystemState } from '../system/systemSlice';
|
||||
import useCheckParameters from '../system/useCheckParameters';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
};
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Buttons to start and cancel image generation.
|
||||
*/
|
||||
const ProcessButtons = () => {
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
const isReady = useCheckParameters();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleClickGenerate = () => dispatch(generateImage());
|
||||
|
||||
const isReady = useCheckParameters();
|
||||
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={() => dispatch(generateImage())}
|
||||
/>
|
||||
<SDButton
|
||||
label='Cancel'
|
||||
colorScheme='red'
|
||||
flexGrow={1}
|
||||
fontSize={'md'}
|
||||
size={'md'}
|
||||
isDisabled={!isConnected || !isProcessing}
|
||||
onClick={() => dispatch(cancelProcessing())}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
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;
|
||||
|
@ -1,21 +1,28 @@
|
||||
import { Textarea } from '@chakra-ui/react';
|
||||
import { ChangeEvent } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import { setPrompt } from '../sd/sdSlice';
|
||||
|
||||
/**
|
||||
* Prompt input text area.
|
||||
*/
|
||||
const PromptInput = () => {
|
||||
const { prompt } = useAppSelector((state: RootState) => state.sd);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChangePrompt = (e: ChangeEvent<HTMLTextAreaElement>) =>
|
||||
dispatch(setPrompt(e.target.value));
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
id='prompt'
|
||||
name='prompt'
|
||||
resize='none'
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
resize="none"
|
||||
size={'lg'}
|
||||
height={'100%'}
|
||||
isInvalid={!prompt.length}
|
||||
onChange={(e) => dispatch(setPrompt(e.target.value))}
|
||||
onChange={handleChangePrompt}
|
||||
value={prompt}
|
||||
placeholder="I'm dreaming of..."
|
||||
/>
|
||||
|
@ -1,51 +0,0 @@
|
||||
import {
|
||||
Slider,
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Text,
|
||||
Flex,
|
||||
SliderProps,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
interface Props extends SliderProps {
|
||||
label: string;
|
||||
value: number;
|
||||
fontSize?: number | string;
|
||||
}
|
||||
|
||||
const SDSlider = ({
|
||||
label,
|
||||
value,
|
||||
fontSize = 'sm',
|
||||
onChange,
|
||||
...rest
|
||||
}: Props) => {
|
||||
return (
|
||||
<FormControl>
|
||||
<Flex gap={2}>
|
||||
<FormLabel marginInlineEnd={0} marginBottom={1}>
|
||||
<Text fontSize={fontSize} whiteSpace='nowrap'>
|
||||
{label}
|
||||
</Text>
|
||||
</FormLabel>
|
||||
<Slider
|
||||
aria-label={label}
|
||||
focusThumbOnChange={true}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
{...rest}
|
||||
>
|
||||
<SliderTrack>
|
||||
<SliderFilledTrack />
|
||||
</SliderTrack>
|
||||
<SliderThumb />
|
||||
</Slider>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default SDSlider;
|
@ -11,52 +11,64 @@ import SDSelect from '../../components/SDSelect';
|
||||
import { SAMPLERS } from '../../app/constants';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
steps: sd.steps,
|
||||
cfgScale: sd.cfgScale,
|
||||
sampler: sd.sampler,
|
||||
};
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
steps: sd.steps,
|
||||
cfgScale: sd.cfgScale,
|
||||
sampler: sd.sampler,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Sampler options. Includes steps, CFG scale, sampler.
|
||||
*/
|
||||
const SamplerOptions = () => {
|
||||
const { steps, cfgScale, sampler } = useAppSelector(sdSelector);
|
||||
const dispatch = useAppDispatch();
|
||||
const { steps, cfgScale, sampler } = useAppSelector(sdSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleChangeSteps = (v: string | number) =>
|
||||
dispatch(setSteps(Number(v)));
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<SDNumberInput
|
||||
label='Steps'
|
||||
min={1}
|
||||
step={1}
|
||||
precision={0}
|
||||
onChange={(v) => dispatch(setSteps(Number(v)))}
|
||||
value={steps}
|
||||
/>
|
||||
<SDNumberInput
|
||||
label='CFG scale'
|
||||
step={0.5}
|
||||
onChange={(v) => dispatch(setCfgScale(Number(v)))}
|
||||
value={cfgScale}
|
||||
/>
|
||||
<SDSelect
|
||||
label='Sampler'
|
||||
value={sampler}
|
||||
onChange={(e) => dispatch(setSampler(e.target.value))}
|
||||
validValues={SAMPLERS}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
const handleChangeCfgScale = (v: string | number) =>
|
||||
dispatch(setCfgScale(Number(v)));
|
||||
|
||||
const handleChangeSampler = (e: ChangeEvent<HTMLSelectElement>) =>
|
||||
dispatch(setSampler(e.target.value));
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<SDNumberInput
|
||||
label="Steps"
|
||||
min={1}
|
||||
step={1}
|
||||
precision={0}
|
||||
onChange={handleChangeSteps}
|
||||
value={steps}
|
||||
/>
|
||||
<SDNumberInput
|
||||
label="CFG scale"
|
||||
step={0.5}
|
||||
onChange={handleChangeCfgScale}
|
||||
value={cfgScale}
|
||||
/>
|
||||
<SDSelect
|
||||
label="Sampler"
|
||||
value={sampler}
|
||||
onChange={handleChangeSampler}
|
||||
validValues={SAMPLERS}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default SamplerOptions;
|
||||
|
@ -1,144 +1,159 @@
|
||||
import {
|
||||
Flex,
|
||||
Input,
|
||||
HStack,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Text,
|
||||
Button,
|
||||
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/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDNumberInput from '../../components/SDNumberInput';
|
||||
import SDSwitch from '../../components/SDSwitch';
|
||||
import {
|
||||
randomizeSeed,
|
||||
SDState,
|
||||
setIterations,
|
||||
setSeed,
|
||||
setSeedWeights,
|
||||
setShouldGenerateVariations,
|
||||
setShouldRandomizeSeed,
|
||||
setVariantAmount,
|
||||
SDState,
|
||||
setIterations,
|
||||
setSeed,
|
||||
setSeedWeights,
|
||||
setShouldGenerateVariations,
|
||||
setShouldRandomizeSeed,
|
||||
setVariationAmount,
|
||||
} from './sdSlice';
|
||||
import randomInt from './util/randomInt';
|
||||
import { validateSeedWeights } from './util/seedWeightPairs';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
variantAmount: sd.variantAmount,
|
||||
seedWeights: sd.seedWeights,
|
||||
shouldGenerateVariations: sd.shouldGenerateVariations,
|
||||
shouldRandomizeSeed: sd.shouldRandomizeSeed,
|
||||
seed: sd.seed,
|
||||
iterations: sd.iterations,
|
||||
};
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
variationAmount: sd.variationAmount,
|
||||
seedWeights: sd.seedWeights,
|
||||
shouldGenerateVariations: sd.shouldGenerateVariations,
|
||||
shouldRandomizeSeed: sd.shouldRandomizeSeed,
|
||||
seed: sd.seed,
|
||||
iterations: sd.iterations,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Seed & variation options. Includes iteration, seed, seed randomization, variation options.
|
||||
*/
|
||||
const SeedVariationOptions = () => {
|
||||
const {
|
||||
shouldGenerateVariations,
|
||||
variantAmount,
|
||||
seedWeights,
|
||||
shouldRandomizeSeed,
|
||||
seed,
|
||||
iterations,
|
||||
} = useAppSelector(sdSelector);
|
||||
const {
|
||||
shouldGenerateVariations,
|
||||
variationAmount,
|
||||
seedWeights,
|
||||
shouldRandomizeSeed,
|
||||
seed,
|
||||
iterations,
|
||||
} = useAppSelector(sdSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<Flex gap={2} direction={'column'}>
|
||||
<SDNumberInput
|
||||
label='Images to generate'
|
||||
step={1}
|
||||
min={1}
|
||||
precision={0}
|
||||
onChange={(v) => dispatch(setIterations(Number(v)))}
|
||||
value={iterations}
|
||||
/>
|
||||
<SDSwitch
|
||||
label='Randomize seed on generation'
|
||||
isChecked={shouldRandomizeSeed}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldRandomizeSeed(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<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={(v) => dispatch(setSeed(Number(v)))}
|
||||
value={seed}
|
||||
/>
|
||||
<Button
|
||||
size={'sm'}
|
||||
isDisabled={shouldRandomizeSeed}
|
||||
onClick={() => dispatch(randomizeSeed())}
|
||||
>
|
||||
<Text pl={2} pr={2}>
|
||||
Shuffle
|
||||
</Text>
|
||||
</Button>
|
||||
</Flex>
|
||||
<SDSwitch
|
||||
label='Generate variations'
|
||||
isChecked={shouldGenerateVariations}
|
||||
width={'auto'}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldGenerateVariations(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<SDNumberInput
|
||||
label='Variation amount'
|
||||
value={variantAmount}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={1}
|
||||
isDisabled={!shouldGenerateVariations}
|
||||
onChange={(v) => dispatch(setVariantAmount(Number(v)))}
|
||||
/>
|
||||
<FormControl
|
||||
isInvalid={
|
||||
shouldGenerateVariations &&
|
||||
!(validateSeedWeights(seedWeights) || seedWeights === '')
|
||||
}
|
||||
flexGrow={1}
|
||||
isDisabled={!shouldGenerateVariations}
|
||||
>
|
||||
<HStack>
|
||||
<FormLabel marginInlineEnd={0} marginBottom={1}>
|
||||
<Text whiteSpace='nowrap'>
|
||||
Seed Weights
|
||||
</Text>
|
||||
</FormLabel>
|
||||
<Input
|
||||
size={'sm'}
|
||||
value={seedWeights}
|
||||
onChange={(e) =>
|
||||
dispatch(setSeedWeights(e.target.value))
|
||||
}
|
||||
/>
|
||||
</HStack>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
);
|
||||
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}
|
||||
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;
|
||||
|
@ -1,92 +0,0 @@
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
HStack,
|
||||
Input,
|
||||
Text,
|
||||
} from '@chakra-ui/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDNumberInput from '../../components/SDNumberInput';
|
||||
import SDSwitch from '../../components/SDSwitch';
|
||||
import {
|
||||
SDState,
|
||||
setSeedWeights,
|
||||
setShouldGenerateVariations,
|
||||
setVariantAmount,
|
||||
} from './sdSlice';
|
||||
import { validateSeedWeights } from './util/seedWeightPairs';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
variantAmount: sd.variantAmount,
|
||||
seedWeights: sd.seedWeights,
|
||||
shouldGenerateVariations: sd.shouldGenerateVariations,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const Variant = () => {
|
||||
const { shouldGenerateVariations, variantAmount, seedWeights } =
|
||||
useAppSelector(sdSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
return (
|
||||
<Flex gap={2} alignItems={'center'} pl={1}>
|
||||
<SDSwitch
|
||||
label='Generate variations'
|
||||
isChecked={shouldGenerateVariations}
|
||||
width={'auto'}
|
||||
onChange={(e) =>
|
||||
dispatch(setShouldGenerateVariations(e.target.checked))
|
||||
}
|
||||
/>
|
||||
<SDNumberInput
|
||||
label='Amount'
|
||||
value={variantAmount}
|
||||
step={0.01}
|
||||
min={0}
|
||||
max={1}
|
||||
width={240}
|
||||
isDisabled={!shouldGenerateVariations}
|
||||
onChange={(v) => dispatch(setVariantAmount(Number(v)))}
|
||||
/>
|
||||
<FormControl
|
||||
isInvalid={
|
||||
shouldGenerateVariations &&
|
||||
!(validateSeedWeights(seedWeights) || seedWeights === '')
|
||||
}
|
||||
flexGrow={1}
|
||||
isDisabled={!shouldGenerateVariations}
|
||||
>
|
||||
<HStack>
|
||||
<FormLabel marginInlineEnd={0} marginBottom={1}>
|
||||
<Text fontSize={'sm'} whiteSpace='nowrap'>
|
||||
Seed Weights
|
||||
</Text>
|
||||
</FormLabel>
|
||||
<Input
|
||||
size={'sm'}
|
||||
value={seedWeights}
|
||||
onChange={(e) =>
|
||||
dispatch(setSeedWeights(e.target.value))
|
||||
}
|
||||
/>
|
||||
</HStack>
|
||||
</FormControl>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default Variant;
|
@ -1,8 +1,6 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { SDMetadata } from '../gallery/gallerySlice';
|
||||
import randomInt from './util/randomInt';
|
||||
import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from '../../app/constants';
|
||||
|
||||
const calculateRealSteps = (
|
||||
steps: number,
|
||||
@ -12,7 +10,7 @@ const calculateRealSteps = (
|
||||
return hasInitImage ? Math.floor(strength * steps) : steps;
|
||||
};
|
||||
|
||||
export type UpscalingLevel = 0 | 2 | 3 | 4;
|
||||
export type UpscalingLevel = 0 | 2 | 4;
|
||||
|
||||
export interface SDState {
|
||||
prompt: string;
|
||||
@ -34,7 +32,7 @@ export interface SDState {
|
||||
seamless: boolean;
|
||||
shouldFitToWidthHeight: boolean;
|
||||
shouldGenerateVariations: boolean;
|
||||
variantAmount: number;
|
||||
variationAmount: number;
|
||||
seedWeights: string;
|
||||
shouldRunESRGAN: boolean;
|
||||
shouldRunGFPGAN: boolean;
|
||||
@ -58,7 +56,7 @@ const initialSDState: SDState = {
|
||||
maskPath: '',
|
||||
shouldFitToWidthHeight: true,
|
||||
shouldGenerateVariations: false,
|
||||
variantAmount: 0.1,
|
||||
variationAmount: 0.1,
|
||||
seedWeights: '',
|
||||
shouldRunESRGAN: false,
|
||||
upscalingLevel: 4,
|
||||
@ -151,9 +149,6 @@ export const sdSlice = createSlice({
|
||||
resetSeed: (state) => {
|
||||
state.seed = -1;
|
||||
},
|
||||
randomizeSeed: (state) => {
|
||||
state.seed = randomInt(NUMPY_RAND_MIN, NUMPY_RAND_MAX);
|
||||
},
|
||||
setParameter: (
|
||||
state,
|
||||
action: PayloadAction<{ key: string; value: string | number | boolean }>
|
||||
@ -171,8 +166,8 @@ export const sdSlice = createSlice({
|
||||
setShouldGenerateVariations: (state, action: PayloadAction<boolean>) => {
|
||||
state.shouldGenerateVariations = action.payload;
|
||||
},
|
||||
setVariantAmount: (state, action: PayloadAction<number>) => {
|
||||
state.variantAmount = action.payload;
|
||||
setVariationAmount: (state, action: PayloadAction<number>) => {
|
||||
state.variationAmount = action.payload;
|
||||
},
|
||||
setSeedWeights: (state, action: PayloadAction<string>) => {
|
||||
state.seedWeights = action.payload;
|
||||
@ -267,13 +262,12 @@ export const {
|
||||
setInitialImagePath,
|
||||
setMaskPath,
|
||||
resetSeed,
|
||||
randomizeSeed,
|
||||
resetSDState,
|
||||
setShouldFitToWidthHeight,
|
||||
setParameter,
|
||||
setShouldGenerateVariations,
|
||||
setSeedWeights,
|
||||
setVariantAmount,
|
||||
setVariationAmount,
|
||||
setAllParameters,
|
||||
setShouldRunGFPGAN,
|
||||
setShouldRunESRGAN,
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {
|
||||
IconButton,
|
||||
useColorModeValue,
|
||||
Flex,
|
||||
Text,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
useColorModeValue,
|
||||
Flex,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import { RootState } from '../../app/store';
|
||||
@ -14,112 +14,111 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const logSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => system.log,
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: (a, b) => a.length === b.length,
|
||||
},
|
||||
}
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => system.log,
|
||||
{
|
||||
memoizeOptions: {
|
||||
// We don't need a deep equality check for this selector.
|
||||
resultEqualityCheck: (a, b) => a.length === b.length,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return { shouldShowLogViewer: system.shouldShowLogViewer };
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return { shouldShowLogViewer: system.shouldShowLogViewer };
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Basic log viewer, floats on bottom of page.
|
||||
*/
|
||||
const LogViewer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const bg = useColorModeValue('gray.50', 'gray.900');
|
||||
const borderColor = useColorModeValue('gray.500', 'gray.500');
|
||||
const [shouldAutoscroll, setShouldAutoscroll] = useState<boolean>(true);
|
||||
const dispatch = useAppDispatch();
|
||||
const log = useAppSelector(logSelector);
|
||||
const { shouldShowLogViewer } = useAppSelector(systemSelector);
|
||||
|
||||
const log = useAppSelector(logSelector);
|
||||
const { shouldShowLogViewer } = useAppSelector(systemSelector);
|
||||
const bg = useColorModeValue('gray.50', 'gray.900');
|
||||
const borderColor = useColorModeValue('gray.500', 'gray.500');
|
||||
|
||||
const viewerRef = useRef<HTMLDivElement>(null);
|
||||
// Rudimentary autoscroll
|
||||
const [shouldAutoscroll, setShouldAutoscroll] = useState<boolean>(true);
|
||||
const viewerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (viewerRef.current !== null && shouldAutoscroll) {
|
||||
viewerRef.current.scrollTop = viewerRef.current.scrollHeight;
|
||||
}
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
if (viewerRef.current !== null && shouldAutoscroll) {
|
||||
viewerRef.current.scrollTop = viewerRef.current.scrollHeight;
|
||||
}
|
||||
}, [shouldAutoscroll]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{shouldShowLogViewer && (
|
||||
<Flex
|
||||
position={'fixed'}
|
||||
left={0}
|
||||
bottom={0}
|
||||
height='200px'
|
||||
width='100vw'
|
||||
overflow='auto'
|
||||
direction='column'
|
||||
fontFamily='monospace'
|
||||
fontSize='sm'
|
||||
pl={12}
|
||||
pr={2}
|
||||
pb={2}
|
||||
borderTopWidth='4px'
|
||||
borderColor={borderColor}
|
||||
background={bg}
|
||||
ref={viewerRef}
|
||||
>
|
||||
{log.map((entry, i) => (
|
||||
<Flex gap={2} key={i}>
|
||||
<Text fontSize='sm' fontWeight={'semibold'}>
|
||||
{entry.timestamp}:
|
||||
</Text>
|
||||
<Text fontSize='sm' wordBreak={'break-all'}>
|
||||
{entry.message}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
{shouldShowLogViewer && (
|
||||
<Tooltip
|
||||
label={
|
||||
shouldAutoscroll ? 'Autoscroll on' : 'Autoscroll off'
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
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'}>
|
||||
<IconButton
|
||||
size='sm'
|
||||
position={'fixed'}
|
||||
left={2}
|
||||
bottom={2}
|
||||
variant={'solid'}
|
||||
aria-label='Toggle Log Viewer'
|
||||
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
|
||||
onClick={() =>
|
||||
dispatch(setShouldShowLogViewer(!shouldShowLogViewer))
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
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}
|
||||
>
|
||||
{log.map((entry, i) => (
|
||||
<Flex gap={2} key={i}>
|
||||
<Text fontSize="sm" fontWeight={'semibold'}>
|
||||
{entry.timestamp}:
|
||||
</Text>
|
||||
<Text fontSize="sm" wordBreak={'break-all'}>
|
||||
{entry.message}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
{shouldShowLogViewer && (
|
||||
<Tooltip label={shouldAutoscroll ? 'Autoscroll on' : 'Autoscroll off'}>
|
||||
<IconButton
|
||||
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'}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
position={'fixed'}
|
||||
left={2}
|
||||
bottom={2}
|
||||
variant={'solid'}
|
||||
aria-label="Toggle Log Viewer"
|
||||
icon={shouldShowLogViewer ? <FaMinus /> : <FaCode />}
|
||||
onClick={() => dispatch(setShouldShowLogViewer(!shouldShowLogViewer))}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogViewer;
|
||||
|
@ -1,170 +1,164 @@
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Heading,
|
||||
HStack,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Switch,
|
||||
Text,
|
||||
useDisclosure,
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Heading,
|
||||
HStack,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Switch,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@chakra-ui/react';
|
||||
import { useAppDispatch, useAppSelector } from '../../app/hooks';
|
||||
import {
|
||||
setShouldConfirmOnDelete,
|
||||
setShouldDisplayInProgress,
|
||||
SystemState,
|
||||
setShouldConfirmOnDelete,
|
||||
setShouldDisplayInProgress,
|
||||
SystemState,
|
||||
} from './systemSlice';
|
||||
import { RootState } from '../../app/store';
|
||||
import SDButton from '../../components/SDButton';
|
||||
import { persistor } from '../../main';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { isEqual } from 'lodash';
|
||||
import { cloneElement, ReactElement } from 'react';
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
const { shouldDisplayInProgress, shouldConfirmOnDelete } = system;
|
||||
return { shouldDisplayInProgress, shouldConfirmOnDelete };
|
||||
},
|
||||
{
|
||||
memoizeOptions: { resultEqualityCheck: isEqual },
|
||||
}
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
const { shouldDisplayInProgress, shouldConfirmOnDelete } = system;
|
||||
return { shouldDisplayInProgress, shouldConfirmOnDelete };
|
||||
},
|
||||
{
|
||||
memoizeOptions: { resultEqualityCheck: isEqual },
|
||||
}
|
||||
);
|
||||
|
||||
type Props = {
|
||||
children: ReactElement;
|
||||
type SettingsModalProps = {
|
||||
/* The button to open the Settings Modal */
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
const SettingsModal = ({ children }: Props) => {
|
||||
const {
|
||||
isOpen: isSettingsModalOpen,
|
||||
onOpen: onSettingsModalOpen,
|
||||
onClose: onSettingsModalClose,
|
||||
} = useDisclosure();
|
||||
/**
|
||||
* Modal for app settings. Also provides Reset functionality in which the
|
||||
* app's localstorage is wiped via redux-persist.
|
||||
*
|
||||
* Secondary post-reset modal is included here.
|
||||
*/
|
||||
const SettingsModal = ({ children }: SettingsModalProps) => {
|
||||
const {
|
||||
isOpen: isSettingsModalOpen,
|
||||
onOpen: onSettingsModalOpen,
|
||||
onClose: onSettingsModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: isRefreshModalOpen,
|
||||
onOpen: onRefreshModalOpen,
|
||||
onClose: onRefreshModalClose,
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isRefreshModalOpen,
|
||||
onOpen: onRefreshModalOpen,
|
||||
onClose: onRefreshModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const { shouldDisplayInProgress, shouldConfirmOnDelete } =
|
||||
useAppSelector(systemSelector);
|
||||
const { shouldDisplayInProgress, shouldConfirmOnDelete } =
|
||||
useAppSelector(systemSelector);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClickResetWebUI = () => {
|
||||
persistor.purge().then(() => {
|
||||
onSettingsModalClose();
|
||||
onRefreshModalOpen();
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Resets localstorage, then opens a secondary modal informing user to
|
||||
* refresh their browser.
|
||||
* */
|
||||
const handleClickResetWebUI = () => {
|
||||
persistor.purge().then(() => {
|
||||
onSettingsModalClose();
|
||||
onRefreshModalOpen();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{cloneElement(children, {
|
||||
onClick: onSettingsModalOpen,
|
||||
})}
|
||||
return (
|
||||
<>
|
||||
{cloneElement(children, {
|
||||
onClick: onSettingsModalOpen,
|
||||
})}
|
||||
|
||||
<Modal isOpen={isSettingsModalOpen} onClose={onSettingsModalClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>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>
|
||||
<Modal isOpen={isSettingsModalOpen} onClose={onSettingsModalClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>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>
|
||||
|
||||
<Heading size={'md'}>Reset Web UI</Heading>
|
||||
<Text>
|
||||
Resetting the web UI only resets the browser's
|
||||
local cache of your images and remembered
|
||||
settings. It does not delete any images from
|
||||
disk.
|
||||
</Text>
|
||||
<Text>
|
||||
If images aren't showing up in the gallery or
|
||||
something else isn't working, please try
|
||||
resetting before submitting an issue on GitHub.
|
||||
</Text>
|
||||
<SDButton
|
||||
label='Reset Web UI'
|
||||
colorScheme='red'
|
||||
onClick={handleClickResetWebUI}
|
||||
/>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<Heading size={'md'}>Reset Web UI</Heading>
|
||||
<Text>
|
||||
Resetting the web UI only resets the browser's local cache of
|
||||
your images and remembered settings. It does not delete any
|
||||
images from disk.
|
||||
</Text>
|
||||
<Text>
|
||||
If images aren't showing up in the gallery or something else
|
||||
isn't working, please try resetting before submitting an issue
|
||||
on GitHub.
|
||||
</Text>
|
||||
<Button colorScheme="red" onClick={handleClickResetWebUI}>
|
||||
Reset Web UI
|
||||
</Button>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SDButton
|
||||
label='Close'
|
||||
onClick={onSettingsModalClose}
|
||||
/>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<ModalFooter>
|
||||
<Button onClick={onSettingsModalClose}>Close</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
closeOnOverlayClick={false}
|
||||
isOpen={isRefreshModalOpen}
|
||||
onClose={onRefreshModalClose}
|
||||
isCentered
|
||||
>
|
||||
<ModalOverlay bg='blackAlpha.300' backdropFilter='blur(40px)' />
|
||||
<ModalContent>
|
||||
<ModalBody pb={6} pt={6}>
|
||||
<Flex justifyContent={'center'}>
|
||||
<Text fontSize={'lg'}>
|
||||
Web UI has been reset. Refresh the page to
|
||||
reload.
|
||||
</Text>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
<Modal
|
||||
closeOnOverlayClick={false}
|
||||
isOpen={isRefreshModalOpen}
|
||||
onClose={onRefreshModalClose}
|
||||
isCentered
|
||||
>
|
||||
<ModalOverlay bg="blackAlpha.300" backdropFilter="blur(40px)" />
|
||||
<ModalContent>
|
||||
<ModalBody pb={6} pt={6}>
|
||||
<Flex justifyContent={'center'}>
|
||||
<Text fontSize={'lg'}>
|
||||
Web UI has been reset. Refresh the page to reload.
|
||||
</Text>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsModal;
|
||||
|
@ -8,101 +8,97 @@ import { validateSeedWeights } from '../sd/util/seedWeightPairs';
|
||||
import { SystemState } from './systemSlice';
|
||||
|
||||
const sdSelector = createSelector(
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
prompt: sd.prompt,
|
||||
shouldGenerateVariations: sd.shouldGenerateVariations,
|
||||
seedWeights: sd.seedWeights,
|
||||
maskPath: sd.maskPath,
|
||||
initialImagePath: sd.initialImagePath,
|
||||
seed: sd.seed,
|
||||
};
|
||||
(state: RootState) => state.sd,
|
||||
(sd: SDState) => {
|
||||
return {
|
||||
prompt: sd.prompt,
|
||||
shouldGenerateVariations: sd.shouldGenerateVariations,
|
||||
seedWeights: sd.seedWeights,
|
||||
maskPath: sd.maskPath,
|
||||
initialImagePath: sd.initialImagePath,
|
||||
seed: sd.seed,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const systemSelector = createSelector(
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
};
|
||||
(state: RootState) => state.system,
|
||||
(system: SystemState) => {
|
||||
return {
|
||||
isProcessing: system.isProcessing,
|
||||
isConnected: system.isConnected,
|
||||
};
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
{
|
||||
memoizeOptions: {
|
||||
resultEqualityCheck: isEqual,
|
||||
},
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
Checks relevant pieces of state to confirm generation will not deterministically fail.
|
||||
/**
|
||||
* Checks relevant pieces of state to confirm generation will not deterministically fail.
|
||||
* This is used to prevent the 'Generate' button from being clicked.
|
||||
*/
|
||||
const useCheckParameters = (): boolean => {
|
||||
const {
|
||||
prompt,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
maskPath,
|
||||
initialImagePath,
|
||||
seed,
|
||||
} = useAppSelector(sdSelector);
|
||||
|
||||
This is used to prevent the 'Generate' button from being clicked.
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
|
||||
Other parameter values may cause failure but we rely on input validation for those.
|
||||
*/
|
||||
const useCheckParameters = () => {
|
||||
const {
|
||||
prompt,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
maskPath,
|
||||
initialImagePath,
|
||||
seed,
|
||||
} = useAppSelector(sdSelector);
|
||||
return useMemo(() => {
|
||||
// Cannot generate without a prompt
|
||||
if (!prompt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { isProcessing, isConnected } = useAppSelector(systemSelector);
|
||||
// Cannot generate with a mask without img2img
|
||||
if (maskPath && !initialImagePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return useMemo(() => {
|
||||
// Cannot generate without a prompt
|
||||
if (!prompt) {
|
||||
return false;
|
||||
}
|
||||
// TODO: job queue
|
||||
// Cannot generate if already processing an image
|
||||
if (isProcessing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate with a mask without img2img
|
||||
if (maskPath && !initialImagePath) {
|
||||
return false;
|
||||
}
|
||||
// Cannot generate if not connected
|
||||
if (!isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: job queue
|
||||
// Cannot generate if already processing an image
|
||||
if (isProcessing) {
|
||||
return false;
|
||||
}
|
||||
// Cannot generate variations without valid seed weights
|
||||
if (
|
||||
shouldGenerateVariations &&
|
||||
(!(validateSeedWeights(seedWeights) || seedWeights === '') || seed === -1)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate if not connected
|
||||
if (!isConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot generate variations without valid seed weights
|
||||
if (
|
||||
shouldGenerateVariations &&
|
||||
(!(validateSeedWeights(seedWeights) || seedWeights === '') ||
|
||||
seed === -1)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All good
|
||||
return true;
|
||||
}, [
|
||||
prompt,
|
||||
maskPath,
|
||||
initialImagePath,
|
||||
isProcessing,
|
||||
isConnected,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
seed,
|
||||
]);
|
||||
// All good
|
||||
return true;
|
||||
}, [
|
||||
prompt,
|
||||
maskPath,
|
||||
initialImagePath,
|
||||
isProcessing,
|
||||
isConnected,
|
||||
shouldGenerateVariations,
|
||||
seedWeights,
|
||||
seed,
|
||||
]);
|
||||
};
|
||||
|
||||
export default useCheckParameters;
|
||||
|
Loading…
Reference in New Issue
Block a user