Add search function
This commit is contained in:
108
src/app.tsx
108
src/app.tsx
@@ -65,6 +65,7 @@ export interface ChatStore {
|
||||
image_gen_key: string;
|
||||
json_mode: boolean;
|
||||
logprobs: boolean;
|
||||
contents_for_index: string[];
|
||||
}
|
||||
|
||||
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
|
||||
@@ -128,6 +129,7 @@ export const newChatStore = (
|
||||
json_mode: json_mode,
|
||||
tts_format: tts_format,
|
||||
logprobs,
|
||||
contents_for_index: [],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -159,6 +161,32 @@ export function clearTotalCost() {
|
||||
localStorage.setItem(STORAGE_NAME_TOTALCOST, `0`);
|
||||
}
|
||||
|
||||
export function BuildFiledForSearch(chatStore: ChatStore): string[] {
|
||||
const contents_for_index: string[] = [];
|
||||
|
||||
if (chatStore.systemMessageContent.trim()) {
|
||||
contents_for_index.push(chatStore.systemMessageContent.trim());
|
||||
}
|
||||
|
||||
for (const msg of chatStore.history) {
|
||||
if (typeof msg.content === "string") {
|
||||
contents_for_index.push(msg.content);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const chunk of msg.content) {
|
||||
if (chunk.type === "text") {
|
||||
const text = chunk.text;
|
||||
if (text?.trim()) {
|
||||
contents_for_index.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return contents_for_index;
|
||||
}
|
||||
|
||||
export function App() {
|
||||
// init selected index
|
||||
const [selectedChatIndex, setSelectedChatIndex] = useState(
|
||||
@@ -170,30 +198,62 @@ export function App() {
|
||||
localStorage.setItem(STORAGE_NAME_SELECTED, `${selectedChatIndex}`);
|
||||
}, [selectedChatIndex]);
|
||||
|
||||
const db = openDB<ChatStore>(STORAGE_NAME, 1, {
|
||||
upgrade(db) {
|
||||
const store = db.createObjectStore(STORAGE_NAME, {
|
||||
autoIncrement: true,
|
||||
});
|
||||
const db = openDB<ChatStore>(STORAGE_NAME, 10, {
|
||||
async upgrade(db, oldVersion, newVersion, transaction) {
|
||||
if (oldVersion < 1) {
|
||||
const store = db.createObjectStore(STORAGE_NAME, {
|
||||
autoIncrement: true,
|
||||
});
|
||||
|
||||
// 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);
|
||||
if (keyCount > 0) {
|
||||
alert(
|
||||
"v2.0.0 Update: Imported chat history from localStorage to indexedDB. 🎉"
|
||||
// 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);
|
||||
if (keyCount > 0) {
|
||||
alert(
|
||||
"v2.0.0 Update: Imported chat history from localStorage to indexedDB. 🎉"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 10) {
|
||||
if (
|
||||
transaction
|
||||
.objectStore(STORAGE_NAME)
|
||||
.indexNames.contains("contents_for_index")
|
||||
) {
|
||||
transaction
|
||||
.objectStore(STORAGE_NAME)
|
||||
.deleteIndex("contents_for_index");
|
||||
}
|
||||
transaction.objectStore(STORAGE_NAME).createIndex(
|
||||
"contents_for_index", // name
|
||||
"contents_for_index", // keyPath
|
||||
{
|
||||
multiEntry: true,
|
||||
unique: false,
|
||||
}
|
||||
);
|
||||
|
||||
// iter through all chatStore and update contents_for_index
|
||||
const store = transaction.objectStore(STORAGE_NAME);
|
||||
const allChatStoreIndexes = await store.getAllKeys();
|
||||
for (const i of allChatStoreIndexes) {
|
||||
const chatStore: ChatStore = await store.get(i);
|
||||
|
||||
chatStore.contents_for_index = BuildFiledForSearch(chatStore);
|
||||
await store.put(chatStore, i);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -222,6 +282,9 @@ export function App() {
|
||||
|
||||
const [chatStore, _setChatStore] = useState(newChatStore());
|
||||
const setChatStore = async (chatStore: ChatStore) => {
|
||||
// building field for search
|
||||
chatStore.contents_for_index = BuildFiledForSearch(chatStore);
|
||||
|
||||
console.log("recalculate postBeginIndex");
|
||||
const max = chatStore.maxTokens - chatStore.tokenMargin;
|
||||
let sum = 0;
|
||||
@@ -415,6 +478,7 @@ export function App() {
|
||||
)}
|
||||
</div>
|
||||
<ChatBOX
|
||||
db={db}
|
||||
chatStore={chatStore}
|
||||
setChatStore={setChatStore}
|
||||
selectedChatIndex={selectedChatIndex}
|
||||
|
||||
@@ -32,12 +32,15 @@ import { AddImage } from "./addImage";
|
||||
import { ListAPIs } from "./listAPIs";
|
||||
import { ListToolsTempaltes } from "./listToolsTemplates";
|
||||
import { autoHeight } from "./textarea";
|
||||
import Search from "./search";
|
||||
import { IDBPDatabase } from "idb";
|
||||
|
||||
export interface TemplateChatStore extends ChatStore {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default function ChatBOX(props: {
|
||||
db: Promise<IDBPDatabase<ChatStore>>;
|
||||
chatStore: ChatStore;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
selectedChatIndex: number;
|
||||
@@ -56,6 +59,7 @@ export default function ChatBOX(props: {
|
||||
const [showAddToolMsg, setShowAddToolMsg] = useState(false);
|
||||
const [newToolCallID, setNewToolCallID] = useState("");
|
||||
const [newToolContent, setNewToolContent] = useState("");
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
let default_follow = localStorage.getItem("follow");
|
||||
if (default_follow === null) {
|
||||
default_follow = "true";
|
||||
@@ -451,10 +455,29 @@ export default function ChatBOX(props: {
|
||||
setTemplateTools={setTemplateTools}
|
||||
/>
|
||||
)}
|
||||
{showSearch && (
|
||||
<Search
|
||||
setSelectedChatIndex={props.setSelectedChatIndex}
|
||||
db={props.db}
|
||||
chatStore={chatStore}
|
||||
setShow={setShowSearch}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="cursor-pointer rounded bg-cyan-300 dark:text-white p-1 dark:bg-cyan-800"
|
||||
className="relative cursor-pointer rounded bg-cyan-300 dark:text-white p-1 dark:bg-cyan-800"
|
||||
onClick={() => setShowSettings(true)}
|
||||
>
|
||||
<button
|
||||
className="absolute right-1 bg-gray-300 rounded p-1 m-1"
|
||||
onClick={(event) => {
|
||||
// stop propagation to parent
|
||||
event.stopPropagation();
|
||||
|
||||
setShowSearch(true);
|
||||
}}
|
||||
>
|
||||
🔍
|
||||
</button>
|
||||
<div>
|
||||
<button className="underline">
|
||||
{chatStore.systemMessageContent.length > 16
|
||||
|
||||
160
src/search.tsx
Normal file
160
src/search.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { IDBPDatabase } from "idb";
|
||||
import { ChatStore } from "./app";
|
||||
import { useState } from "react";
|
||||
import { StateUpdater, useRef } from "preact/hooks";
|
||||
|
||||
interface ChatStoreSearchResult {
|
||||
key: IDBValidKey;
|
||||
cs: ChatStore;
|
||||
}
|
||||
|
||||
export default function Search(props: {
|
||||
db: Promise<IDBPDatabase<ChatStore>>;
|
||||
setSelectedChatIndex: StateUpdater<number>;
|
||||
chatStore: ChatStore;
|
||||
setShow: (show: boolean) => void;
|
||||
}) {
|
||||
const [searchResult, setSearchResult] = useState<ChatStoreSearchResult[]>([]);
|
||||
const [searching, setSearching] = useState<boolean>(false);
|
||||
const [searchingNow, setSearchingNow] = useState<number>(0);
|
||||
const [pageIndex, setPageIndex] = useState<number>(0);
|
||||
const searchAbortRef = useRef<AbortController | null>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => props.setShow(false)}
|
||||
className="left-0 top-0 overflow-scroll flex justify-center absolute w-screen h-full bg-black bg-opacity-50 z-10"
|
||||
>
|
||||
<div
|
||||
onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
className="m-2 p-2 bg-white rounded-lg h-fit w-2/3 z-20"
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<span className="m-1 p-1 font-bold">Search</span>
|
||||
<button className="m-1 p-1 bg-cyan-400 rounded">Close</button>
|
||||
</div>
|
||||
<hr />
|
||||
<div>
|
||||
<input
|
||||
className="m-1 p-1 w-full border"
|
||||
type="text"
|
||||
onInput={async (event: any) => {
|
||||
const query = event.target.value.trim();
|
||||
if (!query) {
|
||||
setSearchResult([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// abort previous search
|
||||
if (searchAbortRef.current) {
|
||||
searchAbortRef.current.abort();
|
||||
}
|
||||
|
||||
// Create a new AbortController for the new operation
|
||||
const abortController = new AbortController();
|
||||
searchAbortRef.current = abortController;
|
||||
const signal = abortController.signal;
|
||||
|
||||
setSearching(true);
|
||||
|
||||
const db = await props.db;
|
||||
const resultKeys = await db.getAllKeys("chatgpt-api-web");
|
||||
|
||||
const result: ChatStoreSearchResult[] = [];
|
||||
for (const key of resultKeys) {
|
||||
// abort the operation if the signal is set
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Math.floor(
|
||||
(result.length / resultKeys.length) * 100
|
||||
);
|
||||
if (now !== searchingNow) setSearchingNow(now);
|
||||
|
||||
const value: ChatStore = await db.get("chatgpt-api-web", key);
|
||||
if (value.contents_for_index.join(" ").includes(query)) {
|
||||
result.push({
|
||||
key,
|
||||
cs: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// sort by key desc
|
||||
result.sort((a, b) => {
|
||||
if (a.key < b.key) {
|
||||
return 1;
|
||||
}
|
||||
if (a.key > b.key) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
console.log(result);
|
||||
|
||||
setPageIndex(0);
|
||||
setSearchResult(result);
|
||||
setSearching(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{searching && <div>Searching {searchingNow}%...</div>}
|
||||
{searchResult.length > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
className="m-1 p-1 rounded bg-green-300 disabled:bg-slate-400"
|
||||
disabled={pageIndex === 0}
|
||||
onClick={() => {
|
||||
if (pageIndex === 0) {
|
||||
return;
|
||||
}
|
||||
setPageIndex(pageIndex - 1);
|
||||
}}
|
||||
>
|
||||
Last page
|
||||
</button>
|
||||
<div className="m-1 p-1">
|
||||
Page: {pageIndex + 1} / {Math.floor(searchResult.length / 10) + 1}
|
||||
</div>
|
||||
<button
|
||||
className="m-1 p-1 rounded bg-green-300 disabled:bg-slate-400"
|
||||
disabled={pageIndex === Math.floor(searchResult.length / 10)}
|
||||
onClick={() => {
|
||||
if (pageIndex === Math.floor(searchResult.length / 10)) {
|
||||
return;
|
||||
}
|
||||
setPageIndex(pageIndex + 1);
|
||||
}}
|
||||
>
|
||||
Next page
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{searchResult
|
||||
.slice(pageIndex * 10, (pageIndex + 1) * 10)
|
||||
.map((result) => {
|
||||
return (
|
||||
<div
|
||||
className="flex justify-start p-1 m-1 rounded border bg-slate-300 cursor-pointer"
|
||||
key={result.key}
|
||||
onClick={() => {
|
||||
props.setSelectedChatIndex(parseInt(result.key.toString()));
|
||||
props.setShow(false);
|
||||
}}
|
||||
>
|
||||
<div className="m-1 p-1 font-bold">{result.key}</div>
|
||||
<div className="m-1 p-1">
|
||||
{result.cs.contents_for_index.join(" ").slice(0, 390)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user