Dogtiti пре 1 година
родитељ
комит
9d55adbaf2

+ 2 - 0
.gitignore

@@ -44,3 +44,5 @@ dev
 
 *.key
 *.key.pub
+
+masks.json

+ 40 - 29
app/components/home.tsx

@@ -59,7 +59,7 @@ const Sd = dynamic(async () => (await import("./sd")).Sd, {
   loading: () => <Loading noLogo />,
 });
 
-const SdPanel = dynamic(async () => (await import("./sd-panel")).SdPanel, {
+const SdPanel = dynamic(async () => (await import("./sd")).SdPanel, {
   loading: () => <Loading noLogo />,
 });
 
@@ -130,12 +130,22 @@ const loadAsyncGoogleFont = () => {
   document.head.appendChild(linkEl);
 };
 
+export function WindowContent(props: { children: React.ReactNode }) {
+  return (
+    <div className={styles["window-content"]} id={SlotID.AppBody}>
+      {props?.children}
+    </div>
+  );
+}
+
 function Screen() {
   const config = useAppConfig();
   const location = useLocation();
-  const isHome =
-    location.pathname === Path.Home || location.pathname === Path.SdPanel;
+  const isHome = location.pathname === Path.Home;
   const isAuth = location.pathname === Path.Auth;
+  const isSd = location.pathname === Path.Sd;
+  const isSdPanel = location.pathname === Path.SdPanel;
+
   const isMobileScreen = useMobileScreen();
   const shouldTightBorder =
     getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
@@ -143,35 +153,36 @@ function Screen() {
   useEffect(() => {
     loadAsyncGoogleFont();
   }, []);
+
+  const renderContent = () => {
+    if (isAuth) return <AuthPage />;
+    if (isSd) return <Sd />;
+    if (isSdPanel) return <SdPanel />;
+    return (
+      <>
+        <SideBar className={isHome ? styles["sidebar-show"] : ""} />
+        <WindowContent>
+          <Routes>
+            <Route path={Path.Home} element={<Chat />} />
+            <Route path={Path.NewChat} element={<NewChat />} />
+            <Route path={Path.Masks} element={<MaskPage />} />
+            <Route path={Path.Chat} element={<Chat />} />
+            <Route path={Path.Sd} element={<Sd />} />
+            <Route path={Path.SdPanel} element={<Sd />} />
+            <Route path={Path.Settings} element={<Settings />} />
+          </Routes>
+        </WindowContent>
+      </>
+    );
+  };
+
   return (
     <div
-      className={
-        styles.container +
-        ` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
-          getLang() === "ar" ? styles["rtl-screen"] : ""
-        }`
-      }
+      className={`${styles.container} ${
+        shouldTightBorder ? styles["tight-container"] : styles.container
+      } ${getLang() === "ar" ? styles["rtl-screen"] : ""}`}
     >
-      {isAuth ? (
-        <>
-          <AuthPage />
-        </>
-      ) : (
-        <>
-          <SideBar className={isHome ? styles["sidebar-show"] : ""} />
-          <div className={styles["window-content"]} id={SlotID.AppBody}>
-            <Routes>
-              <Route path={Path.Home} element={<Chat />} />
-              <Route path={Path.NewChat} element={<NewChat />} />
-              <Route path={Path.Masks} element={<MaskPage />} />
-              <Route path={Path.Chat} element={<Chat />} />
-              <Route path={Path.Sd} element={<Sd />} />
-              <Route path={Path.SdPanel} element={<Sd />} />
-              <Route path={Path.Settings} element={<Settings />} />
-            </Routes>
-          </div>
-        </>
-      )}
+      {renderContent()}
     </div>
   );
 }

+ 0 - 278
app/components/sd.tsx

@@ -1,278 +0,0 @@
-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 } 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 { 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";
-import { removeImage } from "@/app/utils/chat";
-
-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 sdStore = useSdStore();
-  const [sdImages, setSdImages] = useState(sdStore.draw);
-
-  useEffect(() => {
-    setSdImages(sdStore.draw);
-  }, [sdStore.currentId]);
-
-  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={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"]}>
-                      {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: "",
-                            };
-                            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>{locales.Sd.EmptyRecord}</div>
-          )}
-        </div>
-      </div>
-    </div>
-  );
-}

