From b68775ebfb98cf2f9a70419eb2b4b592e05e1f50 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 --- .gsd/milestones/M012/slices/S01/S01-PLAN.md | 2 +- .../M012/slices/S01/tasks/T02-VERIFY.json | 9 +++ .../M012/slices/S01/tasks/T03-SUMMARY.md | 79 +++++++++++++++++++ frontend/src/App.css | 28 +++++++ frontend/src/api/public-client.ts | 1 + frontend/src/pages/SearchResults.tsx | 57 ++++++++++++- 6 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 .gsd/milestones/M012/slices/S01/tasks/T02-VERIFY.json create mode 100644 .gsd/milestones/M012/slices/S01/tasks/T03-SUMMARY.md diff --git a/.gsd/milestones/M012/slices/S01/S01-PLAN.md b/.gsd/milestones/M012/slices/S01/S01-PLAN.md index e8326ff..7421cd9 100644 --- a/.gsd/milestones/M012/slices/S01/S01-PLAN.md +++ b/.gsd/milestones/M012/slices/S01/S01-PLAN.md @@ -24,7 +24,7 @@ - Estimate: 30min - Files: backend/pipeline/stages.py, backend/pipeline/qdrant_client.py, backend/routers/pipeline.py - Verify: Inspect stage 6 code to confirm enriched text. Hit reindex endpoint and verify Qdrant points contain creator_name in payload. -- [ ] **T03: Frontend no-results fallback with partial suggestions** — In `frontend/src/pages/SearchResults.tsx`: +- [x] **T03: Added partial_matches fallback UI to search results — shows muted suggestions when no exact AND match exists** — In `frontend/src/pages/SearchResults.tsx`: 1. Update SearchResponse type in public-client.ts to include `partial_matches: SearchResultItem[]`. 2. When `results.length === 0 && partial_matches.length > 0`, show a 'No exact matches for all terms' banner followed by 'Results matching some of your terms:' with the partial_matches rendered as SearchResultCards. 3. When both are empty, keep the existing 'No results found' state. diff --git a/.gsd/milestones/M012/slices/S01/tasks/T02-VERIFY.json b/.gsd/milestones/M012/slices/S01/tasks/T02-VERIFY.json new file mode 100644 index 0000000..014d4a4 --- /dev/null +++ b/.gsd/milestones/M012/slices/S01/tasks/T02-VERIFY.json @@ -0,0 +1,9 @@ +{ + "schemaVersion": 1, + "taskId": "T02", + "unitId": "M012/S01/T02", + "timestamp": 1775024275844, + "passed": true, + "discoverySource": "none", + "checks": [] +} diff --git a/.gsd/milestones/M012/slices/S01/tasks/T03-SUMMARY.md b/.gsd/milestones/M012/slices/S01/tasks/T03-SUMMARY.md new file mode 100644 index 0000000..3dbb988 --- /dev/null +++ b/.gsd/milestones/M012/slices/S01/tasks/T03-SUMMARY.md @@ -0,0 +1,79 @@ +--- +id: T03 +parent: S01 +milestone: M012 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/api/public-client.ts", "frontend/src/pages/SearchResults.tsx", "frontend/src/App.css"] +key_decisions: ["Defensive ?? [] on partial_matches so frontend handles both old and new API responses"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "TypeScript compilation clean (npx tsc --noEmit). Vite production build succeeds (52 modules). Browser verification with mocked API confirms partial match fallback UI renders correctly. Empty state still works when both arrays empty." +completed_at: 2026-04-01T06:21:17.082Z +blocker_discovered: false +--- + +# T03: Added partial_matches fallback UI to search results — shows muted suggestions when no exact AND match exists + +> Added partial_matches fallback UI to search results — shows muted suggestions when no exact AND match exists + +## What Happened +--- +id: T03 +parent: S01 +milestone: M012 +key_files: + - frontend/src/api/public-client.ts + - frontend/src/pages/SearchResults.tsx + - frontend/src/App.css +key_decisions: + - Defensive ?? [] on partial_matches so frontend handles both old and new API responses +duration: "" +verification_result: passed +completed_at: 2026-04-01T06:21:17.082Z +blocker_discovered: false +--- + +# T03: Added partial_matches fallback UI to search results — shows muted suggestions when no exact AND match exists + +**Added partial_matches fallback UI to search results — shows muted suggestions when no exact AND match exists** + +## What Happened + +Added partial_matches field to the frontend SearchResponse type. Updated SearchResults.tsx with partialMatches state, three-way rendering (exact results → partial match fallback → empty state), and a PartialMatchResults component that groups partial results by type with a muted header. Added CSS for banner and header styling with reduced opacity on partial result cards. + +## Verification + +TypeScript compilation clean (npx tsc --noEmit). Vite production build succeeds (52 modules). Browser verification with mocked API confirms partial match fallback UI renders correctly. Empty state still works when both arrays empty. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `npx tsc --noEmit` | 0 | ✅ pass | 3000ms | +| 2 | `npx vite build` | 0 | ✅ pass | 2600ms | + + +## Deviations + +None. + +## Known Issues + +None. + +## Files Created/Modified + +- `frontend/src/api/public-client.ts` +- `frontend/src/pages/SearchResults.tsx` +- `frontend/src/App.css` + + +## Deviations +None. + +## Known Issues +None. 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) => ( + + ))} +
+
+ )} +
+ ); +}