sd-panel.tsx 10 KB

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