Wave 2 — Patient UI Redesign Implementation Spec¶
Status: Partially implemented — design system + 8 components bootstrapped in Session 46; full patient UI wiring in progress
Prerequisites: Wave 1 Intake Restructuring (complete, feature-flagged triage_agent_v3)
Frontend repo: curaway-health-navigator (monorepo, apps/patient-app/)
Design system: .impeccable.md (Teal #008B8B, Coral #FF7F50, Deep Ocean #004D4D, Montserrat + Crimson Pro)
Estimated effort: 2–3 weeks
Scope¶
4 P1 components that surface Wave 1's layer-based intake model in the patient UI. All behind feature flag wave2_layer_ui.
| # | Component | Current File | Change Type |
|---|---|---|---|
| 2.5 | Progress Rail Redesign | ProgressStrip.tsx |
Rewrite |
| 2.6 | Layer Summary Cards | (new) | New component |
| 2.7 | Summary Panel Redesign | panels/SummaryPanel.tsx |
Rewrite |
| 2.8 | PFS/HSS/FMS Score Display | MatchResults.tsx |
Enhancement |
Data Contract¶
All 4 components consume the same backend data. Backend prerequisite: layer_state and scores are not yet exposed in the API — the changes below must land before frontend work begins.
Backend Changes Required (before Week 1)¶
-
Add
layer_stateto case response —app/routers/cases.py,get_case()endpoint. Add"layer_state": case.layer_stateto the response dict, gated bytriage_agent_v3feature flag. -
Add scores endpoint —
GET /api/v1/cases/{case_id}/scoresreturning PFS, HSS (if matched), FMS (if matched). -
Add
LayerStatetype to shared-core —packages/shared-core/src/types/api.ts.
Layer State (from GET /api/v1/cases/{id} → case.layer_state)¶
Backend layer names (from app/services/layer_state.py LayerName enum):
// packages/shared-core/src/types/api.ts
/** Backend layer keys — match LayerName enum in app/services/layer_state.py */
type LayerKey = "intent_capture" | "medical_status" | "travel_readiness" | "logistics" | "financial_readiness";
/** Display labels for patient UI — map from backend keys */
const LAYER_DISPLAY: Record<LayerKey, string> = {
intent_capture: "About You",
medical_status: "Health Profile",
travel_readiness: "Travel Readiness",
financial_readiness: "Planning", // Merges financial + logistics for patient display
logistics: "Planning", // Same display group
};
interface LayerState {
intent_capture: LayerData;
medical_status: LayerData;
travel_readiness: LayerData;
logistics: LayerData;
financial_readiness: LayerData;
}
interface LayerData {
completion: number; // 0.0–1.0 (backend field name: "completion", not "completeness")
status: "not_started" | "in_progress" | "complete";
data: Record<string, any>; // Backend field name: "data", not "fields"
summary_card_fired: boolean;
}
Note: The UI merges financial_readiness + logistics into a single "Planning" display group. The backend tracks them separately (for extraction), but the patient sees 4 steps, not 5.
Scores (from GET /api/v1/cases/{case_id}/scores — new endpoint)¶
// New endpoint response type
interface CaseScores {
pfs: { score: number; band: "ready" | "conditionally_ready" | "needs_attention" | "not_ready"; gaps: string[] } | null;
hss: { score: number; dimensions: Record<string, number> } | null; // null until matching complete
fms: { score: number; factors: Record<string, number> } | null; // null until matching complete
}
Null/Missing Handling¶
When triage_agent_v3=false (backend flag off), layer_state is null. All 4 components
must check for null and fall back to the legacy workflow_state-based UI. This is enforced
by the wave2_layer_ui feature flag check:
2.5 — Progress Rail Redesign¶
Current State¶
ProgressStrip.tsx — 8 binary steps mapped from case.workflow_state flags. Step dots are ✓/○ with a vertical progress line. Hardcoded step definitions.
Target State¶
Layer-based progress with completion rings. 8 steps grouped into two zones:
INTAKE (Layers) POST-INTAKE (Binary)
───────────────── ────────────────────
◉ About You 100% ○ Finding Matches
◉ Health Profile 70% ○ Compare & Choose
○ Travel Ready 0% ○ Next Steps
◑ Planning 20%
Component Design¶
Step definition structure:
interface ProgressStep {
id: string;
label: string;
icon: LucideIcon;
type: "layer" | "merged_layer" | "binary";
// For layer steps:
layerKeys?: LayerKey[]; // One or more backend layer keys
// For binary steps:
workflowKey?: string;
}
const STEPS: ProgressStep[] = [
// Layer-based (intake) — 4 display steps from 5 backend layers
{ id: "about_you", label: "About You", icon: User, type: "layer", layerKeys: ["intent_capture"] },
{ id: "health_profile", label: "Health Profile", icon: Heart, type: "layer", layerKeys: ["medical_status"] },
{ id: "travel_readiness", label: "Travel Ready", icon: Plane, type: "layer", layerKeys: ["travel_readiness"] },
{ id: "planning", label: "Planning", icon: Calendar, type: "merged_layer", layerKeys: ["financial_readiness", "logistics"] },
// Binary (post-intake)
{ id: "finding_matches", label: "Finding Matches", icon: Search, type: "binary", workflowKey: "matching_complete" },
{ id: "compare_choose", label: "Compare & Choose", icon: BarChart3, type: "binary", workflowKey: "providers_selected" },
{ id: "next_steps", label: "Next Steps", icon: ArrowRight, type: "binary", workflowKey: "consent_given" },
];
// For merged_layer type, completion = average of constituent layers
function getMergedCompletion(layerState: LayerState, keys: LayerKey[]): number {
return keys.reduce((sum, k) => sum + layerState[k].completion, 0) / keys.length;
}
Completion Ring (reusable):
interface CompletionRingProps {
percentage: number; // 0–100
size?: "sm" | "md" | "lg"; // 24px | 36px | 56px
showLabel?: boolean; // Show % text in center
color?: string; // Override ring color
}
Visual spec:
- SVG circle with strokeDasharray animation (existing pattern from EHRPanel)
- Size sm (24px): Used in collapsed rail step dots. No center label.
- Size md (36px): Used in expanded rail. Center label shows percentage.
- Size lg (56px): Used in EHR panel header (replaces existing ring).
- Ring colors by completion band:
- 0%: slate-200 (empty)
- 1–49%: amber-400 (needs attention)
- 50–89%: #008B8B teal (in progress)
- 90–100%: emerald-500 (nearly/fully complete)
- Fill animation: transition: stroke-dashoffset 800ms ease-out
- Completion flash: brief scale(1.05) pulse when reaching 100%
Collapsed Rail (56px wide):
[Logo]
[New Case]
────────
[◉] ← CompletionRing sm, About You
|
[◑] ← CompletionRing sm, Health Profile (70%)
|
[○] ← Empty ring, Travel Ready
|
[◑] ← CompletionRing sm, Planning (20%)
─ ─ ─ ← Zone separator (dashed)
[○] ← Gray dot, Finding Matches
|
[○] ← Gray dot, Compare & Choose
|
[○] ← Gray dot, Next Steps
────────
[v0.2.5]
[Avatar]
- Vertical progress line: solid teal up to last completed layer, dashed after
- Collapsed ring tooltips on hover: "Health Profile — 70%"
- Zone separator: 1px dashed
slate-300line between layer and binary zones
Expanded Rail (280px):
┌─────────────────────────────┐
│ YOUR JOURNEY │
├─────────────────────────────┤
│ ◉ About You 100% │
│ Case type: Elective │
│ Primary: Knee replacement │
│ │
│ ◑ Health Profile 70% │
│ 3 of 5 docs uploaded │
│ 2 conditions identified │
│ │
│ ○ Travel Readiness 0% │
│ Not started │
│ │
│ ◑ Planning 20% │
│ Budget range provided │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ ○ Finding Matches │
│ ○ Compare & Choose │
│ ○ Next Steps │
└─────────────────────────────┘
- Each layer step: CompletionRing md + label + percentage + 1–2 line description
- Description is data-driven (same pattern as current ProgressStrip descriptions)
- Binary steps: simple dot + label (same as current)
Mobile:
- Collapsed: horizontal bar at top, 4 layer rings + 3 binary dots
- Tap to expand: bottom sheet with full step list
- Current step highlighted with teal background
Feature Flag Behavior¶
wave2_layer_ui = true → New layer-based rail (reads case.layer_state)
wave2_layer_ui = false → Current 8-step binary rail (reads case.workflow_state)
Both code paths coexist in the same file. The flag selects which step definitions and data source to use.
2.6 — Layer Summary Cards (In-Chat)¶
Current State¶
No equivalent. Chat has rich content cards (match_results, file_request, consent_form, document_checklist) but no layer completion cards. Rich cards are rendered by RichContentRenderer.tsx based on content_type field in message metadata.
Target State¶
New layer_summary rich content card type. Rendered inline in chat when the backend detects a layer has been completed or significantly updated.
Component Design¶
Card structure:
┌──────────────────────────────────────────────────┐
│ [Heart] Health Profile Complete 100% │
│ │
│ Diagnosis │
│ Bilateral knee osteoarthritis (ICD M17.11) │
│ │
│ Comorbidities │
│ Prediabetes (HbA1c 6.3%) · Low risk │
│ │
│ Documents │
│ 3 of 5 mandatory uploaded │
│ ⚠ CBC and Chest X-ray still needed │
│ │
│ Risk Assessment │
│ ✓ No blocking risk factors │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Anything I got wrong? You can correct me │ │
│ │ anytime — just tell me in chat. │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
Props:
interface LayerSummaryCardProps {
layerKey: LayerKey; // "intent_capture" | "medical_status" | "travel_readiness" | "financial_readiness" | "logistics"
layerLabel: string; // Display name: "Health Profile"
completion: number; // 0.0–1.0
status: "in_progress" | "complete";
findings: LayerFinding[]; // Key-value summaries
warnings: string[]; // Missing items, risk factors
icon: LucideIcon;
}
interface LayerFinding {
category: string; // "Diagnosis", "Comorbidities", "Documents"
items: string[]; // Displayable lines
status?: "ok" | "warning" | "critical";
}
Visual spec:
- Border:
border-l-4with layer color: - About You:
teal-500 - Health Profile:
rose-400 - Travel Readiness:
sky-400 - Planning:
amber-400 - Logistics:
violet-400 - Header: Icon + layer name + CompletionRing sm
- Complete state: green checkmark badge, full teal ring
- In-progress state: percentage shown, partial ring
- Findings: grouped by category, compact key-value layout
- Warnings: amber
AlertTriangleicon + text - Correction prompt: subtle
slate-100background box with italic text - Max width: matches chat column (780px)
- Cards do NOT stack — if 2 layers complete on same turn, render 2 cards with 8px gap
Integration:
Add layer_summary to RichContentRenderer.tsx switch:
The backend already sends rich content via content_type in message metadata. The triage agent will emit layer_summary cards when a layer reaches status: "complete" or crosses a completeness threshold (e.g., 50%).
2.7 — Summary Panel Redesign¶
Current State¶
panels/SummaryPanel.tsx — 3 cards (Patient Information, AI Observations, Provider Brief). Data from EHR snapshot and case data. Flat list of key-value pairs.
Target State¶
Layer-organized dashboard that mirrors the progress rail layers. Replaces all 3 existing cards with a single layered view.
Component Design¶
Layout:
┌───────────────────────────────────────┐
│ Case Summary [↻] │ ← Refresh button
├───────────────────────────────────────┤
│ │
│ ABOUT YOU 100% │ ← Section header + ring
│ ┌───────────────────────────────────┐ │
│ │ Case type Elective │ │
│ │ Primary Mobility limit. │ │
│ │ Emotional Moderate anxiety │ │
│ │ Role Self (patient) │ │
│ └───────────────────────────────────┘ │
│ │
│ HEALTH PROFILE 70% │ ← Collapsible
│ ┌───────────────────────────────────┐ │
│ │ Diagnosis Bilateral knee OA │ │
│ │ Comorbidities Prediabetes │ │
│ │ Risk level Low │ │
│ │ Documents 3 of 5 uploaded │ │
│ │ ⚠ 2 items still needed │ │
│ └───────────────────────────────────┘ │
│ │
│ TRAVEL READINESS 0% │ ← Collapsed (empty)
│ ┌───────────────────────────────────┐ │
│ │ Not started — we'll ask about │ │
│ │ this when your health profile │ │
│ │ is further along. │ │
│ └───────────────────────────────────┘ │
│ │
│ PLANNING 20% │
│ ┌───────────────────────────────────┐ │
│ │ Budget $6K–$10K (OOP) │ │
│ │ Passport Not yet provided │ │
│ │ Dates Not yet provided │ │
│ └───────────────────────────────────┘ │
│ │
│ ── SCORES ── │ ← After intake complete
│ PFS 78% ● Conditionally ready │
│ HSS -- ○ Pending matching │
│ FMS -- ○ Pending matching │
│ │
└───────────────────────────────────────┘
Section component:
interface SummarySectionProps {
layerKey: string;
label: string;
icon: LucideIcon;
completeness: number;
status: "not_started" | "in_progress" | "complete";
fields: SummaryField[];
warnings?: string[];
defaultExpanded?: boolean;
}
interface SummaryField {
label: string;
value: string | null;
status?: "ok" | "warning" | "missing";
}
Visual spec:
- Each section: collapsible with
Collapsiblefrom shadcn/ui - Section header: layer icon + name + CompletionRing sm + chevron
- Default expanded: layers with
status !== "not_started" - Empty state: italic text explaining when this layer activates
- Fields: 2-column grid (label left in
slate-500, value right inslate-900) - Missing fields:
slate-400italic "Not yet provided" - Warnings: amber background strip below the section
- Scores section: only visible when
layer_state.health_profile.status === "complete" - PFS band colors:
- Ready:
emerald-500 - Conditionally ready:
amber-500 - Needs attention:
orange-500 - Not ready:
red-500
Feature flag behavior:
wave2_layer_ui = true → Layer-organized summary (reads case.layer_state)
wave2_layer_ui = false → Current 3-card summary (reads EHR snapshot)
2.8 — PFS / HSS / FMS Score Display¶
Current State¶
MatchResults.tsx — shows a single total_score (0–1) per provider with a radar chart of 6 dimensions. No PFS/HSS/FMS breakdown.
Target State¶
Three named scores per match, plus PFS inline in the summary panel and coordinator queue.
Component Design¶
Score Bar (reusable):
interface ScoreBarProps {
label: string; // "Hospital Match", "Your Readiness", "Overall Fit"
score: number; // 0–100
color?: string; // Override bar color
band?: string; // "Ready", "Conditionally Ready" — shown as badge
compact?: boolean; // Coordinator queue mode (label + score inline)
}
Visual:
- Full mode: label on left, bar in center, percentage on right
- Bar: 4px height, rounded-full, slate-100 background, colored fill with transition-all duration-500
- Color by score: 0–40 red, 41–60 amber, 61–80 teal, 81–100 emerald
- Band badge (if provided): small pill after the percentage
- Compact mode: "PFS: 78 ● Conditionally ready" — single line, dot-colored by band
Match Card Enhancement:
In MatchResults.tsx, replace the single total_score badge with 3 score bars:
┌──────────────────────────────────────────────────┐
│ #1 Acibadem Maslak Hospital │
│ Istanbul, Turkey │
│ │
│ [Radar Chart] Hospital Match ████████░░ 86%│
│ Your Readiness ██████░░░░ 78%│
│ Overall Fit █████░░░░░ 72%│
│ │
│ ⚠ Passport timeline tight │
│ ✓ Clinical fit: Excellent │
│ ✓ Within budget │
│ │
│ [View Details] [Why this match?] │
└──────────────────────────────────────────────────┘
Patient-friendly labels:
| Technical | Patient-facing | Source |
|---|---|---|
| HSS | Hospital Match | How well this hospital fits your case |
| PFS | Your Readiness | How ready you are for this journey |
| FMS | Overall Fit | Combined match strength |
PFS gap surfacing:
When PFS has gaps, show them inline below the score bar:
Your Readiness ██████░░░░ 78% ● Conditionally ready
└ Passport expires in 3 months — confirm renewal
└ CBC test needed (within 90 days)
Gaps are expandable (collapsed by default, show first gap + "+N more").
Coordinator queue display (future — Phase 1 UI):
Compact ScoreBar:
This is specced here for the shared component but implemented in coordinator-app during Phase 1.
New Shared Components¶
These go in packages/shared-web/ for reuse across patient, coordinator, and provider apps:
| Component | Package | Path | Used by |
|---|---|---|---|
CompletionRing |
shared-web |
components/ui/completion-ring.tsx |
ProgressStrip, SummaryPanel, LayerSummaryCard, EHR |
ScoreBar |
shared-web |
components/ui/score-bar.tsx |
MatchResults, SummaryPanel, Coordinator queue |
LayerSummaryCard |
patient-app |
src/components/chat/LayerSummaryCard.tsx |
Chat rich content (patient-app only) |
Implementation Order¶
Week 0 (backend, ~half day):
- Add layer_state to GET /api/v1/cases/{id} response (gated by triage_agent_v3)
- Add GET /api/v1/cases/{id}/scores endpoint (PFS/HSS/FMS)
- Add LayerState + CaseScores types to packages/shared-core/src/types/api.ts
Week 1:
Day 1: CompletionRing + ScoreBar shared components + tests
Day 2: ProgressStrip rewrite (layer steps + feature flag)
Day 3: ProgressStrip mobile layout + animations
Week 2:
Day 1: LayerSummaryCard component + RichContentRenderer integration
Day 2: SummaryPanel rewrite (layer sections + feature flag)
Day 3: PFS/HSS/FMS in MatchResults + gap display
Week 3:
Day 1: Integration testing with live backend (triage_agent_v3=true)
Day 2: Mobile responsive polish + animation refinement
Day 3: Cross-browser testing + accessibility audit
Loading & Error States¶
| Component | Loading State | Error State | Empty State |
|---|---|---|---|
| ProgressStrip | Skeleton rings (pulse animation) | Falls back to legacy binary rail | N/A (always has data) |
| LayerSummaryCard | N/A (rendered from existing data) | Omit card if data malformed | Show card with "No details yet" if findings is empty |
| SummaryPanel | Skeleton rows per section | "Unable to load summary" inline alert | Per-section: italic "Not started yet" with explanation |
| Scores (PFS) | "Calculating..." with spinner | "Score unavailable" in slate-400 | Hidden until intake_complete |
| Scores (HSS/FMS) | Hidden until matching complete | "Score unavailable" | Hidden |
Animation Spec¶
Per .impeccable.md: subtle, purposeful, never decorative.
| Element | Animation | Duration | Easing |
|---|---|---|---|
| Completion ring fill | stroke-dashoffset |
800ms | ease-out |
| Ring color change | transition: stroke |
300ms | ease-in-out |
| Layer complete flash | scale(1) → scale(1.05) → scale(1) |
400ms | ease-out |
| Summary section expand | max-height: 0 → 500px |
200ms | ease-out |
| Score bar fill | width: 0 → N% |
500ms | ease-out (on mount) |
| Layer card enter (chat) | opacity: 0 → 1, translateY: 8px → 0 |
300ms | ease-out |
| PFS gap expand | max-height: 0 → 200px |
150ms | ease-out |
Implementation note: CSS cannot animate height: auto. Use max-height with a sufficiently large value, or use shadcn/ui Collapsible component (already in shared-web) which handles this with data-state attributes. Do NOT add framer-motion as a dependency.
No bounce. No parallax. No decorative motion.
Feature Flag¶
Single flag controls all 4 components. When off, current UI renders unchanged.
Dependency: requires triage_agent_v3=true on the backend (Wave 1). If backend flag is off, layer_state will be null and UI falls back to workflow_state automatically.
Test Plan¶
| Area | Test Type | Count (est.) |
|---|---|---|
| CompletionRing | Vitest + RTL | 8–10 |
| ScoreBar | Vitest + RTL | 6–8 |
| ProgressStrip (layer mode) | Vitest + RTL | 10–12 |
| ProgressStrip (legacy mode) | Vitest + RTL | 5 (regression) |
| LayerSummaryCard | Vitest + RTL | 8–10 |
| SummaryPanel (layer mode) | Vitest + RTL | 10–12 |
| SummaryPanel (legacy mode) | Vitest + RTL | 5 (regression) |
| MatchResults (scores) | Vitest + RTL | 8–10 |
| Feature flag toggle | Vitest | 4 |
| Mobile responsive | Playwright | 5–8 |
| Total | ~70–90 |
Resolved Decisions¶
-
Layer completion threshold: Use the backend
statusfield ("complete" when layer crosses its threshold), not the raw percentage. Thresholds are defined inLAYER_COMPLETE_THRESHOLDSinapp/services/layer_state.py. -
Logistics + Financial merge: YES — merge
financial_readiness+logisticsinto a single "Planning" display group. Backend tracks separately (for extraction accuracy), UI shows 4 steps. Completion = average of both layers. -
Score visibility: PFS shown in SummaryPanel after
medical_status.status === "complete". HSS/FMS shown only in MatchResults (null/hidden until matching runs). -
Correction flow: Plain text — "Anything I got wrong? You can correct me anytime — just tell me in chat." No special correction UI.
Open Questions¶
-
RTL layout:
.impeccable.mdmentions RTL support for Arabic. The progress rail, score bars, and 2-column fields all need RTL variants. Defer to implementation — Tailwind'srtl:modifier handles most cases. Flag any component that needs explicit RTL treatment during implementation. -
Mobile breakpoints: The mobile layout for ProgressStrip (horizontal bar + bottom sheet) needs detailed design. Defer to Week 3 (mobile polish phase) — desktop-first implementation in Weeks 1-2.