Merge pull request #191 from caranicas/beta-react

Beta react
This commit is contained in:
cmdr2 2022-09-21 23:18:00 +05:30 committed by GitHub
commit 2590dc690e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 12880 additions and 3 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

6
.gitignore vendored
View File

@ -2,4 +2,10 @@ __pycache__
installer
installer.tar
dist
# built code for the front end
!/ui/frontend/dist
ui/frontend/.idea/*
ui/frontend/build_src/.idea/*
.idea/*

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,251 @@
[
[
"Drawing Style",
[
"Cel Shading",
"Children's Drawing",
"Crosshatch",
"Detailed and Intricate",
"Doodle",
"Dot Art",
"Line Art",
"Sketch"
]
],
[
"Visual Style",
[
"2D",
"8-bit",
"16-bit",
"Anaglyph",
"Anime",
"Art Nouveau",
"Bauhaus",
"Baroque",
"CGI",
"Cartoon",
"Comic Book",
"Concept Art",
"Constructivist",
"Cubist",
"Digital Art",
"Dadaist",
"Expressionist",
"Fantasy",
"Fauvist",
"Figurative",
"Graphic Novel",
"Geometric",
"Hard Edge Painting",
"Hydrodipped",
"Impressionistic",
"Lithography",
"Manga",
"Minimalist",
"Modern Art",
"Mosaic",
"Mural",
"Naive",
"Neoclassical",
"Photo",
"Realistic",
"Rococo",
"Romantic",
"Street Art",
"Symbolist",
"Stuckist",
"Surrealist",
"Visual Novel",
"Watercolor"
]
],
[
"Pen",
["Chalk", "Colored Pencil", "Graphite", "Ink", "Oil Paint", "Pastel Art"]
],
[
"Carving and Etching",
[
"Etching",
"Linocut",
"Paper Model",
"Paper-Mache",
"Papercutting",
"Pyrography",
"Wood-Carving"
]
],
[
"Camera",
[
"Aerial View",
"Canon50",
"Cinematic",
"Close-up",
"Color Grading",
"Dramatic",
"Film Grain",
"Fisheye Lens",
"Glamor Shot",
"Golden Hour",
"HD",
"Landscape",
"Lens Flare",
"Macro",
"Polaroid",
"Photoshoot",
"Portrait",
"Studio Lighting",
"Vintage",
"War Photography",
"White Balance",
"Wildlife Photography"
]
],
[
"Color",
[
"Beautiful Lighting",
"Cold Color Palette",
"Colorful",
"Dynamic Lighting",
"Electric Colors",
"Infrared",
"Pastel",
"Neon",
"Synthwave",
"Warm Color Palette"
]
],
[
"Emotions",
[
"Angry",
"Bitter",
"Disgusted",
"Embarrassed",
"Evil",
"Excited",
"Fear",
"Funny",
"Happy",
"Horrifying",
"Lonely",
"Sad",
"Serene",
"Surprised",
"Melancholic"
]
],
[
"Style of an artist or community",
[
"Artstation",
"trending on Artstation",
"by Agnes Lawrence Pelton",
"by Akihito Yoshida",
"by Alex Grey",
"by Alexander Jansson",
"by Alphonse Mucha",
"by Andy Warhol",
"by Artgerm",
"by Asaf Hanuka",
"by Aubrey Beardsley",
"by Banksy",
"by Beeple",
"by Ben Enwonwu",
"by Bob Eggleton",
"by Caravaggio Michelangelo Merisi",
"by Caspar David Friedrich",
"by Chris Foss",
"by Claude Monet",
"by Dan Mumford",
"by David Mann",
"by Diego Velázquez",
"by Disney Animation Studios",
"by Édouard Manet",
"by Esao Andrews",
"by Frida Kahlo",
"by Gediminas Pranckevicius",
"by Georgia O'Keeffe",
"by Greg Rutkowski",
"by Gustave Doré",
"by Gustave Klimt",
"by H.R. Giger",
"by Hayao Miyazaki",
"by Henri Matisse",
"by HP Lovecraft",
"by Ivan Shishkin",
"by Jack Kirby",
"by Jackson Pollock",
"by James Jean",
"by Jim Burns",
"by Johannes Vermeer",
"by John William Waterhouse",
"by Katsushika Hokusai",
"by Kim Tschang Yeul",
"by Ko Young Hoon",
"by Leonardo da Vinci",
"by Lisa Frank",
"by M.C. Escher",
"by Mahmoud Saïd",
"by Makoto Shinkai",
"by Marc Simonetti",
"by Mark Brooks",
"by Michelangelo",
"by Pablo Picasso",
"by Paul Klee",
"by Peter Mohrbacher",
"by Pierre-Auguste Renoir",
"by Pixar Animation Studios",
"by Rembrandt",
"by Richard Dadd",
"by Rossdraws",
"by Salvador Dalí",
"by Sam Does Arts",
"by Sandro Botticelli",
"by Ted Nasmith",
"by Ten Hundred",
"by Thomas Kinkade",
"by Tivadar Csontváry Kosztka",
"by Victo Ngai",
"by Vincent Di Fate",
"by Vincent van Gogh",
"by Wes Anderson",
"by wlop",
"by Yoshitaka Amano"
]
],
[
"CGI Software",
[
"3D Model",
"3D Sculpt",
"3Ds Max Model",
"Blender Model",
"Cinema4d Model",
"Maya Model",
"Unreal Engine",
"Zbrush Sculpt"
]
],
[
"CGI Rendering",
[
"3D Render",
"Corona Render",
"Creature Design",
"Cycles Render",
"Detailed Render",
"Environment Design",
"Intricate Environment",
"LSD Render",
"Octane Render",
"PBR",
"Glass Caustics",
"Global Illumination",
"Subsurface Scattering"
]
]
]

View File

@ -0,0 +1,87 @@
const path = require("path");
module.exports = {
env: {
browser: true,
es2021: true,
},
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
tsconfigRootDir: __dirname,
},
plugins: ["react"],
extends: [
"prettier",
"plugin:react/recommended",
"standard-with-typescript",
"plugin:i18next/recommended",
"plugin:i18n-json/recommended",
],
settings: {
react: {
version: "detect",
},
},
rules: {
// general things turned off for now
"no-debugger": "warn",
"eol-last": "off",
"comma-dangle": ["off", "always-multiline"],
"no-void": ["off"],
"array-callback-return": ["off"],
"spaced-comment": ["off"],
"padded-blocks": ["off"],
"no-multiple-empty-lines": ["off", { max: 2, maxEOF: 1 }],
quotes: ["off", "double"],
semi: ["off", "always"],
yoda: ["off"],
eqeqeq: ["off"],
"react/display-name": "warn",
// TS THINGS WE DONT WANT
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/ban-ts-comment": "off",
// these are things that fight with prettier
"@typescript-eslint/comma-dangle": "off",
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/quotes": "off",
"@typescript-eslint/semi": "off",
"@typescript-eslint/brace-style": "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/member-delimiter-style": "off",
// TS WARNINGS WE WANT
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-non-null-assertion": "warn",
// i18n stuff no string literal works but turned off for now
"i18next/no-literal-string": "off",
// still need to figure out how to get this to work
// it should error if we dont haev all the keys in the translation file
"i18n-json/identical-keys": [
"error",
{
filePath: {
"home.json/": path.resolve("./Translation/locales/en/home.json"),
},
},
],
},
overrides: [
{
files: ["*.ts", "*.tsx"],
parserOptions: {
project: ["./tsconfig.json"], // Specify it only for TypeScript files
},
},
],
// eslint-disable-next-line semi
};

18
ui/frontend/build_src/.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# local ignores - We could move these to the global ignores,
# but I think it makes sense to keep them here
# env
*.local
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# installed dependencies
node_modules

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>Stable Diffusion UI</title>
</head>
<body>
<!-- The react app entry point. Currently no ui just poc importing and logging -->
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

8195
ui/frontend/build_src/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
{
"name": "react-ts",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"pretty": "prettier --write .",
"dev": "vite",
"build": "tsc && vite build --emptyOutDir",
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-location": "^3.7.4",
"@tanstack/react-query": "^4.2.3",
"@tanstack/react-query-devtools": "^4.2.3",
"@vanilla-extract/css": "^1.9.0",
"@vanilla-extract/recipes": "^0.2.5",
"@vanilla-extract/vite-plugin": "^3.5.0",
"i18next": "^21.9.2",
"immer": "^9.0.15",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^11.18.6",
"uuid": "^9.0.0",
"zustand": "^4.1.1"
},
"devDependencies": {
"@types/node": "^18.7.18",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.37.0",
"@typescript-eslint/parser": "^5.37.0",
"@vitejs/plugin-react": "^2.0.1",
"eslint": "^8.23.1",
"eslint-config-prettier": "^8.5.0",
"eslint-config-standard-with-typescript": "^23.0.0",
"eslint-plugin-i18n-json": "^4.0.0",
"eslint-plugin-i18next": "^6.0.0-4",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.2.5",
"eslint-plugin-promise": "^6.0.1",
"eslint-plugin-react": "^7.31.8",
"prettier": "^2.7.1",
"typescript": "^4.8.3",
"vite": "^3.0.7",
"vite-plugin-eslint": "^1.8.1"
},
"overrides": {
"@vanilla-extract/vite-plugin": {
"vite": "^3"
}
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
singleQuote: true,
tabWidth: 2,
semi: true,
trailingComma: "es5",
endOfLine: "lf",
};

View File

@ -0,0 +1,28 @@
import React, { useState } from "react";
import { ReactLocation, Router } from "@tanstack/react-location";
import Home from "./pages/Home";
import Settings from "./pages/Settings";
// @ts-expect-error
import { darkTheme, lightTheme } from "./styles/theme/index.css.ts";
import "./Translation/config";
const location = new ReactLocation();
function App() {
// just check for the theme one 1 time
// var { matches } = window.matchMedia('(prefers-color-scheme: dark)')
const matches = true;
const themeClass = matches ? darkTheme : lightTheme;
return (
<Router
location={location}
routes={[
{ path: "/", element: <Home className={themeClass} /> },
{ path: "/settings", element: <Settings className={themeClass} /> },
]}
></Router>
);
}
export default App;

View File

@ -0,0 +1,32 @@
import i18n from "i18next";
// this should be updated to an interface
import ENTranslation from "./locales/en/home.json";
import ESTranslation from "./locales/es/home.json";
import { initReactI18next } from "react-i18next";
export const resources = {
en: {
translation: ENTranslation,
},
es: {
translation: ESTranslation,
},
} as const;
i18n
.use(initReactI18next)
.init({
lng: "en",
interpolation: {
escapeValue: false,
},
resources,
})
.then(() => {
console.log("i18n initialized");
})
.catch((err) => {
console.error("i18n initialization failed", err);
})
.finally(() => {
console.log("i18n initialization finished");
});

View File

@ -0,0 +1,108 @@
{
"title": "Stable Diffusion UI",
"description": "",
"navbar": {
"home": "Home",
"history": "History",
"community": "Community",
"settings": "Settings"
},
"land-cre": {
"cp": "Create Profile",
"cp-place": "Profile name",
"pp": "Profile Picture",
"pp-disc": "",
"ast": "Automatically save to",
"ast-disc": "File path to auto save your creations",
"place": "File path",
"cre": "Create"
},
"land-pre": {
"user": "Username",
"add": "Add Profile"
},
"home": {
"status-starting": "Stable Diffusion is starting...",
"status-ready": "Stable Diffusion is ready to use!",
"status-error": "Stable Diffusion is not running!",
"editor-title": "Prompt",
"initial-img-txt": "Initial Image: (optional)",
"initial-img-btn": "Browse...",
"initial-img-text2": "No file selected.",
"make-img-btn": "Make Image",
"make-img-btn-stop": "Stop"
},
"in-paint": {
"txt": "In-Painting (select the area which the AI will paint into)",
"clear": "Clear"
},
"settings": {
"base-img": "Use base image:",
"seed": "Seed:",
"amount-of-img": "Amount of images to make:",
"how-many": "How many at once:",
"width": "Width:",
"height": "Height:",
"steps": "Number of inference steps:",
"guide-scale": "Guidance Scale:",
"prompt-str": "Prompt Strength:",
"live-preview": "Show a live preview of the image (disable this for faster image generation)",
"fix-face": "Fix incorrect faces and eyes (uses GFPGAN)",
"ups": "Upscale the image to 4x resolution using:",
"no-ups": "No Upscaling",
"corrected": "Show only the corrected/upscaled image"
},
"tags": {
"txt": "Image Modifiers (art styles, tags etc)"
},
"preview-prompt": {
"part1": "Type a prompt and press the \"Make Image\" button.",
"part2": "You can set an \"Initial Image\" if you want to guide the AI.\n",
"part3": "You can also add modifiers like \"Realistic\", \"Pencil Sketch\", \"ArtStation\" etc by browsing through the \"Image Modifiers\" section and selecting the desired modifiers.\n",
"part4": "Click \"Advanced Settings\" for additional settings like seed, image size, number of images to generate etc.",
"part5": "Enjoy! :)"
},
"current-task": "Current task",
"recent-create": "Recently Created",
"popup": {
"use-btn": "Use Image",
"use-btn2": "Use Image and Tags"
},
"history": {
"fave": "Favorites Only",
"search": "Search"
},
"advanced-settings": {
"sound": "Play sound on task completion",
"sound-disc": "Will play a sound so user can hear when image is done.",
"turbo": "Turbo mode",
"turbo-disc": "Generates images faster, but uses an additional 1 GB of GPU memory",
"cpu": "Use CPU instead of GPU",
"cpu-disc": "Warning: this will be *very* slow",
"gpu": "Use full precision",
"gpu-disc": "(for GPU-only. warning: this will consume more VRAM)",
"beta": "Beta Features",
"beta-disc": "Get the latest features immediately (but could be less stable). \nPlease restart the program after changing this.",
"save": "SAVE"
},
"storage": {
"ast": "Automatically save to",
"ast-disc": "File path to auto save your creations",
"place": "File path",
"cps": "Cross profile sharing",
"cps-disc": "Profiles will see suggestions from each other.",
"acb": "Allow cloud backup",
"acb-disc": "A button will show up for images on hover",
"acb-place": "Choose your",
"acc-api": "Api key",
"acb-api-place": "Your API key",
"save": "SAVE"
},
"import": {
"imp-btn": "IMPORT",
"exp-btn": "EXPORT",
"disc": "It is a good idea to leave the exported file as it is. Otherwise it may not import correctly",
"disc:2": "When importing, only profiles that are not already present on the will be added."
},
"about": "If you found this project useful and want to help keep it alive, please to help cover the cost of development and maintenance! Thank you for your support!\n\nPlease feel free to join the discord community or file an issue if you have any problems or suggestions in using this interface.\n\nDisclaimer: The authors of this project are not responsible for any content generated using this interface.\n\nThis license of this software forbids you from sharing any content that violates any laws, produce any harm to a person, disseminate any personal information that would be meant for harm,\nspread misinformation and target vulnerable groups. For the full list of restrictions please read the license.\n\nBy using this software, you consent to the terms and conditions of the license.\n"
}

View File

@ -0,0 +1,108 @@
{
"title": "Stable Diffusion UI",
"description": "",
"navbar": {
"home": "Home",
"history": "History",
"community": "Community",
"settings": "Settings"
},
"land-cre": {
"cp": "Create Profile",
"cp-place": "Profile name",
"pp": "Profile Picture",
"pp-disc": "",
"ast": "Automatically save to",
"ast-disc": "File path to auto save your creations",
"place": "File path",
"cre": "Create"
},
"land-pre": {
"user": "Username",
"add": "Add Profile"
},
"home": {
"status-starting": "Stable Diffusion is starting...",
"status-ready": "Stable Diffusion is ready to use!",
"status-error": "Stable Diffusion is not running!",
"editor-title": "Prompt",
"initial-img-txt": "Initial Image: (optional)",
"initial-img-btn": "Browse...",
"initial-img-text2": "No file selected.",
"make-img-btn": "Make Image",
"make-img-btn-stop": "Stop"
},
"in-paint": {
"txt": "In-Painting (select the area which the AI will paint into)",
"clear": "Clear"
},
"settings": {
"base-img": "Use base image:",
"seed": "Seed:",
"amount-of-img": "Amount of images to make:",
"how-many": "How many at once:",
"width": "Width:",
"height": "Height:",
"steps": "Number of inference steps:",
"guide-scale": "Guidance Scale:",
"prompt-str": "Prompt Strength:",
"live-preview": "Show a live preview of the image (disable this for faster image generation)",
"fix-face": "Fix incorrect faces and eyes (uses GFPGAN)",
"ups": "Upscale the image to 4x resolution using:",
"no-ups": "No Upscaling",
"corrected": "Show only the corrected/upscaled image"
},
"tags": {
"txt": "Image Modifiers (art styles, tags etc)"
},
"preview-prompt": {
"part1": "Type a prompt and press the \"Make Image\" button.",
"part2": "You can set an \"Initial Image\" if you want to guide the AI.\n",
"part3": "You can also add modifiers like \"Realistic\", \"Pencil Sketch\", \"ArtStation\" etc by browsing through the \"Image Modifiers\" section and selecting the desired modifiers.\n",
"part4": "Click \"Advanced Settings\" for additional settings like seed, image size, number of images to generate etc.",
"part5": "Enjoy! :)"
},
"current-task": "Current task",
"recent-create": "Recently Created",
"popup": {
"use-btn": "Use Image",
"use-btn2": "Use Image and Tags"
},
"history": {
"fave": "Favorites Only",
"search": "Search"
},
"advanced-settings": {
"sound": "Play sound on task completion",
"sound-disc": "Will play a sound so user can hear when image is done.",
"turbo": "Turbo mode",
"turbo-disc": "Generates images faster, but uses an additional 1 GB of GPU memory",
"cpu": "Use CPU instead of GPU",
"cpu-disc": "Warning: this will be *very* slow",
"gpu": "Use full precision",
"gpu-disc": "(for GPU-only. warning: this will consume more VRAM)",
"beta": "Beta Features",
"beta-disc": "Get the latest features immediately (but could be less stable). \nPlease restart the program after changing this.",
"save": "SAVE"
},
"storage": {
"ast": "Automatically save to",
"ast-disc": "File path to auto save your creations",
"place": "File path",
"cps": "Cross profile sharing",
"cps-disc": "Profiles will see suggestions from each other.",
"acb": "Allow cloud backup",
"acb-disc": "A button will show up for images on hover",
"acb-place": "Choose your",
"acc-api": "Api key",
"acb-api-place": "Your API key",
"save": "SAVE"
},
"import": {
"imp-btn": "IMPORT",
"exp-btn": "EXPORT",
"disc": "It is a good idea to leave the exported file as it is. Otherwise it may not import correctly",
"disc:2": "When importing, only profiles that are not already present on the will be added."
},
"about": "If you found this project useful and want to help keep it alive, please to help cover the cost of development and maintenance! Thank you for your support!\n\nPlease feel free to join the discord community or file an issue if you have any problems or suggestions in using this interface.\n\nDisclaimer: The authors of this project are not responsible for any content generated using this interface.\n\nThis license of this software forbids you from sharing any content that violates any laws, produce any harm to a person, disseminate any personal information that would be meant for harm,\nspread misinformation and target vulnerable groups. For the full list of restrictions please read the license.\n\nBy using this software, you consent to the terms and conditions of the license.\n"
}

View File

@ -0,0 +1,85 @@
/**
* basic server health
*/
import type { ImageRequest } from "../stores/imageCreateStore";
// when we are on dev we want to specifiy 9000 as the port for the backend
// when we are on prod we want be realtive to the current url
export const API_URL = import.meta.env.DEV ? "http://localhost:9000" : "";
export const HEALTH_PING_INTERVAL = 5000; // 5 seconds
export const healthPing = async () => {
const pingUrl = `${API_URL}/ping`;
const response = await fetch(pingUrl);
const data = await response.json();
return data;
};
/**
* the local list of modifications
*/
export const loadModifications = async () => {
const response = await fetch(`${API_URL}/modifiers.json`);
const data = await response.json();
return data;
};
export const getSaveDirectory = async () => {
const response = await fetch(`${API_URL}/output_dir`);
const data = await response.json();
return data[0];
};
export const KEY_CONFIG = "config";
export const getConfig = async () => {
const response = await fetch(`${API_URL}/app_config`);
const data = await response.json();
return data;
};
export const KEY_TOGGLE_CONFIG = "toggle_config";
export const toggleBetaConfig = async (branch: string) => {
const response = await fetch(`${API_URL}/app_config`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
update_branch: branch,
}),
});
const data = await response.json();
return data;
};
/**
* post a new request for an image
*/
// TODO; put hese some place better
export interface ImageOutput {
data: string;
path_abs: string | null;
seed: number;
}
export interface ImageReturnType {
output: ImageOutput[];
request: {};
status: string;
session_id: string;
}
export const MakeImageKey = "MakeImage";
export const doMakeImage = async (reqBody: ImageRequest) => {
const res = await fetch(`${API_URL}/image`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(reqBody),
});
const data = await res.json();
return data;
};

