HEP / Plan Builder Redesign — Variant C → Final Synthesis (G) — Detailed Changes¶
Date: 2026-06-29 (Variant C) · Updated: 2026-07-02 (Synthesis G) · Status refreshed: 2026-07-29
Repos: OktaPT-FE
Status: shipped on development, behind the "New plan builder (beta)" toggle, default off. The classic InitialPlanCreationModal / EditPlanModal remain the default path and are untouched.
History:
- Variant C — merged into development (commit 003010c).
- Six refinement variants HEP-EDITS-A…F — worktree explorations on top of merged C (AI integration + polish, each with a different bet). Superseded.
- HEP-EDITS-G — the final synthesis: best-of-breed from A–F plus new work. Scope: 19 files, +1,320 / −246, 2 new files. Merged into development via PRs #559 and #561 (commits cbc446b, fa7d84c, 88dabf7). The split view, docked AI column, useNewBuilderPref, addInstances, fromAiPlan, and the actualDate → focus type fix are all live at HEAD.
- Post-G polish (PR #563, 3f2bd7c, 2026-07-07) — newly added exercises flash and scroll into view; useInitialPlanState.getDateForDay fixed so day 1 is today rather than tomorrow, and dates are formatted locally instead of via toISOString, which had shifted them by a day in negative-UTC-offset zones.
See HEP Builder for the user-facing description.
Summary¶
A clinician-facing builder for creating and editing a patient's Home Exercise Program (HEP). The goal: as fast as writing exercises on paper, as structured as software — "this one a couple times a week, this one every day, twice a day" — while keeping the software powers (multi-session days, videos, library, RTM).
The builder is a full-screen Sheet with a persistent exercise library palette and three synced projections of one canonical store:
- List view — per-exercise "prescription" rows: tap/typeable dose steppers, Daily / Weekdays / M·W·F / Custom cadence presets, a 1× / 2× / 3× per day control, inline + Note, color-coded blocks.
- Week view — a 7-day grid with AM/PM/3rd session lanes; drag from the palette into a day/lane.
- Split view (G) — on ≥1536px screens, List + a live Week mirror side-by-side (the original "Studio" concept): edit a row on the left, watch the week update on the right. Default at that size; falls back to the List/Week toggle below it.
An AI assistant (G) docks as a right-hand column: seeded from the AI-notes step (notes + attachments + plan summary), tenant-gated guidelines picker, and the canvas as its stage — streaming preview while generating, old→new PlanComparisonView on proposal. The create flow always starts at the AI-notes step; "Skip & create manually" opens the builder blank, so AI assists but never blocks.
Architecture (the key bet — unchanged since C)¶
Canonical store = the existing day-indexed Maps; cadence is a pure projection on top.
useEditPlanState / useInitialPlanState store the plan as workouts: Map<dayNumber, DayWorkout> + extraSessions; deriveRows groups instances by identical editable fields into Prescription Rows with a derived schedule; useCadenceRows translates row edits back into Map mutations. Create/edit/save/load/dirty-tracking/started-day locking all come from the existing hooks; cadence is frontend-only — no new DB columns. (Details: lib/utils/cadence/*, lib/hooks/useCadenceRows.ts.)
G adds three additive projection layers, none of which touch the canonical store:
- Media hydration — AI/template exercises arrive as id+name only;
useExerciseDetails+enrichExerciseWithMediaproject enriched copies into the surface (create mode, or once the assistant has been used) so thumbnails render; media is not part ofgroupKeyand save strips it. - AI assistant — reuses
useAIPlanEdit(same hook as the classic modals); accept writes proposed days viaplan.addDayWorkout, skipping locked days, preserving the AI's per-day assignment. - RTM time tracking —
useRtmTimeTrackingrecords the same billing-relevant sessions the classic modals do.
How G was decided (the variant program)¶
Six parallel variants were built on merged C and reviewed side-by-side. What each contributed / where each lost:
| Variant | Adopted into G | Rejected |
|---|---|---|
| A | Custom as an explicit 4th cadence segment; auto-open assistant when AI-seeded | Always-on media hydration; dead onRetryAI; chip-click bubbling |
| B | useNewBuilderPref concept; RequestConnection (onboarding) entry |
Palette Library/AI mode toggle (hid scope/tags); "apply to every day" accept checkbox; no-clone seed; unseeded chat; no hydration |
| C | Library-faithful palette AI search (in-grid AiSearchButtonCard + ring + banner); "2×/day" label; chip stopPropagation |
Palette hidden whenever AI open; no pref persistence |
| D | Slide-in panel's clean chip handling | Overlay embodiment (blocks the plan while chatting); summary-only chat seed |
| E | Docked AI column + canvas-as-stage; mobile close-on-send + AIMessageBanner; actualDate→focus type fix at the source |
Flatten-to-Daily AI seeding (discarded the AI's periodization); chip-click bubbling |
| F | Foundation: conditional media hydration, RTM tracking, useSyncExternalStore pref, fullest onboarding wiring, per-day-preserving fromAiPlan, Custom segment, palette-hosted Templates |
Overlay panel (superseded by E's column) |
All six agreed on (adopted): AI-notes-first create flow, clickable week chips → detail dialog, inline custom day-dots, "2×/day" wording, palette AI search, useCadenceRows.addInstances bulk op for templates.
G changes by file¶
| File | Δ | What changed |
|---|---|---|
hep-builder/HepBuilder.tsx |
+832/−~120 | AI column (guidelines picker, notes-seeded chat, auto-open on seed); canvas stage (streaming / comparison takeover, palette steps aside); split view (viewMode: list\|week\|split, live matchMedia 1536px gate, default split at 2xl); media hydration; RTM; armed add-target; destination toasts for click and drag adds; templates fetch guard + error toast |
hep-builder/ExercisePalette.tsx |
+178 | AI search (shared library components, ring on AI matches, banner); Templates button (mobile-sheet-reachable); search autofocus (desktop); Enter adds top match (falls through to AI search on no match); armed-target context chip ("Adding to Wed · PM ✕"); feedback moved to the builder (no double toasts); 36px add overlay on touch |
hep-builder/PrescriptionRowCard.tsx |
+141/−~90 | Explicit Custom cadence segment + inline day dots (shared renderDaysBlock); name no longer hard-capped at 128px (flex-1 line-clamp-2); drag handle aligned to thumbnail height (64px grab target); 2×/day enabled on mixed rows (no active chip + tooltip; tap unifies); remove moved to the card footer as a trash icon (was top-right, crowding the Reps/Time toggle) |
hep-builder/SessionLane.tsx |
+51 | Chip remove visible on touch (md:-only hover reveal) with a 32px hit area; chip click → details with stopPropagation; empty/"+ session" hints are now buttons that arm the add target |
hep-builder/WeekView.tsx / DayColumn.tsx |
+24/+8 | onAddHere threading; compact mirror wraps the week to two rows (4+3) so days are ~2× wider in split view; day pills py-2 |
hep-builder/controls/* (5 files) |
+40 | Touch-target pass: Stepper buttons w-10 md:w-8, Segmented xs/sm grow on mobile, DayDots 40px, ScalePicker 36px; Segmented gains title; AmPmToggle gains mixed; "AM·PM" → "2×/day" (ru "2×/день") |
lib/hooks/useCadenceRows.ts |
+38 | addInstances(templates, schedule?) bulk insert (single write per session — safe where an addRow loop would clobber itself); day defaults use shared PLAN_DAYS |
lib/hooks/useNewBuilderPref.ts |
new (49) | Persisted beta preference — localStorage key oktapt.useNewBuilder, default false, SSR-safe (starts false, hydrates on mount). Synced by a custom oktapt:useNewBuilder window event (same-tab, multiple consumers) plus the native storage event (cross-tab). Implemented with useState + useEffect, not useSyncExternalStore as the variant-F row above describes |
lib/utils/cadence/fromAiPlan.ts |
new (32) | AI plan → canonical Maps, preserving per-day assignment (clones instances) |
lib/hooks/useAIInitialPlanGeneration.ts |
2 | Type fix at the source: workoutsMap value actualDate → focus (was wrong at runtime) |
pages/doctor/dashboard.tsx |
+23 | Persistent pref; create always starts at the notes step; remount key={create-…-ai\|blank}; passes aiGeneratedPlan/userPromptNotes/userAttachments; skip-to-manual clears notes (stale-notes leak fix) |
components/RequestConnection.tsx |
+110 | Patient-onboarding entry: new builder behind the same pref, notes restored via initialNotes, notes/attachments forwarded to both builder branches; hook-order fix (early initialData return moved below hooks) |
public/locales/{en,ru}/common.json |
+19 each | view.split ("Split"/"Оба"), toast.addedDaily/addedTo, palette.addingTo/templates, templateAdded, templatesError, perDay.mixedHint; RU keeps fitness wording |
UX rough edges fixed (present in merged C, fixed in G)¶
- Week-chip remove was hover-only → unreachable on touch.
- "+ session" looked like a button but was drop-only → now clickable (arms the add target, opens the palette sheet on mobile).
- Click-add destination differed by view with no signposting; drag-adds were silent → unified destination toasts ("Added Squats — daily" / "Added Squats → Wed PM").
- "Custom" cadence had no selected state → explicit segment.
- 2×/day silently disabled on non-uniform rows → enabled with mixed state + unify-on-tap.
- Sub-40px tap targets throughout (steppers 32px, scale dots 28px, xs segments) → mobile-first size pass.
- Exercise names hard-truncated at 128px; magic
mt-5handle offset → proper flex layout. - Remove (X) crowded the Reps/Time toggle → trash icon in the card footer.
Out of scope (deferred) / known simplifications¶
- No missed-session roll-forward; cadence criteria still not persisted (compiled to sessions); no new DB columns.
- Week within-lane reorder doesn't enforce block order; cross-lane chip move still remove + re-add.
- Keyboard activation of week-chip details deferred (Enter/Space are dnd-kit's drag-lift keys on that element; needs a dedicated focusable control). Details remain keyboard-reachable via List view.
- Mobile dictation entry — future.
Open items¶
- ~~Adopt G: merge
HEP-EDITS-G→development~~ — done (PRs #559, #561); worktrees A–G still to clean up. - Flip the beta toggle default (or remove the classic modals) now that G has soaked.
- ~~Consider persisting cadence criteria + missed-session auto-roll as the next iteration.~~ — done 2026-09-29: Plan Scheduling Redesign.
Verification¶
tsc --noEmit: 0 errors; locale JSONs valid; EN/RUhepBuilder.*key parity.- External code review (2026-07-02): 6 findings fixed (templates error surfacing + in-flight guard, RequestConnection hook order, stale skip-to-manual notes,
PLAN_DAYSconstant, richer List-view detail payload, DayDots dedup); 2 rejected with rationale (RU "Оба" over "Сплит" — fitness-jargon collision; naive chiponKeyDownwould break dnd-kit keyboard drag). - Run: now on
developmentin the main checkout —npm run dev, then tick New plan builder (beta) on the doctor dashboard. Suggested checks: create via notes→AI (column auto-opens, per-day seed) and via skip (blank); split-view sync + drag into a day; "+ session" arming (desktop + mobile); Enter-to-add; mixed 2×/day unify; template inject (thumbnails via hydration); chip remove on a phone viewport; RU locale.