You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			322 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			322 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			TypeScript
		
	
| import styles from "./sd-panel.module.scss";
 | |
| import React from "react";
 | |
| import { Select } from "@/app/components/ui-lib";
 | |
| import { IconButton } from "@/app/components/button";
 | |
| import Locale from "@/app/locales";
 | |
| import { useSdStore } from "@/app/store/sd";
 | |
| import clsx from "clsx";
 | |
| 
 | |
| export const params = [
 | |
|   {
 | |
|     name: Locale.SdPanel.Prompt,
 | |
|     value: "prompt",
 | |
|     type: "textarea",
 | |
|     placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
 | |
|     required: true,
 | |
|   },
 | |
|   {
 | |
|     name: Locale.SdPanel.ModelVersion,
 | |
|     value: "model",
 | |
|     type: "select",
 | |
|     default: "sd3-medium",
 | |
|     support: ["sd3"],
 | |
|     options: [
 | |
|       { name: "SD3 Medium", value: "sd3-medium" },
 | |
|       { name: "SD3 Large", value: "sd3-large" },
 | |
|       { name: "SD3 Large Turbo", value: "sd3-large-turbo" },
 | |
|     ],
 | |
|   },
 | |
|   {
 | |
|     name: Locale.SdPanel.NegativePrompt,
 | |
|     value: "negative_prompt",
 | |
|     type: "textarea",
 | |
|     placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
 | |
|   },
 | |
|   {
 | |
|     name: Locale.SdPanel.AspectRatio,
 | |
|     value: "aspect_ratio",
 | |
|     type: "select",
 | |
|     default: "1:1",
 | |
|     options: [
 | |
|       { name: "1:1", value: "1:1" },
 | |
|       { name: "16:9", value: "16:9" },
 | |
|       { name: "21:9", value: "21:9" },
 | |
|       { name: "2:3", value: "2:3" },
 | |
|       { name: "3:2", value: "3:2" },
 | |
|       { name: "4:5", value: "4:5" },
 | |
|       { name: "5:4", value: "5:4" },
 | |
|       { name: "9:16", value: "9:16" },
 | |
|       { name: "9:21", value: "9:21" },
 | |
|     ],
 | |
|   },
 | |
|   {
 | |
|     name: Locale.SdPanel.ImageStyle,
 | |
|     value: "style",
 | |
|     type: "select",
 | |
|     default: "3d-model",
 | |
|     support: ["core"],
 | |
|     options: [
 | |
|       { name: Locale.SdPanel.Styles.D3Model, value: "3d-model" },
 | |
|       { name: Locale.SdPanel.Styles.AnalogFilm, value: "analog-film" },
 | |
|       { name: Locale.SdPanel.Styles.Anime, value: "anime" },
 | |
|       { name: Locale.SdPanel.Styles.Cinematic, value: "cinematic" },
 | |
|       { name: Locale.SdPanel.Styles.ComicBook, value: "comic-book" },
 | |
|       { name: Locale.SdPanel.Styles.DigitalArt, value: "digital-art" },
 | |
|       { name: Locale.SdPanel.Styles.Enhance, value: "enhance" },
 | |
|       { name: Locale.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
 | |
|       { name: Locale.SdPanel.Styles.Isometric, value: "isometric" },
 | |
|       { name: Locale.SdPanel.Styles.LineArt, value: "line-art" },
 | |
|       { name: Locale.SdPanel.Styles.LowPoly, value: "low-poly" },
 | |
|       {
 | |
|         name: Locale.SdPanel.Styles.ModelingCompound,
 | |
|         value: "modeling-compound",
 | |
|       },
 | |
|       { name: Locale.SdPanel.Styles.NeonPunk, value: "neon-punk" },
 | |
|       { name: Locale.SdPanel.Styles.Origami, value: "origami" },
 | |
|       { name: Locale.SdPanel.Styles.Photographic, value: "photographic" },
 | |
|       { name: Locale.SdPanel.Styles.PixelArt, value: "pixel-art" },
 | |
|       { name: Locale.SdPanel.Styles.TileTexture, value: "tile-texture" },
 | |
|     ],
 | |
|   },
 | |
|   {
 | |
|     name: "Seed",
 | |
|     value: "seed",
 | |
|     type: "number",
 | |
|     default: 0,
 | |
|     min: 0,
 | |
|     max: 4294967294,
 | |
|   },
 | |
|   {
 | |
|     name: Locale.SdPanel.OutFormat,
 | |
|     value: "output_format",
 | |
|     type: "select",
 | |
|     default: "png",
 | |
|     options: [
 | |
|       { name: "PNG", value: "png" },
 | |
|       { name: "JPEG", value: "jpeg" },
 | |
|       { name: "WebP", value: "webp" },
 | |
|     ],
 | |
|   },
 | |
| ];
 | |
| 
 | |
| const sdCommonParams = (model: string, data: any) => {
 | |
|   return params.filter((item) => {
 | |
|     return !(item.support && !item.support.includes(model));
 | |
|   });
 | |
| };
 | |
| 
 | |
| export 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).filter((item) => {
 | |
|         return !(
 | |
|           data.model === "sd3-large-turbo" && item.value == "negative_prompt"
 | |
|         );
 | |
|       });
 | |
|     },
 | |
|   },
 | |
| ];
 | |
| 
 | |
