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;
|
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}
|
||||||
|
|||||||
@@ -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
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