Skip to content

ai-partner: context-aware chip selection logic #1474

@CraigBuckmaster

Description

@CraigBuckmaster

Parent epic: #1446 (Amicus — AI Study Partner v1)
Phase: 3 · Size: S · Depends on: #1461 (chip pool — closed), #1462 (FAB + peek — closed)

Client-side logic that decides which prompt chips to surface in the FAB peek based on the user's current screen context. Pure function — no LLM call, no network. Reads pre-computed chips from scripture.db and filters by profile variant.


Status — partial existing work

This card was originally spec'd as a three-file pure-function decomposition. During implementation the work collapsed into a single hook (app/src/hooks/useAmicusChips.ts) with the route-detection logic inlined in AmicusPeekSheet.tsx. The hook works and is in production, but the spec's decomposition goal was not achieved and two pieces of behavior from the spec were never built.

What's in the codebase now (Apr 21 2026)

Spec called for Actual state
app/src/services/amicus/chipSelection.ts ❌ Not created. The selection logic is inside useAmicusChips.ts as the internal loadChips function (exported via _internal for testing).
app/src/services/amicus/contextDetection.ts (pure detectContextFromRoute) ❌ Not created. Logic lives inline in AmicusPeekSheet.tsx as useNavigationContext + findDeepestRoute, tightly coupled to useNavigationState.
app/src/hooks/useAmicusChips.ts ✅ Exists (4KB, imported from AmicusPeekSheet.tsx).
__tests__/chipSelection.test.ts ⚠️ Replaced by app/src/hooks/__tests__/useAmicusChips.test.tsx which covers variantLookupOrder and entityKeyFromContext via the _internal export. DB-hitting loadChips is not unit-tested.
__tests__/contextDetection.test.ts ❌ Not created. The inline route→context logic has zero test coverage.

Feature gaps (not just refactoring)

These are the reasons this card stays open, beyond the decomposition concern:

  1. place context is a dead branch. ChipContext in the hook declares { kind: 'place'; placeId }, but useNavigationContext in PeekSheet only handles Chapter, PersonDetail, and DebateDetail routes. PlaceDetail and Map routes fall through to { kind: 'none' }. Place-scoped chips from the chip pool are currently unreachable.

  2. Profile-variant selection was never implemented. The hook accepts a preferredVariant?: string parameter, but the one call site in AmicusPeekSheet.tsx (line 55: useAmicusChips(ctx)) passes nothing. The hook therefore always falls through to generic_balanced for every user regardless of their profile lean. The selectProfileVariant(profile) → variant function specified in the original card does not exist anywhere in the codebase.

  3. Route-detection coupling makes testing harder. Because useNavigationContext is a hook that calls useNavigationState, unit tests of the route-to-context mapping have to mock React Navigation. A pure detectContextFromRoute(route) function would be trivially testable with plain route objects. (This also came up during review of PR Fix iOS map and Amicus crash paths #1562, which tried to refactor it via prop-drilling — that PR was closed; the right fix is the original spec's decomposition.)


Remaining work

Files to create

  • app/src/services/amicus/contextDetection.ts — export a pure function detectContextFromRoute(route) that maps a route object to ChipContext. Include PlaceDetail and Map route handling (currently missing from the inline PeekSheet version). The ChipContext type moves here too (re-export from useAmicusChips.ts for back-compat).
  • app/src/services/amicus/profileVariant.ts — export selectProfileVariant(profile: CompressedProfile): ProfileVariant. Deterministic selection over the 6 variants based on dominant tradition + dominant genre. Examples:
    • tradition: "Reformed" + genre: "narrative""reformed_narrative"
    • tradition: "Jewish" + focus: "Torah""jewish_pentateuch"
    • no dominant lean → "generic_balanced"
  • app/src/services/amicus/__tests__/contextDetection.test.ts — cover every route branch (Chapter, PersonDetail, PlaceDetail, Map, DebateDetail, unknown, malformed params, nested navigator state). Use plain route objects; no navigation mocks.
  • app/src/services/amicus/__tests__/profileVariant.test.ts — assert determinism (same profile → same variant) and the documented mapping examples. Cover edge cases (empty profile, tied leans, missing fields).

Files to modify

  • app/src/hooks/useAmicusChips.ts — re-export ChipContext from contextDetection.ts (single source of truth). No behavioral change to loadChips.
  • app/src/components/amicus/AmicusPeekSheet.tsx — replace inline useNavigationContext with const ctx = detectContextFromRoute(findDeepestRoute(state)). Read the user's compressed profile (via whatever hook surfaces it — check ai-partner: compressed profile generator #1452's integration), call selectProfileVariant(profile), and pass the result as the second arg to useAmicusChips(ctx, variant). Delete the local findDeepestRoute helper only if contextDetection.ts exposes its own (to avoid duplication).

Acceptance criteria

  • detectContextFromRoute lives in its own module as a pure function, imported by AmicusPeekSheet.tsx
  • PlaceDetail and Map routes produce { kind: 'place', placeId } — verified end-to-end (place screen → peek shows place-scoped chips)
  • selectProfileVariant exists, is deterministic, and is called from AmicusPeekSheet.tsx so non-default-leaning users get their variant's chips
  • contextDetection.test.ts exercises every switch branch + malformed inputs with plain objects
  • profileVariant.test.ts asserts determinism + documented mapping
  • useAmicusChips.test.tsx continues to pass unchanged
  • No any types; lint clean; npx tsc --noEmit clean

Out of scope (deliberate)

  • Extracting loadChips into a separate pure selectChips module. It already hits the DB; splitting the pure filter/slice from the DB call is low-value given _internal.entityKeyFromContext + _internal.variantLookupOrder are already testable.
  • Changing the precached_prompts DB schema or the _tools/build_prompts.py generator.
  • Any changes to navigation architecture — this card is the decomposition + missing features, not a refactor of how navigation state flows.

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions