sd.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import chatStyles from "@/app/components/chat.module.scss";
  2. import styles from "@/app/components/sd/sd.module.scss";
  3. import { IconButton } from "@/app/components/button";
  4. import ReturnIcon from "@/app/icons/return.svg";
  5. import Locale from "@/app/locales";
  6. import { Path } from "@/app/constant";
  7. import React, { useEffect, useMemo, useRef, useState } from "react";
  8. import {
  9. copyToClipboard,
  10. getMessageTextContent,
  11. useMobileScreen,
  12. } from "@/app/utils";
  13. import { useNavigate } from "react-router-dom";
  14. import { useAppConfig } from "@/app/store";
  15. import MinIcon from "@/app/icons/min.svg";
  16. import MaxIcon from "@/app/icons/max.svg";
  17. import { getClientConfig } from "@/app/config/client";
  18. import { ChatAction } from "@/app/components/chat";
  19. import DeleteIcon from "@/app/icons/clear.svg";
  20. import CopyIcon from "@/app/icons/copy.svg";
  21. import PromptIcon from "@/app/icons/prompt.svg";
  22. import ResetIcon from "@/app/icons/reload.svg";
  23. import { useSdStore } from "@/app/store/sd";
  24. import locales from "@/app/locales";
  25. import LoadingIcon from "@/app/icons/three-dots.svg";
  26. import ErrorIcon from "@/app/icons/delete.svg";
  27. import { Property } from "csstype";
  28. import {
  29. showConfirm,
  30. showImageModal,
  31. showModal,
  32. } from "@/app/components/ui-lib";
  33. import { removeImage } from "@/app/utils/chat";
  34. import { SideBar } from "./sd-sidebar";
  35. import { WindowContent } from "@/app/components/home";
  36. import { params } from "./sd-panel";
  37. function getSdTaskStatus(item: any) {
  38. let s: string;
  39. let color: Property.Color | undefined = undefined;
  40. switch (item.status) {
  41. case "success":
  42. s = Locale.Sd.Status.Success;
  43. color = "green";
  44. break;
  45. case "error":
  46. s = Locale.Sd.Status.Error;
  47. color = "red";
  48. break;
  49. case "wait":
  50. s = Locale.Sd.Status.Wait;
  51. color = "yellow";
  52. break;
  53. case "running":
  54. s = Locale.Sd.Status.Running;
  55. color = "blue";
  56. break;
  57. default:
  58. s = item.status.toUpperCase();
  59. }
  60. return (
  61. <p className={styles["line-1"]} title={item.error} style={{ color: color }}>
  62. <span>
  63. {locales.Sd.Status.Name}: {s}
  64. </span>
  65. {item.status === "error" && (
  66. <span
  67. className="clickable"
  68. onClick={() => {
  69. showModal({
  70. title: locales.Sd.Detail,
  71. children: (
  72. <div style={{ color: color, userSelect: "text" }}>
  73. {item.error}
  74. </div>
  75. ),
  76. });
  77. }}
  78. >
  79. {" "}
  80. - {item.error}
  81. </span>
  82. )}
  83. </p>
  84. );
  85. }
  86. export function Sd() {
  87. const isMobileScreen = useMobileScreen();
  88. const navigate = useNavigate();
  89. const clientConfig = useMemo(() => getClientConfig(), []);
  90. const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
  91. const config = useAppConfig();
  92. const scrollRef = useRef<HTMLDivElement>(null);
  93. const sdStore = useSdStore();
  94. const [sdImages, setSdImages] = useState(sdStore.draw);
  95. useEffect(() => {
  96. setSdImages(sdStore.draw);
  97. }, [sdStore.currentId]);
  98. return (
  99. <>
  100. <SideBar />
  101. <WindowContent>
  102. <div className={chatStyles.chat} key={"1"}>
  103. <div className="window-header" data-tauri-drag-region>
  104. {isMobileScreen && (
  105. <div className="window-actions">
  106. <div className={"window-action-button"}>
  107. <IconButton
  108. icon={<ReturnIcon />}
  109. bordered
  110. title={Locale.Chat.Actions.ChatList}
  111. onClick={() => navigate(Path.SdNew)}
  112. />
  113. </div>
  114. </div>
  115. )}
  116. <div
  117. className={`window-header-title ${chatStyles["chat-body-title"]}`}
  118. >
  119. <div className={`window-header-main-title`}>Stability AI</div>
  120. <div className="window-header-sub-title">
  121. {Locale.Sd.SubTitle(sdImages.length || 0)}
  122. </div>
  123. </div>
  124. <div className="window-actions">
  125. {showMaxIcon && (
  126. <div className="window-action-button">
  127. <IconButton
  128. icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
  129. bordered
  130. onClick={() => {
  131. config.update(
  132. (config) => (config.tightBorder = !config.tightBorder),
  133. );
  134. }}
  135. />
  136. </div>
  137. )}
  138. </div>
  139. </div>
  140. <div className={chatStyles["chat-body"]} ref={scrollRef}>
  141. <div className={styles["sd-img-list"]}>
  142. {sdImages.length > 0 ? (
  143. sdImages.map((item: any) => {
  144. return (
  145. <div
  146. key={item.id}
  147. style={{ display: "flex" }}
  148. className={styles["sd-img-item"]}
  149. >
  150. {item.status === "success" ? (
  151. <img
  152. className={styles["img"]}
  153. src={item.img_data}
  154. alt={item.id}
  155. onClick={(e) =>
  156. showImageModal(
  157. item.img_data,
  158. true,
  159. isMobileScreen
  160. ? { width: "100%", height: "fit-content" }
  161. : { maxWidth: "100%", maxHeight: "100%" },
  162. isMobileScreen
  163. ? { width: "100%", height: "fit-content" }
  164. : { width: "100%", height: "100%" },
  165. )
  166. }
  167. />
  168. ) : item.status === "error" ? (
  169. <div className={styles["pre-img"]}>
  170. <ErrorIcon />
  171. </div>
  172. ) : (
  173. <div className={styles["pre-img"]}>
  174. <LoadingIcon />
  175. </div>
  176. )}
  177. <div
  178. style={{ marginLeft: "10px" }}
  179. className={styles["sd-img-item-info"]}
  180. >
  181. <p className={styles["line-1"]}>
  182. {locales.SdPanel.Prompt}:{" "}
  183. <span
  184. className="clickable"
  185. title={item.params.prompt}
  186. onClick={() => {
  187. showModal({
  188. title: locales.Sd.Detail,
  189. children: (
  190. <div style={{ userSelect: "text" }}>
  191. {item.params.prompt}
  192. </div>
  193. ),
  194. });
  195. }}
  196. >
  197. {item.params.prompt}
  198. </span>
  199. </p>
  200. <p>
  201. {locales.SdPanel.AIModel}: {item.model_name}
  202. </p>
  203. {getSdTaskStatus(item)}
  204. <p>{item.created_at}</p>
  205. <div className={chatStyles["chat-message-actions"]}>
  206. <div className={chatStyles["chat-input-actions"]}>
  207. <ChatAction
  208. text={Locale.Sd.Actions.Params}
  209. icon={<PromptIcon />}
  210. onClick={() => {
  211. showModal({
  212. title: locales.Sd.GenerateParams,
  213. children: (
  214. <div style={{ userSelect: "text" }}>
  215. {Object.keys(item.params).map((key) => {
  216. let label = key;
  217. let value = item.params[key];
  218. switch (label) {
  219. case "prompt":
  220. label = Locale.SdPanel.Prompt;
  221. break;
  222. case "negative_prompt":
  223. label =
  224. Locale.SdPanel.NegativePrompt;
  225. break;
  226. case "aspect_ratio":
  227. label = Locale.SdPanel.AspectRatio;
  228. break;
  229. case "seed":
  230. label = "Seed";
  231. value = value || 0;
  232. break;
  233. case "output_format":
  234. label = Locale.SdPanel.OutFormat;
  235. value = value?.toUpperCase();
  236. break;
  237. case "style":
  238. label = Locale.SdPanel.ImageStyle;
  239. value = params
  240. .find(
  241. (item) =>
  242. item.value === "style",
  243. )
  244. ?.options?.find(
  245. (item) => item.value === value,
  246. )?.name;
  247. break;
  248. default:
  249. break;
  250. }
  251. return (
  252. <div
  253. key={key}
  254. style={{ margin: "10px" }}
  255. >
  256. <strong>{label}: </strong>
  257. {value}
  258. </div>
  259. );
  260. })}
  261. </div>
  262. ),
  263. });
  264. }}
  265. />
  266. <ChatAction
  267. text={Locale.Sd.Actions.Copy}
  268. icon={<CopyIcon />}
  269. onClick={() =>
  270. copyToClipboard(
  271. getMessageTextContent({
  272. role: "user",
  273. content: item.params.prompt,
  274. }),
  275. )
  276. }
  277. />
  278. <ChatAction
  279. text={Locale.Sd.Actions.Retry}
  280. icon={<ResetIcon />}
  281. onClick={() => {
  282. const reqData = {
  283. model: item.model,
  284. model_name: item.model_name,
  285. status: "wait",
  286. params: { ...item.params },
  287. created_at: new Date().toLocaleString(),
  288. img_data: "",
  289. };
  290. sdStore.sendTask(reqData);
  291. }}
  292. />
  293. <ChatAction
  294. text={Locale.Sd.Actions.Delete}
  295. icon={<DeleteIcon />}
  296. onClick={async () => {
  297. if (
  298. await showConfirm(Locale.Sd.Danger.Delete)
  299. ) {
  300. // remove img_data + remove item in list
  301. removeImage(item.img_data).finally(() => {
  302. sdStore.draw = sdImages.filter(
  303. (i: any) => i.id !== item.id,
  304. );
  305. sdStore.getNextId();
  306. });
  307. }
  308. }}
  309. />
  310. </div>
  311. </div>
  312. </div>
  313. </div>
  314. );
  315. })
  316. ) : (
  317. <div>{locales.Sd.EmptyRecord}</div>
  318. )}
  319. </div>
  320. </div>
  321. </div>
  322. </WindowContent>
  323. </>
  324. );
  325. }