sd.tsx 10 KB

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