From c1cdba14f2d31b19f7f1c6f3e0cfe580a1b6d01e Mon Sep 17 00:00:00 2001 From: jlightner Date: Wed, 1 Apr 2026 06:21:29 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Added=20partial=5Fmatches=20fallback=20?= =?UTF-8?q?UI=20to=20search=20results=20=E2=80=94=20shows=20muted=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "frontend/src/api/public-client.ts" - "frontend/src/pages/SearchResults.tsx" - "frontend/src/App.css" GSD-Task: S01/T03 --- frontend/src/App.css | 28 ++++++++++++++ frontend/src/api/public-client.ts | 1 + frontend/src/pages/SearchResults.tsx | 57 +++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/frontend/src/App.css b/frontend/src/App.css index d375724..7d890c4 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -772,6 +772,34 @@ a.app-footer__repo:hover { font-size: 0.875rem; } +/* ── Partial match fallback ─────────────────────────────────────────── */ + +.partial-match-banner { + text-align: center; + padding: 1rem; + margin-bottom: 1rem; + color: var(--color-text-muted); + font-size: 0.875rem; + border: 1px solid var(--color-border); + border-radius: var(--radius-md, 8px); + background: var(--color-surface-raised, var(--color-surface)); +} + +.partial-match-results__header { + font-size: 0.8125rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--color-text-muted); + margin-bottom: 0.75rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--color-border); +} + +.partial-match-results .search-result-card { + opacity: 0.85; +} + .error-text { color: var(--color-error); } diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index 1148f5d..7addf57 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -22,6 +22,7 @@ export interface SearchResultItem { export interface SearchResponse { items: SearchResultItem[]; + partial_matches: SearchResultItem[]; total: number; query: string; fallback_used: boolean; diff --git a/frontend/src/pages/SearchResults.tsx b/frontend/src/pages/SearchResults.tsx index 5713990..f769ac6 100644 --- a/frontend/src/pages/SearchResults.tsx +++ b/frontend/src/pages/SearchResults.tsx @@ -22,12 +22,14 @@ export default function SearchResults() { useDocumentTitle(q ? `Search: ${q} — Chrysopedia` : "Search — Chrysopedia"); const [results, setResults] = useState([]); + const [partialMatches, setPartialMatches] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const doSearch = useCallback(async (query: string) => { if (!query.trim()) { setResults([]); + setPartialMatches([]); return; } @@ -36,9 +38,11 @@ export default function SearchResults() { try { const res = await searchApi(query.trim()); setResults(res.items); + setPartialMatches(res.partial_matches ?? []); } catch (err) { setError(err instanceof Error ? err.message : "Search failed"); setResults([]); + setPartialMatches([]); } finally { setLoading(false); } @@ -69,8 +73,18 @@ export default function SearchResults() { {loading &&
Searching…
} {error &&
Error: {error}
} - {/* No results */} - {!loading && !error && q && results.length === 0 && ( + {/* No exact results — partial match fallback */} + {!loading && !error && q && results.length === 0 && partialMatches.length > 0 && ( + <> +
+

No exact matches for all terms in "{q}"

+
+ + + )} + + {/* No results at all */} + {!loading && !error && q && results.length === 0 && partialMatches.length === 0 && (

No results found for "{q}"

@@ -155,3 +169,42 @@ function SearchResultCard({ item, staggerIndex }: { item: SearchResultItem; stag ); } + +function PartialMatchResults({ items }: { items: SearchResultItem[] }) { + const techniqueResults = items.filter((r) => r.type === "technique_page"); + const momentResults = items.filter((r) => r.type === "key_moment"); + + return ( +
+

+ Results matching some of your terms +

+ + {techniqueResults.length > 0 && ( +
+

+ Techniques ({techniqueResults.length}) +

+
+ {techniqueResults.map((item, i) => ( + + ))} +
+
+ )} + + {momentResults.length > 0 && ( +
+

+ Key Moments ({momentResults.length}) +

+
+ {momentResults.map((item, i) => ( + + ))} +
+
+ )} +
+ ); +}