View File

@ -0,0 +1,23 @@
import React from "react";
import { useImageCreate } from "../../../stores/imageCreateStore";
interface ModifierTagProps {
name: string;
}
export default function ModifierTag({ name }: ModifierTagProps) {
const hasTag = useImageCreate((state) => state.hasTag(name))
? "selected"
: "";
const toggleTag = useImageCreate((state) => state.toggleTag);
const _toggleTag = () => {
toggleTag(name);
};
return (
<div className={"modifierTag " + hasTag} onClick={_toggleTag}>
<p>{name}</p>
</div>
);
}

View File

@ -0,0 +1,73 @@
import React, { useState, useEffect } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
KEY_CONFIG,
getConfig,
KEY_TOGGLE_CONFIG,
toggleBetaConfig,
} from "../../../api";
import { useTranslation } from "react-i18next";
export default function BetaMode() {
const { t } = useTranslation();
// gate for the toggle
const [shouldSetCofig, setShouldSetConfig] = useState(false);
// next branch to get
const [branchToGetNext, setBranchToGetNext] = useState("beta");
// our basic config
const { status: configStatus, data: configData } = useQuery(
[KEY_CONFIG],
getConfig
);
const queryClient = useQueryClient();
// the toggle config
const { status: toggleStatus, data: toggleData } = useQuery(
[KEY_TOGGLE_CONFIG],
async () => await toggleBetaConfig(branchToGetNext),
{
enabled: shouldSetCofig,
}
);
// this is also in the Header Display
// TODO: make this a custom hook
useEffect(() => {
if (configStatus === "success") {
const { update_branch: updateBranch } = configData;
if (updateBranch === "main") {
setBranchToGetNext("beta");
} else {
// setIsBeta(true);
setBranchToGetNext("main");
}
}
}, [configStatus, configData]);
useEffect(() => {
if (toggleStatus === "success") {
if (toggleData[0] === "OK") {
// force a refetch of the config
queryClient.invalidateQueries([KEY_CONFIG]);
}
setShouldSetConfig(false);
}
}, [toggleStatus, toggleData, setShouldSetConfig]);
return (
<label>
<input
type="checkbox"
checked={branchToGetNext === "main"}
onChange={(e) => {
setShouldSetConfig(true);
}}
/>
{t("advanced-settings.beta")} {t("advanced-settings.beta-disc")}
</label>
);
}

View File

@ -0,0 +1,22 @@
import { style, globalStyle } from "@vanilla-extract/css";
export const DrawImageMain = style({
position: "relative",
});
globalStyle(`${DrawImageMain} > canvas`, {
position: "absolute",
top: "0",
left: "0",
width: "100%",
height: "100%",
});
globalStyle(`${DrawImageMain} > canvas:first-of-type`, {
opacity: ".7",
});
globalStyle(`${DrawImageMain} > img`, {
top: "0",
left: "0",
});

View File

@ -0,0 +1,180 @@
// @ts-nocheck
import React, { useRef, useState, useEffect } from "react";
import {
DrawImageMain, // @ts-expect-error
} from "./drawImage.css.ts";
// https://github.com/embiem/react-canvas-draw
interface DrawImageProps {
imageData: string;
brushSize: string;
brushShape: string;
brushColor: string;
isErasing: boolean;
}
export default function DrawImage({
imageData,
brushSize,
brushShape,
brushColor,
isErasing,
}: DrawImageProps) {
const drawingRef = useRef<HTMLCanvasElement>(null);
const cursorRef = useRef<HTMLCanvasElement>(null);
const [isUpdating, setIsUpdating] = useState(false);
const [canvasWidth, setCanvasWidth] = useState(512);
const [canvasHeight, setCanvasHeight] = useState(512);
useEffect(() => {
const img = new Image();
img.onload = () => {
setCanvasWidth(img.width);
setCanvasHeight(img.height);
};
img.src = imageData;
}, [imageData]);
useEffect(() => {
// when the brush color changes, change the color of all the
// drawn pixels to the new color
if (drawingRef.current != null) {
const ctx = drawingRef.current.getContext("2d");
const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] > 0) {
data[i] = parseInt(brushColor, 16);
data[i + 1] = parseInt(brushColor, 16);
data[i + 2] = parseInt(brushColor, 16);
}
}
ctx.putImageData(imageData, 0, 0);
}
}, [brushColor]);
const _handleMouseDown = (
e: React.MouseEvent<HTMLCanvasElement, MouseEvent>
) => {
const {
nativeEvent: { offsetX, offsetY },
} = e;
setIsUpdating(true);
};
const _handleMouseUp = (
e: React.MouseEvent<HTMLCanvasElement, MouseEvent>
) => {
setIsUpdating(false);
const canvas = drawingRef.current;
if (canvas != null) {
const data = canvas.toDataURL();
// TODO: SEND THIS TO THE STATE
}
};
const _drawCanvas = (x, y, brushSize, brushShape, brushColor) => {
const canvas = drawingRef.current;
if (canvas != null) {
const ctx = canvas.getContext("2d");
if (isErasing) {
// stack overflow https://stackoverflow.com/questions/10396991/clearing-circular-regions-from-html5-canvas
const offset = brushSize / 2;
ctx.clearRect(x - offset, y - offset, brushSize, brushSize);
} else {
ctx.beginPath();
ctx.lineWidth = brushSize;
ctx.lineCap = brushShape;
ctx.strokeStyle = brushColor;
ctx.moveTo(x, y);
ctx.lineTo(x, y);
ctx.stroke();
}
}
};
const _drawCursor = (
x: number,
y: number,
brushSize: number,
brushShape: string,
brushColor: string
) => {
const canvas = cursorRef.current;
if (canvas != null) {
const ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (isErasing) {
const offset = brushSize / 2;
// draw a quare
ctx.lineWidth = 2;
ctx.lineCap = "butt";
ctx.strokeStyle = brushColor;
ctx.moveTo(x - offset, y - offset);
ctx.lineTo(x + offset, y - offset);
ctx.lineTo(x + offset, y + offset);
ctx.lineTo(x - offset, y + offset);
ctx.lineTo(x - offset, y - offset);
ctx.stroke();
} else {
ctx.lineWidth = brushSize;
ctx.lineCap = brushShape;
ctx.strokeStyle = brushColor;
ctx.moveTo(x, y);
ctx.lineTo(x, y);
ctx.stroke();
}
}
};
const _handleMouseMove = (
e: React.MouseEvent<HTMLCanvasElement, MouseEvent>
) => {
const {
nativeEvent: { offsetX: x, offsetY: y },
} = e;
_drawCursor(x, y, brushSize, brushShape, brushColor);
if (isUpdating) {
_drawCanvas(x, y, brushSize, brushShape, brushColor);
}
};
// function for external use
const fillCanvas = () => {
const canvas = drawingRef.current;
if (canvas != null) {
const ctx = canvas.getContext("2d");
ctx.fillStyle = brushColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
};
return (
<div className={DrawImageMain}>
<img src={imageData} />
<canvas
ref={drawingRef}
width={canvasWidth}
height={canvasHeight}
></canvas>
<canvas
ref={cursorRef}
width={canvasWidth}
height={canvasHeight}
onMouseDown={_handleMouseDown}
onMouseUp={_handleMouseUp}
onMouseMove={_handleMouseMove}
></canvas>
</div>
);
}

