# Project export: Cadence

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Cadence is a proactive AI companion monitoring high-risk pregnancies between doctor visits—catching complications before they become emergencies, and giving doctors a guided-context before they meet.
- Devpost: https://devpost.com/software/cadence-rt1hgp
- GitHub: https://github.com/luissuarez21/cadence-care
- Demo: https://cadence-patient.vercel.app/
- Video: https://www.youtube.com/embed/CkDH2Ww55TA?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Claude Sonnet 4.6 (79 commits), aditchintamaneni (67 commits), Luis Suarez (38 commits)

## Devpost submission (written by the team)

### Inspiration

Our motivation for creating Cadence came from personal experience and loss. Members in our team have family and friends who have experienced pregnancy with complications, as well as pregnancies that turned into tragedy. At the time (and even now) we didn't know every clinical detail, any early warning signs were, and what could have been caught or acted on sooner. What we do know all too well is what came after: not only physical recovery, but emotional impact that the family and us carry to this day. From our research, we realized that this situation isn't all that rare. In the US, roughly 1 in 5 pregnancies involves a complication like preeclampsia, gestational diabetes, or preterm birth, and an estimated 6–8% are formally classified as high-risk - which translates to around 30,000 to 50,000 women a year. For every death related to pregnancy, there are 70 to 80 more women who survive a near-death event tied to pregnancy complications. This is the population we decided to build Cadence for because such an event is not a rare tragedy. If science, data, and intelligence can but a structural blind spot affecting hundreds of thousands of families a year. The pattern is almost always the same: the warning signs were there: rising blood pressure, recurring headaches, sudden swelling; but a high-risk patient sees her OB once every two weeks and has nothing in between. No structured check-in. No one watching. That 10–14 day silence is where preventable harm happens.

### What it does

Cadence is a proactive AI companion that watches high-risk pregnancies in the gap between doctor visits—catching complications before they become emergencies, and handing doctors a complete picture before they walk in. Our one engine is able to face both the patient and their OB team, allowing for more informed check ins and a wholistic view of the pregnancy process. For the patient, Cadence is a warm, daily check-in which avoids the stereotypical sterile and anxiety inducing medical dashboard. Our patient reports her blood pressure, symptoms, and how she's feeling in plain conversation. Cadence shows her a personalized "Things to Watch For" card pulled specifically from her own care plan, her symptom history so far ("9 check-ins completed, no red flags"), and a "Take to Your Appointment" summary she can walk in with to better address any concerns a pregnant mother may forget out of stress. If something crosses a threshold, she's never shown a scary alert, but a notification that her doctor has been notified. For the OB, Cadence is a briefing tool that turns a 15-minute appointment into one where the doctor already knows what has happened. A risk-ranked patient panel surfaces the 3 patients out of 50 who need attention right now. Pattern detection will flag trends a human might miss across a long dialogue of messages. For example: "BP trending up over 4 days, headaches mentioned 3 of the last 9, increasing frequency." Every risk score comes with a plain-English rationale and suggestions for discussion, so that before the doctor even opens their charts, Cadence suggests conversation starters like "Ask about headache severity. Consider a 24-hour urine protein test." When a real red flag appears, Cadence generates a structured clinical summary and pushes a zero-PHI alert straight to the clinician's browser. This kind of support would usually take 12 days, but now it can come to fruition in the same night.

### How we built it

Our team built one engine, two faces, with three people. Claude (Anthropic) is the brain of Cadence: it ingests a care plan from a photo or PDF using vision and turns it into structured protocol JSON, runs the daily check-in conversation through a tool-use loop, classifies risk against a clinical rubric, detects multi-day patterns, and generates both the patient- and clinician-facing visit summaries. Redis holds everything stateful: session history, symptom logs, the risk timeline, and a RAG vector store over each patient's specific care plan—so every answer Cadence gives is grounded in her document, not a generic guess. Arize Phoenix traces every single agent decision, and an LLM-as-judge eval runs on every escalation, scoring whether it was clinically appropriate—so nothing about the system is a black box. FastAPI + Pydantic power the backend; Vite + React + TanStack Router + Tailwind power two distinct frontends—a warm, wellness-app-style patient experience and a clean, clinical dashboard for the OB. Web Push delivers the escalation alert to the clinician's browser with zero protected health information in the payload. Before writing a line of code, we agreed on every data schema and API endpoint as a team, then split the work by layer rather than by feature—one person owned the AI brain (prompts, condition packs, risk logic, evals), one owned backend infrastructure (FastAPI, Redis, the agent loop), and one owned both frontends and demo execution. That meant nobody was ever blocked waiting on someone else.

### Challenges we ran into

We spent a lot of time thinking through catching issues vs. calming patients. The patient can never see a scary alert which would cause unnecessary anxiety, but the clinician can never miss a real signal. This kind of tension is exacerbated by the fact that it's the exact same underlying data informing both of these 'types' of actions we have to take. We had to design two completely different emotional treatments of one risk score. We also aren't 100% health experts. We know that we aren't allowed to directly diagnose patients, which is a complicated ask of a medical support platform. We decided upon the process that Cadence prepares and collects data, but that ultimately the doctor decides. We hard-blocked diagnostic language at the system-prompt level and kept a licensed clinician as the only one who makes the actual call, even when the AI flags something serious. Making a non-deterministic system demo-able. Live LLM calls don't always say the same thing twice, which is a problem on a stage with a clock running. We built a "golden path" of pre-cached responses so our core demo flow is fast and reliable every single time, with live calls still running underneath for everything outside that path. Saying no to our own roadmap. The platform vision is large—more condition packs, voice check-ins, EHR integration. We had to keep cutting back to one condition, one check-in loop, one escalation flow, and two views, and trust that a focused demo beats a broad one.

