mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-11-04 16:23:41 +08:00 
			
		
		
		
	feat: Improve SD list data and API integration
This commit is contained in:
		@@ -337,7 +337,7 @@ function ClearContextDivider() {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ChatAction(props: {
 | 
			
		||||
export function ChatAction(props: {
 | 
			
		||||
  text: string;
 | 
			
		||||
  icon: JSX.Element;
 | 
			
		||||
  onClick: () => void;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,5 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { Sd } from "@/app/components/sd";
 | 
			
		||||
 | 
			
		||||
require("../polyfill");
 | 
			
		||||
 | 
			
		||||
import { useState, useEffect } from "react";
 | 
			
		||||
@@ -32,6 +30,7 @@ import { getClientConfig } from "../config/client";
 | 
			
		||||
import { ClientApi } from "../client/api";
 | 
			
		||||
import { useAccessStore } from "../store";
 | 
			
		||||
import { identifyDefaultClaudeModel } from "../utils/checkers";
 | 
			
		||||
import { initDB } from "react-indexed-db-hook";
 | 
			
		||||
 | 
			
		||||
export function Loading(props: { noLogo?: boolean }) {
 | 
			
		||||
  return (
 | 
			
		||||
@@ -58,6 +57,14 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
 | 
			
		||||
  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() {
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
 | 
			
		||||
@@ -128,7 +135,8 @@ const loadAsyncGoogleFont = () => {
 | 
			
		||||
function Screen() {
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
  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 isMobileScreen = useMobileScreen();
 | 
			
		||||
  const shouldTightBorder =
 | 
			
		||||
@@ -137,7 +145,6 @@ function Screen() {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    loadAsyncGoogleFont();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className={
 | 
			
		||||
@@ -154,7 +161,6 @@ function Screen() {
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          <SideBar className={isHome ? styles["sidebar-show"] : ""} />
 | 
			
		||||
 | 
			
		||||
          <div className={styles["window-content"]} id={SlotID.AppBody}>
 | 
			
		||||
            <Routes>
 | 
			
		||||
              <Route path={Path.Home} element={<Chat />} />
 | 
			
		||||
@@ -162,6 +168,7 @@ function Screen() {
 | 
			
		||||
              <Route path={Path.Masks} element={<MaskPage />} />
 | 
			
		||||
              <Route path={Path.Chat} element={<Chat />} />
 | 
			
		||||
              <Route path={Path.Sd} element={<Sd />} />
 | 
			
		||||
              <Route path={Path.SdPanel} element={<Sd />} />
 | 
			
		||||
              <Route path={Path.Settings} element={<Settings />} />
 | 
			
		||||
            </Routes>
 | 
			
		||||
          </div>
 | 
			
		||||
@@ -173,7 +180,6 @@ function Screen() {
 | 
			
		||||
 | 
			
		||||
export function useLoadData() {
 | 
			
		||||
  const config = useAppConfig();
 | 
			
		||||
 | 
			
		||||
  var api: ClientApi;
 | 
			
		||||
  if (config.modelConfig.model.startsWith("gemini")) {
 | 
			
		||||
    api = new ClientApi(ModelProvider.GeminiPro);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,14 @@
 | 
			
		||||
import styles from "./sd-panel.module.scss";
 | 
			
		||||
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 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) => {
 | 
			
		||||
  return [
 | 
			
		||||
@@ -89,7 +95,7 @@ const sdCommonParams = (model: string, data: any) => {
 | 
			
		||||
      name: locales.SdPanel.OutFormat,
 | 
			
		||||
      value: "output_format",
 | 
			
		||||
      type: "select",
 | 
			
		||||
      default: 0,
 | 
			
		||||
      default: "png",
 | 
			
		||||
      options: [
 | 
			
		||||
        { name: "PNG", value: "png" },
 | 
			
		||||
        { name: "JPEG", value: "jpeg" },
 | 
			
		||||
@@ -128,6 +134,7 @@ const models = [
 | 
			
		||||
export function ControlParamItem(props: {
 | 
			
		||||
  title: string;
 | 
			
		||||
  subTitle?: string;
 | 
			
		||||
  required?: boolean;
 | 
			
		||||
  children?: JSX.Element | JSX.Element[];
 | 
			
		||||
  className?: string;
 | 
			
		||||
}) {
 | 
			
		||||
@@ -135,7 +142,10 @@ export function ControlParamItem(props: {
 | 
			
		||||
    <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}</div>
 | 
			
		||||
          <div>
 | 
			
		||||
            {props.title}
 | 
			
		||||
            {props.required && <span style={{ color: "red" }}>*</span>}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      {props.children}
 | 
			
		||||
@@ -160,7 +170,11 @@ export function ControlParam(props: {
 | 
			
		||||
        switch (item.type) {
 | 
			
		||||
          case "textarea":
 | 
			
		||||
            element = (
 | 
			
		||||
              <ControlParamItem title={item.name} subTitle={item.sub}>
 | 
			
		||||
              <ControlParamItem
 | 
			
		||||
                title={item.name}
 | 
			
		||||
                subTitle={item.sub}
 | 
			
		||||
                required={item.required}
 | 
			
		||||
              >
 | 
			
		||||
                <textarea
 | 
			
		||||
                  rows={item.rows || 3}
 | 
			
		||||
                  style={{ maxWidth: "100%", width: "100%", padding: "10px" }}
 | 
			
		||||
@@ -175,7 +189,11 @@ export function ControlParam(props: {
 | 
			
		||||
            break;
 | 
			
		||||
          case "select":
 | 
			
		||||
            element = (
 | 
			
		||||
              <ControlParamItem title={item.name} subTitle={item.sub}>
 | 
			
		||||
              <ControlParamItem
 | 
			
		||||
                title={item.name}
 | 
			
		||||
                subTitle={item.sub}
 | 
			
		||||
                required={item.required}
 | 
			
		||||
              >
 | 
			
		||||
                <Select
 | 
			
		||||
                  value={props.data[item.value]}
 | 
			
		||||
                  onChange={(e) => {
 | 
			
		||||
@@ -195,7 +213,11 @@ export function ControlParam(props: {
 | 
			
		||||
            break;
 | 
			
		||||
          case "number":
 | 
			
		||||
            element = (
 | 
			
		||||
              <ControlParamItem title={item.name} subTitle={item.sub}>
 | 
			
		||||
              <ControlParamItem
 | 
			
		||||
                title={item.name}
 | 
			
		||||
                subTitle={item.sub}
 | 
			
		||||
                required={item.required}
 | 
			
		||||
              >
 | 
			
		||||
                <input
 | 
			
		||||
                  type="number"
 | 
			
		||||
                  min={item.min}
 | 
			
		||||
@@ -210,7 +232,11 @@ export function ControlParam(props: {
 | 
			
		||||
            break;
 | 
			
		||||
          default:
 | 
			
		||||
            element = (
 | 
			
		||||
              <ControlParamItem title={item.name} subTitle={item.sub}>
 | 
			
		||||
              <ControlParamItem
 | 
			
		||||
                title={item.name}
 | 
			
		||||
                subTitle={item.sub}
 | 
			
		||||
                required={item.required}
 | 
			
		||||
              >
 | 
			
		||||
                <input
 | 
			
		||||
                  type="text"
 | 
			
		||||
                  value={props.data[item.value]}
 | 
			
		||||
@@ -260,14 +286,43 @@ export function SdPanel() {
 | 
			
		||||
    setCurrentModel(model);
 | 
			
		||||
    setParams(getModelParamBasicData(model.params({}), params));
 | 
			
		||||
  };
 | 
			
		||||
  const sdListDb = useIndexedDB(StoreKey.SdList);
 | 
			
		||||
  const { execCountInc } = useSdStore();
 | 
			
		||||
  const handleSubmit = () => {
 | 
			
		||||
    const columns = currentModel.params(params);
 | 
			
		||||
    const reqData: any = {};
 | 
			
		||||
    columns.forEach((item: any) => {
 | 
			
		||||
      reqData[item.value] = params[item.value] ?? null;
 | 
			
		||||
    });
 | 
			
		||||
    console.log(JSON.stringify(reqData, null, 4));
 | 
			
		||||
    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(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 (
 | 
			
		||||
    <>
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
  return <div>sd</div>;
 | 
			
		||||
import chatStyles from "@/app/components/chat.module.scss";
 | 
			
		||||
import styles from "@/app/components/sd.module.scss";
 | 
			
		||||
import { IconButton } from "@/app/components/button";
 | 
			
		||||
import ReturnIcon from "@/app/icons/return.svg";
 | 
			
		||||
import Locale from "@/app/locales";
 | 
			
		||||
import { Path, 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;
 | 
			
		||||
  switch (location.pathname) {
 | 
			
		||||
    case Path.Sd:
 | 
			
		||||
    case Path.SdPanel:
 | 
			
		||||
      bodyComponent = <SdPanel />;
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
@@ -220,6 +221,7 @@ export function SideBar(props: { className?: string }) {
 | 
			
		||||
 | 
			
		||||
      <div className={styles["sidebar-tail"]}>
 | 
			
		||||
        <div className={styles["sidebar-actions"]}>
 | 
			
		||||
          {isChat && (
 | 
			
		||||
            <div className={styles["sidebar-action"] + " " + styles.mobile}>
 | 
			
		||||
              <IconButton
 | 
			
		||||
                icon={<DeleteIcon />}
 | 
			
		||||
@@ -230,6 +232,7 @@ export function SideBar(props: { className?: string }) {
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
          <div className={styles["sidebar-action"]}>
 | 
			
		||||
            <Link to={Path.Settings}>
 | 
			
		||||
              <IconButton icon={<SettingsIcon />} shadow />
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import { BuildConfig, getBuildConfig } from "./build";
 | 
			
		||||
export function getClientConfig() {
 | 
			
		||||
  if (typeof document !== "undefined") {
 | 
			
		||||
    // client side
 | 
			
		||||
    return JSON.parse(queryMeta("config")) as BuildConfig;
 | 
			
		||||
    return JSON.parse(queryMeta("config") || "{}") as BuildConfig;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (typeof process !== "undefined") {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ export enum Path {
 | 
			
		||||
  Masks = "/masks",
 | 
			
		||||
  Auth = "/auth",
 | 
			
		||||
  Sd = "/sd",
 | 
			
		||||
  SdPanel = "/sd-panel",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum ApiPath {
 | 
			
		||||
@@ -48,6 +49,7 @@ export enum StoreKey {
 | 
			
		||||
  Prompt = "prompt-store",
 | 
			
		||||
  Update = "chat-update",
 | 
			
		||||
  Sync = "sync",
 | 
			
		||||
  SdList = "sd-list",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
 | 
			
		||||
 
 | 
			
		||||
@@ -494,6 +494,7 @@ const cn = {
 | 
			
		||||
    AIModel: "AI模型",
 | 
			
		||||
    ModelVersion: "模型版本",
 | 
			
		||||
    Submit: "提交生成",
 | 
			
		||||
    ParamIsRequired: (name: string) => `${name}不能为空`,
 | 
			
		||||
    Styles: {
 | 
			
		||||
      D3Model: "3D模型",
 | 
			
		||||
      AnalogFilm: "模拟电影",
 | 
			
		||||
@@ -514,6 +515,26 @@ const cn = {
 | 
			
		||||
      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
 | 
			
		||||
 
 | 
			
		||||
@@ -500,6 +500,7 @@ const en: LocaleType = {
 | 
			
		||||
    AIModel: "AI Model",
 | 
			
		||||
    ModelVersion: "Model Version",
 | 
			
		||||
    Submit: "Submit",
 | 
			
		||||
    ParamIsRequired: (name: string) => `${name} is required`,
 | 
			
		||||
    Styles: {
 | 
			
		||||
      D3Model: "3d-model",
 | 
			
		||||
      AnalogFilm: "analog-film",
 | 
			
		||||
@@ -520,6 +521,26 @@ const en: LocaleType = {
 | 
			
		||||
      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;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { Analytics } from "@vercel/analytics/react";
 | 
			
		||||
import { Home } from "./components/home";
 | 
			
		||||
 | 
			
		||||
import { getServerSideConfig } from "./config/server";
 | 
			
		||||
import { SdDbInit } from "@/app/store/sd";
 | 
			
		||||
 | 
			
		||||
const serverConfig = getServerSideConfig();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								app/store/sd.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								app/store/sd.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-indexed-db-hook": "^1.0.14",
 | 
			
		||||
    "react-markdown": "^8.0.7",
 | 
			
		||||
    "react-router-dom": "^6.15.0",
 | 
			
		||||
    "rehype-highlight": "^6.0.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -5110,6 +5110,11 @@ react-dom@^18.2.0:
 | 
			
		||||
    loose-envify "^1.1.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:
 | 
			
		||||
  version "16.13.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user