diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..eefd90e
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,58 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [main]
+ push:
+ branches: [main]
+
+# Cancel in-progress runs for the same ref so a force-push doesn't queue
+# stale CI.
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build-test:
+ name: build-test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ # vite.config.js conditionally emits --no-experimental-webstorage
+ # for Node >= 22, so CI must run Node 22 to match local dev.
+ node-version: 22
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ # Lint currently fails on pre-existing errors in main (e.g. vite.config.js
+ # 'process' is not defined, useNLP exhaustive-deps warnings). Run as a
+ # non-blocking informational step until those are fixed in a follow-up;
+ # build + tests are the real merge gate.
+ - name: Lint (non-blocking)
+ run: npm run lint
+ continue-on-error: true
+
+ - name: Test
+ run: npm run test:run
+
+ - name: Build
+ run: npm run build
+
+ # Upload the built dist/ as a workflow artifact so reviewers can
+ # download and serve it locally (e.g. `npx serve dist`) to verify the
+ # production bundle of the PR's exact code. Retention is short to
+ # avoid clutter; the artifact is for review-time only, not archival.
+ - name: Upload built site
+ uses: actions/upload-artifact@v4
+ with:
+ name: dist-${{ github.event.pull_request.number || github.sha }}
+ path: dist/
+ retention-days: 7
+ if-no-files-found: error
diff --git a/Classification Harness.html b/Classification Harness.html
new file mode 100644
index 0000000..a838276
--- /dev/null
+++ b/Classification Harness.html
@@ -0,0 +1,338 @@
+
+
+
+
+
+IRIS — classification test harness
+
+
+
+
+
+
Classification harness
+
Standalone test rig for the proposed two-stage pipeline. Stage 1 classifies trials as LIKELY / POSSIBLE / UNLIKELY using a short prompt, before the full simplification runs. Use this to validate latency, parse-rate, and verdict stability against real ClinicalTrials.gov payloads before wiring it into the app.
+
+
+ Wire your model in classifyOne(). The harness ships with a mock implementation that simulates 200–1500ms latency and ~85% parse success. Replace the body of classifyOne() with your real on-device call (Gemma 2 2B / MediaPipe / etc.).
+
+
+
+
Inputs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Results
+
+
+
+
Trial
+
Verdict
+
Latency
+
Raw output / reason
+
Expected
+
+
+
+
+
+
+
+
+ How to wire your model
+
// Replace the body of classifyOne() in this file's <script> block.
+// It must return: { verdict, reason, raw, latencyMs }
+//
+// async function classifyOne(prompt) {
+// const t0 = performance.now();
+// const raw = await window.iris.generate({ prompt, maxTokens: 60 });
+// const latencyMs = performance.now() - t0;
+// return { ...parseVerdict(raw), raw, latencyMs };
+// }
+//
+// parseVerdict() is already implemented — it looks for
+// /^(LIKELY|POSSIBLE|UNLIKELY)\s*[|:\-]\s*(.+)$/im in the model output.
+
+
+
+
+
+
diff --git a/Handoff.md b/Handoff.md
new file mode 100644
index 0000000..d99965b
--- /dev/null
+++ b/Handoff.md
@@ -0,0 +1,159 @@
+# IRIS — Claude Code handoff
+
+Design exploration is settled. This document is the bridge from the Claude.ai prototypes to the live `johnoooh/iris` codebase. Hand it to Claude Code along with the three artifact files.
+
+---
+
+## What's settled
+
+**Direction:** Triage two-pane layout (list + detail). Mobile collapses to list + tap-to-sheet.
+
+**Visual system:** Evolved warm-parchment palette + new iris-violet accent. Source Serif 4 (display), Inter Tight (UI), JetBrains Mono (technical). Tokens live in `styles/tokens.css`.
+
+**Locked tweak values from the prototype:**
+- Accent: `iris` (the violet)
+- Density: `comfy`
+- List width: `400px`
+
+**New patterns introduced:**
+- **Fit meter** — three-bar visualization (Likely / May / Unclear fit) shown per row and per detail pane
+- **Two-stage AI pipeline** — fast classify pass, then on-demand full simplification (still being validated, see harness)
+- **Local-AI badge** — always-visible mono pill in header announcing on-device model
+- **Compare** — pin up to 3 trials; sticky bar; compare-view page is later work
+- **Streaming shimmer** — placeholder lines shimmer until tokens land, then fade in
+- **Unified search** — NL + structured form become one input with a mode toggle
+
+---
+
+## Files to reference
+
+| File | Role |
+|---|---|
+| `IRIS Triage.html` | Final chosen direction. Reference for layout, spacing, microcopy. |
+| `styles/tokens.css` | Drop-in CSS variables. Colors, fonts, shadows, shimmer keyframes. |
+| `shared/iris-shared.jsx` | Reference implementations of header, search bar, fit meter, status pill, streaming text, action row. **Translate to your stack** (vanilla JS / whatever the live app uses) — don't copy React if the app isn't React. |
+| `Classification Harness.html` | Standalone rig for validating the two-stage classify before wiring it in. |
+
+---
+
+## Integration plan (recommended order)
+
+### Phase 1 — Visual system (low risk, ship first)
+
+1. **Add Google Fonts link** to ``:
+ ```html
+
+ ```
+2. **Drop in `styles/tokens.css`** as new variables alongside existing ones; gradually replace the old palette.
+3. **Replace header markup** with the new dense header + privacy chip + local-AI badge.
+4. **Demote dedication banner** to a footer or "About IRIS" disclosure. Don't lose it — it's part of the project's soul, just not above-the-fold task-blocking.
+5. **Compress the privacy paragraph** into the on-device chip + an expandable details element with the long form.
+
+Ship after Phase 1 — already a meaningful UX improvement, no logic changes.
+
+### Phase 2 — Row format + accordion or two-pane
+
+The current cards are full-width prose blocks. Swap for compact rows:
+
+```
+[ ☐ ] Phase IIIb Study of Ribociclib + ET in Early Breast Cancer
+ ▌▌▌ Likely fit · 0.1 mi · Phase 3
+```
+
+Two implementation paths — pick one based on engineering appetite:
+
+- **(a) Accordion in place** — easier. Click a row, it expands inline with the detail content. Works at any width. No layout fork.
+- **(b) Two-pane** — matches the prototype. CSS grid `grid-template-columns: 400px 1fr`, collapses to single-column under 820px (`@media` query + state-driven sheet on mobile).
+
+Recommend **(a) for first pass**, **(b) when you're ready to invest in the layout fork.**
+
+### Phase 3 — Two-stage classification
+
+**Don't ship this until the harness validates it.** See [Classification harness](#classification-harness) below.
+
+When ready:
+1. After search returns trial list, immediately render rows with title/distance/phase only — no fit meter yet.
+2. Kick off `classifyAll(trials, userDesc)` with concurrency 2–3.
+3. As each verdict returns, update that row's fit meter in place.
+4. Show `evaluating fit · 7 of 20` indicator in the toolbar while running.
+5. Once stage 1 is complete, default sort flips to "Best fit"; collapse UNLIKELY trials under a `12 less likely matches` disclosure.
+6. Stage 2 (full simplification) only fires for the currently-selected trial in the detail pane, or top N likely matches as the user scrolls.
+
+### Phase 4 — Compare
+
+1. `Set` in memory, max size 3.
+2. Checkbox on each row.
+3. Sticky bar appears when set is non-empty: `[ 2 in compare ] [ Compare → ]`.
+4. Compare view itself is later — start with a placeholder route.
+
+### Phase 5 — Mobile polish
+
+1. Bottom-sheet pattern: tap row → sheet slides up with full detail. Backdrop dismiss + close button + drag handle.
+2. Sticky compare bar at bottom.
+3. Compact search summary chip replaces the full search bar on mobile (tap to expand).
+
+### Phase 6 — Persistence (optional, session-only)
+
+1. `sessionStorage` only — no PII to disk, in keeping with the privacy story.
+2. Save: search query, comparing set, currently-selected trial.
+3. Clear on a "Start over" button.
+
+---
+
+## Classification harness
+
+`Classification Harness.html` is a standalone page with a mocked `classifyOne()` that simulates 200–1500ms latency and ~85% parse success.
+
+**To validate the real model:**
+
+1. Open the harness.
+2. Replace the body of `classifyOne()` with your live on-device call — the function signature is `(prompt, trial) => Promise<{ verdict, reason, raw, latencyMs }>`.
+3. Run with the included fixture (6 trials, with expected verdicts).
+4. Check the stats row: parse rate, avg latency, max latency, agreement with expected.
+
+**Pass criteria for moving to Phase 3:**
+- Parse rate ≥ 90% on 50+ real trials
+- Avg latency < 1.5s per trial on a mid-range laptop
+- Agreement ≥ 80% on a labeled held-out set
+- No catastrophic UNLIKELY false-negatives (a viable trial ranked as UNLIKELY)
+
+**If parse rate is low:** try constrained decoding, or tighten the prompt to demand a single token first (`Output a single token: LIKELY, POSSIBLE, or UNLIKELY. Then on a new line, one sentence of reasoning.`).
+
+**If latency is high:** drop concurrency to 1 (avoid model thrashing on small WebGPU buffers), truncate eligibility more aggressively (1500 → 800 chars), or run only on the top 10 by simple keyword pre-filter.
+
+---
+
+## Things explicitly out of scope for this pass
+
+- Compare view (3-up side-by-side) — deferred
+- Account / login / save across sessions — deferred, conflicts with privacy story
+- Server-side fallback for the model — not consistent with on-device promise
+- Distance map view — nice-to-have, not on the critical path
+- Question-prep checklist — separate feature, separate PRD
+
+---
+
+## Open questions for product
+
+1. **Fit meter wording** — "Likely fit / May fit / Unclear fit" is the current draft. Does that read right, or do we want softer phrasing ("Worth a look / Maybe / Probably not")?
+2. **UNLIKELY default behavior** — collapse them, or just sort to bottom? Risk of hiding viable trials if the model is wrong.
+3. **Compare view** — which dimensions matter most? Probably: phase, distance, drug/intervention, eligibility deltas, contact info.
+4. **Fit meter on mobile rows** — keep at full size or shrink to just the bars? Currently same component, both contexts.
+
+---
+
+## Microcopy already drafted
+
+- Header sub: `clinical trial finder`
+- On-device chip: `on-device only`
+- Local-AI badge: `Gemma 2 2B · on-device`
+- Mode toggle: `Describe in your words` / `Structured form`
+- Mode toggle pill: `AI · on-device`
+- Understood section: `understood:` (mono, lowercase)
+- Section labels in detail: `What this study is testing`, `Who can join`
+- Fit panel caption: `based on what you described`
+- Toolbar count: `20 trials · near Boston · within 50 mi · recruiting`
+- Sort options: `Best fit`, `Distance`, `Phase`, `Most recent`
+- Compare bar (mobile): `**N** in compare` / `Compare →`
+- Sheet handle: drag affordance only, no label
+- Fit verdicts: `Likely fit`, `May fit`, `Unclear fit`
diff --git a/IRIS Redesign.html b/IRIS Redesign.html
new file mode 100644
index 0000000..111bacb
--- /dev/null
+++ b/IRIS Redesign.html
@@ -0,0 +1,143 @@
+
+
+
+
+
+IRIS — redesign explorations
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/IRIS Triage.html b/IRIS Triage.html
new file mode 100644
index 0000000..73b1a81
--- /dev/null
+++ b/IRIS Triage.html
@@ -0,0 +1,431 @@
+
+
+
+
+
+IRIS — Triage prototype
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/clinical-trial-finder-plan.md b/clinical-trial-finder-plan.md
new file mode 100644
index 0000000..0b32dfe
--- /dev/null
+++ b/clinical-trial-finder-plan.md
@@ -0,0 +1,244 @@
+# IRIS: A Privacy-First Clinical Trial Finder
+
+*Named for Iris Long (1934–), a pharmaceutical chemist who worked at Sloan-Kettering for eleven years and became a pivotal ACT-UP activist. She walked into an ACT-UP meeting in 1987 and told the room they didn't know anything about the science, the drugs, or how the system worked — then offered to teach anyone who wanted to learn. She founded the Treatment and Data Committee, organized the AIDS Treatment Registry, and dedicated herself to making clinical trial and treatment information accessible to the people who needed it most. Her work helped turn AIDS from a death sentence into a manageable condition.*
+
+## The Problem
+
+The existing ClinicalTrials.gov interface was built for researchers, not patients. It surfaces overwhelming amounts of technical jargon — ECOG performance status scores, MeSH terms, complex eligibility criteria — that creates a barrier for the people who need this information most. Conversations with ACT-UP members confirmed that finding relevant clinical trials remains a significant challenge for their community, particularly for people without medical backgrounds or institutional support.
+
+Communities that have historically had to fight for access to medical information deserve tools built with their needs and skepticism in mind.
+
+## The Vision
+
+IRIS is a fully open-source, privacy-first clinical trial discovery tool that runs entirely in the user's browser. No data is collected, no servers store queries, and no accounts are required. A fine-tuned small language model runs locally in the browser to translate between plain language and structured clinical trial searches, ensuring that a user's health information never leaves their computer.
+
+The core promise: **your health information never leaves your device — not because we promise, but because there is nowhere for it to go.**
+
+## Architecture
+
+### Design Principles
+
+- **Zero data persistence.** No database, no logs, no session storage, no cookies, no analytics of any kind.
+- **No server.** The application is a static site (HTML/JS/CSS) hosted on GitHub Pages. There is no backend to compromise.
+- **Local AI.** A fine-tuned, quantized language model runs in the browser via WebGPU/WebAssembly. Health information is processed on-device.
+- **Minimal network calls.** The only outbound requests are structured queries to the ClinicalTrials.gov public REST API — containing only clinical terms (e.g., `condition=breast+cancer&location=New+York`), not the user's raw input.
+- **Fully auditable.** The entire codebase is open source. Anyone can verify every claim about privacy.
+
+### System Components
+
+The application has three layers, each designed to function independently:
+
+**Layer 1 — Structured Search (No AI Required)**
+A clean, accessible form interface with dropdowns and text fields for condition, location, age, gender, phase preference, and intervention type. This layer works without any AI — it maps directly to ClinicalTrials.gov API query parameters. This is the default experience and the most trustworthy one.
+
+**Layer 2 — Natural Language Input (Local LLM)**
+An optional text box where users can describe their situation in their own words. A fine-tuned Gemma 2B model running locally in the browser via WebLLM parses the input into structured query parameters. The model performs entity extraction (conditions, biomarkers, demographics, location) and maps them to the same API parameters used by Layer 1. Users who are skeptical of AI can ignore this entirely and use the structured form.
+
+**Layer 3 — Plain-Language Results (Local LLM)**
+Raw trial data returned from ClinicalTrials.gov is passed through the local model to generate plain-language summaries. Technical eligibility criteria like "ECOG performance status 0-1" become "you need to be able to carry out daily activities without much difficulty." Phase descriptions, randomization, and intervention types are all explained in accessible language. A "show original" toggle lets users see the raw trial data alongside the simplified version for full transparency.
+
+### Data Flow
+
+```
+User types health description
+ │
+ ▼
+[Local Gemma 2B in browser]
+ │
+ ▼
+Structured query parameters
+(condition, location, age, etc.)
+ │
+ ▼
+ClinicalTrials.gov Public API ◄── only network call, contains no personal narrative
+ │
+ ▼
+Raw trial JSON response
+ │
+ ▼
+[Local Gemma 2B in browser]
+ │
+ ▼
+Plain-language trial cards
+displayed to user
+```
+
+### Technology Stack
+
+- **Frontend:** React single-page application, static build, hosted on GitHub Pages
+- **Local LLM:** WebLLM with fine-tuned Gemma 2B (quantized to int4), loaded from CDN and cached in browser
+- **Trial Data:** ClinicalTrials.gov v2 REST API (public, no authentication required)
+- **Build/Deploy:** Vite + GitHub Actions, automatic deployment on push
+- **Installable:** Progressive Web App (PWA) — after first visit, the app and model are cached and can work offline (except for fetching new trial data)
+
+### Accessibility and Inclusion
+
+- Mobile-first responsive design — not everyone has a laptop
+- Screen reader compatible (ARIA labels, semantic HTML, keyboard navigation)
+- Multilingual support as a future goal — the local LLM could be fine-tuned for Spanish, Mandarin, and other languages
+- Low-bandwidth friendly — the model downloads once and is cached; subsequent visits load instantly
+- No account or email required — zero friction to use
+
+## Fine-Tuning the Local Model
+
+### Overview
+
+The browser-deployed model is a fine-tuned Gemma 2B (or Gemma 4 1B) optimized for two narrow tasks:
+
+1. **Input Parsing:** Extract structured clinical trial search parameters from natural language patient descriptions
+2. **Output Simplification:** Rewrite raw clinical trial text (eligibility criteria, study descriptions, intervention details) into plain language accessible to a non-medical audience
+
+Fine-tuning a small model on these constrained tasks yields much better performance than prompting a general-purpose 2B model, while keeping the model small enough to run in a browser tab on consumer hardware.
+
+### Training Data Generation
+
+Training data is generated entirely on local hardware to avoid API costs and keep the workflow self-contained.
+
+**Hardware:**
+- Primary: RTX 5080 (16GB VRAM) running a Q4-quantized 27B model (Gemma 2 27B or Gemma 3 27B) via llama.cpp
+- Secondary: RTX 3060 Ti available for parallel generation or evaluation runs
+
+**Source Data:**
+- Bulk download of 10,000–20,000 trials from the ClinicalTrials.gov v2 API, sampled across diverse conditions, phases, locations, and intervention types
+- Emphasis on conditions relevant to underserved communities (HIV/AIDS, cancers with demographic disparities, rare diseases)
+
+**Task 1 — Input Parsing Training Pairs**
+
+The 27B model generates synthetic patient descriptions from structured trial data, then the training pair is the description → structured output. The generation prompt instructs the 27B to create varied, realistic patient descriptions at different literacy levels and verbosity, including colloquial language, misspellings, and non-medical phrasing.
+
+Example training pair:
+
+```
+Input: "im a 52 yr old black woman in brooklyn, diagnosed with
+ triple negative breast cancer last year, already did chemo
+ but its back"
+
+Output: {
+ "condition": "triple negative breast cancer",
+ "age": 52,
+ "gender": "female",
+ "race": "black",
+ "location": "Brooklyn, NY",
+ "prior_treatment": ["chemotherapy"],
+ "status": "recurrent"
+}
+```
+
+**Task 2 — Output Simplification Training Pairs**
+
+The 27B model takes raw trial eligibility criteria and study descriptions and rewrites them in plain language. The generation prompt emphasizes accuracy (never misrepresent what a trial requires), completeness (don't omit important criteria), and clarity (assume a high school reading level).
+
+Example training pair:
+
+```
+Input: "Inclusion Criteria: Age >= 18; ECOG performance status 0-1;
+ Histologically confirmed HER2-positive breast cancer;
+ Measurable disease per RECIST v1.1; Adequate organ function
+ as defined by: ANC >= 1500/uL, platelets >= 100,000/uL,
+ total bilirubin <= 1.5x ULN"
+
+Output: "Who can join: You must be 18 or older and able to carry out
+ your normal daily activities without much difficulty. You need
+ a confirmed diagnosis of HER2-positive breast cancer with
+ tumors that can be measured on a scan. Your blood work needs
+ to show that your liver, kidneys, and blood cell counts are
+ in a healthy range — your doctor can check this with standard
+ lab tests."
+```
+
+**Generation Workflow:**
+
+1. Pull trial data from ClinicalTrials.gov API and store as JSON
+2. Write generation prompts for each task (input parsing and output simplification)
+3. Run the 27B model via llama.cpp in batch mode overnight on the 5080
+4. Post-process outputs: validate JSON structure for Task 1, basic quality filtering for Task 2
+5. Manual review of a random subset (~200 examples per task) to catch systematic errors
+6. Adjust prompts and re-generate if quality issues are found
+7. Target: 5,000–10,000 training pairs per task
+
+**Quality Assurance:**
+
+- Hold out 10% of generated data as a test set
+- Generate a small "gold standard" comparison set (~200–500 examples) using a frontier model (Claude batch API, ~$10–20) to benchmark local generation quality
+- Domain review leveraging institutional knowledge of clinical terminology from MSK
+- Automated validation: JSON schema checks for Task 1, readability scoring (Flesch-Kincaid) for Task 2
+
+### Fine-Tuning Process
+
+**Method:** QLoRA (Quantized Low-Rank Adaptation)
+- Keeps the base model weights frozen and trains small adapter matrices
+- Dramatically reduces memory requirements — the full fine-tune runs comfortably on 16GB VRAM
+- Adapter weights are small (10–50MB), making them easy to ship alongside the app
+
+**Tools:**
+- Hugging Face `transformers` + `peft` + `trl` for the training loop, or Unsloth for optimized throughput
+- Base model: `google/gemma-2-2b-it` or `google/gemma-3-1b-it`(instruction-tuned variant)
+- 4-bit quantization via bitsandbytes during training
+
+**Training Configuration (Starting Point):**
+- LoRA rank: 16–32
+- LoRA alpha: 32–64
+- Learning rate: 2e-4 with cosine schedule
+- Batch size: 4–8 (gradient accumulation as needed)
+- Epochs: 3–5 (monitor eval loss for early stopping)
+- Training time estimate: 2–4 hours on the RTX 5080
+
+**Multi-Task Training:**
+Both tasks (input parsing and output simplification) are trained together by prepending a task identifier token to each example. This keeps a single model and single set of LoRA weights for both capabilities.
+
+### Evaluation Framework
+
+Evaluation is structured similarly to a RAG evaluation pipeline, with automated metrics and human review:
+
+**Input Parsing Evaluation:**
+- Exact match rate on extracted fields (condition, age, location, gender)
+- Partial match scoring for conditions (did it get close even if not exact?)
+- MeSH term mapping accuracy — does the extracted condition map to the correct ClinicalTrials.gov vocabulary?
+- Error categorization: missed fields, hallucinated fields, misinterpreted values
+
+**Output Simplification Evaluation:**
+- Factual accuracy: does the plain-language version correctly represent the original criteria? (Scored by comparing against 27B or Claude-generated reference summaries)
+- Readability: Flesch-Kincaid grade level targeting 8th grade or below
+- Completeness: are important eligibility criteria preserved or dropped?
+- Safety check: does the simplification ever make a trial sound more permissive than it actually is? (Critical failure mode)
+
+**Iteration Cycle:**
+1. Train → Evaluate → Identify failure categories → Generate targeted training data for those categories → Retrain
+2. Each cycle can run in a single day on local hardware
+3. Target 3–5 iteration cycles before initial deployment
+
+### Browser Deployment
+
+After fine-tuning:
+
+1. Merge LoRA adapters into the base model
+2. Quantize to int4 using the MLC-LLM or llama.cpp quantization pipeline
+3. Convert to the WebLLM-compatible format (MLC weight format)
+4. Host the quantized model weights on a CDN (GitHub LFS, Hugging Face Hub, or Cloudflare R2)
+5. WebLLM loads the model into the browser on first visit, caches it via the Cache API (~1–2GB for a quantized 2B model)
+6. Subsequent visits load the model from cache — no download needed
+
+## Roadmap
+
+**Phase 1 — Structured Search MVP**
+Build the static site with the structured form interface (Layer 1). No AI, just a well-designed frontend that queries ClinicalTrials.gov directly. Ship it, get feedback from ACT-UP members.
+
+**Phase 2 — Training Data Pipeline**
+Pull trial data, write generation prompts, generate synthetic training data on local hardware. Build the evaluation framework.
+
+**Phase 3 — Fine-Tune and Integrate**
+Train the model, evaluate, iterate. Integrate WebLLM into the app for natural language input (Layer 2) and plain-language results (Layer 3).
+
+**Phase 4 — Community Testing**
+Share with ACT-UP members and patient advocates. Collect feedback on result quality, usability, and trust. Use feedback to generate additional training data for edge cases.
+
+**Phase 5 — Hardening**
+PWA support for offline use. Accessibility audit. Performance optimization. Multilingual fine-tuning.
+
+## Guiding Principles
+
+- **Trust is earned through transparency, not promises.** Open source everything. Let the architecture speak for itself.
+- **The tool works without AI.** AI is an enhancement, not a requirement. Skeptical users lose nothing.
+- **Medical accuracy is non-negotiable.** A misleading simplification is worse than the original jargon. When in doubt, show both.
+- **Build with the community, not for them.** ACT-UP members should be collaborators, testers, and co-designers, not just end users.
+- **This is information access, not medical advice.** The tool helps people find trials. Decisions happen with their doctors.
diff --git a/index.html b/index.html
index 1660076..baecf8c 100644
--- a/index.html
+++ b/index.html
@@ -2,8 +2,8 @@
-
-
+
+
diff --git a/iris-cowork-prompt.md b/iris-cowork-prompt.md
new file mode 100644
index 0000000..3b14f67
--- /dev/null
+++ b/iris-cowork-prompt.md
@@ -0,0 +1,158 @@
+# Cowork Prompt: Build IRIS — Clinical Trial Finder MVP
+
+## Context
+
+Build a static, privacy-first clinical trial search web application called **IRIS**, named after Iris Long — a pharmaceutical chemist who worked at Sloan-Kettering and became a key ACT-UP activist in the 1980s. Her greatest contribution was translating complex clinical trial and drug information into language that patients and activists could understand. This tool continues that mission.
+
+IRIS helps people find relevant clinical trials by providing a clean, accessible interface to ClinicalTrials.gov's public API, with optional AI-powered natural language input and plain-language result summaries — all running entirely in the user's browser with zero data collection.
+
+## What to Build (Phase 1 — Structured Search MVP)
+
+Build a React single-page application with the following:
+
+### Landing Page
+- Clean, minimal design. Warm but professional. Not clinical or cold.
+- A brief dedication to Iris Long explaining who she was and why the tool is named after her. Something like: "Named for Iris Long (1934–), a pharmaceutical chemist and ACT-UP activist who dedicated her career to making clinical trial information accessible to the people who needed it most."
+- A clear, prominent privacy statement in plain language: "Your information never leaves your device. IRIS collects no data, uses no cookies, requires no account, and has no tracking. The only network request is a structured search query to ClinicalTrials.gov's public database."
+- Direct entry into the search interface — no signup, no onboarding, no friction.
+
+### Search Interface (Layer 1 — No AI)
+A structured form with the following fields:
+- **Condition/Disease** — text input with common condition suggestions
+- **Location** — text input (city, state, or zip code) with distance radius selector (25mi, 50mi, 100mi, 200mi, anywhere)
+- **Age** — number input
+- **Gender** — select (any, male, female)
+- **Phase** — multi-select checkboxes (Phase 1, Phase 2, Phase 3, Phase 4, any)
+- **Recruitment Status** — select (recruiting, not yet recruiting, all)
+- **Search button**
+
+All fields optional except condition. The form maps directly to ClinicalTrials.gov v2 API query parameters.
+
+### Natural Language Input (Layer 2 — Future, stub only for now)
+Below the structured form, include a collapsed/expandable section:
+- "Or, describe your situation in your own words"
+- A text area with placeholder text like: "Example: I'm a 52 year old woman in Brooklyn with triple negative breast cancer. I've already done chemotherapy but it came back."
+- A note: "This feature uses a small AI model that runs entirely in your browser. Your words are never sent to any server."
+- **For Phase 1, this section should be visible but disabled with a "Coming soon" badge.** Do not integrate WebLLM yet — just build the UI shell so the design accounts for it.
+
+### Results Display
+When results come back from ClinicalTrials.gov:
+- Display as cards, not a table
+- Each card shows:
+ - **Trial title** — rewritten to be more human-readable if possible (strip excessive acronyms, capitalize properly)
+ - **What it's studying** — brief intervention/treatment description
+ - **Status** — recruiting, not yet recruiting, etc. with a colored badge
+ - **Phase** — with a plain-language tooltip explaining what phases mean
+ - **Location(s)** — nearest site to the user's specified location, with distance if possible
+ - **Who can join** — a brief summary of key eligibility criteria (age range, gender, key inclusion points)
+ - **Contact** — phone/email from the trial record if available
+ - **Link** — "View full details on ClinicalTrials.gov" linking to the trial's page
+- Pagination or infinite scroll for large result sets
+- A count of total results found
+- Sort options: relevance, distance, most recently updated
+
+### Phase Explainer
+Include a small, accessible reference that explains clinical trial phases in plain language:
+- Phase 1: Testing safety and dosage in a small group
+- Phase 2: Testing whether the treatment works and studying side effects
+- Phase 3: Comparing the treatment to existing standard treatments in a large group
+- Phase 4: Monitoring long-term safety after the treatment is approved
+
+### Footer
+- Link to the GitHub repository
+- "IRIS is not medical advice. Always discuss clinical trial options with your healthcare provider."
+- "IRIS is open source and collects no data."
+- A link to learn more about Iris Long and ACT-UP
+
+## Technical Requirements
+
+### Stack
+- **React** (Vite for build tooling)
+- **No backend.** This is a fully static site.
+- **No database, no cookies, no localStorage for user data, no analytics.**
+- **ClinicalTrials.gov v2 API** — `https://clinicaltrials.gov/api/v2/studies` — all queries go directly from the browser. Handle CORS if needed (the API may support CORS directly; if not, note this as a deployment consideration).
+- **Styling:** Tailwind CSS or a minimal CSS approach. No heavy component libraries. The design should feel human and approachable, not like a medical portal or a Silicon Valley SaaS app.
+- **Responsive/mobile-first.** Many users will access this on phones.
+- **Accessible:** Proper ARIA labels, keyboard navigation, semantic HTML, sufficient color contrast.
+- PWA manifest for future offline support (include the manifest now, full offline support comes later).
+
+### ClinicalTrials.gov API Integration
+The v2 API endpoint is: `https://clinicaltrials.gov/api/v2/studies`
+
+Key query parameters to use:
+- `query.cond` — condition/disease
+- `query.locn` — location
+- `filter.overallStatus` — recruitment status (RECRUITING, NOT_YET_RECRUITING, etc.)
+- `filter.phase` — phase filter
+- `filter.sex` — sex/gender filter
+- `filter.age` — age eligibility
+- `pageSize` — results per page (default 10, max 100)
+- `pageToken` — pagination
+- `sort` — sort order
+- `fields` — specify which fields to return to minimize response size
+
+Return fields to request: NCTId, BriefTitle, OfficialTitle, OverallStatus, Phase, BriefSummary, EligibilityCriteria, InterventionName, InterventionType, LocationCity, LocationState, LocationCountry, LocationFacility, CentralContactName, CentralContactPhone, CentralContactEMail, MinimumAge, MaximumAge, Sex, EnrollmentCount, StartDate, CompletionDate, LastUpdatePostDate
+
+### Error Handling
+- If the API is unreachable, show a friendly message: "We couldn't reach ClinicalTrials.gov right now. This might be a temporary issue — please try again in a few minutes."
+- If no results are found, suggest broadening search criteria with specific suggestions (remove location filter, try different condition phrasing, include all phases).
+- Handle rate limiting gracefully.
+
+### Project Structure
+```
+iris/
+├── public/
+│ ├── manifest.json
+│ └── favicon (use an iris flower or similar)
+├── src/
+│ ├── components/
+│ │ ├── SearchForm.jsx
+│ │ ├── NaturalLanguageInput.jsx (stub/disabled)
+│ │ ├── ResultCard.jsx
+│ │ ├── ResultsList.jsx
+│ │ ├── PhaseExplainer.jsx
+│ │ ├── PrivacyStatement.jsx
+│ │ ├── Header.jsx
+│ │ ├── Footer.jsx
+│ │ └── DedicationBanner.jsx
+│ ├── hooks/
+│ │ └── useClinicalTrials.js (API query hook)
+│ ├── utils/
+│ │ └── apiHelpers.js (query parameter mapping, response parsing)
+│ ├── App.jsx
+│ ├── main.jsx
+│ └── index.css
+├── package.json
+├── vite.config.js
+├── tailwind.config.js
+└── README.md
+```
+
+## Design Direction
+
+The visual design should feel:
+- **Warm and trustworthy** — not clinical white with blue accents (that's every hospital portal). Consider warm neutrals, soft purples or teals, generous whitespace.
+- **Simple and unintimidating** — a person in crisis should be able to use this without cognitive overload.
+- **Text-forward** — the content is what matters, not decorative elements.
+- **Clearly not commercial** — no "sign up" CTAs, no premium tiers, no upsells. This is a public good.
+
+Inspiration: think of it as a tool Iris Long herself would have wanted to build — practical, no-nonsense, focused entirely on getting information to people who need it.
+
+## README
+
+The project README should include:
+- What IRIS is and why it exists
+- The dedication to Iris Long with historical context about her role in ACT-UP
+- How the privacy architecture works (explained plainly)
+- How to run locally (`npm install && npm run dev`)
+- How to contribute
+- The project roadmap (structured search → local LLM integration → fine-tuned model → multilingual support)
+- License: MIT
+- A note that this is not medical advice
+
+## What NOT to Build Yet
+- Do NOT integrate WebLLM or any AI model — just build the UI shell for it
+- Do NOT add any server-side component
+- Do NOT add authentication or user accounts
+- Do NOT add analytics, tracking, or telemetry of any kind
+- Do NOT use localStorage to persist user searches or preferences
diff --git a/shared/iris-shared.jsx b/shared/iris-shared.jsx
new file mode 100644
index 0000000..ed90d44
--- /dev/null
+++ b/shared/iris-shared.jsx
@@ -0,0 +1,478 @@
+/* global React */
+// Shared mock data — three realistic trials that demonstrate different states:
+// recruiting / streaming / completed simplification, with varying eligibility fit.
+
+const MOCK_USER = {
+ description: "I'm 58 years old with breast cancer in Boston",
+ fields: { condition: 'breast cancer', location: 'Boston', age: 58, sex: 'FEMALE' },
+};
+
+const MOCK_TRIALS = [
+ {
+ id: 'NCT05952557',
+ title: 'Phase IIIb Study of Ribociclib + ET in Early Breast Cancer',
+ status: 'RECRUITING',
+ phase: 'Phase 3',
+ facility: 'Boston Medical Center',
+ city: 'Boston',
+ state: 'MA',
+ distanceMi: 0.1,
+ intervention: 'Ribociclib + endocrine therapy',
+ enrollmentCount: 4000,
+ fit: 'strong', // strong | partial | weak
+ fitReason: 'HR-positive postmenopausal patients 50–65 — your description matches.',
+ rawSummary:
+ "This is a Phase IIIb open-label study evaluating ribociclib in combination with endocrine therapy in postmenopausal women with HR-positive, HER2-negative early breast cancer following definitive locoregional therapy.",
+ rawEligibility:
+ "Adult female, ≥18 years. HR-positive, HER2-negative breast cancer. Completed surgery. Postmenopausal status. ECOG 0-1. Adequate organ function. No prior CDK4/6 inhibitor.",
+ plain: {
+ summary:
+ "This study tests if adding ribociclib (a targeted cancer drug) to standard hormone treatment helps prevent breast cancer from coming back after surgery.",
+ eligibility:
+ "You may qualify if you're an adult woman who's had surgery for HR-positive, HER2-negative breast cancer. You can't have taken a CDK4/6 inhibitor before.",
+ fitNote:
+ "This trial is looking for HR-positive, HER2-negative breast cancer after surgery. You said you're 58 with breast cancer — to know for sure, ask your oncologist whether yours is HR-positive and HER2-negative.",
+ },
+ contact: { phone: '+1 617-555-0142', email: 'trials@bmc.org' },
+ ctGovUrl: 'https://clinicaltrials.gov/study/NCT05952557',
+ },
+ {
+ id: 'NCT06104020',
+ title: 'Sacituzumab Govitecan in Metastatic Triple-Negative Breast Cancer',
+ status: 'RECRUITING',
+ phase: 'Phase 2',
+ facility: 'Dana-Farber Cancer Institute',
+ city: 'Boston',
+ state: 'MA',
+ distanceMi: 1.4,
+ intervention: 'Sacituzumab govitecan',
+ enrollmentCount: 220,
+ fit: 'partial',
+ fitReason: 'TNBC subtype required — depends on your tumor profile.',
+ rawSummary:
+ "Phase II evaluation of sacituzumab govitecan in patients with metastatic triple-negative breast cancer who have received at least one prior line of systemic therapy.",
+ rawEligibility:
+ "Adult, any sex. Metastatic TNBC confirmed. Prior chemotherapy. ECOG 0-2. Measurable disease per RECIST 1.1.",
+ plain: null, // streaming state — populated by app at runtime
+ contact: { phone: '+1 617-555-0298', email: 'breast-trials@dfci.org' },
+ ctGovUrl: 'https://clinicaltrials.gov/study/NCT06104020',
+ },
+ {
+ id: 'NCT05887492',
+ title: 'Adaptive Radiation Boost in Locally Advanced HER2+ Breast Cancer',
+ status: 'RECRUITING',
+ phase: 'Phase 2',
+ facility: 'Massachusetts General Hospital',
+ city: 'Boston',
+ state: 'MA',
+ distanceMi: 1.7,
+ intervention: 'Adaptive radiation therapy',
+ enrollmentCount: 95,
+ fit: 'partial',
+ fitReason: 'Requires HER2-positive subtype and prior chemo.',
+ rawSummary:
+ "Single-arm Phase II trial of adaptive MR-guided boost radiation in HER2-positive locally advanced breast cancer following neoadjuvant chemotherapy.",
+ rawEligibility:
+ "Adult female. HER2-positive breast cancer. Stage II-III. Completed neoadjuvant chemotherapy. ECOG 0-1.",
+ plain: {
+ summary:
+ "This study tests a more precise form of radiation that adjusts in real time, for women whose breast cancer has grown into nearby tissue and is HER2-positive.",
+ eligibility:
+ "You may qualify if you're an adult woman with HER2-positive breast cancer that has spread to nearby tissue and you've finished chemotherapy before surgery.",
+ fitNote:
+ "This trial requires HER2-positive cancer specifically. If your cancer is HR-positive instead of HER2-positive, this one likely isn't a fit.",
+ },
+ contact: { phone: '+1 617-555-0411', email: 'rad-onc-trials@mgh.harvard.edu' },
+ ctGovUrl: 'https://clinicaltrials.gov/study/NCT05887492',
+ },
+ {
+ id: 'NCT06221340',
+ title: 'Aerobic Exercise During Adjuvant Chemo for Breast Cancer Survivors',
+ status: 'RECRUITING',
+ phase: 'N/A',
+ facility: 'Beth Israel Deaconess',
+ city: 'Boston',
+ state: 'MA',
+ distanceMi: 2.3,
+ intervention: 'Supervised aerobic exercise program',
+ enrollmentCount: 180,
+ fit: 'strong',
+ fitReason: 'Open to most breast cancer patients in active treatment.',
+ rawSummary:
+ "Behavioral intervention study examining the impact of supervised aerobic exercise on chemotherapy tolerance and quality of life in breast cancer patients undergoing adjuvant treatment.",
+ rawEligibility:
+ "Adult, any sex. Breast cancer, any stage. Currently receiving or scheduled for adjuvant chemotherapy. Cleared by oncologist for moderate exercise.",
+ plain: {
+ summary:
+ "This study looks at whether a supervised exercise program helps people tolerate chemotherapy better and feel more like themselves during treatment.",
+ eligibility:
+ "You may qualify if you have breast cancer at any stage and you're receiving (or about to start) chemotherapy after surgery, and your oncologist says exercise is safe for you.",
+ fitNote:
+ "This trial is broadly open — it doesn't require a specific subtype. If you're starting or in chemotherapy, it could be a fit alongside another treatment trial.",
+ },
+ contact: { phone: '+1 617-555-0560', email: 'exercise-study@bidmc.harvard.edu' },
+ ctGovUrl: 'https://clinicaltrials.gov/study/NCT06221340',
+ },
+ {
+ id: 'NCT05790474',
+ title: 'Datopotamab Deruxtecan vs. Chemo in HR+/HER2- Breast Cancer',
+ status: 'RECRUITING',
+ phase: 'Phase 3',
+ facility: 'Brigham and Women\u2019s Hospital',
+ city: 'Boston',
+ state: 'MA',
+ distanceMi: 1.2,
+ intervention: 'Datopotamab deruxtecan',
+ enrollmentCount: 700,
+ fit: 'strong',
+ fitReason: 'HR-positive, HER2-low or negative — common subtype at your age.',
+ rawSummary:
+ "Randomized Phase III study comparing datopotamab deruxtecan (Dato-DXd) to investigator's choice chemotherapy in patients with inoperable or metastatic HR-positive, HER2-low or negative breast cancer.",
+ rawEligibility:
+ "Adult. HR+, HER2-low or HER2-negative breast cancer. Inoperable or metastatic. 1-2 prior chemo regimens for advanced disease. ECOG 0-1.",
+ plain: {
+ summary:
+ "This study compares a newer antibody-drug therapy against standard chemotherapy in people with advanced HR-positive breast cancer.",
+ eligibility:
+ "You may qualify if you have HR-positive breast cancer that's advanced or has spread, and you've already had one or two rounds of chemotherapy for it.",
+ fitNote:
+ "This is for advanced or metastatic HR-positive disease that's been treated before. If your cancer is early stage or recently diagnosed, the first trial here is more likely your fit.",
+ },
+ contact: { phone: '+1 617-555-0823', email: 'dana-trials@bwh.harvard.edu' },
+ ctGovUrl: 'https://clinicaltrials.gov/study/NCT05790474',
+ },
+];
+
+// Simulate the streaming token-by-token reveal that the real app produces.
+// Components opt in by passing a trial whose plain==null and calling useStreamedSimplification.
+function useStreamedSimplification(trial, { delayMs = 700, charsPerTick = 4 } = {}) {
+ const [state, setState] = React.useState(() => {
+ if (trial.plain) return { status: 'complete', plain: trial.plain };
+ return { status: 'queued', plain: { summary: '', eligibility: '', fitNote: '' } };
+ });
+
+ React.useEffect(() => {
+ if (trial.plain) return;
+ const target = {
+ summary:
+ "This study tests a newer targeted drug for triple-negative breast cancer that has spread, in patients who've already had at least one chemotherapy.",
+ eligibility:
+ "You may qualify if you have triple-negative breast cancer that has spread to other parts of the body, and you've had at least one round of chemotherapy already.",
+ fitNote:
+ "This trial is for triple-negative breast cancer that has spread. To know if it fits, you'll need to confirm your tumor's HR/HER2 status with your oncologist.",
+ };
+ let i = 0;
+ let cancelled = false;
+ const start = setTimeout(function tick() {
+ if (cancelled) return;
+ i += charsPerTick;
+ const total = target.summary.length + target.eligibility.length + target.fitNote.length;
+ const summary = target.summary.slice(0, Math.min(i, target.summary.length));
+ let rest = i - target.summary.length;
+ const eligibility = rest > 0 ? target.eligibility.slice(0, Math.min(rest, target.eligibility.length)) : '';
+ rest = rest - target.eligibility.length;
+ const fitNote = rest > 0 ? target.fitNote.slice(0, rest) : '';
+ setState({ status: i >= total ? 'complete' : 'streaming', plain: { summary, eligibility, fitNote } });
+ if (i < total) setTimeout(tick, 32);
+ }, delayMs);
+ return () => { cancelled = true; clearTimeout(start); };
+ }, [trial.id]);
+
+ return state;
+}
+
+// Status pill
+function StatusPill({ status }) {
+ const map = {
+ RECRUITING: { bg: 'var(--signal-good-bg)', fg: 'var(--signal-good)', label: 'Recruiting' },
+ NOT_YET_RECRUITING: { bg: 'var(--signal-warn-bg)', fg: 'var(--signal-warn)', label: 'Not yet recruiting' },
+ COMPLETED: { bg: 'var(--p-200)', fg: 'var(--p-700)', label: 'Completed' },
+ };
+ const m = map[status] || map.RECRUITING;
+ return (
+
+
+ {m.label}
+
+ );
+}
+
+// Fit meter — the new differentiator. Visualizes how the patient's described
+// situation maps to the trial's eligibility, using the simplifier output.
+function FitMeter({ fit, size = 'md' }) {
+ const map = {
+ strong: { bars: 3, label: 'Likely fit', color: 'var(--signal-good)' },
+ partial: { bars: 2, label: 'May fit', color: 'var(--signal-warn)' },
+ weak: { bars: 1, label: 'Unclear fit', color: 'var(--p-500)' },
+ };
+ const m = map[fit] || map.weak;
+ const barH = size === 'sm' ? 6 : 9;
+ const barW = size === 'sm' ? 3 : 4;
+ return (
+
+
+ {[1, 2, 3].map(n => (
+
+ ))}
+
+ {m.label}
+
+ );
+}
+
+// Local-AI badge — front-and-center about the privacy story.
+function LocalAIBadge({ active = false, label = 'Gemma 2 2B · on-device' }) {
+ return (
+
+
+ {label}
+
+ );
+}
+
+// Header used by all variations — single, dense, with privacy chip.
+function IrisHeader({ compact = false }) {
+ return (
+
+
+ iris
+ clinical trial finder
+
+
+
+
+ on-device only
+
+
+
+
+ );
+}
+
+// Search bar (unified) used by all variations
+function IrisSearchBar({ user = MOCK_USER, dense = false }) {
+ return (
+
+
+ {/* Mode toggle */}
+
+ {['Describe in your words', 'Structured form'].map((m, i) => (
+
+ ))}
+
+ );
+}
+
+// Streaming text with shimmer placeholder
+function StreamingText({ value, status, placeholderLines = 2 }) {
+ if (status === 'queued') {
+ // Rendered inside
in some call sites — keep everything inline-level.
+ return (
+
+ {Array.from({ length: placeholderLines }).map((_, i) => (
+
+ ))}
+
+ );
+ }
+ return (
+
+ {value}
+ {status === 'streaming' && (
+
+ )}
+
+ );
+}
+
+Object.assign(window, {
+ MOCK_USER, MOCK_TRIALS, useStreamedSimplification,
+ StatusPill, FitMeter, LocalAIBadge, IrisHeader, IrisSearchBar,
+ ResultsToolbar, ActionRow, StreamingText,
+});
diff --git a/src/App.jsx b/src/App.jsx
index 64ee962..83666b7 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,10 +1,7 @@
import { lazy, Suspense, useState } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import Header from './components/Header'
-import DedicationBanner from './components/DedicationBanner'
-import PrivacyStatement from './components/PrivacyStatement'
-import SearchForm from './components/SearchForm'
-import NaturalLanguageInput from './components/NaturalLanguageInput'
+import UnifiedSearchBar from './components/UnifiedSearchBar'
import ResultsList from './components/ResultsList'
import Footer from './components/Footer'
import { resolveModelKey } from './utils/nlpModels'
@@ -69,11 +66,12 @@ function IrisApp() {
return (
-
-
-
-
-
+
+
{searchParams && (
-
- Named for{' '}
- Iris Long (1934–2026), a
- pharmaceutical chemist and ACT-UP activist who dedicated her career to making clinical
- trial information accessible to the people who needed it most.
-