Skip to content

Commit edb63c8

Browse files
authored
feat: configurable highlighted text (#61)
1 parent b7cd75c commit edb63c8

File tree

13 files changed

+373
-138
lines changed

13 files changed

+373
-138
lines changed

packages/docs/components/custom-search.tsx

Lines changed: 173 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,34 @@ import {
88
} from "@anaralabs/lector";
99
import { useEffect, useState } from "react";
1010

11+
// Define the TextPosition interface to match the one in useSearchPosition.tsx
12+
interface TextPosition {
13+
pageNumber: number;
14+
text: string;
15+
matchIndex: number;
16+
searchText?: string;
17+
}
18+
1119
interface ResultItemProps {
1220
result: SearchResult;
21+
originalSearchText?: string;
1322
}
1423

15-
const ResultItem = ({ result }: ResultItemProps) => {
24+
const ResultItem = ({ result, originalSearchText }: ResultItemProps) => {
1625
const { jumpToHighlightRects } = usePdfJump();
1726
const getPdfPageProxy = usePdf((state) => state.getPdfPageProxy);
1827

1928
const onClick = async () => {
2029
const pageProxy = getPdfPageProxy(result.pageNumber);
2130

22-
const rects = await calculateHighlightRects(pageProxy, {
31+
const textPosition: TextPosition = {
2332
pageNumber: result.pageNumber,
2433
text: result.text,
2534
matchIndex: result.matchIndex,
26-
});
35+
searchText: originalSearchText,
36+
};
37+
38+
const rects = await calculateHighlightRects(pageProxy, textPosition);
2739

2840
jumpToHighlightRects(rects, "pixels");
2941
};
@@ -46,13 +58,51 @@ const ResultItem = ({ result }: ResultItemProps) => {
4658
);
4759
};
4860

61+
// Component for full context highlighting
62+
const ResultItemFullHighlight = ({ result }: { result: SearchResult }) => {
63+
const { jumpToHighlightRects } = usePdfJump();
64+
const getPdfPageProxy = usePdf((state) => state.getPdfPageProxy);
65+
66+
const onClick = async () => {
67+
const pageProxy = getPdfPageProxy(result.pageNumber);
68+
69+
const textPosition: TextPosition = {
70+
pageNumber: result.pageNumber,
71+
text: result.text,
72+
matchIndex: result.matchIndex,
73+
// No searchText parameter = highlight full context
74+
};
75+
76+
const rects = await calculateHighlightRects(pageProxy, textPosition);
77+
jumpToHighlightRects(rects, "pixels");
78+
};
79+
80+
return (
81+
<div
82+
className="flex py-2 hover:bg-gray-50 flex-col cursor-pointer"
83+
onClick={onClick}
84+
>
85+
<div className="flex-1 min-w-0">
86+
<p className="text-sm text-gray-900">{result.text}</p>
87+
</div>
88+
<div className="flex items-center gap-4 flex-shrink-0 text-sm text-gray-500">
89+
{!result.isExactMatch && (
90+
<span>{(result.score * 100).toFixed()}% match</span>
91+
)}
92+
<span className="ml-auto">Page {result.pageNumber}</span>
93+
</div>
94+
</div>
95+
);
96+
};
97+
4998
interface ResultGroupProps {
5099
title: string;
51100
results: SearchResult[];
52101
displayCount?: number;
102+
originalSearchText?: string;
53103
}
54104

55-
const ResultGroup = ({ title, results, displayCount }: ResultGroupProps) => {
105+
const ResultGroup = ({ title, results, displayCount, originalSearchText }: ResultGroupProps) => {
56106
if (!results.length) return null;
57107

58108
const displayResults = displayCount
@@ -67,6 +117,30 @@ const ResultGroup = ({ title, results, displayCount }: ResultGroupProps) => {
67117
<ResultItem
68118
key={`${result.pageNumber}-${result.matchIndex}`}
69119
result={result}
120+
originalSearchText={originalSearchText}
121+
/>
122+
))}
123+
</div>
124+
</div>
125+
);
126+
};
127+
128+
// Modified ResultGroup for full highlighting
129+
const ResultGroupFullHighlight = ({ title, results, displayCount }: Omit<ResultGroupProps, 'originalSearchText'>) => {
130+
if (!results.length) return null;
131+
132+
const displayResults = displayCount
133+
? results.slice(0, displayCount)
134+
: results;
135+
136+
return (
137+
<div className="space-y-2">
138+
<h3 className="text-sm font-medium text-gray-700">{title}</h3>
139+
<div className="divide-y divide-gray-100">
140+
{displayResults.map((result) => (
141+
<ResultItemFullHighlight
142+
key={`${result.pageNumber}-${result.matchIndex}`}
143+
result={result}
70144
/>
71145
))}
72146
</div>
@@ -146,8 +220,101 @@ export const SearchResults = ({
146220

147221
return (
148222
<div className="flex flex-col gap-4">
149-
<ResultGroup title="Exact Matches" results={results.exactMatches} />
150-
<ResultGroup title="Similar Matches" results={results.fuzzyMatches} />
223+
<ResultGroup
224+
title="Exact Matches"
225+
results={results.exactMatches}
226+
originalSearchText={searchText}
227+
/>
228+
<ResultGroup
229+
title="Similar Matches"
230+
results={results.fuzzyMatches}
231+
originalSearchText={searchText}
232+
/>
233+
234+
{results.hasMoreResults && (
235+
<button
236+
onClick={onLoadMore}
237+
className="w-full py-2 px-4 text-sm text-white bg-blue-500 rounded-lg hover:bg-blue-600 transition-colors"
238+
>
239+
Load More Results
240+
</button>
241+
)}
242+
</div>
243+
);
244+
};
245+
246+
// New component for full context highlighting
247+
export const SearchUIFullHighlight = () => {
248+
const [searchText, setSearchText] = useState("");
249+
const [debouncedSearchText] = useDebounce(searchText, 500);
250+
const [limit, setLimit] = useState(5);
251+
const { searchResults: results, search } = useSearch();
252+
253+
useEffect(() => {
254+
setLimit(5);
255+
search(debouncedSearchText, { limit: 5 });
256+
}, [debouncedSearchText]);
257+
258+
const handleSearch = (searchText: string) => {
259+
setSearchText(searchText);
260+
};
261+
262+
const handleLoadMore = async () => {
263+
const newLimit = limit + 5;
264+
await search(debouncedSearchText, { limit: newLimit });
265+
setLimit(newLimit);
266+
};
267+
268+
return (
269+
<div className="flex flex-col w-80 h-full">
270+
<div className="px-4 py-4 border-b border-gray-200 bg-white">
271+
<div className="relative">
272+
<input
273+
type="text"
274+
value={searchText || ""}
275+
onChange={(e) => handleSearch(e.target.value)}
276+
placeholder="Search in document..."
277+
className="w-full px-4 py-2 bg-white border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
278+
/>
279+
</div>
280+
</div>
281+
<div className="flex-1 overflow-y-auto px-4">
282+
<div className="py-4">
283+
<SearchResultsFullHighlight
284+
searchText={searchText}
285+
results={results}
286+
onLoadMore={handleLoadMore}
287+
/>
288+
</div>
289+
</div>
290+
</div>
291+
);
292+
};
293+
294+
// Results component for full context highlighting
295+
const SearchResultsFullHighlight = ({
296+
searchText,
297+
results,
298+
onLoadMore,
299+
}: SearchResultsProps) => {
300+
if (!searchText) return null;
301+
302+
if (!results.exactMatches.length && !results.fuzzyMatches.length) {
303+
return (
304+
<div className="text-center py-4 text-gray-500">No results found</div>
305+
);
306+
}
307+
308+
return (
309+
<div className="flex flex-col gap-4">
310+
<ResultGroupFullHighlight
311+
title="Exact Matches"
312+
results={results.exactMatches}
313+
/>
314+
<ResultGroupFullHighlight
315+
title="Similar Matches"
316+
results={results.fuzzyMatches}
317+
/>
151318

152319
{results.hasMoreResults && (
153320
<button

packages/docs/components/search-control.tsx

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,55 @@ import {
1111
} from "@anaralabs/lector";
1212

1313
import "@/lib/setup";
14-
import { SearchUI } from "./custom-search";
14+
import { SearchUI, SearchUIFullHighlight } from "./custom-search";
1515

16-
const fileUrl = "/pdf/large.pdf";
16+
const fileUrl = "/pdf/pathways.pdf";
1717

1818
const ViewerZoomControl = () => {
1919
return (
20-
<Root
21-
source={fileUrl}
22-
className="flex bg-gray-50 h-[500px]"
23-
loader={<div className="p-4">Loading...</div>}
24-
>
25-
<Search>
26-
<SearchUI />
27-
</Search>
28-
<Pages className="p-4 w-full">
29-
<Page>
30-
<CanvasLayer />
31-
<TextLayer />
32-
<HighlightLayer className="bg-yellow-200/70" />
33-
</Page>
34-
</Pages>
35-
</Root>
20+
<div className="flex flex-col gap-8">
21+
<div className="flex flex-col">
22+
<h3 className="text-lg font-semibold mb-2">Exact Search Term Highlighting</h3>
23+
<p className="text-sm text-gray-600 mb-4">This viewer highlights only the exact search term you type</p>
24+
<Root
25+
source={fileUrl}
26+
className="flex bg-gray-50 h-[500px]"
27+
loader={<div className="p-4">Loading...</div>}
28+
>
29+
<Search>
30+
<SearchUI />
31+
</Search>
32+
<Pages className="p-4 w-full">
33+
<Page>
34+
<CanvasLayer />
35+
<TextLayer />
36+
<HighlightLayer className="bg-yellow-200/70" />
37+
</Page>
38+
</Pages>
39+
</Root>
40+
</div>
41+
42+
<div className="flex flex-col">
43+
<h3 className="text-lg font-semibold mb-2">Full Context Highlighting</h3>
44+
<p className="text-sm text-gray-600 mb-4">This viewer highlights the entire text chunk containing your search term</p>
45+
<Root
46+
source={fileUrl}
47+
className="flex bg-gray-50 h-[500px]"
48+
loader={<div className="p-4">Loading...</div>}
49+
>
50+
<Search>
51+
<SearchUIFullHighlight />
52+
</Search>
53+
<Pages className="p-4 w-full">
54+
<Page>
55+
<CanvasLayer />
56+
<TextLayer />
57+
<HighlightLayer className="bg-yellow-200/70" />
58+
</Page>
59+
</Pages>
60+
</Root>
61+
</div>
62+
</div>
3663
);
3764
};
3865

0 commit comments

Comments
 (0)