Improve the Stability parameter control panel

This commit is contained in:
licoy 2024-07-02 10:24:19 +08:00
parent 34034be0e3
commit bbbf59c74a
11 changed files with 299 additions and 8 deletions

View File

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

View File

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

View File

@ -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
app/components/sd-panel.tsx Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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