import chatStyles from "@/app/components/chat.module.scss"; import styles from "@/app/components/sd.module.scss"; import { IconButton } from "@/app/components/button"; import ReturnIcon from "@/app/icons/return.svg"; import Locale from "@/app/locales"; import { Path, StoreKey } from "@/app/constant"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { copyToClipboard, getMessageTextContent, useMobileScreen, } from "@/app/utils"; import { useNavigate } 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 { useIndexedDB } from "react-indexed-db-hook"; import { sendSdTask, useSdStore } from "@/app/store/sd"; import locales from "@/app/locales"; import LoadingIcon from "../icons/three-dots.svg"; import ErrorIcon from "../icons/delete.svg"; import { Property } from "csstype"; import { showConfirm, showImageModal, showModal, } from "@/app/components/ui-lib"; function getBase64ImgUrl(base64Data: string, contentType: string) { const byteCharacters = atob(base64Data); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: contentType }); return URL.createObjectURL(blob); } 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> {locales.Sd.Status.Name}: {s} </span> {item.status === "error" && ( <span className="clickable" onClick={() => { showModal({ title: locales.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 clientConfig = useMemo(() => getClientConfig(), []); const showMaxIcon = !isMobileScreen && !clientConfig?.isApp; const config = useAppConfig(); const scrollRef = useRef<HTMLDivElement>(null); const sdListDb = useIndexedDB(StoreKey.SdList); const [sdImages, setSdImages] = useState([]); const { execCount, execCountInc } = useSdStore(); useEffect(() => { sdListDb.getAll().then((data) => { setSdImages(((data as never[]) || []).reverse()); }); }, [execCount]); return ( <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.SdPanel)} /> </div> </div> )} <div className={`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 icon={config.tightBorder ? <MinIcon /> : <MaxIcon />} bordered onClick={() => { config.update( (config) => (config.tightBorder = !config.tightBorder), ); }} /> </div> )} </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={`data:image/png;base64,${item.img_data}`} alt={`${item.id}`} onClick={(e) => { showImageModal( getBase64ImgUrl(item.img_data, "image/png"), true, ); }} /> ) : 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"]}> {locales.SdPanel.Prompt}:{" "} <span className="clickable" title={item.params.prompt} onClick={() => { showModal({ title: locales.Sd.Detail, children: ( <div style={{ userSelect: "text" }}> {item.params.prompt} </div> ), }); }} > {item.params.prompt} </span> </p> <p> {locales.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: locales.Sd.GenerateParams, children: ( <div style={{ userSelect: "text" }}> {Object.keys(item.params).map((key) => ( <div key={key} style={{ margin: "10px" }}> <strong>{key}: </strong> {item.params[key]} </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: "", }; sendSdTask(reqData, sdListDb, execCountInc); }} /> <ChatAction text={Locale.Sd.Actions.Delete} icon={<DeleteIcon />} onClick={async () => { if (await showConfirm(Locale.Sd.Danger.Delete)) { sdListDb.deleteRecord(item.id).then( () => { setSdImages( sdImages.filter( (i: any) => i.id !== item.id, ), ); }, (error) => { console.error(error); }, ); } }} /> </div> </div> </div> </div> ); }) ) : ( <div>{locales.Sd.EmptyRecord}</div> )} </div> </div> </div> ); }