| export function ControlParamItem(props: {
 | |
|   title: string;
 | |
|   subTitle?: string;
 | |
|   required?: boolean;
 | |
|   children?: JSX.Element | JSX.Element[];
 | |
|   className?: string;
 | |
| }) {
 | |
|   return (
 | |
|     <div className={clsx(styles["ctrl-param-item"], props.className)}>
 | |
|       <div className={styles["ctrl-param-item-header"]}>
 | |
|         <div className={styles["ctrl-param-item-title"]}>
 | |
|           <div>
 | |
|             {props.title}
 | |
|             {props.required && <span style={{ color: "red" }}>*</span>}
 | |
|           </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;
 | |
|   onChange: (field: string, val: any) => void;
 | |
| }) {
 | |
|   return (
 | |
|     <>
 | |
|       {props.columns?.map((item) => {
 | |
|         let element: null | JSX.Element;
 | |
|         switch (item.type) {
 | |
|           case "textarea":
 | |
|             element = (
 | |
|               <ControlParamItem
 | |
|                 title={item.name}
 | |
|                 subTitle={item.sub}
 | |
|                 required={item.required}
 | |
|               >
 | |
|                 <textarea
 | |
|                   rows={item.rows || 3}
 | |
|                   style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
 | |
|                   placeholder={item.placeholder}
 | |
|                   onChange={(e) => {
 | |
|                     props.onChange(item.value, e.currentTarget.value);
 | |
|                   }}
 | |
|                   value={props.data[item.value]}
 | |
|                 ></textarea>
 | |
|               </ControlParamItem>
 | |
|             );
 | |
|             break;
 | |
|           case "select":
 | |
|             element = (
 | |
|               <ControlParamItem
 | |
|                 title={item.name}
 | |
|                 subTitle={item.sub}
 | |
|                 required={item.required}
 | |
|               >
 | |
|                 <Select
 | |
|                   aria-label={item.name}
 | |
|                   value={props.data[item.value]}
 | |
|                   onChange={(e) => {
 | |
|                     props.onChange(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}
 | |
|                 required={item.required}
 | |
|               >
 | |
|                 <input
 | |
|                   aria-label={item.name}
 | |
|                   type="number"
 | |
|                   min={item.min}
 | |
|                   max={item.max}
 | |
|                   value={props.data[item.value] || 0}
 | |
|                   onChange={(e) => {
 | |
|                     props.onChange(item.value, parseInt(e.currentTarget.value));
 | |
|                   }}
 | |
|                 />
 | |
|               </ControlParamItem>
 | |
|             );
 | |
|             break;
 | |
|           default:
 | |
|             element = (
 | |
|               <ControlParamItem
 | |
|                 title={item.name}
 | |
|                 subTitle={item.sub}
 | |
|                 required={item.required}
 | |
|               >
 | |
|                 <input
 | |
|                   aria-label={item.name}
 | |
|                   type="text"
 | |
|                   value={props.data[item.value]}
 | |
|                   style={{ maxWidth: "100%", width: "100%" }}
 | |
|                   onChange={(e) => {
 | |
|                     props.onChange(item.value, e.currentTarget.value);
 | |
|                   }}
 | |
|                 />
 | |
|               </ControlParamItem>
 | |
|             );
 | |
|         }
 | |
|         return <div key={item.value}>{element}</div>;
 | |
|       })}
 | |
|     </>
 | |
|   );
 | |
| }
 | |
| 
 | |
| export const getModelParamBasicData = (
 | |
|   columns: any[],
 | |
|   data: any,
 | |
|   clearText?: boolean,
 | |
| ) => {
 | |
|   const newParams: any = {};
 | |
|   columns.forEach((item: any) => {
 | |
|     if (clearText && ["text", "textarea", "number"].includes(item.type)) {
 | |
|       newParams[item.value] = item.default || "";
 | |
|     } else {
 | |
|       // @ts-ignore
 | |
|       newParams[item.value] = data[item.value] || item.default || "";
 | |
|     }
 | |
|   });
 | |
|   return newParams;
 | |
| };
 | |
| 
 | |
| export const getParams = (model: any, params: any) => {
 | |
|   return models.find((m) => m.value === model.value)?.params(params) || [];
 | |
| };
 | |
| 
 | |
| export function SdPanel() {
 | |
|   const sdStore = useSdStore();
 | |
|   const currentModel = sdStore.currentModel;
 | |
|   const setCurrentModel = sdStore.setCurrentModel;
 | |
|   const params = sdStore.currentParams;
 | |
|   const setParams = sdStore.setCurrentParams;
 | |
| 
 | |
|   const handleValueChange = (field: string, val: any) => {
 | |
|     setParams({
 | |
|       ...params,
 | |
|       [field]: val,
 | |
|     });
 | |
|   };
 | |
|   const handleModelChange = (model: any) => {
 | |
|     setCurrentModel(model);
 | |
|     setParams(getModelParamBasicData(model.params({}), params));
 | |
|   };
 | |
| 
 | |
|   return (
 | |
|     <>
 | |
|       <ControlParamItem title={Locale.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={() => handleModelChange(item)}
 | |
|               />
 | |
|             );
 | |
|           })}
 | |
|         </div>
 | |
|       </ControlParamItem>
 | |
|       <ControlParam
 | |
|         columns={getParams?.(currentModel, params) as any[]}
 | |
|         data={params}
 | |
|         onChange={handleValueChange}
 | |
|       ></ControlParam>
 | |
|     </>
 | |
|   );
 | |
| }
 |