diff --git a/.gsd/milestones/M014/slices/S07/S07-PLAN.md b/.gsd/milestones/M014/slices/S07/S07-PLAN.md index 48ddfff..8d367f5 100644 --- a/.gsd/milestones/M014/slices/S07/S07-PLAN.md +++ b/.gsd/milestones/M014/slices/S07/S07-PLAN.md @@ -48,7 +48,7 @@ - Estimate: 2h - Files: backend/schemas.py, backend/pipeline/stages.py, backend/pipeline/qdrant_client.py, backend/search_service.py, backend/pipeline/test_section_embedding.py - Verify: PYTHONPATH=backend python -m pytest backend/pipeline/test_section_embedding.py -v && PYTHONPATH=backend python -c "from pipeline.stages import _slugify_heading; assert _slugify_heading('Grain Position Control') == 'grain-position-control'; print('slugify OK')" && grep -q 'section_anchor' backend/schemas.py && grep -q 'technique_section' backend/search_service.py -- [ ] **T02: Frontend — Hash scroll handler + section search result rendering** — Extend TechniquePage hash scrolling to handle section anchors (not just #km-). Update search result components to display and link technique_section results. Add section_anchor and section_heading to the TypeScript SearchResultItem type. +- [x] **T02: Added technique_section result rendering with Section badge, deep links to page#anchor, and generalized hash scrolling for all anchor types** — Extend TechniquePage hash scrolling to handle section anchors (not just #km-). Update search result components to display and link technique_section results. Add section_anchor and section_heading to the TypeScript SearchResultItem type. ## Steps diff --git a/.gsd/milestones/M014/slices/S07/tasks/T01-VERIFY.json b/.gsd/milestones/M014/slices/S07/tasks/T01-VERIFY.json new file mode 100644 index 0000000..b7853ba --- /dev/null +++ b/.gsd/milestones/M014/slices/S07/tasks/T01-VERIFY.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M014/S07/T01", + "timestamp": 1775182376809, + "passed": true, + "discoverySource": "task-plan", + "checks": [ + { + "command": "PYTHONPATH=backend python -m pytest backend/pipeline/test_section_embedding.py -v", + "exitCode": 0, + "durationMs": 2237, + "verdict": "pass" + }, + { + "command": "grep -q 'section_anchor' backend/schemas.py", + "exitCode": 0, + "durationMs": 7, + "verdict": "pass" + }, + { + "command": "grep -q 'technique_section' backend/search_service.py", + "exitCode": 0, + "durationMs": 8, + "verdict": "pass" + } + ] +} diff --git a/.gsd/milestones/M014/slices/S07/tasks/T02-SUMMARY.md b/.gsd/milestones/M014/slices/S07/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..713048c --- /dev/null +++ b/.gsd/milestones/M014/slices/S07/tasks/T02-SUMMARY.md @@ -0,0 +1,80 @@ +--- +id: T02 +parent: S07 +milestone: M014 +provides: [] +requires: [] +affects: [] +key_files: ["frontend/src/api/public-client.ts", "frontend/src/pages/TechniquePage.tsx", "frontend/src/pages/SearchResults.tsx", "frontend/src/components/SearchAutocomplete.tsx"] +key_decisions: ["Also fixed autocomplete links for key_moment type which was previously linking incorrectly"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "Frontend TypeScript build passes with zero errors: `cd frontend && npm run build` — 57 modules transformed, built in 925ms." +completed_at: 2026-04-03T02:15:04.871Z +blocker_discovered: false +--- + +# T02: Added technique_section result rendering with Section badge, deep links to page#anchor, and generalized hash scrolling for all anchor types + +> Added technique_section result rendering with Section badge, deep links to page#anchor, and generalized hash scrolling for all anchor types + +## What Happened +--- +id: T02 +parent: S07 +milestone: M014 +key_files: + - frontend/src/api/public-client.ts + - frontend/src/pages/TechniquePage.tsx + - frontend/src/pages/SearchResults.tsx + - frontend/src/components/SearchAutocomplete.tsx +key_decisions: + - Also fixed autocomplete links for key_moment type which was previously linking incorrectly +duration: "" +verification_result: passed +completed_at: 2026-04-03T02:15:04.893Z +blocker_discovered: false +--- + +# T02: Added technique_section result rendering with Section badge, deep links to page#anchor, and generalized hash scrolling for all anchor types + +**Added technique_section result rendering with Section badge, deep links to page#anchor, and generalized hash scrolling for all anchor types** + +## What Happened + +Extended four frontend files to support technique_section search results: added section_anchor/section_heading to the TypeScript type, generalized TechniquePage hash scroll from #km- only to any fragment, added technique_section link routing and Section badge to SearchResults and SearchAutocomplete, and fixed a pre-existing autocomplete link bug where all result types linked to /techniques/${slug}. + +## Verification + +Frontend TypeScript build passes with zero errors: `cd frontend && npm run build` — 57 modules transformed, built in 925ms. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd frontend && npm run build` | 0 | ✅ pass | 3100ms | + + +## Deviations + +Fixed pre-existing bug in SearchAutocomplete where all autocomplete result links pointed to /techniques/${item.slug} regardless of type — key_moment and technique_section results now link correctly with hash fragments. + +## Known Issues + +None. + +## Files Created/Modified + +- `frontend/src/api/public-client.ts` +- `frontend/src/pages/TechniquePage.tsx` +- `frontend/src/pages/SearchResults.tsx` +- `frontend/src/components/SearchAutocomplete.tsx` + + +## Deviations +Fixed pre-existing bug in SearchAutocomplete where all autocomplete result links pointed to /techniques/${item.slug} regardless of type — key_moment and technique_section results now link correctly with hash fragments. + +## Known Issues +None. diff --git a/frontend/src/api/public-client.ts b/frontend/src/api/public-client.ts index 2e436fd..c0c5ecb 100644 --- a/frontend/src/api/public-client.ts +++ b/frontend/src/api/public-client.ts @@ -19,6 +19,8 @@ export interface SearchResultItem { topic_tags: string[]; technique_page_slug?: string; match_context?: string; + section_anchor?: string; + section_heading?: string; } export interface SearchResponse { diff --git a/frontend/src/components/SearchAutocomplete.tsx b/frontend/src/components/SearchAutocomplete.tsx index bd0460e..c8d91be 100644 --- a/frontend/src/components/SearchAutocomplete.tsx +++ b/frontend/src/components/SearchAutocomplete.tsx @@ -163,6 +163,7 @@ export default function SearchAutocomplete({ creator: "Creator", technique_page: "Technique", key_moment: "Key Moment", + technique_section: "Section", }; return ( @@ -215,26 +216,37 @@ export default function SearchAutocomplete({ {showSearch && ( <> - {searchResults.map((item) => ( - setShowDropdown(false)} - > - {item.title} - - - {typeLabel[item.type] ?? item.type} - - {item.creator_name && ( - - {item.creator_name} - + {searchResults.map((item) => { + let linkTo = `/techniques/${item.slug}`; + if (item.type === "technique_section" && item.technique_page_slug) { + linkTo = `/techniques/${item.technique_page_slug}${item.section_anchor ? `#${item.section_anchor}` : ""}`; + } else if (item.type === "key_moment" && item.technique_page_slug) { + linkTo = `/techniques/${item.technique_page_slug}#km-${item.slug || item.title}`; + } + return ( + setShowDropdown(false)} + > + {item.title} + {item.type === "technique_section" && item.section_heading && ( + § {item.section_heading} )} - - - ))} + + + {typeLabel[item.type] ?? item.type} + + {item.creator_name && ( + + {item.creator_name} + + )} + + + ); + })} r.type === "technique_page"); + const sectionResults = results.filter((r) => r.type === "technique_section"); const momentResults = results.filter((r) => r.type === "key_moment"); return ( @@ -124,6 +125,20 @@ export default function SearchResults() { )} + {/* Technique sections */} + {sectionResults.length > 0 && ( +
+

+ Sections ({sectionResults.length}) +

+
+ {sectionResults.map((item, i) => ( + + ))} +
+
+ )} + {/* Key moments */} {momentResults.length > 0 && (
@@ -142,6 +157,15 @@ export default function SearchResults() { } function getSearchResultLink(item: SearchResultItem): string { + if (item.type === "technique_section") { + if (item.technique_page_slug && item.section_anchor) { + return `/techniques/${item.technique_page_slug}#${item.section_anchor}`; + } + if (item.technique_page_slug) { + return `/techniques/${item.technique_page_slug}`; + } + return `/search?q=${encodeURIComponent(item.title)}`; + } if (item.type === "key_moment") { if (item.technique_page_slug) { return `/techniques/${item.technique_page_slug}#km-${item.slug || item.title}`; @@ -153,6 +177,12 @@ function getSearchResultLink(item: SearchResultItem): string { } function SearchResultCard({ item, staggerIndex }: { item: SearchResultItem; staggerIndex: number }) { + const typeLabels: Record = { + technique_page: "Technique", + key_moment: "Key Moment", + technique_section: "Section", + }; + return ( {item.title} - {item.type === "technique_page" ? "Technique" : "Key Moment"} + {typeLabels[item.type] ?? item.type} + {item.type === "technique_section" && item.section_heading && ( +
+ § {item.section_heading} +
+ )} {item.match_context && (
@@ -198,6 +233,7 @@ function SearchResultCard({ item, staggerIndex }: { item: SearchResultItem; stag function PartialMatchResults({ items }: { items: SearchResultItem[] }) { const techniqueResults = items.filter((r) => r.type === "technique_page"); + const sectionResults = items.filter((r) => r.type === "technique_section"); const momentResults = items.filter((r) => r.type === "key_moment"); return ( @@ -219,6 +255,19 @@ function PartialMatchResults({ items }: { items: SearchResultItem[] }) {
)} + {sectionResults.length > 0 && ( +
+

+ Sections ({sectionResults.length}) +

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

diff --git a/frontend/src/pages/TechniquePage.tsx b/frontend/src/pages/TechniquePage.tsx index 38771cd..570593e 100644 --- a/frontend/src/pages/TechniquePage.tsx +++ b/frontend/src/pages/TechniquePage.tsx @@ -171,12 +171,12 @@ export default function TechniquePage() { }; }, [slug, selectedVersion]); - // Scroll to key moment if URL has a #km- hash fragment + // Scroll to hash fragment after technique loads (key moments, section anchors, etc.) useEffect(() => { if (!technique) return; - const hash = window.location.hash; - if (hash.startsWith("#km-")) { - const el = document.getElementById(hash.slice(1)); + const hash = window.location.hash.slice(1); + if (hash) { + const el = document.getElementById(hash); if (el) { el.scrollIntoView({ behavior: "smooth", block: "start" }); }