mirror of
				https://github.com/Yidadaa/ChatGPT-Next-Web.git
				synced 2025-10-31 21:59:19 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			341 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			341 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import chatStyles from "@/app/components/chat.module.scss";
 | |
| import styles from "@/app/components/sd/sd.module.scss";
 | |
| import homeStyles from "@/app/components/home.module.scss";
 | |
| 
 | |
| import { IconButton } from "@/app/components/button";
 | |
| import ReturnIcon from "@/app/icons/return.svg";
 | |
| import Locale from "@/app/locales";
 | |
| import { Path } from "@/app/constant";
 | |
| import React, { useEffect, useMemo, useRef, useState } from "react";
 | |
| import {
 | |
|   copyToClipboard,
 | |
|   getMessageTextContent,
 | |
|   useMobileScreen,
 | |
| } from "@/app/utils";
 | |
| import { useNavigate, useLocation } from "react-router-dom";
 | |
| import { useAppConfig } from "@/app/store";
 | |
| import MinIcon from "@/app/icons/min.svg";
 | |
| import MaxIcon from "@/app/icons/max.svg";
 | |
| import { getClientConfig } from "@/app/config/client";
 | |
| import { ChatAction } from "@/app/components/chat";
 | |
| import DeleteIcon from "@/app/icons/clear.svg";
 | |
| import CopyIcon from "@/app/icons/copy.svg";
 | |
| import PromptIcon from "@/app/icons/prompt.svg";
 | |
| import ResetIcon from "@/app/icons/reload.svg";
 | |
| import { useSdStore } from "@/app/store/sd";
 | |
| import LoadingIcon from "@/app/icons/three-dots.svg";
 | |
| import ErrorIcon from "@/app/icons/delete.svg";
 | |
| import SDIcon from "@/app/icons/sd.svg";
 | |
| import { Property } from "csstype";
 | |
| import {
 | |
|   showConfirm,
 | |
|   showImageModal,
 | |
|   showModal,
 | |
| } from "@/app/components/ui-lib";
 | |
| import { removeImage } from "@/app/utils/chat";
 | |
| import { SideBar } from "./sd-sidebar";
 | |
| import { WindowContent } from "@/app/components/home";
 | |
| import { params } from "./sd-panel";
 | |
| import clsx from "clsx";
 | |
| 
 | |
| function getSdTaskStatus(item: any) {
 | |
|   let s: string;
 | |
|   let color: Property.Color | undefined = undefined;
 | |
|   switch (item.status) {
 | |
|     case "success":
 | |
|       s = Locale.Sd.Status.Success;
 | |
|       color = "green";
 | |
|       break;
 | |
|     case "error":
 | |
|       s = Locale.Sd.Status.Error;
 | |
|       color = "red";
 | |
|       break;
 | |
|     case "wait":
 | |
|       s = Locale.Sd.Status.Wait;
 | |
|       color = "yellow";
 | |
|       break;
 | |
|     case "running":
 | |
|       s = Locale.Sd.Status.Running;
 | |
|       color = "blue";
 | |
|       break;
 | |
|     default:
 | |
|       s = item.status.toUpperCase();
 | |
|   }
 | |
|   return (
 | |
|     <p className={styles["line-1"]} title={item.error} style={{ color: color }}>
 | |
|       <span>
 | |
|         {Locale.Sd.Status.Name}: {s}
 | |
|       </span>
 | |
|       {item.status === "error" && (
 | |
|         <span
 | |
|           className="clickable"
 | |
|           onClick={() => {
 | |
|             showModal({
 | |
|               title: Locale.Sd.Detail,
 | |
|               children: (
 | |
|                 <div style={{ color: color, userSelect: "text" }}>
 | |
|                   {item.error}
 | |
|                 </div>
 | |
|               ),
 | |
|             });
 | |
|           }}
 | |
|         >
 | |
|           - {item.error}
 | |
|         </span>
 | |
|       )}
 | |
|     </p>
 | |
|   );
 | |
| }
 | |
| 
 | |
