sd.tsx 13 KB

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