mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-09 04:02:41 +08:00
merge main
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import * as React from "react";
|
||||
|
||||
import styles from "./button.module.scss";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export type ButtonType = "primary" | "danger" | null;
|
||||
|
||||
@@ -16,6 +17,7 @@ export function IconButton(props: {
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
autoFocus?: boolean;
|
||||
style?: CSSProperties;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
@@ -31,6 +33,7 @@ export function IconButton(props: {
|
||||
role="button"
|
||||
tabIndex={props.tabIndex}
|
||||
autoFocus={props.autoFocus}
|
||||
style={props.style}
|
||||
>
|
||||
{props.icon && (
|
||||
<div
|
||||
|
@@ -340,7 +340,7 @@ function ClearContextDivider() {
|
||||
);
|
||||
}
|
||||
|
||||
function ChatAction(props: {
|
||||
export function ChatAction(props: {
|
||||
text: string;
|
||||
icon: JSX.Element;
|
||||
onClick: () => void;
|
||||
|
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { IconButton } from "./button";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
|
@@ -59,6 +59,10 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
const Sd = dynamic(async () => (await import("./sd")).Sd, {
|
||||
loading: () => <Loading noLogo />,
|
||||
});
|
||||
|
||||
export function useSwitchTheme() {
|
||||
const config = useAppConfig();
|
||||
|
||||
@@ -126,12 +130,23 @@ 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 isArtifact = location.pathname.includes(Path.Artifact);
|
||||
const isHome = location.pathname === Path.Home;
|
||||
const isAuth = location.pathname === Path.Auth;
|
||||
const isSd = location.pathname === Path.Sd;
|
||||
const isSdNew = location.pathname === Path.SdNew;
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const shouldTightBorder =
|
||||
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
|
||||
@@ -147,35 +162,33 @@ function Screen() {
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
const renderContent = () => {
|
||||
if (isAuth) return <AuthPage />;
|
||||
if (isSd) return <Sd />;
|
||||
if (isSdNew) return <Sd />;
|
||||
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.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.Settings} element={<Settings />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
2
app/components/sd/index.tsx
Normal file
2
app/components/sd/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./sd";
|
||||
export * from "./sd-panel";
|
45
app/components/sd/sd-panel.module.scss
Normal file
45
app/components/sd/sd-panel.module.scss
Normal file
@@ -0,0 +1,45 @@
|
||||
.ctrl-param-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-height: 40px;
|
||||
padding: 10px 0;
|
||||
animation: slide-in ease 0.6s;
|
||||
flex-direction: column;
|
||||
|
||||
.ctrl-param-item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.ctrl-param-item-title {
|
||||
font-size: 14px;
|
||||
font-weight: bolder;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.ctrl-param-item-sub-title {
|
||||
font-size: 12px;
|
||||
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 {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
317
app/components/sd/sd-panel.tsx
Normal file
317
app/components/sd/sd-panel.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
import styles from "./sd-panel.module.scss";
|
||||
import React from "react";
|
||||
import { Select } from "@/app/components/ui-lib";
|
||||
import { IconButton } from "@/app/components/button";
|
||||
import Locale from "@/app/locales";
|
||||
import { useSdStore } from "@/app/store/sd";
|
||||
|
||||
export const params = [
|
||||
{
|
||||
name: Locale.SdPanel.Prompt,
|
||||
value: "prompt",
|
||||
type: "textarea",
|
||||
placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.Prompt),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: Locale.SdPanel.ModelVersion,
|
||||
value: "model",
|
||||
type: "select",
|
||||
default: "sd3-medium",
|
||||
support: ["sd3"],
|
||||
options: [
|
||||
{ name: "SD3 Medium", value: "sd3-medium" },
|
||||
{ name: "SD3 Large", value: "sd3-large" },
|
||||
{ name: "SD3 Large Turbo", value: "sd3-large-turbo" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: Locale.SdPanel.NegativePrompt,
|
||||
value: "negative_prompt",
|
||||
type: "textarea",
|
||||
placeholder: Locale.SdPanel.PleaseInput(Locale.SdPanel.NegativePrompt),
|
||||
},
|
||||
{
|
||||
name: Locale.SdPanel.AspectRatio,
|
||||
value: "aspect_ratio",
|
||||
type: "select",
|
||||
default: "1:1",
|
||||
options: [
|
||||
{ name: "1:1", value: "1:1" },
|
||||
{ name: "16:9", value: "16:9" },
|
||||
{ name: "21:9", value: "21:9" },
|
||||
{ name: "2:3", value: "2:3" },
|
||||
{ name: "3:2", value: "3:2" },
|
||||
{ name: "4:5", value: "4:5" },
|
||||
{ name: "5:4", value: "5:4" },
|
||||
{ name: "9:16", value: "9:16" },
|
||||
{ name: "9:21", value: "9:21" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: Locale.SdPanel.ImageStyle,
|
||||
value: "style",
|
||||
type: "select",
|
||||
default: "3d-model",
|
||||
support: ["core"],
|
||||
options: [
|
||||
{ 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: Locale.SdPanel.Styles.ModelingCompound,
|
||||
value: "modeling-compound",
|
||||
},
|
||||
{ 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" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Seed",
|
||||
value: "seed",
|
||||
type: "number",
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 4294967294,
|
||||
},
|
||||
{
|
||||
name: Locale.SdPanel.OutFormat,
|
||||
value: "output_format",
|
||||
type: "select",
|
||||
default: "png",
|
||||
options: [
|
||||
{ name: "PNG", value: "png" },
|
||||
{ name: "JPEG", value: "jpeg" },
|
||||
{ name: "WebP", value: "webp" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const sdCommonParams = (model: string, data: any) => {
|
||||
return params.filter((item) => {
|
||||
return !(item.support && !item.support.includes(model));
|
||||
});
|
||||
};
|
||||
|
||||
export const models = [
|
||||
{
|
||||
name: "Stable Image Ultra",
|
||||
value: "ultra",
|
||||
params: (data: any) => sdCommonParams("ultra", data),
|
||||
},
|
||||
{
|
||||
name: "Stable Image Core",
|
||||
value: "core",
|
||||
params: (data: any) => sdCommonParams("core", data),
|
||||
},
|
||||
{
|
||||
name: "Stable Diffusion 3",
|
||||
value: "sd3",
|
||||
params: (data: any) => {
|
||||
return sdCommonParams("sd3", data).filter((item) => {
|
||||
return !(
|
||||
data.model === "sd3-large-turbo" && item.value == "negative_prompt"
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function ControlParamItem(props: {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
required?: boolean;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
|
||||
<div className={styles["ctrl-param-item-header"]}>
|
||||
<div className={styles["ctrl-param-item-title"]}>
|
||||
<div>
|
||||
{props.title}
|
||||
{props.required && <span style={{ color: "red" }}>*</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{props.children}
|
||||
{props.subTitle && (
|
||||
<div className={styles["ctrl-param-item-sub-title"]}>
|
||||
{props.subTitle}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ControlParam(props: {
|
||||
columns: any[];
|
||||
data: any;
|
||||
onChange: (field: string, val: any) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{props.columns?.map((item) => {
|
||||
let element: null | JSX.Element;
|
||||
switch (item.type) {
|
||||
case "textarea":
|
||||
element = (
|
||||
<ControlParamItem
|
||||
title={item.name}
|
||||
subTitle={item.sub}
|
||||
required={item.required}
|
||||
>
|
||||
<textarea
|
||||
rows={item.rows || 3}
|
||||
style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
|
||||
placeholder={item.placeholder}
|
||||
onChange={(e) => {
|
||||
props.onChange(item.value, e.currentTarget.value);
|
||||
}}
|
||||
value={props.data[item.value]}
|
||||
></textarea>
|
||||
</ControlParamItem>
|
||||
);
|
||||
break;
|
||||
case "select":
|
||||
element = (
|
||||
<ControlParamItem
|
||||
title={item.name}
|
||||
subTitle={item.sub}
|
||||
required={item.required}
|
||||
>
|
||||
<Select
|
||||
value={props.data[item.value]}
|
||||
onChange={(e) => {
|
||||
props.onChange(item.value, e.currentTarget.value);
|
||||
}}
|
||||
>
|
||||
{item.options.map((opt: any) => {
|
||||
return (
|
||||
<option value={opt.value} key={opt.value}>
|
||||
{opt.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</ControlParamItem>
|
||||
);
|
||||
break;
|
||||
case "number":
|
||||
element = (
|
||||
<ControlParamItem
|
||||
title={item.name}
|
||||
subTitle={item.sub}
|
||||
required={item.required}
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
value={props.data[item.value] || 0}
|
||||
onChange={(e) => {
|
||||
props.onChange(item.value, parseInt(e.currentTarget.value));
|
||||
}}
|
||||
/>
|
||||
</ControlParamItem>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
element = (
|
||||
<ControlParamItem
|
||||
title={item.name}
|
||||
subTitle={item.sub}
|
||||
required={item.required}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={props.data[item.value]}
|
||||
style={{ maxWidth: "100%", width: "100%" }}
|
||||
onChange={(e) => {
|
||||
props.onChange(item.value, e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</ControlParamItem>
|
||||
);
|
||||
}
|
||||
return <div key={item.value}>{element}</div>;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getModelParamBasicData = (
|
||||
columns: any[],
|
||||
data: any,
|
||||
clearText?: boolean,
|
||||
) => {
|
||||
const newParams: any = {};
|
||||
columns.forEach((item: any) => {
|
||||
if (clearText && ["text", "textarea", "number"].includes(item.type)) {
|
||||
newParams[item.value] = item.default || "";
|
||||
} else {
|
||||
// @ts-ignore
|
||||
newParams[item.value] = data[item.value] || item.default || "";
|
||||
}
|
||||
});
|
||||
return newParams;
|
||||
};
|
||||
|
||||
export const getParams = (model: any, params: any) => {
|
||||
return models.find((m) => m.value === model.value)?.params(params) || [];
|
||||
};
|
||||
|
||||
export function SdPanel() {
|
||||
const sdStore = useSdStore();
|
||||
const currentModel = sdStore.currentModel;
|
||||
const setCurrentModel = sdStore.setCurrentModel;
|
||||
const params = sdStore.currentParams;
|
||||
const setParams = sdStore.setCurrentParams;
|
||||
|
||||
const handleValueChange = (field: string, val: any) => {
|
||||
setParams({
|
||||
...params,
|
||||
[field]: val,
|
||||
});
|
||||
};
|
||||
const handleModelChange = (model: any) => {
|
||||
setCurrentModel(model);
|
||||
setParams(getModelParamBasicData(model.params({}), params));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ControlParamItem title={Locale.SdPanel.AIModel}>
|
||||
<div className={styles["ai-models"]}>
|
||||
{models.map((item) => {
|
||||
return (
|
||||
<IconButton
|
||||
text={item.name}
|
||||
key={item.value}
|
||||
type={currentModel.value == item.value ? "primary" : null}
|
||||
shadow
|
||||
onClick={() => handleModelChange(item)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ControlParamItem>
|
||||
<ControlParam
|
||||
columns={getParams?.(currentModel, params) as any[]}
|
||||
data={params}
|
||||
onChange={handleValueChange}
|
||||
></ControlParam>
|
||||
</>
|
||||
);
|
||||
}
|
140
app/components/sd/sd-sidebar.tsx
Normal file
140
app/components/sd/sd-sidebar.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
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 HistoryIcon from "@/app/icons/history.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,
|
||||
SideBarHeader,
|
||||
SideBarTail,
|
||||
useDragSideBar,
|
||||
useHotKey,
|
||||
} from "@/app/components/sidebar";
|
||||
|
||||
import { getParams, getModelParamBasicData } from "./sd-panel";
|
||||
import { useSdStore } from "@/app/store/sd";
|
||||
import { showToast } from "@/app/components/ui-lib";
|
||||
import { useMobileScreen } from "@/app/utils";
|
||||
|
||||
const SdPanel = dynamic(
|
||||
async () => (await import("@/app/components/sd")).SdPanel,
|
||||
{
|
||||
loading: () => null,
|
||||
},
|
||||
);
|
||||
|
||||
export function SideBar(props: { className?: string }) {
|
||||
useHotKey();
|
||||
const isMobileScreen = useMobileScreen();
|
||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||
const navigate = useNavigate();
|
||||
const sdStore = useSdStore();
|
||||
const currentModel = sdStore.currentModel;
|
||||
const params = sdStore.currentParams;
|
||||
const setParams = sdStore.setCurrentParams;
|
||||
|
||||
const handleSubmit = () => {
|
||||
const columns = getParams?.(currentModel, params);
|
||||
const reqParams: any = {};
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const item = columns[i];
|
||||
reqParams[item.value] = params[item.value] ?? null;
|
||||
if (item.required) {
|
||||
if (!reqParams[item.value]) {
|
||||
showToast(Locale.SdPanel.ParamIsRequired(item.name));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
let data: any = {
|
||||
model: currentModel.value,
|
||||
model_name: currentModel.name,
|
||||
status: "wait",
|
||||
params: reqParams,
|
||||
created_at: new Date().toLocaleString(),
|
||||
img_data: "",
|
||||
};
|
||||
sdStore.sendTask(data, () => {
|
||||
setParams(getModelParamBasicData(columns, params, true));
|
||||
navigate(Path.SdNew);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SideBarContainer
|
||||
onDragStart={onDragStart}
|
||||
shouldNarrow={shouldNarrow}
|
||||
{...props}
|
||||
>
|
||||
{isMobileScreen ? (
|
||||
<div
|
||||
className="window-header"
|
||||
data-tauri-drag-region
|
||||
style={{
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
}}
|
||||
>
|
||||
<div className="window-actions">
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<ReturnIcon />}
|
||||
bordered
|
||||
title={Locale.Sd.Actions.ReturnHome}
|
||||
onClick={() => navigate(Path.Home)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SDIcon width={50} height={50} />
|
||||
<div className="window-actions">
|
||||
<div className="window-action-button">
|
||||
<IconButton
|
||||
icon={<HistoryIcon />}
|
||||
bordered
|
||||
title={Locale.Sd.Actions.History}
|
||||
onClick={() => navigate(Path.SdNew)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SideBarHeader
|
||||
title={
|
||||
<IconButton
|
||||
icon={<ReturnIcon />}
|
||||
bordered
|
||||
title={Locale.Sd.Actions.ReturnHome}
|
||||
onClick={() => navigate(Path.Home)}
|
||||
/>
|
||||
}
|
||||
logo={<SDIcon width={38} height={"100%"} />}
|
||||
></SideBarHeader>
|
||||
)}
|
||||
<SideBarBody>
|
||||
<SdPanel />
|
||||
</SideBarBody>
|
||||
<SideBarTail
|
||||
primaryAction={
|
||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||
<IconButton icon={<GithubIcon />} shadow />
|
||||
</a>
|
||||
}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
text={Locale.SdPanel.Submit}
|
||||
type="primary"
|
||||
shadow
|
||||
onClick={handleSubmit}
|
||||
></IconButton>
|
||||
}
|
||||
/>
|
||||
</SideBarContainer>
|
||||
);
|
||||
}
|
53
app/components/sd/sd.module.scss
Normal file
53
app/components/sd/sd.module.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
.sd-img-list{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.sd-img-item{
|
||||
width: 48%;
|
||||
.sd-img-item-info{
|
||||
flex:1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
user-select: text;
|
||||
p{
|
||||
margin: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.line-1{
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.pre-img{
|
||||
display: flex;
|
||||
width: 130px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--second);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.img{
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all .3s;
|
||||
&:hover{
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
&:not(:last-child){
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.sd-img-list{
|
||||
.sd-img-item{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
336
app/components/sd/sd.tsx
Normal file
336
app/components/sd/sd.tsx
Normal file
@@ -0,0 +1,336 @@
|
||||
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 locales from "@/app/locales";
|
||||
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";
|
||||
|
||||
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 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={isSd ? homeStyles["sidebar-show"] : ""} />
|
||||
<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={`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>
|
||||
)}
|
||||
{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"]}>
|
||||
{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) => {
|
||||
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>{locales.Sd.EmptyRecord}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</WindowContent>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -65,6 +65,7 @@ import {
|
||||
ServiceProvider,
|
||||
SlotID,
|
||||
UPDATE_URL,
|
||||
Stability,
|
||||
} from "../constant";
|
||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||
import { ErrorBoundary } from "./error";
|
||||
@@ -1041,6 +1042,45 @@ export function Settings() {
|
||||
</>
|
||||
);
|
||||
|
||||
const stabilityConfigComponent = accessStore.provider ===
|
||||
ServiceProvider.Stability && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Stability.Endpoint.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.Stability.Endpoint.SubTitle +
|
||||
Stability.ExampleEndpoint
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={accessStore.stabilityUrl}
|
||||
placeholder={Stability.ExampleEndpoint}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) => (access.stabilityUrl = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Stability.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Access.Stability.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.stabilityApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.Stability.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.stabilityApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className="window-header" data-tauri-drag-region>
|
||||
@@ -1324,6 +1364,7 @@ export function Settings() {
|
||||
{baiduConfigComponent}
|
||||
{byteDanceConfigComponent}
|
||||
{alibabaConfigComponent}
|
||||
{stabilityConfigComponent}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useMemo } from "react";
|
||||
import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
|
||||
|
||||
import styles from "./home.module.scss";
|
||||
|
||||
@@ -10,8 +10,8 @@ import AddIcon from "../icons/add.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import MaskIcon from "../icons/mask.svg";
|
||||
import PluginIcon from "../icons/plugin.svg";
|
||||
import DragIcon from "../icons/drag.svg";
|
||||
import DiscoveryIcon from "../icons/discovery.svg";
|
||||
|
||||
import Locale from "../locales";
|
||||
|
||||
@@ -23,19 +23,20 @@ import {
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
NARROW_SIDEBAR_WIDTH,
|
||||
Path,
|
||||
PLUGINS,
|
||||
REPO_URL,
|
||||
} from "../constant";
|
||||
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { isIOS, useMobileScreen } from "../utils";
|
||||
import dynamic from "next/dynamic";
|
||||
import { showConfirm, showToast } from "./ui-lib";
|
||||
import { showConfirm, Selector } from "./ui-lib";
|
||||
|
||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
||||
loading: () => null,
|
||||
});
|
||||
|
||||
function useHotKey() {
|
||||
export function useHotKey() {
|
||||
const chatStore = useChatStore();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -54,7 +55,7 @@ function useHotKey() {
|
||||
});
|
||||
}
|
||||
|
||||
function useDragSideBar() {
|
||||
export function useDragSideBar() {
|
||||
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
||||
|
||||
const config = useAppConfig();
|
||||
@@ -127,25 +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],
|
||||
);
|
||||
|
||||
useHotKey();
|
||||
|
||||
const { children, className, onDragStart, shouldNarrow } = props;
|
||||
return (
|
||||
<div
|
||||
className={`${styles.sidebar} ${props.className} ${
|
||||
className={`${styles.sidebar} ${className} ${
|
||||
shouldNarrow && styles["narrow-sidebar"]
|
||||
}`}
|
||||
style={{
|
||||
@@ -153,43 +150,128 @@ 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: {
|
||||
title?: string | React.ReactNode;
|
||||
subTitle?: string | React.ReactNode;
|
||||
logo?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const { title, subTitle, logo, children } = props;
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||
NextChat
|
||||
</div>
|
||||
<div className={styles["sidebar-sub-title"]}>
|
||||
Build your own AI assistant.
|
||||
</div>
|
||||
<div className={styles["sidebar-logo"] + " no-dark"}>
|
||||
<ChatGptIcon />
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles["sidebar-sub-title"]}>{subTitle}</div>
|
||||
<div className={styles["sidebar-logo"] + " no-dark"}>{logo}</div>
|
||||
</div>
|
||||
{children}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
<div className={styles["sidebar-header-bar"]}>
|
||||
<IconButton
|
||||
icon={<MaskIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => {
|
||||
if (config.dontShowMaskSplashScreen !== true) {
|
||||
navigate(Path.NewChat, { state: { fromHome: true } });
|
||||
} else {
|
||||
navigate(Path.Masks, { state: { fromHome: true } });
|
||||
}
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
<IconButton
|
||||
icon={<PluginIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Plugin.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => showToast(Locale.WIP)}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
<div
|
||||
className={styles["sidebar-body"]}
|
||||
export function SideBarTail(props: {
|
||||
primaryAction?: React.ReactNode;
|
||||
secondaryAction?: React.ReactNode;
|
||||
}) {
|
||||
const { primaryAction, secondaryAction } = props;
|
||||
|
||||
return (
|
||||
<div className={styles["sidebar-tail"]}>
|
||||
<div className={styles["sidebar-actions"]}>{primaryAction}</div>
|
||||
<div className={styles["sidebar-actions"]}>{secondaryAction}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SideBar(props: { className?: string }) {
|
||||
useHotKey();
|
||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||
const [showPluginSelector, setShowPluginSelector] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const config = useAppConfig();
|
||||
const chatStore = useChatStore();
|
||||
|
||||
return (
|
||||
<SideBarContainer
|
||||
onDragStart={onDragStart}
|
||||
shouldNarrow={shouldNarrow}
|
||||
{...props}
|
||||
>
|
||||
<SideBarHeader
|
||||
title="NextChat"
|
||||
subTitle="Build your own AI assistant."
|
||||
logo={<ChatGptIcon />}
|
||||
>
|
||||
<div className={styles["sidebar-header-bar"]}>
|
||||
<IconButton
|
||||
icon={<MaskIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => {
|
||||
if (config.dontShowMaskSplashScreen !== true) {
|
||||
navigate(Path.NewChat, { state: { fromHome: true } });
|
||||
} else {
|
||||
navigate(Path.Masks, { state: { fromHome: true } });
|
||||
}
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
<IconButton
|
||||
icon={<DiscoveryIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Discovery.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => setShowPluginSelector(true)}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
{showPluginSelector && (
|
||||
<Selector
|
||||
items={[
|
||||
{
|
||||
title: "👇 Please select the plugin you need to use",
|
||||
value: "-",
|
||||
disable: true,
|
||||
},
|
||||
...PLUGINS.map((item) => {
|
||||
return {
|
||||
title: item.name,
|
||||
value: item.path,
|
||||
};
|
||||
}),
|
||||
]}
|
||||
onClose={() => setShowPluginSelector(false)}
|
||||
onSelection={(s) => {
|
||||
navigate(s[0], { state: { fromHome: true } });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SideBarHeader>
|
||||
<SideBarBody
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
navigate(Path.Home);
|
||||
@@ -197,32 +279,33 @@ export function SideBar(props: { className?: string }) {
|
||||
}}
|
||||
>
|
||||
<ChatList narrow={shouldNarrow} />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</SideBarBody>
|
||||
<SideBarTail
|
||||
primaryAction={
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
||||
@@ -236,15 +319,8 @@ export function SideBar(props: { className?: string }) {
|
||||
}}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles["sidebar-drag"]}
|
||||
onPointerDown={(e) => onDragStart(e as any)}
|
||||
>
|
||||
<DragIcon />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</SideBarContainer>
|
||||
);
|
||||
}
|
||||
|
@@ -61,6 +61,19 @@
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical{
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
.list-header{
|
||||
.list-item-title{
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.list-item-sub-title{
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
@@ -291,6 +304,10 @@
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
|
||||
.selector-item-disabled{
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&-content {
|
||||
.list {
|
||||
max-height: 90vh;
|
||||
|
@@ -14,7 +14,9 @@ import Locale from "../locales";
|
||||
|
||||
import { createRoot } from "react-dom/client";
|
||||
import React, {
|
||||
CSSProperties,
|
||||
HTMLProps,
|
||||
MouseEvent,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
@@ -53,11 +55,16 @@ export function ListItem(props: {
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
icon?: JSX.Element;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
onClick?: (event: MouseEvent) => void;
|
||||
vertical?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={styles["list-item"] + ` ${props.className || ""}`}
|
||||
className={
|
||||
styles["list-item"] +
|
||||
` ${props.vertical ? styles["vertical"] : ""} ` +
|
||||
` ${props.className || ""}`
|
||||
}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div className={styles["list-header"]}>
|
||||
@@ -426,17 +433,25 @@ export function showPrompt(content: any, value = "", rows = 3) {
|
||||
});
|
||||
}
|
||||
|
||||
export function showImageModal(img: string) {
|
||||
export function showImageModal(
|
||||
img: string,
|
||||
defaultMax?: boolean,
|
||||
style?: CSSProperties,
|
||||
boxStyle?: CSSProperties,
|
||||
) {
|
||||
showModal({
|
||||
title: Locale.Export.Image.Modal,
|
||||
defaultMax: defaultMax,
|
||||
children: (
|
||||
<div>
|
||||
<div style={{ display: "flex", justifyContent: "center", ...boxStyle }}>
|
||||
<img
|
||||
src={img}
|
||||
alt="preview"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
style={
|
||||
style ?? {
|
||||
maxWidth: "100%",
|
||||
}
|
||||
}
|
||||
></img>
|
||||
</div>
|
||||
),
|
||||
@@ -448,6 +463,7 @@ export function Selector<T>(props: {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
value: T;
|
||||
disable?: boolean;
|
||||
}>;
|
||||
defaultSelectedValue?: T[] | T;
|
||||
onSelection?: (selection: T[]) => void;
|
||||
@@ -465,13 +481,18 @@ export function Selector<T>(props: {
|
||||
: props.defaultSelectedValue === item.value;
|
||||
return (
|
||||
<ListItem
|
||||
className={styles["selector-item"]}
|
||||
className={`${styles["selector-item"]} ${
|
||||
item.disable && styles["selector-item-disabled"]
|
||||
}`}
|
||||
key={i}
|
||||
title={item.title}
|
||||
subTitle={item.subTitle}
|
||||
onClick={() => {
|
||||
props.onSelection?.([item.value]);
|
||||
props.onClose?.();
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
if (!item.disable) {
|
||||
props.onSelection?.([item.value]);
|
||||
props.onClose?.();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selected ? (
|
||||
|
Reference in New Issue
Block a user