View File

@ -0,0 +1,20 @@
import { style } from "@vanilla-extract/css";
export const generatedImageMain = style({
position: "relative",
});
// export const imageContain = style({
// width: "512px",
// height: "512px",
// backgroundColor: "black",
// display: "flex",
// justifyContent: "center",
// alignItems: "center",
// });
export const image = style({
// width: "512px",
// height: "512px",
// objectFit: "contain",
});

View File

@ -0,0 +1,27 @@
import React from "react";
import { ImageRequest } from "../../../stores/imageCreateStore";
import {
generatedImageMain,
image, // @ts-expect-error
} from "./generatedImage.css.ts";
interface GeneretaedImageProps {
imageData: string | undefined;
metadata: ImageRequest | undefined;
className?: string;
// children: never[];
}
export default function GeneratedImage({
imageData,
metadata,
className,
}: GeneretaedImageProps) {
return (
<div className={[generatedImageMain, className].join(" ")}>
<img className={image} src={imageData} alt={metadata!.prompt} />
</div>
);
}

View File

@ -0,0 +1,32 @@
import { style, globalStyle } from "@vanilla-extract/css";
// @ts-expect-error
import { vars } from "../../../../styles/theme/index.css.ts";
// import { PanelBox } from "../../../../styles/shared.css.ts";
export const AdvancedSettingsList = style({
// marginBottom: vars.spacing.small,
paddingLeft: 0,
listStyleType: "none",
});
export const AdvancedSettingGrouping = style({
marginTop: vars.spacing.medium,
});
export const MenuButton = style({
display: "block",
width: "100%",
textAlign: "left",
backgroundColor: "transparent",
color: vars.colors.text.normal,
border: "0 none",
cursor: "pointer",
padding: "0",
marginBottom: vars.spacing.medium,
});
globalStyle(`${MenuButton}> h4`, {
color: "#e7ba71",
});

View File

