Context. surface customer · domain discovery · route /search · auth signed-in · source apps/oshun/web/src/app/search/page.tsx
Last walked. 2026-06-27 targeted search walk — direct URL filter hydration, live /v1/search guarded-speech seed, filter-driven empty state, Reset all, Clear to no-query, saved/recent search memory, real BFF result handoffs, preview, assistant handoff, evidence sidebar, library save/resume, fetch failure Retry, and open-result analytics verified in Playwright. Evidence: apps/oshun/web/e2e/search-saved-and-recent.spec.ts, apps/oshun/web/e2e/search-workspace-layout.spec.ts, apps/oshun/web/e2e/search-result-analytics.spec.ts, apps/oshun/web/e2e/sophia-grounded-answer-page.spec.ts
Purpose#
A dedicated search results surface that complements /explore. Issues queries
against the BFF /v1/search endpoint, groups results by domain, supports
relevance / kind / score / saved / match-scope filters, persists recent and
saved searches in localStorage, and surfaces an evidence sidebar for trust
signals.
Entry points#
- Shell nav: Explore → Search — breadcrumbs
Explore → Search(active="explore"); reached from typing in the shell search bar, or via direct URL with?q= - Explore page — issuing a search in
/explorecan navigate here (verify internal cross-link) or stay in the embedded search feed; both surfaces hit the same/v1/searchBFF - Direct URL / bookmark — yes;
?q=<query>and other filter params hydrate state viauseBrowserSearchParams():qorquery— initial querydomain— initialSearchDomainkindorkinds— initialkindFilters[]sort,score,saved,scope— initial sort/score/saved/match-scope
- Cross-domain "Save search" / recents — stored under
oshun-search-saved-v1andoshun-search-recent-v1
Layout regions#
page.tsx mounts ShellLayout with active="explore" and breadcrumbs
Explore → Search, renders <h1 className="sr-only">Search · OSHUN</h1>, then
<SearchResultsView />.
Inside SearchResultsView:
- Shell persistent context strip —
ShellPersistentContextStripat top - Search input lane — large
<input ref={inputRef}>with submit button, saved/recent-query menu, suggestion chips (getSearchSuggestions(domain)) - Filter toolbar —
WorkspaceToolbarwith:- Domain segments (
SEARCH_DOMAIN_FILTERS) - Sort segments — Relevance / Recent / Domain
- Advanced filters panel (collapsible, id
oshun-search-advanced-filters) holding kind multi-select chips, score filter (All / Good+ / Strong+ / Excellent), saved filter (All / Saved only / Unsaved only), match-scope filter (Anywhere / Title hits / Summary hits)
- Domain segments (
- Results list / domain groups — grouped by domain when sortMode allows;
each result renders one of seven templates
(
default | concept | passage | source | claim | notebook | program | ritual) - Skeleton list —
SKELETON_COUNT = 5skeletons during loading - Evidence sidebar —
EvidenceSidebarshown on laptop+ breakpoints (splitLayout = isLaptopUp(viewport)) - Empty / no-query states — distinct screens for "type a search" vs "no matches"
States#
- No query —
submittedQuery.trim() === ''; results cleared,searched === false; suggestion + recent-search affordances visible - Loading (skeleton) —
loading === trueafter submitting a query; five skeleton rows render with the shared count-up animation - Results populated —
results.length > 0; grouped byWebNavigableDomainIdwhen grouping is active - Empty results —
loading === false && searched === true && results.length === 0; animated empty state copy - Filter-driven empty — kind/score/saved/match-scope filter active but
backend returned results (filtered client-side); empty branch shows the
no-results copy and the toolbar exposes
Reset all - Fetch failure — non-OK response sets
data-search-error, clears result rows, and exposes same-queryTry again - Saved/recent search restore — selecting a stored query rehydrates
every filter via
applySearchWorkspaceState - Result preview —
previewedResultKeycontrols in-place result preview affordance (verify exact UI) - Library save toggle — saving a result calls
toggleOshunWebLibraryItem(buildLibraryItemFromSearchResult(...))and firestrackLibraryItemSaved/trackLibraryItemUnsaved - Reduced motion —
disableAnimationremoves input focus pulses, result entrance staggers, and count-up animations - Standalone PWA / Offline — no view-specific branch; relies on shell networking.
Interactions#
Search input#
- Query input (text input,
inputRef)- Function: updates
query; submit (Enter or button) setssubmittedQueryand triggers/v1/searchfetch - URL:
applySearchWorkspaceStaterewrites?q=and other params viawindow.history(no Next router navigation)
- Function: updates
- Submit / search button — submits current
query - Clear (×) — clears query,
submittedQuery, results, and returns to the no-query state - Suggestion chip — empty-state suggestions render as static text chips; decide whether they should become clickable query refinements
- Recent search entry (per
SEARCH_RECENT_STORAGE_KEY)- Function:
applySearchWorkspaceState(stored, { recordRecent: false, openFilters: true }); rehydrates the full saved workspace - Storage:
localStorage['oshun-search-recent-v1'], max 6 entries
- Function:
- Saved search entry (per
SEARCH_SAVED_STORAGE_KEY)- Storage:
localStorage['oshun-search-saved-v1'], max 8 entries
- Storage:
Filter toolbar#
- Domain segment rail —
SEARCH_DOMAIN_FILTERS; setsdomainFilter; re-issues/v1/searchwith the newdomainparameter - Sort segment rail — Relevance / Recent / Domain
- Advanced filters toggle — opens/closes the
#oshun-search-advanced-filterspanel; default-open when any non-default advanced filter is present in the URL - Kind multi-select chips — toggle
kindFilters[]; URL writes as?kind=(one) or?kinds=(csv) - Score filter — All / Good+ / Strong+ / Excellent (client-side filter
against
result.score) - Saved filter — All / Saved only / Unsaved only (uses
useOshunWebLibraryStore) - Match-scope filter — Anywhere / Title hits / Summary hits (highlights
the search term using
getSearchResultPresentation)
Result row#
- Title link — opens
buildSearchResultLaunchPath(result) - Library save toggle — visible when
canSaveSearchResultToLibrary(result) - Result detail templates — vary by
kind: passage / source / claim / notebook / program / ritual / concept / default; each surfaces domain-specific metadata (citation count, confidence score, progress, …) - Preview — opens an in-place preview (
previewedResultKey) when the result template supports it - Open result — fires
trackSearchResultOpenedanalytics
Evidence sidebar (laptop+)#
- Evidence list — driven by
deriveGroundedEvidenceStatus(results); shows current trust posture (high | medium | low | unknown) for the visible result set
Assistant entry#
- Dispatch assistant from a result —
dispatchOshunAssistantOpen(...)opens the in-app assistant scoped to the result context
Data & contracts#
- Reads:
- BFF
GET /v1/search?q=&domain=— minimal payload here:{ results?: SearchResultItem[] }(see localSearchResultItemshape with domain-specific optional fields) useOshunWebLibraryStore()— saved-item snapshot for the saved filteruseBrowserSearchParams()— URL hydrationlocalStorage['oshun-search-recent-v1']— recent searcheslocalStorage['oshun-search-saved-v1']— saved searches
- BFF
- Writes:
toggleOshunWebLibraryItem(...)— client storelocalStoragewrites for recents/saved viawriteStoredSearchEntries
- Realtime: None.
- Caching:
cancelledflag on each fetch prevents stale writes; debouncing is not present in this file (submit-driven), unlike/explore - Auth/role check:
Authorization: Bearer ${resolveBffAuthToken() ?? ''}.resolveBffAuthToken()(lib/bff-auth.ts) prefers the real session token (tryGetApiAuthToken()) and only returns theLOCAL_DEV_FALLBACK_TOKENwhenNODE_ENV !== 'production', returningnullin prod — not a hard-coded literal - Telemetry:
trackSearchResultOpened,trackLibraryItemSaved,trackLibraryItemUnsaved
Cross-references#
- Shell:
shell/01-app-shell.md - Sibling routes:
explore.md— parent in breadcrumb; shares/v1/searchhome.md,library.md,activity.md,messages.md,switcher.md
- Component sources:
apps/oshun/web/src/components/search/SearchResultsView.tsxapps/oshun/web/src/components/search/search-config.tsapps/oshun/web/src/components/search/SearchScanCard.tsxapps/oshun/web/src/design-system/components/EvidenceSidebar.tsx
- Feature spec:
V1/features.md
E2E coverage#
apps/oshun/web/e2e/search-saved-and-recent.spec.ts— recent/saved memory, direct URL filter hydration, live BFF guarded-speech seed, filter-driven empty state, Reset all, Clear to no-query, result opens, save/resume, fetch failure Retry, and Browser Back restorationapps/oshun/web/e2e/search-workspace-layout.spec.ts— split/stacked preview lane and assistant handoff from selected result contextapps/oshun/web/e2e/search-result-analytics.spec.ts— dedicated search-pagesearch_result_openedpayloadsapps/oshun/web/e2e/sophia-grounded-answer-page.spec.ts— live BFF Nisaba search result preview, grounded evidence sidebar, and support notes
Open questions / known gaps#
- Document the search analytics funnel —
trackSearchResultOpenedfires on result open/full handoff; preview and library save use separate events - Confirm whether
/searchshould also surface partial-failure outage banners (it issues the same BFF endpoint as/explore, which already handlespartialFailure) - Decide whether empty-state suggestion chips should become interactive; the
current
EmptyStaterenders them as static spans - Dev
Authorization: Bearer dev....fallback is already prod-safe —resolveBffAuthToken()returnsnullin production (the dev literal is gated behindNODE_ENV !== 'production') - Map the full set of result template kinds to the BFF contract — current
file enumerates seven templates but only checks the minimal
SearchResultItemfields on parse