sd-panel.tsx 9.2 KB

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