Skip to content

Commit 72d7b4f

Browse files
joecodex
authored andcommitted
group search results by library
Refs: DOCS-12518 Render search results as library groups, with each library shown once and matching document hits nested below it. Preserve concise document rows with title, path, source link, description, and deduplicated snippets without repeating library metadata for every hit. Verification: npm run build; npm test -- --run; git diff --check. Co-authored-by: Codex <noreply@openai.com>
1 parent 76b1d64 commit 72d7b4f

1 file changed

Lines changed: 66 additions & 36 deletions

File tree

web/src/views/pages.tsx

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,7 @@ export function SearchView() {
16941694
const [librariesLoading, setLibrariesLoading] = useState(libraries.length === 0);
16951695
const [filtersOpen, setFiltersOpen] = useState(false);
16961696
const [results, setResults] = useState<SearchResult[]>([]);
1697+
const groupedResults = groupSearchResults(results);
16971698

16981699
useEffect(() => {
16991700
void apiGet<Library[]>("/api/v1/libraries")
@@ -1784,52 +1785,81 @@ export function SearchView() {
17841785
) : null}
17851786
</CardHeader>
17861787
<CardContent className="space-y-3">
1787-
{results.map((result) => {
1788-
const snippets = resultSnippets(result);
1789-
const displayPath = resultDisplayPath(result);
1790-
return (
1791-
<div key={`${result.library}-${result.path || result.title}-${result.score}`} className="rounded-lg border border-border p-4">
1792-
<div className="min-w-0">
1793-
<div className="flex flex-wrap items-start justify-between gap-3">
1794-
<div className="min-w-0">
1795-
<h3 className="text-base font-semibold leading-6 tracking-normal">{result.title}</h3>
1796-
<div className="mt-1 truncate text-xs text-muted-foreground">
1797-
<Link to="/catalog/$slug" params={{ slug: result.library }} className="font-medium text-foreground hover:underline">
1798-
{result.library}
1799-
</Link>
1800-
{result.source && <span className="whitespace-nowrap"> /{result.source}</span>}
1788+
{groupedResults.map((group) => (
1789+
<div key={group.library} className="overflow-hidden rounded-lg border border-border">
1790+
<div className="flex flex-wrap items-center justify-between gap-3 bg-muted/50 px-4 py-3">
1791+
<div className="min-w-0 truncate text-sm">
1792+
<Link to="/catalog/$slug" params={{ slug: group.library }} className="font-semibold text-foreground hover:underline">
1793+
{group.library}
1794+
</Link>
1795+
{group.sources.length === 1 && <span className="whitespace-nowrap text-muted-foreground"> /{group.sources[0]}</span>}
1796+
{group.sources.length > 1 && <span className="text-muted-foreground"> /{group.sources.join(", ")}</span>}
1797+
</div>
1798+
<span className="shrink-0 text-xs text-muted-foreground">{group.results.length} {group.results.length === 1 ? "match" : "matches"}</span>
1799+
</div>
1800+
{group.results.map((result) => {
1801+
const snippets = resultSnippets(result);
1802+
const displayPath = resultDisplayPath(result);
1803+
return (
1804+
<div key={`${result.library}-${result.path || result.title}-${result.score}`} className="border-t border-border p-4 first:border-t-0">
1805+
<div className="flex flex-wrap items-start justify-between gap-3">
1806+
<div className="min-w-0">
1807+
<h3 className="text-base font-semibold leading-6 tracking-normal">{result.title}</h3>
1808+
{group.sources.length > 1 && result.source && <p className="mt-1 text-xs text-muted-foreground">{result.source}</p>}
18011809
</div>
1810+
{result.source_url && (
1811+
<a className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground" href={result.source_url} target="_blank" rel="noreferrer">
1812+
Source <ExternalLink className="h-3 w-3" />
1813+
</a>
1814+
)}
18021815
</div>
1803-
{result.source_url && (
1804-
<a className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground" href={result.source_url} target="_blank" rel="noreferrer">
1805-
Source <ExternalLink className="h-3 w-3" />
1806-
</a>
1816+
{displayPath && (
1817+
<p className="mt-2 truncate font-mono text-xs text-muted-foreground">{displayPath}</p>
1818+
)}
1819+
{result.description && <p className="mt-3 line-clamp-2 text-sm text-muted-foreground">{result.description}</p>}
1820+
{snippets.length > 0 ? (
1821+
<div className="mt-3 space-y-2 text-sm leading-6">
1822+
{snippets.map((match, index) => (
1823+
<p key={`${result.title}-${index}`} className="line-clamp-3">{renderMarked(match)}</p>
1824+
))}
1825+
</div>
1826+
) : (
1827+
result.snippet && <p className="mt-3 line-clamp-3 text-sm leading-6 text-muted-foreground">{result.snippet}</p>
18071828
)}
18081829
</div>
1809-
{displayPath && (
1810-
<p className="mt-2 truncate font-mono text-xs text-muted-foreground">{displayPath}</p>
1811-
)}
1812-
{result.description && <p className="mt-3 line-clamp-2 text-sm text-muted-foreground">{result.description}</p>}
1813-
{snippets.length > 0 ? (
1814-
<div className="mt-3 space-y-2 text-sm leading-6">
1815-
{snippets.map((match, index) => (
1816-
<p key={`${result.title}-${index}`} className="line-clamp-3">{renderMarked(match)}</p>
1817-
))}
1818-
</div>
1819-
) : (
1820-
result.snippet && <p className="mt-3 line-clamp-3 text-sm leading-6 text-muted-foreground">{result.snippet}</p>
1821-
)}
1822-
</div>
1823-
</div>
1824-
);
1825-
})}
1826-
{results.length === 0 && <p className="text-sm text-muted-foreground">No results found.</p>}
1830+
);
1831+
})}
1832+
</div>
1833+
))}
1834+
{groupedResults.length === 0 && <p className="text-sm text-muted-foreground">No results found.</p>}
18271835
</CardContent>
18281836
</Card>
18291837
</section>
18301838
);
18311839
}
18321840

1841+
type SearchResultGroup = {
1842+
library: string;
1843+
sources: string[];
1844+
results: SearchResult[];
1845+
};
1846+
1847+
function groupSearchResults(results: SearchResult[]): SearchResultGroup[] {
1848+
const groups = new Map<string, SearchResultGroup>();
1849+
for (const result of results) {
1850+
let group = groups.get(result.library);
1851+
if (!group) {
1852+
group = { library: result.library, sources: [], results: [] };
1853+
groups.set(result.library, group);
1854+
}
1855+
group.results.push(result);
1856+
if (result.source && !group.sources.includes(result.source)) {
1857+
group.sources.push(result.source);
1858+
}
1859+
}
1860+
return [...groups.values()];
1861+
}
1862+
18331863
function resultDisplayPath(result: SearchResult) {
18341864
const path = result.path || "";
18351865
if (!path) return "";

0 commit comments

Comments
 (0)