Add search function

This commit is contained in:
2024-05-14 18:51:41 +08:00
parent 8e1e82cf4b
commit 117fce390c
3 changed files with 270 additions and 23 deletions

View File

@@ -65,6 +65,7 @@ export interface ChatStore {
image_gen_key: string; image_gen_key: string;
json_mode: boolean; json_mode: boolean;
logprobs: boolean; logprobs: boolean;
contents_for_index: string[];
} }
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions"; const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
@@ -128,6 +129,7 @@ export const newChatStore = (
json_mode: json_mode, json_mode: json_mode,
tts_format: tts_format, tts_format: tts_format,
logprobs, logprobs,
contents_for_index: [],
}; };
}; };
@@ -159,6 +161,32 @@ export function clearTotalCost() {
localStorage.setItem(STORAGE_NAME_TOTALCOST, `0`); 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() { export function App() {
// init selected index // init selected index
const [selectedChatIndex, setSelectedChatIndex] = useState( const [selectedChatIndex, setSelectedChatIndex] = useState(
@@ -170,30 +198,62 @@ export function App() {
localStorage.setItem(STORAGE_NAME_SELECTED, `${selectedChatIndex}`); localStorage.setItem(STORAGE_NAME_SELECTED, `${selectedChatIndex}`);
}, [selectedChatIndex]); }, [selectedChatIndex]);
const db = openDB<ChatStore>(STORAGE_NAME, 1, { const db = openDB<ChatStore>(STORAGE_NAME, 10, {
upgrade(db) { async upgrade(db, oldVersion, newVersion, transaction) {
const store = db.createObjectStore(STORAGE_NAME, { if (oldVersion < 1) {
autoIncrement: true, const store = db.createObjectStore(STORAGE_NAME, {
}); autoIncrement: true,
});
// copy from localStorage to indexedDB // copy from localStorage to indexedDB
const allChatStoreIndexes: number[] = JSON.parse( const allChatStoreIndexes: number[] = JSON.parse(
localStorage.getItem(STORAGE_NAME_INDEXES) ?? "[]" 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. 🎉"
); );
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 [chatStore, _setChatStore] = useState(newChatStore());
const setChatStore = async (chatStore: ChatStore) => { const setChatStore = async (chatStore: ChatStore) => {
// building field for search
chatStore.contents_for_index = BuildFiledForSearch(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;
@@ -415,6 +478,7 @@ export function App() {
)} )}
</div> </div>
<ChatBOX <ChatBOX
db={db}
chatStore={chatStore} chatStore={chatStore}
setChatStore={setChatStore} setChatStore={setChatStore}
selectedChatIndex={selectedChatIndex} selectedChatIndex={selectedChatIndex}

View File

@@ -32,12 +32,15 @@ import { AddImage } from "./addImage";
import { ListAPIs } from "./listAPIs"; import { ListAPIs } from "./listAPIs";
import { ListToolsTempaltes } from "./listToolsTemplates"; import { ListToolsTempaltes } from "./listToolsTemplates";
import { autoHeight } from "./textarea"; import { autoHeight } from "./textarea";
import Search from "./search";
import { IDBPDatabase } from "idb";
export interface TemplateChatStore extends ChatStore { export interface TemplateChatStore extends ChatStore {
name: string; name: string;
} }
export default function ChatBOX(props: { export default function ChatBOX(props: {
db: Promise<IDBPDatabase<ChatStore>>;
chatStore: ChatStore; chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void; setChatStore: (cs: ChatStore) => void;
selectedChatIndex: number; selectedChatIndex: number;
@@ -56,6 +59,7 @@ export default function ChatBOX(props: {
const [showAddToolMsg, setShowAddToolMsg] = useState(false); const [showAddToolMsg, setShowAddToolMsg] = useState(false);
const [newToolCallID, setNewToolCallID] = useState(""); const [newToolCallID, setNewToolCallID] = useState("");
const [newToolContent, setNewToolContent] = useState(""); const [newToolContent, setNewToolContent] = useState("");
const [showSearch, setShowSearch] = useState(false);
let default_follow = localStorage.getItem("follow"); let default_follow = localStorage.getItem("follow");
if (default_follow === null) { if (default_follow === null) {
default_follow = "true"; default_follow = "true";
@@ -451,10 +455,29 @@ export default function ChatBOX(props: {
setTemplateTools={setTemplateTools} setTemplateTools={setTemplateTools}
/> />
)} )}
{showSearch && (
<Search
setSelectedChatIndex={props.setSelectedChatIndex}
db={props.db}
chatStore={chatStore}
setShow={setShowSearch}
/>
)}
<div <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)} 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> <div>
<button className="underline"> <button className="underline">
{chatStore.systemMessageContent.length > 16 {chatStore.systemMessageContent.length > 16

160
src/search.tsx Normal file
View 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>
);
}