### Accomplishments we're proud of

A full closed loop, start to finish: a care plan goes in as a photo, a patient has a real conversation, a risk is detected, a clinician gets a structured alert, and a doctor walks into the room already briefed — all on real data, no mocks. An AI system that's auditable, not a black box: every agent decision is traced in Arize, and every escalation is independently scored by a second model call before a human ever sees it. A product that treats two very different users with two very different needs—without duplicating the underlying intelligence that powers both. Getting the emotional tone right. "Things to Watch For" reads like care, not a warning label. The patient sees support; the doctor sees signal. Doing the homework to ground this in reality rather than vibes—the billing pathway (Medicaid RPM CPT codes), the market precedent (Babyscripts), and the actual prevalence numbers behind "high-risk pregnancy" all shaped the product, not just the pitch.

### What we learned

The hardest part of building in healthcare isn't model capability — it's restraint. A system that can catch real danger without ever frightening the person it's monitoring takes more careful prompt design than raw intelligence. Grounding beats cleverness. RAG over a patient's own care plan made every Cadence response traceable to a real source, and that traceability is what actually makes a system trustworthy to a clinician. Trust has to be built, not claimed. Saying "you can audit this" means nothing without an actual audit trail—that's what pushed us to wire up tracing and an LLM-as-judge eval on every single escalation rather than just describing the idea. A surprising share of the work was non-technical: understanding what "high-risk pregnancy" actually means clinically, learning the real prevalence numbers instead of assuming, and figuring out what an OB needs to see in the first 15 seconds of an appointment.

### What's next

A second condition pack on gestational diabetes to prove the platform generalizes beyond one diagnosis to any structured care plan. This would allow generalizability across the many different possible cases in healthcare. A real voice check-in option which takes place in the form of a friendly phone call, for older patients who'd rather talk than type. Direct EHR integration so a care plan never has to be manually uploaded. Clinical validation with a real OB or maternal health practice, moving from synthetic demo data to a pilot with real (consented, BAA-covered) patients. Expansion beyond pregnancy entirely—the same architecture (care plan in, daily companion out, clinician briefing on the other end) applies to any condition where the danger lives in the silence between appointments.

## README (from the GitHub repository)

# Cadence

**A proactive AI health companion for high-risk pregnancy — and the OB who cares for her.**

Cadence turns any OB care plan into a daily AI companion for the patient and a full-picture
briefing tool for the clinician. It lives in the silent 7–14 day gap between prenatal
appointments — where dangerous symptoms (rising blood pressure, recurring headaches, sudden
swelling) build undetected — catching problems early, never causing panic, and handing the
doctor a complete, traceable story before the conversation even starts.

---

## System Overview

Cadence is **two experiences sharing one engine**. A warm, mobile-first **Patient PWA** runs a
daily conversational check-in grounded in the patient's specific care plan; a clean
**Clinician Dashboard** ranks a whole panel by risk, surfaces between-visit trends, and receives
real-time escalations. Both talk to a single **FastAPI** backend that drives an **Anthropic
Claude** orchestrator.

Every patient message flows through Claude's tool-use loop: the agent logs structured symptoms,
runs RAG over the patient's own care document, assesses risk against the condition pack's red
flags, detects multi-day patterns, and — when thresholds are crossed — generates a structured
clinical summary, writes it to Redis, fires a zero-PHI Web Push to the clinician, and kicks off
an independent **Arize** LLM-as-judge evaluation of whether that escalation was appropriate.
Nothing is a black box: every agent decision is traced and every escalation is independently
scored.

The engine is condition-agnostic. What changes per condition is a JSON **condition pack** (the
demo runs preeclampsia risk); the orchestration, memory, and dashboard never change.

## Tech Stack

![Cadence Tech Stack](docs/cadence-tech-stack.png)

## How Cadence Works

```
OB care plan ─(Claude Vision)─► structured ProtocolJSON ─► Redis (plan + RAG vectors)
        │
Patient opens PWA ─► POST /api/chat/message
        │
Claude orchestrator (claude-sonnet-4-6, tool-use loop):
   lookup_plan ── RAG over the patient's care doc (grounded answers only)
   log_symptom ── structured check-in → Redis time-series
   assess_risk ── current readings vs. red flags → ok | monitor | escalate
   detect_pattern ─ trends over history ("BP up 4 days", "headaches 3 of 9")
   escalate_to_clinician ─► escalations:{id} + Web Push + Arize judge
   generate_visit_summary ─ patient + clinician pre-visit briefs
   schedule_followup ── clinician "book sooner" adjusts the next check-in
        │
Clinician Dashboard ─► panel (risk-ranked) · patient detail (timeline + patterns +
   briefing) · live escalation inbox over WebSocket · one-click actions
        │
Every turn + tool call ─► Arize span (de-identified) · every escalation ─► LLM-as-judge score
```

---

