sd.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import chatStyles from "@/app/components/chat.module.scss";
  2. import styles from "@/app/components/sd/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 } 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 { useSdStore } from "@/app/store/sd";
  24. import locales from "@/app/locales";
  25. import LoadingIcon from "@/app/icons/three-dots.svg";
  26. import ErrorIcon from "@/app/icons/delete.svg";
  27. import { Property } from "csstype";
  28. import {
  29. showConfirm,
  30. showImageModal,
  31. showModal,
  32. } from "@/app/components/ui-lib";
  33. import { removeImage } from "@/app/utils/chat";
  34. import { SideBar } from "./sd-sidebar";
  35. import { WindowContent } from "@/app/components/home";
  36. function getSdTaskStatus(item: any) {
  37. let s: string;
  38. let color: Property.Color | undefined = undefined;
  39. switch (item.status) {
  40. case "success":
  41. s = Locale.Sd.Status.Success;
  42. color = "green";
  43. break;
  44. case "error":
  45. s = Locale.Sd.Status.Error;
  46. color = "red";
  47. break;
  48. case "wait":
  49. s = Locale.Sd.Status.Wait;
  50. color = "yellow";
  51. break;
  52. case "running":
  53. s = Locale.Sd.Status.Running;
  54. color = "blue";
  55. break;
  56. default:
  57. s = item.status.toUpperCase();
  58. }
  59. return (
  60. <p className={styles["line-1"]} title={item.error} style={{ color: color }}>
  61. <span>
  62. {locales.Sd.Status.Name}: {s}
  63. </span>
  64. {item.status === "error" && (
  65. <span
  66. className="clickable"
  67. onClick={() => {
  68. showModal({
  69. title: locales.Sd.Detail,
  70. children: (
  71. <div style={{ color: color, userSelect: "text" }}>
  72. {item.error}
  73. </div>
  74. ),
  75. });
  76. }}
  77. >
  78. {" "}
  79. - {item.error}
  80. </span>
  81. )}
  82. </p>
  83. );
  84. }
  85. export function Sd() {
  86. const isMobileScreen = useMobileScreen();
  87. const navigate = useNavigate();
  88. const clientConfig = useMemo(() => getClientConfig(), []);
  89. const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
  90. const config = useAppConfig();
  91. const scrollRef = useRef<HTMLDivElement>(null);
  92. const sdStore = useSdStore();
  93. const [sdImages, setSdImages] = useState(sdStore.draw);
  94. useEffect(() => {
  95. setSdImages(sdStore.draw);
  96. }, [sdStore.currentId]);
  97. return (
  98. <>
  99. <SideBar />
  100. <WindowContent>
  101. <div className={chatStyles.chat} key={"1"}>
  102. <div className="window-header" data-tauri-drag-region>
  103. {isMobileScreen && (
  104. <div className="window-actions">
  105. <div className={"window-action-button"}>
  106. <IconButton
  107. icon={<ReturnIcon />}
  108. bordered
  109. title={Locale.Chat.Actions.ChatList}
  110. onClick={() => navigate(Path.SdNew)}
  111. />
  112. </div>
  113. </div>
  114. )}
  115. <div
  116. className={`window-header-title ${chatStyles["chat-body-title"]}`}
  117. >
  118. <div className={`window-header-main-title`}>Stability AI</div>
  119. <div className="window-header-sub-title">
  120. {Locale.Sd.SubTitle(sdImages.length || 0)}
  121. </div>
  122. </div>
  123. <div className="window-actions">
  124. {showMaxIcon && (
  125. <div className="window-action-button">
  126. <IconButton
  127. icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
  128. bordered
  129. onClick={() => {
  130. config.update(
  131. (config) => (config.tightBorder = !config.tightBorder),
  132. );
  133. }}
  134. />
  135. </div>
  136. )}
  137. </div>
  138. </div>
  139. <div className={chatStyles["chat-body"]} ref={scrollRef}>
  140. <div className={styles["sd-img-list"]}>
  141. {sdImages.length > 0 ? (
  142. sdImages.map((item: any) => {
  143. return (
  144. <div
  145. key={item.id}
  146. style={{ display: "flex" }}
  147. className={styles["sd-img-item"]}
  148. >
  149. {item.status === "success" ? (
  150. <img
  151. className={styles["img"]}
  152. src={item.img_data}
  153. alt={item.id}
  154. onClick={(e) =>
  155. showImageModal(
  156. item.img_data,
  157. true,
  158. isMobileScreen
  159. ? { width: "100%", height: "fit-content" }
  160. : { maxWidth: "100%", maxHeight: "100%" },
  161. isMobileScreen
  162. ? { width: "100%", height: "fit-content" }
  163. : { width: "100%", height: "100%" },
  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
  216. key={key}
  217. style={{ margin: "10px" }}
  218. >
  219. <strong>{key}: </strong>
  220. {item.params[key]}
  221. </div>
  222. ))}
  223. </div>
  224. ),
  225. });
  226. }}
  227. />
  228. <ChatAction
  229. text={Locale.Sd.Actions.Copy}
  230. icon={<CopyIcon />}
  231. onClick={() =>
  232. copyToClipboard(
  233. getMessageTextContent({
  234. role: "user",
  235. content: item.params.prompt,
  236. }),
  237. )
  238. }
  239. />
  240. <ChatAction
  241. text={Locale.Sd.Actions.Retry}
  242. icon={<ResetIcon />}
  243. onClick={() => {
  244. const reqData = {
  245. model: item.model,
  246. model_name: item.model_name,
  247. status: "wait",
  248. params: { ...item.params },
  249. created_at: new Date().toLocaleString(),
  250. img_data: "",
  251. };
  252. sdStore.sendTask(reqData);
  253. }}
  254. />
  255. <ChatAction
  256. text={Locale.Sd.Actions.Delete}
  257. icon={<DeleteIcon />}
  258. onClick={async () => {
  259. if (
  260. await showConfirm(Locale.Sd.Danger.Delete)
  261. ) {
  262. // remove img_data + remove item in list
  263. removeImage(item.img_data).finally(() => {
  264. sdStore.draw = sdImages.filter(
  265. (i: any) => i.id !== item.id,
  266. );
  267. sdStore.getNextId();
  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. </WindowContent>
  285. </>
  286. );
  287. }