Meet Coach — Feature & Configuration Guide
Overview
Meet Coach is the configuration surface where users shape their AI Coach: who shows up (persona), how it talks (tone + depth), what it pays attention to (focus areas), and when it reaches out (frequency). It runs as a four-step wizard for first-time users and as an editable summary for returning users.
This page covers the wizard UX, the data model, and the integration points. The Coach's runtime behavior (insight cards, Goal Coach modal, milestone generation) lives in Coach.
Related docs:
- Coach — runtime AI features that consume these preferences
- Onboarding Timeline — where Meet Coach fits in the activation arc
- Data Models —
UserDetail.datastorage details - Meet Coach PRD — original product rationale and design exploration
Route and entry points
- Route:
/meet-coach(MeetCoachinionic_frontend/src/router/index.ts) - View:
ionic_frontend/src/views/MeetCoach.vue - Triggered by: the dashboard's "From the Sage" coach card, the side menu's Coach link, and the post-onboarding
/welcomeflow'sDoneSlideCTA.
The wizard and the confirmation screen share a single route. State is held in two refs:
phase—'wizard'(steps 1–3) or'ready'(the confirmation screen).currentStep—1 | 2 | 3while in the wizard.
Page modes
MeetCoach.vue has two top-level modes (pageMode):
editing— first-time configuration. Renders the four-step wizard. Saves on step 3 and advances tophase: 'ready'.summary— returning user. Renders a summary card with three sections (Persona, Tone & Focus, Frequency) and an "Edit" link on each that drops back into the wizard scoped to the picked section. Saving from a section returns to summary.
Mode is decided on load from coachingPreferences.configured:
configured: false→editing(first-time wizard).configured: true→summary(returning user).
The four-step wizard
Step 1 — Pick your Coach
Six persona cards in a 2×3 grid (mobile: 1×6 stack). One selection.
| ID | Name | Tagline |
|---|---|---|
captain | The Captain | Clear eyes, steady course. |
spark | The Spark | Energy is contagious — starting now. |
mirror | The Mirror | The answer is usually already in your head. |
rival | The Rival | You said you wanted this. |
sage | The Sage | Progress is a practice, not a performance. |
analyst | The Analyst | The data tells a story. |
Source of truth: ionic_frontend/src/constants/coachPersonas.ts (six personas, each with id, name, tagline, color, accent, palette, lofiIcon). The hi-fi persona logo is the bespoke System C chip icon rendered by <CoachPersonaIcon> (no fidelity prop or fidelity="hi"). The legacy Lucide line icon is retained in the lofiIcon field as a low-fidelity fallback (fidelity="lo" prop). Used by PersonaCard.vue here and in the onboarding PersonaSlide.
Selecting a persona enables the Next: Style → CTA. The selected card carries a per-persona color border via inline :style and a ✓ SELECTED chip.
Step 2 — Dial it in
Two columns on desktop (lg:grid-cols-2), stacked on mobile.
Left — CoachTonePad (2-D pad). A 5×5 grid pad with two axes:
- X — Gentle ↔ Tough (
tone,1..5). Labels: 1 Gentle, 2 Supportive, 3 Balanced, 4 Direct, 5 Tough. Source:ionic_frontend/src/constants/coachToneLabels.ts. Renamed from Demanding in v4.47 wave 1 (OBJ-3018) — the axis label disagreed with the resolved label below it (already "Tough") and the guide; the guide won. - Y — Brief ↔ Detailed (
depth,1..5). Labels: 1 Brief, 2 Concise, 3 Medium, 4 Thorough, 5 Detailed. Source:ionic_frontend/src/constants/coachDepthLabels.ts.
Below the pad, the resolved labels (e.g. "Tough · Brief") render live as the dot moves, followed by a one-sentence explanation that concatenates both axes — tone first, then depth (coachToneExplanations + coachDepthExplanations, both in the constants files above). "Tough · Brief" reads: "Holds you to what you said. No softening. One sentence. Maybe two."
The pad ships with a "Reset to Balanced · Medium" link that sets both axes to 3.
Accessibility: outer wrapper has role="application", the draggable dot is role="slider" with a single aria-valuetext announcing both axes ("Tone: Balanced. Depth: Medium."). Pointer drag, arrow keys (one cell per press), and Home/End (jump to extremes) all supported. Honors prefers-reduced-motion.
Right — CoachFocusList. Vertical list of five focus cards. Pick up to two. Source: ionic_frontend/src/constants/coachFocusAreas.ts.
| ID | Label | Description |
|---|---|---|
streaks | Streaks | Daily consistency, habit formation, showing up. |
milestones | Milestones | Progress toward your stated goals. |
reflection | Reflection | Mood, energy, and the story behind the data. |
accountability | Accountability | What you said you'd do, and whether you did. |
energy | Energy | When you show up best, and how to protect it. |
Selection model: the first pick becomes primaryFocus; the second becomes secondaryFocus. Picking a third deselects the secondary and replaces it. Toggling the primary off promotes the secondary into primary.
Roving tabindex per CoachFocusList.vue — single tab stop on the active item, arrow keys cycle.
Step 3 — Set the rhythm
CoachFrequencyCardGrid renders four cards in a 2×2 grid plus a pinned example-notification card. Source: ionic_frontend/src/constants/coachFrequencies.ts.
| ID | Label | Description |
|---|---|---|
daily | Every day | Daily check-in prompts and insight cards. |
active | When I'm active | Only on days you sign in. |
when_needed | When I need it | On streak breaks, overdue milestones, or inactivity. |
weekly | Weekly | A single Sunday planning + Friday review. |
The example notification card swaps copy per persona so the user previews how Coach will read on their phone.
Step 3 ends with Meet my Coach — the only commit CTA in the wizard. Tapping it calls updateCoachingPreferences and advances to phase: 'ready'.
Step 4 — Coach Ready
A centered confirmation screen with the persona avatar (green checkmark overlay), the eyebrow YOUR COACH IS READY, the headline Meet *<persona name>*., the first message preview, a row of tag pills (Persona / Tone / Focus / Frequency — MeetCoach.vue:117-120), and two CTAs:
- Go to my dashboard → — persona-accented commit CTA. Routes to
/. - Retune — text link. Returns to step 1 with current selections preserved.
On first configuration only (wasFirstConfig true at save time), the screen also fires a confetti burst (gated on prefers-reduced-motion) and surfaces the toast Coach's Pick badge earned! +50 XP.
Layout
Desktop (lg: and up)
Three columns:
- Left rail — three step rows (active row carries
aria-current="step") plus a "Why this matters" callout inbg-coach/10. - Main column — current step content.
- Right rail — sticky
CoachPreviewcard showingPreview · <Persona> / ● LIVEwith a persona-tinted left border. Visible on steps 1–3 of the wizard; hidden on the Coach Ready phase (v-if="phase === 'wizard' && selectedPersona").
Step transitions are CTA-driven only — clicking ahead in the rail does not navigate (status indicator only).
Mobile (default)
Each step is a discrete page:
- Top bar — back arrow (
aria-label="Back", oraria-label="Exit setup"on step 1), step title (Pick your Coach/Dial it in/Set the rhythm),Step X of 3label, and a 3-segment progress bar. - Main column — current step content.
- Inline preview — same
CoachPreviewcard, rendered above the bottom CTA on every wizard step. - Sticky bottom CTA —
pb-[env(safe-area-inset-bottom)]clears the home indicator.
Coach Ready is a centered single column on both breakpoints with the persona/tone/focus/rhythm pill row visible.
Tokens and styling
| Surface | Token | Notes |
|---|---|---|
| "Next" CTAs (steps 1–2) | bg-coach (<UiButton variant="coach">) | Coach feature thematic purple, defined in ionic_frontend/src/theme/tailwind.css (--coach: 271 91% 65%). |
| Commit CTAs ("Meet my Coach", "Go to my dashboard") | persona accent via inline :style | Each persona's color field from coachPersonas.ts. |
| Italic headline accents | text-primary (blue) | Same on every step on every breakpoint. |
WHY THIS MATTERS callout | bg-coach/10 dark:bg-coach/15 border-coach/20 | Desktop only. |
| Persona card selected state | border-2 + persona color via :style + aria-checked="true" + ✓ SELECTED chip | No border-accent utility (welcome onboarding's PersonaSlide follows the same contract). |
| LIVE indicator | green-success pill | Mobile preview + desktop preview header. |
WCAG AA contrast verified for text-coach and text-primary on light + dark surfaces.
Accessibility highlights
- Tone Pad:
role="application"outer +role="slider"inner with singlearia-valuetext; arrow / Home / End keyboard support; pointer + drag clamping; 200 ms debounce on announcements. - Persona grid: roving tabindex via
personaGroupRef. Single tab stop; arrow keys cycle. - Focus list, Frequency grid: roving tabindex (
tabindex="0"on the active row,-1on the rest). Arrow keys cycle. - Step rail rows are non-interactive — status only.
- Mobile back arrow —
aria-label="Back"on steps 2–3,aria-label="Exit setup"on step 1. - Reduced motion — every animation, including the Coach Ready confetti, is gated on
prefers-reduced-motion. The legacy 5-second auto-scroll-after-pick +personaConfirmedspotlight transitions are removed in this rewrite. - Tap targets — every interactive control clears 44 × 44 px on mobile.
Defaults
Unconfigured users get:
persona: 'spark'
tone: 3 (Balanced)
depth: 3 (Medium)
focus_primary: 'streaks'
focus_secondary: null
frequency: 'daily'
configured: falseSource: User#coaching_preferences (rails_api/app/models/user.rb). These are returned whenever UserDetail.coach_* keys are absent, so anonymous and pre-Meet-Coach users render a working Coach without ever visiting /meet-coach.
Data model
Coaching preferences live on UserDetail.data (JSONB) via store_accessor. See Data Models — User Detail (Coaching Preferences) for full field details.
| Field | Type | Notes |
|---|---|---|
coach_persona | string | One of captain / spark / mirror / rival / sage / analyst. |
coach_tone | integer 1..5 | Stored as a string in JSONB; coerced via .to_i in User#coaching_preferences. |
coach_depth | integer 1..5 | Same coercion. |
coach_focus_primary | string | One of streaks / milestones / reflection / accountability / energy. |
coach_focus_secondary | string | null | Same enum, nullable. |
coach_frequency | string | One of daily / active / when_needed / weekly. |
coach_configured_at | ISO 8601 | Set on first save. Drives configured: true. |
Validation is enforced at the interaction level by UserIdentity::UpdateCoachingPreferences. Invalid inputs return a :invalid_input error code with an enumerated message ("Invalid frequency: must be one of daily, active, when_needed, weekly", etc.).
GraphQL surface
Query
coachingPreferences is exposed on User as a CoachingPreferencesType:
type CoachingPreferences {
persona: String!
tone: Int!
depth: Int!
focusPrimary: String!
focusSecondary: String
frequency: String!
configured: Boolean!
}Defined in rails_api/app/graphql/types/coaching_preferences_type.rb.
Mutation
mutation UpdateCoachingPreferences(
$persona: String,
$tone: Int,
$depth: Int,
$focusPrimary: String,
$focusSecondary: String,
$frequency: String
) {
updateCoachingPreferences(
persona: $persona,
tone: $tone,
depth: $depth,
focusPrimary: $focusPrimary,
focusSecondary: $focusSecondary,
frequency: $frequency
) {
coachingPreferences {
persona
tone
depth
focusPrimary
focusSecondary
frequency
configured
}
errors
}
}All arguments are optional — partial updates (e.g., a returning user changing only the persona) leave the rest of the row alone.
Defined in rails_api/app/graphql/mutations/update_coaching_preferences.rb. Frontend constant lives in ionic_frontend/src/constants/graphql/user.js as UPDATE_COACHING_PREFERENCES_MUTATION. The same query selects the new fields on USER_QUERY.
Side effects
The save call is the only place UserAction.generate(action: :configure_coach) fires (see UserIdentity::UpdateCoachingPreferences#call). It runs only on first configuration, gated on coach_configured_at being blank before the save. This is the configure_coach activation milestone (INST-01) used by the onboarding funnel.
wasFirstConfig on the frontend tracks the same condition for the confetti + "Coach's Pick badge earned! +50 XP" celebration. Returning users editing from the summary card do not re-trigger either.
Components
| Component | Path | Purpose |
|---|---|---|
MeetCoach.vue | ionic_frontend/src/views/MeetCoach.vue | Wizard host + Coach Ready phase + summary mode for returning users. |
PersonaCard.vue | ionic_frontend/src/components/coach/PersonaCard.vue | Persona card with per-persona color border, aria-checked, ✓ SELECTED chip. Shared with onboarding's PersonaSlide. |
CoachTonePad.vue | ionic_frontend/src/components/coach/CoachTonePad.vue | 2-D pad for tone × depth. |
CoachFocusList.vue | ionic_frontend/src/components/coach/CoachFocusList.vue | Vertical list with "pick up to 2" enforcement. |
CoachFrequencyCardGrid.vue | ionic_frontend/src/components/coach/CoachFrequencyCardGrid.vue | 2×2 frequency grid + example notification card. |
CoachPreview.vue | ionic_frontend/src/components/coach/CoachPreview.vue | Live preview card. Shows Tone · Depth · Focus strip and persona-tinted left border. |
All five new components have Storybook stories with tags: ['autodocs', 'a11y'] and dark-mode variants. The legacy CoachToneSlider.vue and CoachFrequencyChips.vue have been removed.
Testing
Backend
cd rails_api
bundle exec rspec spec/interactions/user_identity/update_coaching_preferences_spec.rb
bundle exec rspec spec/requests/mutations/update_coaching_preferences_spec.rbThe interaction spec covers success, all four validation failure paths (persona, tone, depth, focus, frequency), and the configure_coach UserAction side effect. The request spec exercises the GraphQL mutation including the $depth argument.
Frontend
cd ionic_frontend
npm run test:unit -- --run -t "CoachTonePad"
npm run test:unit -- --run -t "CoachFocusList"
npm run test:unit -- --run -t "CoachFrequencyCardGrid"
npm run test:unit -- --run -t "PersonaCard"
npm run test:unit -- --run -t "CoachPreview"
npm run test:unit -- --run -t "MeetCoach"Cypress happy-path E2E lives at ionic_frontend/tests/smoke-playwright/specs/coach-enneagram/meet-coach.spec.ts.
Manual checklist
First-time wizard
- [ ] Navigate to
/meet-coachas a fresh user. - [ ] Verify the desktop three-rail layout (left rail, main, right preview).
- [ ] Pick a persona —
Next: Style →enables, persona color fills the card border,✓ SELECTEDchip appears. - [ ] Drag the Tone Pad with a pointer and with arrow keys; the dot snaps to grid cells.
- [ ] Pick three focus areas — verify the third pick replaces the secondary, not the primary.
- [ ] Pick a frequency; verify the example-notification card swaps copy per persona.
- [ ] Tap Meet my Coach — confetti fires, toast shows "Coach's Pick badge earned! +50 XP", route stays on
/meet-coachwithphase: 'ready'. - [ ] On Coach Ready, verify the persona avatar with green checkmark, eyebrow, headline, first-message card, and pill row.
- [ ] Tap Go to my dashboard → — routes to
/.
Mobile
- [ ] At iPhone-X viewport, verify each step is a discrete page with a top bar and a sticky bottom CTA.
- [ ] Verify the back arrow uses
aria-label="Exit setup"on step 1 andaria-label="Back"on steps 2–3. - [ ] Verify the inline preview card sits above the bottom CTA on steps 2 and 3 (not just step 1).
- [ ] Verify Coach Ready shows the persona/tone/focus/rhythm pill row.
Returning-user summary
- [ ] Reload
/meet-coachafter first configuration — landing screen is the summary card with three "Edit" links. - [ ] Click an "Edit" link, change one field, tap save — page returns to summary, no confetti, no toast.
Reduced motion
- [ ] In OS settings, enable "Reduce motion" — confetti is suppressed, animations short-circuit.
Known behaviors
- Defaults render even without a save.
User#coaching_preferencesreturns the default tuple wheneverUserDetail.coach_*is absent — soconfigured: falseis the only thing distinguishing a fresh user from a saved one in the summary card's UI logic. coach_toneandcoach_depthare stored as strings in JSONB but coerced via.to_iinUser#coaching_preferences. This is intentional forstore_accessorcompatibility; treat the GraphQL surface as the typed contract.coach_focus_secondary: nullis a real value. The interaction always assigns it (user_details.coach_focus_secondary = focus_secondary), so unsetting it via the mutation works.- First-message timing on Coach Ready is a stub.
firstMessageTimeinMeetCoach.vuereturns either'this Sunday at 9:00am'(weekly) or'tomorrow at 7:00am'(everything else). It is not timezone-aware. Replacing the stub with a backend-computed value is tracked as a follow-up. - Returning-user edits are partial saves. The mutation only sends the fields the user changed, so the JSONB row keeps its existing values for the rest. This is why every argument on
updateCoachingPreferencesis optional.
Last updated: 2026-05-22 (Phase 96 audit: wizard steps, data model verified) | Version: v3.9.131