## Sponsors & How We Use Them

### 🟠 Anthropic Claude — the engine

Claude (`claude-sonnet-4-6`) is the core intelligence of Cadence, used in four distinct ways:

- **Agent orchestrator (tool-use loop).** `backend/agent/orchestrator.py` runs a streaming
  tool-use loop where Claude decides which of seven tools to call on each patient turn. The
  orchestrator injects `patient_id` **server-side on every dispatch** — the model never chooses
  whose data it touches, so cross-patient access is architecturally impossible.
- **Vision plan ingestion.** An uploaded OB care plan (PDF/image) is read by Claude Vision and
  parsed into a Pydantic-validated `ProtocolJSON` (goals, meds, tasks, check-in cadence, red
  flags). This is what makes Cadence work for *any* care plan, not a hardcoded protocol.
- **RAG-grounded conversation.** The `lookup_plan` tool does semantic search over the patient's
  own care document; Claude's answers are always traceable to that document — it never gives
  general medical advice it invented, and never diagnoses.
- **LLM-as-judge safety layer.** After every escalation, a **second independent Claude call**
  evaluates "was this escalation appropriate given the symptoms and the care plan's red flags?"
  and returns a confidence score — the auditable safety check behind every human handoff.

### 🔴 Redis — memory, RAG, and real-time state

Redis is the entire "between-visit continuity" layer — the thing that lets Cadence remember,
reason over time, and react instantly. It runs over **TLS (`rediss://`) with authentication**,
and **every key is namespaced by `patient_id`** so queries are always scoped to one patient.

- **Per-patient memory & time-series:** `plan:{id}` (parsed protocol), `session:{id}:{sid}`
  (chat turns), `symptoms:{id}` (structured check-in history), `risk_timeline:{id}` (every risk
  score + rationale).
- **RAG vector store:** `vector:{id}` holds embeddings of the care-plan chunks that power
  `lookup_plan`, so the agent's answers stay grounded in the patient's document.
- **Pattern detection:** `detect_pattern` runs trend logic directly over the `symptoms:{id}`
  time-series (rising BP across days, recurring symptom mentions).
- **Real-time escalation bus:** writes to `escalations:{id}` publish over Redis pub/sub, which
  drives the clinician's live WebSocket inbox — a new escalation appears the instant it happens.
- **In-house clinician↔patient messaging:** `messages:{id}` (clinician → patient), plus
  `notes:{id}`, `followup:{id}`, and `push_subscriptions:{clinician}` — all in Redis, no third
  party.

### 🟣 Arize — the trust & observability layer

Arize is Cadence's proof that the AI is safe and explainable — the beat that answers every
clinical judge's hardest question.

- **Tracing on every agent call.** `setup_tracing()` + an `agent_span()` context manager wrap
  every orchestrator turn and every tool dispatch (`backend/eval/arize_judge.py`,
  `backend/agent/orchestrator.py`).
- **De-identification before egress.** Span attributes are de-identified before they leave our
  infrastructure: `patient_id` is hashed to a `patient_token` and raw symptom text is stripped —
  Arize sees `{ patient_token, tool_called, severity, escalation_appropriate }`, never PHI.
