2024-09-03 01:46:43 +00:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
ResizableHandle,
|
|
|
|
|
ResizablePanel,
|
|
|
|
|
ResizablePanelGroup,
|
|
|
|
|
} from "@/components/ui/resizable";
|
|
|
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
|
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
2024-09-26 03:12:20 +00:00
|
|
|
import { createPathWithQueryParams } from "@/lib/utils";
|
2024-09-03 01:46:43 +00:00
|
|
|
import { SymbolIcon } from "@radix-ui/react-icons";
|
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
|
|
|
import Image from "next/image";
|
2024-09-10 19:59:42 +00:00
|
|
|
import { useRouter } from "next/navigation";
|
2024-09-26 06:31:51 +00:00
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
2024-09-03 01:46:43 +00:00
|
|
|
import logoDark from "../../../public/sb_logo_dark.png";
|
|
|
|
|
import logoLight from "../../../public/sb_logo_light.png";
|
2024-09-26 03:12:20 +00:00
|
|
|
import { search } from "../api/(client)/client";
|
2024-09-03 01:46:43 +00:00
|
|
|
import { SearchBar } from "../searchBar";
|
|
|
|
|
import { SettingsDropdown } from "../settingsDropdown";
|
2024-09-17 04:37:34 +00:00
|
|
|
import useCaptureEvent from "@/hooks/useCaptureEvent";
|
2024-09-26 03:12:20 +00:00
|
|
|
import { CodePreviewPanel } from "./components/codePreviewPanel";
|
|
|
|
|
import { SearchResultsPanel } from "./components/searchResultsPanel";
|
2024-09-26 18:19:04 +00:00
|
|
|
import { SearchQueryParams, SearchResultFile } from "@/lib/types";
|
2024-09-26 06:31:51 +00:00
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
|
|
|
import { Scrollbar } from "@radix-ui/react-scroll-area";
|
|
|
|
|
|
|
|
|
|
const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 200;
|
|
|
|
|
|
2024-09-03 01:46:43 +00:00
|
|
|
export default function SearchPage() {
|
|
|
|
|
const router = useRouter();
|
2024-09-26 06:31:51 +00:00
|
|
|
const searchQuery = useNonEmptyQueryParam(SearchQueryParams.query) ?? "";
|
|
|
|
|
const _maxMatchDisplayCount = parseInt(useNonEmptyQueryParam(SearchQueryParams.maxMatchDisplayCount) ?? `${DEFAULT_MAX_MATCH_DISPLAY_COUNT}`);
|
|
|
|
|
const maxMatchDisplayCount = isNaN(_maxMatchDisplayCount) ? DEFAULT_MAX_MATCH_DISPLAY_COUNT : _maxMatchDisplayCount;
|
2024-09-03 01:46:43 +00:00
|
|
|
|
|
|
|
|
const [selectedMatchIndex, setSelectedMatchIndex] = useState(0);
|
2024-09-10 06:16:41 +00:00
|
|
|
const [selectedFile, setSelectedFile] = useState<SearchResultFile | undefined>(undefined);
|
2024-09-03 01:46:43 +00:00
|
|
|
|
2024-09-17 04:37:34 +00:00
|
|
|
const captureEvent = useCaptureEvent();
|
|
|
|
|
|
2024-09-03 01:46:43 +00:00
|
|
|
const { data: searchResponse, isLoading } = useQuery({
|
2024-09-26 06:31:51 +00:00
|
|
|
queryKey: ["search", searchQuery, maxMatchDisplayCount],
|
2024-09-10 06:16:41 +00:00
|
|
|
queryFn: () => search({
|
|
|
|
|
query: searchQuery,
|
2024-09-26 06:31:51 +00:00
|
|
|
maxMatchDisplayCount,
|
2024-09-10 06:16:41 +00:00
|
|
|
}),
|
2024-09-03 01:46:43 +00:00
|
|
|
enabled: searchQuery.length > 0,
|
2024-09-17 04:37:34 +00:00
|
|
|
refetchOnWindowFocus: false,
|
2024-09-03 01:46:43 +00:00
|
|
|
});
|
|
|
|
|
|
2024-09-17 04:37:34 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!searchResponse) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fileLanguages = searchResponse.Result.Files?.map(file => file.Language) || [];
|
|
|
|
|
|
|
|
|
|
captureEvent("search_finished", {
|
|
|
|
|
contentBytesLoaded: searchResponse.Result.ContentBytesLoaded,
|
|
|
|
|
indexBytesLoaded: searchResponse.Result.IndexBytesLoaded,
|
|
|
|
|
crashes: searchResponse.Result.Crashes,
|
|
|
|
|
durationMs: searchResponse.Result.Duration / 1000000,
|
|
|
|
|
fileCount: searchResponse.Result.FileCount,
|
|
|
|
|
shardFilesConsidered: searchResponse.Result.ShardFilesConsidered,
|
|
|
|
|
filesConsidered: searchResponse.Result.FilesConsidered,
|
|
|
|
|
filesLoaded: searchResponse.Result.FilesLoaded,
|
|
|
|
|
filesSkipped: searchResponse.Result.FilesSkipped,
|
|
|
|
|
shardsScanned: searchResponse.Result.ShardsScanned,
|
|
|
|
|
shardsSkipped: searchResponse.Result.ShardsSkipped,
|
|
|
|
|
shardsSkippedFilter: searchResponse.Result.ShardsSkippedFilter,
|
|
|
|
|
matchCount: searchResponse.Result.MatchCount,
|
|
|
|
|
ngramMatches: searchResponse.Result.NgramMatches,
|
|
|
|
|
ngramLookups: searchResponse.Result.NgramLookups,
|
|
|
|
|
wait: searchResponse.Result.Wait,
|
|
|
|
|
matchTreeConstruction: searchResponse.Result.MatchTreeConstruction,
|
|
|
|
|
matchTreeSearch: searchResponse.Result.MatchTreeSearch,
|
|
|
|
|
regexpsConsidered: searchResponse.Result.RegexpsConsidered,
|
|
|
|
|
flushReason: searchResponse.Result.FlushReason,
|
|
|
|
|
fileLanguages,
|
|
|
|
|
});
|
|
|
|
|
}, [captureEvent, searchResponse]);
|
|
|
|
|
|
2024-09-10 06:16:41 +00:00
|
|
|
const { fileMatches, searchDurationMs } = useMemo((): { fileMatches: SearchResultFile[], searchDurationMs: number } => {
|
2024-09-03 01:46:43 +00:00
|
|
|
if (!searchResponse) {
|
|
|
|
|
return {
|
|
|
|
|
fileMatches: [],
|
|
|
|
|
searchDurationMs: 0,
|
|
|
|
|
};
|
|
|
|
|
}
|
2024-09-10 06:16:41 +00:00
|
|
|
|
2024-09-03 01:46:43 +00:00
|
|
|
return {
|
2024-09-10 06:16:41 +00:00
|
|
|
fileMatches: searchResponse.Result.Files ?? [],
|
|
|
|
|
searchDurationMs: Math.round(searchResponse.Result.Duration / 1000000),
|
2024-09-03 01:46:43 +00:00
|
|
|
}
|
|
|
|
|
}, [searchResponse]);
|
|
|
|
|
|
2024-09-10 19:59:42 +00:00
|
|
|
const isMoreResultsButtonVisible = useMemo(() => {
|
2024-09-26 06:31:51 +00:00
|
|
|
return searchResponse && searchResponse.Result.MatchCount > maxMatchDisplayCount;
|
|
|
|
|
}, [searchResponse, maxMatchDisplayCount]);
|
|
|
|
|
|
|
|
|
|
const numMatches = useMemo(() => {
|
|
|
|
|
// Accumualtes the number of matches across all files
|
|
|
|
|
return searchResponse?.Result.Files?.reduce(
|
|
|
|
|
(acc, file) =>
|
|
|
|
|
acc + file.ChunkMatches.reduce(
|
|
|
|
|
(acc, chunk) => acc + chunk.Ranges.length,
|
|
|
|
|
0,
|
|
|
|
|
),
|
|
|
|
|
0,
|
|
|
|
|
) ?? 0;
|
|
|
|
|
}, [searchResponse]);
|
|
|
|
|
|
|
|
|
|
const onLoadMoreResults = useCallback(() => {
|
|
|
|
|
const url = createPathWithQueryParams('/search',
|
|
|
|
|
[SearchQueryParams.query, searchQuery],
|
|
|
|
|
[SearchQueryParams.maxMatchDisplayCount, `${maxMatchDisplayCount * 2}`],
|
|
|
|
|
)
|
|
|
|
|
router.push(url);
|
|
|
|
|
}, [maxMatchDisplayCount, router, searchQuery]);
|
2024-09-03 01:46:43 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-screen overflow-clip">
|
|
|
|
|
{/* TopBar */}
|
|
|
|
|
<div className="sticky top-0 left-0 right-0 z-10">
|
|
|
|
|
<div className="flex flex-row justify-between items-center py-1.5 px-3 gap-4">
|
|
|
|
|
<div className="grow flex flex-row gap-4 items-center">
|
|
|
|
|
<div
|
2024-09-24 05:30:58 +00:00
|
|
|
className="shrink-0 cursor-pointer"
|
2024-09-03 01:46:43 +00:00
|
|
|
onClick={() => {
|
|
|
|
|
router.push("/");
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Image
|
|
|
|
|
src={logoDark}
|
|
|
|
|
className="h-4 w-auto hidden dark:block"
|
|
|
|
|
alt={"Sourcebot logo"}
|
|
|
|
|
/>
|
|
|
|
|
<Image
|
|
|
|
|
src={logoLight}
|
|
|
|
|
className="h-4 w-auto block dark:hidden"
|
|
|
|
|
alt={"Sourcebot logo"}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<SearchBar
|
|
|
|
|
size="sm"
|
|
|
|
|
defaultQuery={searchQuery}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<SettingsDropdown
|
|
|
|
|
menuButtonClassName="w-8 h-8"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<Separator />
|
2024-09-26 06:31:51 +00:00
|
|
|
<div className="bg-accent py-1 px-2 flex flex-row items-center gap-4">
|
|
|
|
|
{
|
|
|
|
|
isLoading ? (
|
|
|
|
|
<p className="text-sm font-medium">Loading...</p>
|
|
|
|
|
) : fileMatches.length > 0 ? (
|
|
|
|
|
<p className="text-sm font-medium">{`[${searchDurationMs} ms] Displaying ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}</p>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm font-medium">No results</p>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
{isMoreResultsButtonVisible && !isLoading && (
|
2024-09-10 19:59:42 +00:00
|
|
|
<div
|
|
|
|
|
className="cursor-pointer text-blue-500 text-sm hover:underline"
|
2024-09-26 06:31:51 +00:00
|
|
|
onClick={onLoadMoreResults}
|
2024-09-10 19:59:42 +00:00
|
|
|
>
|
2024-09-26 06:31:51 +00:00
|
|
|
(load more)
|
2024-09-10 19:59:42 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
2024-09-03 01:46:43 +00:00
|
|
|
</div>
|
|
|
|
|
<Separator />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Search Results & Code Preview */}
|
|
|
|
|
<ResizablePanelGroup direction="horizontal">
|
|
|
|
|
<ResizablePanel minSize={20}>
|
2024-09-24 17:19:52 +00:00
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center h-full gap-2">
|
|
|
|
|
<SymbolIcon className="h-6 w-6 animate-spin" />
|
|
|
|
|
<p className="font-semibold text-center">Searching...</p>
|
|
|
|
|
</div>
|
2024-09-26 06:31:51 +00:00
|
|
|
) : fileMatches.length > 0 ? (
|
|
|
|
|
<ScrollArea
|
|
|
|
|
className="h-full"
|
|
|
|
|
>
|
|
|
|
|
<SearchResultsPanel
|
|
|
|
|
fileMatches={fileMatches}
|
|
|
|
|
onOpenFileMatch={(fileMatch) => {
|
|
|
|
|
setSelectedFile(fileMatch);
|
|
|
|
|
}}
|
|
|
|
|
onMatchIndexChanged={(matchIndex) => {
|
|
|
|
|
setSelectedMatchIndex(matchIndex);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
{isMoreResultsButtonVisible && (
|
|
|
|
|
<div className="p-3">
|
|
|
|
|
<span
|
|
|
|
|
className="cursor-pointer text-blue-500 hover:underline"
|
|
|
|
|
onClick={onLoadMoreResults}
|
|
|
|
|
>
|
|
|
|
|
Load more results
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<Scrollbar orientation="vertical" />
|
|
|
|
|
</ScrollArea>
|
2024-09-24 17:19:52 +00:00
|
|
|
) : (
|
2024-09-26 06:31:51 +00:00
|
|
|
<div className="flex flex-col items-center justify-center h-full">
|
|
|
|
|
<p className="text-sm text-muted-foreground">No results found</p>
|
|
|
|
|
</div>
|
2024-09-24 17:19:52 +00:00
|
|
|
)}
|
2024-09-03 01:46:43 +00:00
|
|
|
</ResizablePanel>
|
|
|
|
|
<ResizableHandle withHandle={selectedFile !== undefined} />
|
|
|
|
|
<ResizablePanel
|
|
|
|
|
minSize={20}
|
|
|
|
|
hidden={!selectedFile}
|
|
|
|
|
>
|
2024-09-26 03:12:20 +00:00
|
|
|
<CodePreviewPanel
|
2024-09-03 01:46:43 +00:00
|
|
|
fileMatch={selectedFile}
|
|
|
|
|
onClose={() => setSelectedFile(undefined)}
|
|
|
|
|
selectedMatchIndex={selectedMatchIndex}
|
|
|
|
|
onSelectedMatchIndexChange={setSelectedMatchIndex}
|
|
|
|
|
/>
|
|
|
|
|
</ResizablePanel>
|
|
|
|
|
</ResizablePanelGroup>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|