@ -0,0 +1,74 @@
import React from "react";
import { useImageCreate } from "../../../../../stores/imageCreateStore";
import { useCreateUI } from "../../creationPanelUIStore";
import {
SettingItem, // @ts-expect-error
} from "../../../../../styles/shared.css.ts";
import {
MenuButton, // @ts-expect-error
} from "../advancedsettings.css.ts";
import { useTranslation } from "react-i18next";
export default function GpuSettings() {
const { t } = useTranslation();
const turbo = useImageCreate((state) => state.getValueForRequestKey("turbo"));
const useCpu = useImageCreate((state) =>
state.getValueForRequestKey("use_cpu")
);
const useFullPrecision = useImageCreate((state) =>
state.getValueForRequestKey("use_full_precision")
);
const setRequestOption = useImageCreate((state) => state.setRequestOptions);
const gpuOpen = useCreateUI((state) => state.isOpenAdvGPUSettings);
const toggleGpuOpen = useCreateUI((state) => state.toggleAdvGPUSettings);
return (
<div>
<button type="button" className={MenuButton} onClick={toggleGpuOpen}>
<h4>GPU Settings</h4>
</button>
{gpuOpen && (
<>
<div className={SettingItem}>
<label>
<input
checked={turbo}
onChange={(e) => setRequestOption("turbo", e.target.checked)}
type="checkbox"
/>
{t("advanced-settings.turbo")} {t("advanced-settings.turbo-disc")}
</label>
</div>
<div className={SettingItem}>
<label>
<input
type="checkbox"
checked={useCpu}
onChange={(e) => setRequestOption("use_cpu", e.target.checked)}
/>
{t("advanced-settings.cpu")} {t("advanced-settings.cpu-disc")}
</label>
</div>
<div className={SettingItem}>
<label>
<input
checked={useFullPrecision}
onChange={(e) =>
setRequestOption("use_full_precision", e.target.checked)
}
type="checkbox"
/>
{t("advanced-settings.gpu")} {t("advanced-settings.gpu-disc")}
</label>
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,115 @@
import React, { useEffect, useState } from "react";
import { useImageCreate } from "../../../../../stores/imageCreateStore";
import { useCreateUI } from "../../creationPanelUIStore";
import {
SettingItem, // @ts-expect-error
} from "../../../../../styles/shared.css.ts";
import {
MenuButton, // @ts-expect-error
} from "../advancedsettings.css.ts";
import { useTranslation } from "react-i18next";
export default function ImprovementSettings() {
const { t } = useTranslation();
// these are conditionals that should be retired and inferred from the store
const isUsingFaceCorrection = useImageCreate((state) =>
state.isUsingFaceCorrection()
);
const isUsingUpscaling = useImageCreate((state) => state.isUsingUpscaling());
const useUpscale = useImageCreate((state) =>
state.getValueForRequestKey("use_upscale")
);
const filteredOnly = useImageCreate((state) =>
state.getValueForRequestKey("show_only_filtered_image")
);
const toggleUseFaceCorrection = useImageCreate(
(state) => state.toggleUseFaceCorrection
);
const setRequestOption = useImageCreate((state) => state.setRequestOptions);
const improvementOpen = useCreateUI(
(state) => state.isOpenAdvImprovementSettings
);
const toggleImprovementOpen = useCreateUI(
(state) => state.toggleAdvImprovementSettings
);
const [isFilteringDisabled, setIsFilteringDisabled] = useState(false);
// should probably be a store selector
useEffect(() => {
// if either are true we arent disabled
if (isUsingFaceCorrection || useUpscale != "") {
setIsFilteringDisabled(false);
} else {
setIsFilteringDisabled(true);
}
}, [isUsingFaceCorrection, isUsingUpscaling, setIsFilteringDisabled]);
return (
<div>
<button
type="button"
className={MenuButton}
onClick={toggleImprovementOpen}
>
<h4>Improvement Settings</h4>
</button>
{improvementOpen && (
<>
<div className={SettingItem}>
<label>
<input
type="checkbox"
checked={isUsingFaceCorrection}
onChange={(e) => toggleUseFaceCorrection()}
/>
Fix incorrect faces and eyes (uses GFPGAN)
</label>
</div>
<div className={SettingItem}>
<label>
{t("settings.ups")}
<select
id="upscale_model"
name="upscale_model"
value={useUpscale}
onChange={(e) => {
setRequestOption("use_upscale", e.target.value);
}}
>
<option value="">{t("settings.no-ups")}</option>
<option value="RealESRGAN_x4plus">RealESRGAN_x4plus</option>
<option value="RealESRGAN_x4plus_anime_6B">
RealESRGAN_x4plus_anime_6B
</option>
</select>
</label>
</div>
<div className={SettingItem}>
<label>
<input
disabled={isFilteringDisabled}
type="checkbox"
checked={filteredOnly}
onChange={(e) =>
setRequestOption("show_only_filtered_image", e.target.checked)
}
/>
{t("settings.corrected")}
</label>
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,63 @@
import React, { useEffect } from "react";
import { useCreateUI } from "../creationPanelUIStore";
// @ts-expect-error
import { PanelBox } from "../../../../styles/shared.css.ts";
import {
AdvancedSettingsList,
AdvancedSettingGrouping, // @ts-expect-error
} from "./advancedsettings.css.ts";
import ImprovementSettings from "./improvementSettings";
import PropertySettings from "./propertySettings";
import WorkflowSettings from "./workflowSettings";
import GpuSettings from "./gpuSettings";
// import BetaMode from "../../../molecules/betaMode";
function SettingsList() {
return (
<ul className={AdvancedSettingsList}>
<li className={AdvancedSettingGrouping}>
<ImprovementSettings />
</li>
<li className={AdvancedSettingGrouping}>
<PropertySettings />
</li>
<li className={AdvancedSettingGrouping}>
<WorkflowSettings />
</li>
<li className={AdvancedSettingGrouping}>
<GpuSettings />
</li>
{/* <li className={AdvancedSettingGrouping}>
<BetaMode />
</li> */}
</ul>
);
}
export default function AdvancedSettings() {
const advancedSettingsIsOpen = useCreateUI(
(state) => state.isOpenAdvancedSettings
);
const toggleAdvancedSettingsIsOpen = useCreateUI(
(state) => state.toggleAdvancedSettings
);
return (
<div className={PanelBox}>
<button
type="button"
onClick={toggleAdvancedSettingsIsOpen}
className="panel-box-toggle-btn"
>
<h3>Advanced Settings</h3>
</button>
{advancedSettingsIsOpen && <SettingsList />}
</div>
);
}

View File

@ -0,0 +1,185 @@
import React, { useState } from "react";
import { useImageCreate } from "../../../../../stores/imageCreateStore";
import { useCreateUI } from "../../creationPanelUIStore";
import {
SettingItem, // @ts-expect-error
} from "../../../../../styles/shared.css.ts";
import {
MenuButton, // @ts-expect-error
} from "../advancedsettings.css.ts";
import { useTranslation } from "react-i18next";
// todo: move this someplace more global
const IMAGE_DIMENSIONS = [
{ value: 128, label: "128 (*)" },
{ value: 192, label: "192" },
{ value: 256, label: "256 (*)" },
{ value: 320, label: "320" },
{ value: 384, label: "384" },
{ value: 448, label: "448" },
{ value: 512, label: "512 (*)" },
{ value: 576, label: "576" },
{ value: 640, label: "640" },
{ value: 704, label: "704" },
{ value: 768, label: "768 (*)" },
{ value: 832, label: "832" },
{ value: 896, label: "896" },
{ value: 960, label: "960" },
{ value: 1024, label: "1024 (*)" },
];
export default function PropertySettings() {
const { t } = useTranslation();
const setRequestOption = useImageCreate((state) => state.setRequestOptions);
const toggleUseRandomSeed = useImageCreate(
(state) => state.toggleUseRandomSeed
);
const isRandomSeed = useImageCreate((state) => state.isRandomSeed());
const seed = useImageCreate((state) => state.getValueForRequestKey("seed"));
const steps = useImageCreate((state) =>
state.getValueForRequestKey("num_inference_steps")
);
const guidanceScale = useImageCreate((state) =>
state.getValueForRequestKey("guidance_scale")
);
const initImage = useImageCreate((state) =>
state.getValueForRequestKey("init_image")
);
const promptStrength = useImageCreate((state) =>
state.getValueForRequestKey("prompt_strength")
);
const width = useImageCreate((state) => state.getValueForRequestKey("width"));
const height = useImageCreate((state) =>
state.getValueForRequestKey("height")
);
const propertyOpen = useCreateUI((state) => state.isOpenAdvPropertySettings);
const togglePropertyOpen = useCreateUI(
(state) => state.toggleAdvPropertySettings
);
return (
<div>
<button type="button" className={MenuButton} onClick={togglePropertyOpen}>
<h4>Property Settings</h4>
</button>
{propertyOpen && (
<>
<div className={SettingItem}>
<label>
Seed:
<input
size={10}
value={seed}
onChange={(e) => setRequestOption("seed", e.target.value)}
disabled={isRandomSeed}
placeholder="random"
/>
</label>
<label>
<input
type="checkbox"
checked={isRandomSeed}
onChange={(e) => toggleUseRandomSeed()}
/>{" "}
Random Image
</label>
</div>
<div className={SettingItem}>
<label>
{t("settings.steps")}{" "}
<input
value={steps}
onChange={(e) => {
setRequestOption("num_inference_steps", e.target.value);
}}
size={4}
/>
</label>
</div>
<div className={SettingItem}>
<label>
{t("settings.guide-scale")}
<input
value={guidanceScale}
onChange={(e) =>
setRequestOption("guidance_scale", e.target.value)
}
type="range"
min="0"
max="20"
step=".1"
/>
</label>
<span>{guidanceScale}</span>
</div>
{void 0 !== initImage && (
<div className={SettingItem}>
<label>
{t("settings.prompt-str")}{" "}
<input
value={promptStrength}
onChange={(e) =>
setRequestOption("prompt_strength", e.target.value)
}
type="range"
min="0"
max="1"
step=".05"
/>
</label>
<span>{promptStrength}</span>
</div>
)}
<div className={SettingItem}>
<label>
{t("settings.width")}
<select
value={width}
onChange={(e) => setRequestOption("width", e.target.value)}
>
{IMAGE_DIMENSIONS.map((dimension) => (
<option
key={`width-option_${dimension.value}`}
value={dimension.value}
>
{dimension.label}
</option>
))}
</select>
</label>
<label>
{t("settings.height")}
<select
value={height}
onChange={(e) => setRequestOption("height", e.target.value)}
>
{IMAGE_DIMENSIONS.map((dimension) => (
<option
key={`height-option_${dimension.value}`}
value={dimension.value}
>
{dimension.label}
</option>
))}
</select>
</label>
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,109 @@
import React from "react";
import { useImageCreate } from "../../../../../stores/imageCreateStore";
import { useCreateUI } from "../../creationPanelUIStore";
import {
SettingItem, // @ts-expect-error
} from "../../../../../styles/shared.css.ts";
import {
MenuButton, // @ts-expect-error
} from "../advancedsettings.css.ts";
import { useTranslation } from "react-i18next";
export default function WorkflowSettings() {
const { t } = useTranslation();
const numOutputs = useImageCreate((state) =>
state.getValueForRequestKey("num_outputs")
);
const parallelCount = useImageCreate((state) => state.parallelCount);
const isUseAutoSave = useImageCreate((state) => state.isUseAutoSave());
const diskPath = useImageCreate((state) =>
state.getValueForRequestKey("save_to_disk_path")
);
const isSoundEnabled = useImageCreate((state) => state.isSoundEnabled());
const setRequestOption = useImageCreate((state) => state.setRequestOptions);
const setParallelCount = useImageCreate((state) => state.setParallelCount);
const toggleUseAutoSave = useImageCreate((state) => state.toggleUseAutoSave);
const toggleSoundEnabled = useImageCreate(
(state) => state.toggleSoundEnabled
);
const workflowOpen = useCreateUI((state) => state.isOpenAdvWorkflowSettings);
const toggleWorkflowOpen = useCreateUI(
(state) => state.toggleAdvWorkflowSettings
);
return (
<div>
<button type="button" className={MenuButton} onClick={toggleWorkflowOpen}>
<h4>Workflow Settings</h4>
</button>
{workflowOpen && (
<>
<div className={SettingItem}>
<label>
{t("settings.amount-of-img")}{" "}
<input
type="number"
value={numOutputs}
onChange={(e) =>
setRequestOption("num_outputs", parseInt(e.target.value, 10))
}
size={4}
/>
</label>
</div>
<div className={SettingItem}>
<label>
{t("settings.how-many")}
<input
type="number"
value={parallelCount}
onChange={(e) => setParallelCount(parseInt(e.target.value, 10))}
size={4}
/>
</label>
</div>
<div className={SettingItem}>
<label>
<input
checked={isUseAutoSave}
onChange={(e) => toggleUseAutoSave()}
type="checkbox"
/>
{t("storage.ast")}{" "}
</label>
<label>
<input
value={diskPath}
onChange={(e) =>
setRequestOption("save_to_disk_path", e.target.value)
}
size={40}
disabled={!isUseAutoSave}
/>
<span className="visually-hidden">
Path on disk where images will be saved
</span>
</label>
</div>
<div className={SettingItem}>
<label>
<input
checked={isSoundEnabled}
onChange={(e) => toggleSoundEnabled()}
type="checkbox"
/>
{t("advanced-settings.sound")}
</label>
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,20 @@
import React from "react";
import { useImageCreate } from "../../../../../stores/imageCreateStore";
import ModifierTag from "../../../../atoms/modifierTag";
export default function ActiveTags() {
const selectedtags = useImageCreate((state) => state.selectedTags());
return (
<div className="selected-tags">
<p>Active Tags</p>
<ul>
{selectedtags.map((tag) => (
<li key={tag}>
<ModifierTag name={tag}></ModifierTag>
</li>
))}
</ul>
</div>
);
}

View File

@ -0,0 +1,24 @@
import { style, globalStyle } from "@vanilla-extract/css";
export const CreationBasicMain = style({
position: "relative",
width: "100%",
});
globalStyle(`${CreationBasicMain} > *`, {
marginBottom: "10px",
});
export const PromptDisplay = style({});
globalStyle(`${PromptDisplay} > p`, {
fontSize: "1.5em",
fontWeight: "bold",
marginBottom: "10px",
});
globalStyle(`${PromptDisplay} > textarea`, {
width: "100%",
resize: "vertical",
height: "100px",
});

View File

@ -0,0 +1,40 @@
import React, { ChangeEvent } from "react";
import { useImageCreate } from "../../../../stores/imageCreateStore";
import {
CreationBasicMain,
PromptDisplay, // @ts-expect-error
} from "./basicCreation.css.ts";
import SeedImage from "./seedImage";
import ActiveTags from "./activeTags";
import MakeButton from "./makeButton";
import { useTranslation } from "react-i18next";
export default function BasicCreation() {
const { t } = useTranslation();
const promptText = useImageCreate((state) =>
state.getValueForRequestKey("prompt")
);
const setRequestOption = useImageCreate((state) => state.setRequestOptions);
const handlePromptChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setRequestOption("prompt", event.target.value);
};
return (
<div className={CreationBasicMain}>
<div className={PromptDisplay}>
<p>{t("home.editor-title")}</p>
<textarea value={promptText} onChange={handlePromptChange}></textarea>
</div>
<MakeButton></MakeButton>
<SeedImage></SeedImage>
<ActiveTags></ActiveTags>
</div>
);
}

View File

@ -0,0 +1,90 @@
/* eslint-disable @typescript-eslint/naming-convention */
import React from "react";
import { useImageCreate } from "../../../../../stores/imageCreateStore";
import { useImageQueue } from "../../../../../stores/imageQueueStore";
import { v4 as uuidv4 } from "uuid";
import { useRandomSeed } from "../../../../../utils";
import {
MakeButtonStyle, // @ts-expect-error
} from "./makeButton.css.ts";
import { useTranslation } from "react-i18next";
export default function MakeButton() {
const { t } = useTranslation();
const parallelCount = useImageCreate((state) => state.parallelCount);
const builtRequest = useImageCreate((state) => state.builtRequest);
const addNewImage = useImageQueue((state) => state.addNewImage);
const hasQueue = useImageQueue((state) => state.hasQueuedImages());
const isRandomSeed = useImageCreate((state) => state.isRandomSeed());
const setRequestOption = useImageCreate((state) => state.setRequestOptions);
const makeImages = () => {
// potentially update the seed
if (isRandomSeed) {
// update the seed for the next time we click the button
setRequestOption("seed", useRandomSeed());
}
// the request that we have built
const req = builtRequest();
// the actual number of request we will make
const requests = [];
// the number of images we will make
let { num_outputs } = req;
// if making fewer images than the parallel count
// then it is only 1 request
if (parallelCount > num_outputs) {
requests.push(num_outputs);
} else {
// while we have at least 1 image to make
while (num_outputs >= 1) {
// subtract the parallel count from the number of images to make
num_outputs -= parallelCount;
// if we are still 0 or greater we can make the full parallel count
if (num_outputs <= 0) {
requests.push(parallelCount);
}
// otherwise we can only make the remaining images
else {
requests.push(Math.abs(num_outputs));
}
}
}
// make the requests
requests.forEach((num, index) => {
// get the seed we want to use
let seed = req.seed;
if (index !== 0) {
debugger;
// we want to use a random seed for subsequent requests
seed = useRandomSeed();
}
// add the request to the queue
addNewImage(uuidv4(), {
...req,
// updated the number of images to make
num_outputs: num,
// update the seed
seed,
});
});
};
return (
<button
className={MakeButtonStyle}
onClick={makeImages}
disabled={hasQueue}
>
{t("home.make-img-btn")}
</button>
);
}

View File

@ -0,0 +1,31 @@
import { style } from "@vanilla-extract/css";
// @ts-expect-error
import { vars } from "../../../../../styles/theme/index.css.ts";
export const MakeButtonStyle = style({
width: "100%",
backgroundColor: vars.colors.brand,
fontSize: vars.fonts.sizes.Headline,
fontWeight: "bold",
color: vars.colors.text.normal,
padding: vars.spacing.small,
borderRadius: vars.trim.smallBorderRadius,
":hover": {
backgroundColor: vars.colors.brandHover,
},
":active": {
backgroundColor: vars.colors.brandActive,
},
":disabled": {
backgroundColor: vars.colors.brandDimmed,
color: vars.colors.text.dimmed,
},
":focus": {
outline: "none",
},
});

View File

@ -0,0 +1,100 @@
import React, { useRef, ChangeEvent } from "react";
import {
ImageInputDisplay,
InputLabel,
ImageInput,
ImageInputButton,
ImageFixer,
XButton, // @ts-expect-error
} from "./seedImage.css.ts";
import { useImageCreate } from "../../../../../stores/imageCreateStore";
import { useTranslation } from "react-i18next";
// TODO : figure out why this needs props to be passed in.. fixes a type error
// when the component is used in the parent component
export default function SeedImage(_props: any) {
const { t } = useTranslation();
const imageInputRef = useRef<HTMLInputElement>(null);
const initImage = useImageCreate((state) =>
state.getValueForRequestKey("init_image")
);
const isInPaintingMode = useImageCreate((state) => state.isInpainting);
const setRequestOption = useImageCreate((state) => state.setRequestOptions);
const _startFileSelect = () => {
imageInputRef.current?.click();
};
const _handleFileSelect = (event: ChangeEvent<HTMLInputElement>) => {
// @ts-expect-error
const file = event.target.files[0];
if (void 0 !== file) {
const reader = new FileReader();
reader.onload = (e) => {
if (e.target != null) {
setRequestOption("init_image", e.target.result);
}
};
reader.readAsDataURL(file);
}
};
const toggleInpainting = useImageCreate((state) => state.toggleInpainting);
const _handleClearImage = () => {
setRequestOption("init_image", undefined);
if (isInPaintingMode) {
toggleInpainting();
}
};
return (
<div className={ImageInputDisplay}>
<div>
<label className={InputLabel}>
<b>{t("home.initial-img-txt")}</b>
</label>
<input
ref={imageInputRef}
className={ImageInput}
name="init_image"
type="file"
onChange={_handleFileSelect}
/>
<button className={ImageInputButton} onClick={_startFileSelect}>
{t("home.initial-img-btn")}
</button>
</div>
<div className={ImageFixer}>
{void 0 !== initImage && (
<>
<div>
<img src={initImage} width="100" height="100" />
<button className={XButton} onClick={_handleClearImage}>
X
</button>
</div>
{/* <label>
<input
type="checkbox"
onChange={(e) => {
toggleInpainting();
}}
checked={isInPaintingMode}
></input>
{t("in-paint.txt")}
</label> */}
</>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,63 @@
import { style } from "@vanilla-extract/css";
// @ts-expect-error
import { vars } from "../../../../../styles/theme/index.css.ts";
export const ImageInputDisplay = style({
display: "flex",
});
export const InputLabel = style({
marginBottom: vars.spacing.small,
display: "block",
});
export const ImageInput = style({
display: "none",
});
export const ImageInputButton = style({
backgroundColor: vars.colors.brand,
fontSize: vars.fonts.sizes.Subheadline,
fontWeight: "bold",
color: vars.colors.text.normal,
padding: vars.spacing.small,
borderRadius: vars.trim.smallBorderRadius,
":hover": {
backgroundColor: vars.colors.brandHover,
},
":active": {
backgroundColor: vars.colors.brandActive,
},
":disabled": {
backgroundColor: vars.colors.brandDimmed,
color: vars.colors.text.dimmed,
},
});
// this is needed to fix an issue with the image input text
// when that is a drag an drop we can remove this
export const ImageFixer = style({
marginLeft: "20px",
});
// just a 1 off component for now
// dont bother bringing in line with the rest of the app
export const XButton = style({
position: "absolute",
transform: "translateX(-50%) translateY(-35%)",
background: "black",
color: "white",
border: "2pt solid #ccc",
padding: "0",
cursor: "pointer",
outline: "inherit",
borderRadius: "8pt",
width: "16pt",
height: "16pt",
fontFamily: "Verdana",
fontSize: "8pt",
});

View File

@ -0,0 +1,56 @@
.create-layout {
padding: 10px;
}
/* .panel-box-toggle-btn {
display: block;
width: 100%;
text-align: left;
background-color: transparent;
color: #fff;
border: 0 none;
cursor: pointer;
} */
.selected-tags {
margin: 10px 0;
}
.selected-tags ul {
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
}
li {
list-style: none;
}
.modifier-list {
display: flex;
flex-wrap: wrap;
margin: 0;
padding: 0;
}
.modifierTag {
display: inline-block;
padding: 6px;
background-color: rgb(38, 77, 141);
color: #fff;
border-radius: 5px;
margin: 5px;
}
.modifierTag.selected {
background-color: rgb(131, 11, 121);
}
.modifierTag p {
margin: 0;
}
input[type="file"] {
/* Dont show the file name */
color: transparent;
}

View File

@ -0,0 +1,91 @@
import create from "zustand";
import produce from "immer";
import { persist } from "zustand/middleware";
export interface ImageCreationUIOptions {
isOpenAdvancedSettings: boolean;
isOpenAdvImprovementSettings: boolean;
isOpenAdvPropertySettings: boolean;
isOpenAdvWorkflowSettings: boolean;
isOpenAdvGPUSettings: boolean;
isOpenImageModifier: boolean;
imageMofidiersMap: object;
toggleAdvancedSettings: () => void;
toggleAdvImprovementSettings: () => void;
toggleAdvPropertySettings: () => void;
toggleAdvWorkflowSettings: () => void;
toggleAdvGPUSettings: () => void;
toggleImageModifier: () => void;
// addImageModifier: (modifier: string) => void;
}
export const useCreateUI = create<ImageCreationUIOptions>(
//@ts-expect-error
persist(
(set, get) => ({
isOpenAdvancedSettings: false,
isOpenAdvImprovementSettings: false,
isOpenAdvPropertySettings: false,
isOpenAdvWorkflowSettings: false,
isOpenAdvGPUSettings: false,
isOpenImageModifier: false,
imageMofidiersMap: {},
toggleAdvancedSettings: () => {
set(
produce((state: ImageCreationUIOptions) => {
state.isOpenAdvancedSettings = !state.isOpenAdvancedSettings;
})
);
},
toggleAdvImprovementSettings: () => {
set(
produce((state: ImageCreationUIOptions) => {
state.isOpenAdvImprovementSettings =
!state.isOpenAdvImprovementSettings;
})
);
},
toggleAdvPropertySettings: () => {
set(
produce((state: ImageCreationUIOptions) => {
state.isOpenAdvPropertySettings = !state.isOpenAdvPropertySettings;
})
);
},
toggleAdvWorkflowSettings: () => {
set(
produce((state: ImageCreationUIOptions) => {
state.isOpenAdvWorkflowSettings = !state.isOpenAdvWorkflowSettings;
})
);
},
toggleAdvGPUSettings: () => {
set(
produce((state: ImageCreationUIOptions) => {
state.isOpenAdvGPUSettings = !state.isOpenAdvGPUSettings;
})
);
},
toggleImageModifier: () => {
set(
produce((state: ImageCreationUIOptions) => {
state.isOpenImageModifier = !state.isOpenImageModifier;
})
);
},
}),
{
name: "createUI",
// getStorage: () => localStorage,
}
)
);

View File

@ -0,0 +1,17 @@
import { style } from "@vanilla-extract/css";
export const CreationPaneMain = style({
position: "relative",
width: "100%",
height: "100%",
padding: "0 10px",
overflowY: "auto",
});
export const InpaintingSlider = style({
position: "absolute",
top: "10px",
left: "400px",
zIndex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
});

View File

@ -0,0 +1,45 @@
import { style, globalStyle } from "@vanilla-extract/css";
// @ts-expect-error
import { vars } from "../../../../styles/theme/index.css.ts";
export const ImagerModifierGroups = style({
// marginBottom: vars.spacing.small,
paddingLeft: 0,
listStyleType: "none",
});
globalStyle(`${ImagerModifierGroups} li`, {
marginTop: vars.spacing.medium,
});
export const ImageModifierGrouping = style({
marginTop: vars.spacing.medium,
});
export const MenuButton = style({
display: "block",
width: "100%",
textAlign: "left",
backgroundColor: "transparent",
color: vars.colors.text.normal,
border: "0 none",
cursor: "pointer",
padding: "0",
marginBottom: vars.spacing.medium,
});
globalStyle(`${MenuButton}> h4`, {
color: "#e7ba71",
});
export const ModifierListStyle = style({
// marginBottom: vars.spacing.small,
paddingLeft: 0,
listStyleType: "none",
display: "flex",
flexWrap: "wrap",
});
globalStyle(`${ModifierListStyle} li`, {
margin: 0,
});

View File

@ -0,0 +1,96 @@
import React, { useState } from "react";
// @ts-expect-error
import { PanelBox } from "../../../../styles/shared.css.ts";
import {
ImagerModifierGroups,
ImageModifierGrouping,
MenuButton,
ModifierListStyle, //@ts-expect-error
} from "./imageModifiers.css.ts";
import { useImageCreate } from "../../../../stores/imageCreateStore";
import { useCreateUI } from "../creationPanelUIStore";
import ModifierTag from "../../../atoms/modifierTag";
interface ModifierListProps {
tags: string[];
}
function ModifierList({ tags }: ModifierListProps) {
return (
<ul className={ModifierListStyle}>
{tags.map((tag) => (
<li key={tag}>
<ModifierTag name={tag} />
</li>
))}
</ul>
);
}
interface ModifierGroupingProps {
title: string;
tags: string[];
}
function ModifierGrouping({ title, tags }: ModifierGroupingProps) {
// doing this localy for now, but could move to a store
// and persist if we wanted to
const [isExpanded, setIsExpanded] = useState(false);
const _toggleExpand = () => {
setIsExpanded(!isExpanded);
};
return (
<div className={ImageModifierGrouping}>
<button type="button" className={MenuButton} onClick={_toggleExpand}>
<h4>{title}</h4>
</button>
{isExpanded && <ModifierList tags={tags} />}
</div>
);
}
export default function ImageModifers() {
const allModifiers = useImageCreate((state) => state.allModifiers);
const imageModifierIsOpen = useCreateUI((state) => state.isOpenImageModifier);
const toggleImageModifiersIsOpen = useCreateUI(
(state) => state.toggleImageModifier
);
const handleClick = () => {
toggleImageModifiersIsOpen();
};
return (
<div className={PanelBox}>
<button
type="button"
onClick={handleClick}
className="panel-box-toggle-btn"
>
{/* TODO: swap this manual collapse stuff out for some UI component? */}
<h3>Image Modifiers (art styles, tags, ect)</h3>
</button>
{imageModifierIsOpen && (
<ul className={ImagerModifierGroups}>
{allModifiers.map((item, index) => {
return (
// @ts-expect-error
<li key={item[0]}>
{/* @ts-expect-error */}
<ModifierGrouping title={item[0]} tags={item[1]} />
</li>
);
})}
</ul>
)}
</div>
);
}

View File

@ -0,0 +1,18 @@
const Mockifiers = [
[
"Drawing Style",
[
"Cel Shading",
"Children's Drawing",
"Crosshatch",
"Detailed and Intricate",
"Doodle",
"Dot Art",
"Line Art",
"Sketch",
],
],
["Visual Style", ["2D", "8-bit", "16-bit", "Anaglyph", "Anime", "CGI"]],
];
export default Mockifiers;

View File

@ -0,0 +1,160 @@
import {
createGlobalTheme,
createThemeContract,
createTheme,
} from "@vanilla-extract/css";
/**
* Colors are all the same across the themes, this is just to set up a contract
* Colors can be decided later. I am just the architect.
* Tried to pull things from the original app.
*
* Lots of these arent used yet, but once they are defined and useable then they can be set.
*/
// Link color 0, 102, 204
const colors = createThemeContract({
brand: null,
brandDimmed: null,
brandHover: null,
brandActive: null,
brandAccent: null,
brandAccentDimmed: null,
brandAccentActive: null,
secondary: null,
secondaryDimmed: null,
secondaryHover: null,
secondaryActive: null,
secondaryAccent: null,
secondaryAccentDimmed: null,
secondaryAccentActive: null,
background: null,
backgroundAccent: null,
backgroundAlt: null,
backgroundAltAccent: null,
text: {
normal: null,
dimmed: null,
secondary: null,
secondaryDimmed: null,
accent: null,
accentDimmed: null,
},
link: null,
warning: null,
error: null,
success: null,
});
const app = createGlobalTheme(":root", {
spacing: {
small: "5px",
medium: "10px",
large: "25px",
},
trim: {
smallBorderRadius: "5px",
},
fonts: {
body: "Arial, Helvetica, sans-serif;",
sizes: {
Title: "2em",
Headline: "1.5em",
Subheadline: "1.20em",
SubSubheadline: "1em",
Body: "1em",
Caption: ".75em",
Overline: ".5em",
},
},
colors: colors,
});
export const darkTheme = createTheme(colors, {
brand: "#5000b9", // purple
brandDimmed: "#433852", // muted purple
brandHover: "#5d00d6", // bringhter purple
brandActive: "#5d00d6", // bringhter purple
brandAccent: "#28004e", // darker purple
brandAccentDimmed: "#28004e", // darker purple
brandAccentActive: "#28004e", // darker purple
secondary: "#0b8334", // green
secondaryDimmed: "#0b8334", // green
secondaryHover: "#0b8334", // green
secondaryActive: "#0b8334", // green
secondaryAccent: "#0b8334", // green
secondaryAccentDimmed: "#0b8334", // green
secondaryAccentActive: "#0b8334", // green
background: "#202124", // dark grey
backgroundAccent: " #383838", // lighter grey
backgroundAlt: "#2c2d30", // med grey
backgroundAltAccent: "#383838", // lighter grey
text: {
normal: "#ffffff", // white
dimmed: "#d1d5db", // off white
secondary: "#ffffff", // white
secondaryDimmed: "#d1d5db", // off white
accent: "#e7ba71", // orange
accentDimmed: "#7d6641", // muted orange
},
link: "#0066cc", // blue
warning: "#f0ad4e",
error: "#d9534f",
success: "#5cb85c",
});
// Generated by co-pilot
export const lightTheme = createTheme(colors, {
brand: "#1E40AF",
brandDimmed: "#1E40AF",
brandHover: "#1E40AF",
brandActive: "#1E40AF",
brandAccent: "#1E40AF",
brandAccentDimmed: "#1E40AF",
brandAccentActive: "#1E40AF",
secondary: "#DB2777",
secondaryDimmed: "#DB2777",
secondaryHover: "#DB2777",
secondaryActive: "#DB2777",
secondaryAccent: "#DB2777",
secondaryAccentDimmed: "#DB2777",
secondaryAccentActive: "#DB2777",
background: "#EFF6FF",
backgroundAccent: "#EFF6FF",
backgroundAlt: "#EFF6FF",
backgroundAltAccent: "#EFF6FF",
text: {
normal: "#1F2937",
dimmed: "#6B7280",
secondary: "#1F2937",
secondaryDimmed: "#6B7280",
accent: "#1F2937",
accentDimmed: "#6B7280",
},
link: "#0066cc", // blue
warning: "yellow",
error: "red",
success: "green",
});
export const vars = { ...app, colors };

View File

@ -0,0 +1,38 @@
import React, { ChangeEvent } from "react";
import AdvancedSettings from "./advancedSettings";
import ImageModifiers from "./imageModifiers";
import InpaintingPanel from "./inpaintingPanel";
// this works but causes type errors so its not worth it for now
// import { useImageCreate } from "@stores/imageCreateStore.ts";
import { useImageCreate } from "../../../stores/imageCreateStore";
import "./creationPanel.css";
import {
CreationPaneMain,
InpaintingSlider, // @ts-expect-error
} from "./creationpane.css.ts";
import BasicCreation from "./basicCreation";
export default function CreationPanel() {
const isInPaintingMode = useImageCreate((state) => state.isInpainting);
return (
<>
<div className={CreationPaneMain}>
<BasicCreation></BasicCreation>
<AdvancedSettings></AdvancedSettings>
<ImageModifiers></ImageModifiers>
</div>
{isInPaintingMode && (
<div className={InpaintingSlider}>
<InpaintingPanel></InpaintingPanel>
</div>
)}
</>
);
}

View File

@ -0,0 +1,114 @@
import React, { useRef, useState, ChangeEvent } from "react";
import DrawImage from "../../../molecules/drawImage";
import { useImageCreate } from "../../../../stores/imageCreateStore";
import {
InpaintingPanelMain,
InpaintingControls,
InpaintingControlRow, // @ts-expect-error
} from "./inpaintingPanel.css.ts";
export default function InpaintingPanel() {
// no idea if this is the right typing
const drawingRef = useRef(null);
const [brushSize, setBrushSize] = useState("20");
const [brushShape, setBrushShape] = useState("round");
const [brushColor, setBrushColor] = useState("#fff");
const [isErasing, setIsErasing] = useState(false);
const initImage = useImageCreate((state) =>
state.getValueForRequestKey("init_image")
);
const _handleBrushMask = () => {
setIsErasing(false);
};
const _handleBrushErase = () => {
setIsErasing(true);
};
const _handleFillMask = () => {
console.log("fill mask!!", drawingRef);
// drawingRef.current?.fillCanvas();
};
const _handleClearAll = () => {
console.log("clear all");
};
const _handleBrushSize = (event: ChangeEvent<HTMLInputElement>) => {
setBrushSize(event.target.value);
};
return (
<div className={InpaintingPanelMain}>
<DrawImage
// ref={drawingRef}
imageData={initImage}
brushSize={brushSize}
brushShape={brushShape}
brushColor={brushColor}
isErasing={isErasing}
/>
<div className={InpaintingControls}>
<div className={InpaintingControlRow}>
<button onClick={_handleBrushMask}>Mask</button>
<button onClick={_handleBrushErase}>Erase</button>
<button disabled onClick={_handleFillMask}>
Fill
</button>
<button disabled onClick={_handleClearAll}>
Clear
</button>
<label>
Brush Size
<input
type="range"
min="1"
max="100"
value={brushSize}
onChange={_handleBrushSize}
></input>
</label>
</div>
<div className={InpaintingControlRow}>
<button
onClick={() => {
setBrushShape("round");
}}
>
Cirle Brush
</button>
<button
onClick={() => {
setBrushShape("square");
}}
>
Square Brush
</button>
<button
onClick={() => {
setBrushColor("#000");
}}
>
Dark Brush
</button>
<button
onClick={() => {
setBrushColor("#fff");
}}
>
Light Brush
</button>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
import { style } from "@vanilla-extract/css";
export const InpaintingPanelMain = style({
position: "relative",
width: "100%",
height: "100%",
padding: "10px 10px",
});
export const InpaintingControls = style({
display: "flex",
flexDirection: "row",
width: "100%",
flexWrap: "wrap",
});
export const InpaintingControlRow = style({
display: "flex",
flexDirection: "row",
justifyContent: "space-evenly",
alignItems: "center",
width: "100%",
":first-of-type": {
margin: "10px 0",
},
});

View File

@ -0,0 +1,15 @@
import React from "react";
import { API_URL } from "../../../../api";
const url = `${API_URL}/ding.mp3`;
const AudioDing = React.forwardRef((props, ref) => (
// @ts-expect-error
<audio ref={ref} style={{ display: "none" }}>
<source src={url} type="audio/mp3" />
</audio>
));
AudioDing.displayName = "AudioDing";
export default AudioDing;

View File

@ -0,0 +1,60 @@
import { style, globalStyle } from "@vanilla-extract/css";
// @ts-expect-error
import { vars } from "../../../../styles/theme/index.css.ts";
export const completedImagesMain = style({
height: "100%",
width: "100%",
display: "flex",
paddingBottom: vars.spacing.medium,
});
export const completedImagesList = style({
display: "flex",
flexDirection: "row",
flexWrap: "nowrap",
height: "100%",
width: "100%",
overflow: "auto",
paddingLeft: vars.spacing.none,
});
globalStyle(`${completedImagesMain} li`, {
position: "relative",
});
globalStyle(`${completedImagesMain} > li:first-of-type`, {
marginLeft: vars.spacing.medium,
});
globalStyle(`${completedImagesMain} > li:last-of-type`, {
marginRight: 0,
});
export const imageContain = style({
width: "206px",
backgroundColor: "black",
display: "flex",
justifyContent: "center",
alignItems: "center",
flexShrink: 0,
border: "0 none",
padding: "0",
marginLeft: vars.spacing.medium,
cursor: "pointer",
});
globalStyle(`${imageContain} img`, {
width: "100%",
objectFit: "contain",
});
export const RemoveButton = style({
marginLeft: vars.spacing.small,
backgroundColor: vars.colors.brand,
border: "0 none",
padding: vars.spacing.small,
cursor: "pointer",
borderRadius: vars.trim.borderRadiusSmall,
});

View File

@ -0,0 +1,65 @@
import React from "react";
import { CompletedImagesType } from "../index";
import {
completedImagesMain,
completedImagesList,
imageContain,
RemoveButton,
// @ts-expect-error
} from "./completedImages.css.ts";
interface CurrentDisplayProps {
images: CompletedImagesType[] | null;
setCurrentDisplay: (image: CompletedImagesType) => void;
removeImages: () => void;
}
export default function CompletedImages({
images,
setCurrentDisplay,
removeImages,
}: CurrentDisplayProps) {
const _handleSetCurrentDisplay = (index: number) => {
const image = images![index];
setCurrentDisplay(image);
};
return (
<div className={completedImagesMain}>
{/* Adjust the dom do we dont do this check twice */}
{images != null && images.length > 0 && (
<button
className={RemoveButton}
onClick={() => {
removeImages();
}}
>
REMOVE
</button>
)}
<ul className={completedImagesList}>
{images?.map((image, index) => {
if (void 0 === image) {
console.warn(`image ${index} is undefined`);
return null;
}
return (
<li key={image.id}>
<button
className={imageContain}
onClick={() => {
_handleSetCurrentDisplay(index);
}}
>
<img src={image.data} alt={image.info.prompt} />
</button>
</li>
);
})}
</ul>
</div>
);
}

View File

@ -0,0 +1,84 @@
/* eslint-disable multiline-ternary */
/* eslint-disable @typescript-eslint/naming-convention */
import React from "react";
import GeneratedImage from "../../../molecules/generatedImage";
import { useImageCreate } from "../../../../stores/imageCreateStore";
import { CompletedImagesType } from "../index";
interface CurrentDisplayProps {
isLoading: boolean;
image: CompletedImagesType | null;
}
export default function CurrentDisplay({
isLoading,
image,
}: CurrentDisplayProps) {
const { info, data } = image ?? {};
const setRequestOption = useImageCreate((state) => state.setRequestOptions);
const createFileName = () => {
const {
prompt,
seed,
num_inference_steps,
guidance_scale,
use_face_correction,
use_upscale,
width,
height,
} = info!;
// Most important information is the prompt
let underscoreName = prompt.replace(/[^a-zA-Z0-9]/g, "_");
underscoreName = underscoreName.substring(0, 100);
// name and the top level metadata
let fileName = `${underscoreName}_Seed-${seed}_Steps-${num_inference_steps}_Guidance-${guidance_scale}`;
// Add the face correction and upscale
if (typeof use_face_correction == "string") {
fileName += `_FaceCorrection-${use_face_correction}`;
}
if (typeof use_upscale == "string") {
fileName += `_Upscale-${use_upscale}`;
}
// Add the width and height
fileName += `_${width}x${height}`;
// add the file extension
fileName += ".png";
// return fileName
return fileName;
};
const _handleSave = () => {
const link = document.createElement("a");
link.download = createFileName();
link.href = data!;
link.click();
};
const _handleUseAsInput = () => {
setRequestOption("init_image", data);
};
return (
<div className="current-display">
{isLoading ? (
<h4 className="loading">Loading...</h4>
) : (
(image !== null && (
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
<div>
<p> {info?.prompt}</p>
<GeneratedImage imageData={data} metadata={info}></GeneratedImage>
<div>
<button onClick={_handleSave}>Save</button>
<button onClick={_handleUseAsInput}>Use as Input</button>
</div>
</div>
)) || <h4 className="no-image">Try Making a new image!</h4>
)}
</div>
);
}

View File

@ -0,0 +1,17 @@
import { style } from "@vanilla-extract/css";
export const displayPanel = style({
height: "100%",
display: "flex",
flexDirection: "column",
});
export const displayContainer = style({
flexGrow: 1,
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
});
export const previousImages = style({});

View File

@ -0,0 +1,166 @@
import React, { useEffect, useState, useRef } from "react";
import { useImageQueue } from "../../../stores/imageQueueStore";
import { ImageRequest, useImageCreate } from "../../../stores/imageCreateStore";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
doMakeImage,
MakeImageKey,
ImageReturnType,
ImageOutput,
} from "../../../api";
import AudioDing from "./audioDing";
// import GeneratedImage from "../../molecules/generatedImage";
// import DrawImage from "../../molecules/drawImage";
import CurrentDisplay from "./currentDisplay";
import CompletedImages from "./completedImages";
import {
displayPanel,
displayContainer,
previousImages,
// @ts-expect-error
} from "./displayPanel.css.ts";
export interface CompletedImagesType {
id: string;
data: string;
info: ImageRequest;
}
const idDelim = "_batch";
export default function DisplayPanel() {
const dingRef = useRef<HTMLAudioElement>(null);
const isSoundEnabled = useImageCreate((state) => state.isSoundEnabled());
// @ts-expect-error
const { id, options } = useImageQueue((state) => state.firstInQueue());
const removeFirstInQueue = useImageQueue((state) => state.removeFirstInQueue);
const [currentImage, setCurrentImage] = useState<CompletedImagesType | null>(
null
);
const [isEnabled, setIsEnabled] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const { status, data } = useQuery(
[MakeImageKey, id],
async () => await doMakeImage(options),
{
enabled: isEnabled,
}
);
// update the enabled state when the id changes
useEffect(() => {
setIsEnabled(void 0 !== id);
}, [id]);
// helper for the loading state to be enabled aware
useEffect(() => {
if (isEnabled && status === "loading") {
setIsLoading(true);
} else {
setIsLoading(false);
}
}, [isEnabled, status]);
// this is where there loading actually happens
useEffect(() => {
// query is done
if (status === "success") {
// check to make sure that the image was created
if (data.status === "succeeded") {
if (isSoundEnabled) {
// not awaiting the promise or error handling
void dingRef.current?.play();
}
removeFirstInQueue();
}
}
}, [status, data, removeFirstInQueue, dingRef, isSoundEnabled]);
/* COMPLETED IMAGES */
const queryClient = useQueryClient();
const [completedImages, setCompletedImages] = useState<CompletedImagesType[]>(
[]
);
const completedIds = useImageQueue((state) => state.completedImageIds);
const clearCachedIds = useImageQueue((state) => state.clearCachedIds);
// this is where we generate the list of completed images
useEffect(() => {
const completedQueries = completedIds.map((id) => {
const imageData = queryClient.getQueryData([MakeImageKey, id]);
return imageData;
}) as ImageReturnType[];
if (completedQueries.length > 0) {
// map the completedImagesto a new array
// and then set the state
const temp = completedQueries
.map((query, index) => {
if (void 0 !== query) {
return query.output.map((data: ImageOutput, index: number) => {
return {
id: `${completedIds[index]}${idDelim}-${data.seed}-${index}`,
data: data.data,
info: { ...query.request, seed: data.seed },
};
});
}
})
.flat()
.reverse()
.filter((item) => void 0 !== item) as CompletedImagesType[]; // remove undefined items
setCompletedImages(temp);
// could move this to the useEffect for completedImages
if (temp.length > 0) {
setCurrentImage(temp[0]);
} else {
setCurrentImage(null);
}
} else {
setCompletedImages([]);
setCurrentImage(null);
}
}, [setCompletedImages, setCurrentImage, queryClient, completedIds]);
// this is how we remove them
const removeImages = () => {
completedIds.forEach((id) => {
queryClient.removeQueries([MakeImageKey, id]);
});
clearCachedIds();
};
return (
<div className={displayPanel}>
<AudioDing ref={dingRef}></AudioDing>
<div className={displayContainer}>
<CurrentDisplay
isLoading={isLoading}
image={currentImage}
></CurrentDisplay>
</div>
<div className={previousImages}>
<CompletedImages
removeImages={removeImages}
images={completedImages}
setCurrentDisplay={setCurrentImage}
></CompletedImages>
</div>
</div>
);
}

View File

@ -0,0 +1,60 @@
import { style, globalStyle } from "@vanilla-extract/css";
// @ts-expect-error
import { vars } from "../../../styles/theme/index.css.ts";
export const FooterDisplayMain = style({
color: vars.colors.text.normal,
fontSize: vars.fonts.sizes.Caption,
display: "inline-block",
// marginTop: vars.spacing.medium,
// marginBottom: vars.spacing.medium,
// TODO move this to the theme
padding: vars.spacing.small,
boxShadow:
"0 4px 8px 0 rgba(0, 0, 0, 0.15), 0 6px 20px 0 rgba(0, 0, 0, 0.15)",
});
export const CoffeeButton = style({
height: "23px",
transform: "translateY(25%)",
});
globalStyle(`${FooterDisplayMain} a`, {
color: vars.colors.link,
textDecoration: "none",
});
globalStyle(`${FooterDisplayMain} a:hover`, {
textDecoration: "underline",
});
globalStyle(`${FooterDisplayMain} a:visited`, {
color: vars.colors.link,
});
globalStyle(`${FooterDisplayMain} a:active`, {
color: vars.colors.link,
});
globalStyle(`${FooterDisplayMain} a:focus`, {
color: vars.colors.link,
});
globalStyle(`${FooterDisplayMain} p`, {
margin: vars.spacing.min,
});
// .footer-display {
// color: #ffffff;
// display: flex;
// flex-direction: column;
// align-items: center;
// justify-content: center;
// }
// #coffeeButton {
// height: 23px;
// transform: translateY(25%);
// }

View File

@ -0,0 +1,71 @@
import React from "react";
import {
FooterDisplayMain,
CoffeeButton, // @ts-expect-error
} from "./footerDisplay.css.ts";
import { API_URL } from "../../../api";
export default function FooterDisplay() {
return (
<div className={FooterDisplayMain}>
<p>
If you found this project useful and want to help keep it alive, please{" "}
<a
href="https://ko-fi.com/cmdr2_stablediffusion_ui"
target="_blank"
rel="noreferrer"
>
<img src={`${API_URL}/kofi.png`} className={CoffeeButton} />
</a>{" "}
to help cover the cost of development and maintenance! Thank you for
your support!
</p>
<p>
Please feel free to join the{" "}
<a
href="https://discord.com/invite/u9yhsFmEkB"
target="_blank"
rel="noreferrer"
>
discord community
</a>{" "}
or{" "}
<a
href="https://github.com/cmdr2/stable-diffusion-ui/issues"
target="_blank"
rel="noreferrer"
>
file an issue
</a>{" "}
if you have any problems or suggestions in using this interface.
</p>
<div id="footer-legal">
<p>
<b>Disclaimer:</b> The authors of this project are not responsible for
any content generated using this interface.
</p>
<p>
This license of this software forbids you from sharing any content
that violates any laws, produce any harm to a person, disseminate any
personal information that would be meant for harm, <br />
spread misinformation and target vulnerable groups. For the full list
of restrictions please read{" "}
<a
href="https://github.com/cmdr2/stable-diffusion-ui/blob/main/LICENSE"
target="_blank"
rel="noreferrer"
>
the license
</a>
.
</p>
<p>
By using this software, you consent to the terms and conditions of the
license.
</p>
</div>
</div>
);
}

View File

@ -0,0 +1,16 @@
import { style, globalStyle } from "@vanilla-extract/css";
//@ts-expect-error
import { vars } from "../../../styles/theme/index.css.ts";
export const HeaderDisplayMain = style({
color: vars.colors.text.normal,
display: "flex",
alignItems: "center",
justifyContent: "center",
});
globalStyle(`${HeaderDisplayMain} > h1`, {
fontSize: vars.fonts.sizes.Title,
fontWeight: "bold",
marginRight: vars.spacing.medium,
});

View File

@ -0,0 +1,52 @@
import React, { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { KEY_CONFIG, getConfig } from "../../../api";
import StatusDisplay from "./statusDisplay";
import { useTranslation } from "react-i18next";
import {
HeaderDisplayMain, // @ts-expect-error
} from "./headerDisplay.css.ts";
// import LanguageDropdown from "./languageDropdown";
export default function HeaderDisplay() {
const { t } = useTranslation();
const { status, data } = useQuery([KEY_CONFIG], getConfig);
const [version, setVersion] = useState("2.1.0");
const [release, setRelease] = useState("");
// this is also in the Beta Mode
// TODO: make this a custom hook
useEffect(() => {
if (status === "success") {
// TODO also pass down the actual version
const { update_branch: updateBranch } = data;
// just hard coded for now
setVersion("v2.1");
if (updateBranch === "main") {
setRelease("(stable)");
} else {
setRelease("(beta)");
}
}
}, [status, data, setVersion, setVersion]);
return (
<div className={HeaderDisplayMain}>
<h1>
{t("title")} {version} {release}{" "}
</h1>
<StatusDisplay className="status-display"></StatusDisplay>
{/* <LanguageDropdown></LanguageDropdown> */}
</div>
);
}

View File

@ -0,0 +1,23 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
const LanguageDropdown = () => {
const { i18n } = useTranslation();
const [language, setLanguage] = useState("id");
const handleLangChange = (evt: any) => {
const lang = evt.target.value;
console.log(lang);
setLanguage(lang);
i18n.changeLanguage(lang);
};
return (
<select onChange={handleLangChange} value={language}>
<option value="en">EN</option>
<option value="es">ES</option>
</select>
);
};
export default LanguageDropdown;

View File

@ -0,0 +1,49 @@
import React, { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { healthPing, HEALTH_PING_INTERVAL } from "../../../../api";
import {
StartingStatus,
ErrorStatus,
SuccessStatus,
} from "./statusDisplay.css";
const startingMessage = "Stable Diffusion is starting...";
const successMessage = "Stable Diffusion is ready to use!";
const errorMessage = "Stable Diffusion is not running!";
export default function StatusDisplay({ className }: { className?: string }) {
const [statusMessage, setStatusMessage] = useState(startingMessage);
const [statusClass, setStatusClass] = useState(StartingStatus);
// but this will be moved to the status display when it is created
const { status, data } = useQuery(["health"], healthPing, {
refetchInterval: HEALTH_PING_INTERVAL,
});
useEffect(() => {
if (status === "loading") {
setStatusMessage(startingMessage);
setStatusClass(StartingStatus);
} else if (status === "error") {
setStatusMessage(errorMessage);
setStatusClass(ErrorStatus);
} else if (status === "success") {
if (data[0] === "OK") {
setStatusMessage(successMessage);
setStatusClass(SuccessStatus);
} else {
setStatusMessage(errorMessage);
setStatusClass(ErrorStatus);
}
}
}, [status, data]);
return (
<>
{/* alittle hacky but joins the class names, will probably need a better css in js solution or tailwinds */}
<p className={[statusClass, className].join(" ")}>{statusMessage}</p>
</>
);
}

View File

@ -0,0 +1,16 @@
import { style } from "@vanilla-extract/css";
// @ts-expect-error
import { vars } from "../../../../styles/theme/index.css.ts";
export const StartingStatus = style({
color: vars.colors.warning,
});
export const ErrorStatus = style({
color: vars.colors.error,
});
export const SuccessStatus = style({
color: vars.colors.success,
});

View File

@ -0,0 +1,35 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { enableMapSet } from "immer";
import App from "./App";
import "./styles/index.css.ts";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
staleTime: Infinity,
cacheTime: Infinity,
},
},
});
enableMapSet();
// application entry point
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={true} />
</QueryClientProvider>
</React.StrictMode>
);

View File

@ -0,0 +1,144 @@
[
[
"Drawing Style",
[
"Cel Shading",
"Children's Drawing",
"Crosshatch",
"Detailed and Intricate",
"Doodle",
"Dot Art",
"Line Art",
"Sketch"
]
],
[
"Visual Style",
[
"2D",
"8-bit",
"16-bit",
"Anaglyph",
"Anime",
"CGI",
"Cartoon",
"Comic Book",
"Concept Art",
"Digital Art",
"Fantasy",
"Graphic Novel",
"Hard Edge Painting",
"Hydrodipped",
"Lithography",
"Manga",
"Modern Art",
"Mosaic",
"Mural",
"Photo",
"Realistic",
"Street Art",
"Visual Novel",
"Watercolor"
]
],
[
"Pen",
["Chalk", "Colored Pencil", "Graphite", "Ink", "Oil Paint", "Pastel Art"]
],
[
"Carving and Etching",
[
"Etching",
"Linocut",
"Paper Model",
"Paper-Mache",
"Papercutting",
"Pyrography",
"Wood-Carving"
]
],
[
"Camera",
[
"Aerial View",
"Canon50",
"Cinematic",
"Close-up",
"Color Grading",
"Dramatic",
"Film Grain",
"Fisheye Lens",
"Glamor Shot",
"Golden Hour",
"HD",
"Lens Flare",
"Macro",
"Polaroid",
"Vintage",
"War Photography",
"White Balance",
"Wildlife Photography"
]
],
[
"Color",
[
"Beautiful Lighting",
"Colorful",
"Dynamic Lighting",
"Electric Colors",
"Infrared",
"Synthwave",
"Warm Color Palette"
]
],
[
"Emotions",
[
"Angry",
"Disgusted",
"Embarrassed",
"Evil",
"Excited",
"Fear",
"Happy",
"Lonely",
"Sad",
"Surprised"
]
],
[
"Style of an artist or community",
[
"Artstation",
"by Agnes Lawrence Pelton",
"by Akihito Yoshida",
"by Andy Warhol",
"by Artgerm",
"by Asaf Hanuka",
"by Aubrey Beardsley",
"by Banksy",
"by Ben Enwonwu",
"by Caravaggio Michelangelo Merisi",
"by David Mann",
"by Frida Kahlo",
"by H.R. Giger",
"by Hayao Miyazaki",
"by Ivan Shishkin",
"by Johannes Vermeer",
"by John William Waterhouse",
"by Katsushika Hokusai",
"by Ko Young Hoon",
"by Leonardo da Vinci",
"by Lisa Frank",
"by Mahmoud Saïd",
"by Mark Brooks",
"by Pablo Picasso",
"by Richard Dadd",
"by Salvador Dalí",
"by Tivadar Csontváry Kosztka",
"by Yoshitaka Amano",
"by wlop"
]
]
]

View File

@ -0,0 +1,9 @@
import React from "react";
export default function Beta() {
return (
<div>
<h1>Beta</h1>
</div>
);
}

View File

@ -0,0 +1,53 @@
import { style } from "@vanilla-extract/css";
// @ts-expect-error
import { vars } from "../../styles/theme/index.css.ts";
export const AppLayout = style({
position: "relative",
backgroundColor: vars.colors.background,
width: "100%",
height: "100%",
pointerEvents: "auto",
display: "grid",
// backgroundColor: "rgb(32, 33, 36)",
gridTemplateColumns: "400px 1fr",
gridTemplateRows: "100px 1fr 115px",
gridTemplateAreas: `
"header header header"
"create display display"
"create footer footer"
`,
"@media": {
"screen and (max-width: 800px)": {
gridTemplateColumns: "1fr",
gridTemplateRows: "100px 300px 1fr 100px",
gridTemplateAreas: `
"header"
"create"
"display"
"footer"
`,
},
},
});
export const HeaderLayout = style({
gridArea: "header",
});
export const CreateLayout = style({
gridArea: "create",
position: "relative",
});
export const DisplayLayout = style({
gridArea: "display",
overflow: "auto",
});
export const FooterLayout = style({
gridArea: "footer",
display: "flex",
justifyContent: "center",
});

View File

@ -0,0 +1,71 @@
import React, { useEffect } from "react";
import {
AppLayout,
HeaderLayout,
CreateLayout,
DisplayLayout,
FooterLayout, // @ts-expect-error
} from "./home.css.ts";
import { useQuery } from "@tanstack/react-query";
import { getSaveDirectory, loadModifications } from "../../api";
import Mockifiers from "../../components/organisms/creationPanel/imageModifiers/modifiers.mock";
import { useImageCreate } from "../../stores/imageCreateStore";
// Todo - import components here
import HeaderDisplay from "../../components/organisms/headerDisplay";
import CreationPanel from "../../components/organisms/creationPanel";
import DisplayPanel from "../../components/organisms/displayPanel";
import FooterDisplay from "../../components/organisms/footerDisplay";
function Home({ className }: { className: any }) {
// Get the original save directory
const setRequestOption = useImageCreate((state) => state.setRequestOptions);
const { status: statusSave, data: dataSave } = useQuery(
["SaveDir"],
getSaveDirectory
);
const { status: statusMods, data: dataMoads } = useQuery(
["modifications"],
loadModifications
);
const setAllModifiers = useImageCreate((state) => state.setAllModifiers);
useEffect(() => {
if (statusSave === "success") {
setRequestOption("save_to_disk_path", dataSave);
}
}, [setRequestOption, statusSave, dataSave]);
useEffect(() => {
if (statusMods === "success") {
setAllModifiers(dataMoads);
} else if (statusMods === "error") {
// @ts-expect-error
setAllModifiers(Mockifiers);
}
}, [setRequestOption, statusMods, dataMoads]);
return (
<div className={[AppLayout, className].join(" ")}>
<header className={HeaderLayout}>
<HeaderDisplay></HeaderDisplay>
</header>
<nav className={CreateLayout}>
<CreationPanel></CreationPanel>
</nav>
<main className={DisplayLayout}>
<DisplayPanel></DisplayPanel>
</main>
<footer className={FooterLayout}>
<FooterDisplay></FooterDisplay>
</footer>
</div>
);
}
export default Home;

View File

@ -0,0 +1,9 @@
import React from "react";
export default function Settings({ className }: { className: any }) {
return (
<div>
<h1>Settings</h1>
</div>
);
}

View File

@ -0,0 +1,318 @@
import create from "zustand";
import produce from "immer";
import { devtools } from "zustand/middleware";
import { useRandomSeed } from "../utils";
export interface ImageCreationUiOptions {
isUseRandomSeed: boolean;
isUseAutoSave: boolean;
isSoundEnabled: boolean;
}
export interface ImageRequest {
prompt: string;
seed: number;
num_outputs: number;
num_inference_steps: number;
guidance_scale: number;
width:
| 128
| 192
| 256
| 320
| 384
| 448
| 512
| 576
| 640
| 704
| 768
| 832
| 896
| 960
| 1024;
height:
| 128
| 192
| 256
| 320
| 384
| 448
| 512
| 576
| 640
| 704
| 768
| 832
| 896
| 960
| 1024;
// allow_nsfw: boolean
turbo: boolean;
use_cpu: boolean;
use_full_precision: boolean;
save_to_disk_path: null | string;
use_face_correction: null | "GFPGANv1.3";
use_upscale: null | "RealESRGAN_x4plus" | "RealESRGAN_x4plus_anime_6B" | "";
show_only_filtered_image: boolean;
init_image: undefined | string;
prompt_strength: undefined | number;
}
type ModifiersList = string[];
type ModifiersOptions = string | ModifiersList[];
type ModifiersOptionList = ModifiersOptions[];
interface ImageCreateState {
parallelCount: number;
requestOptions: ImageRequest;
allModifiers: ModifiersOptionList;
tags: string[];
isInpainting: boolean;
setParallelCount: (count: number) => void;
setRequestOptions: (key: keyof ImageRequest, value: any) => void;
getValueForRequestKey: (key: keyof ImageRequest) => any;
setAllModifiers: (modifiers: ModifiersOptionList) => void;
setModifierOptions: (key: string, value: any) => void;
toggleTag: (tag: string) => void;
hasTag: (tag: string) => boolean;
selectedTags: () => string[];
builtRequest: () => ImageRequest;
uiOptions: ImageCreationUiOptions;
toggleUseUpscaling: () => void;
// isUsingUpscaling: () => boolean
toggleUseFaceCorrection: () => void;
isUsingFaceCorrection: () => boolean;
isUsingUpscaling: () => boolean;
toggleUseRandomSeed: () => void;
isRandomSeed: () => boolean;
toggleUseAutoSave: () => void;
isUseAutoSave: () => boolean;
toggleSoundEnabled: () => void;
isSoundEnabled: () => boolean;
toggleInpainting: () => void;
}
// devtools breaks TS
export const useImageCreate = create<ImageCreateState>(
// @ts-expect-error
devtools((set, get) => ({
parallelCount: 1,
requestOptions: {
prompt: "a photograph of an astronaut riding a horse",
seed: useRandomSeed(),
num_outputs: 1,
num_inference_steps: 50,
guidance_scale: 7.5,
width: 512,
height: 512,
prompt_strength: 0.8,
// allow_nsfw: false,
turbo: true,
use_cpu: false,
use_full_precision: true,
save_to_disk_path: "null",
use_face_correction: "GFPGANv1.3",
use_upscale: "RealESRGAN_x4plus",
show_only_filtered_image: true,
init_image: undefined,
},
// selected tags
tags: [] as string[],
uiOptions: {
// TODO proper persistence of all UI / user settings centrally somewhere?
// localStorage.getItem('ui:advancedSettingsIsOpen') === 'true',
isUseRandomSeed: true,
isUseAutoSave: false,
isSoundEnabled: false,
},
allModifiers: [[[]]] as ModifiersOptionList,
isInpainting: false,
setParallelCount: (count: number) =>
set(
produce((state) => {
state.parallelCount = count;
})
),
setRequestOptions: (key: keyof ImageRequest, value: any) => {
set(
produce((state) => {
state.requestOptions[key] = value;
})
);
},
getValueForRequestKey: (key: keyof ImageRequest) => {
return get().requestOptions[key];
},
setAllModifiers: (modifiers: ModifiersOptionList) => {
set(
produce((state) => {
state.allModifiers = modifiers;
})
);
},
toggleTag: (tag: string) => {
set(
produce((state) => {
const index = state.tags.indexOf(tag);
if (index > -1) {
state.tags.splice(index, 1);
} else {
state.tags.push(tag);
}
})
);
},
hasTag: (tag: string) => {
return get().tags.includes(tag);
},
selectedTags: () => {
return get().tags;
},
// the request body to send to the server
// this is a computed value, just adding the tags to the request
builtRequest: () => {
const state = get();
const requestOptions = state.requestOptions;
const tags = state.tags;
// join all the tags with a comma and add it to the prompt
const prompt = `${requestOptions.prompt} ${tags.join(",")}`;
const request = {
...requestOptions,
prompt,
};
// if we arent using auto save clear the save path
if (!state.uiOptions.isUseAutoSave) {
// maybe this is "None" ?
// TODO check this
request.save_to_disk_path = null;
}
if (void 0 === request.init_image) {
request.prompt_strength = undefined;
}
// a bit of a hack. figure out what a clean value to pass to the server is
// if we arent using upscaling clear the upscaling
if (request.use_upscale === "") {
request.use_upscale = null;
}
// make sure you look above for the "null" value
// this patches over a a backend issue if you dont ask for a filtered image
// you get nothing back
if (
null === request.use_upscale &&
null === request.use_face_correction
) {
request.show_only_filtered_image = false;
}
return request;
},
toggleUseFaceCorrection: () => {
set(
produce((state) => {
const isSeting =
typeof state.getValueForRequestKey("use_face_correction") ===
"string"
? null
: "GFPGANv1.3";
state.requestOptions.use_face_correction = isSeting;
})
);
},
isUsingFaceCorrection: () => {
const isUsing =
typeof get().getValueForRequestKey("use_face_correction") === "string";
return isUsing;
},
isUsingUpscaling: () => {
const isUsing = get().getValueForRequestKey("use_upscale") !== "";
return isUsing;
},
toggleUseRandomSeed: () => {
set(
produce((state: ImageCreateState) => {
state.uiOptions.isUseRandomSeed = !state.uiOptions.isUseRandomSeed;
state.requestOptions.seed = state.uiOptions.isUseRandomSeed
? useRandomSeed()
: state.requestOptions.seed;
// localStorage.setItem(
// "ui:isUseRandomSeed",
// state.uiOptions.isUseRandomSeed
// );
})
);
},
isRandomSeed: () => {
return get().uiOptions.isUseRandomSeed;
},
toggleUseAutoSave: () => {
//isUseAutoSave
//save_to_disk_path
set(
produce((state: ImageCreateState) => {
state.uiOptions.isUseAutoSave = !state.uiOptions.isUseAutoSave;
// localStorage.setItem(
// "ui:isUseAutoSave",
// state.uiOptions.isUseAutoSave
// );
})
);
},
isUseAutoSave: () => {
return get().uiOptions.isUseAutoSave;
},
toggleSoundEnabled: () => {
set(
produce((state: ImageCreateState) => {
state.uiOptions.isSoundEnabled = !state.uiOptions.isSoundEnabled;
//localStorage.setItem('ui:isSoundEnabled', state.uiOptions.isSoundEnabled);
})
);
},
isSoundEnabled: () => {
return get().uiOptions.isSoundEnabled;
},
toggleInpainting: () => {
set(
produce((state: ImageCreateState) => {
state.isInpainting = !state.isInpainting;
})
);
},
}))
);

View File

@ -0,0 +1,22 @@
import create from "zustand";
import produce from "immer";
interface ImageDisplayState {
imageOptions: Map<string, any>;
currentImage: object | null;
addNewImage: (ImageData: string, imageOptions: any) => void;
}
export const useImageDisplay = create<ImageDisplayState>((set) => ({
imageOptions: new Map<string, any>(),
currentImage: null,
// use produce to make sure we don't mutate state
addNewImage: (ImageData: string, imageOptions: any) => {
set(
produce((state) => {
state.currentImage = { display: ImageData, options: imageOptions };
state.images.set(ImageData, imageOptions);
})
);
},
}));

View File

@ -0,0 +1,59 @@
import create from "zustand";
import produce from "immer";
import { useRandomSeed } from "../utils";
import { ImageRequest } from "./imageCreateStore";
interface ImageQueueState {
images: ImageRequest[];
completedImageIds: string[];
addNewImage: (id: string, imgRec: ImageRequest) => void;
hasQueuedImages: () => boolean;
firstInQueue: () => ImageRequest | {};
removeFirstInQueue: () => void;
clearCachedIds: () => void;
}
export const useImageQueue = create<ImageQueueState>((set, get) => ({
images: [],
completedImageIds: [],
// use produce to make sure we don't mutate state
addNewImage: (id: string, imgRec: ImageRequest, isRandom = false) => {
set(
produce((state) => {
let { seed } = imgRec;
if (isRandom) {
seed = useRandomSeed();
}
state.images.push({ id, options: { ...imgRec, seed } });
})
);
},
hasQueuedImages: () => {
return get().images.length > 0;
},
firstInQueue: () => {
let first: ImageRequest | {} = get().images[0];
first = void 0 !== first ? first : {};
return first;
},
removeFirstInQueue: () => {
set(
produce((state) => {
const image = state.images.shift();
state.completedImageIds.push(image.id);
})
);
},
clearCachedIds: () => {
set(
produce((state) => {
state.completedImageIds = [];
})
);
},
}));

View File

@ -0,0 +1,58 @@
import { globalStyle } from "@vanilla-extract/css";
// @ts-expect-error
import { vars } from "./theme/index.css.ts";
// baisc body style
globalStyle("body", {
margin: 0,
minWidth: "320px",
minHeight: "100vh",
});
// single page style
globalStyle("#root", {
position: "absolute",
top: 0,
left: 0,
width: "100vw",
height: "100vh",
overflow: "hidden",
});
// border box all
globalStyle(`*`, {
boxSizing: "border-box",
});
globalStyle(`button`, {
fontSize: vars.fonts.sizes.Body,
});
/** RESETS */
globalStyle(`p, h1, h2, h3, h4, h5, h6, ul`, {
margin: 0,
});
globalStyle(`h3`, {
fontSize: vars.fonts.sizes.Subheadline,
fontFamily: vars.fonts.body,
});
globalStyle(`h4, h5`, {
fontSize: vars.fonts.sizes.SubSubheadline,
fontFamily: vars.fonts.body,
});
globalStyle(`p, label`, {
fontSize: vars.fonts.sizes.Body,
fontFamily: vars.fonts.body,
});
globalStyle(`textarea`, {
margin: 0,
padding: 0,
border: "none",
fontSize: vars.fonts.sizes.Body,
fontWeight: "bold",
fontFamily: vars.fonts.body,
});

View File

@ -0,0 +1,22 @@
import { recipe } from "@vanilla-extract/recipes";
export const button = recipe({
variants: {
color: {
neutral: { background: "whitesmoke" },
brand: { background: "blueviolet" },
accent: { background: "slateblue" },
},
size: {
small: { padding: 12 },
medium: { padding: 16 },
large: { padding: 24 },
},
},
});
// export const card = recipe({
// variants: {
// color: {
// alt: { background: 'whitesmoke' },

View File

@ -0,0 +1,29 @@
import { style, globalStyle } from "@vanilla-extract/css";
// @ts-expect-error
import { vars } from "./theme/index.css.ts";
export const PanelBox = style({
background: vars.colors.backgroundAlt,
color: vars.colors.text.normal,
padding: vars.spacing.medium,
borderRadius: vars.trim.smallBorderRadius,
marginBottom: vars.spacing.medium,
// TODO move this to the theme
boxShadow:
"0 4px 8px 0 rgba(0, 0, 0, 0.15), 0 6px 20px 0 rgba(0, 0, 0, 0.15)",
});
globalStyle(`${PanelBox} .panel-box-toggle-btn`, {
display: "block",
width: "100%",
textAlign: "left",
backgroundColor: "transparent",
color: vars.colors.text.normal,
border: "0 none",
cursor: "pointer",
padding: "0",
});
export const SettingItem = style({
marginBottom: vars.spacing.medium,
});

View File

@ -0,0 +1,161 @@
import {
createGlobalTheme,
createThemeContract,
createTheme,
} from "@vanilla-extract/css";
/**
* Colors are all the same across the themes, this is just to set up a contract
* Colors can be decided later. I am just the architect.
* Tried to pull things from the original app.
*
* Lots of these arent used yet, but once they are defined and useable then they can be set.
*/
const colors = createThemeContract({
brand: null,
brandDimmed: null,
brandHover: null,
brandActive: null,
brandAccent: null,
brandAccentDimmed: null,
brandAccentActive: null,
secondary: null,
secondaryDimmed: null,
secondaryHover: null,
secondaryActive: null,
secondaryAccent: null,
secondaryAccentDimmed: null,
secondaryAccentActive: null,
background: null,
backgroundAccent: null,
backgroundAlt: null,
backgroundAltAccent: null,
text: {
normal: null,
dimmed: null,
secondary: null,
secondaryDimmed: null,
accent: null,
accentDimmed: null,
},
link: null,
warning: null,
error: null,
success: null,
});
const app = createGlobalTheme(":root", {
spacing: {
none: "0",
min: "2px",
small: "5px",
medium: "10px",
large: "25px",
},
trim: {
smallBorderRadius: "5px",
},
fonts: {
body: "Arial, Helvetica, sans-serif;",
sizes: {
Title: "2em",
Headline: "1.5em",
Subheadline: "1.20em",
SubSubheadline: "1.1em",
Body: "1em",
Plain: "0.8em",
Caption: ".75em",
Overline: ".5em",
},
},
colors,
});
export const darkTheme = createTheme(colors, {
brand: "#5000b9", // purple
brandDimmed: "#433852", // muted purple
brandHover: "#5d00d6", // bringhter purple
brandActive: "#5d00d6", // bringhter purple
brandAccent: "#28004e", // darker purple
brandAccentDimmed: "#28004e", // darker purple
brandAccentActive: "#28004e", // darker purple
secondary: "#0b8334", // green
secondaryDimmed: "#0b8334", // green
secondaryHover: "#0b8334", // green
secondaryActive: "#0b8334", // green
secondaryAccent: "#0b8334", // green
secondaryAccentDimmed: "#0b8334", // green
secondaryAccentActive: "#0b8334", // green
background: "#202124", // dark grey
backgroundAccent: " #383838", // lighter grey
backgroundAlt: "#2c2d30", // med grey
backgroundAltAccent: "#383838", // lighter grey
text: {
normal: "#ffffff", // white
dimmed: "#d1d5db", // off white
secondary: "#ffffff", // white
secondaryDimmed: "#d1d5db", // off white
accent: "#e7ba71", // orange
accentDimmed: "#7d6641", // muted orange
},
link: "#0066cc", // blue
warning: "#f0ad4e",
error: "#d9534f",
success: "#5cb85c",
});
// Generated by co-pilot
export const lightTheme = createTheme(colors, {
brand: "#1E40AF",
brandDimmed: "#1E40AF",
brandHover: "#1E40AF",
brandActive: "#1E40AF",
brandAccent: "#1E40AF",
brandAccentDimmed: "#1E40AF",
brandAccentActive: "#1E40AF",
secondary: "#DB2777",
secondaryDimmed: "#DB2777",
secondaryHover: "#DB2777",
secondaryActive: "#DB2777",
secondaryAccent: "#DB2777",
secondaryAccentDimmed: "#DB2777",
secondaryAccentActive: "#DB2777",
background: "#EFF6FF",
backgroundAccent: "#EFF6FF",
backgroundAlt: "#EFF6FF",
backgroundAltAccent: "#EFF6FF",
text: {
normal: "#1F2937",
dimmed: "#6B7280",
secondary: "#1F2937",
secondaryDimmed: "#6B7280",
accent: "#1F2937",
accentDimmed: "#6B7280",
},
link: "#0066cc", // blue
warning: "yellow",
error: "red",
success: "green",
});
export const vars = { ...app, colors };

View File

@ -0,0 +1,3 @@
export function useRandomSeed() {
return Math.floor(Math.random() * 10000);
}

View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@stores": ["src/stores"]
},
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,42 @@
import { defineConfig } from "vite";
import eslint from "vite-plugin-eslint";
import react from "@vitejs/plugin-react";
import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
// TODO figure out why vs code complains about this even though it works
"@stores": path.resolve(__dirname, "./src/stores"),
// TODO - add more aliases
},
},
plugins: [
eslint(),
react(),
vanillaExtractPlugin({
// configuration
}),
],
server: {
port: 9001,
},
build: {
// make sure everythign is in the same directory
outDir: "../dist",
rollupOptions: {
output: {
// dont hash the file names
// maybe once we update the python server?
entryFileNames: `[name].js`,
chunkFileNames: `[name].js`,
assetFileNames: `[name].[ext]`,
},
},
},
});

1
ui/frontend/dist/index.css vendored Normal file

File diff suppressed because one or more lines are too long

16
ui/frontend/dist/index.html vendored Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="dark light" />
<title>Stable Diffusion UI</title>
<script type="module" crossorigin src="/index.js"></script>
<link rel="stylesheet" href="/index.css">
</head>
<body>
<!-- The react app entry point. Currently no ui just poc importing and logging -->
<div id="root"></div>
</body>
</html>

110
ui/frontend/dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,7 @@
<!--
THIS IS NO LONGER BEING SERVED REMOVE WHEN WE HAVE PORTED ANY
OF THE FEATURES THAT WERE PARTIALLY COMPLETE WHEN WE DID THE CUT OVER
-->
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -1269,7 +1273,6 @@ function showInitImagePreview() {
let file = initImageSelector.files[0]
reader.addEventListener('load', function() {
// console.log(file.name, reader.result)
initImagePreview.src = reader.result
initImagePreviewContainer.style.display = 'block'
inpaintingEditorContainer.style.display = 'none'

View File

@ -18,12 +18,26 @@ from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from starlette.responses import FileResponse, StreamingResponse
from pydantic import BaseModel
# this is needed for development.
from fastapi.middleware.cors import CORSMiddleware
import logging
from sd_internal import Request, Response
app = FastAPI()
# we need to be able to run a local server for the UI (9001)
# and still be able to hit our python port (9000)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
model_loaded = False
model_is_loading = False
@ -63,7 +77,18 @@ app.mount('/media', StaticFiles(directory=os.path.join(SD_UI_DIR, 'media/')), na
@app.get('/')
def read_root():
headers = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"}
return FileResponse(os.path.join(SD_UI_DIR, 'index.html'), headers=headers)
return FileResponse(os.path.join(SD_UI_DIR,'frontend/dist/index.html'), headers=headers)
# then get the js files
@app.get('/index.js')
def read_scripts():
return FileResponse(os.path.join(SD_UI_DIR, 'frontend/dist/index.js'))
#then get the css files
@app.get('/index.css')
def read_styles():
return FileResponse(os.path.join(SD_UI_DIR, 'frontend/dist/index.css'))
@app.get('/ping')
async def ping():
@ -194,9 +219,19 @@ def getAppConfig():
print(traceback.format_exc())
return HTTPException(status_code=500, detail=str(e))
# moved these to the root for easier pathing
# TODO: change the vite config for public files
@app.get('/ding.mp3')
def read_ding():
return FileResponse(os.path.join(SD_UI_DIR, 'frontend/assets/ding.mp3'))
@app.get('/kofi.png')
def read_kofi():
return FileResponse(os.path.join(SD_UI_DIR, 'frontend/assets/kofi.png'))
@app.get('/modifiers.json')
def read_modifiers():
return FileResponse(os.path.join(SD_UI_DIR, 'modifiers.json'))
return FileResponse(os.path.join(SD_UI_DIR, 'frontend/assets/modifiers.json'))
@app.get('/output_dir')
def read_home_dir():