Merge branch 'feature-artifacts' of https://github.com/ConnectAI-E/ChatGPT-Next-Web into feature/artifacts-style
This commit is contained in:
commit
51e8f0440d
|
@ -326,6 +326,14 @@ You can use this option if you want to increase the number of webdav service add
|
||||||
|
|
||||||
Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
|
Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
|
||||||
|
|
||||||
|
### `STABILITY_API_KEY` (optional)
|
||||||
|
|
||||||
|
Stability API key.
|
||||||
|
|
||||||
|
### `STABILITY_URL` (optional)
|
||||||
|
|
||||||
|
Customize Stability API url.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
NodeJS >= 18, Docker >= 20
|
NodeJS >= 18, Docker >= 20
|
||||||
|
|
|
@ -218,6 +218,15 @@ ByteDance Api Url.
|
||||||
|
|
||||||
自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
|
自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
|
||||||
|
|
||||||
|
### `STABILITY_API_KEY` (optional)
|
||||||
|
|
||||||
|
Stability API密钥
|
||||||
|
|
||||||
|
### `STABILITY_URL` (optional)
|
||||||
|
|
||||||
|
自定义的Stability API请求地址
|
||||||
|
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
点击下方按钮,开始二次开发:
|
点击下方按钮,开始二次开发:
|
||||||
|
|
|
@ -4,18 +4,37 @@ import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
|
||||||
async function handle(req: NextRequest, res: NextResponse) {
|
async function handle(req: NextRequest, res: NextResponse) {
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
const storeUrl = (key: string) =>
|
const storeUrl = () =>
|
||||||
`https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}/values/${key}`;
|
`https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`;
|
||||||
const storeHeaders = () => ({
|
const storeHeaders = () => ({
|
||||||
Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
|
Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
|
||||||
});
|
});
|
||||||
if (req.method === "POST") {
|
if (req.method === "POST") {
|
||||||
const clonedBody = await req.text();
|
const clonedBody = await req.text();
|
||||||
const hashedCode = md5.hash(clonedBody).trim();
|
const hashedCode = md5.hash(clonedBody).trim();
|
||||||
const res = await fetch(storeUrl(hashedCode), {
|
const body: {
|
||||||
headers: storeHeaders(),
|
key: string;
|
||||||
|
value: string;
|
||||||
|
expiration_ttl?: number;
|
||||||
|
} = {
|
||||||
|
key: hashedCode,
|
||||||
|
value: clonedBody,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
|
||||||
|
if (ttl > 60) {
|
||||||
|
body["expiration_ttl"] = ttl;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
const res = await fetch(`${storeUrl()}/bulk`, {
|
||||||
|
headers: {
|
||||||
|
...storeHeaders(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: clonedBody,
|
body: JSON.stringify([body]),
|
||||||
});
|
});
|
||||||
const result = await res.json();
|
const result = await res.json();
|
||||||
console.log("save data", result);
|
console.log("save data", result);
|
||||||
|
@ -32,7 +51,7 @@ async function handle(req: NextRequest, res: NextResponse) {
|
||||||
}
|
}
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
const id = req?.nextUrl?.searchParams?.get("id");
|
const id = req?.nextUrl?.searchParams?.get("id");
|
||||||
const res = await fetch(storeUrl(id as string), {
|
const res = await fetch(`${storeUrl()}/values/${id}`, {
|
||||||
headers: storeHeaders(),
|
headers: storeHeaders(),
|
||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
|
|
|
@ -67,6 +67,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
||||||
let systemApiKey: string | undefined;
|
let systemApiKey: string | undefined;
|
||||||
|
|
||||||
switch (modelProvider) {
|
switch (modelProvider) {
|
||||||
|
case ModelProvider.Stability:
|
||||||
|
systemApiKey = serverConfig.stabilityApiKey;
|
||||||
|
break;
|
||||||
case ModelProvider.GeminiPro:
|
case ModelProvider.GeminiPro:
|
||||||
systemApiKey = serverConfig.googleApiKey;
|
systemApiKey = serverConfig.googleApiKey;
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getServerSideConfig } from "@/app/config/server";
|
||||||
|
import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant";
|
||||||
|
import { auth } from "@/app/api/auth";
|
||||||
|
|
||||||
|
async function handle(
|
||||||
|
req: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } },
|
||||||
|
) {
|
||||||
|
console.log("[Stability] params ", params);
|
||||||
|
|
||||||
|
if (req.method === "OPTIONS") {
|
||||||
|
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL;
|
||||||
|
|
||||||
|
if (!baseUrl.startsWith("http")) {
|
||||||
|
baseUrl = `https://${baseUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseUrl.endsWith("/")) {
|
||||||
|
baseUrl = baseUrl.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", "");
|
||||||
|
|
||||||
|
console.log("[Stability Proxy] ", path);
|
||||||
|
console.log("[Stability Base Url]", baseUrl);
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => {
|
||||||
|
controller.abort();
|
||||||
|
},
|
||||||
|
10 * 60 * 1000,
|
||||||
|
);
|
||||||
|
|
||||||
|
const authResult = auth(req, ModelProvider.Stability);
|
||||||
|
|
||||||
|
if (authResult.error) {
|
||||||
|
return NextResponse.json(authResult, {
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const bearToken = req.headers.get("Authorization") ?? "";
|
||||||
|
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||||
|
|
||||||
|
const key = token ? token : serverConfig.stabilityApiKey;
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: true,
|
||||||
|
message: `missing STABILITY_API_KEY in server env vars`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUrl = `${baseUrl}/${path}`;
|
||||||
|
console.log("[Stability Url] ", fetchUrl);
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": req.headers.get("Content-Type") || "multipart/form-data",
|
||||||
|
Accept: req.headers.get("Accept") || "application/json",
|
||||||
|
Authorization: `Bearer ${key}`,
|
||||||
|
},
|
||||||
|
method: req.method,
|
||||||
|
body: req.body,
|
||||||
|
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
|
||||||
|
redirect: "manual",
|
||||||
|
// @ts-ignore
|
||||||
|
duplex: "half",
|
||||||
|
signal: controller.signal,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(fetchUrl, fetchOptions);
|
||||||
|
// to prevent browser prompt for credentials
|
||||||
|
const newHeaders = new Headers(res.headers);
|
||||||
|
newHeaders.delete("www-authenticate");
|
||||||
|
// to disable nginx buffering
|
||||||
|
newHeaders.set("X-Accel-Buffering", "no");
|
||||||
|
return new Response(res.body, {
|
||||||
|
status: res.status,
|
||||||
|
statusText: res.statusText,
|
||||||
|
headers: newHeaders,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = handle;
|
||||||
|
export const POST = handle;
|
||||||
|
|
||||||
|
export const runtime = "edge";
|
|
@ -37,9 +37,13 @@ async function handle(
|
||||||
const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
|
const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
|
||||||
const normalizedEndpoint = normalizeUrl(endpoint as string);
|
const normalizedEndpoint = normalizeUrl(endpoint as string);
|
||||||
|
|
||||||
return normalizedEndpoint &&
|
return (
|
||||||
|
normalizedEndpoint &&
|
||||||
normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
|
normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
|
||||||
normalizedEndpoint.pathname.startsWith(normalizedAllowedEndpoint.pathname);
|
normalizedEndpoint.pathname.startsWith(
|
||||||
|
normalizedAllowedEndpoint.pathname,
|
||||||
|
)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|
|
@ -168,6 +168,19 @@ export class ClientApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBearerToken(
|
||||||
|
apiKey: string,
|
||||||
|
noBearer: boolean = false,
|
||||||
|
): string {
|
||||||
|
return validString(apiKey)
|
||||||
|
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validString(x: string): boolean {
|
||||||
|
return x?.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
export function getHeaders() {
|
export function getHeaders() {
|
||||||
const accessStore = useAccessStore.getState();
|
const accessStore = useAccessStore.getState();
|
||||||
const chatStore = useChatStore.getState();
|
const chatStore = useChatStore.getState();
|
||||||
|
@ -214,15 +227,6 @@ export function getHeaders() {
|
||||||
return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization";
|
return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBearerToken(apiKey: string, noBearer: boolean = false): string {
|
|
||||||
return validString(apiKey)
|
|
||||||
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function validString(x: string): boolean {
|
|
||||||
return x?.length > 0;
|
|
||||||
}
|
|
||||||
const {
|
const {
|
||||||
isGoogle,
|
isGoogle,
|
||||||
isAzure,
|
isAzure,
|
||||||
|
|
|
@ -34,14 +34,18 @@ export function HTMLPreview(props: {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("message", (e) => {
|
const handleMessage = (e: any) => {
|
||||||
const { id, height, title } = e.data;
|
const { id, height, title } = e.data;
|
||||||
setTitle(title);
|
setTitle(title);
|
||||||
if (id == frameId.current) {
|
if (id == frameId.current) {
|
||||||
setIframeHeight(height);
|
setIframeHeight(height);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
}, [iframeHeight]);
|
window.addEventListener("message", handleMessage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", handleMessage);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const height = useMemo(() => {
|
const height = useMemo(() => {
|
||||||
const parentHeight = props.height || 600;
|
const parentHeight = props.height || 600;
|
||||||
|
@ -186,8 +190,17 @@ export function Artifact() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
fetch(`${ApiPath.Artifact}?id=${id}`)
|
fetch(`${ApiPath.Artifact}?id=${id}`)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status > 300) {
|
||||||
|
throw Error("can not get content");
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
})
|
||||||
.then((res) => res.text())
|
.then((res) => res.text())
|
||||||
.then(setCode);
|
.then(setCode)
|
||||||
|
.catch((e) => {
|
||||||
|
showToast(Locale.Export.Artifact.Error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import styles from "./button.module.scss";
|
import styles from "./button.module.scss";
|
||||||
|
import { CSSProperties } from "react";
|
||||||
|
|
||||||
export type ButtonType = "primary" | "danger" | null;
|
export type ButtonType = "primary" | "danger" | null;
|
||||||
|
|
||||||
|
@ -16,6 +17,7 @@ export function IconButton(props: {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -31,6 +33,7 @@ export function IconButton(props: {
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={props.tabIndex}
|
tabIndex={props.tabIndex}
|
||||||
autoFocus={props.autoFocus}
|
autoFocus={props.autoFocus}
|
||||||
|
style={props.style}
|
||||||
>
|
>
|
||||||
{props.icon && (
|
{props.icon && (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -340,7 +340,7 @@ function ClearContextDivider() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatAction(props: {
|
export function ChatAction(props: {
|
||||||
text: string;
|
text: string;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
import GithubIcon from "../icons/github.svg";
|
import GithubIcon from "../icons/github.svg";
|
||||||
|
|
|
@ -59,6 +59,10 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
|
||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Sd = dynamic(async () => (await import("./sd")).Sd, {
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
});
|
||||||
|
|
||||||
export function useSwitchTheme() {
|
export function useSwitchTheme() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
|
||||||
|
@ -126,12 +130,23 @@ const loadAsyncGoogleFont = () => {
|
||||||
document.head.appendChild(linkEl);
|
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() {
|
function Screen() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isArtifact = location.pathname.includes(Path.Artifact);
|
const isArtifact = location.pathname.includes(Path.Artifact);
|
||||||
const isHome = location.pathname === Path.Home;
|
const isHome = location.pathname === Path.Home;
|
||||||
const isAuth = location.pathname === Path.Auth;
|
const isAuth = location.pathname === Path.Auth;
|
||||||
|
const isSd = location.pathname === Path.Sd;
|
||||||
|
const isSdNew = location.pathname === Path.SdNew;
|
||||||
|
|
||||||
const isMobileScreen = useMobileScreen();
|
const isMobileScreen = useMobileScreen();
|
||||||
const shouldTightBorder =
|
const shouldTightBorder =
|
||||||
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
|
getClientConfig()?.isApp || (config.tightBorder && !isMobileScreen);
|
||||||
|
@ -147,35 +162,33 @@ function Screen() {
|
||||||
</Routes>
|
</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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={`${styles.container} ${
|
||||||
styles.container +
|
shouldTightBorder ? styles["tight-container"] : styles.container
|
||||||
` ${shouldTightBorder ? styles["tight-container"] : styles.container} ${
|
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`}
|
||||||
getLang() === "ar" ? styles["rtl-screen"] : ""
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{isAuth ? (
|
{renderContent()}
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./sd";
|
||||||
|
export * from "./sd-panel";
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
ServiceProvider,
|
||||||
SlotID,
|
SlotID,
|
||||||
UPDATE_URL,
|
UPDATE_URL,
|
||||||
|
Stability,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||||
import { ErrorBoundary } from "./error";
|
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 (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<div className="window-header" data-tauri-drag-region>
|
<div className="window-header" data-tauri-drag-region>
|
||||||
|
@ -1324,6 +1364,7 @@ export function Settings() {
|
||||||
{baiduConfigComponent}
|
{baiduConfigComponent}
|
||||||
{byteDanceConfigComponent}
|
{byteDanceConfigComponent}
|
||||||
{alibabaConfigComponent}
|
{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";
|
import styles from "./home.module.scss";
|
||||||
|
|
||||||
|
@ -10,8 +10,8 @@ import AddIcon from "../icons/add.svg";
|
||||||
import CloseIcon from "../icons/close.svg";
|
import CloseIcon from "../icons/close.svg";
|
||||||
import DeleteIcon from "../icons/delete.svg";
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
import MaskIcon from "../icons/mask.svg";
|
import MaskIcon from "../icons/mask.svg";
|
||||||
import PluginIcon from "../icons/plugin.svg";
|
|
||||||
import DragIcon from "../icons/drag.svg";
|
import DragIcon from "../icons/drag.svg";
|
||||||
|
import DiscoveryIcon from "../icons/discovery.svg";
|
||||||
|
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
|
||||||
|
@ -23,19 +23,20 @@ import {
|
||||||
MIN_SIDEBAR_WIDTH,
|
MIN_SIDEBAR_WIDTH,
|
||||||
NARROW_SIDEBAR_WIDTH,
|
NARROW_SIDEBAR_WIDTH,
|
||||||
Path,
|
Path,
|
||||||
|
PLUGINS,
|
||||||
REPO_URL,
|
REPO_URL,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
|
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { isIOS, useMobileScreen } from "../utils";
|
import { isIOS, useMobileScreen } from "../utils";
|
||||||
import dynamic from "next/dynamic";
|
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, {
|
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
||||||
loading: () => null,
|
loading: () => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
function useHotKey() {
|
export function useHotKey() {
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -54,7 +55,7 @@ function useHotKey() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDragSideBar() {
|
export function useDragSideBar() {
|
||||||
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
|
||||||
|
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
@ -127,25 +128,21 @@ function useDragSideBar() {
|
||||||
shouldNarrow,
|
shouldNarrow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export function SideBarContainer(props: {
|
||||||
export function SideBar(props: { className?: string }) {
|
children: React.ReactNode;
|
||||||
const chatStore = useChatStore();
|
onDragStart: (e: MouseEvent) => void;
|
||||||
|
shouldNarrow: boolean;
|
||||||
// drag side bar
|
className?: string;
|
||||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
}) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const config = useAppConfig();
|
|
||||||
const isMobileScreen = useMobileScreen();
|
const isMobileScreen = useMobileScreen();
|
||||||
const isIOSMobile = useMemo(
|
const isIOSMobile = useMemo(
|
||||||
() => isIOS() && isMobileScreen,
|
() => isIOS() && isMobileScreen,
|
||||||
[isMobileScreen],
|
[isMobileScreen],
|
||||||
);
|
);
|
||||||
|
const { children, className, onDragStart, shouldNarrow } = props;
|
||||||
useHotKey();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${styles.sidebar} ${props.className} ${
|
className={`${styles.sidebar} ${className} ${
|
||||||
shouldNarrow && styles["narrow-sidebar"]
|
shouldNarrow && styles["narrow-sidebar"]
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
|
@ -153,43 +150,128 @@ export function SideBar(props: { className?: string }) {
|
||||||
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
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-header"]} data-tauri-drag-region>
|
||||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||||
NextChat
|
{title}
|
||||||
</div>
|
|
||||||
<div className={styles["sidebar-sub-title"]}>
|
|
||||||
Build your own AI assistant.
|
|
||||||
</div>
|
|
||||||
<div className={styles["sidebar-logo"] + " no-dark"}>
|
|
||||||
<ChatGptIcon />
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles["sidebar-sub-title"]}>{subTitle}</div>
|
||||||
|
<div className={styles["sidebar-logo"] + " no-dark"}>{logo}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{children}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<div className={styles["sidebar-header-bar"]}>
|
export function SideBarBody(props: {
|
||||||
<IconButton
|
children: React.ReactNode;
|
||||||
icon={<MaskIcon />}
|
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||||
text={shouldNarrow ? undefined : Locale.Mask.Name}
|
}) {
|
||||||
className={styles["sidebar-bar-button"]}
|
const { onClick, children } = props;
|
||||||
onClick={() => {
|
return (
|
||||||
if (config.dontShowMaskSplashScreen !== true) {
|
<div className={styles["sidebar-body"]} onClick={onClick}>
|
||||||
navigate(Path.NewChat, { state: { fromHome: true } });
|
{children}
|
||||||
} else {
|
</div>
|
||||||
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>
|
|
||||||
|
|
||||||
<div
|
export function SideBarTail(props: {
|
||||||
className={styles["sidebar-body"]}
|
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) => {
|
onClick={(e) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
navigate(Path.Home);
|
navigate(Path.Home);
|
||||||
|
@ -197,32 +279,33 @@ export function SideBar(props: { className?: string }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChatList narrow={shouldNarrow} />
|
<ChatList narrow={shouldNarrow} />
|
||||||
</div>
|
</SideBarBody>
|
||||||
|
<SideBarTail
|
||||||
<div className={styles["sidebar-tail"]}>
|
primaryAction={
|
||||||
<div className={styles["sidebar-actions"]}>
|
<>
|
||||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<DeleteIcon />}
|
icon={<DeleteIcon />}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (await showConfirm(Locale.Home.DeleteChat)) {
|
if (await showConfirm(Locale.Home.DeleteChat)) {
|
||||||
chatStore.deleteSession(chatStore.currentSessionIndex);
|
chatStore.deleteSession(chatStore.currentSessionIndex);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["sidebar-action"]}>
|
<div className={styles["sidebar-action"]}>
|
||||||
<Link to={Path.Settings}>
|
<Link to={Path.Settings}>
|
||||||
<IconButton icon={<SettingsIcon />} shadow />
|
<IconButton icon={<SettingsIcon />} shadow />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles["sidebar-action"]}>
|
<div className={styles["sidebar-action"]}>
|
||||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||||
<IconButton icon={<GithubIcon />} shadow />
|
<IconButton icon={<GithubIcon />} shadow />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
<div>
|
}
|
||||||
|
secondaryAction={
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<AddIcon />}
|
icon={<AddIcon />}
|
||||||
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
text={shouldNarrow ? undefined : Locale.Home.NewChat}
|
||||||
|
@ -236,15 +319,8 @@ export function SideBar(props: { className?: string }) {
|
||||||
}}
|
}}
|
||||||
shadow
|
shadow
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
|
</SideBarContainer>
|
||||||
<div
|
|
||||||
className={styles["sidebar-drag"]}
|
|
||||||
onPointerDown={(e) => onDragStart(e as any)}
|
|
||||||
>
|
|
||||||
<DragIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,19 @@
|
||||||
font-weight: normal;
|
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 {
|
.list {
|
||||||
|
@ -291,6 +304,10 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
|
||||||
|
.selector-item-disabled{
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
&-content {
|
&-content {
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
.list {
|
.list {
|
||||||
|
|
|
@ -14,7 +14,9 @@ import Locale from "../locales";
|
||||||
|
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import React, {
|
import React, {
|
||||||
|
CSSProperties,
|
||||||
HTMLProps,
|
HTMLProps,
|
||||||
|
MouseEvent,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
@ -53,11 +55,16 @@ export function ListItem(props: {
|
||||||
children?: JSX.Element | JSX.Element[];
|
children?: JSX.Element | JSX.Element[];
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
onClick?: (e: MouseEvent) => void;
|
||||||
|
vertical?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={styles["list-item"] + ` ${props.className || ""}`}
|
className={
|
||||||
|
styles["list-item"] +
|
||||||
|
` ${props.vertical ? styles["vertical"] : ""} ` +
|
||||||
|
` ${props.className || ""}`
|
||||||
|
}
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
>
|
>
|
||||||
<div className={styles["list-header"]}>
|
<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({
|
showModal({
|
||||||
title: Locale.Export.Image.Modal,
|
title: Locale.Export.Image.Modal,
|
||||||
|
defaultMax: defaultMax,
|
||||||
children: (
|
children: (
|
||||||
<div>
|
<div style={{ display: "flex", justifyContent: "center", ...boxStyle }}>
|
||||||
<img
|
<img
|
||||||
src={img}
|
src={img}
|
||||||
alt="preview"
|
alt="preview"
|
||||||
style={{
|
style={
|
||||||
maxWidth: "100%",
|
style ?? {
|
||||||
}}
|
maxWidth: "100%",
|
||||||
|
}
|
||||||
|
}
|
||||||
></img>
|
></img>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
@ -448,6 +463,7 @@ export function Selector<T>(props: {
|
||||||
title: string;
|
title: string;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
value: T;
|
value: T;
|
||||||
|
disable?: boolean;
|
||||||
}>;
|
}>;
|
||||||
defaultSelectedValue?: T[] | T;
|
defaultSelectedValue?: T[] | T;
|
||||||
onSelection?: (selection: T[]) => void;
|
onSelection?: (selection: T[]) => void;
|
||||||
|
@ -462,10 +478,7 @@ export function Selector<T>(props: {
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelection = (
|
const handleSelection = (e: MouseEvent, value: T) => {
|
||||||
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
|
||||||
value: T,
|
|
||||||
) => {
|
|
||||||
if (props.multiple) {
|
if (props.multiple) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const newSelectedValues = selectedValues.includes(value)
|
const newSelectedValues = selectedValues.includes(value)
|
||||||
|
@ -488,11 +501,19 @@ export function Selector<T>(props: {
|
||||||
const selected = selectedValues.includes(item.value);
|
const selected = selectedValues.includes(item.value);
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
className={styles["selector-item"]}
|
className={`${styles["selector-item"]} ${
|
||||||
|
item.disable && styles["selector-item-disabled"]
|
||||||
|
}`}
|
||||||
key={i}
|
key={i}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
subTitle={item.subTitle}
|
subTitle={item.subTitle}
|
||||||
onClick={(e) => handleSelection(e, item.value)}
|
onClick={(e) => {
|
||||||
|
if (item.disable) {
|
||||||
|
e.stopPropagation();
|
||||||
|
} else {
|
||||||
|
handleSelection(e, item.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<div
|
<div
|
||||||
|
@ -526,11 +547,15 @@ export function FullScreen(props: any) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener("fullscreenchange", (e) => {
|
const handleScreenChange = (e: any) => {
|
||||||
if (e.target === ref.current) {
|
if (e.target === ref.current) {
|
||||||
setFullScreen(!!document.fullscreenElement);
|
setFullScreen(!!document.fullscreenElement);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
document.addEventListener("fullscreenchange", handleScreenChange);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("fullscreenchange", handleScreenChange);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<div ref={ref} style={{ position: "relative" }} {...rest}>
|
<div ref={ref} style={{ position: "relative" }} {...rest}>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { BuildConfig, getBuildConfig } from "./build";
|
||||||
export function getClientConfig() {
|
export function getClientConfig() {
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
// client side
|
// client side
|
||||||
return JSON.parse(queryMeta("config")) as BuildConfig;
|
return JSON.parse(queryMeta("config") || "{}") as BuildConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof process !== "undefined") {
|
if (typeof process !== "undefined") {
|
||||||
|
|
|
@ -23,6 +23,10 @@ declare global {
|
||||||
CUSTOM_MODELS?: string; // to control custom models
|
CUSTOM_MODELS?: string; // to control custom models
|
||||||
DEFAULT_MODEL?: string; // to control default model in every new chat window
|
DEFAULT_MODEL?: string; // to control default model in every new chat window
|
||||||
|
|
||||||
|
// stability only
|
||||||
|
STABILITY_URL?: string;
|
||||||
|
STABILITY_API_KEY?: string;
|
||||||
|
|
||||||
// azure only
|
// azure only
|
||||||
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
|
AZURE_URL?: string; // https://{azure-url}/openai/deployments/{deploy-name}
|
||||||
AZURE_API_KEY?: string;
|
AZURE_API_KEY?: string;
|
||||||
|
@ -107,6 +111,8 @@ export const getServerSideConfig = () => {
|
||||||
if (defaultModel.startsWith("gpt-4")) defaultModel = "";
|
if (defaultModel.startsWith("gpt-4")) defaultModel = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isStability = !!process.env.STABILITY_API_KEY;
|
||||||
|
|
||||||
const isAzure = !!process.env.AZURE_URL;
|
const isAzure = !!process.env.AZURE_URL;
|
||||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||||
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||||
|
@ -131,6 +137,10 @@ export const getServerSideConfig = () => {
|
||||||
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
apiKey: getApiKey(process.env.OPENAI_API_KEY),
|
||||||
openaiOrgId: process.env.OPENAI_ORG_ID,
|
openaiOrgId: process.env.OPENAI_ORG_ID,
|
||||||
|
|
||||||
|
isStability,
|
||||||
|
stabilityUrl: process.env.STABILITY_URL,
|
||||||
|
stabilityApiKey: getApiKey(process.env.STABILITY_API_KEY),
|
||||||
|
|
||||||
isAzure,
|
isAzure,
|
||||||
azureUrl: process.env.AZURE_URL,
|
azureUrl: process.env.AZURE_URL,
|
||||||
azureApiKey: getApiKey(process.env.AZURE_API_KEY),
|
azureApiKey: getApiKey(process.env.AZURE_API_KEY),
|
||||||
|
@ -161,6 +171,7 @@ export const getServerSideConfig = () => {
|
||||||
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||||
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
||||||
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
|
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
|
||||||
|
cloudflareKVTTL: process.env.CLOUDFLARE_KV_TTL,
|
||||||
|
|
||||||
gtmId: process.env.GTM_ID,
|
gtmId: process.env.GTM_ID,
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,8 @@ export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/c
|
||||||
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||||
|
|
||||||
export const PREVIEW_URL = "https://app.nextchat.dev";
|
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
||||||
|
|
||||||
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
|
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
|
||||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||||
|
@ -32,6 +33,8 @@ export enum Path {
|
||||||
NewChat = "/new-chat",
|
NewChat = "/new-chat",
|
||||||
Masks = "/masks",
|
Masks = "/masks",
|
||||||
Auth = "/auth",
|
Auth = "/auth",
|
||||||
|
Sd = "/sd",
|
||||||
|
SdNew = "/sd-new",
|
||||||
Artifact = "/artifact",
|
Artifact = "/artifact",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +47,7 @@ export enum ApiPath {
|
||||||
Baidu = "/api/baidu",
|
Baidu = "/api/baidu",
|
||||||
ByteDance = "/api/bytedance",
|
ByteDance = "/api/bytedance",
|
||||||
Alibaba = "/api/alibaba",
|
Alibaba = "/api/alibaba",
|
||||||
|
Stability = "/api/stability",
|
||||||
Artifact = "/api/artifact",
|
Artifact = "/api/artifact",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +73,7 @@ export enum StoreKey {
|
||||||
Prompt = "prompt-store",
|
Prompt = "prompt-store",
|
||||||
Update = "chat-update",
|
Update = "chat-update",
|
||||||
Sync = "sync",
|
Sync = "sync",
|
||||||
|
SdList = "sd-list",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
||||||
|
@ -95,6 +100,7 @@ export enum ServiceProvider {
|
||||||
Baidu = "Baidu",
|
Baidu = "Baidu",
|
||||||
ByteDance = "ByteDance",
|
ByteDance = "ByteDance",
|
||||||
Alibaba = "Alibaba",
|
Alibaba = "Alibaba",
|
||||||
|
Stability = "Stability",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
||||||
|
@ -107,6 +113,7 @@ export enum GoogleSafetySettingsThreshold {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ModelProvider {
|
export enum ModelProvider {
|
||||||
|
Stability = "Stability",
|
||||||
GPT = "GPT",
|
GPT = "GPT",
|
||||||
GeminiPro = "GeminiPro",
|
GeminiPro = "GeminiPro",
|
||||||
Claude = "Claude",
|
Claude = "Claude",
|
||||||
|
@ -115,6 +122,11 @@ export enum ModelProvider {
|
||||||
Qwen = "Qwen",
|
Qwen = "Qwen",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Stability = {
|
||||||
|
GeneratePath: "v2beta/stable-image/generate",
|
||||||
|
ExampleEndpoint: "https://api.stability.ai",
|
||||||
|
};
|
||||||
|
|
||||||
export const Anthropic = {
|
export const Anthropic = {
|
||||||
ChatPath: "v1/messages",
|
ChatPath: "v1/messages",
|
||||||
ChatPath1: "v1/complete",
|
ChatPath1: "v1/complete",
|
||||||
|
@ -358,3 +370,5 @@ export const internalAllowedWebDavEndpoints = [
|
||||||
"https://webdav.yandex.com",
|
"https://webdav.yandex.com",
|
||||||
"https://app.koofr.net/dav/Koofr",
|
"https://app.koofr.net/dav/Koofr",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const PLUGINS = [{ name: "Stable Diffusion", path: Path.Sd }];
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path
|
||||||
|
d="M11.307 9.739L15 9l-.739 3.693a2 2 0 0 1-1.568 1.569L9 15l.739-3.693a2 2 0 0 1 1.568-1.568" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 371 B |
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5.81836 6.72729V14H13.0911" stroke="#333" stroke-width="4" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path
|
||||||
|
d="M4 24C4 35.0457 12.9543 44 24 44V44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C16.598 4 10.1351 8.02111 6.67677 13.9981"
|
||||||
|
stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
<path d="M24.005 12L24.0038 24.0088L32.4832 32.4882" stroke="#333" stroke-width="4"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round" />
|
||||||
|
</svg>
|
After Width: | Height: | Size: 660 B |
|
@ -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 |
|
@ -37,7 +37,10 @@ export default function RootLayout({
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta name="config" content={JSON.stringify(getClientConfig())} />
|
<meta name="config" content={JSON.stringify(getClientConfig())} />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||||
|
/>
|
||||||
<link rel="manifest" href="/site.webmanifest"></link>
|
<link rel="manifest" href="/site.webmanifest"></link>
|
||||||
<script src="/serviceWorkerRegister.js" defer></script>
|
<script src="/serviceWorkerRegister.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -393,6 +393,17 @@ const cn = {
|
||||||
SubTitle: "样例:",
|
SubTitle: "样例:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Stability: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "接口密钥",
|
||||||
|
SubTitle: "使用自定义 Stability API Key",
|
||||||
|
Placeholder: "Stability API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "接口地址",
|
||||||
|
SubTitle: "样例:",
|
||||||
|
},
|
||||||
|
},
|
||||||
CustomModel: {
|
CustomModel: {
|
||||||
Title: "自定义模型名",
|
Title: "自定义模型名",
|
||||||
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
|
SubTitle: "增加自定义模型可选项,使用英文逗号隔开",
|
||||||
|
@ -452,6 +463,9 @@ const cn = {
|
||||||
Name: "插件",
|
Name: "插件",
|
||||||
Artifact: "Artifact",
|
Artifact: "Artifact",
|
||||||
},
|
},
|
||||||
|
Discovery: {
|
||||||
|
Name: "发现",
|
||||||
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "你是一个助手",
|
Sysmessage: "你是一个助手",
|
||||||
},
|
},
|
||||||
|
@ -531,6 +545,61 @@ const cn = {
|
||||||
Topic: "主题",
|
Topic: "主题",
|
||||||
Time: "时间",
|
Time: "时间",
|
||||||
},
|
},
|
||||||
|
SdPanel: {
|
||||||
|
Prompt: "画面提示",
|
||||||
|
NegativePrompt: "否定提示",
|
||||||
|
PleaseInput: (name: string) => `请输入${name}`,
|
||||||
|
AspectRatio: "横纵比",
|
||||||
|
ImageStyle: "图像风格",
|
||||||
|
OutFormat: "输出格式",
|
||||||
|
AIModel: "AI模型",
|
||||||
|
ModelVersion: "模型版本",
|
||||||
|
Submit: "提交生成",
|
||||||
|
ParamIsRequired: (name: string) => `${name}不能为空`,
|
||||||
|
Styles: {
|
||||||
|
D3Model: "3D模型",
|
||||||
|
AnalogFilm: "模拟电影",
|
||||||
|
Anime: "动漫",
|
||||||
|
Cinematic: "电影风格",
|
||||||
|
ComicBook: "漫画书",
|
||||||
|
DigitalArt: "数字艺术",
|
||||||
|
Enhance: "增强",
|
||||||
|
FantasyArt: "幻想艺术",
|
||||||
|
Isometric: "等角",
|
||||||
|
LineArt: "线描",
|
||||||
|
LowPoly: "低多边形",
|
||||||
|
ModelingCompound: "建模材料",
|
||||||
|
NeonPunk: "霓虹朋克",
|
||||||
|
Origami: "折纸",
|
||||||
|
Photographic: "摄影",
|
||||||
|
PixelArt: "像素艺术",
|
||||||
|
TileTexture: "贴图",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sd: {
|
||||||
|
SubTitle: (count: number) => `共 ${count} 条绘画`,
|
||||||
|
Actions: {
|
||||||
|
Params: "查看参数",
|
||||||
|
Copy: "复制提示词",
|
||||||
|
Delete: "删除",
|
||||||
|
Retry: "重试",
|
||||||
|
ReturnHome: "返回首页",
|
||||||
|
History: "查看历史",
|
||||||
|
},
|
||||||
|
EmptyRecord: "暂无绘画记录",
|
||||||
|
Status: {
|
||||||
|
Name: "状态",
|
||||||
|
Success: "成功",
|
||||||
|
Error: "失败",
|
||||||
|
Wait: "等待中",
|
||||||
|
Running: "运行中",
|
||||||
|
},
|
||||||
|
Danger: {
|
||||||
|
Delete: "确认删除?",
|
||||||
|
},
|
||||||
|
GenerateParams: "生成参数",
|
||||||
|
Detail: "详情",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeepPartial<T> = T extends object
|
type DeepPartial<T> = T extends object
|
||||||
|
|
|
@ -376,6 +376,17 @@ const en: LocaleType = {
|
||||||
SubTitle: "Example: ",
|
SubTitle: "Example: ",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Stability: {
|
||||||
|
ApiKey: {
|
||||||
|
Title: "Stability API Key",
|
||||||
|
SubTitle: "Use a custom Stability API Key",
|
||||||
|
Placeholder: "Stability API Key",
|
||||||
|
},
|
||||||
|
Endpoint: {
|
||||||
|
Title: "Endpoint Address",
|
||||||
|
SubTitle: "Example: ",
|
||||||
|
},
|
||||||
|
},
|
||||||
CustomModel: {
|
CustomModel: {
|
||||||
Title: "Custom Models",
|
Title: "Custom Models",
|
||||||
SubTitle: "Custom model options, seperated by comma",
|
SubTitle: "Custom model options, seperated by comma",
|
||||||
|
@ -459,6 +470,9 @@ const en: LocaleType = {
|
||||||
Name: "Plugin",
|
Name: "Plugin",
|
||||||
Artifact: "Artifact",
|
Artifact: "Artifact",
|
||||||
},
|
},
|
||||||
|
Discovery: {
|
||||||
|
Name: "Discovery",
|
||||||
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "You are an assistant that",
|
Sysmessage: "You are an assistant that",
|
||||||
},
|
},
|
||||||
|
@ -533,11 +547,65 @@ const en: LocaleType = {
|
||||||
Topic: "Topic",
|
Topic: "Topic",
|
||||||
Time: "Time",
|
Time: "Time",
|
||||||
},
|
},
|
||||||
|
|
||||||
URLCommand: {
|
URLCommand: {
|
||||||
Code: "Detected access code from url, confirm to apply? ",
|
Code: "Detected access code from url, confirm to apply? ",
|
||||||
Settings: "Detected settings from url, confirm to apply?",
|
Settings: "Detected settings from url, confirm to apply?",
|
||||||
},
|
},
|
||||||
|
SdPanel: {
|
||||||
|
Prompt: "Prompt",
|
||||||
|
NegativePrompt: "Negative Prompt",
|
||||||
|
PleaseInput: (name: string) => `Please input ${name}`,
|
||||||
|
AspectRatio: "Aspect Ratio",
|
||||||
|
ImageStyle: "Image Style",
|
||||||
|
OutFormat: "Output Format",
|
||||||
|
AIModel: "AI Model",
|
||||||
|
ModelVersion: "Model Version",
|
||||||
|
Submit: "Submit",
|
||||||
|
ParamIsRequired: (name: string) => `${name} is required`,
|
||||||
|
Styles: {
|
||||||
|
D3Model: "3d-model",
|
||||||
|
AnalogFilm: "analog-film",
|
||||||
|
Anime: "anime",
|
||||||
|
Cinematic: "cinematic",
|
||||||
|
ComicBook: "comic-book",
|
||||||
|
DigitalArt: "digital-art",
|
||||||
|
Enhance: "enhance",
|
||||||
|
FantasyArt: "fantasy-art",
|
||||||
|
Isometric: "isometric",
|
||||||
|
LineArt: "line-art",
|
||||||
|
LowPoly: "low-poly",
|
||||||
|
ModelingCompound: "modeling-compound",
|
||||||
|
NeonPunk: "neon-punk",
|
||||||
|
Origami: "origami",
|
||||||
|
Photographic: "photographic",
|
||||||
|
PixelArt: "pixel-art",
|
||||||
|
TileTexture: "tile-texture",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sd: {
|
||||||
|
SubTitle: (count: number) => `${count} images`,
|
||||||
|
Actions: {
|
||||||
|
Params: "See Params",
|
||||||
|
Copy: "Copy Prompt",
|
||||||
|
Delete: "Delete",
|
||||||
|
Retry: "Retry",
|
||||||
|
ReturnHome: "Return Home",
|
||||||
|
History: "History",
|
||||||
|
},
|
||||||
|
EmptyRecord: "No images yet",
|
||||||
|
Status: {
|
||||||
|
Name: "Status",
|
||||||
|
Success: "Success",
|
||||||
|
Error: "Error",
|
||||||
|
Wait: "Waiting",
|
||||||
|
Running: "Running",
|
||||||
|
},
|
||||||
|
Danger: {
|
||||||
|
Delete: "Confirm to delete?",
|
||||||
|
},
|
||||||
|
GenerateParams: "Generate Params",
|
||||||
|
Detail: "Detail",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default en;
|
export default en;
|
||||||
|
|
|
@ -241,7 +241,7 @@ const tw = {
|
||||||
},
|
},
|
||||||
List: "自訂提示詞列表",
|
List: "自訂提示詞列表",
|
||||||
ListCount: (builtin: number, custom: number) =>
|
ListCount: (builtin: number, custom: number) =>
|
||||||
`內建 ${builtin} 條,使用者自訂 ${custom} 條`,
|
`內建 ${builtin} 條,使用者自訂 ${custom} 條`,
|
||||||
Edit: "編輯",
|
Edit: "編輯",
|
||||||
Modal: {
|
Modal: {
|
||||||
Title: "提示詞列表",
|
Title: "提示詞列表",
|
||||||
|
|
|
@ -39,7 +39,9 @@ const DEFAULT_ALIBABA_URL = isApp
|
||||||
? DEFAULT_API_HOST + "/api/proxy/alibaba"
|
? DEFAULT_API_HOST + "/api/proxy/alibaba"
|
||||||
: ApiPath.Alibaba;
|
: ApiPath.Alibaba;
|
||||||
|
|
||||||
console.log("DEFAULT_ANTHROPIC_URL", DEFAULT_ANTHROPIC_URL);
|
const DEFAULT_STABILITY_URL = isApp
|
||||||
|
? DEFAULT_API_HOST + "/api/proxy/stability"
|
||||||
|
: ApiPath.Stability;
|
||||||
|
|
||||||
const DEFAULT_ACCESS_STATE = {
|
const DEFAULT_ACCESS_STATE = {
|
||||||
accessCode: "",
|
accessCode: "",
|
||||||
|
@ -80,6 +82,10 @@ const DEFAULT_ACCESS_STATE = {
|
||||||
alibabaUrl: DEFAULT_ALIBABA_URL,
|
alibabaUrl: DEFAULT_ALIBABA_URL,
|
||||||
alibabaApiKey: "",
|
alibabaApiKey: "",
|
||||||
|
|
||||||
|
//stability
|
||||||
|
stabilityUrl: DEFAULT_STABILITY_URL,
|
||||||
|
stabilityApiKey: "",
|
||||||
|
|
||||||
// server config
|
// server config
|
||||||
needCode: true,
|
needCode: true,
|
||||||
hideUserApiKey: false,
|
hideUserApiKey: false,
|
||||||
|
|
|
@ -0,0 +1,163 @@
|
||||||
|
import {
|
||||||
|
Stability,
|
||||||
|
StoreKey,
|
||||||
|
ACCESS_CODE_PREFIX,
|
||||||
|
ApiPath,
|
||||||
|
} from "@/app/constant";
|
||||||
|
import { getBearerToken } from "@/app/client/api";
|
||||||
|
import { createPersistStore } from "@/app/utils/store";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { uploadImage, base64Image2Blob } from "@/app/utils/chat";
|
||||||
|
import { models, getModelParamBasicData } from "@/app/components/sd/sd-panel";
|
||||||
|
import { useAccessStore } from "./access";
|
||||||
|
|
||||||
|
const defaultModel = {
|
||||||
|
name: models[0].name,
|
||||||
|
value: models[0].value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultParams = getModelParamBasicData(models[0].params({}), {});
|
||||||
|
|
||||||
|
const DEFAULT_SD_STATE = {
|
||||||
|
currentId: 0,
|
||||||
|
draw: [],
|
||||||
|
currentModel: defaultModel,
|
||||||
|
currentParams: defaultParams,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSdStore = createPersistStore<
|
||||||
|
{
|
||||||
|
currentId: number;
|
||||||
|
draw: any[];
|
||||||
|
currentModel: typeof defaultModel;
|
||||||
|
currentParams: any;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
getNextId: () => number;
|
||||||
|
sendTask: (data: any, okCall?: Function) => void;
|
||||||
|
updateDraw: (draw: any) => void;
|
||||||
|
setCurrentModel: (model: any) => void;
|
||||||
|
setCurrentParams: (data: any) => void;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
DEFAULT_SD_STATE,
|
||||||
|
(set, _get) => {
|
||||||
|
function get() {
|
||||||
|
return {
|
||||||
|
..._get(),
|
||||||
|
...methods,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const methods = {
|
||||||
|
getNextId() {
|
||||||
|
const id = ++_get().currentId;
|
||||||
|
set({ currentId: id });
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
sendTask(data: any, okCall?: Function) {
|
||||||
|
data = { ...data, id: nanoid(), status: "running" };
|
||||||
|
set({ draw: [data, ..._get().draw] });
|
||||||
|
this.getNextId();
|
||||||
|
this.stabilityRequestCall(data);
|
||||||
|
okCall?.();
|
||||||
|
},
|
||||||
|
stabilityRequestCall(data: any) {
|
||||||
|
const accessStore = useAccessStore.getState();
|
||||||
|
let prefix: string = ApiPath.Stability as string;
|
||||||
|
let bearerToken = "";
|
||||||
|
if (accessStore.useCustomConfig) {
|
||||||
|
prefix = accessStore.stabilityUrl || (ApiPath.Stability as string);
|
||||||
|
bearerToken = getBearerToken(accessStore.stabilityApiKey);
|
||||||
|
}
|
||||||
|
if (!bearerToken && accessStore.enabledAccessControl()) {
|
||||||
|
bearerToken = getBearerToken(
|
||||||
|
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const headers = {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: bearerToken,
|
||||||
|
};
|
||||||
|
const path = `${prefix}/${Stability.GeneratePath}/${data.model}`;
|
||||||
|
const formData = new FormData();
|
||||||
|
for (let paramsKey in data.params) {
|
||||||
|
formData.append(paramsKey, data.params[paramsKey]);
|
||||||
|
}
|
||||||
|
fetch(path, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((resData) => {
|
||||||
|
if (resData.errors && resData.errors.length > 0) {
|
||||||
|
this.updateDraw({
|
||||||
|
...data,
|
||||||
|
status: "error",
|
||||||
|
error: resData.errors[0],
|
||||||
|
});
|
||||||
|
this.getNextId();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const self = this;
|
||||||
|
if (resData.finish_reason === "SUCCESS") {
|
||||||
|
uploadImage(base64Image2Blob(resData.image, "image/png"))
|
||||||
|
.then((img_data) => {
|
||||||
|
console.debug("uploadImage success", img_data, self);
|
||||||
|
self.updateDraw({
|
||||||
|
...data,
|
||||||
|
status: "success",
|
||||||
|
img_data,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("uploadImage error", e);
|
||||||
|
self.updateDraw({
|
||||||
|
...data,
|
||||||
|
status: "error",
|
||||||
|
error: JSON.stringify(e),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.updateDraw({
|
||||||
|
...data,
|
||||||
|
status: "error",
|
||||||
|
error: JSON.stringify(resData),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.getNextId();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.updateDraw({ ...data, status: "error", error: error.message });
|
||||||
|
console.error("Error:", error);
|
||||||
|
this.getNextId();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateDraw(_draw: any) {
|
||||||
|
const draw = _get().draw || [];
|
||||||
|
draw.some((item, index) => {
|
||||||
|
if (item.id === _draw.id) {
|
||||||
|
draw[index] = _draw;
|
||||||
|
set(() => ({ draw }));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setCurrentModel(model: any) {
|
||||||
|
set({ currentModel: model });
|
||||||
|
},
|
||||||
|
setCurrentParams(data: any) {
|
||||||
|
set({
|
||||||
|
currentParams: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return methods;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: StoreKey.SdList,
|
||||||
|
version: 1.0,
|
||||||
|
},
|
||||||
|
);
|
|
@ -112,7 +112,7 @@ export function base64Image2Blob(base64Data: string, contentType: string) {
|
||||||
return new Blob([byteArray], { type: contentType });
|
return new Blob([byteArray], { type: contentType });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uploadImage(file: File): Promise<string> {
|
export function uploadImage(file: Blob): Promise<string> {
|
||||||
if (!window._SW_ENABLED) {
|
if (!window._SW_ENABLED) {
|
||||||
// if serviceWorker register error, using compressImage
|
// if serviceWorker register error, using compressImage
|
||||||
return compressImage(file, 256 * 1024);
|
return compressImage(file, 256 * 1024);
|
||||||
|
|
|
@ -15,6 +15,10 @@ self.addEventListener("install", function (event) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function jsonify(data) {
|
||||||
|
return new Response(JSON.stringify(data), { headers: { 'content-type': 'application/json' } })
|
||||||
|
}
|
||||||
|
|
||||||
async function upload(request, url) {
|
async function upload(request, url) {
|
||||||
const formData = await request.formData()
|
const formData = await request.formData()
|
||||||
const file = formData.getAll('file')[0]
|
const file = formData.getAll('file')[0]
|
||||||
|
@ -33,13 +37,13 @@ async function upload(request, url) {
|
||||||
'server': 'ServiceWorker',
|
'server': 'ServiceWorker',
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
return Response.json({ code: 0, data: fileUrl })
|
return jsonify({ code: 0, data: fileUrl })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(request, url) {
|
async function remove(request, url) {
|
||||||
const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE)
|
const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE)
|
||||||
const res = await cache.delete(request.url)
|
const res = await cache.delete(request.url)
|
||||||
return Response.json({ code: 0 })
|
return jsonify({ code: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener("fetch", (e) => {
|
self.addEventListener("fetch", (e) => {
|
||||||
|
@ -56,4 +60,3 @@ self.addEventListener("fetch", (e) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue