sd.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  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. function IndexDBImage({ img_data, title, isMobileScreen }) {
  95. const [src, setSrc] = useState(img_data);
  96. const sdListDb = useIndexedDB(StoreKey.SdList);
  97. const img_id = useMemo(
  98. () => img_data.replace("indexeddb://", "").split("@").pop(),
  99. [img_data],
  100. );
  101. useEffect(() => {
  102. sdListDb
  103. .getByID(img_id)
  104. .then(({ data }) => {
  105. setSrc(data);
  106. })
  107. .catch((e) => {
  108. setSrc(img_data);
  109. });
  110. }, [img_data, img_id]);
  111. return (
  112. <img
  113. className={styles["img"]}
  114. src={`data:image/png;base64,${src}`}
  115. alt={title}
  116. onClick={(e) => {
  117. showImageModal(
  118. getBase64ImgUrl(src, "image/png"),
  119. true,
  120. isMobileScreen
  121. ? { width: "100%", height: "fit-content" }
  122. : { maxWidth: "100%", maxHeight: "100%" },
  123. isMobileScreen
  124. ? { width: "100%", height: "fit-content" }
  125. : { width: "100%", height: "100%" },
  126. );
  127. }}
  128. />
  129. );
  130. }
  131. export function Sd() {
  132. const isMobileScreen = useMobileScreen();
  133. const navigate = useNavigate();
  134. const clientConfig = useMemo(() => getClientConfig(), []);
  135. const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
  136. const config = useAppConfig();
  137. const scrollRef = useRef<HTMLDivElement>(null);
  138. const sdListDb = useIndexedDB(StoreKey.SdList);
  139. const sdStore = useSdStore();
  140. const [sdImages, setSdImages] = useState(sdStore.draw);
  141. useEffect(() => {
  142. setSdImages(sdStore.draw);
  143. }, [sdStore.currentId]);
  144. return (
  145. <div className={chatStyles.chat} key={"1"}>
  146. <div className="window-header" data-tauri-drag-region>
  147. {isMobileScreen && (
  148. <div className="window-actions">
  149. <div className={"window-action-button"}>
  150. <IconButton
  151. icon={<ReturnIcon />}
  152. bordered
  153. title={Locale.Chat.Actions.ChatList}
  154. onClick={() => navigate(Path.SdPanel)}
  155. />
  156. </div>
  157. </div>
  158. )}
  159. <div className={`window-header-title ${chatStyles["chat-body-title"]}`}>
  160. <div className={`window-header-main-title`}>Stability AI</div>
  161. <div className="window-header-sub-title">
  162. {Locale.Sd.SubTitle(sdImages.length || 0)}
  163. </div>
  164. </div>
  165. <div className="window-actions">
  166. {showMaxIcon && (
  167. <div className="window-action-button">
  168. <IconButton
  169. icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
  170. bordered
  171. onClick={() => {
  172. config.update(
  173. (config) => (config.tightBorder = !config.tightBorder),
  174. );
  175. }}
  176. />
  177. </div>
  178. )}
  179. </div>
  180. </div>
  181. <div className={chatStyles["chat-body"]} ref={scrollRef}>
  182. <div className={styles["sd-img-list"]}>
  183. {sdImages.length > 0 ? (
  184. sdImages.map((item: any) => {
  185. return (
  186. <div
  187. key={item.id}
  188. style={{ display: "flex" }}
  189. className={styles["sd-img-item"]}
  190. >
  191. {item.status === "success" ? (
  192. <>
  193. <IndexDBImage
  194. img_data={item.img_data}
  195. title={item.id}
  196. isMobileScreen={isMobileScreen}
  197. />
  198. </>
  199. ) : item.status === "error" ? (
  200. <div className={styles["pre-img"]}>
  201. <ErrorIcon />
  202. </div>
  203. ) : (
  204. <div className={styles["pre-img"]}>
  205. <LoadingIcon />
  206. </div>
  207. )}
  208. <div
  209. style={{ marginLeft: "10px" }}
  210. className={styles["sd-img-item-info"]}
  211. >
  212. <p className={styles["line-1"]}>
  213. {locales.SdPanel.Prompt}:{" "}
  214. <span
  215. className="clickable"
  216. title={item.params.prompt}
  217. onClick={() => {
  218. showModal({
  219. title: locales.Sd.Detail,
  220. children: (
  221. <div style={{ userSelect: "text" }}>
  222. {item.params.prompt}
  223. </div>
  224. ),
  225. });
  226. }}
  227. >
  228. {item.params.prompt}
  229. </span>
  230. </p>
  231. <p>
  232. {locales.SdPanel.AIModel}: {item.model_name}
  233. </p>
  234. {getSdTaskStatus(item)}
  235. <p>{item.created_at}</p>
  236. <div className={chatStyles["chat-message-actions"]}>
  237. <div className={chatStyles["chat-input-actions"]}>
  238. <ChatAction
  239. text={Locale.Sd.Actions.Params}
  240. icon={<PromptIcon />}
  241. onClick={() => {
  242. showModal({
  243. title: locales.Sd.GenerateParams,
  244. children: (
  245. <div style={{ userSelect: "text" }}>
  246. {Object.keys(item.params).map((key) => (
  247. <div key={key} style={{ margin: "10px" }}>
  248. <strong>{key}: </strong>
  249. {item.params[key]}
  250. </div>
  251. ))}
  252. </div>
  253. ),
  254. });
  255. }}
  256. />
  257. <ChatAction
  258. text={Locale.Sd.Actions.Copy}
  259. icon={<CopyIcon />}
  260. onClick={() =>
  261. copyToClipboard(
  262. getMessageTextContent({
  263. role: "user",
  264. content: item.params.prompt,
  265. }),
  266. )
  267. }
  268. />
  269. <ChatAction
  270. text={Locale.Sd.Actions.Retry}
  271. icon={<ResetIcon />}
  272. onClick={() => {
  273. const reqData = {
  274. model: item.model,
  275. model_name: item.model_name,
  276. status: "wait",
  277. params: { ...item.params },
  278. created_at: new Date().toLocaleString(),
  279. img_data: "",
  280. };
  281. sdStore.sendTask(reqData, sdListDb);
  282. }}
  283. />
  284. <ChatAction
  285. text={Locale.Sd.Actions.Delete}
  286. icon={<DeleteIcon />}
  287. onClick={async () => {
  288. if (await showConfirm(Locale.Sd.Danger.Delete)) {
  289. // remove img_data + remove item in list
  290. sdListDb.deleteRecord(item.id).then(
  291. () => {
  292. sdStore.draw = sdImages.filter(
  293. (i: any) => i.id !== item.id,
  294. );
  295. sdStore.getNextId();
  296. },
  297. (error) => {
  298. console.error(error);
  299. },
  300. );
  301. }
  302. }}
  303. />
  304. </div>
  305. </div>
  306. </div>
  307. </div>
  308. );
  309. })
  310. ) : (
  311. <div>{locales.Sd.EmptyRecord}</div>
  312. )}
  313. </div>
  314. </div>
  315. </div>
  316. );
  317. }