| export function Sd() {
 | |
|   const isMobileScreen = useMobileScreen();
 | |
|   const navigate = useNavigate();
 | |
|   const location = useLocation();
 | |
|   const clientConfig = useMemo(() => getClientConfig(), []);
 | |
|   const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
 | |
|   const config = useAppConfig();
 | |
|   const scrollRef = useRef<HTMLDivElement>(null);
 | |
|   const sdStore = useSdStore();
 | |
|   const [sdImages, setSdImages] = useState(sdStore.draw);
 | |
|   const isSd = location.pathname === Path.Sd;
 | |
| 
 | |
|   useEffect(() => {
 | |
|     setSdImages(sdStore.draw);
 | |
|   }, [sdStore.currentId]);
 | |
| 
 | |
|   return (
 | |
|     <>
 | |
|       <SideBar className={clsx({ [homeStyles["sidebar-show"]]: isSd })} />
 | |
|       <WindowContent>
 | |
|         <div className={chatStyles.chat} key={"1"}>
 | |
|           <div className="window-header" data-tauri-drag-region>
 | |
|             {isMobileScreen && (
 | |
|               <div className="window-actions">
 | |
|                 <div className={"window-action-button"}>
 | |
|                   <IconButton
 | |
|                     icon={<ReturnIcon />}
 | |
|                     bordered
 | |
|                     title={Locale.Chat.Actions.ChatList}
 | |
|                     onClick={() => navigate(Path.Sd)}
 | |
|                   />
 | |
|                 </div>
 | |
|               </div>
 | |
|             )}
 | |
|             <div
 | |
|               className={clsx(
 | |
|                 "window-header-title",
 | |
|                 chatStyles["chat-body-title"],
 | |
|               )}
 | |
|             >
 | |
|               <div className={`window-header-main-title`}>Stability AI</div>
 | |
|               <div className="window-header-sub-title">
 | |
|                 {Locale.Sd.SubTitle(sdImages.length || 0)}
 | |
|               </div>
 | |
|             </div>
 | |
| 
 | |
|             <div className="window-actions">
 | |
|               {showMaxIcon && (
 | |
|                 <div className="window-action-button">
 | |
|                   <IconButton
 | |
|                     aria={Locale.Chat.Actions.FullScreen}
 | |
|                     icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
 | |
|                     bordered
 | |
|                     onClick={() => {
 | |
|                       config.update(
 | |
|                         (config) => (config.tightBorder = !config.tightBorder),
 | |
|                       );
 | |
|                     }}
 | |
|                   />
 | |
|                 </div>
 | |
|               )}
 | |
|               {isMobileScreen && <SDIcon width={50} height={50} />}
 | |
|             </div>
 | |
|           </div>
 | |
|           <div className={chatStyles["chat-body"]} ref={scrollRef}>
 | |
|             <div className={styles["sd-img-list"]}>
 | |
|               {sdImages.length > 0 ? (
 | |
|                 sdImages.map((item: any) => {
 | |
|                   return (
 | |
|                     <div
 | |
|                       key={item.id}
 | |
|                       style={{ display: "flex" }}
 | |
|                       className={styles["sd-img-item"]}
 | |
|                     >
 | |
|                       {item.status === "success" ? (
 | |
|                         <img
 | |
|                           className={styles["img"]}
 | |
|                           src={item.img_data}
 | |
|                           alt={item.id}
 | |
|                           onClick={(e) =>
 | |
|                             showImageModal(
 | |
|                               item.img_data,
 | |
|                               true,
 | |
|                               isMobileScreen
 | |
|                                 ? { width: "100%", height: "fit-content" }
 | |
|                                 : { maxWidth: "100%", maxHeight: "100%" },
 | |
|                               isMobileScreen
 | |
|                                 ? { width: "100%", height: "fit-content" }
 | |
|                                 : { width: "100%", height: "100%" },
 | |
|                             )
 | |
|                           }
 | |
|                         />
 | |
|                       ) : item.status === "error" ? (
 | |
|                         <div className={styles["pre-img"]}>
 | |
|                           <ErrorIcon />
 | |
|                         </div>
 | |
|                       ) : (
 | |
|                         <div className={styles["pre-img"]}>
 | |
|                           <LoadingIcon />
 | |
|                         </div>
 | |
|                       )}
 | |
|                       <div
 | |
|                         style={{ marginLeft: "10px" }}
 | |
|                         className={styles["sd-img-item-info"]}
 | |
|                       >
 | |
|                         <p className={styles["line-1"]}>
 | |
|                           {Locale.SdPanel.Prompt}:{" "}
 | |
|                           <span
 | |
|                             className="clickable"
 | |
|                             title={item.params.prompt}
 | |
|                             onClick={() => {
 | |
|                               showModal({
 | |
|                                 title: Locale.Sd.Detail,
 | |
|                                 children: (
 | |
|                                   <div style={{ userSelect: "text" }}>
 | |
|                                     {item.params.prompt}
 | |
|                                   </div>
 | |
|                                 ),
 | |
|                               });
 | |
|                             }}
 | |
|                           >
 | |
|                             {item.params.prompt}
 | |
|                           </span>
 | |
|                         </p>
 | |
|                         <p>
 | |
|                           {Locale.SdPanel.AIModel}: {item.model_name}
 | |
|                         </p>
 | |
|                         {getSdTaskStatus(item)}
 | |
|                         <p>{item.created_at}</p>
 | |
|                         <div className={chatStyles["chat-message-actions"]}>
 | |
|                           <div className={chatStyles["chat-input-actions"]}>
 | |
|                             <ChatAction
 | |
|                               text={Locale.Sd.Actions.Params}
 | |
|                               icon={<PromptIcon />}
 | |
|                               onClick={() => {
 | |
|                                 showModal({
 | |
|                                   title: Locale.Sd.GenerateParams,
 | |
|                                   children: (
 | |
|                                     <div style={{ userSelect: "text" }}>
 | |
|                                       {Object.keys(item.params).map((key) => {
 | |
|                                         let label = key;
 | |
|                                         let value = item.params[key];
 | |
|                                         switch (label) {
 | |
|                                           case "prompt":
 | |
|                                             label = Locale.SdPanel.Prompt;
 | |
|                                             break;
 | |
|                                           case "negative_prompt":
 | |
|                                             label =
 | |
|                                               Locale.SdPanel.NegativePrompt;
 | |
|                                             break;
 | |
|                                           case "aspect_ratio":
 | |
|                                             label = Locale.SdPanel.AspectRatio;
 | |
|                                             break;
 | |
|                                           case "seed":
 | |
|                                             label = "Seed";
 | |
|                                             value = value || 0;
 | |
|                                             break;
 | |
|                                           case "output_format":
 | |
|                                             label = Locale.SdPanel.OutFormat;
 | |
|                                             value = value?.toUpperCase();
 | |
|                                             break;
 | |
|                                           case "style":
 | |
|                                             label = Locale.SdPanel.ImageStyle;
 | |
|                                             value = params
 | |
|                                               .find(
 | |
|                                                 (item) =>
 | |
|                                                   item.value === "style",
 | |
|                                               )
 | |
|                                               ?.options?.find(
 | |
|                                                 (item) => item.value === value,
 | |
|                                               )?.name;
 | |
|                                             break;
 | |
|                                           default:
 | |
|                                             break;
 | |
|                                         }
 | |
| 
 | |
|                                         return (
 | |
|                                           <div
 | |
|                                             key={key}
 | |
|                                             style={{ margin: "10px" }}
 | |
|                                           >
 | |
|                                             <strong>{label}: </strong>
 | |
|                                             {value}
 | |
|                                           </div>
 | |
|                                         );
 | |
|                                       })}
 | |
|                                     </div>
 | |
|                                   ),
 | |
|                                 });
 | |
|                               }}
 | |
|                             />
 | |
|                             <ChatAction
 | |
|                               text={Locale.Sd.Actions.Copy}
 | |
|                               icon={<CopyIcon />}
 | |
|                               onClick={() =>
 | |
|                                 copyToClipboard(
 | |
|                                   getMessageTextContent({
 | |
|                                     role: "user",
 | |
|                                     content: item.params.prompt,
 | |
|                                   }),
 | |
|                                 )
 | |
|                               }
 | |
|                             />
 | |
|                             <ChatAction
 | |
|                               text={Locale.Sd.Actions.Retry}
 | |
|                               icon={<ResetIcon />}
 | |
|                               onClick={() => {
 | |
|                                 const reqData = {
 | |
|                                   model: item.model,
 | |
|                                   model_name: item.model_name,
 | |
|                                   status: "wait",
 | |
|                                   params: { ...item.params },
 | |
|                                   created_at: new Date().toLocaleString(),
 | |
|                                   img_data: "",
 | |
|                                 };
 | |
|                                 sdStore.sendTask(reqData);
 | |
|                               }}
 | |
|                             />
 | |
|                             <ChatAction
 | |
|                               text={Locale.Sd.Actions.Delete}
 | |
|                               icon={<DeleteIcon />}
 | |
|                               onClick={async () => {
 | |
|                                 if (
 | |
|                                   await showConfirm(Locale.Sd.Danger.Delete)
 | |
|                                 ) {
 | |
|                                   // remove img_data + remove item in list
 | |
|                                   removeImage(item.img_data).finally(() => {
 | |
|                                     sdStore.draw = sdImages.filter(
 | |
|                                       (i: any) => i.id !== item.id,
 | |
|                                     );
 | |
|                                     sdStore.getNextId();
 | |
|                                   });
 | |
|                                 }
 | |
|                               }}
 | |
|                             />
 | |
|                           </div>
 | |
|                         </div>
 | |
|                       </div>
 | |
|                     </div>
 | |
|                   );
 | |
|                 })
 | |
|               ) : (
 | |
|                 <div>{Locale.Sd.EmptyRecord}</div>
 | |
|               )}
 | |
|             </div>
 | |
|           </div>
 | |
|         </div>
 | |
|       </WindowContent>
 | |
|     </>
 | |
|   );
 | |
| }
 |