You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			341 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
		
		
			
		
	
	
			341 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
| 
											9 months ago
										 | 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> | ||
|  |     </> | ||
|  |   ); | ||
|  | } |