From bab3e0bc9bf72f41f905add1b9140960d345d3db Mon Sep 17 00:00:00 2001 From: lloydzhou Date: Tue, 16 Jul 2024 01:19:40 +0800 Subject: [PATCH] using CacheStorage to store image #5013 --- app/components/home.tsx | 1 - app/components/sd-panel.tsx | 7 +-- app/components/sd.tsx | 19 ++++---- app/constant.ts | 2 + app/store/sd.ts | 51 ++++++++++++-------- app/utils/chat.ts | 38 +++++++++++++++ app/utils/file.tsx | 95 ------------------------------------- package.json | 1 - public/serviceWorker.js | 37 ++++++++++++++- yarn.lock | 5 -- 10 files changed, 115 insertions(+), 141 deletions(-) delete mode 100644 app/utils/file.tsx diff --git a/app/components/home.tsx b/app/components/home.tsx index 875f37e54..aacd23264 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 36842c742..c6b28f221 100644 --- a/app/components/sd-panel.tsx +++ b/app/components/sd-panel.tsx @@ -4,12 +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 { useSdStore } from "@/app/store/sd"; -import { FileDbInit } from "@/app/utils/file"; - -FileDbInit(); const sdCommonParams = (model: string, data: any) => { return [ @@ -287,7 +283,6 @@ export function SdPanel() { setCurrentModel(model); setParams(getModelParamBasicData(model.params({}), params)); }; - const sdListDb = useIndexedDB(StoreKey.SdList); const sdStore = useSdStore(); const handleSubmit = () => { const columns = currentModel.params(params); @@ -310,7 +305,7 @@ export function SdPanel() { created_at: new Date().toLocaleString(), img_data: "", }; - sdStore.sendTask(data, sdListDb, () => { + sdStore.sendTask(data, () => { setParams(getModelParamBasicData(columns, params, true)); }); }; diff --git a/app/components/sd.tsx b/app/components/sd.tsx index dfbe640d6..e06ee1683 100644 --- a/app/components/sd.tsx +++ b/app/components/sd.tsx @@ -30,8 +30,7 @@ import { showImageModal, showModal, } from "@/app/components/ui-lib"; -import { func } from "prop-types"; -import { useFileDB, IndexDBImage } from "@/app/utils/file"; +import { removeImage } from "@/app/utils/chat"; function getSdTaskStatus(item: any) { let s: string; @@ -90,7 +89,6 @@ export function Sd() { const showMaxIcon = !isMobileScreen && !clientConfig?.isApp; const config = useAppConfig(); const scrollRef = useRef(null); - const fileDb = useFileDB(); const sdStore = useSdStore(); const [sdImages, setSdImages] = useState(sdStore.draw); @@ -147,14 +145,13 @@ export function Sd() { className={styles["sd-img-item"]} > {item.status === "success" ? ( - { + onClick={(e) => showImageModal( - data, + item.img_data, true, isMobileScreen ? { width: "100%", height: "fit-content" } @@ -162,8 +159,8 @@ export function Sd() { isMobileScreen ? { width: "100%", height: "fit-content" } : { width: "100%", height: "100%" }, - ); - }} + ) + } /> ) : item.status === "error" ? (
@@ -247,7 +244,7 @@ export function Sd() { created_at: new Date().toLocaleString(), img_data: "", }; - sdStore.sendTask(reqData, fileDb); + sdStore.sendTask(reqData); }} /> { if (await showConfirm(Locale.Sd.Danger.Delete)) { // remove img_data + remove item in list - fileDb.deleteRecord(item.id).then( + removeImage(item.img_data).finally( () => { sdStore.draw = sdImages.filter( (i: any) => i.id !== item.id, diff --git a/app/constant.ts b/app/constant.ts index 509b90ee0..d50db15f9 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -23,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", diff --git a/app/store/sd.ts b/app/store/sd.ts index 842763f9f..d40c53a85 100644 --- a/app/store/sd.ts +++ b/app/store/sd.ts @@ -3,7 +3,7 @@ import { showToast } from "@/app/components/ui-lib"; import { getHeaders } from "@/app/client/api"; import { createPersistStore } from "@/app/utils/store"; import { nanoid } from "nanoid"; -import { saveFileData, base64Image2Blob } from "@/app/utils/file"; +import { uploadImage, base64Image2Blob } from "@/app/utils/chat"; export const useSdStore = createPersistStore< { @@ -12,7 +12,7 @@ export const useSdStore = createPersistStore< }, { getNextId: () => number; - sendTask: (data: any, db: any, okCall?: Function) => void; + sendTask: (data: any, okCall?: Function) => void; updateDraw: (draw: any) => void; } >( @@ -34,15 +34,14 @@ export const useSdStore = createPersistStore< set({ currentId: id }); return id; }, - sendTask(data: any, db: any, okCall?: Function) { + sendTask(data: any, okCall?: Function) { data = { ...data, id: nanoid(), status: "running" }; set({ draw: [data, ..._get().draw] }); - // db.update(data); this.getNextId(); - this.stabilityRequestCall(data, db); + this.stabilityRequestCall(data); okCall?.(); }, - stabilityRequestCall(data: any, db: any) { + stabilityRequestCall(data: any) { const formData = new FormData(); for (let paramsKey in data.params) { formData.append(paramsKey, data.params[paramsKey]); @@ -69,18 +68,26 @@ export const useSdStore = createPersistStore< return; } if (resData.finish_reason === "SUCCESS") { - this.updateDraw({ - ...data, - status: "success", - // save blob to indexeddb instead of base64 image string - img_data: saveFileData( - db, - data.id, - base64Image2Blob(resData.image, "image/png"), - ), - }); + 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 { - this.updateDraw({ + self.updateDraw({ ...data, status: "error", error: JSON.stringify(resData), @@ -94,10 +101,12 @@ export const useSdStore = createPersistStore< this.getNextId(); }); }, - updateDraw(draw: any) { - _get().draw.some((item, index) => { - if (item.id === draw.id) { - _get().draw[index] = draw; + updateDraw(_draw: any) { + const draw = _get().draw || []; + draw.some((item, index) => { + if (item.id === _draw.id) { + draw[index] = _draw; + set(() => ({ draw })); return true; } }); diff --git a/app/utils/chat.ts b/app/utils/chat.ts index 991d06b73..55127f50e 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: File): 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/app/utils/file.tsx b/app/utils/file.tsx deleted file mode 100644 index b9b697f56..000000000 --- a/app/utils/file.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useState, useMemo, useEffect } from "react"; -import { initDB } from "react-indexed-db-hook"; -import { StoreKey } from "@/app/constant"; -import { useIndexedDB } from "react-indexed-db-hook"; - -export const FileDbConfig = { - name: "@chatgpt-next-web/file", - version: 1, - objectStoresMeta: [ - { - store: StoreKey.File, - storeConfig: { keyPath: "id", autoIncrement: true }, - storeSchema: [ - { name: "data", keypath: "data", options: { unique: false } }, - { - name: "created_at", - keypath: "created_at", - options: { unique: false }, - }, - ], - }, - ], -}; - -export function FileDbInit() { - if (typeof window !== "undefined") { - initDB(FileDbConfig); - } -} - -export function useFileDB() { - return useIndexedDB(StoreKey.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 saveFileData(db: any, fileId: string, data: Blob | string) { - // save file content and return url start with `indexeddb://` - db.add({ id: fileId, data }); - return `indexeddb://${StoreKey.File}@${fileId}`; -} - -export async function getFileData( - db: any, - fileId: string, - contentType = "image/png", -) { - const { data } = await db.getByID(fileId); - if (typeof data == "object") { - return URL.createObjectURL(data); - } - return `data:${contentType};base64,${data}`; -} - -export function IndexDBImage({ - src, - alt, - onClick, - db, - className, -}: { - src: string; - alt: string; - onClick: any; - db: any; - className: string; -}) { - const [data, setData] = useState(src); - const imgId = useMemo( - () => src.replace("indexeddb://", "").split("@").pop(), - [src], - ); - useEffect(() => { - getFileData(db, imgId as string) - .then((data) => setData(data)) - .catch((e) => setData(src)); - }, [src, imgId]); - - return ( - {alt} onClick(data, e)} - /> - ); -} diff --git a/package.json b/package.json index 15ae9b699..ed5edb043 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 f5a24b701..b9b80c319 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) + const cache = await caches.open(CHATGPT_NEXT_WEB_FILE_CACHE) + await cache.put(new Request(fileUrl), new Response(file)) + 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 af46f1fea..c323a5c38 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"