Skip to content

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)

  1. Add layer_state to case responseapp/routers/cases.py, get_case() endpoint. Add "layer_state": case.layer_state to the response dict, gated by triage_agent_v3 feature flag.

  2. Add scores endpointGET /api/v1/cases/{case_id}/scores returning PFS, HSS (if matched), FMS (if matched).

  3. Add LayerState type to shared-corepackages/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:

const useLayerData = caseData.layer_state != null && isFeatureEnabled("wave2_layer_ui");

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

File: apps/patient-app/src/components/ProgressStrip.tsx (rewrite)

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):

File: packages/shared-web/src/components/ui/completion-ring.tsx (new)
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-300 line 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

File: apps/patient-app/src/components/chat/LayerSummaryCard.tsx (new)

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-4 with 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 AlertTriangle icon + text
  • Correction prompt: subtle slate-100 background 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:

case "layer_summary":
  return <LayerSummaryCard {...content.layer_summary} />;

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

File: apps/patient-app/src/components/panels/SummaryPanel.tsx (rewrite)

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 Collapsible from 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 in slate-900)
  • Missing fields: slate-400 italic "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):

File: packages/shared-web/src/components/ui/score-bar.tsx (new)
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:

CRW-247  Aisha M.  TKR  PFS: 78 ● Conditionally ready  HSS: 86  FMS: 72

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

wave2_layer_ui: false   # Enable layer-based UI (progress rail, summary, cards)

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

  1. Layer completion threshold: Use the backend status field ("complete" when layer crosses its threshold), not the raw percentage. Thresholds are defined in LAYER_COMPLETE_THRESHOLDS in app/services/layer_state.py.

  2. Logistics + Financial merge: YES — merge financial_readiness + logistics into a single "Planning" display group. Backend tracks separately (for extraction accuracy), UI shows 4 steps. Completion = average of both layers.

  3. Score visibility: PFS shown in SummaryPanel after medical_status.status === "complete". HSS/FMS shown only in MatchResults (null/hidden until matching runs).

  4. Correction flow: Plain text — "Anything I got wrong? You can correct me anytime — just tell me in chat." No special correction UI.

Open Questions

  1. RTL layout: .impeccable.md mentions RTL support for Arabic. The progress rail, score bars, and 2-column fields all need RTL variants. Defer to implementation — Tailwind's rtl: modifier handles most cases. Flag any component that needs explicit RTL treatment during implementation.

  2. 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.