sd.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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. useWindowSize,
  13. } from "@/app/utils";
  14. import { useNavigate } 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 { useIndexedDB } from "react-indexed-db-hook";
  25. import { useSdStore } from "@/app/store/sd";
  26. import locales from "@/app/locales";
  27. import LoadingIcon from "../icons/three-dots.svg";
  28. import ErrorIcon from "../icons/delete.svg";
  29. import { Property } from "csstype";
  30. import { showConfirm } from "@/app/components/ui-lib";
  31. function openBase64ImgUrl(base64Data: string, contentType: string) {
  32. const byteCharacters = atob(base64Data);
  33. const byteNumbers = new Array(byteCharacters.length);
  34. for (let i = 0; i < byteCharacters.length; i++) {
  35. byteNumbers[i] = byteCharacters.charCodeAt(i);
  36. }
  37. const byteArray = new Uint8Array(byteNumbers);
  38. const blob = new Blob([byteArray], { type: contentType });
  39. const blobUrl = URL.createObjectURL(blob);
  40. window.open(blobUrl);
  41. }
  42. function getSdTaskStatus(item: any) {
  43. let s: string;
  44. let color: Property.Color | undefined = undefined;
  45. switch (item.status) {
  46. case "success":
  47. s = Locale.Sd.Status.Success;
  48. color = "green";
  49. break;
  50. case "error":
  51. s = Locale.Sd.Status.Error;
  52. color = "red";
  53. break;
  54. case "wait":
  55. s = Locale.Sd.Status.Wait;
  56. color = "yellow";
  57. break;
  58. case "running":
  59. s = Locale.Sd.Status.Running;
  60. color = "blue";
  61. break;
  62. default:
  63. s = item.status.toUpperCase();
  64. }
  65. return (
  66. <p className={styles["line-1"]} title={item.error} style={{ color: color }}>
  67. <span>
  68. {locales.Sd.Status.Name}: {s}
  69. </span>
  70. {item.status === "error" && <span> - {item.error}</span>}
  71. </p>
  72. );
  73. }
  74. export function Sd() {
  75. const isMobileScreen = useMobileScreen();
  76. const navigate = useNavigate();
  77. const clientConfig = useMemo(() => getClientConfig(), []);
  78. const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
  79. const config = useAppConfig();
  80. const scrollRef = useRef<HTMLDivElement>(null);
  81. const sdListDb = useIndexedDB(StoreKey.SdList);
  82. const [sdImages, setSdImages] = useState([]);
  83. const { execCount } = useSdStore();
  84. useEffect(() => {
  85. sdListDb.getAll().then((data) => {
  86. setSdImages(((data as never[]) || []).reverse());
  87. });
  88. }, [execCount]);
  89. return (
  90. <div className={chatStyles.chat} key={"1"}>
  91. <div className="window-header" data-tauri-drag-region>
  92. {isMobileScreen && (
  93. <div className="window-actions">
  94. <div className={"window-action-button"}>
  95. <IconButton
  96. icon={<ReturnIcon />}
  97. bordered
  98. title={Locale.Chat.Actions.ChatList}
  99. onClick={() => navigate(Path.SdPanel)}
  100. />
  101. </div>
  102. </div>
  103. )}
  104. <div className={`window-header-title ${chatStyles["chat-body-title"]}`}>
  105. <div className={`window-header-main-title`}>Stability AI</div>
  106. <div className="window-header-sub-title">
  107. {Locale.Sd.SubTitle(sdImages.length || 0)}
  108. </div>
  109. </div>
  110. <div className="window-actions">
  111. {showMaxIcon && (
  112. <div className="window-action-button">
  113. <IconButton
  114. icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
  115. bordered
  116. onClick={() => {
  117. config.update(
  118. (config) => (config.tightBorder = !config.tightBorder),
  119. );
  120. }}
  121. />
  122. </div>
  123. )}
  124. </div>
  125. </div>
  126. <div className={chatStyles["chat-body"]} ref={scrollRef}>
  127. <div className={styles["sd-img-list"]}>
  128. {sdImages.length > 0 ? (
  129. sdImages.map((item: any) => {
  130. return (
  131. <div
  132. key={item.id}
  133. style={{ display: "flex" }}
  134. className={styles["sd-img-item"]}
  135. >
  136. {item.status === "success" ? (
  137. <img
  138. className={styles["img"]}
  139. src={`data:image/png;base64,${item.img_data}`}
  140. alt={`${item.id}`}
  141. onClick={(e) => {
  142. openBase64ImgUrl(item.img_data, "image/png");
  143. }}
  144. />
  145. ) : item.status === "error" ? (
  146. <div className={styles["pre-img"]}>
  147. <ErrorIcon />
  148. </div>
  149. ) : (
  150. <div className={styles["pre-img"]}>
  151. <LoadingIcon />
  152. </div>
  153. )}
  154. <div
  155. style={{ marginLeft: "10px" }}
  156. className={styles["sd-img-item-info"]}
  157. >
  158. <p className={styles["line-1"]}>
  159. {locales.SdPanel.Prompt}:{" "}
  160. <span title={item.params.prompt}>
  161. {item.params.prompt}
  162. </span>
  163. </p>
  164. <p>
  165. {locales.SdPanel.AIModel}: {item.model_name}
  166. </p>
  167. {getSdTaskStatus(item)}
  168. <p>{item.created_at}</p>
  169. <div className={chatStyles["chat-message-actions"]}>
  170. <div className={chatStyles["chat-input-actions"]}>
  171. <ChatAction
  172. text={Locale.Sd.Actions.Params}
  173. icon={<PromptIcon />}
  174. onClick={() => console.log(1)}
  175. />
  176. <ChatAction
  177. text={Locale.Sd.Actions.Copy}
  178. icon={<CopyIcon />}
  179. onClick={() =>
  180. copyToClipboard(
  181. getMessageTextContent({
  182. role: "user",
  183. content: item.params.prompt,
  184. }),
  185. )
  186. }
  187. />
  188. <ChatAction
  189. text={Locale.Sd.Actions.Retry}
  190. icon={<ResetIcon />}
  191. onClick={() => console.log(1)}
  192. />
  193. <ChatAction
  194. text={Locale.Sd.Actions.Delete}
  195. icon={<DeleteIcon />}
  196. onClick={async () => {
  197. if (await showConfirm(Locale.Sd.Danger.Delete)) {
  198. sdListDb.deleteRecord(item.id).then(
  199. () => {
  200. setSdImages(
  201. sdImages.filter(
  202. (i: any) => i.id !== item.id,
  203. ),
  204. );
  205. },
  206. (error) => {
  207. console.error(error);
  208. },
  209. );
  210. }
  211. }}
  212. />
  213. </div>
  214. </div>
  215. </div>
  216. </div>
  217. );
  218. })
  219. ) : (
  220. <div>{locales.Sd.EmptyRecord}</div>
  221. )}
  222. </div>
  223. </div>
  224. </div>
  225. );
  226. }