- **LLM-as-judge eval logged to Arize.** `judge_escalation()` posts the appropriateness verdict
  and confidence to the Arize `cadence` project (verified live returning **"appropriate: YES
  (0.97)"**), giving an auditable record that each escalation was independently evaluated.

### Supporting infrastructure

- **Web Push API** — Clinician escalation alerts use the Web Push Protocol (VAPID +
  `pywebpush`, service worker in `public/sw.js`). The payload contains **zero PHI** ("a patient
  needs your attention — tap to review"); the clinician taps into their authenticated dashboard
  to see the clinical detail. No SMS, no carrier network.
- **Vercel** — Both apps ship as separate Vercel projects from one repo: the **Patient PWA**
  (installable, mobile-first) and the **Clinician Dashboard** (desktop), each pointed at the
  deployed backend via `VITE_API_URL`.
- **Railway** — The FastAPI backend is deployed on Railway (`railway.toml`: NIXPACKS build,
  `uvicorn` start command, `/api/health` healthcheck). This is the API both Vercel apps target.

---

## Architecture

**Backend (`backend/`)** — FastAPI. Claude tool-use orchestrator (`agent/orchestrator.py`),
seven frozen tools (`agent/tools.py`), Vision plan ingestion (`ingestion/pipeline.py`), Redis
memory + RAG (`memory/`), risk engine + pattern detection (`risk/`), escalation handler
(`escalation/`), Web Push (`notifications/push.py`), Arize tracing + judge (`eval/`). Every tool
returns a Pydantic model from the frozen schema; the data shapes *are* the contract. Deployed on
**Railway** (`railway.toml`).

**Frontend** — Two TanStack Start (Vite + Nitro) apps: `part-1` (Patient PWA) and `part-2`
(Clinician Dashboard), both React + Tailwind, deployed on Vercel.

## Security & Compliance

Built compliance-first, even with synthetic demo data: **100% synthetic patients** (no real
PHI), Redis over TLS with per-patient key scoping (cross-patient leakage is architecturally
impossible),

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 203 recognized source files, 843 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers
- AI coding agent: Codex — evidence: config files committed to the repository
- AI coding agent: Cursor — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 234)

```
.env.example
.gitignore
AGENTS.md
backend/__init__.py
backend/agent/__init__.py
backend/agent/orchestrator.py
backend/agent/prompts/lmj_eval.txt
backend/agent/prompts/rubrics/preeclampsia.txt
backend/agent/prompts/system.txt
backend/agent/prompts/visit_summary_clinician.txt
backend/agent/prompts/visit_summary_patient.txt
backend/agent/tools.py
backend/auth.py
backend/CONTRACT.md
backend/demo/__init__.py
backend/demo/golden_path.py
backend/demo/maria_data.py
backend/demo/panel_data.py
backend/demo/seed_data.py
backend/escalation/__init__.py
backend/escalation/handler.py
backend/eval/__init__.py
backend/eval/arize_judge.py
backend/ingestion/__init__.py
backend/ingestion/api_models.py
backend/ingestion/pipeline.py
backend/ingestion/schema.py
backend/main.py
backend/memory/__init__.py
backend/memory/rag.py
backend/memory/redis_client.py
backend/notifications/__init__.py
backend/notifications/push.py
backend/requirements.txt
backend/risk/__init__.py
backend/risk/classifier.py
backend/risk/patterns.py
backend/routes/__init__.py
backend/routes/auth_routes.py
backend/routes/chat.py
backend/routes/clinician.py
backend/routes/demo.py
backend/routes/ingest.py
backend/routes/patient.py
backend/routes/push.py
backend/safety/__init__.py
backend/safety/classifier.py
backend/summaries/__init__.py
backend/summaries/visit_summary.py
docs/gen_diagrams.py
docs/system-diagrams.md
packs/preeclampsia_risk.json
part-1/.lovable/project.json
part-1/.prettierignore
part-1/.prettierrc
part-1/AGENTS.md
part-1/bun.lock
part-1/bunfig.toml
part-1/components.json
part-1/eslint.config.js
part-1/package.json
part-1/public/manifest.webmanifest
part-1/src/components/Conversation.tsx
part-1/src/components/DailyCheckin.tsx
part-1/src/components/PatientShell.tsx
part-1/src/components/Splash.tsx
part-1/src/components/ui/accordion.tsx
part-1/src/components/ui/alert-dialog.tsx
part-1/src/components/ui/alert.tsx
part-1/src/components/ui/aspect-ratio.tsx
part-1/src/components/ui/avatar.tsx
part-1/src/components/ui/badge.tsx
part-1/src/components/ui/breadcrumb.tsx
part-1/src/components/ui/button.tsx
part-1/src/components/ui/calendar.tsx
part-1/src/components/ui/card.tsx
part-1/src/components/ui/carousel.tsx
part-1/src/components/ui/chart.tsx
part-1/src/components/ui/checkbox.tsx
part-1/src/components/ui/collapsible.tsx
part-1/src/components/ui/command.tsx
part-1/src/components/ui/context-menu.tsx
part-1/src/components/ui/dialog.tsx
part-1/src/components/ui/drawer.tsx
part-1/src/components/ui/dropdown-menu.tsx
part-1/src/components/ui/form.tsx
part-1/src/components/ui/hover-card.tsx
part-1/src/components/ui/input-otp.tsx
part-1/src/components/ui/input.tsx
part-1/src/components/ui/label.tsx
part-1/src/components/ui/menubar.tsx
part-1/src/components/ui/navigation-menu.tsx
part-1/src/components/ui/pagination.tsx
part-1/src/components/ui/popover.tsx
part-1/src/components/ui/progress.tsx
part-1/src/components/ui/radio-group.tsx
part-1/src/components/ui/resizable.tsx
part-1/src/components/ui/scroll-area.tsx
part-1/src/components/ui/select.tsx
part-1/src/components/ui/separator.tsx
part-1/src/components/ui/sheet.tsx
part-1/src/components/ui/sidebar.tsx
part-1/src/components/ui/skeleton.tsx
part-1/src/components/ui/slider.tsx
part-1/src/components/ui/sonner.tsx
part-1/src/components/ui/switch.tsx
part-1/src/components/ui/table.tsx
part-1/src/components/ui/tabs.tsx
part-1/src/components/ui/textarea.tsx
part-1/src/components/ui/toggle-group.tsx
part-1/src/components/ui/toggle.tsx
part-1/src/components/ui/tooltip.tsx
part-1/src/hooks/use-mobile.tsx
part-1/src/lib/api.ts
part-1/src/lib/error-capture.ts
part-1/src/lib/error-page.ts
part-1/src/lib/lovable-error-reporting.ts
part-1/src/lib/pastChats.ts
part-1/src/lib/utils.ts
part-1/src/router.tsx
[114 more files omitted for size]
```

### Dependencies

- backend/requirements.txt: anthropic, arize-otel, fastapi, openinference-instrumentation-anthropic, opentelemetry-sdk, pydantic, python-dotenv, python-jose[cryptography], python-multipart, pywebpush, redis, uvicorn[standard]
- part-1/package.json: @eslint/js@^9.32.0, @hookform/resolvers@^5.2.2, @lovable.dev/vite-tanstack-config@^2.5.2, @radix-ui/react-accordion@^1.2.12, @radix-ui/react-alert-dialog@^1.1.15, @radix-ui/react-aspect-ratio@^1.1.8, @radix-ui/react-avatar@^1.1.11, @radix-ui/react-checkbox@^1.3.3, @radix-ui/react-collapsible@^1.1.12, @radix-ui/react-context-menu@^2.2.16, @radix-ui/react-dialog@^1.1.15, @radix-ui/react-dropdown-menu@^2.1.16, @radix-ui/react-hover-card@^1.1.15, @radix-ui/react-label@^2.1.8, @radix-ui/react-menubar@^1.1.16, @radix-ui/react-navigation-menu@^1.2.14, @radix-ui/react-popover@^1.1.15, @radix-ui/react-progress@^1.1.8, @radix-ui/react-radio-group@^1.3.8, @radix-ui/react-scroll-area@^1.2.10, @radix-ui/react-select@^2.2.6, @radix-ui/react-separator@^1.1.8, @radix-ui/react-slider@^1.3.6, @radix-ui/react-slot@^1.2.4, @radix-ui/react-switch@^1.2.6, @radix-ui/react-tabs@^1.1.13, @radix-ui/react-toggle@^1.1.10, @radix-ui/react-toggle-group@^1.1.11, @radix-ui/react-tooltip@^1.2.8, @tailwindcss/vite@^4.2.1, @tanstack/react-query@^5.83.0, @tanstack/react-router@^1.168.25, @tanstack/react-start@^1.167.50, @tanstack/router-plugin@^1.167.28, @types/node@^22.16.5, @types/react@^19.2.0, @types/react-dom@^19.2.0, @vitejs/plugin-react@^5.2.0, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@^1.1.1, date-fns@^4.1.0, embla-carousel-react@^8.6.0, eslint@^9.32.0, eslint-config-prettier@^10.1.1, eslint-plugin-prettier@^5.2.6, eslint-plugin-react-hooks@^5.2.0, eslint-plugin-react-refresh@^0.4.20, globals@^15.15.0, input-otp@^1.4.2, lucide-react@^0.575.0, nitro@3.0.260603-beta, prettier@^3.7.3, react@^19.2.0, react-day-picker@^9.14.0, react-dom@^19.2.0, react-hook-form@^7.71.2, react-resizable-panels@^4.6.5, recharts@^2.15.4, sonner@^2.0.7, tailwind-merge@^3.5.0, tailwindcss@^4.2.1, tw-animate-css@^1.3.4, typescript@^5.8.3, typescript-eslint@^8.56.1, vaul@^1.1.2, vite@^8.0.16, vite-tsconfig-paths@^6.0.2, zod@^3.24.2
- part-2/package.json: @eslint/js@^9.32.0, @hookform/resolvers@^5.2.2, @lovable.dev/vite-tanstack-config@^2.5.2, @radix-ui/react-accordion@^1.2.12, @radix-ui/react-alert-dialog@^1.1.15, @radix-ui/react-aspect-ratio@^1.1.8, @radix-ui/react-avatar@^1.1.11, @radix-ui/react-checkbox@^1.3.3, @radix-ui/react-collapsible@^1.1.12, @radix-ui/react-context-menu@^2.2.16, @radix-ui/react-dialog@^1.1.15, @radix-ui/react-dropdown-menu@^2.1.16, @radix-ui/react-hover-card@^1.1.15, @radix-ui/react-label@^2.1.8, @radix-ui/react-menubar@^1.1.16, @radix-ui/react-navigation-menu@^1.2.14, @radix-ui/react-popover@^1.1.15, @radix-ui/react-progress@^1.1.8, @radix-ui/react-radio-group@^1.3.8, @radix-ui/react-scroll-area@^1.2.10, @radix-ui/react-select@^2.2.6, @radix-ui/react-separator@^1.1.8, @radix-ui/react-slider@^1.3.6, @radix-ui/react-slot@^1.2.4, @radix-ui/react-switch@^1.2.6, @radix-ui/react-tabs@^1.1.13, @radix-ui/react-toggle@^1.1.10, @radix-ui/react-toggle-group@^1.1.11, @radix-ui/react-tooltip@^1.2.8, @tailwindcss/vite@^4.2.1, @tanstack/react-query@^5.83.0, @tanstack/react-router@^1.168.25, @tanstack/react-start@^1.167.50, @tanstack/router-plugin@^1.167.28, @types/node@^22.16.5, @types/react@^19.2.0, @types/react-dom@^19.2.0, @vitejs/plugin-react@^5.2.0, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@^1.1.1, date-fns@^4.1.0, embla-carousel-react@^8.6.0, eslint@^9.32.0, eslint-config-prettier@^10.1.1, eslint-plugin-prettier@^5.2.6, eslint-plugin-react-hooks@^5.2.0, eslint-plugin-react-refresh@^0.4.20, globals@^15.15.0, input-otp@^1.4.2, lucide-react@^0.575.0, nitro@3.0.260603-beta, prettier@^3.7.3, react@^19.2.0, react-day-picker@^9.14.0, react-dom@^19.2.0, react-hook-form@^7.71.2, react-resizable-panels@^4.6.5, recharts@^2.15.4, sonner@^2.0.7, tailwind-merge@^3.5.0, tailwindcss@^4.2.1, tw-animate-css@^1.3.4, typescript@^5.8.3, typescript-eslint@^8.56.1, vaul@^1.1.2, vite@^8.0.16, vite-tsconfig-paths@^6.0.2, zod@^3.24.2
- requirements.txt: anthropic, arize-otel, fastapi, openinference-instrumentation-anthropic, opentelemetry-sdk, pydantic, python-dotenv, python-jose[cryptography], python-multipart, pywebpush, redis, uvicorn[standard]

### Recent commits (newest first)

- finished
- finished
- style: shift brand palette from purple to yellow
- feat(demo): seed two standing escalations (Priya, Rosa)
- Merge feature/seed-standing-escalations: non-empty demo inbox
- fix(arize): self-heal stripped base64 padding on ARIZE_SPACE_ID
- Merge feature/demo-reset: one-tap demo reset endpoint
- feat(demo): one-tap demo reset endpoint
- backend: log Arize tracing status on first request (LIVE vs DISABLED)
- Merge backend/arize-instrumentation: OpenInference LLM traces + request waterfall + judge-as-eval
- part-2: full visual redesign — patient portal design language
- backend: log LLM-as-judge verdict as an Arize evaluation
- backend(wip): Arize OpenInference instrumentation + request root span
- feat(part-2): cap smart inferences at 2, always side-by-side grid
- chore: update part-1 route tree (auto-generated)
- feat(part-2): fixed viewport layout, adherence ring card, concise stats
- feat(demo): add realistic patient narratives to all panel check-ins
- fix(part-2): timeline entries blank for patients with empty raw_text
- fix(backend): add port 5174 to CORS allowed origins
- chore: add Cadence app icons to part-2 public

## Key source files (fetched from GitHub, selected and truncated for size)

### part-1/AGENTS.md

```markdown
<!-- LOVABLE:BEGIN -->
> [!IMPORTANT]
> This project is connected to [Lovable](https://lovable.dev). Avoid rewriting
> published git history — force pushing, or rebasing/amending/squashing commits
> that are already pushed — as it rewrites history on Lovable's side and the
> user will likely lose their project history.
>
> Commits you push to the connected branch sync back to Lovable and show up in
> the editor, so keep the branch in a working state.
<!-- LOVABLE:END -->

```

### part-2/AGENTS.md

```markdown
<!-- LOVABLE:BEGIN -->
> [!IMPORTANT]
> This project is connected to [Lovable](https://lovable.dev). Avoid rewriting
> published git history — force pushing, or rebasing/amending/squashing commits
> that are already pushed — as it rewrites history on Lovable's side and the
> user will likely lose their project history.
>
> Commits you push to the connected branch sync back to Lovable and show up in
> the editor, so keep the branch in a working state.
<!-- LOVABLE:END -->

```

### requirements.txt

```
fastapi
uvicorn[standard]
pydantic
python-multipart
redis
anthropic
pywebpush
python-dotenv
arize-otel
openinference-instrumentation-anthropic
opentelemetry-sdk
python-jose[cryptography]

```

### backend/requirements.txt

```
fastapi
uvicorn[standard]
pydantic
python-multipart
redis
anthropic
pywebpush
python-dotenv
arize-otel
openinference-instrumentation-anthropic
opentelemetry-sdk
python-jose[cryptography]

```

### part-1/package.json

```
{
  "name": "tanstack_start_ts",
  "private": true,
  "sideEffects": false,
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "build:dev": "vite build --mode development",
    "preview": "vite preview",
    "lint": "eslint .",
    "format": "prettier --write ."
  },
  "dependencies": {
    "@hookform/resolvers": "^5.2.2",
    "@radix-ui/react-accordion": "^1.2.12",
    "@radix-ui/react-alert-dialog": "^1.1.15",
    "@radix-ui/react-aspect-ratio": "^1.1.8",
    "@radix-ui/react-avatar": "^1.1.11",
    "@radix-ui/react-checkbox": "^1.3.3",
    "@radix-ui/react-collapsible": "^1.1.12",
    "@radix-ui/react-context-menu": "^2.2.16",
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-dropdown-menu": "^2.1.16",
    "@radix-ui/react-hover-card": "^1.1.15",
    "@radix-ui/react-label": "^2.1.8",
    "@radix-ui/react-menubar": "^1.1.16",
    "@radix-ui/react-navigation-menu": "^1.2.14",
    "@radix-ui/react-popover": "^1.1.15",
    "@radix-ui/react-progress": "^1.1.8",
    "@radix-ui/react-radio-group": "^1.3.8",
    "@radix-ui/react-scroll-area": "^1.2.10",
    "@radix-ui/react-select": "^2.2.6",
    "@radix-ui/react-separator": "^1.1.8",
    "@radix-ui/react-slider": "^1.3.6",
    "@radix-ui/react-slot": "^1.2.4",
    "@radix-ui/react-switch": "^1.2.6",
    "@radix-ui/react-tabs": "^1.1.13",
    "@radix-ui/react-toggle": "^1.1.10",
    "@radix-ui/react-toggle-group": "^1.1.11",
    "@radix-ui/react-tooltip": "^1.2.8",
    "@tailwindcss/vite": "^4.2.1",
    "@tanstack/react-query": "^5.83.0",
    "@tanstack/react-router": "^1.168.25",
    "@tanstack/react-start": "^1.167.50",
    "@tanstack/router-plugin": "^1.167.28",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.1.1",
    "date-fns": "^4.1.0",
    "embla-carousel-react": "^8.6.0",
    "input-otp": "^1.4.2",
    "lucide-react": "^0.575.0",
    "react": "^19.2.0",
    "react-day-picker": "^9.14.0",
    "react-dom": "^19.2.0",
    "react-hook-form": "^7.71.2",
    "react-resizable-panels": "^4.6.5",
    "recharts": "^2.15.4",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.5.0",
    "tailwindcss": "^4.2.1",
    "tw-animate-css": "^1.3.4",
    "vaul": "^1.1.2",
    "vite-tsconfig-paths": "^6.0.2",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@eslint/js": "^9.32.0",
    "@lovable.dev/vite-tanstack-config": "^2.5.2",
    "@types/node": "^22.16.5",
    "@types/react": "^19.2.0",
    "@types/react-dom": "^19.2.0",
    "@vitejs/plugin-react": "^5.2.0",
    "eslint": "^9.32.0",
    "eslint-config-prettier": "^10.1.1",
    "eslint-plugin-prettier": "^5.2.6",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.20",
    "globals": "^15.15.0",
    "nitro": "3.0.260603-beta",
    "prettier": "^3.7.3",
    "typescript": "^5.8.3",
    "typescript-eslint": "^8.56.1",
    "vite": "^8.0.16"
  }
}

```

### part-2/package.json

```
{
  "name": "tanstack_start_ts",
  "private": true,
  "sideEffects": false,
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "build:dev": "vite build --mode development",
    "preview": "vite preview",
    "lint": "eslint .",
    "format": "prettier --write ."
  },
  "dependencies": {
    "@hookform/resolvers": "^5.2.2",
    "@radix-ui/react-accordion": "^1.2.12",
    "@radix-ui/react-alert-dialog": "^1.1.15",
    "@radix-ui/react-aspect-ratio": "^1.1.8",
    "@radix-ui/react-avatar": "^1.1.11",
    "@radix-ui/react-checkbox": "^1.3.3",
    "@radix-ui/react-collapsible": "^1.1.12",
    "@radix-ui/react-context-menu": "^2.2.16",
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-dropdown-menu": "^2.1.16",
    "@radix-ui/react-hover-card": "^1.1.15",
    "@radix-ui/react-label": "^2.1.8",
    "@radix-ui/react-menubar": "^1.1.16",
    "@radix-ui/react-navigation-menu": "^1.2.14",
    "@radix-ui/react-popover": "^1.1.15",
    "@radix-ui/react-progress": "^1.1.8",
    "@radix-ui/react-radio-group": "^1.3.8",
    "@radix-ui/react-scroll-area": "^1.2.10",
    "@radix-ui/react-select": "^2.2.6",
    "@radix-ui/react-separator": "^1.1.8",
    "@radix-ui/react-slider": "^1.3.6",
    "@radix-ui/react-slot": "^1.2.4",
    "@radix-ui/react-switch": "^1.2.6",
    "@radix-ui/react-tabs": "^1.1.13",
    "@radix-ui/react-toggle": "^1.1.10",
    "@radix-ui/react-toggle-group": "^1.1.11",
    "@radix-ui/react-tooltip": "^1.2.8",
    "@tailwindcss/vite": "^4.2.1",
    "@tanstack/react-query": "^5.83.0",
    "@tanstack/react-router": "^1.168.25",
    "@tanstack/react-start": "^1.167.50",
    "@tanstack/router-plugin": "^1.167.28",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.1.1",
    "date-fns": "^4.1.0",
    "embla-carousel-react": "^8.6.0",
    "input-otp": "^1.4.2",
    "lucide-react": "^0.575.0",
    "react": "^19.2.0",
    "react-day-picker": "^9.14.0",
    "react-dom": "^19.2.0",
    "react-hook-form": "^7.71.2",
    "react-resizable-panels": "^4.6.5",
    "recharts": "^2.15.4",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.5.0",
    "tailwindcss": "^4.2.1",
    "tw-animate-css": "^1.3.4",
    "vaul": "^1.1.2",
    "vite-tsconfig-paths": "^6.0.2",
    "zod": "^3.24.2"
  },
  "devDependencies": {
    "@eslint/js": "^9.32.0",
    "@lovable.dev/vite-tanstack-config": "^2.5.2",
    "@types/node": "^22.16.5",
    "@types/react": "^19.2.0",
    "@types/react-dom": "^19.2.0",
    "@vitejs/plugin-react": "^5.2.0",
    "eslint": "^9.32.0",
    "eslint-config-prettier": "^10.1.1",
    "eslint-plugin-prettier": "^5.2.6",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.20",
    "globals": "^15.15.0",
    "nitro": "3.0.260603-beta",
    "prettier": "^3.7.3",
    "typescript": "^5.8.3",
    "typescript-eslint": "^8.56.1",
    "vite": "^8.0.16"
  }
}

```

### backend/main.py

```python
"""
Cadence — FastAPI application entrypoint.

Wires CORS for the Vite frontend and registers every router.
All endpoints read from Redis; no mock data remains.

Run locally:  uvicorn backend.main:app --reload --port 8000
"""

from __future__ import annotations

import os

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from .routes import auth_routes, chat, clinician, demo, ingest, patient, push

# ── App ──────────────────────────────────────────────────────────────────────
app = FastAPI(title="Cadence API", version="0.1.0")

# CORS for the Vite frontend (configurable via comma-separated CORS_ORIGINS).
_origins = os.getenv(
    "CORS_ORIGINS",
    "http://localhost:5173,http://127.0.0.1:5173,http://localhost:5174,http://127.0.0.1:5174,http://localhost:3000",
).split(",")
app.add_middleware(
    CORSMiddleware,
    allow_origins=[o.strip() for o in _origins if o.strip()],
    allow_credentials=True,  # required: JWT travels in an HttpOnly cookie
    allow_methods=["*"],
    allow_headers=["*"],
)

# ── Routers ──────────────────────────────────────────────────────────────────
app.include_router(auth_routes.router)
app.include_router(chat.router)
app.include_router(patient.router)
app.include_router(ingest.router)
app.include_router(clinician.router)
app.include_router(clinician.ws_router)  # WS /ws/escalations (no /api prefix)
app.include_router(push.router)
app.include_router(demo.router)  # GET/POST /api/demo/reset (token-guarded, bookmarkable)


@app.get("/api/health", tags=["meta"])
async def health() -> dict[str, str]:
    return {"status": "ok", "service": "cadence-api"}

```

### part-1/src/server.ts

```typescript
import "./lib/error-capture";

import { consumeLastCapturedError } from "./lib/error-capture";
import { renderErrorPage } from "./lib/error-page";

type ServerEntry = {
  fetch: (request: Request, env: unknown, ctx: unknown) => Promise<Response> | Response;
};

let serverEntryPromise: Promise<ServerEntry> | undefined;

async function getServerEntry(): Promise<ServerEntry> {
  if (!serverEntryPromise) {
    serverEntryPromise = import("@tanstack/react-start/server-entry").then(
      (m) => (m.default ?? m) as ServerEntry,
    );
  }
  return serverEntryPromise;
}

// h3 swallows in-handler throws into a normal 500 Response with body
// {"unhandled":true,"message":"HTTPError"} — try/catch alone never fires for those.
async function normalizeCatastrophicSsrResponse(response: Response): Promise<Response> {
  if (response.status < 500) return response;
  const contentType = response.headers.get("content-type") ?? "";
  if (!contentType.includes("application/json")) return response;

  const body = await response.clone().text();
  if (!body.includes('"unhandled":true') || !body.includes('"message":"HTTPError"')) {
    return response;
  }

  console.error(consumeLastCapturedError() ?? new Error(`h3 swallowed SSR error: ${body}`));
  return new Response(renderErrorPage(), {
    status: 500,
    headers: { "content-type": "text/html; charset=utf-8" },
  });
}

export default {
  async fetch(request: Request, env: unknown, ctx: unknown) {
    try {
      const handler = await getServerEntry();
      const response = await handler.fetch(request, env, ctx);
      return await normalizeCatastrophicSsrResponse(response);
    } catch (error) {
      console.error(error);
      return new Response(renderErrorPage(), {
        status: 500,
        headers: { "content-type": "text/html; charset=utf-8" },
      });
    }
  },
};

```

### part-2/src/server.ts

```typescript
import "./lib/error-capture";

import { consumeLastCapturedError } from "./lib/error-capture";
import { renderErrorPage } from "./lib/error-page";

type ServerEntry = {
  fetch: (request: Request, env: unknown, ctx: unknown) => Promise<Response> | Response;
};

let serverEntryPromise: Promise<ServerEntry> | undefined;

async function getServerEntry(): Promise<ServerEntry> {
  if (!serverEntryPromise) {
    serverEntryPromise = import("@tanstack/react-start/server-entry").then(
      (m) => (m.default ?? m) as ServerEntry,
    );
  }
  return serverEntryPromise;
}

// h3 swallows in-handler throws into a normal 500 Response with body
// {"unhandled":true,"message":"HTTPError"} — try/catch alone never fires for those.
async function normalizeCatastrophicSsrResponse(response: Response): Promise<Response> {
  if (response.status < 500) return response;
  const contentType = response.headers.get("content-type") ?? "";
  if (!contentType.includes("application/json")) return response;

  const body = await response.clone().text();
  if (!body.includes('"unhandled":true') || !body.includes('"message":"HTTPError"')) {
    return response;
  }

  console.error(consumeLastCapturedError() ?? new Error(`h3 swallowed SSR error: ${body}`));
  return new Response(renderErrorPage(), {
    status: 500,
    headers: { "content-type": "text/html; charset=utf-8" },
  });
}

export default {
  async fetch(request: Request, env: unknown, ctx: unknown) {
    try {
      const handler = await getServerEntry();
      const response = await handler.fetch(request, env, ctx);
      return await normalizeCatastrophicSsrResponse(response);
    } catch (error) {
      console.error(error);
      return new Response(renderErrorPage(), {
        status: 500,
        headers: { "content-type": "text/html; charset=utf-8" },
      });
    }
  },
};

```

### part-1/src/routes/index.tsx

```typescript
import { createFileRoute } from "@tanstack/react-router";
import { LiveChat } from "@/components/Conversation";

export const Route = createFileRoute("/")({
  head: () => ({
    meta: [
      { title: "Cade — Cadence" },
      { name: "description", content: "Your daily check-in with Cade." },
    ],
  }),
  component: LiveChat,
});

```

[180 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]