From a2a13d9334bc6e0a81b83cf51c21fc85cc90f6ac Mon Sep 17 00:00:00 2001 From: Juan David Arias Date: Sat, 26 Aug 2023 11:51:08 -0500 Subject: [PATCH] feat: list the models in the imagine sidenav (#107) * disnplay models in side nav * styles: add open and close animation to the accordion component * reorganize imagine settings * split inputs into base settings and model settings * fix overflow-x when showing input info bug * styles: display imagine image inputs before the prompt input on mobile * show the morpheus brand in the navbar on mobile * feat: standardize the ipainting image sizes * feat: display the imagine menu inside a modal on mobile * fix responsive problem with image inputs width --- .../ImageGallery/ImageGallery.module.scss | 4 + .../ImagineBase/ImagineBase.module.scss | 42 +-- .../components/ImagineBase/ImagineBase.tsx | 59 ++-- .../ImagineImageInput.module.scss} | 109 ++++--- .../ImagineImageInput.tsx} | 50 ++- .../ImagineInput/ImagineInput.module.scss | 4 +- .../ImagineMenu/ImagineMenu.module.scss | 99 +++--- .../components/ImagineMenu/ImagineMenu.tsx | 301 +++++++++++++----- .../ImagineSettings.module.scss | 8 +- .../ImagineSettings/ImagineSettings.tsx | 229 ++++++------- .../MaskPaintingCanvas/MaskPaintingCanvas.tsx | 27 +- .../components/Navbar/Navbar.module.scss | 1 + morpheus-client/components/Navbar/Navbar.tsx | 42 ++- .../OpenSource/OpenSource.module.scss | 3 +- .../atoms/accordion/Accordion.module.scss | 42 +++ .../components/atoms/accordion/Accordion.tsx | 36 +++ .../components/icons/arrowDown.tsx | 17 + .../components/icons/arrowRight.tsx | 17 + morpheus-client/context/CNContext.tsx | 2 +- morpheus-client/context/ImagineContext.tsx | 1 + morpheus-client/context/ModelsContext.tsx | 149 +++++++++ morpheus-client/context/SDContext.tsx | 35 +- morpheus-client/excalidraw/css/app.scss | 4 + morpheus-client/models/models.ts | 4 +- morpheus-client/pages/_app.tsx | 21 +- morpheus-client/utils/images.ts | 6 +- .../scripts/models/models-info.yaml | 4 +- 27 files changed, 885 insertions(+), 431 deletions(-) rename morpheus-client/components/{ImageDraggable/ImageDraggable.module.scss => ImagineImageInput/ImagineImageInput.module.scss} (69%) rename morpheus-client/components/{ImageDraggable/ImageDraggable.tsx => ImagineImageInput/ImagineImageInput.tsx} (80%) create mode 100644 morpheus-client/components/atoms/accordion/Accordion.module.scss create mode 100644 morpheus-client/components/atoms/accordion/Accordion.tsx create mode 100644 morpheus-client/components/icons/arrowDown.tsx create mode 100644 morpheus-client/components/icons/arrowRight.tsx create mode 100644 morpheus-client/context/ModelsContext.tsx diff --git a/morpheus-client/components/ImageGallery/ImageGallery.module.scss b/morpheus-client/components/ImageGallery/ImageGallery.module.scss index e58b667f..45844536 100644 --- a/morpheus-client/components/ImageGallery/ImageGallery.module.scss +++ b/morpheus-client/components/ImageGallery/ImageGallery.module.scss @@ -36,6 +36,10 @@ min-height: 420px; flex: 1; + @include media.mobile { + min-height: auto; + } + &.small { height: 300px; } diff --git a/morpheus-client/components/ImagineBase/ImagineBase.module.scss b/morpheus-client/components/ImagineBase/ImagineBase.module.scss index e705ec06..cc9501de 100644 --- a/morpheus-client/components/ImagineBase/ImagineBase.module.scss +++ b/morpheus-client/components/ImagineBase/ImagineBase.module.scss @@ -1,16 +1,34 @@ @use "styles/colors"; @use "styles/media"; +.imageInputsContainer { + width: 100%; + display: flex; + flex-wrap: wrap; + flex-direction: row; + justify-content: flex-start; + gap: 24px; + flex: 1; + margin-bottom: 48px; + + @include media.mobile { + margin-bottom: 16px; + } +} + .imagineBase { height: 100%; flex: 1 auto; display: flex; flex-direction: column; max-height: calc(100vh - 80px); + min-width: 300px; + max-width: 100vw; @include media.mobile { flex-direction: column; max-height: 100%; + background-color: colors.$background-secondary; } .imagesContent { @@ -24,32 +42,18 @@ border-bottom-left-radius: 24px; background-color: colors.$background-primary; + @include media.tablet { + border-bottom-left-radius: 0; + } + @include media.mobile { flex-direction: column; - padding: 24px 24px 24px 24px; + padding: 48px 24px 48px 24px; height: 100%; max-height: 100%; border-bottom-left-radius: 0; } - .inputImage { - max-width: 100%; - display: flex; - flex-direction: row; - justify-content: flex-start; - flex: 1; - gap: 30px; - margin-bottom: 48px; - - @include media.mobile { - flex: 0 1 50%; - flex-direction: column; - width: 100%; - max-width: 100%; - margin-right: 0; - } - } - .results { width: 100%; height: 100%; diff --git a/morpheus-client/components/ImagineBase/ImagineBase.tsx b/morpheus-client/components/ImagineBase/ImagineBase.tsx index ed1b8d36..dc764762 100644 --- a/morpheus-client/components/ImagineBase/ImagineBase.tsx +++ b/morpheus-client/components/ImagineBase/ImagineBase.tsx @@ -1,6 +1,6 @@ -import React from "react"; +import React, { Fragment } from "react"; import ImagineMenu from "../../components/ImagineMenu/ImagineMenu"; -import ImageDraggable from "@/components/ImageDraggable/ImageDraggable"; +import ImagineImageInput from "@/components/ImagineImageInput/ImagineImageInput"; import ImageGallery from "@/components/ImageGallery/ImageGallery"; import ImagineInput from "@/components/ImagineInput/ImagineInput"; import ImagineLayout from "@/layout/ImagineLayout/ImagineLayout"; @@ -29,35 +29,42 @@ const ImagineBase = (props: MainContainerProps) => { /> ); + const ImageInputs = (props.showImageInput || props.showMaskInput) && ( +
+ {props.showImageInput && ( + + )} + {props.showMaskInput && ( + } + showPaintMask={img2imgFile !== null} + /> + )} +
+ ); + return (
- {isMobile && } - {isMobile && ImagineInputInstance} + {isMobile && ( + + + {ImageInputs} + {ImagineInputInstance} + + )}
- {(props.showImageInput || props.showMaskInput) && ( -
- {props.showImageInput && ( - - )} - {props.showMaskInput && ( - } - showPaintMask={img2imgFile !== null} - /> - )} -
- )} + {!isMobile && ImageInputs}
diff --git a/morpheus-client/components/ImageDraggable/ImageDraggable.module.scss b/morpheus-client/components/ImagineImageInput/ImagineImageInput.module.scss similarity index 69% rename from morpheus-client/components/ImageDraggable/ImageDraggable.module.scss rename to morpheus-client/components/ImagineImageInput/ImagineImageInput.module.scss index 3740f27a..c53edddd 100644 --- a/morpheus-client/components/ImageDraggable/ImageDraggable.module.scss +++ b/morpheus-client/components/ImagineImageInput/ImagineImageInput.module.scss @@ -1,11 +1,31 @@ @use "styles/colors"; @use "styles/media"; -.imageDraggable { +.imagineImageInput { + min-width: 244px; display: flex; flex-direction: column; gap: 12px; - min-width: 244px; + + @include media.tablet { + min-width: auto; + } + + @include media.mobile { + padding: 0 24px; + min-width: auto; + width: auto; + flex: 1; + } + + @include media.xsmall { + max-width: 100%; + } + + .label { + z-index: 9; + max-width: 250px; + } .formContainer { width: 100%; @@ -23,21 +43,11 @@ @include media.mobile { width: 100%; height: auto; - flex-direction: column; - padding: 16px; - } - - .separator { - display: none; - width: 100%; - border-bottom: 5px; - border-style: solid; - margin: 24px 0; - border-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100' fill='none' rx='8' ry='8' stroke='%23312E47FF' stroke-width='4' stroke-dasharray='10%2c 10' stroke-dashoffset='53' stroke-linecap='round'/%3e%3c/svg%3e") 30 round; - - @include media.mobile { - display: block; - } + background-image: none; + justify-content: flex-start; + align-items: flex-start; + padding: 0; + gap: 24px; } .verticalSeparator { @@ -53,7 +63,13 @@ top: 0; bottom: 0; width: 2px; - background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 10px, #312E47FF 10px, #312E47FF 22px); + background-image: repeating-linear-gradient( + to bottom, transparent 0, transparent 10px, #312E47FF 10px, #312E47FF 22px + ); + } + + @include media.tablet { + margin: 0 8px; } @include media.mobile { @@ -62,29 +78,36 @@ } } - .paintInfo { - display: flex; - flex-direction: column; - align-items: center; - padding-bottom: 48px; - position: relative; - - @include media.mobile { - padding-bottom: 0; - } - } - - .formFileUpload { + .formInputItem { position: relative; display: flex; flex-direction: column; justify-content: center; align-items: center; + min-width: 150px; + max-width: 170px; + + @include media.tablet { + span { + display: none; + } + } @include media.mobile { - width: 100%; - height: auto; - padding: 0; + width: 160px; + height: 160px; + padding: 5px; + border-radius: 8px; + border: 1px solid colors.$background-border; + background-color: colors.$background-primary; + + a { + font-size: 18px !important; + } + + span { + display: none; + } } .inputFileUpload { @@ -111,10 +134,6 @@ display: flex; flex-direction: column; align-items: center; - - a { - text-decoration-line: underline; - } } .uploadButton:hover { @@ -136,7 +155,9 @@ .selectedFile { width: 100%; - min-height: 294px; + height: auto; + max-width: 300px; + max-height: 400px; position: relative; background-color: colors.$background-secondary; border-radius: 8px; @@ -144,6 +165,13 @@ flex-direction: column; padding: 8px; + @include media.mobile { + max-width: 100%; + max-height: initial; + margin-top: -40px; + padding: 0; + } + .header { height: auto; display: flex; @@ -159,9 +187,6 @@ } img { - width: auto; - max-width: 100%; - max-height: 100%; border-radius: 8px; object-fit: contain; box-shadow: colors.$box-shadow; diff --git a/morpheus-client/components/ImageDraggable/ImageDraggable.tsx b/morpheus-client/components/ImagineImageInput/ImagineImageInput.tsx similarity index 80% rename from morpheus-client/components/ImageDraggable/ImageDraggable.tsx rename to morpheus-client/components/ImagineImageInput/ImagineImageInput.tsx index 17b3526d..d5d448bd 100644 --- a/morpheus-client/components/ImageDraggable/ImageDraggable.tsx +++ b/morpheus-client/components/ImagineImageInput/ImagineImageInput.tsx @@ -7,7 +7,6 @@ import React, { useState, } from "react"; -import AppImage from "@/components/AppImage/AppImage"; import Modal from "../Modal/Modal"; import MaskPaintingCanvas from "../MaskPaintingCanvas/MaskPaintingCanvas"; import { CloseIcon } from "../icons/close"; @@ -15,7 +14,7 @@ import { UploadImageIcon } from "../icons/uploadImage"; import { PaintImageIcon } from "../icons/paintImage"; import useWindowDimensions from "@/hooks/useWindowDimensions"; import { MOBILE_SCREEN_WIDTH } from "@/utils/constants"; -import styles from "./ImageDraggable.module.scss"; +import styles from "./ImagineImageInput.module.scss"; import Link from "next/link"; interface DragDropFileProps { @@ -31,28 +30,28 @@ interface DragDropFileProps { id?: string; } -const DragDropFile = (props: DragDropFileProps) => { +const ImagineImageInput = (props: DragDropFileProps) => { const inputRef = useRef(null); const { width } = useWindowDimensions(); const isMobile = width < MOBILE_SCREEN_WIDTH; const [dragActive, setDragActive] = useState(false); const [selectedFile, setSelectedFile] = useState(null); - const [imgSrc, setImgSrc] = useState(null); + const [imageSrc, setImageSrc] = useState(null); const [showEditModal, setShowEditModal] = useState(false); useEffect(() => { if (selectedFile) { - setImgSrc(URL.createObjectURL(selectedFile)); + setImageSrc(URL.createObjectURL(selectedFile)); props.setImageFile(selectedFile); } }, [selectedFile]); useEffect(() => { if (props.imageFile) { - setImgSrc(URL.createObjectURL(props.imageFile)); + setImageSrc(URL.createObjectURL(props.imageFile)); } else { - setImgSrc(null); + setImageSrc(null); } }, [props.imageFile]); @@ -89,7 +88,7 @@ const DragDropFile = (props: DragDropFileProps) => { }; const clearImage = () => { - setImgSrc(null); + setImageSrc(null); setSelectedFile(null); props.setImageFile(null); }; @@ -97,7 +96,7 @@ const DragDropFile = (props: DragDropFileProps) => { const ImageInputForm = (
e.preventDefault()} style={props.styles} @@ -117,11 +116,11 @@ const DragDropFile = (props: DragDropFileProps) => { >
{props.icon ? props.icon : } - - Click to upload + + {isMobile ? "Upload" : "Upload an image"} - or drag and drop -

Maximum file size 50 MB

+ or drag and drop + Maximum file size 50 MB
@@ -138,29 +137,28 @@ const DragDropFile = (props: DragDropFileProps) => { {props.showPaintImageLink || props.showPaintMask ? ( -
) : null} {props.showPaintImageLink && ( -
+
- Paint an image + {isMobile ? "Paint" : "Paint an image"}
)} {props.showPaintMask && ( -
+
- setShowEditModal(true)} > - Paint a mask - + {isMobile ? "Paint" : "Paint an image"} +
)}
@@ -174,20 +172,20 @@ const DragDropFile = (props: DragDropFileProps) => {
- + {props.label}
); return ( -
+
{props.label && ( - + )} - {imgSrc ? ImageResultDetail : ImageInputForm} + {imageSrc ? ImageResultDetail : ImageInputForm}
{ ); }; -export default DragDropFile; +export default ImagineImageInput; diff --git a/morpheus-client/components/ImagineInput/ImagineInput.module.scss b/morpheus-client/components/ImagineInput/ImagineInput.module.scss index c0849605..97d868b0 100644 --- a/morpheus-client/components/ImagineInput/ImagineInput.module.scss +++ b/morpheus-client/components/ImagineInput/ImagineInput.module.scss @@ -17,7 +17,9 @@ width: 100%; display: flex; flex-direction: row; + flex-wrap: wrap; margin-top: 24px; + gap: 16px; @include media.mobile { height: auto; @@ -28,6 +30,7 @@ .inputText { flex: 1; + min-width: 300px; } .ImagineActions { @@ -35,7 +38,6 @@ flex-direction: row; justify-content: space-between; align-items: flex-end; - margin-left: 8px; @include media.mobile { gap: 12px; diff --git a/morpheus-client/components/ImagineMenu/ImagineMenu.module.scss b/morpheus-client/components/ImagineMenu/ImagineMenu.module.scss index 4f95f0ae..8d67e1c1 100644 --- a/morpheus-client/components/ImagineMenu/ImagineMenu.module.scss +++ b/morpheus-client/components/ImagineMenu/ImagineMenu.module.scss @@ -1,46 +1,25 @@ @use "styles/colors"; @use "styles/media"; -%item { - width: 100%; - height: 64px; - display: flex; - align-items: center; - cursor: pointer; - padding: 0 24px; - - &:hover { - background-color: colors.$background-primary-3; - border-radius: 8px; - } - - &.active { - background-color: colors.$background-primary-3; - border-radius: 8px; - } +.mobileButton { + margin: 96px 24px 24px 24px; + width: calc(100% - 48px); +} - @include media.mobile { - padding: 8px; - justify-content: center; +.imagineMenu { + flex: 0 0 300px; + background-color: colors.$background-secondary; + max-height: 100%; + overflow-y: auto; + padding: 24px; - p { - display: none; - } + &::-webkit-scrollbar { + display: none; } - - .icon { - margin-right: 12px; - - @include media.mobile { - margin-right: 0; - } + &::-webkit-scrollbar-thumb { + background-color: transparent; } -} - -.imagineMenu { - flex: 0 0 250px; - background-color: colors.$background-secondary; @include media.mobile { flex: 1; @@ -48,6 +27,7 @@ max-width: 100vw; height: auto; margin: 94px 12px 0 12px; + padding: 0; display: flex; flex-direction: row; justify-content: center; @@ -59,7 +39,6 @@ .brandContainer { width: 100%; height: auto; - padding: 24px; @include media.mobile { display: none; @@ -67,34 +46,44 @@ } p[class^="base-1 white"] { - margin: 0 24px 24px 24px; + margin: 24px 0; @include media.mobile { display: none; } } +} - .menuIcon { - @extend %item; - margin-top: 8px; - margin-bottom: 32px; +.menuItem { + width: 100%; + height: auto; + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + margin-top: 16px; + margin-left: 24px; + font-weight: bold; - &:hover { - background-color: transparent; - } + &:hover { + color: colors.$white-color !important; + } - @include media.mobile { - display: none; - } + &.active { + color: colors.$main-color !important; + } - .closeTitle { - width: 100%; - display: flex; - align-items: center; - } + @include media.mobile { + flex-direction: row; + padding: 8px; } - .menuItem { - @extend %item; + + .icon { + margin-right: 8px; + + @include media.mobile { + margin-right: 16px; + } } -} +} \ No newline at end of file diff --git a/morpheus-client/components/ImagineMenu/ImagineMenu.tsx b/morpheus-client/components/ImagineMenu/ImagineMenu.tsx index 9cb410a4..d45ba88a 100644 --- a/morpheus-client/components/ImagineMenu/ImagineMenu.tsx +++ b/morpheus-client/components/ImagineMenu/ImagineMenu.tsx @@ -1,8 +1,10 @@ -import { ReactNode } from "react"; +import { Fragment, ReactNode, useEffect, useState } from "react"; import { useRouter } from "next/router"; import Brand from "../Typography/Brand/Brand"; import { SDOption } from "@/context/SDContext"; +import { ModelFeature, useModels } from "@/context/ModelsContext"; +import { Accordion } from "@/components/atoms/accordion/Accordion"; import { Text2ImgIcon } from "../icons/text2img"; import { Img2ImgIcon } from "../icons/img2img"; import { ControlNetIcon } from "../icons/controlnet"; @@ -11,6 +13,8 @@ import { InpaintingIcon } from "../icons/inpainting"; import { OpenSource } from "@/components/OpenSource/OpenSource"; import { EnhanceIcon } from "../icons/enhance"; import AppTooltip from "@/components/Tooltip/AppTooltip"; +import ButtonPrimary from "@/components/buttons/ButtonPrimary/ButtonPrimary"; +import Modal from "@/components/Modal/Modal"; import { ControlNetDescription, Img2ImgDescription, @@ -20,20 +24,223 @@ import { UpscalingDescription, } from "@/components/ImagineActionsDescription/ImagineActionsDescription"; import useWindowDimensions from "@/hooks/useWindowDimensions"; +import { Model } from "@/models/models"; import { MOBILE_SCREEN_WIDTH } from "@/utils/constants"; import styles from "./ImagineMenu.module.scss"; -interface LongItemProps { +const ImagineMenu = () => { + const router = useRouter(); + const { width } = useWindowDimensions(); + const { + models, + selectedModel, + activeLink, + setActiveLink, + findValidModelForFeature, + } = useModels(); + const imagineOptionPath = router.pathname.split("/").pop(); + const isMobile = width < MOBILE_SCREEN_WIDTH; + const [openItem, setOpenItem] = useState(); + const [showModelsModal, setShowModelsModal] = useState(false); + + useEffect(() => { + if (imagineOptionPath) { + if (!selectedModel.features.includes(imagineOptionPath)) { + const validModel = findValidModelForFeature( + imagineOptionPath as ModelFeature + ); + + setActiveLink({ + model: validModel, + feature: imagineOptionPath as ModelFeature, + }); + } else { + setActiveLink({ + model: selectedModel, + feature: imagineOptionPath as ModelFeature, + }); + } + } + }, []); + + const ModelsAccordion = models.map((model: Model) => ( + + + + )); + + return isMobile ? ( + + setShowModelsModal(true)} + loading={false} + className={styles.mobileButton} + /> + setShowModelsModal(!showModelsModal)} + > + {ModelsAccordion} + + + ) : ( +
+
+ +
+ +

Models

+ {ModelsAccordion} + + +
+ ); +}; + +interface ImagineMenuFeaturesProps { + model: Model; +} + +const ModelMenuFeatures = (props: ImagineMenuFeaturesProps) => { + const { activeLink } = useModels(); + + const getItemActive = (option: SDOption | string) => { + return ( + activeLink.model.source === props.model.source && + activeLink.feature === option + ); + }; + + const getIconColor = (option: SDOption | string) => { + return getItemActive(option) ? "#B3005E" : "#6D6D94"; + }; + + return ( + + {props.model.text2img && ( + } + active={getItemActive(SDOption.Text2Image)} + icon={ + + } + option={SDOption.Text2Image} + model={props.model} + /> + )} + {props.model.img2img && ( + } + active={getItemActive(SDOption.Image2Image)} + icon={ + + } + option={SDOption.Image2Image} + model={props.model} + /> + )} + {props.model.pix2pix && ( + } + active={getItemActive(SDOption.Pix2Pix)} + icon={ + + } + option={SDOption.Pix2Pix} + model={props.model} + /> + )} + {props.model.controlnet && ( + } + active={getItemActive(SDOption.ControlNet)} + icon={ + + } + option={SDOption.ControlNet} + model={props.model} + /> + )} + {props.model.inpainting && ( + } + active={getItemActive(SDOption.Inpainting)} + icon={ + + } + option={SDOption.Inpainting} + model={props.model} + /> + )} + {props.model.upscaling && ( + } + active={getItemActive(SDOption.Upscaling)} + icon={ + + } + option={SDOption.Upscaling} + model={props.model} + /> + )} + + ); +}; + +interface ImagineMenuItemProps { active?: boolean; icon: ReactNode; title: string; description: ReactNode; option: string; + model: Model; } -const ImagineMenuItem = (props: LongItemProps) => { +const ImagineMenuItem = (props: ImagineMenuItemProps) => { const router = useRouter(); const { width } = useWindowDimensions(); + const { setActiveLink } = useModels(); const isMobile = width < MOBILE_SCREEN_WIDTH; const getItemStyles = () => { @@ -41,100 +248,28 @@ const ImagineMenuItem = (props: LongItemProps) => { }; const handleOnClick = () => { - router.push(`/imagine/${props.option}`); + setActiveLink({ + model: props.model, + feature: props.option as ModelFeature, + }); + router.push(props.option); }; return (
{props.icon} -

+ {props.title} -

+
); }; -const ImagineMenu = () => { - const router = useRouter(); - const currentPath = router.pathname; - - const getItemActive = (option: SDOption | string) => { - const lastPath = currentPath.split("/").pop(); - return lastPath === option; - }; - - const getIconColor = (option: SDOption | string) => { - const isActive = getItemActive(option); - return isActive ? "#ffffff" : "#6D6D94"; - }; - - return ( -
-
- -
- -

Models

- - } - active={getItemActive(SDOption.Text2Image)} - icon={} - option={SDOption.Text2Image} - /> - } - active={getItemActive(SDOption.Image2Image)} - icon={} - option={SDOption.Image2Image} - /> - } - active={getItemActive(SDOption.Pix2Pix)} - icon={} - option={SDOption.Pix2Pix} - /> - } - active={getItemActive(SDOption.ControlNet)} - icon={} - option={SDOption.ControlNet} - /> - } - active={getItemActive(SDOption.Inpainting)} - icon={} - option={SDOption.Inpainting} - /> - } - active={getItemActive(SDOption.Upscaling)} - icon={ - - } - option={SDOption.Upscaling} - /> - - -
- ); -}; - export default ImagineMenu; diff --git a/morpheus-client/components/ImagineSettings/ImagineSettings.module.scss b/morpheus-client/components/ImagineSettings/ImagineSettings.module.scss index eb241bf6..ce401373 100644 --- a/morpheus-client/components/ImagineSettings/ImagineSettings.module.scss +++ b/morpheus-client/components/ImagineSettings/ImagineSettings.module.scss @@ -22,11 +22,11 @@ display: none; position: absolute; bottom: 28px; - left: 144px; - width: 300px; - max-width: 300px; + left: 50px; + width: 280px; + max-width: 280px; height: auto; - max-height: 200px; + max-height: 300px; padding: 8px; transform: translateX(-50%); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); diff --git a/morpheus-client/components/ImagineSettings/ImagineSettings.tsx b/morpheus-client/components/ImagineSettings/ImagineSettings.tsx index d99362fc..49bd3422 100644 --- a/morpheus-client/components/ImagineSettings/ImagineSettings.tsx +++ b/morpheus-client/components/ImagineSettings/ImagineSettings.tsx @@ -6,7 +6,8 @@ import InputNumber from "../Inputs/InputNumber/InputNumber"; import InputSeed from "../Inputs/InputSeed/InputSeed"; import InputSelect from "../Inputs/InputSelect/InputSelect"; import InputTextArea from "../Inputs/InputTextArea/InputTextArea"; -import ModelSelect from "../ModelSelect/ModelSelect"; +import InputEmbedding from "@/components/Inputs/InputEmbedding/InputEmbedding"; +import InputLora from "@/components/Inputs/InputLora/InputLora"; import SamplerSelect from "../SamplerSelect/SamplerSelect"; import AppTooltip from "@/components/Tooltip/AppTooltip"; import { CloseIcon } from "../icons/close"; @@ -14,8 +15,6 @@ import { InfoIcon } from "../icons/info"; import { SettingsIcon } from "../icons/settings"; import { useShowSettings } from "@/hooks/useShowSettings"; import styles from "./ImagineSettings.module.scss"; -import InputLora from "../Inputs/InputLora/InputLora"; -import InputEmbedding from "../Inputs/InputEmbedding/InputEmbedding"; interface OptionState { title: string; @@ -57,13 +56,51 @@ const ImagineSettings = () => { setAmount, negativePrompt, setNegativePrompt, + useLora, loraScale, - setLoraScale + setLoraScale, } = useDiffusion(); const SettingsContent = (
+
+ + +
+ +
+ + + +
+
{ />
-
- - -
- - {selectedOption === SDOption.ControlNet && ( -
- - -
- )} - -
- - -
-
{ />
+
+ + + +
+ {(selectedOption === SDOption.Image2Image || selectedOption === SDOption.ControlNet) && (
@@ -151,42 +182,6 @@ const ImagineSettings = () => {
)} -
- - - -
- -
- - - -
-
{
+
- {(selectedOption === SDOption.Text2Image || selectedOption === SDOption.Image2Image || - selectedOption === SDOption.ControlNet) && ( +

Model settings

+
+ {selectedOption === SDOption.ControlNet && (
- - +
)} - {(selectedOption === SDOption.Text2Image || selectedOption === SDOption.Image2Image || - selectedOption === SDOption.ControlNet) && ( +
+ + +
+ + {(selectedOption === SDOption.Text2Image || + selectedOption === SDOption.Image2Image || + selectedOption === SDOption.ControlNet) && (
- +
)} - {(selectedOption === SDOption.Text2Image || selectedOption === SDOption.Image2Image || - selectedOption === SDOption.ControlNet) && ( + {useLora.value && + (selectedOption === SDOption.Text2Image || + selectedOption === SDOption.Image2Image || + selectedOption === SDOption.ControlNet) && ( +
+ + + +
+ )} + + {(selectedOption === SDOption.Text2Image || + selectedOption === SDOption.Image2Image || + selectedOption === SDOption.ControlNet) && (
)} - -
- - -
); diff --git a/morpheus-client/components/MaskPaintingCanvas/MaskPaintingCanvas.tsx b/morpheus-client/components/MaskPaintingCanvas/MaskPaintingCanvas.tsx index 03a34c8e..861c6a10 100644 --- a/morpheus-client/components/MaskPaintingCanvas/MaskPaintingCanvas.tsx +++ b/morpheus-client/components/MaskPaintingCanvas/MaskPaintingCanvas.tsx @@ -20,7 +20,7 @@ type MaskedCanvasProps = { }; const MaskPaintingCanvas = (props: MaskedCanvasProps) => { - const { img2imgFile, maskFile, setMaskFile } = useImagine(); + const { img2imgFile, setImg2imgFile, maskFile, setMaskFile } = useImagine(); const imageCanvasRef = useRef(null); const drawingCanvasRef = useRef(null); @@ -162,17 +162,28 @@ const MaskPaintingCanvas = (props: MaskedCanvasProps) => { }; const handleCompleted = () => { - const canvas = drawingCanvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext("2d"); - if (!ctx) return; + const drawingCanvas = drawingCanvasRef.current; + const imageCanvas = imageCanvasRef.current; + if (!drawingCanvas || !imageCanvas) return; + + const drawingCtx = drawingCanvas.getContext("2d"); + if (!drawingCtx) return; - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const imageData = drawingCtx.getImageData( + 0, + 0, + drawingCanvas.width, + drawingCanvas.height + ); const invertedImageData = convertMaskToInpaintingMask(imageData); - ctx.putImageData(invertedImageData, 0, 0); - canvas.toBlob((blob) => { + drawingCtx.putImageData(invertedImageData, 0, 0); + drawingCanvas.toBlob((blob) => { if (!blob) return; setMaskFile(getFileFromBlob(blob, "mask.png")); + imageCanvas.toBlob((blob) => { + if (!blob) return; + setImg2imgFile(getFileFromBlob(blob, "image.png")); + }); props.closeModal(); }); }; diff --git a/morpheus-client/components/Navbar/Navbar.module.scss b/morpheus-client/components/Navbar/Navbar.module.scss index e1287576..8ffe0025 100644 --- a/morpheus-client/components/Navbar/Navbar.module.scss +++ b/morpheus-client/components/Navbar/Navbar.module.scss @@ -6,6 +6,7 @@ height: 80px; min-height: 80px; max-height: 80px; + min-width: 300px; width: 100%; padding: 0 32px; display: flex; diff --git a/morpheus-client/components/Navbar/Navbar.tsx b/morpheus-client/components/Navbar/Navbar.tsx index 6e71dda5..1dd44c69 100644 --- a/morpheus-client/components/Navbar/Navbar.tsx +++ b/morpheus-client/components/Navbar/Navbar.tsx @@ -116,22 +116,32 @@ const Navbar = (props: NavbarProps) => { return (
{isMobile ? ( - setShowMobileMenu(state.isOpen)} - > -
- setShowMobileMenu(false)} - /> -
-
+ + setShowMobileMenu(state.isOpen)} + > +
+ setShowMobileMenu(false)} + /> +
+
+ +
) : ( void; + children: ReactNode; +} + +export const Accordion = (props: AccordionProps) => { + const onToggle = () => { + props.setOpenedItem(props.itemId) + }; + + return ( +
+
+ {props.isOpen ? : } +

{props.title}

+
+ {props.isOpen && ( +
+ {props.children} +
+ )} +
+ ); +}; diff --git a/morpheus-client/components/icons/arrowDown.tsx b/morpheus-client/components/icons/arrowDown.tsx new file mode 100644 index 00000000..8b8b2086 --- /dev/null +++ b/morpheus-client/components/icons/arrowDown.tsx @@ -0,0 +1,17 @@ +export const ArrowDownIcon = () => ( + + + +); diff --git a/morpheus-client/components/icons/arrowRight.tsx b/morpheus-client/components/icons/arrowRight.tsx new file mode 100644 index 00000000..4f17e607 --- /dev/null +++ b/morpheus-client/components/icons/arrowRight.tsx @@ -0,0 +1,17 @@ +export const ArrowRightIcon = () => ( + + + +); diff --git a/morpheus-client/context/CNContext.tsx b/morpheus-client/context/CNContext.tsx index 84c69b73..a6c0e7ff 100644 --- a/morpheus-client/context/CNContext.tsx +++ b/morpheus-client/context/CNContext.tsx @@ -5,7 +5,7 @@ import { useEffect, useState, } from "react"; -import { getAvailableModels } from "../services/models"; +import { getAvailableModels } from "@/services/models"; import { useDiffusion } from "./SDContext"; export interface IControlNetContext { diff --git a/morpheus-client/context/ImagineContext.tsx b/morpheus-client/context/ImagineContext.tsx index 4e41be58..ef821b3e 100644 --- a/morpheus-client/context/ImagineContext.tsx +++ b/morpheus-client/context/ImagineContext.tsx @@ -81,6 +81,7 @@ const ImagineProvider = (props: { children: ReactNode }) => { ); useEffect(() => { + console.log("img2ImgURL", img2ImgURL) if (img2ImgURL) { getFileBlobFromURL(img2ImgURL) .then((file: File) => { diff --git a/morpheus-client/context/ModelsContext.tsx b/morpheus-client/context/ModelsContext.tsx new file mode 100644 index 00000000..c3db685d --- /dev/null +++ b/morpheus-client/context/ModelsContext.tsx @@ -0,0 +1,149 @@ +import { + createContext, + ReactNode, + useContext, + useEffect, + useState, +} from "react"; +import { getAvailableModels } from "@/services/models"; +import { getAvailableSamplers } from "@/services/samplers"; +import { Model } from "@/models/models"; + +export enum ModelFeature { + Empty = "", + Text2Image = "text2img", + Image2Image = "img2img", + Inpainting = "inpainting", + ControlNet = "controlnet", + Pix2Pix = "pix2pix", + Upscaling = "upscaling", +} + +export interface ActiveLink { + model: Model; + feature: ModelFeature; +} + +export interface IModelsContext { + models: Model[]; + selectedModel: Model; + setSelectedModel: (model: Model) => void; + samplers: any[]; + sampler: string; + setSampler: (sampler: string) => void; + activeLink: ActiveLink; + setActiveLink: (option: ActiveLink) => void; + findValidModelForFeature: (feature: ModelFeature) => Model; +} + +const initialConfig = { + model: undefined, + sampler: "PNDMScheduler", + initialFeatures: [ModelFeature.Text2Image, ModelFeature.Image2Image], +}; + +const defaultState = { + models: [], + selectedModel: {} as Model, + setSelectedModel: () => console.log("setSelectedSDModel"), + samplers: [], + sampler: initialConfig.sampler, + setSampler: () => console.log("setSampler"), + activeLink: {} as ActiveLink, + setActiveLink: () => console.log("setActiveLink"), + findValidModelForFeature: () => ({} as Model), +}; + +const mapModelBooleanFeaturesToStringFeatures = (model: Model) => { + const features = []; + if (model.text2img) features.push(ModelFeature.Text2Image); + if (model.img2img) features.push(ModelFeature.Image2Image); + if (model.inpainting) features.push(ModelFeature.Inpainting); + if (model.controlnet) features.push(ModelFeature.ControlNet); + if (model.pix2pix) features.push(ModelFeature.Pix2Pix); + if (model.upscaling) features.push(ModelFeature.Upscaling); + return features; +}; + +const ModelsContext = createContext(defaultState); + +const ModelsProvider = (props: { children: ReactNode }) => { + // Stable Diffusion Models + const [models, setModels] = useState([]); + const [selectedModel, setSelectedModel] = useState(models[0]); + + // Sampler settings + const [samplers, setSamplers] = useState([]); + const [sampler, setSampler] = useState(initialConfig.sampler); + + // Selected model and feature + const [activeLink, setActiveLink] = useState({} as ActiveLink); + + useEffect(() => { + getAvailableModels("/models").then((response) => { + if (response.success && response.data) { + if (response.data.length > 0) { + const modelsWithFeatures = response.data.map((model: Model) => { + return { + ...model, + features: mapModelBooleanFeaturesToStringFeatures(model), + }; + }); + setModels(modelsWithFeatures || []); + setActiveLink({ + model: modelsWithFeatures[0], + feature: modelsWithFeatures[0].features[0], + }); + } + } + }); + + getAvailableSamplers().then((response) => { + if (response.success && response.data) { + const samplersData = response.data || []; + setSamplers(samplersData); + } + }); + }, []); + + useEffect(() => { + if (activeLink.model) { + setSelectedModel(activeLink.model); + } + }, [activeLink]); + + const findValidModelForFeature = (feature: ModelFeature) => { + return ( + models.find((model: Model) => model.features.includes(feature)) || + models[0] + ); + }; + + return ( + + {props.children} + + ); +}; + +const useModels = () => { + const context = useContext(ModelsContext); + if (context === undefined) { + throw new Error("useModels must be used within a ModelsProvider"); + } + return context; +}; + +export { ModelsProvider, useModels }; diff --git a/morpheus-client/context/SDContext.tsx b/morpheus-client/context/SDContext.tsx index c43a3dff..5e7d8939 100644 --- a/morpheus-client/context/SDContext.tsx +++ b/morpheus-client/context/SDContext.tsx @@ -36,6 +36,10 @@ export enum SDOption { Upscaling = "upscaling", } +const DEFAULT_NEGATIVE_PROMPT = + "Bad proportions, cropped, bad anatomy, bad composition, bad proportions, bad shadow, blurred, blurry, " + + "colorless, deformed, dehydrated, disfigured, duplicate, error, gross proportions, low quality, worst quality"; + export interface IDiffusionContext { selectedOption: SDOption; validSDModels: any[]; @@ -76,21 +80,6 @@ export interface IDiffusionContext { restartSDSettings: () => void; } -const DEFAULT_NEGATIVE_PROMPT = - "(((deformed))), (extra_limb), (long body :1.3), (mutated hands and fingers:1.5), (mutation poorly drawn :1.2), " + - "(poorly drawn hands), (ugly), Images cut out at the top, anatomical nonsense, bad anatomy, bad anatomy, " + - "bad breasts, bad composition, bad ears, bad hands, bad proportions, bad shadow, blurred, blurry, blurry imag, " + - "bottom, broken legs, cloned face, colorless, cropped, deformed, deformed body feature, dehydrated, " + - "disappearing arms, disappearing calf, disappearing legs, disappearing thigh, disfigure, disfigured, " + - "duplicate, error, extra arms, extra breasts, extra ears, extra fingers, extra legs, extra limbs, " + - "fused ears, fused fingers, fused hand, gross proportions, heavy breasts, heavy ears, left, liquid body, " + - "liquid breasts, liquid ears, liquid tongue, long neck, low quality, low res, low resolution, lowers, " + - "malformed, malformed hands, malformed limbs, messy drawing, missing arms, missing breasts, missing ears, " + - "missing hand, missing legs, morbid, mutated, mutated body part, mutated hands, mutation, mutilated, " + - "old photo, out of frame, oversaturate, poor facial detail, poorly Rendered fac, poorly drawn fac, " + - "poorly drawn face, poorly drawn hand, poorly drawn hands, poorly rendered hand, right, signature, " + - "text font ui, too many fingers, ugly, uncoordinated body, unnatural body, username, watermark, worst quality"; - const PROMPTS = [ "An astronaut cycling on the moon, Abstract Expressionism", "A Man looking at the Starry Sky by Vincent Van Gogh", @@ -145,7 +134,7 @@ const initialConfig = { useEmbedding: initializeCheckbox(false), loraPath: initializeText(String("")), useLora: initializeCheckbox(false), - loraScale: initializeNumber(1.0) + loraScale: initializeNumber(1.0), }; const defaultState = { @@ -221,11 +210,17 @@ const DiffusionProvider = (props: { children: ReactNode }) => { const [randomizeSeed, setRandomizeSeed] = useState( initialConfig.randomizeSeed ); - const [embeddingPath, setEmbeddingPath] = useState(initialConfig.embeddingPath); - const [useEmbedding, setEmbedding] = useState(initialConfig.useEmbedding); + const [embeddingPath, setEmbeddingPath] = useState( + initialConfig.embeddingPath + ); + const [useEmbedding, setEmbedding] = useState( + initialConfig.useEmbedding + ); const [loraPath, setLoraPath] = useState(initialConfig.loraPath); const [useLora, setLora] = useState(initialConfig.useLora); - const [loraScale, setLoraScale] = useState(initialConfig.loraScale); + const [loraScale, setLoraScale] = useState( + initialConfig.loraScale + ); useEffect(() => { // Fetch Stable Diffusion models @@ -296,7 +291,7 @@ const DiffusionProvider = (props: { children: ReactNode }) => { lora_path: loraPath.value, use_embedding: useEmbedding.value, embedding_path: embeddingPath.value, - lora_scale: loraScale.value + lora_scale: loraScale.value, }; }; diff --git a/morpheus-client/excalidraw/css/app.scss b/morpheus-client/excalidraw/css/app.scss index 62e5d469..f73ce488 100644 --- a/morpheus-client/excalidraw/css/app.scss +++ b/morpheus-client/excalidraw/css/app.scss @@ -1,5 +1,9 @@ @import "open-color/open-color.scss"; +//* { +// outline: 1px solid #f00 !important; +//} + .visually-hidden { position: absolute !important; height: 1px; diff --git a/morpheus-client/models/models.ts b/morpheus-client/models/models.ts index 27c4d81d..9be562ae 100644 --- a/morpheus-client/models/models.ts +++ b/morpheus-client/models/models.ts @@ -60,7 +60,7 @@ export interface ArtWork { export interface Model { id?: string; name: string; - source?: string; + source: string; description?: string; is_active: boolean; url_docs?: string; @@ -69,6 +69,8 @@ export interface Model { inpainting?: boolean; controlnet?: boolean; pix2pix?: boolean; + upscaling?: boolean; + features: string[]; } export interface ControlNetModel extends Model { diff --git a/morpheus-client/pages/_app.tsx b/morpheus-client/pages/_app.tsx index fddb751d..023a39c4 100644 --- a/morpheus-client/pages/_app.tsx +++ b/morpheus-client/pages/_app.tsx @@ -15,6 +15,7 @@ import CookiesConsent from "@/components/CookiesConsent/CookiesConsent"; import "../App.scss"; import "../styles/globals.css"; import "../excalidraw/main.scss"; +import { ModelsProvider } from "@/context/ModelsContext"; const App: FC = ({ Component, pageProps }) => { return ( @@ -128,15 +129,17 @@ const App: FC = ({ Component, pageProps }) => { - - - - - - - - - + + + + + + + + + + + diff --git a/morpheus-client/utils/images.ts b/morpheus-client/utils/images.ts index 40b1979a..e9b62642 100644 --- a/morpheus-client/utils/images.ts +++ b/morpheus-client/utils/images.ts @@ -48,7 +48,7 @@ export const getFileFromBlob = (blob: Blob, filename: string) => { return new File([blob], filename, { type: "image/png" }); }; -export const convertMaskToInpaintingMask = (imageData: ImageData) => { +export const convertMaskToInpaintingMask = (imageData: any) => { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const r = data[i]; @@ -73,7 +73,7 @@ export const convertMaskToInpaintingMask = (imageData: ImageData) => { return imageData; }; -export const convertInpaintingMaskToMask = (imageData: ImageData) => { +export const convertInpaintingMaskToMask = (imageData: any) => { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { @@ -96,4 +96,4 @@ export const convertInpaintingMaskToMask = (imageData: ImageData) => { } return imageData; -}; +}; \ No newline at end of file diff --git a/morpheus-server/scripts/models/models-info.yaml b/morpheus-server/scripts/models/models-info.yaml index a5423e55..666c83fd 100644 --- a/morpheus-server/scripts/models/models-info.yaml +++ b/morpheus-server/scripts/models/models-info.yaml @@ -48,7 +48,7 @@ models: pix2pix: true upscaling: false - - name: "Stable Diffusion Inpaint" + - name: "SD Inpainting" description: "Modify an existing image with a text prompt and an image mask." source: "runwayml/stable-diffusion-inpainting" is_active: true @@ -60,7 +60,7 @@ models: pix2pix: false upscaling: false - - name: "Stable Diffusion x4 Upscaler" + - name: "SD x4 Upscaler" description: "Stable Diffusion x4 Upscaler can be used to enhance the resolution of input images by a factor of 4." source: "stabilityai/stable-diffusion-x4-upscaler" is_active: true