V1 Web PWA · Surface walkthrough

Search

A per-surface walkthrough of the V1 Web PWA customer surface: layout, states, interactions, data, and cross-references.

walked
9sections6 minread

On this page

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 /explore can navigate here (verify internal cross-link) or stay in the embedded search feed; both surfaces hit the same /v1/search BFF
  • Direct URL / bookmark — yes; ?q=<query> and other filter params hydrate state via useBrowserSearchParams():
    • q or query — initial query
    • domain — initial SearchDomain
    • kind or kinds — initial kindFilters[]
    • sort, score, saved, scope — initial sort/score/saved/match-scope
  • Cross-domain "Save search" / recents — stored under oshun-search-saved-v1 and oshun-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 stripShellPersistentContextStrip at top
  • Search input lane — large <input ref={inputRef}> with submit button, saved/recent-query menu, suggestion chips (getSearchSuggestions(domain))
  • Filter toolbarWorkspaceToolbar with:
    • 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)
  • 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 listSKELETON_COUNT = 5 skeletons during loading
  • Evidence sidebarEvidenceSidebar shown on laptop+ breakpoints (splitLayout = isLaptopUp(viewport))
  • Empty / no-query states — distinct screens for "type a search" vs "no matches"

States#

  • No querysubmittedQuery.trim() === ''; results cleared, searched === false; suggestion + recent-search affordances visible
  • Loading (skeleton)loading === true after submitting a query; five skeleton rows render with the shared count-up animation
  • Results populatedresults.length > 0; grouped by WebNavigableDomainId when grouping is active
  • Empty resultsloading === 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-query Try again
  • Saved/recent search restore — selecting a stored query rehydrates every filter via applySearchWorkspaceState
  • Result previewpreviewedResultKey controls in-place result preview affordance (verify exact UI)
  • Library save toggle — saving a result calls toggleOshunWebLibraryItem(buildLibraryItemFromSearchResult(...)) and fires trackLibraryItemSaved / trackLibraryItemUnsaved
  • Reduced motiondisableAnimation removes input focus pulses, result entrance staggers, and count-up animations
  • Standalone PWA / Offlineno view-specific branch; relies on shell networking.

Interactions#

Search input#

  • Query input (text input, inputRef)
    • Function: updates query; submit (Enter or button) sets submittedQuery and triggers /v1/search fetch
    • URL: applySearchWorkspaceState rewrites ?q= and other params via window.history (no Next router navigation)
  • 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
  • Saved search entry (per SEARCH_SAVED_STORAGE_KEY)
    • Storage: localStorage['oshun-search-saved-v1'], max 8 entries

Filter toolbar#

  • Domain segment railSEARCH_DOMAIN_FILTERS; sets domainFilter; re-issues /v1/search with the new domain parameter
  • Sort segment rail — Relevance / Recent / Domain
  • Advanced filters toggle — opens/closes the #oshun-search-advanced-filters panel; 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 trackSearchResultOpened analytics

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 resultdispatchOshunAssistantOpen(...) 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 local SearchResultItem shape with domain-specific optional fields)
    • useOshunWebLibraryStore() — saved-item snapshot for the saved filter
    • useBrowserSearchParams() — URL hydration
    • localStorage['oshun-search-recent-v1'] — recent searches
    • localStorage['oshun-search-saved-v1'] — saved searches
  • Writes:
    • toggleOshunWebLibraryItem(...) — client store
    • localStorage writes for recents/saved via writeStoredSearchEntries
  • Realtime: None.
  • Caching: cancelled flag 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 the LOCAL_DEV_FALLBACK_TOKEN when NODE_ENV !== 'production', returning null in prod — not a hard-coded literal
  • Telemetry: trackSearchResultOpened, trackLibraryItemSaved, trackLibraryItemUnsaved

Cross-references#

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 restoration
  • apps/oshun/web/e2e/search-workspace-layout.spec.ts — split/stacked preview lane and assistant handoff from selected result context
  • apps/oshun/web/e2e/search-result-analytics.spec.ts — dedicated search-page search_result_opened payloads
  • apps/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 — trackSearchResultOpened fires on result open/full handoff; preview and library save use separate events
  • Confirm whether /search should also surface partial-failure outage banners (it issues the same BFF endpoint as /explore, which already handles partialFailure)
  • Decide whether empty-state suggestion chips should become interactive; the current EmptyState renders them as static spans
  • Dev Authorization: Bearer dev.... fallback is already prod-safe — resolveBffAuthToken() returns null in production (the dev literal is gated behind NODE_ENV !== 'production')
  • Map the full set of result template kinds to the BFF contract — current file enumerates seven templates but only checks the minimal SearchResultItem fields on parse