sd-panel.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. import styles from "./sd-panel.module.scss";
  2. import React from "react";
  3. import { Select } from "@/app/components/ui-lib";
  4. import { IconButton } from "@/app/components/button";
  5. import Locale from "@/app/locales";
  6. import { useSdStore } from "@/app/store/sd";
  7. export const params = [
  8. {
  9. name: Locale.SdPanel.Prompt,
  10. value: "prompt",
  11. type: "textarea",
  12. placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
  13. required: true,
  14. },
  15. {
  16. name: Locale.SdPanel.ModelVersion,
  17. value: "model",
  18. type: "select",
  19. default: "sd3-medium",
  20. support: ["sd3"],
  21. options: [
  22. { name: "SD3 Medium", value: "sd3-medium" },
  23. { name: "SD3 Large", value: "sd3-large" },
  24. { name: "SD3 Large Turbo", value: "sd3-large-turbo" },
  25. ],
  26. },
  27. {
  28. name: Locale.SdPanel.NegativePrompt,
  29. value: "negative_prompt",
  30. type: "textarea",
  31. placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
  32. },
  33. {
  34. name: Locale.SdPanel.AspectRatio,
  35. value: "aspect_ratio",
  36. type: "select",
  37. default: "1:1",
  38. options: [
  39. { name: "1:1", value: "1:1" },
  40. { name: "16:9", value: "16:9" },
  41. { name: "21:9", value: "21:9" },
  42. { name: "2:3", value: "2:3" },
  43. { name: "3:2", value: "3:2" },
  44. { name: "4:5", value: "4:5" },
  45. { name: "5:4", value: "5:4" },
  46. { name: "9:16", value: "9:16" },
  47. { name: "9:21", value: "9:21" },
  48. ],
  49. },
  50. {
  51. name: Locale.SdPanel.ImageStyle,
  52. value: "style",
  53. type: "select",
  54. default: "3d-model",
  55. support: ["core"],
  56. options: [
  57. { name: Locale.SdPanel.Styles.D3Model, value: "3d-model" },
  58. { name: Locale.SdPanel.Styles.AnalogFilm, value: "analog-film" },
  59. { name: Locale.SdPanel.Styles.Anime, value: "anime" },
  60. { name: Locale.SdPanel.Styles.Cinematic, value: "cinematic" },
  61. { name: Locale.SdPanel.Styles.ComicBook, value: "comic-book" },
  62. { name: Locale.SdPanel.Styles.DigitalArt, value: "digital-art" },
  63. { name: Locale.SdPanel.Styles.Enhance, value: "enhance" },
  64. { name: Locale.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
  65. { name: Locale.SdPanel.Styles.Isometric, value: "isometric" },
  66. { name: Locale.SdPanel.Styles.LineArt, value: "line-art" },
  67. { name: Locale.SdPanel.Styles.LowPoly, value: "low-poly" },
  68. {
  69. name: Locale.SdPanel.Styles.ModelingCompound,
  70. value: "modeling-compound",
  71. },
  72. { name: Locale.SdPanel.Styles.NeonPunk, value: "neon-punk" },
  73. { name: Locale.SdPanel.Styles.Origami, value: "origami" },
  74. { name: Locale.SdPanel.Styles.Photographic, value: "photographic" },
  75. { name: Locale.SdPanel.Styles.PixelArt, value: "pixel-art" },
  76. { name: Locale.SdPanel.Styles.TileTexture, value: "tile-texture" },
  77. ],
  78. },
  79. {
  80. name: "Seed",
  81. value: "seed",
  82. type: "number",
  83. default: 0,
  84. min: 0,
  85. max: 4294967294,
  86. },
  87. {
  88. name: Locale.SdPanel.OutFormat,
  89. value: "output_format",
  90. type: "select",
  91. default: "png",
  92. options: [
  93. { name: "PNG", value: "png" },
  94. { name: "JPEG", value: "jpeg" },
  95. { name: "WebP", value: "webp" },
  96. ],
  97. },
  98. ];
  99. const sdCommonParams = (model: string, data: any) => {
  100. return params.filter((item) => {
  101. return !(item.support && !item.support.includes(model));
  102. });
  103. };
  104. export const models = [
  105. {
  106. name: "Stable Image Ultra",
  107. value: "ultra",
  108. params: (data: any) => sdCommonParams("ultra", data),
  109. },
  110. {
  111. name: "Stable Image Core",
  112. value: "core",
  113. params: (data: any) => sdCommonParams("core", data),
  114. },
  115. {
  116. name: "Stable Diffusion 3",
  117. value: "sd3",
  118. params: (data: any) => {
  119. return sdCommonParams("sd3", data).filter((item) => {
  120. return !(
  121. data.model === "sd3-large-turbo" && item.value == "negative_prompt"
  122. );
  123. });
  124. },
  125. },
  126. ];
  127. export function ControlParamItem(props: {
  128. title: string;
  129. subTitle?: string;
  130. required?: boolean;
  131. children?: JSX.Element | JSX.Element[];
  132. className?: string;
  133. }) {
  134. return (
  135. <div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
  136. <div className={styles["ctrl-param-item-header"]}>
  137. <div className={styles["ctrl-param-item-title"]}>
  138. <div>
  139. {props.title}
  140. {props.required && <span style={{ color: "red" }}>*</span>}
  141. </div>
  142. </div>
  143. </div>
  144. {props.children}
  145. {props.subTitle && (
  146. <div className={styles["ctrl-param-item-sub-title"]}>
  147. {props.subTitle}
  148. </div>
  149. )}
  150. </div>
  151. );
  152. }
  153. export function ControlParam(props: {
  154. columns: any[];
  155. data: any;
  156. onChange: (field: string, val: any) => void;
  157. }) {
  158. return (
  159. <>
  160. {props.columns?.map((item) => {
  161. let element: null | JSX.Element;
  162. switch (item.type) {
  163. case "textarea":
  164. element = (
  165. <ControlParamItem
  166. title={item.name}
  167. subTitle={item.sub}
  168. required={item.required}
  169. >
  170. <textarea
  171. rows={item.rows || 3}
  172. style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
  173. placeholder={item.placeholder}
  174. onChange={(e) => {
  175. props.onChange(item.value, e.currentTarget.value);
  176. }}
  177. value={props.data[item.value]}
  178. ></textarea>
  179. </ControlParamItem>
  180. );
  181. break;
  182. case "select":
  183. element = (
  184. <ControlParamItem
  185. title={item.name}
  186. subTitle={item.sub}
  187. required={item.required}
  188. >
  189. <Select
  190. aria-label={item.name}
  191. value={props.data[item.value]}
  192. onChange={(e) => {
  193. props.onChange(item.value, e.currentTarget.value);
  194. }}
  195. >
  196. {item.options.map((opt: any) => {
  197. return (
  198. <option value={opt.value} key={opt.value}>
  199. {opt.name}
  200. </option>
  201. );
  202. })}
  203. </Select>
  204. </ControlParamItem>
  205. );
  206. break;
  207. case "number":
  208. element = (
  209. <ControlParamItem
  210. title={item.name}
  211. subTitle={item.sub}
  212. required={item.required}
  213. >
  214. <input
  215. aria-label={item.name}
  216. type="number"
  217. min={item.min}
  218. max={item.max}
  219. value={props.data[item.value] || 0}
  220. onChange={(e) => {
  221. props.onChange(item.value, parseInt(e.currentTarget.value));
  222. }}
  223. />
  224. </ControlParamItem>
  225. );
  226. break;
  227. default:
  228. element = (
  229. <ControlParamItem
  230. title={item.name}
  231. subTitle={item.sub}
  232. required={item.required}
  233. >
  234. <input
  235. aria-label={item.name}
  236. type="text"
  237. value={props.data[item.value]}
  238. style={{ maxWidth: "100%", width: "100%" }}
  239. onChange={(e) => {
  240. props.onChange(item.value, e.currentTarget.value);
  241. }}
  242. />
  243. </ControlParamItem>
  244. );
  245. }
  246. return <div key={item.value}>{element}</div>;
  247. })}
  248. </>
  249. );
  250. }
  251. export const getModelParamBasicData = (
  252. columns: any[],
  253. data: any,
  254. clearText?: boolean,
  255. ) => {
  256. const newParams: any = {};
  257. columns.forEach((item: any) => {
  258. if (clearText && ["text", "textarea", "number"].includes(item.type)) {
  259. newParams[item.value] = item.default || "";
  260. } else {
  261. // @ts-ignore
  262. newParams[item.value] = data[item.value] || item.default || "";
  263. }
  264. });
  265. return newParams;
  266. };
  267. export const getParams = (model: any, params: any) => {
  268. return models.find((m) => m.value === model.value)?.params(params) || [];
  269. };
  270. export function SdPanel() {
  271. const sdStore = useSdStore();
  272. const currentModel = sdStore.currentModel;
  273. const setCurrentModel = sdStore.setCurrentModel;
  274. const params = sdStore.currentParams;
  275. const setParams = sdStore.setCurrentParams;
  276. const handleValueChange = (field: string, val: any) => {
  277. setParams({
  278. ...params,
  279. [field]: val,
  280. });
  281. };
  282. const handleModelChange = (model: any) => {
  283. setCurrentModel(model);
  284. setParams(getModelParamBasicData(model.params({}), params));
  285. };
  286. return (
  287. <>
  288. <ControlParamItem title={Locale.SdPanel.AIModel}>
  289. <div className={styles["ai-models"]}>
  290. {models.map((item) => {
  291. return (
  292. <IconButton
  293. text={item.name}
  294. key={item.value}
  295. type={currentModel.value == item.value ? "primary" : null}
  296. shadow
  297. onClick={() => handleModelChange(item)}
  298. />
  299. );
  300. })}
  301. </div>
  302. </ControlParamItem>
  303. <ControlParam
  304. columns={getParams?.(currentModel, params) as any[]}
  305. data={params}
  306. onChange={handleValueChange}
  307. ></ControlParam>
  308. </>
  309. );
  310. }