+ 2 - 0
app/components/sd/index.tsx

@@ -0,0 +1,2 @@
+export * from "./sd";
+export * from "./sd-panel";

+ 17 - 5
app/components/sd-panel.module.scss → app/components/sd/sd-panel.module.scss

@@ -10,7 +10,7 @@
     display: flex;
     align-items: center;
 
-    .ctrl-param-item-title{
+    .ctrl-param-item-title {
       font-size: 14px;
       font-weight: bolder;
       margin-bottom: 5px;
@@ -22,12 +22,24 @@
     font-weight: normal;
     margin-top: 3px;
   }
+  textarea {
+    appearance: none;
+    border-radius: 10px;
+    border: var(--border-in-light);
+    min-height: 36px;
+    box-sizing: border-box;
+    background: var(--white);
+    color: var(--black);
+    padding: 0 10px;
+    max-width: 50%;
+    font-family: inherit;
+  }
 }
 
-.ai-models{
-  button{
+.ai-models {
+  button {
     margin-bottom: 10px;
-    padding:10px;
+    padding: 10px;
     width: 100%;
   }
-}
+}

+ 29 - 29
app/components/sd-panel.tsx → app/components/sd/sd-panel.tsx

@@ -2,7 +2,7 @@ import styles from "./sd-panel.module.scss";
 import React, { useState } from "react";
 import { Select, showToast } from "@/app/components/ui-lib";
 import { IconButton } from "@/app/components/button";
-import locales from "@/app/locales";
+import Locale from "@/app/locales";
 import { nanoid } from "nanoid";
 import { StoreKey } from "@/app/constant";
 import { useSdStore } from "@/app/store/sd";
@@ -10,14 +10,14 @@ import { useSdStore } from "@/app/store/sd";
 const sdCommonParams = (model: string, data: any) => {
   return [
     {
-      name: locales.SdPanel.Prompt,
+      name: Locale.SdPanel.Prompt,
       value: "prompt",
       type: "textarea",
-      placeholder: locales.SdPanel.PleaseInput(locales.SdPanel.Prompt),
+      placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
       required: true,
     },
     {
-      name: locales.SdPanel.ModelVersion,
+      name: Locale.SdPanel.ModelVersion,
       value: "model",
       type: "select",
       default: "sd3-medium",
@@ -29,13 +29,13 @@ const sdCommonParams = (model: string, data: any) => {
       ],
     },
     {
-      name: locales.SdPanel.NegativePrompt,
+      name: Locale.SdPanel.NegativePrompt,
       value: "negative_prompt",
       type: "textarea",
-      placeholder: locales.SdPanel.PleaseInput(locales.SdPanel.NegativePrompt),
+      placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
     },
     {
-      name: locales.SdPanel.AspectRatio,
+      name: Locale.SdPanel.AspectRatio,
       value: "aspect_ratio",
       type: "select",
       default: "1:1",
@@ -52,32 +52,32 @@ const sdCommonParams = (model: string, data: any) => {
       ],
     },
     {
-      name: locales.SdPanel.ImageStyle,
+      name: Locale.SdPanel.ImageStyle,
       value: "style",
       type: "select",
       default: "3d",
       support: ["core"],
       options: [
-        { name: locales.SdPanel.Styles.D3Model, value: "3d-model" },
-        { name: locales.SdPanel.Styles.AnalogFilm, value: "analog-film" },
-        { name: locales.SdPanel.Styles.Anime, value: "anime" },
-        { name: locales.SdPanel.Styles.Cinematic, value: "cinematic" },
-        { name: locales.SdPanel.Styles.ComicBook, value: "comic-book" },
-        { name: locales.SdPanel.Styles.DigitalArt, value: "digital-art" },
-        { name: locales.SdPanel.Styles.Enhance, value: "enhance" },
-        { name: locales.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
-        { name: locales.SdPanel.Styles.Isometric, value: "isometric" },
-        { name: locales.SdPanel.Styles.LineArt, value: "line-art" },
-        { name: locales.SdPanel.Styles.LowPoly, value: "low-poly" },
+        { name: Locale.SdPanel.Styles.D3Model, value: "3d-model" },
+        { name: Locale.SdPanel.Styles.AnalogFilm, value: "analog-film" },
+        { name: Locale.SdPanel.Styles.Anime, value: "anime" },
+        { name: Locale.SdPanel.Styles.Cinematic, value: "cinematic" },
+        { name: Locale.SdPanel.Styles.ComicBook, value: "comic-book" },
+        { name: Locale.SdPanel.Styles.DigitalArt, value: "digital-art" },
+        { name: Locale.SdPanel.Styles.Enhance, value: "enhance" },
+        { name: Locale.SdPanel.Styles.FantasyArt, value: "fantasy-art" },
+        { name: Locale.SdPanel.Styles.Isometric, value: "isometric" },
+        { name: Locale.SdPanel.Styles.LineArt, value: "line-art" },
+        { name: Locale.SdPanel.Styles.LowPoly, value: "low-poly" },
         {
-          name: locales.SdPanel.Styles.ModelingCompound,
+          name: Locale.SdPanel.Styles.ModelingCompound,
           value: "modeling-compound",
         },
-        { name: locales.SdPanel.Styles.NeonPunk, value: "neon-punk" },
-        { name: locales.SdPanel.Styles.Origami, value: "origami" },
-        { name: locales.SdPanel.Styles.Photographic, value: "photographic" },
-        { name: locales.SdPanel.Styles.PixelArt, value: "pixel-art" },
-        { name: locales.SdPanel.Styles.TileTexture, value: "tile-texture" },
+        { name: Locale.SdPanel.Styles.NeonPunk, value: "neon-punk" },
+        { name: Locale.SdPanel.Styles.Origami, value: "origami" },
+        { name: Locale.SdPanel.Styles.Photographic, value: "photographic" },
+        { name: Locale.SdPanel.Styles.PixelArt, value: "pixel-art" },
+        { name: Locale.SdPanel.Styles.TileTexture, value: "tile-texture" },
       ],
     },
     {
@@ -89,7 +89,7 @@ const sdCommonParams = (model: string, data: any) => {
       max: 4294967294,
     },
     {
-      name: locales.SdPanel.OutFormat,
+      name: Locale.SdPanel.OutFormat,
       value: "output_format",
       type: "select",
       default: "png",
@@ -292,7 +292,7 @@ export function SdPanel() {
       reqParams[item.value] = params[item.value] ?? null;
       if (item.required) {
         if (!reqParams[item.value]) {
-          showToast(locales.SdPanel.ParamIsRequired(item.name));
+          showToast(Locale.SdPanel.ParamIsRequired(item.name));
           return;
         }
       }
@@ -311,7 +311,7 @@ export function SdPanel() {
   };
   return (
     <>
-      <ControlParamItem title={locales.SdPanel.AIModel}>
+      <ControlParamItem title={Locale.SdPanel.AIModel}>
         <div className={styles["ai-models"]}>
           {models.map((item) => {
             return (
@@ -332,7 +332,7 @@ export function SdPanel() {
         onChange={handleValueChange}
       ></ControlParam>
       <IconButton
-        text={locales.SdPanel.Submit}
+        text={Locale.SdPanel.Submit}
         type="primary"
         style={{ marginTop: "20px" }}
         shadow

+ 65 - 0
app/components/sd/sd-sidebar.tsx

@@ -0,0 +1,65 @@
+import styles from "@/app/components/home.module.scss";
+
+import { IconButton } from "@/app/components/button";
+import GithubIcon from "@/app/icons/github.svg";
+import SDIcon from "@/app/icons/sd.svg";
+import ReturnIcon from "@/app/icons/return.svg";
+import Locale from "@/app/locales";
+
+import { Path, REPO_URL } from "@/app/constant";
+
+import { useNavigate } from "react-router-dom";
+import dynamic from "next/dynamic";
+import {
+  SideBarContainer,
+  SideBarBody,
+  useDragSideBar,
+  useHotKey,
+} from "@/app/components/sidebar";
+
+const SdPanel = dynamic(
+  async () => (await import("@/app/components/sd/sd-panel")).SdPanel,
+  {
+    loading: () => null,
+  },
+);
+
+export function SideBar(props: { className?: string }) {
+  useHotKey();
+  const { onDragStart, shouldNarrow } = useDragSideBar();
+  const navigate = useNavigate();
+
+  return (
+    <SideBarContainer
+      onDragStart={onDragStart}
+      shouldNarrow={shouldNarrow}
+      {...props}
+    >
+      <div className={styles["sidebar-header"]} data-tauri-drag-region>
+        <div className={styles["sidebar-title"]} data-tauri-drag-region>
+          <IconButton
+            icon={<ReturnIcon />}
+            bordered
+            title={Locale.Chat.Actions.ChatList}
+            onClick={() => navigate(Path.Chat)}
+          />
+        </div>
+        <div className={styles["sidebar-logo"] + " no-dark"}>
+          <SDIcon width={38} height={38} />
+        </div>
+      </div>
+      <SideBarBody>
+        <SdPanel />
+      </SideBarBody>
+      <div className={styles["sidebar-tail"]}>
+        <div className={styles["sidebar-actions"]}>
+          <div className={styles["sidebar-action"]}>
+            <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
+              <IconButton icon={<GithubIcon />} shadow />
+            </a>
+          </div>
+        </div>
+      </div>
+    </SideBarContainer>
+  );
+}

+ 0 - 0
app/components/sd.module.scss → app/components/sd/sd.module.scss


+ 292 - 0
app/components/sd/sd.tsx

@@ -0,0 +1,292 @@
+import chatStyles from "@/app/components/chat.module.scss";
+import styles from "@/app/components/sd/sd.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 } 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 locales from "@/app/locales";
+import LoadingIcon from "@/app/icons/three-dots.svg";
+import ErrorIcon from "@/app/icons/delete.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";
+
+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 sdStore = useSdStore();
+  const [sdImages, setSdImages] = useState(sdStore.draw);
+
+  useEffect(() => {
+    setSdImages(sdStore.draw);
+  }, [sdStore.currentId]);
+
+  return (
+    <>
+      <SideBar />
+      <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.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={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"]}>
+                          {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: "",
+                                };
+                                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>{locales.Sd.EmptyRecord}</div>
+              )}
+            </div>
+          </div>
+        </div>
+      </WindowContent>
+    </>
+  );
+}

+ 119 - 98
app/components/sidebar.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useMemo, useState } from "react";
+import { useEffect, useRef, useMemo, useState } from "react";
 
 import styles from "./home.module.scss";
 
@@ -15,7 +15,7 @@ import DragIcon from "../icons/drag.svg";
 
 import Locale from "../locales";
 
-import { ModelType, useAppConfig, useChatStore } from "../store";
+import { useAppConfig, useChatStore } from "../store";
 
 import {
   DEFAULT_SIDEBAR_WIDTH,
@@ -27,20 +27,16 @@ import {
   REPO_URL,
 } from "../constant";
 
-import { Link, useLocation, useNavigate } from "react-router-dom";
+import { Link, useNavigate } from "react-router-dom";
 import { isIOS, useMobileScreen } from "../utils";
 import dynamic from "next/dynamic";
-import { Selector, showConfirm, showToast } from "./ui-lib";
+import { showConfirm, Selector } from "./ui-lib";
 
 const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
   loading: () => null,
 });
 
-const SdPanel = dynamic(async () => (await import("./sd-panel")).SdPanel, {
-  loading: () => null,
-});
-
-function useHotKey() {
+export function useHotKey() {
   const chatStore = useChatStore();
 
   useEffect(() => {
@@ -59,7 +55,7 @@ function useHotKey() {
   });
 }
 
-function useDragSideBar() {
+export function useDragSideBar() {
   const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
 
   const config = useAppConfig();
@@ -132,39 +128,21 @@ function useDragSideBar() {
     shouldNarrow,
   };
 }
-
-export function SideBar(props: { className?: string }) {
-  const chatStore = useChatStore();
-
-  // drag side bar
-  const { onDragStart, shouldNarrow } = useDragSideBar();
-  const navigate = useNavigate();
-  const config = useAppConfig();
+export function SideBarContainer(props: {
+  children: React.ReactNode;
+  onDragStart: (e: MouseEvent) => void;
+  shouldNarrow: boolean;
+  className?: string;
+}) {
   const isMobileScreen = useMobileScreen();
   const isIOSMobile = useMemo(
     () => isIOS() && isMobileScreen,
     [isMobileScreen],
   );
-  const [showPluginSelector, setShowPluginSelector] = useState(false);
-  const location = useLocation();
-
-  useHotKey();
-
-  let bodyComponent: React.JSX.Element;
-  let isChat: boolean = false;
-  switch (location.pathname) {
-    case Path.Sd:
-    case Path.SdPanel:
-      bodyComponent = <SdPanel />;
-      break;
-    default:
-      isChat = true;
-      bodyComponent = <ChatList narrow={shouldNarrow} />;
-  }
-  // @ts-ignore
+  const { children, className, onDragStart, shouldNarrow } = props;
   return (
     <div
-      className={`${styles.sidebar} ${props.className} ${
+      className={`${styles.sidebar} ${className} ${
         shouldNarrow && styles["narrow-sidebar"]
       }`}
       style={{
@@ -172,6 +150,25 @@ export function SideBar(props: { className?: string }) {
         transition: isMobileScreen && isIOSMobile ? "none" : undefined,
       }}
     >
+      {children}
+      <div
+        className={styles["sidebar-drag"]}
+        onPointerDown={(e) => onDragStart(e as any)}
+      >
+        <DragIcon />
+      </div>
+    </div>
+  );
+}
+
+export function SideBarHeader(props: { shouldNarrow: boolean }) {
+  const navigate = useNavigate();
+  const config = useAppConfig();
+  const { shouldNarrow } = props;
+  const [showPluginSelector, setShowPluginSelector] = useState(false);
+
+  return (
+    <>
       <div className={styles["sidebar-header"]} data-tauri-drag-region>
         <div className={styles["sidebar-title"]} data-tauri-drag-region>
           NextChat
@@ -206,68 +203,6 @@ export function SideBar(props: { className?: string }) {
           shadow
         />
       </div>
-
-      <div
-        className={styles["sidebar-body"]}
-        onClick={(e) => {
-          if (isChat && e.target === e.currentTarget) {
-            navigate(Path.Home);
-          }
-        }}
-      >
-        {bodyComponent}
-      </div>
-
-      <div className={styles["sidebar-tail"]}>
-        <div className={styles["sidebar-actions"]}>
-          {isChat && (
-            <div className={styles["sidebar-action"] + " " + styles.mobile}>
-              <IconButton
-                icon={<DeleteIcon />}
-                onClick={async () => {
-                  if (await showConfirm(Locale.Home.DeleteChat)) {
-                    chatStore.deleteSession(chatStore.currentSessionIndex);
-                  }
-                }}
-              />
-            </div>
-          )}
-          <div className={styles["sidebar-action"]}>
-            <Link to={Path.Settings}>
-              <IconButton icon={<SettingsIcon />} shadow />
-            </Link>
-          </div>
-          <div className={styles["sidebar-action"]}>
-            <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
-              <IconButton icon={<GithubIcon />} shadow />
-            </a>
-          </div>
-        </div>
-        {isChat && (
-          <div>
-            <IconButton
-              icon={<AddIcon />}
-              text={shouldNarrow ? undefined : Locale.Home.NewChat}
-              onClick={() => {
-                if (config.dontShowMaskSplashScreen) {
-                  chatStore.newSession();
-                  navigate(Path.Chat);
-                } else {
-                  navigate(Path.NewChat);
-                }
-              }}
-              shadow
-            />
-          </div>
-        )}
-      </div>
-
-      <div
-        className={styles["sidebar-drag"]}
-        onPointerDown={(e) => onDragStart(e as any)}
-      >
-        <DragIcon />
-      </div>
       {showPluginSelector && (
         <Selector
           items={[
@@ -289,6 +224,92 @@ export function SideBar(props: { className?: string }) {
           }}
         />
       )}
+    </>
+  );
+}
+
+export function SideBarBody(props: {
+  children: React.ReactNode;
+  onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
+}) {
+  const { onClick, children } = props;
+  return (
+    <div className={styles["sidebar-body"]} onClick={onClick}>
+      {children}
     </div>
   );
 }
+
+export function SideBarTail(props: { shouldNarrow: boolean }) {
+  const { shouldNarrow } = props;
+  const chatStore = useChatStore();
+  const navigate = useNavigate();
+  const config = useAppConfig();
+  return (
+    <div className={styles["sidebar-tail"]}>
+      <div className={styles["sidebar-actions"]}>
+        <div className={styles["sidebar-action"] + " " + styles.mobile}>
+          <IconButton
+            icon={<DeleteIcon />}
+            onClick={async () => {
+              if (await showConfirm(Locale.Home.DeleteChat)) {
+                chatStore.deleteSession(chatStore.currentSessionIndex);
+              }
+            }}
+          />
+        </div>
+        <div className={styles["sidebar-action"]}>
+          <Link to={Path.Settings}>
+            <IconButton icon={<SettingsIcon />} shadow />
+          </Link>
+        </div>
+        <div className={styles["sidebar-action"]}>
+          <a href={REPO_URL} target="_blank" rel="noopener noreferrer">
+            <IconButton icon={<GithubIcon />} shadow />
+          </a>
+        </div>
+      </div>
+      <div>
+        <IconButton
+          icon={<AddIcon />}
+          text={shouldNarrow ? undefined : Locale.Home.NewChat}
+          onClick={() => {
+            if (config.dontShowMaskSplashScreen) {
+              chatStore.newSession();
+              navigate(Path.Chat);
+            } else {
+              navigate(Path.NewChat);
+            }
+          }}
+          shadow
+        />
+      </div>
+    </div>
+  );
+}
+
+export function SideBar(props: { className?: string }) {
+  useHotKey();
+  const { onDragStart, shouldNarrow } = useDragSideBar();
+  const navigate = useNavigate();
+
+  return (
+    <SideBarContainer
+      onDragStart={onDragStart}
+      shouldNarrow={shouldNarrow}
+      {...props}
+    >
+      <SideBarHeader shouldNarrow={shouldNarrow} />
+      <SideBarBody
+        onClick={(e) => {
+          if (e.target === e.currentTarget) {
+            navigate(Path.Home);
+          }
+        }}
+      >
+        <ChatList narrow={shouldNarrow} />
+      </SideBarBody>
+      <SideBarTail shouldNarrow={shouldNarrow} />
+    </SideBarContainer>
+  );
+}

+ 12 - 0
app/icons/sd.svg

@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1.21em" height="1em" viewBox="0 0 256 213">
+    <defs>
+        <linearGradient id="logosStabilityAiIcon0" x1="50%" x2="50%" y1="0%" y2="100%">
+            <stop offset="0%" stop-color="#9d39ff" />
+            <stop offset="100%" stop-color="#a380ff" />
+        </linearGradient>
+    </defs>
+    <path fill="url(#logosStabilityAiIcon0)"
+        d="M72.418 212.45c49.478 0 81.658-26.205 81.658-65.626c0-30.572-19.572-49.998-54.569-58.043l-22.469-6.74c-19.71-4.424-31.215-9.738-28.505-23.312c2.255-11.292 9.002-17.667 24.69-17.667c49.872 0 68.35 17.667 68.35 17.667V16.237S123.583 0 73.223 0C25.757 0 0 24.424 0 62.236c0 30.571 17.85 48.35 54.052 56.798q3.802.95 3.885.976q8.26 2.556 22.293 6.755c18.504 4.425 23.262 9.121 23.262 23.2c0 12.872-13.374 20.19-31.074 20.19C21.432 170.154 0 144.36 0 144.36v47.078s13.402 21.01 72.418 21.01" />
+    <path fill="#e80000"
+        d="M225.442 209.266c17.515 0 30.558-12.67 30.558-29.812c0-17.515-12.67-29.813-30.558-29.813c-17.515 0-30.185 12.298-30.185 29.813s12.67 29.812 30.185 29.812" />
+</svg>

+ 1 - 2
app/styles/globals.scss

@@ -226,8 +226,7 @@ input[type="range"]::-ms-thumb:hover {
 
 input[type="number"],
 input[type="text"],
-input[type="password"],
-textarea {
+input[type="password"] {
   appearance: none;
   border-radius: 10px;
   border: var(--border-in-light);