Переглянути джерело

Improve the Stability parameter control panel

licoy 1 рік тому
батько
коміт
bbbf59c74a

+ 3 - 0
app/components/button.tsx

@@ -1,6 +1,7 @@
 import * as React from "react";
 
 import styles from "./button.module.scss";
+import { CSSProperties } from "react";
 
 export type ButtonType = "primary" | "danger" | null;
 
@@ -16,6 +17,7 @@ export function IconButton(props: {
   disabled?: boolean;
   tabIndex?: number;
   autoFocus?: boolean;
+  style?: CSSProperties;
 }) {
   return (
     <button
@@ -31,6 +33,7 @@ export function IconButton(props: {
       role="button"
       tabIndex={props.tabIndex}
       autoFocus={props.autoFocus}
+      style={props.style}
     >
       {props.icon && (
         <div

+ 0 - 0
app/components/sd-list.module.scss


+ 0 - 3
app/components/sd-list.tsx

@@ -1,3 +0,0 @@
-export function SdList() {
-  return <div>sd-list</div>;
-}

+ 33 - 0
app/components/sd-panel.module.scss

@@ -0,0 +1,33 @@
+.ctrl-param-item {
+  display: flex;
+  justify-content: space-between;
+  min-height: 40px;
+  padding: 10px 0;
+  animation: slide-in ease 0.6s;
+  flex-direction: column;
+
+  .ctrl-param-item-header {
+    display: flex;
+    align-items: center;
+
+    .ctrl-param-item-title{
+      font-size: 14px;
+      font-weight: bolder;
+      margin-bottom: 5px;
+    }
+  }
+
+  .ctrl-param-item-sub-title {
+    font-size: 12px;
+    font-weight: normal;
+    margin-top: 3px;
+  }
+}
+
+.ai-models{
+  button{
+    margin-bottom: 10px;
+    padding:10px;
+    width: 100%;
+  }
+}

+ 220 - 0
app/components/sd-panel.tsx

@@ -0,0 +1,220 @@
+import styles from "./sd-panel.module.scss";
+import React, { useState } from "react";
+import { Select } from "@/app/components/ui-lib";
+import { IconButton } from "@/app/components/button";
+import locales from "@/app/locales";
+
+const sdCommonParams = (model: string, data: any) => {
+  return [
+    {
+      name: locales.SdPanel.Prompt,
+      value: "prompt",
+      type: "textarea",
+      placeholder: locales.SdPanel.PleaseInput(locales.SdPanel.Prompt),
+      required: true,
+    },
+    {
+      name: locales.SdPanel.NegativePrompt,
+      value: "negative_prompt",
+      type: "textarea",
+      placeholder: locales.SdPanel.PleaseInput(locales.SdPanel.NegativePrompt),
+    },
+    {
+      name: locales.SdPanel.AspectRatio,
+      value: "aspect_ratio",
+      type: "select",
+      default: "1:1",
+      options: [
+        { name: "1:1", value: "1:1" },
+        { name: "2:2", value: "2:2" },
+      ],
+    },
+    {
+      name: locales.SdPanel.ImageStyle,
+      value: "style",
+      type: "select",
+      default: "3d",
+      support: ["core"],
+      options: [{ name: "3D", value: "3d" }],
+    },
+    { name: "Seed", value: "seed", type: "number", default: 0 },
+    {
+      name: locales.SdPanel.OutFormat,
+      value: "output_format",
+      type: "select",
+      default: 0,
+      options: [
+        { name: "PNG", value: "png" },
+        { name: "JPEG", value: "jpeg" },
+        { name: "WebP", value: "webp" },
+      ],
+    },
+  ].filter((item) => {
+    return !(item.support && !item.support.includes(model));
+  });
+};
+
+const models = [
+  {
+    name: "Stable Image Ultra",
+    value: "ultra",
+    params: (data: any) => sdCommonParams("ultra", data),
+  },
+  {
+    name: "Stable Image Core",
+    value: "core",
+    params: (data: any) => sdCommonParams("core", data),
+  },
+  {
+    name: "Stable Diffusion 3",
+    value: "sd3",
+    params: (data: any) => {
+      return sdCommonParams("sd3", data);
+    },
+  },
+];
+
+export function ControlParamItem(props: {
+  title: string;
+  subTitle?: string;
+  children?: JSX.Element | JSX.Element[];
+  className?: string;
+}) {
+  return (
+    <div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
+      <div className={styles["ctrl-param-item-header"]}>
+        <div className={styles["ctrl-param-item-title"]}>
+          <div>{props.title}</div>
+        </div>
+      </div>
+      {props.children}
+      {props.subTitle && (
+        <div className={styles["ctrl-param-item-sub-title"]}>
+          {props.subTitle}
+        </div>
+      )}
+    </div>
+  );
+}
+
+export function ControlParam(props: {
+  columns: any[];
+  data: any;
+  set: React.Dispatch<React.SetStateAction<{}>>;
+}) {
+  const handleValueChange = (field: string, val: any) => {
+    props.set((prevParams) => ({
+      ...prevParams,
+      [field]: val,
+    }));
+  };
+
+  return (
+    <>
+      {props.columns.map((item) => {
+        let element: null | JSX.Element;
+        switch (item.type) {
+          case "textarea":
+            element = (
+              <ControlParamItem title={item.name} subTitle={item.sub}>
+                <textarea
+                  rows={item.rows || 3}
+                  style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
+                  placeholder={item.placeholder}
+                  onChange={(e) => {
+                    handleValueChange(item.value, e.currentTarget.value);
+                  }}
+                  value={props.data[item.value]}
+                ></textarea>
+              </ControlParamItem>
+            );
+            break;
+          case "select":
+            element = (
+              <ControlParamItem title={item.name} subTitle={item.sub}>
+                <Select
+                  value={props.data[item.value]}
+                  onChange={(e) => {
+                    handleValueChange(item.value, e.currentTarget.value);
+                  }}
+                >
+                  {item.options.map((opt: any) => {
+                    return (
+                      <option value={opt.value} key={opt.value}>
+                        {opt.name}
+                      </option>
+                    );
+                  })}
+                </Select>
+              </ControlParamItem>
+            );
+            break;
+          case "number":
+            element = (
+              <ControlParamItem title={item.name} subTitle={item.sub}>
+                <input
+                  type="number"
+                  value={props.data[item.value]}
+                  onChange={(e) => {
+                    handleValueChange(item.value, e.currentTarget.value);
+                  }}
+                />
+              </ControlParamItem>
+            );
+            break;
+          default:
+            element = (
+              <ControlParamItem title={item.name} subTitle={item.sub}>
+                <input
+                  type="text"
+                  value={props.data[item.value]}
+                  style={{ maxWidth: "100%", width: "100%" }}
+                  onChange={(e) => {
+                    handleValueChange(item.value, e.currentTarget.value);
+                  }}
+                />
+              </ControlParamItem>
+            );
+        }
+        return <div key={item.value}>{element}</div>;
+      })}
+    </>
+  );
+}
+
+export function SdPanel() {
+  const [currentModel, setCurrentModel] = useState(models[0]);
+  const [params, setParams] = useState({});
+  return (
+    <>
+      <ControlParamItem title={locales.SdPanel.AIModel}>
+        <div className={styles["ai-models"]}>
+          {models.map((item) => {
+            return (
+              <IconButton
+                text={item.name}
+                key={item.value}
+                type={currentModel.value == item.value ? "primary" : null}
+                shadow
+                onClick={() => {
+                  setCurrentModel(item);
+                }}
+              />
+            );
+          })}
+        </div>
+      </ControlParamItem>
+      <ControlParam
+        columns={currentModel.params(params) as any[]}
+        set={setParams}
+        data={params}
+      ></ControlParam>
+      <IconButton
+        text={locales.SdPanel.Submit}
+        type="primary"
+        style={{ marginTop: "20px" }}
+        shadow
+      ></IconButton>
+    </>
+  );
+}

+ 2 - 2
app/components/sidebar.tsx

@@ -37,7 +37,7 @@ const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
   loading: () => null,
 });
 
-const SdList = dynamic(async () => (await import("./sd-list")).SdList, {
+const SdPanel = dynamic(async () => (await import("./sd-panel")).SdPanel, {
   loading: () => null,
 });
 
@@ -155,7 +155,7 @@ export function SideBar(props: { className?: string }) {
   let isChat: boolean = false;
   switch (location.pathname) {
     case Path.Sd:
-      bodyComponent = <SdList />;
+      bodyComponent = <SdPanel />;
       break;
     default:
       isChat = true;

+ 13 - 0
app/components/ui-lib.module.scss

@@ -61,6 +61,19 @@
       font-weight: normal;
     }
   }
+
+  &.vertical{
+    flex-direction: column;
+    align-items: start;
+    .list-header{
+      .list-item-title{
+        margin-bottom: 5px;
+      }
+      .list-item-sub-title{
+        margin-bottom: 2px;
+      }
+    }
+  }
 }
 
 .list {

+ 6 - 1
app/components/ui-lib.tsx

@@ -48,10 +48,15 @@ export function ListItem(props: {
   icon?: JSX.Element;
   className?: string;
   onClick?: (event: MouseEvent) => void;
+  vertical?: boolean;
 }) {
   return (
     <div
-      className={styles["list-item"] + ` ${props.className || ""}`}
+      className={
+        styles["list-item"] +
+        ` ${props.vertical ? styles["vertical"] : ""} ` +
+        ` ${props.className || ""}`
+      }
       onClick={props.onClick}
     >
       <div className={styles["list-header"]}>

+ 10 - 0
app/locales/cn.ts

@@ -484,6 +484,16 @@ const cn = {
     Topic: "主题",
     Time: "时间",
   },
+  SdPanel: {
+    Prompt: "画面提示",
+    NegativePrompt: "否定提示",
+    PleaseInput: (name: string) => `请输入${name}`,
+    AspectRatio: "横纵比",
+    ImageStyle: "图像风格",
+    OutFormat: "输出格式",
+    AIModel: "AI模型",
+    Submit: "提交生成",
+  },
 };
 
 type DeepPartial<T> = T extends object

+ 10 - 1
app/locales/en.ts

@@ -486,11 +486,20 @@ const en: LocaleType = {
     Topic: "Topic",
     Time: "Time",
   },
-
   URLCommand: {
     Code: "Detected access code from url, confirm to apply? ",
     Settings: "Detected settings from url, confirm to apply?",
   },
+  SdPanel: {
+    Prompt: "Prompt",
+    NegativePrompt: "Negative Prompt",
+    PleaseInput: (name: string) => `Please input ${name}`,
+    AspectRatio: "Aspect Ratio",
+    ImageStyle: "Image Style",
+    OutFormat: "Output Format",
+    AIModel: "AI Model",
+    Submit: "Submit",
+  },
 };
 
 export default en;

+ 2 - 1
app/styles/globals.scss

@@ -226,7 +226,8 @@ input[type="range"]::-ms-thumb:hover {
 
 input[type="number"],
 input[type="text"],
-input[type="password"] {
+input[type="password"],
+textarea {
   appearance: none;
   border-radius: 10px;
   border: var(--border-in-light);