sd-panel.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import styles from "./sd-panel.module.scss";
  2. import React, { useState } from "react";
  3. import { Select, showToast } from "@/app/components/ui-lib";
  4. import { IconButton } from "@/app/components/button";
  5. import Locale from "@/app/locales";
  6. import { nanoid } from "nanoid";
  7. import { StoreKey } from "@/app/constant";
  8. import { useSdStore } from "@/app/store/sd";
  9. const sdCommonParams = (model: string, data: any) => {
  10. return [
  11. {
  12. name: Locale.SdPanel.Prompt,
  13. value: "prompt",
  14. type: "textarea",
  15. placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
  16. required: true,
  17. },
  18. {
  19. name: Locale.SdPanel.ModelVersion,
  20. value: "model",
  21. type: "select",
  22. default: "sd3-medium",
  23. support: ["sd3"],
  24. options: [
  25. { name: "SD3 Medium", value: "sd3-medium" },
  26. { name: "SD3 Large", value: "sd3-large" },
  27. { name: "SD3 Large Turbo", value: "sd3-large-turbo" },
  28. ],
  29. },
  30. {
  31. name: Locale.SdPanel.NegativePrompt,
  32. value: "negative_prompt",
  33. type: "textarea",
  34. placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
  35. },
  36. {
  37. name: Locale.SdPanel.AspectRatio,
  38. value: "aspect_ratio",
  39. type: "select",
  40. default: "1:1",
  41. options: [
  42. { name: "1:1", value: "1:1" },
  43. { name: "16:9", value: "16:9" },
  44. { name: "21:9", value: "21:9" },
  45. { name: "2:3", value: "2:3" },
  46. { name: "3:2", value: "3:2" },
  47. { name: "4:5", value: "4:5" },
  48. { name: "5:4", value: "5:4" },
  49. { name: "9:16", value: "9:16" },
  50. { name: "9:21", value: "9:21" },
  51. ],
  52. },
  53. {
  54. name: Locale.SdPanel.ImageStyle,
  55. value: "style",
  56. type: "select",
  57. default: "3d",
  58. support: ["core"],
  59. options: [
  60. { name: Locale.SdPanel.Styles.D3Model, value: "3d-model" },
  61. { name: Locale.SdPanel.Styles.AnalogFilm, value: "analog-film" },
  62. { name: Locale.SdPanel.Styles.Anime, value: "anime" },
  63. { name: Locale.SdPanel.Styles.Cinematic, value: "cinematic" },
  64. { name: Locale.SdPanel.Styles.ComicBook, value: "comic-book" },
  65. { name: Locale.SdPanel.Styles.DigitalArt, value: "digital-art" },
  66. { name: Locale.SdPanel.Styles.Enhance, value: "enhance" },
  67. { name: Locale.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
  68. { name: Locale.SdPanel.Styles.Isometric, value: "isometric" },
  69. { name: Locale.SdPanel.Styles.LineArt, value: "line-art" },
  70. { name: Locale.SdPanel.Styles.LowPoly, value: "low-poly" },
  71. {
  72. name: Locale.SdPanel.Styles.ModelingCompound,
  73. value: "modeling-compound",
  74. },
  75. { name: Locale.SdPanel.Styles.NeonPunk, value: "neon-punk" },
  76. { name: Locale.SdPanel.Styles.Origami, value: "origami" },
  77. { name: Locale.SdPanel.Styles.Photographic, value: "photographic" },
  78. { name: Locale.SdPanel.Styles.PixelArt, value: "pixel-art" },
  79. { name: Locale.SdPanel.Styles.TileTexture, value: "tile-texture" },
  80. ],
  81. },
  82. {
  83. name: "Seed",
  84. value: "seed",
  85. type: "number",
  86. default: 0,
  87. min: 0,
  88. max: 4294967294,
  89. },
  90. {
  91. name: Locale.SdPanel.OutFormat,
  92. value: "output_format",
  93. type: "select",
  94. default: "png",
  95. options: [
  96. { name: "PNG", value: "png" },
  97. { name: "JPEG", value: "jpeg" },
  98. { name: "WebP", value: "webp" },
  99. ],
  100. },
  101. ].filter((item) => {
  102. return !(item.support && !item.support.includes(model));
  103. });
  104. };
  105. 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={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. 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. type="number"
  216. min={item.min}
  217. max={item.max}
  218. value={props.data[item.value] || 0}
  219. onChange={(e) => {
  220. props.onChange(item.value, parseInt(e.currentTarget.value));
  221. }}
  222. />
  223. </ControlParamItem>
  224. );
  225. break;
  226. default:
  227. element = (
  228. <ControlParamItem
  229. title={item.name}
  230. subTitle={item.sub}
  231. required={item.required}
  232. >
  233. <input
  234. type="text"
  235. value={props.data[item.value]}
  236. style={{ maxWidth: "100%", width: "100%" }}
  237. onChange={(e) => {
  238. props.onChange(item.value, e.currentTarget.value);
  239. }}
  240. />
  241. </ControlParamItem>
  242. );
  243. }
  244. return <div key={item.value}>{element}</div>;
  245. })}
  246. </>
  247. );
  248. }
  249. const getModelParamBasicData = (
  250. columns: any[],
  251. data: any,
  252. clearText?: boolean,
  253. ) => {
  254. const newParams: any = {};
  255. columns.forEach((item: any) => {
  256. if (clearText && ["text", "textarea", "number"].includes(item.type)) {
  257. newParams[item.value] = item.default || "";
  258. } else {
  259. // @ts-ignore
  260. newParams[item.value] = data[item.value] || item.default || "";
  261. }
  262. });
  263. return newParams;
  264. };
  265. export function SdPanel() {
  266. const [currentModel, setCurrentModel] = useState(models[0]);
  267. const [params, setParams] = useState(
  268. getModelParamBasicData(currentModel.params({}), {}),
  269. );
  270. const handleValueChange = (field: string, val: any) => {
  271. setParams((prevParams: any) => ({
  272. ...prevParams,
  273. [field]: val,
  274. }));
  275. };
  276. const handleModelChange = (model: any) => {
  277. setCurrentModel(model);
  278. setParams(getModelParamBasicData(model.params({}), params));
  279. };
  280. const sdStore = useSdStore();
  281. const handleSubmit = () => {
  282. const columns = currentModel.params(params);
  283. const reqParams: any = {};
  284. for (let i = 0; i < columns.length; i++) {
  285. const item = columns[i];
  286. reqParams[item.value] = params[item.value] ?? null;
  287. if (item.required) {
  288. if (!reqParams[item.value]) {
  289. showToast(Locale.SdPanel.ParamIsRequired(item.name));
  290. return;
  291. }
  292. }
  293. }
  294. let data: any = {
  295. model: currentModel.value,
  296. model_name: currentModel.name,
  297. status: "wait",
  298. params: reqParams,
  299. created_at: new Date().toLocaleString(),
  300. img_data: "",
  301. };
  302. sdStore.sendTask(data, () => {
  303. setParams(getModelParamBasicData(columns, params, true));
  304. });
  305. };
  306. return (
  307. <>
  308. <ControlParamItem title={Locale.SdPanel.AIModel}>
  309. <div className={styles["ai-models"]}>
  310. {models.map((item) => {
  311. return (
  312. <IconButton
  313. text={item.name}
  314. key={item.value}
  315. type={currentModel.value == item.value ? "primary" : null}
  316. shadow
  317. onClick={() => handleModelChange(item)}
  318. />
  319. );
  320. })}
  321. </div>
  322. </ControlParamItem>
  323. <ControlParam
  324. columns={currentModel.params(params) as any[]}
  325. data={params}
  326. onChange={handleValueChange}
  327. ></ControlParam>
  328. <IconButton
  329. text={Locale.SdPanel.Submit}
  330. type="primary"
  331. style={{ marginTop: "20px" }}
  332. shadow
  333. onClick={handleSubmit}
  334. ></IconButton>
  335. </>
  336. );
  337. }