sd.tsx 11 KB

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