sd.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
  131. bordered
  132. onClick={() => {
  133. config.update(
  134. (config) => (config.tightBorder = !config.tightBorder),
  135. );
  136. }}
  137. />
  138. </div>
  139. )}
  140. {isMobileScreen && <SDIcon width={50} height={50} />}
  141. </div>
  142. </div>
  143. <div className={chatStyles["chat-body"]} ref={scrollRef}>
  144. <div className={styles["sd-img-list"]}>
  145. {sdImages.length > 0 ? (
  146. sdImages.map((item: any) => {
  147. return (
  148. <div
  149. key={item.id}
  150. style={{ display: "flex" }}
  151. className={styles["sd-img-item"]}
  152. >
  153. {item.status === "success" ? (
  154. <img
  155. className={styles["img"]}
  156. src={item.img_data}
  157. alt={item.id}
  158. onClick={(e) =>
  159. showImageModal(
  160. item.img_data,
  161. true,
  162. isMobileScreen
  163. ? { width: "100%", height: "fit-content" }
  164. : { maxWidth: "100%", maxHeight: "100%" },
  165. isMobileScreen
  166. ? { width: "100%", height: "fit-content" }
  167. : { width: "100%", height: "100%" },
  168. )
  169. }
  170. />
  171. ) : item.status === "error" ? (
  172. <div className={styles["pre-img"]}>
  173. <ErrorIcon />
  174. </div>
  175. ) : (
  176. <div className={styles["pre-img"]}>
  177. <LoadingIcon />
  178. </div>
  179. )}
  180. <div
  181. style={{ marginLeft: "10px" }}
  182. className={styles["sd-img-item-info"]}
  183. >
  184. <p className={styles["line-1"]}>
  185. {Locale.SdPanel.Prompt}:{" "}
  186. <span
  187. className="clickable"
  188. title={item.params.prompt}
  189. onClick={() => {
  190. showModal({
  191. title: Locale.Sd.Detail,
  192. children: (
  193. <div style={{ userSelect: "text" }}>
  194. {item.params.prompt}
  195. </div>
  196. ),
  197. });
  198. }}
  199. >
  200. {item.params.prompt}
  201. </span>
  202. </p>
  203. <p>
  204. {Locale.SdPanel.AIModel}: {item.model_name}
  205. </p>
  206. {getSdTaskStatus(item)}
  207. <p>{item.created_at}</p>
  208. <div className={chatStyles["chat-message-actions"]}>
  209. <div className={chatStyles["chat-input-actions"]}>
  210. <ChatAction
  211. text={Locale.Sd.Actions.Params}
  212. icon={<PromptIcon />}
  213. onClick={() => {
  214. showModal({
  215. title: Locale.Sd.GenerateParams,
  216. children: (
  217. <div style={{ userSelect: "text" }}>
  218. {Object.keys(item.params).map((key) => {
  219. let label = key;
  220. let value = item.params[key];
  221. switch (label) {
  222. case "prompt":
  223. label = Locale.SdPanel.Prompt;
  224. break;
  225. case "negative_prompt":
  226. label =
  227. Locale.SdPanel.NegativePrompt;
  228. break;
  229. case "aspect_ratio":
  230. label = Locale.SdPanel.AspectRatio;
  231. break;
  232. case "seed":
  233. label = "Seed";
  234. value = value || 0;
  235. break;
  236. case "output_format":
  237. label = Locale.SdPanel.OutFormat;
  238. value = value?.toUpperCase();
  239. break;
  240. case "style":
  241. label = Locale.SdPanel.ImageStyle;
  242. value = params
  243. .find(
  244. (item) =>
  245. item.value === "style",
  246. )
  247. ?.options?.find(
  248. (item) => item.value === value,
  249. )?.name;
  250. break;
  251. default:
  252. break;
  253. }
  254. return (
  255. <div
  256. key={key}
  257. style={{ margin: "10px" }}
  258. >
  259. <strong>{label}: </strong>
  260. {value}
  261. </div>
  262. );
  263. })}
  264. </div>
  265. ),
  266. });
  267. }}
  268. />
  269. <ChatAction
  270. text={Locale.Sd.Actions.Copy}
  271. icon={<CopyIcon />}
  272. onClick={() =>
  273. copyToClipboard(
  274. getMessageTextContent({
  275. role: "user",
  276. content: item.params.prompt,
  277. }),
  278. )
  279. }
  280. />
  281. <ChatAction
  282. text={Locale.Sd.Actions.Retry}
  283. icon={<ResetIcon />}
  284. onClick={() => {
  285. const reqData = {
  286. model: item.model,
  287. model_name: item.model_name,
  288. status: "wait",
  289. params: { ...item.params },
  290. created_at: new Date().toLocaleString(),
  291. img_data: "",
  292. };
  293. sdStore.sendTask(reqData);
  294. }}
  295. />
  296. <ChatAction
  297. text={Locale.Sd.Actions.Delete}
  298. icon={<DeleteIcon />}
  299. onClick={async () => {
  300. if (
  301. await showConfirm(Locale.Sd.Danger.Delete)
  302. ) {
  303. // remove img_data + remove item in list
  304. removeImage(item.img_data).finally(() => {
  305. sdStore.draw = sdImages.filter(
  306. (i: any) => i.id !== item.id,
  307. );
  308. sdStore.getNextId();
  309. });
  310. }
  311. }}
  312. />
  313. </div>
  314. </div>
  315. </div>
  316. </div>
  317. );
  318. })
  319. ) : (
  320. <div>{Locale.Sd.EmptyRecord}</div>
  321. )}
  322. </div>
  323. </div>
  324. </div>
  325. </WindowContent>
  326. </>
  327. );
  328. }