v2.0.0 migrate to indexedDB

This commit is contained in:
2023-11-08 13:54:51 +08:00
parent ed32f836ba
commit 053495254b
5 changed files with 119 additions and 115 deletions

View File

@@ -12,6 +12,7 @@
"@types/ungap__structured-clone": "^0.3.1", "@types/ungap__structured-clone": "^0.3.1",
"@ungap/structured-clone": "^1.2.0", "@ungap/structured-clone": "^1.2.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"idb": "^7.1.1",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"preact": "^10.18.1", "preact": "^10.18.1",
"preact-markdown": "^2.1.0", "preact-markdown": "^2.1.0",

View File

@@ -1,3 +1,3 @@
const CHATGPT_API_WEB_VERSION = "v1.6.0"; const CHATGPT_API_WEB_VERSION = "v2.0.0";
export default CHATGPT_API_WEB_VERSION; export default CHATGPT_API_WEB_VERSION;

View File

@@ -1,3 +1,4 @@
import { IDBPDatabase, openDB } from "idb";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import "./global.css"; import "./global.css";
@@ -51,7 +52,7 @@ export interface ChatStore {
} }
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions"; const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
const newChatStore = ( export const newChatStore = (
apiKey = "", apiKey = "",
systemMessageContent = "", systemMessageContent = "",
apiEndpoint = _defaultAPIEndpoint, apiEndpoint = _defaultAPIEndpoint,
@@ -97,7 +98,7 @@ const newChatStore = (
}; };
}; };
const STORAGE_NAME = "chatgpt-api-web"; export const STORAGE_NAME = "chatgpt-api-web";
const STORAGE_NAME_SELECTED = `${STORAGE_NAME}-selected`; const STORAGE_NAME_SELECTED = `${STORAGE_NAME}-selected`;
const STORAGE_NAME_INDEXES = `${STORAGE_NAME}-indexes`; const STORAGE_NAME_INDEXES = `${STORAGE_NAME}-indexes`;
const STORAGE_NAME_TOTALCOST = `${STORAGE_NAME}-totalcost`; const STORAGE_NAME_TOTALCOST = `${STORAGE_NAME}-totalcost`;
@@ -122,36 +123,45 @@ export function clearTotalCost() {
} }
export function App() { export function App() {
// init indexes
const initAllChatStoreIndexes: number[] = JSON.parse(
localStorage.getItem(STORAGE_NAME_INDEXES) ?? "[0]"
);
const [allChatStoreIndexes, setAllChatStoreIndexes] = useState(
initAllChatStoreIndexes
);
useEffect(() => {
if (allChatStoreIndexes.length === 0) allChatStoreIndexes.push(0);
console.log("saved all chat store indexes", allChatStoreIndexes);
localStorage.setItem(
STORAGE_NAME_INDEXES,
JSON.stringify(allChatStoreIndexes)
);
}, [allChatStoreIndexes]);
// init selected index // init selected index
const [selectedChatIndex, setSelectedChatIndex] = useState( const [selectedChatIndex, setSelectedChatIndex] = useState(
parseInt(localStorage.getItem(STORAGE_NAME_SELECTED) ?? "0") parseInt(localStorage.getItem(STORAGE_NAME_SELECTED) ?? "1")
); );
console.log("selectedChatIndex", selectedChatIndex);
useEffect(() => { useEffect(() => {
console.log("set selected chat index", selectedChatIndex); console.log("set selected chat index", selectedChatIndex);
localStorage.setItem(STORAGE_NAME_SELECTED, `${selectedChatIndex}`); localStorage.setItem(STORAGE_NAME_SELECTED, `${selectedChatIndex}`);
}, [selectedChatIndex]); }, [selectedChatIndex]);
const getChatStoreByIndex = (index: number): ChatStore => { const db = openDB<ChatStore>(STORAGE_NAME, 1, {
const key = `${STORAGE_NAME}-${index}`; upgrade(db) {
const val = localStorage.getItem(key); const store = db.createObjectStore(STORAGE_NAME, {
if (val === null) return newChatStore(); autoIncrement: true,
const ret = JSON.parse(val) as ChatStore; });
// copy from localStorage to indexedDB
const allChatStoreIndexes: number[] = JSON.parse(
localStorage.getItem(STORAGE_NAME_INDEXES) ?? "[]"
);
let keyCount = 0;
for (const i of allChatStoreIndexes) {
console.log("importing chatStore from localStorage", i);
const key = `${STORAGE_NAME}-${i}`;
const val = localStorage.getItem(key);
if (val === null) continue;
store.add(JSON.parse(val));
keyCount += 1;
}
setSelectedChatIndex(keyCount);
alert(
"v2.0.0 Update: Imported chat history from localStorage to indexedDB. 🎉"
);
},
});
const getChatStoreByIndex = async (index: number): Promise<ChatStore> => {
const ret: ChatStore = await (await db).get(STORAGE_NAME, index);
if (ret === null || ret === undefined) return newChatStore();
// handle read from old version chatstore // handle read from old version chatstore
if (ret.model === undefined) ret.model = "gpt-3.5-turbo"; if (ret.model === undefined) ret.model = "gpt-3.5-turbo";
if (ret.responseModelName === undefined) ret.responseModelName = ""; if (ret.responseModelName === undefined) ret.responseModelName = "";
@@ -168,16 +178,8 @@ export function App() {
return ret; return ret;
}; };
const [chatStore, _setChatStore] = useState( const [chatStore, _setChatStore] = useState(newChatStore());
getChatStoreByIndex(selectedChatIndex) const setChatStore = async (chatStore: ChatStore) => {
);
const setChatStore = (chatStore: ChatStore) => {
console.log("saved chat", selectedChatIndex, chatStore);
localStorage.setItem(
`${STORAGE_NAME}-${selectedChatIndex}`,
JSON.stringify(chatStore)
);
console.log("recalculate postBeginIndex"); console.log("recalculate postBeginIndex");
const max = chatStore.maxTokens - chatStore.tokenMargin; const max = chatStore.maxTokens - chatStore.tokenMargin;
let sum = 0; let sum = 0;
@@ -205,59 +207,75 @@ export function App() {
chatStore.totalTokens += msg.token; chatStore.totalTokens += msg.token;
} }
console.log("saved chat", selectedChatIndex, chatStore);
(await db).put(STORAGE_NAME, chatStore, selectedChatIndex);
_setChatStore(chatStore); _setChatStore(chatStore);
}; };
useEffect(() => { useEffect(() => {
_setChatStore(getChatStoreByIndex(selectedChatIndex)); const run = async () => {
_setChatStore(await getChatStoreByIndex(selectedChatIndex));
};
run();
}, [selectedChatIndex]); }, [selectedChatIndex]);
const handleNewChatStore = () => { // all chat store indexes
const max = Math.max(...allChatStoreIndexes); const [allChatStoreIndexes, setAllChatStoreIndexes] = useState<IDBValidKey>(
const next = max + 1; []
console.log("save next chat", next); );
localStorage.setItem(
`${STORAGE_NAME}-${next}`, const handleNewChatStore = async () => {
JSON.stringify( const newKey = await (
newChatStore( await db
chatStore.apiKey, ).add(
chatStore.systemMessageContent, STORAGE_NAME,
chatStore.apiEndpoint, newChatStore(
chatStore.streamMode, chatStore.apiKey,
chatStore.model, chatStore.systemMessageContent,
chatStore.temperature, chatStore.apiEndpoint,
!!chatStore.develop_mode, chatStore.streamMode,
chatStore.whisper_api, chatStore.model,
chatStore.whisper_key, chatStore.temperature,
chatStore.tts_api, !!chatStore.develop_mode,
chatStore.tts_key, chatStore.whisper_api,
chatStore.tts_speed, chatStore.whisper_key,
chatStore.tts_speed_enabled chatStore.tts_api,
) chatStore.tts_key,
chatStore.tts_speed,
chatStore.tts_speed_enabled
) )
); );
allChatStoreIndexes.push(next); setSelectedChatIndex(newKey as number);
setAllChatStoreIndexes([...allChatStoreIndexes]); setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
setSelectedChatIndex(next);
}; };
// if there are any params in URL, create a new chatStore // if there are any params in URL, create a new chatStore
useEffect(() => { useEffect(() => {
const api = getDefaultParams("api", ""); const run = async () => {
const key = getDefaultParams("key", ""); const api = getDefaultParams("api", "");
const sys = getDefaultParams("sys", ""); const key = getDefaultParams("key", "");
const mode = getDefaultParams("mode", ""); const sys = getDefaultParams("sys", "");
const model = getDefaultParams("model", ""); const mode = getDefaultParams("mode", "");
// only create new chatStore if the params in URL are NOT const model = getDefaultParams("model", "");
// equal to the current selected chatStore // only create new chatStore if the params in URL are NOT
if ( // equal to the current selected chatStore
(api && api !== chatStore.apiEndpoint) || if (
(key && key !== chatStore.apiKey) || (api && api !== chatStore.apiEndpoint) ||
(sys && sys !== chatStore.systemMessageContent) || (key && key !== chatStore.apiKey) ||
(mode && mode !== (chatStore.streamMode ? "stream" : "fetch")) || (sys && sys !== chatStore.systemMessageContent) ||
(model && model !== chatStore.model) (mode && mode !== (chatStore.streamMode ? "stream" : "fetch")) ||
) { (model && model !== chatStore.model)
handleNewChatStore(); ) {
} handleNewChatStore();
}
await db;
const allidx = await (await db).getAllKeys(STORAGE_NAME);
if (allidx.length === 0) {
handleNewChatStore();
}
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
};
run();
}, []); }, []);
return ( return (
@@ -271,7 +289,7 @@ export function App() {
{Tr("NEW")} {Tr("NEW")}
</button> </button>
<ul> <ul>
{allChatStoreIndexes {(allChatStoreIndexes as number[])
.slice() .slice()
.reverse() .reverse()
.map((i) => { .map((i) => {
@@ -295,43 +313,26 @@ export function App() {
</div> </div>
<button <button
className="rounded bg-rose-400 p-1 my-1 w-full" className="rounded bg-rose-400 p-1 my-1 w-full"
onClick={() => { onClick={async () => {
if (!confirm("Are you sure you want to delete this chat history?")) if (!confirm("Are you sure you want to delete this chat history?"))
return; return;
console.log("remove item", `${STORAGE_NAME}-${selectedChatIndex}`); console.log("remove item", `${STORAGE_NAME}-${selectedChatIndex}`);
localStorage.removeItem(`${STORAGE_NAME}-${selectedChatIndex}`); (await db).delete(STORAGE_NAME, selectedChatIndex);
const newAllChatStoreIndexes = [ const newAllChatStoreIndexes = await (
...allChatStoreIndexes.filter((v) => v !== selectedChatIndex), await db
]; ).getAllKeys(STORAGE_NAME);
if (newAllChatStoreIndexes.length === 0) { if (newAllChatStoreIndexes.length === 0) {
newAllChatStoreIndexes.push(0); handleNewChatStore();
setChatStore( return;
newChatStore(
chatStore.apiKey,
chatStore.systemMessageContent,
chatStore.apiEndpoint,
chatStore.streamMode,
chatStore.model,
chatStore.temperature,
!!chatStore.develop_mode,
chatStore.whisper_api,
chatStore.whisper_key,
chatStore.tts_api,
chatStore.tts_key,
chatStore.tts_speed,
chatStore.tts_speed_enabled
)
);
} }
// find nex selected chat index // find nex selected chat index
const next = const next =
newAllChatStoreIndexes[newAllChatStoreIndexes.length - 1]; newAllChatStoreIndexes[newAllChatStoreIndexes.length - 1];
console.log("next is", next); console.log("next is", next);
setSelectedChatIndex(next); setSelectedChatIndex(next as number);
setAllChatStoreIndexes(newAllChatStoreIndexes);
setAllChatStoreIndexes([...newAllChatStoreIndexes]);
}} }}
> >
{Tr("DEL")} {Tr("DEL")}
@@ -339,20 +340,17 @@ export function App() {
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<button <button
className="rounded bg-rose-800 p-1 my-1 w-full text-white" className="rounded bg-rose-800 p-1 my-1 w-full text-white"
onClick={() => { onClick={async () => {
if ( if (
!confirm( !confirm(
"Are you sure you want to delete **ALL** chat history?" "Are you sure you want to delete **ALL** chat history?"
) )
) )
return; return;
for (const i of allChatStoreIndexes) {
console.log("remove item", `${STORAGE_NAME}-${i}`); await (await db).clear(STORAGE_NAME);
localStorage.removeItem(`${STORAGE_NAME}-${i}`);
}
setAllChatStoreIndexes([]); setAllChatStoreIndexes([]);
setSelectedChatIndex(0); setSelectedChatIndex(1);
// reload page
window.location.reload(); window.location.reload();
}} }}
> >

View File

@@ -324,7 +324,7 @@ export default function ChatBOX(props: {
chatStore.history.filter((msg) => !msg.example).length == 0 || chatStore.history.filter((msg) => !msg.example).length == 0 ||
!chatStore.apiEndpoint || !chatStore.apiEndpoint ||
!chatStore.apiKey) && ( !chatStore.apiKey) && (
<p className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black"> <div className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black">
<h2>{Tr("Saved API templates")}</h2> <h2>{Tr("Saved API templates")}</h2>
<hr className="my-2" /> <hr className="my-2" />
<div className="flex flex-wrap"> <div className="flex flex-wrap">
@@ -376,10 +376,10 @@ export default function ChatBOX(props: {
</div> </div>
))} ))}
</div> </div>
</p> </div>
)} )}
{chatStore.history.filter((msg) => !msg.example).length == 0 && ( {chatStore.history.filter((msg) => !msg.example).length == 0 && (
<p className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black"> <div className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black">
<h2> <h2>
<span>{Tr("Saved prompt templates")}</span> <span>{Tr("Saved prompt templates")}</span>
<button <button
@@ -448,7 +448,7 @@ export default function ChatBOX(props: {
</div> </div>
))} ))}
</div> </div>
</p> </div>
)} )}
{chatStore.history.length === 0 && ( {chatStore.history.length === 0 && (
<p className="break-all opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black"> <p className="break-all opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black">

View File

@@ -773,6 +773,11 @@ hasown@^2.0.0:
dependencies: dependencies:
function-bind "^1.1.2" function-bind "^1.1.2"
idb@^7.1.1:
version "7.1.1"
resolved "https://registry.npmmirror.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b"
integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==
inflight@^1.0.4: inflight@^1.0.4:
version "1.0.6" version "1.0.6"
resolved "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" resolved "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"