feat: Improve SD list data and API integration
This commit is contained in:
parent
54401162bd
commit
a16725ac17
|
@ -337,7 +337,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,7 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Sd } from "@/app/components/sd";
|
|
||||||
|
|
||||||
require("../polyfill");
|
require("../polyfill");
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
@ -32,6 +30,7 @@ import { getClientConfig } from "../config/client";
|
||||||
import { ClientApi } from "../client/api";
|
import { ClientApi } from "../client/api";
|
||||||
import { useAccessStore } from "../store";
|
import { useAccessStore } from "../store";
|
||||||
import { identifyDefaultClaudeModel } from "../utils/checkers";
|
import { identifyDefaultClaudeModel } from "../utils/checkers";
|
||||||
|
import { initDB } from "react-indexed-db-hook";
|
||||||
|
|
||||||
export function Loading(props: { noLogo?: boolean }) {
|
export function Loading(props: { noLogo?: boolean }) {
|
||||||
return (
|
return (
|
||||||
|
@ -58,6 +57,14 @@ 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 />,
|
||||||
|
});
|
||||||
|
|
||||||
|
const SdPanel = dynamic(async () => (await import("./sd-panel")).SdPanel, {
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
});
|
||||||
|
|
||||||
export function useSwitchTheme() {
|
export function useSwitchTheme() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
|
||||||
|
@ -128,7 +135,8 @@ const loadAsyncGoogleFont = () => {
|
||||||
function Screen() {
|
function Screen() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isHome = location.pathname === Path.Home;
|
const isHome =
|
||||||
|
location.pathname === Path.Home || location.pathname === Path.SdPanel;
|
||||||
const isAuth = location.pathname === Path.Auth;
|
const isAuth = location.pathname === Path.Auth;
|
||||||
const isMobileScreen = useMobileScreen();
|
const isMobileScreen = useMobileScreen();
|
||||||
const shouldTightBorder =
|
const shouldTightBorder =
|
||||||
|
@ -137,7 +145,6 @@ function Screen() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAsyncGoogleFont();
|
loadAsyncGoogleFont();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
@ -154,7 +161,6 @@ function Screen() {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||||
|
|
||||||
<div className={styles["window-content"]} id={SlotID.AppBody}>
|
<div className={styles["window-content"]} id={SlotID.AppBody}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path={Path.Home} element={<Chat />} />
|
<Route path={Path.Home} element={<Chat />} />
|
||||||
|
@ -162,6 +168,7 @@ function Screen() {
|
||||||
<Route path={Path.Masks} element={<MaskPage />} />
|
<Route path={Path.Masks} element={<MaskPage />} />
|
||||||
<Route path={Path.Chat} element={<Chat />} />
|
<Route path={Path.Chat} element={<Chat />} />
|
||||||
<Route path={Path.Sd} element={<Sd />} />
|
<Route path={Path.Sd} element={<Sd />} />
|
||||||
|
<Route path={Path.SdPanel} element={<Sd />} />
|
||||||
<Route path={Path.Settings} element={<Settings />} />
|
<Route path={Path.Settings} element={<Settings />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
|
@ -173,7 +180,6 @@ function Screen() {
|
||||||
|
|
||||||
export function useLoadData() {
|
export function useLoadData() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
|
||||||
var api: ClientApi;
|
var api: ClientApi;
|
||||||
if (config.modelConfig.model.startsWith("gemini")) {
|
if (config.modelConfig.model.startsWith("gemini")) {
|
||||||
api = new ClientApi(ModelProvider.GeminiPro);
|
api = new ClientApi(ModelProvider.GeminiPro);
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import styles from "./sd-panel.module.scss";
|
import styles from "./sd-panel.module.scss";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Select } from "@/app/components/ui-lib";
|
import { Select, showToast } from "@/app/components/ui-lib";
|
||||||
import { IconButton } from "@/app/components/button";
|
import { IconButton } from "@/app/components/button";
|
||||||
import locales from "@/app/locales";
|
import locales from "@/app/locales";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { useIndexedDB } from "react-indexed-db-hook";
|
||||||
|
import { StoreKey } from "@/app/constant";
|
||||||
|
import { SdDbInit, sendSdTask, useSdStore } from "@/app/store/sd";
|
||||||
|
|
||||||
|
SdDbInit();
|
||||||
|
|
||||||
const sdCommonParams = (model: string, data: any) => {
|
const sdCommonParams = (model: string, data: any) => {
|
||||||
return [
|
return [
|
||||||
|
@ -89,7 +95,7 @@ const sdCommonParams = (model: string, data: any) => {
|
||||||
name: locales.SdPanel.OutFormat,
|
name: locales.SdPanel.OutFormat,
|
||||||
value: "output_format",
|
value: "output_format",
|
||||||
type: "select",
|
type: "select",
|
||||||
default: 0,
|
default: "png",
|
||||||
options: [
|
options: [
|
||||||
{ name: "PNG", value: "png" },
|
{ name: "PNG", value: "png" },
|
||||||
{ name: "JPEG", value: "jpeg" },
|
{ name: "JPEG", value: "jpeg" },
|
||||||
|
@ -128,6 +134,7 @@ const models = [
|
||||||
export function ControlParamItem(props: {
|
export function ControlParamItem(props: {
|
||||||
title: string;
|
title: string;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
|
required?: boolean;
|
||||||
children?: JSX.Element | JSX.Element[];
|
children?: JSX.Element | JSX.Element[];
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
|
@ -135,7 +142,10 @@ export function ControlParamItem(props: {
|
||||||
<div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
|
<div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
|
||||||
<div className={styles["ctrl-param-item-header"]}>
|
<div className={styles["ctrl-param-item-header"]}>
|
||||||
<div className={styles["ctrl-param-item-title"]}>
|
<div className={styles["ctrl-param-item-title"]}>
|
||||||
<div>{props.title}</div>
|
<div>
|
||||||
|
{props.title}
|
||||||
|
{props.required && <span style={{ color: "red" }}>*</span>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
@ -160,7 +170,11 @@ export function ControlParam(props: {
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case "textarea":
|
case "textarea":
|
||||||
element = (
|
element = (
|
||||||
<ControlParamItem title={item.name} subTitle={item.sub}>
|
<ControlParamItem
|
||||||
|
title={item.name}
|
||||||
|
subTitle={item.sub}
|
||||||
|
required={item.required}
|
||||||
|
>
|
||||||
<textarea
|
<textarea
|
||||||
rows={item.rows || 3}
|
rows={item.rows || 3}
|
||||||
style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
|
style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
|
||||||
|
@ -175,7 +189,11 @@ export function ControlParam(props: {
|
||||||
break;
|
break;
|
||||||
case "select":
|
case "select":
|
||||||
element = (
|
element = (
|
||||||
<ControlParamItem title={item.name} subTitle={item.sub}>
|
<ControlParamItem
|
||||||
|
title={item.name}
|
||||||
|
subTitle={item.sub}
|
||||||
|
required={item.required}
|
||||||
|
>
|
||||||
<Select
|
<Select
|
||||||
value={props.data[item.value]}
|
value={props.data[item.value]}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
@ -195,7 +213,11 @@ export function ControlParam(props: {
|
||||||
break;
|
break;
|
||||||
case "number":
|
case "number":
|
||||||
element = (
|
element = (
|
||||||
<ControlParamItem title={item.name} subTitle={item.sub}>
|
<ControlParamItem
|
||||||
|
title={item.name}
|
||||||
|
subTitle={item.sub}
|
||||||
|
required={item.required}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={item.min}
|
min={item.min}
|
||||||
|
@ -210,7 +232,11 @@ export function ControlParam(props: {
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
element = (
|
element = (
|
||||||
<ControlParamItem title={item.name} subTitle={item.sub}>
|
<ControlParamItem
|
||||||
|
title={item.name}
|
||||||
|
subTitle={item.sub}
|
||||||
|
required={item.required}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={props.data[item.value]}
|
value={props.data[item.value]}
|
||||||
|
@ -260,14 +286,43 @@ export function SdPanel() {
|
||||||
setCurrentModel(model);
|
setCurrentModel(model);
|
||||||
setParams(getModelParamBasicData(model.params({}), params));
|
setParams(getModelParamBasicData(model.params({}), params));
|
||||||
};
|
};
|
||||||
|
const sdListDb = useIndexedDB(StoreKey.SdList);
|
||||||
|
const { execCountInc } = useSdStore();
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
const columns = currentModel.params(params);
|
const columns = currentModel.params(params);
|
||||||
const reqData: any = {};
|
const reqParams: any = {};
|
||||||
columns.forEach((item: any) => {
|
for (let i = 0; i < columns.length; i++) {
|
||||||
reqData[item.value] = params[item.value] ?? null;
|
const item = columns[i];
|
||||||
});
|
reqParams[item.value] = params[item.value] ?? null;
|
||||||
console.log(JSON.stringify(reqData, null, 4));
|
if (item.required) {
|
||||||
setParams(getModelParamBasicData(columns, params, true));
|
if (!reqParams[item.value]) {
|
||||||
|
showToast(locales.SdPanel.ParamIsRequired(item.name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// console.log(JSON.stringify(reqParams, null, 4));
|
||||||
|
let data: any = {
|
||||||
|
model: currentModel.value,
|
||||||
|
model_name: currentModel.name,
|
||||||
|
status: "wait",
|
||||||
|
params: reqParams,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
img_data: "",
|
||||||
|
};
|
||||||
|
sdListDb.add(data).then(
|
||||||
|
(id) => {
|
||||||
|
data = { ...data, id, status: "running" };
|
||||||
|
sdListDb.update(data);
|
||||||
|
execCountInc();
|
||||||
|
sendSdTask(data, sdListDb, execCountInc);
|
||||||
|
setParams(getModelParamBasicData(columns, params, true));
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error(error);
|
||||||
|
showToast(`error: ` + error.message);
|
||||||
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,232 @@
|
||||||
export function Sd() {
|
import chatStyles from "@/app/components/chat.module.scss";
|
||||||
return <div>sd</div>;
|
import styles from "@/app/components/sd.module.scss";
|
||||||
|
import { IconButton } from "@/app/components/button";
|
||||||
|
import ReturnIcon from "@/app/icons/return.svg";
|
||||||
|
import Locale from "@/app/locales";
|
||||||
|
import { Path, StoreKey } from "@/app/constant";
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
copyToClipboard,
|
||||||
|
getMessageTextContent,
|
||||||
|
useMobileScreen,
|
||||||
|
useWindowSize,
|
||||||
|
} from "@/app/utils";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useAppConfig } from "@/app/store";
|
||||||
|
import MinIcon from "@/app/icons/min.svg";
|
||||||
|
import MaxIcon from "@/app/icons/max.svg";
|
||||||
|
import { getClientConfig } from "@/app/config/client";
|
||||||
|
import { ChatAction } from "@/app/components/chat";
|
||||||
|
import DeleteIcon from "@/app/icons/clear.svg";
|
||||||
|
import CopyIcon from "@/app/icons/copy.svg";
|
||||||
|
import PromptIcon from "@/app/icons/prompt.svg";
|
||||||
|
import ResetIcon from "@/app/icons/reload.svg";
|
||||||
|
import { useIndexedDB } from "react-indexed-db-hook";
|
||||||
|
import { useSdStore } from "@/app/store/sd";
|
||||||
|
import locales from "@/app/locales";
|
||||||
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
|
import ErrorIcon from "../icons/delete.svg";
|
||||||
|
import { Property } from "csstype";
|
||||||
|
import { showConfirm } from "@/app/components/ui-lib";
|
||||||
|
|
||||||
|
function openBase64ImgUrl(base64Data: string, contentType: string) {
|
||||||
|
const byteCharacters = atob(base64Data);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length);
|
||||||
|
for (let i = 0; i < byteCharacters.length; i++) {
|
||||||
|
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const byteArray = new Uint8Array(byteNumbers);
|
||||||
|
const blob = new Blob([byteArray], { type: contentType });
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
window.open(blobUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
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> - {item.error}</span>}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sd() {
|
||||||
|
const isMobileScreen = useMobileScreen();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const clientConfig = useMemo(() => getClientConfig(), []);
|
||||||
|
const showMaxIcon = !isMobileScreen && !clientConfig?.isApp;
|
||||||
|
const config = useAppConfig();
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const sdListDb = useIndexedDB(StoreKey.SdList);
|
||||||
|
const [sdImages, setSdImages] = useState([]);
|
||||||
|
const { execCount } = useSdStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sdListDb.getAll().then((data) => {
|
||||||
|
setSdImages(((data as never[]) || []).reverse());
|
||||||
|
});
|
||||||
|
}, [execCount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={chatStyles.chat} key={"1"}>
|
||||||
|
<div className="window-header" data-tauri-drag-region>
|
||||||
|
{isMobileScreen && (
|
||||||
|
<div className="window-actions">
|
||||||
|
<div className={"window-action-button"}>
|
||||||
|
<IconButton
|
||||||
|
icon={<ReturnIcon />}
|
||||||
|
bordered
|
||||||
|
title={Locale.Chat.Actions.ChatList}
|
||||||
|
onClick={() => navigate(Path.SdPanel)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`window-header-title ${chatStyles["chat-body-title"]}`}>
|
||||||
|
<div className={`window-header-main-title`}>Stability AI</div>
|
||||||
|
<div className="window-header-sub-title">
|
||||||
|
{Locale.Sd.SubTitle(sdImages.length || 0)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="window-actions">
|
||||||
|
{showMaxIcon && (
|
||||||
|
<div className="window-action-button">
|
||||||
|
<IconButton
|
||||||
|
icon={config.tightBorder ? <MinIcon /> : <MaxIcon />}
|
||||||
|
bordered
|
||||||
|
onClick={() => {
|
||||||
|
config.update(
|
||||||
|
(config) => (config.tightBorder = !config.tightBorder),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={chatStyles["chat-body"]} ref={scrollRef}>
|
||||||
|
<div className={styles["sd-img-list"]}>
|
||||||
|
{sdImages.length > 0 ? (
|
||||||
|
sdImages.map((item: any) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
style={{ display: "flex" }}
|
||||||
|
className={styles["sd-img-item"]}
|
||||||
|
>
|
||||||
|
{item.status === "success" ? (
|
||||||
|
<img
|
||||||
|
className={styles["img"]}
|
||||||
|
src={`data:image/png;base64,${item.img_data}`}
|
||||||
|
alt={`${item.id}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
openBase64ImgUrl(item.img_data, "image/png");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : 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 title={item.params.prompt}>
|
||||||
|
{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={() => console.log(1)}
|
||||||
|
/>
|
||||||
|
<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={() => console.log(1)}
|
||||||
|
/>
|
||||||
|
<ChatAction
|
||||||
|
text={Locale.Sd.Actions.Delete}
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
onClick={async () => {
|
||||||
|
if (await showConfirm(Locale.Sd.Danger.Delete)) {
|
||||||
|
sdListDb.deleteRecord(item.id).then(
|
||||||
|
() => {
|
||||||
|
setSdImages(
|
||||||
|
sdImages.filter(
|
||||||
|
(i: any) => i.id !== item.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div>{locales.Sd.EmptyRecord}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,6 +155,7 @@ export function SideBar(props: { className?: string }) {
|
||||||
let isChat: boolean = false;
|
let isChat: boolean = false;
|
||||||
switch (location.pathname) {
|
switch (location.pathname) {
|
||||||
case Path.Sd:
|
case Path.Sd:
|
||||||
|
case Path.SdPanel:
|
||||||
bodyComponent = <SdPanel />;
|
bodyComponent = <SdPanel />;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -220,16 +221,18 @@ export function SideBar(props: { className?: string }) {
|
||||||
|
|
||||||
<div className={styles["sidebar-tail"]}>
|
<div className={styles["sidebar-tail"]}>
|
||||||
<div className={styles["sidebar-actions"]}>
|
<div className={styles["sidebar-actions"]}>
|
||||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
{isChat && (
|
||||||
<IconButton
|
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||||
icon={<DeleteIcon />}
|
<IconButton
|
||||||
onClick={async () => {
|
icon={<DeleteIcon />}
|
||||||
if (await showConfirm(Locale.Home.DeleteChat)) {
|
onClick={async () => {
|
||||||
chatStore.deleteSession(chatStore.currentSessionIndex);
|
if (await showConfirm(Locale.Home.DeleteChat)) {
|
||||||
}
|
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 />
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -22,6 +22,7 @@ export enum Path {
|
||||||
Masks = "/masks",
|
Masks = "/masks",
|
||||||
Auth = "/auth",
|
Auth = "/auth",
|
||||||
Sd = "/sd",
|
Sd = "/sd",
|
||||||
|
SdPanel = "/sd-panel",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ApiPath {
|
export enum ApiPath {
|
||||||
|
@ -48,6 +49,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;
|
||||||
|
|
|
@ -494,6 +494,7 @@ const cn = {
|
||||||
AIModel: "AI模型",
|
AIModel: "AI模型",
|
||||||
ModelVersion: "模型版本",
|
ModelVersion: "模型版本",
|
||||||
Submit: "提交生成",
|
Submit: "提交生成",
|
||||||
|
ParamIsRequired: (name: string) => `${name}不能为空`,
|
||||||
Styles: {
|
Styles: {
|
||||||
D3Model: "3D模型",
|
D3Model: "3D模型",
|
||||||
AnalogFilm: "模拟电影",
|
AnalogFilm: "模拟电影",
|
||||||
|
@ -514,6 +515,26 @@ const cn = {
|
||||||
TileTexture: "贴图",
|
TileTexture: "贴图",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Sd: {
|
||||||
|
SubTitle: (count: number) => `共 ${count} 条绘画`,
|
||||||
|
Actions: {
|
||||||
|
Params: "查看参数",
|
||||||
|
Copy: "复制提示词",
|
||||||
|
Delete: "删除",
|
||||||
|
Retry: "重试",
|
||||||
|
},
|
||||||
|
EmptyRecord: "暂无绘画记录",
|
||||||
|
Status: {
|
||||||
|
Name: "状态",
|
||||||
|
Success: "成功",
|
||||||
|
Error: "失败",
|
||||||
|
Wait: "等待中",
|
||||||
|
Running: "运行中",
|
||||||
|
},
|
||||||
|
Danger: {
|
||||||
|
Delete: "确认删除?",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeepPartial<T> = T extends object
|
type DeepPartial<T> = T extends object
|
||||||
|
|
|
@ -500,6 +500,7 @@ const en: LocaleType = {
|
||||||
AIModel: "AI Model",
|
AIModel: "AI Model",
|
||||||
ModelVersion: "Model Version",
|
ModelVersion: "Model Version",
|
||||||
Submit: "Submit",
|
Submit: "Submit",
|
||||||
|
ParamIsRequired: (name: string) => `${name} is required`,
|
||||||
Styles: {
|
Styles: {
|
||||||
D3Model: "3d-model",
|
D3Model: "3d-model",
|
||||||
AnalogFilm: "analog-film",
|
AnalogFilm: "analog-film",
|
||||||
|
@ -520,6 +521,26 @@ const en: LocaleType = {
|
||||||
TileTexture: "tile-texture",
|
TileTexture: "tile-texture",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Sd: {
|
||||||
|
SubTitle: (count: number) => `${count} images`,
|
||||||
|
Actions: {
|
||||||
|
Params: "See Params",
|
||||||
|
Copy: "Copy Prompt",
|
||||||
|
Delete: "Delete",
|
||||||
|
Retry: "Retry",
|
||||||
|
},
|
||||||
|
EmptyRecord: "No images yet",
|
||||||
|
Status: {
|
||||||
|
Name: "Status",
|
||||||
|
Success: "Success",
|
||||||
|
Error: "Error",
|
||||||
|
Wait: "Waiting",
|
||||||
|
Running: "Running",
|
||||||
|
},
|
||||||
|
Danger: {
|
||||||
|
Delete: "Confirm to delete?",
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default en;
|
export default en;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Analytics } from "@vercel/analytics/react";
|
||||||
import { Home } from "./components/home";
|
import { Home } from "./components/home";
|
||||||
|
|
||||||
import { getServerSideConfig } from "./config/server";
|
import { getServerSideConfig } from "./config/server";
|
||||||
|
import { SdDbInit } from "@/app/store/sd";
|
||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { initDB, useIndexedDB } from "react-indexed-db-hook";
|
||||||
|
import { StoreKey } from "@/app/constant";
|
||||||
|
import { create, StoreApi } from "zustand";
|
||||||
|
|
||||||
|
export const SdDbConfig = {
|
||||||
|
name: "@chatgpt-next-web/sd",
|
||||||
|
version: 1,
|
||||||
|
objectStoresMeta: [
|
||||||
|
{
|
||||||
|
store: StoreKey.SdList,
|
||||||
|
storeConfig: { keyPath: "id", autoIncrement: true },
|
||||||
|
storeSchema: [
|
||||||
|
{ name: "model", keypath: "model", options: { unique: false } },
|
||||||
|
{
|
||||||
|
name: "model_name",
|
||||||
|
keypath: "model_name",
|
||||||
|
options: { unique: false },
|
||||||
|
},
|
||||||
|
{ name: "status", keypath: "status", options: { unique: false } },
|
||||||
|
{ name: "params", keypath: "params", options: { unique: false } },
|
||||||
|
{ name: "img_data", keypath: "img_data", options: { unique: false } },
|
||||||
|
{ name: "error", keypath: "error", options: { unique: false } },
|
||||||
|
{
|
||||||
|
name: "created_at",
|
||||||
|
keypath: "created_at",
|
||||||
|
options: { unique: false },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SdDbInit() {
|
||||||
|
initDB(SdDbConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
type SdStore = {
|
||||||
|
execCount: number;
|
||||||
|
execCountInc: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSdStore = create<SdStore>()((set) => ({
|
||||||
|
execCount: 1,
|
||||||
|
execCountInc: () => set((state) => ({ execCount: state.execCount + 1 })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function sendSdTask(data: any, db: any, inc: any) {
|
||||||
|
const formData = new FormData();
|
||||||
|
for (let paramsKey in data.params) {
|
||||||
|
formData.append(paramsKey, data.params[paramsKey]);
|
||||||
|
}
|
||||||
|
fetch("https://api.stability.ai/v2beta/stable-image/generate/" + data.model, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((resData) => {
|
||||||
|
if (resData.errors && resData.errors.length > 0) {
|
||||||
|
db.update({ ...data, status: "error", error: resData.errors[0] });
|
||||||
|
inc();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (resData.finish_reason === "SUCCESS") {
|
||||||
|
db.update({ ...data, status: "success", img_data: resData.image });
|
||||||
|
} else {
|
||||||
|
db.update({ ...data, status: "error", error: JSON.stringify(resData) });
|
||||||
|
}
|
||||||
|
inc();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
db.update({ ...data, status: "error", error: error.message });
|
||||||
|
console.error("Error:", error);
|
||||||
|
inc();
|
||||||
|
});
|
||||||
|
}
|
|
@ -32,6 +32,7 @@
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-indexed-db-hook": "^1.0.14",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
"rehype-highlight": "^6.0.0",
|
"rehype-highlight": "^6.0.0",
|
||||||
|
|
|
@ -5110,6 +5110,11 @@ react-dom@^18.2.0:
|
||||||
loose-envify "^1.1.0"
|
loose-envify "^1.1.0"
|
||||||
scheduler "^0.23.0"
|
scheduler "^0.23.0"
|
||||||
|
|
||||||
|
react-indexed-db-hook@^1.0.14:
|
||||||
|
version "1.0.14"
|
||||||
|
resolved "https://registry.npmmirror.com/react-indexed-db-hook/-/react-indexed-db-hook-1.0.14.tgz#a29cd732d592735b6a68dfc94316b7a4a091e6be"
|
||||||
|
integrity sha512-tQ6rWofgXUCBhZp9pRpWzthzPbjqcll5uXMo07lbQTKl47VyL9nw9wfVswRxxzS5yj5Sq/VHUkNUjamWbA/M/w==
|
||||||
|
|
||||||
react-is@^16.13.1, react-is@^16.7.0:
|
react-is@^16.13.1, react-is@^16.7.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
|
Loading…
Reference in New Issue