Skip to content

Commit 10aa249

Browse files
improve UX around how we communicate 'load more results'
1 parent afede30 commit 10aa249

5 files changed

Lines changed: 101 additions & 66 deletions

File tree

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
'use client';
22

3-
import { ScrollArea } from "@/components/ui/scroll-area";
4-
import { Scrollbar } from "@radix-ui/react-scroll-area";
5-
import { FileMatchContainer } from "./fileMatchContainer";
63
import { SearchResultFile } from "@/lib/types";
4+
import { FileMatchContainer } from "./fileMatchContainer";
75

86
interface SearchResultsPanelProps {
97
fileMatches: SearchResultFile[];
@@ -16,32 +14,16 @@ export const SearchResultsPanel = ({
1614
onOpenFileMatch,
1715
onMatchIndexChanged,
1816
}: SearchResultsPanelProps) => {
19-
20-
if (fileMatches.length === 0) {
21-
return (
22-
<div className="flex flex-col items-center justify-center h-full">
23-
<p className="text-sm text-muted-foreground">No results found</p>
24-
</div>
25-
);
26-
}
27-
28-
return (
29-
<ScrollArea
30-
className="h-full"
31-
>
32-
{fileMatches.map((fileMatch, index) => (
33-
<FileMatchContainer
34-
key={index}
35-
file={fileMatch}
36-
onOpenFile={() => {
37-
onOpenFileMatch(fileMatch);
38-
}}
39-
onMatchIndexChanged={(matchIndex) => {
40-
onMatchIndexChanged(matchIndex);
41-
}}
42-
/>
43-
))}
44-
<Scrollbar orientation="vertical" />
45-
</ScrollArea>
46-
)
17+
return fileMatches.map((fileMatch, index) => (
18+
<FileMatchContainer
19+
key={index}
20+
file={fileMatch}
21+
onOpenFile={() => {
22+
onOpenFileMatch(fileMatch);
23+
}}
24+
onMatchIndexChanged={(matchIndex) => {
25+
onMatchIndexChanged(matchIndex);
26+
}}
27+
/>
28+
))
4729
}

src/app/search/page.tsx

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { SymbolIcon } from "@radix-ui/react-icons";
1212
import { useQuery } from "@tanstack/react-query";
1313
import Image from "next/image";
1414
import { useRouter } from "next/navigation";
15-
import { useEffect, useMemo, useState } from "react";
15+
import { useCallback, useEffect, useMemo, useState } from "react";
1616
import logoDark from "../../../public/sb_logo_dark.png";
1717
import logoLight from "../../../public/sb_logo_light.png";
1818
import { search } from "../api/(client)/client";
@@ -22,25 +22,33 @@ import useCaptureEvent from "@/hooks/useCaptureEvent";
2222
import { CodePreviewPanel } from "./components/codePreviewPanel";
2323
import { SearchResultsPanel } from "./components/searchResultsPanel";
2424
import { SearchResultFile } from "@/lib/types";
25+
import { ScrollArea } from "@/components/ui/scroll-area";
26+
import { Scrollbar } from "@radix-ui/react-scroll-area";
27+
28+
const DEFAULT_MAX_MATCH_DISPLAY_COUNT = 200;
29+
30+
export enum SearchQueryParams {
31+
query = "query",
32+
maxMatchDisplayCount = "maxMatchDisplayCount",
33+
}
2534

26-
const DEFAULT_NUM_RESULTS = 100;
2735

2836
export default function SearchPage() {
2937
const router = useRouter();
30-
const searchQuery = useNonEmptyQueryParam("query") ?? "";
31-
const _numResults = parseInt(useNonEmptyQueryParam("numResults") ?? `${DEFAULT_NUM_RESULTS}`);
32-
const numResults = isNaN(_numResults) ? DEFAULT_NUM_RESULTS : _numResults;
38+
const searchQuery = useNonEmptyQueryParam(SearchQueryParams.query) ?? "";
39+
const _maxMatchDisplayCount = parseInt(useNonEmptyQueryParam(SearchQueryParams.maxMatchDisplayCount) ?? `${DEFAULT_MAX_MATCH_DISPLAY_COUNT}`);
40+
const maxMatchDisplayCount = isNaN(_maxMatchDisplayCount) ? DEFAULT_MAX_MATCH_DISPLAY_COUNT : _maxMatchDisplayCount;
3341

3442
const [selectedMatchIndex, setSelectedMatchIndex] = useState(0);
3543
const [selectedFile, setSelectedFile] = useState<SearchResultFile | undefined>(undefined);
3644

3745
const captureEvent = useCaptureEvent();
3846

3947
const { data: searchResponse, isLoading } = useQuery({
40-
queryKey: ["search", searchQuery, numResults],
48+
queryKey: ["search", searchQuery, maxMatchDisplayCount],
4149
queryFn: () => search({
4250
query: searchQuery,
43-
numResults,
51+
maxMatchDisplayCount,
4452
}),
4553
enabled: searchQuery.length > 0,
4654
refetchOnWindowFocus: false,
@@ -93,8 +101,28 @@ export default function SearchPage() {
93101
}, [searchResponse]);
94102

95103
const isMoreResultsButtonVisible = useMemo(() => {
96-
return searchResponse && searchResponse.Result.MatchCount > numResults;
97-
}, [searchResponse, numResults]);
104+
return searchResponse && searchResponse.Result.MatchCount > maxMatchDisplayCount;
105+
}, [searchResponse, maxMatchDisplayCount]);
106+
107+
const numMatches = useMemo(() => {
108+
// Accumualtes the number of matches across all files
109+
return searchResponse?.Result.Files?.reduce(
110+
(acc, file) =>
111+
acc + file.ChunkMatches.reduce(
112+
(acc, chunk) => acc + chunk.Ranges.length,
113+
0,
114+
),
115+
0,
116+
) ?? 0;
117+
}, [searchResponse]);
118+
119+
const onLoadMoreResults = useCallback(() => {
120+
const url = createPathWithQueryParams('/search',
121+
[SearchQueryParams.query, searchQuery],
122+
[SearchQueryParams.maxMatchDisplayCount, `${maxMatchDisplayCount * 2}`],
123+
)
124+
router.push(url);
125+
}, [maxMatchDisplayCount, router, searchQuery]);
98126

99127
return (
100128
<div className="flex flex-col h-screen overflow-clip">
@@ -129,20 +157,22 @@ export default function SearchPage() {
129157
/>
130158
</div>
131159
<Separator />
132-
<div className="bg-accent py-1 px-2 flex flex-row items-center justify-between">
133-
<p className="text-sm font-medium">Results for: {fileMatches.length} files in {searchDurationMs} ms</p>
134-
{isMoreResultsButtonVisible && (
160+
<div className="bg-accent py-1 px-2 flex flex-row items-center gap-4">
161+
{
162+
isLoading ? (
163+
<p className="text-sm font-medium">Loading...</p>
164+
) : fileMatches.length > 0 ? (
165+
<p className="text-sm font-medium">{`[${searchDurationMs} ms] Displaying ${numMatches} matches in ${fileMatches.length} ${fileMatches.length > 1 ? 'files' : 'file'}`}</p>
166+
) : (
167+
<p className="text-sm font-medium">No results</p>
168+
)
169+
}
170+
{isMoreResultsButtonVisible && !isLoading && (
135171
<div
136172
className="cursor-pointer text-blue-500 text-sm hover:underline"
137-
onClick={() => {
138-
const url = createPathWithQueryParams('/search',
139-
["query", searchQuery],
140-
["numResults", `${numResults * 2}`],
141-
)
142-
router.push(url);
143-
}}
173+
onClick={onLoadMoreResults}
144174
>
145-
More results
175+
(load more)
146176
</div>
147177
)}
148178
</div>
@@ -157,16 +187,35 @@ export default function SearchPage() {
157187
<SymbolIcon className="h-6 w-6 animate-spin" />
158188
<p className="font-semibold text-center">Searching...</p>
159189
</div>
190+
) : fileMatches.length > 0 ? (
191+
<ScrollArea
192+
className="h-full"
193+
>
194+
<SearchResultsPanel
195+
fileMatches={fileMatches}
196+
onOpenFileMatch={(fileMatch) => {
197+
setSelectedFile(fileMatch);
198+
}}
199+
onMatchIndexChanged={(matchIndex) => {
200+
setSelectedMatchIndex(matchIndex);
201+
}}
202+
/>
203+
{isMoreResultsButtonVisible && (
204+
<div className="p-3">
205+
<span
206+
className="cursor-pointer text-blue-500 hover:underline"
207+
onClick={onLoadMoreResults}
208+
>
209+
Load more results
210+
</span>
211+
</div>
212+
)}
213+
<Scrollbar orientation="vertical" />
214+
</ScrollArea>
160215
) : (
161-
<SearchResultsPanel
162-
fileMatches={fileMatches}
163-
onOpenFileMatch={(fileMatch) => {
164-
setSelectedFile(fileMatch);
165-
}}
166-
onMatchIndexChanged={(matchIndex) => {
167-
setSelectedMatchIndex(matchIndex);
168-
}}
169-
/>
216+
<div className="flex flex-col items-center justify-center h-full">
217+
<p className="text-sm text-muted-foreground">No results found</p>
218+
</div>
170219
)}
171220
</ResizablePanel>
172221
<ResizableHandle withHandle={selectedFile !== undefined} />

src/app/searchBar.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import {
88
FormMessage,
99
} from "@/components/ui/form";
1010
import { Input } from "@/components/ui/input";
11-
import { cn } from "@/lib/utils";
11+
import { cn, createPathWithQueryParams } from "@/lib/utils";
1212
import { zodResolver } from "@hookform/resolvers/zod";
1313
import { cva } from "class-variance-authority";
1414
import { useRouter } from "next/navigation";
1515
import { useForm } from "react-hook-form";
1616
import { z } from "zod";
1717
import { useHotkeys } from 'react-hotkeys-hook'
1818
import { useRef } from "react";
19+
import { SearchQueryParams } from "./search/page";
1920

2021
interface SearchBarProps {
2122
className?: string;
@@ -65,7 +66,10 @@ export const SearchBar = ({
6566
});
6667

6768
const onSubmit = (values: z.infer<typeof formSchema>) => {
68-
router.push(`/search?query=${values.query}&numResults=100`);
69+
const url = createPathWithQueryParams('/search',
70+
[SearchQueryParams.query, values.query],
71+
)
72+
router.push(url);
6973
}
7074

7175
return (

src/lib/schemas.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { z } from "zod";
22

33
export const searchRequestSchema = z.object({
44
query: z.string(),
5-
numResults: z.number(),
5+
maxMatchDisplayCount: z.number(),
66
whole: z.boolean().optional(),
77
});
88

src/lib/server/searchService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import { fileNotFound, invalidZoektResponse, ServiceError, unexpectedError } fro
66
import { isServiceError } from "../utils";
77
import { zoektFetch } from "./zoektClient";
88

9-
export const search = async ({ query, numResults, whole }: SearchRequest): Promise<SearchResponse | ServiceError> => {
9+
export const search = async ({ query, maxMatchDisplayCount, whole }: SearchRequest): Promise<SearchResponse | ServiceError> => {
1010
const body = JSON.stringify({
1111
q: query,
1212
// @see: https://github.com/TaqlaAI/zoekt/blob/main/api.go#L892
1313
opts: {
1414
NumContextLines: 2,
1515
ChunkMatches: true,
16-
MaxMatchDisplayCount: numResults,
16+
MaxMatchDisplayCount: maxMatchDisplayCount,
1717
Whole: !!whole,
1818
ShardMaxMatchCount: SHARD_MAX_MATCH_COUNT,
1919
TotalMaxMatchCount: TOTAL_MAX_MATCH_COUNT,
@@ -46,7 +46,7 @@ export const getFileSource = async ({ fileName, repository }: FileSourceRequest)
4646

4747
const searchResponse = await search({
4848
query: `${escapedFileName} repo:^${escapedRepository}$`,
49-
numResults: 1,
49+
maxMatchDisplayCount: 1,
5050
whole: true,
5151
});
5252

0 commit comments

Comments
 (0)