sd.tsx 14 KB

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