Files
chatgpt-api-web/src/search.tsx

202 lines
6.6 KiB
TypeScript

import { IDBPDatabase } from "idb";
import { StateUpdater, useRef, useState, Dispatch } from "preact/hooks";
import { ChatStore } from "@/types/chatstore";
import { MessageDetail } from "./chatgpt";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { Input } from "./components/ui/input";
interface ChatStoreSearchResult {
key: IDBValidKey;
cs: ChatStore;
query: string;
preview: string;
}
export default function Search(props: {
db: Promise<IDBPDatabase<ChatStore>>;
setSelectedChatIndex: Dispatch<StateUpdater<number>>;
chatStore: ChatStore;
show: boolean;
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 (
<Dialog open={props.show} onOpenChange={props.setShow}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Search</DialogTitle>
<DialogDescription>Search messages by content.</DialogDescription>
</DialogHeader>
<div>
<Input
autoFocus
type="text"
placeholder="Type Something..."
onInput={async (event: any) => {
const query = event.target.value.trim().toLowerCase();
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);
let preview: string = "";
for (const msg of value.history) {
const contentType = typeof msg.content;
if (contentType === "string") {
if (!msg.content.includes(query)) continue;
const beginIndex = msg.content.indexOf(query);
preview = msg.content.slice(
Math.max(0, beginIndex - 100),
Math.min(msg.content.length, beginIndex + 239)
) as string;
break;
} else if (contentType === "object") {
const details = msg.content as MessageDetail[];
for (const detail of details) {
if (detail.type !== "text") continue;
if (!detail.text?.includes(query)) continue;
const beginIndex = detail.text.indexOf(query);
preview = detail.text.slice(
Math.max(0, beginIndex - 100),
Math.min(detail.text.length, beginIndex + 239)
) as string;
break;
}
}
}
if (preview === "") continue;
result.push({
key,
cs: value,
query: query,
preview: preview,
});
}
// 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>}
<div>
{searchResult
.slice(pageIndex * 10, (pageIndex + 1) * 10)
.map((result: ChatStoreSearchResult) => {
return (
<div
className="flex justify-start p-1 m-1 rounded border bg-base-200 cursor-pointer"
key={result.key as number}
onClick={() => {
props.setSelectedChatIndex(parseInt(result.key.toString()));
props.setShow(false);
}}
>
<div className="m-1 p-1 font-bold">{result.key as number}</div>
<div className="m-1 p-1">{result.preview}</div>
</div>
);
})}
</div>
{searchResult.length > 0 && (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => {
if (pageIndex === 0) return;
setPageIndex(pageIndex - 1);
}}
// disabled={pageIndex === 0}
/>
</PaginationItem>
<PaginationItem>
{pageIndex + 1} of {Math.floor(searchResult.length / 10) + 1}
</PaginationItem>
<PaginationItem>
<PaginationNext
onClick={() => {
if (pageIndex === Math.floor(searchResult.length / 10))
return;
setPageIndex(pageIndex + 1);
}}
// disabled={pageIndex === Math.floor(searchResult.length / 10)}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</DialogContent>
</Dialog>
);
}