v2.0.0 migrate to indexedDB
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
218
src/app.tsx
218
src/app.tsx
@@ -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();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user