Skip to content

Add AI event summary highlights on detail pages#247

Merged
chrisballinger merged 16 commits into
masterfrom
ai-event-summary
May 31, 2026
Merged

Add AI event summary highlights on detail pages#247
chrisballinger merged 16 commits into
masterfrom
ai-event-summary

Conversation

@chrisballinger
Copy link
Copy Markdown
Member

Summary

  • Adds AI-generated pro-tip summaries of hosted events on camp, art, and event detail pages
  • Summaries also appear as a header in the "See all N events" list view
  • Uses existing workflow pipeline (retryWithCandidateFiltering + withContextWindowRetry) for graceful handling of guardrail violations and context overflow
  • Loads asynchronously as Phase 3 after hosted events resolve, with a loading spinner placeholder

Test plan

  • Open a camp detail page with hosted events — verify loading spinner then 1-2 sentence highlight appears after "See all N events"
  • Open an art detail page with hosted events — same behavior
  • Open an event detail page with host that has other events — summary appears after host events section
  • Tap "See all N events" — verify summary header appears above the event list
  • Verify no summary cell appears on pre-iOS 26 or when AI is unavailable
  • Verify no crashes when a camp/art has zero events

🤖 Generated with Claude Code

chrisballinger and others added 16 commits April 12, 2026 20:58
Remove hidesBottomBarWhenPushed from detail screens, tracks,
AI guide, recently viewed, and feature flags views for a
consistent Liquid Glass tab bar experience.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show a concise AI-generated pro-tip summarizing hosted events on camp,
art, and event detail pages, plus the hosted events list view. Uses the
existing workflow pipeline (retryWithCandidateFiltering + withContextWindowRetry)
so summaries work even when individual events trigger guardrails or exceed
the context window.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of regenerating the AI summary, pass it through from the
detail page. Also move the summary into the scrollable list content
so it scrolls away with the rest of the rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split AI summary into Swift-generated schedule tips (factual, from real
occurrence data) and LLM-generated overview (no timing info allowed).
This prevents the on-device model from fabricating event times.

- Schedule tips: group by event name (fixes duplicates), sorted by
  day-of-week, strikethrough for expired (only during festival),
  tappable to navigate to event detail, themed highlight color
- RAM cache (actor-based EventSummaryCache) keyed by host UID
- Model pre-warming at AgentOrchestrator init
- LLM overview: 1-2 sentences, can mention event names, no times/days
- Host camp description passed to LLM for richer context
- Tips show instantly, LLM overview arrives async

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pass 1 generates the overview with tightened instructions ("only
reference provided data"). Pass 2 validates against the exact source
data (camp description + event names/descriptions) using a separate
LLM session that flags unsupported claims. Swift strips flagged
phrases. If validation fails or strips too much, the overview is
discarded entirely — no unverified text shown.

Each step handles its own retries via withContextWindowRetry /
retryWithCandidateFiltering, matching the pattern used by other
AI workflows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-load event host (camp or art) via batch query so list cells can show
"[Host Name] ... [Address]" without N+1 async resolution. Adds
PlaceDataObject protocol with address fallback chains, populates
EventObjectOccurrence.host on fetch, and removes the old async
resolveHosts() machinery from 5 view models. DetailViewModel reuses the
pre-loaded host instead of refetching.

Tests cover address fallback, host delegation, EventOccurrenceJoinedRow
precedence, and end-to-end batch resolution through fetchEvents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move hour grouping into PlayaDB via observeEventsByHour and a new
EventHourSection model so the SwiftUI Events list consumes pre-shaped
sections, matching the legacy YapDB-grouped UIKit path. Build a
tappable + drag-scrubbable EventHourIndexView strip on the trailing
edge with bare hour digits, and split the view model into browse vs.
search modes so search runs as its own observation.

Search now hits event_objects_fts (Porter stemming/unicode folding) by
pre-resolving matching event UIDs and constraining the occurrence
request, replacing the in-memory .lowercased().contains() fallback
that was masking FTS for both UI and AI-tool callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a tap+drag scrubber bubble to the hour quick-scroll strip with light
haptic feedback, fading in over the row content above the user's finger
so the active hour stays visible during a drag. Collapse the strip's
padding and material when idle so digits sit flush at the trailing edge,
expanding only on scrub-active; widen the gesture's hit area with an
invisible leading-side extension so the collapsed strip is comfortably
tappable without expanding its visible footprint or reflowing the table.
Pin the row trailing inset to the strip's active width so layout stays
stable across both states.

Move the favorite heart from a trailing Button to a leading Image with
its own onTapGesture, restoring the legacy UIKit cell shape and unblocking
row-level taps that the nested Button-in-Button was swallowing. Drop the
fixed 100pt description height so cells with short descriptions don't pad
out unnecessarily.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace SwiftUI List with ScrollView+LazyVStack so day swaps no longer
process ~1500 row identities. Collapse the per-tap GRDB fetch into a
single long-lived observation with a single JOIN, sliced by day in the
view model. Cache Calendar boundaries in the bucket so ~16k Calendar
calls become ~30. Skip the bulk metadata-ensure write on the new
observation to avoid a startup feedback loop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapse the 8-workflow AI Guide catalog, the separate chat surface, and the
orphaned assistant path into one immediacy-first screen: free-text + suggestion
chips, a time-of-day picker, and a map-area picker. It returns "what's near you
happening now" plus "what to do next."

- New: RightNowWorkflow + RightNowView/RightNowViewModel, TimeOfDay, Vibe,
  AreaPickerView, AIResolvedObject, WorkflowProgressTypes.
- Delete Adventure/CampCrawl/GoldenHour/DayPlan/ScheduleOptimizer/Serendipity/
  WhatDidIMiss/GeneralChat workflows, the chat UI, the catalog, and the dead
  AIAssistantService path (21 files).
- Preserve semantic search (AISearchService.search) and the detail-page event
  summaries (WorkflowUtilities + the two kept Generables).
- Add tests: TimeOfDay windows, Vibe/chip mapping, area-region conversion,
  candidate gathering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The time-of-day picker now pairs with a day selector covering the full festival
(Sunday through Sunday).

- TimeOfDay.dateWindow(on:) anchors the window to any festival day (`.now` stays
  "next 2h" for today, else the whole selected day); dateWindow(now:) kept as a
  convenience.
- RightNowViewModel tracks selectedDay, defaulting to today's festival day.
- RightNowView adds a Day menu (from YearSettings.festivalDays) beside Time and
  Place, in a horizontally scrollable filter bar.
- Tests: day-anchored windows, consecutive-day ordering, week-length coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes from on-device testing:

- Event-first results: lead with events ("now" = happening now, "next" =
  upcoming in the window) and only fall back to camps/art when no events match.
  If the vibe's event types return nothing, relax to any event type so we still
  surface events instead of a pile of camps. Steer the curation prompt to honor
  each candidate's type (don't call a camp an "event").
