diff --git a/app/components/home.tsx b/app/components/home.tsx index 875f37e5445..aacd2326480 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -29,7 +29,6 @@ import { AuthPage } from "./auth"; import { getClientConfig } from "../config/client"; import { type ClientApi, getClientApi } from "../client/api"; import { useAccessStore } from "../store"; -import { initDB } from "react-indexed-db-hook"; export function Loading(props: { noLogo?: boolean }) { return ( diff --git a/app/components/sd-panel.tsx b/app/components/sd-panel.tsx index d7476ccf84a..c6b28f22157 100644 --- a/app/components/sd-panel.tsx +++ b/app/components/sd-panel.tsx @@ -4,11 +4,8 @@ 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(); +import { useSdStore } from "@/app/store/sd"; const sdCommonParams = (model: string, data: any) => { return [ @@ -286,8 +283,7 @@ export function SdPanel() { setCurrentModel(model); setParams(getModelParamBasicData(model.params({}), params)); }; - const sdListDb = useIndexedDB(StoreKey.SdList); - const { execCountInc } = useSdStore(); + const sdStore = useSdStore(); const handleSubmit = () => { const columns = currentModel.params(params); const reqParams: any = {}; @@ -309,7 +305,7 @@ export function SdPanel() { created_at: new Date().toLocaleString(), img_data: "", }; - sendSdTask(data, sdListDb, execCountInc, () => { + sdStore.sendTask(data, () => { setParams(getModelParamBasicData(columns, params, true)); }); }; diff --git a/app/components/sd.tsx b/app/components/sd.tsx index cc0e326e393..19684c24088 100644 --- a/app/components/sd.tsx +++ b/app/components/sd.tsx @@ -3,7 +3,7 @@ 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 { Path } from "@/app/constant"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { copyToClipboard, @@ -20,8 +20,7 @@ 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 { sendSdTask, useSdStore } from "@/app/store/sd"; +import { useSdStore } from "@/app/store/sd"; import locales from "@/app/locales"; import LoadingIcon from "../icons/three-dots.svg"; import ErrorIcon from "../icons/delete.svg"; @@ -31,17 +30,7 @@ import { showImageModal, showModal, } from "@/app/components/ui-lib"; - -function getBase64ImgUrl(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 }); - return URL.createObjectURL(blob); -} +import { removeImage } from "@/app/utils/chat"; function getSdTaskStatus(item: any) { let s: string; @@ -100,15 +89,12 @@ export function Sd() { const showMaxIcon = !isMobileScreen && !clientConfig?.isApp; const config = useAppConfig(); const scrollRef = useRef(null); - const sdListDb = useIndexedDB(StoreKey.SdList); - const [sdImages, setSdImages] = useState([]); - const { execCount, execCountInc } = useSdStore(); + const sdStore = useSdStore(); + const [sdImages, setSdImages] = useState(sdStore.draw); useEffect(() => { - sdListDb.getAll().then((data) => { - setSdImages(((data as never[]) || []).reverse()); - }); - }, [execCount]); + setSdImages(sdStore.draw); + }, [sdStore.currentId]); return (
@@ -161,11 +147,11 @@ export function Sd() { {item.status === "success" ? ( {`${item.id}`} { + src={item.img_data} + alt={item.id} + onClick={(e) => showImageModal( - getBase64ImgUrl(item.img_data, "image/png"), + item.img_data, true, isMobileScreen ? { width: "100%", height: "fit-content" } @@ -173,8 +159,8 @@ export function Sd() { isMobileScreen ? { width: "100%", height: "fit-content" } : { width: "100%", height: "100%" }, - ); - }} + ) + } /> ) : item.status === "error" ? (
@@ -258,7 +244,7 @@ export function Sd() { created_at: new Date().toLocaleString(), img_data: "", }; - sendSdTask(reqData, sdListDb, execCountInc); + sdStore.sendTask(reqData); }} /> } 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); - }, - ); + // remove img_data + remove item in list + removeImage(item.img_data).finally(() => { + sdStore.draw = sdImages.filter( + (i: any) => i.id !== item.id, + ); + sdStore.getNextId(); + }); } }} /> diff --git a/app/constant.ts b/app/constant.ts index c51e85a150a..d50db15f911 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -1,5 +1,3 @@ -import { stabilityRequestCall } from "@/app/store/sd"; - export const OWNER = "Yidadaa"; export const REPO = "ChatGPT-Next-Web"; export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; @@ -25,6 +23,8 @@ export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com"; export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/"; +export const UPLOAD_URL = "/api/cache/upload"; + export enum Path { Home = "/", Chat = "/chat", @@ -57,6 +57,7 @@ export enum FileName { } export enum StoreKey { + File = "chat-next-web-file", Chat = "chat-next-web-store", Access = "access-control", Config = "app-config", diff --git a/app/page.tsx b/app/page.tsx index 6d24b015438..b3f169a9b74 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,7 +3,6 @@ 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(); diff --git a/app/store/sd.ts b/app/store/sd.ts index 970f190f816..d40c53a8525 100644 --- a/app/store/sd.ts +++ b/app/store/sd.ts @@ -1,99 +1,122 @@ -import { initDB, useIndexedDB } from "react-indexed-db-hook"; import { StabilityPath, StoreKey } from "@/app/constant"; -import { create, StoreApi } from "zustand"; import { showToast } from "@/app/components/ui-lib"; import { getHeaders } from "@/app/client/api"; +import { createPersistStore } from "@/app/utils/store"; +import { nanoid } from "nanoid"; +import { uploadImage, base64Image2Blob } from "@/app/utils/chat"; -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()((set) => ({ - execCount: 1, - execCountInc: () => set((state) => ({ execCount: state.execCount + 1 })), -})); +export const useSdStore = createPersistStore< + { + currentId: number; + draw: any[]; + }, + { + getNextId: () => number; + sendTask: (data: any, okCall?: Function) => void; + updateDraw: (draw: any) => void; + } +>( + { + currentId: 0, + draw: [], + }, + (set, _get) => { + function get() { + return { + ..._get(), + ...methods, + }; + } -export function sendSdTask(data: any, db: any, inc: any, okCall?: Function) { - db.add(data).then( - (id: number) => { - data = { ...data, id, status: "running" }; - db.update(data); - inc(); - stabilityRequestCall(data, db, inc); - okCall?.(); - }, - (error: any) => { - console.error(error); - showToast(`error: ` + error.message); - }, - ); -} + 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 formData = new FormData(); + for (let paramsKey in data.params) { + formData.append(paramsKey, data.params[paramsKey]); + } + const headers = getHeaders(); + delete headers["Content-Type"]; + fetch(`/api/stability/${StabilityPath.GeneratePath}/${data.model}`, { + method: "POST", + headers: { + ...headers, + Accept: "application/json", + }, + 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; + } + if (resData.finish_reason === "SUCCESS") { + const self = this; + 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(resData), + }); + }); + } 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; + } + }); + }, + }; -export function stabilityRequestCall(data: any, db: any, inc: any) { - const formData = new FormData(); - for (let paramsKey in data.params) { - formData.append(paramsKey, data.params[paramsKey]); - } - const headers = getHeaders(); - delete headers["Content-Type"]; - fetch(`/api/stability/${StabilityPath.GeneratePath}/${data.model}`, { - method: "POST", - headers: { - ...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(); - }); -} + return methods; + }, + { + name: StoreKey.SdList, + version: 1.0, + }, +); diff --git a/app/utils/chat.ts b/app/utils/chat.ts index 991d06b7320..be9908906ce 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -1,3 +1,4 @@ +import { UPLOAD_URL } from "@/app/constant"; import heic2any from "heic2any"; export function compressImage(file: File, maxSize: number): Promise { @@ -52,3 +53,40 @@ export function compressImage(file: File, maxSize: number): Promise { reader.readAsDataURL(file); }); } + +export function base64Image2Blob(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); + return new Blob([byteArray], { type: contentType }); +} + +export function uploadImage(file: Blob): Promise { + const body = new FormData(); + body.append("file", file); + return fetch(UPLOAD_URL, { + method: "post", + body, + mode: "cors", + credentials: "include", + }) + .then((res) => res.json()) + .then((res) => { + console.log("res", res); + if (res?.code == 0 && res?.data) { + return res?.data; + } + throw Error(`upload Error: ${res?.msg}`); + }); +} + +export function removeImage(imageUrl: string) { + return fetch(imageUrl, { + method: "DELETE", + mode: "cors", + credentials: "include", + }); +} diff --git a/package.json b/package.json index 15ae9b69987..ed5edb04330 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "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", diff --git a/public/serviceWorker.js b/public/serviceWorker.js index f5a24b70176..154f2247e9b 100644 --- a/public/serviceWorker.js +++ b/public/serviceWorker.js @@ -1,4 +1,6 @@ const CHATGPT_NEXT_WEB_CACHE = "chatgpt-next-web-cache"; +const CHATGPT_NEXT_WEB_FILE_CACHE = "chatgpt-next-web-file"; +let a="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";let nanoid=(e=21)=>{let t="",r=crypto.getRandomValues(new Uint8Array(e));for(let n=0;n {}); +async function upload(request, url) { + const formData = await request.formData() + const file = formData.getAll('file')[0] + let ext = file.name.split('.').pop() + if (ext === 'blob') { + ext = file.type.split('/').pop() + } + const fileUrl = `${url.origin}/api/cache/${nanoid()}.${ext}` + // console.debug('file', file, fileUrl, request) + const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE) + await cache.put(new Request(fileUrl), new Response(file, { + headers: { + 'content-type': file.type, + 'content-length': file.size, + 'cache-control': 'no-cache', // file already store in disk + 'server': 'ServiceWorker', + } + })) + return Response.json({ code: 0, data: fileUrl }) +} + +async function remove(request, url) { + const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE) + const res = await cache.delete(request.url) + return Response.json({ code: 0 }) +} + +self.addEventListener("fetch", (e) => { + const url = new URL(e.request.url); + if (/^\/api\/cache/.test(url.pathname)) { + if ('GET' == e.request.method) { + e.respondWith(caches.match(e.request)) + } + if ('POST' == e.request.method) { + e.respondWith(upload(e.request, url)) + } + if ('DELETE' == e.request.method) { + e.respondWith(remove(e.request, url)) + } + } +}); diff --git a/yarn.lock b/yarn.lock index af46f1feac1..c323a5c38db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5278,11 +5278,6 @@ 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"