202 lines
6.6 KiB
TypeScript
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>
|
|
);
|
|
}
|