- Bound LLM curation with a 22s timeout + graceful fallback: if the on-device
  model stalls (cold first-use / asset load) or is filtered, show the gathered
  candidates without AI pitches instead of hanging forever on "Picking the best."

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Region filtering on events was a client-side Swift post-filter (fetch all
time-matching occurrences, then drop out-of-region ones). Index it instead, and
route art/camp region filtering through the existing point R*Tree too.

- New event_occurrence_rtree (lat/lon + start/end epoch), self-maintained by
  insert/delete triggers on event_occurrences (lat/lon from the parent event's
  denormalized GPS; time via strftime), plus a full rebuild on import and a
  one-time backfill in setupDatabase for installs whose DB predates the index.
- eventObjectOccurrences[/Joined](filter:) now push region into SQL via an
  occurrence-id prefilter from the rtree (spatial); time/type stay exact in SQL.
- Art/camp inRegion() now query spatial_index instead of a 4-clause bbox WHERE;
  added FilterRegion.bounds.
- Added event_occurrence_rtree to event observations' tracked regions.
- Tests: EventOccurrenceRTreeTests (region/window/type/in-region-camp/nil-GPS/
  rebuild + art/camp inRegion parity). Full PlayaDB suite: 145 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The occurrence R*Tree's minT/maxT time columns were never queried (region
filtering via occurrenceIDsInRegion is spatial-only), yet they violated the
rtree (minT<=maxT) constraint on the real bundled dataset — for occurrences
whose stored TEXT dates don't parse via SQLite strftime, or whose end precedes
start, one bound resolved to a large epoch and the other to 0. That failed the
seed import outright (DependencyContainer init), so the app ran on an empty DB.
In-memory test fixtures were clean, so it never tripped in tests.

Convert event_occurrence_rtree to spatial-only (id, minLat, maxLat, minLon,
maxLon). A PRAGMA table_info migration drops the prior 7-column table + its
triggers and recreates the index, which self-heals the broken install on next
launch (the never-completed seed re-runs). The insert trigger and
rebuildOccurrenceRTree now index lat/lon only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A compact, swipeable SwiftUI card pinned near the bottom of the main map
that surfaces art/camps/events within ~100m of the user. Events come first
(happening now / starting soon, by start time), then art + camps by distance.

- NearbyCardViewModel: observes Art/Camp/Event data providers over a tight
  region + the location stream; emits a flat, ordered, de-duped [NearbyItem]
  (ordering extracted into a pure, unit-tested static function). Selection is
  tracked by item id so location updates don't yank the user mid-swipe.
- NearbyCardView: paged card (image, 1-line title/description, event timing,
  favorite heart, audio-tour play button for art), a minimize button, and a
  badged FAB. iOS 26 Liquid Glass morph between card and FAB
  (GlassEffectContainer + shared glassEffectID), gated behind
  canImport(FoundationModels) + #available with a .ultraThinMaterial +
  matchedGeometry fallback for the iOS 16.6 deployment target.
- NearbyCardHostingController: hosts the view as a proper child VC of the map
  (addChild/didMove, intrinsic-content sizing so it doesn't block the map),
  pushes detail via DetailViewControllerFactory on tap.
- DependencyContainer.makeNearbyCardViewModel(); MainMapViewController wires it
  in via setupNearbyCard().
- Tests: NearbyCardViewModelTests covers events-first, distance sort, radius
  exclusion, ended-event exclusion, dedup, cap, and no-GPS drop.

Note: final build/test runs in this environment failed at SPM package
resolution (network), so a green build+test was not reconfirmed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add nearby card overlay to the main map
@chrisballinger chrisballinger merged commit 0e91c3b into master May 31, 2026
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant