sd-panel.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  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 locales from "@/app/locales";
  6. import { nanoid } from "nanoid";
  7. import { useIndexedDB } from "react-indexed-db-hook";
  8. import { StoreKey } from "@/app/constant";
  9. import { SdDbInit, sendSdTask, useSdStore } from "@/app/store/sd";
  10. SdDbInit();
  11. const sdCommonParams = (model: string, data: any) => {
  12. return [
  13. {
  14. name: locales.SdPanel.Prompt,
  15. value: "prompt",
  16. type: "textarea",
  17. placeholder: locales.SdPanel.PleaseInput(locales.SdPanel.Prompt),
  18. required: true,
  19. },
  20. {
  21. name: locales.SdPanel.ModelVersion,
  22. value: "model",
  23. type: "select",
  24. default: 0,
  25. support: ["sd3"],
  26. options: [
  27. { name: "SD3 Medium", value: "sd3-medium" },
  28. { name: "SD3 Large", value: "sd3-large" },
  29. { name: "SD3 Large Turbo", value: "sd3-large-turbo" },
  30. ],
  31. },
  32. {
  33. name: locales.SdPanel.NegativePrompt,
  34. value: "negative_prompt",
  35. type: "textarea",
  36. placeholder: locales.SdPanel.PleaseInput(locales.SdPanel.NegativePrompt),
  37. },
  38. {
  39. name: locales.SdPanel.AspectRatio,
  40. value: "aspect_ratio",
  41. type: "select",
  42. default: "1:1",
  43. options: [
  44. { name: "1:1", value: "1:1" },
  45. { name: "16:9", value: "16:9" },
  46. { name: "21:9", value: "21:9" },
  47. { name: "2:3", value: "2:3" },
  48. { name: "3:2", value: "3:2" },
  49. { name: "4:5", value: "4:5" },
  50. { name: "5:4", value: "5:4" },
  51. { name: "9:16", value: "9:16" },
  52. { name: "9:21", value: "9:21" },
  53. ],
  54. },
  55. {
  56. name: locales.SdPanel.ImageStyle,
  57. value: "style",
  58. type: "select",
  59. default: "3d",
  60. support: ["core"],
  61. options: [
  62. { name: locales.SdPanel.Styles.D3Model, value: "3d-model" },
  63. { name: locales.SdPanel.Styles.AnalogFilm, value: "analog-film" },
  64. { name: locales.SdPanel.Styles.Anime, value: "anime" },
  65. { name: locales.SdPanel.Styles.Cinematic, value: "cinematic" },
  66. { name: locales.SdPanel.Styles.ComicBook, value: "comic-book" },
  67. { name: locales.SdPanel.Styles.DigitalArt, value: "digital-art" },
  68. { name: locales.SdPanel.Styles.Enhance, value: "enhance" },
  69. { name: locales.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
  70. { name: locales.SdPanel.Styles.Isometric, value: "isometric" },
  71. { name: locales.SdPanel.Styles.LineArt, value: "line-art" },
  72. { name: locales.SdPanel.Styles.LowPoly, value: "low-poly" },
  73. {
  74. name: locales.SdPanel.Styles.ModelingCompound,
  75. value: "modeling-compound",
  76. },
  77. { name: locales.SdPanel.Styles.NeonPunk, value: "neon-punk" },
  78. { name: locales.SdPanel.Styles.Origami, value: "origami" },
  79. { name: locales.SdPanel.Styles.Photographic, value: "photographic" },
  80. { name: locales.SdPanel.Styles.PixelArt, value: "pixel-art" },
  81. { name: locales.SdPanel.Styles.TileTexture, value: "tile-texture" },
  82. ],
  83. },
  84. {
  85. name: "Seed",
  86. value: "seed",
  87. type: "number",
  88. default: 0,
  89. min: 0,
  90. max: 4294967294,
  91. },
  92. {
  93. name: locales.SdPanel.OutFormat,
  94. value: "output_format",
  95. type: "select",
  96. default: "png",
  97. options: [
  98. { name: "PNG", value: "png" },
  99. { name: "JPEG", value: "jpeg" },
  100. { name: "WebP", value: "webp" },
  101. ],
  102. },
  103. ].filter((item) => {
  104. return !(item.support && !item.support.includes(model));
  105. });
  106. };
  107. const models = [
  108. {
  109. name: "Stable Image Ultra",
  110. value: "ultra",
  111. params: (data: any) => sdCommonParams("ultra", data),
  112. },
  113. {
  114. name: "Stable Image Core",
  115. value: "core",
  116. params: (data: any) => sdCommonParams("core", data),
  117. },
  118. {
  119. name: "Stable Diffusion 3",
  120. value: "sd3",
  121. params: (data: any) => {
  122. return sdCommonParams("sd3", data).filter((item) => {
  123. return !(
  124. data.model === "sd3-large-turbo" && item.value == "negative_prompt"
  125. );
  126. });
  127. },
  128. },
  129. ];
  130. export function ControlParamItem(props: {
  131. title: string;
  132. subTitle?: string;
  133. required?: boolean;
  134. children?: JSX.Element | JSX.Element[];
  135. className?: string;
  136. }) {
  137. return (
  138. <div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
  139. <div className={styles["ctrl-param-item-header"]}>
  140. <div className={styles["ctrl-param-item-title"]}>
  141. <div>
  142. {props.title}
  143. {props.required && <span style={{ color: "red" }}>*</span>}
  144. </div>
  145. </div>
  146. </div>
  147. {props.children}
  148. {props.subTitle && (
  149. <div className={styles["ctrl-param-item-sub-title"]}>
  150. {props.subTitle}
  151. </div>
  152. )}
  153. </div>
  154. );
  155. }
  156. export function ControlParam(props: {
  157. columns: any[];
  158. data: any;
  159. onChange: (field: string, val: any) => void;
  160. }) {
  161. return (
  162. <>
  163. {props.columns.map((item) => {
  164. let element: null | JSX.Element;
  165. switch (item.type) {
  166. case "textarea":
  167. element = (
  168. <ControlParamItem
  169. title={item.name}
  170. subTitle={item.sub}
  171. required={item.required}
  172. >
  173. <textarea
  174. rows={item.rows || 3}
  175. style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
  176. placeholder={item.placeholder}
  177. onChange={(e) => {
  178. props.onChange(item.value, e.currentTarget.value);
  179. }}
  180. value={props.data[item.value]}
  181. ></textarea>
  182. </ControlParamItem>
  183. );
  184. break;
  185. case "select":
  186. element = (
  187. <ControlParamItem
  188. title={item.name}
  189. subTitle={item.sub}
  190. required={item.required}
  191. >
  192. <Select
  193. value={props.data[item.value]}
  194. onChange={(e) => {
  195. props.onChange(item.value, e.currentTarget.value);
  196. }}
  197. >
  198. {item.options.map((opt: any) => {
  199. return (
  200. <option value={opt.value} key={opt.value}>
  201. {opt.name}
  202. </option>
  203. );
  204. })}
  205. </Select>
  206. </ControlParamItem>
  207. );
  208. break;
  209. case "number":
  210. element = (
  211. <ControlParamItem
  212. title={item.name}
  213. subTitle={item.sub}
  214. required={item.required}
  215. >
  216. <input
  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. 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. 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 function SdPanel() {
  268. const [currentModel, setCurrentModel] = useState(models[0]);
  269. const [params, setParams] = useState(
  270. getModelParamBasicData(currentModel.params({}), {}),
  271. );
  272. const handleValueChange = (field: string, val: any) => {
  273. setParams((prevParams: any) => ({
  274. ...prevParams,
  275. [field]: val,
  276. }));
  277. };
  278. const handleModelChange = (model: any) => {
  279. setCurrentModel(model);
  280. setParams(getModelParamBasicData(model.params({}), params));
  281. };
  282. const sdListDb = useIndexedDB(StoreKey.SdList);
  283. const { execCountInc } = useSdStore();
  284. const handleSubmit = () => {
  285. const columns = currentModel.params(params);
  286. const reqParams: any = {};
  287. for (let i = 0; i < columns.length; i++) {
  288. const item = columns[i];
  289. reqParams[item.value] = params[item.value] ?? null;
  290. if (item.required) {
  291. if (!reqParams[item.value]) {
  292. showToast(locales.SdPanel.ParamIsRequired(item.name));
  293. return;
  294. }
  295. }
  296. }
  297. // console.log(JSON.stringify(reqParams, null, 4));
  298. let data: any = {
  299. model: currentModel.value,
  300. model_name: currentModel.name,
  301. status: "wait",
  302. params: reqParams,
  303. created_at: new Date().toISOString(),
  304. img_data: "",
  305. };
  306. sdListDb.add(data).then(
  307. (id) => {
  308. data = { ...data, id, status: "running" };
  309. sdListDb.update(data);
  310. execCountInc();
  311. sendSdTask(data, sdListDb, execCountInc);
  312. setParams(getModelParamBasicData(columns, params, true));
  313. },
  314. (error) => {
  315. console.error(error);
  316. showToast(`error: ` + error.message);
  317. },
  318. );
  319. };
  320. return (
  321. <>
  322. <ControlParamItem title={locales.SdPanel.AIModel}>
  323. <div className={styles["ai-models"]}>
  324. {models.map((item) => {
  325. return (
  326. <IconButton
  327. text={item.name}
  328. key={item.value}
  329. type={currentModel.value == item.value ? "primary" : null}
  330. shadow
  331. onClick={() => handleModelChange(item)}
  332. />
  333. );
  334. })}
  335. </div>
  336. </ControlParamItem>
  337. <ControlParam
  338. columns={currentModel.params(params) as any[]}
  339. data={params}
  340. onChange={handleValueChange}
  341. ></ControlParam>
  342. <IconButton
  343. text={locales.SdPanel.Submit}
  344. type="primary"
  345. style={{ marginTop: "20px" }}
  346. shadow
  347. onClick={handleSubmit}
  348. ></IconButton>
  349. </>
  350. );
  351. }