sd.tsx 13 KB

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