refator: sd

This commit is contained in:
Dogtiti 2024-07-23 00:51:58 +08:00
parent e468fecf12
commit 9d55adbaf2
12 changed files with 579 additions and 441 deletions

2
.gitignore vendored
View File

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

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

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

View File

@ -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%;
}
}
}

View File

@ -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

View File

@ -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>
);
}

292
app/components/sd/sd.tsx Normal file
View File

@ -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>
</>
);
}

View File

@ -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
app/icons/sd.svg Normal file
View File

@ -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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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);