Skip to content

Commit 7da9c5a

Browse files
authored
Merge pull request #1 from johnoooh/redesign/phase-1-visual-system
Visual redesign + unified search bar + NL UX improvements
2 parents 334c48e + 3c2e750 commit 7da9c5a

30 files changed

Lines changed: 3132 additions & 318 deletions

.github/workflows/ci.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
9+
# Cancel in-progress runs for the same ref so a force-push doesn't queue
10+
# stale CI.
11+
concurrency:
12+
group: ci-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
build-test:
17+
name: build-test
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout
21+
uses: actions/checkout@v4
22+
23+
- name: Setup Node
24+
uses: actions/setup-node@v4
25+
with:
26+
# vite.config.js conditionally emits --no-experimental-webstorage
27+
# for Node >= 22, so CI must run Node 22 to match local dev.
28+
node-version: 22
29+
cache: 'npm'
30+
31+
- name: Install dependencies
32+
run: npm ci
33+
34+
# Lint currently fails on pre-existing errors in main (e.g. vite.config.js
35+
# 'process' is not defined, useNLP exhaustive-deps warnings). Run as a
36+
# non-blocking informational step until those are fixed in a follow-up;
37+
# build + tests are the real merge gate.
38+
- name: Lint (non-blocking)
39+
run: npm run lint
40+
continue-on-error: true
41+
42+
- name: Test
43+
run: npm run test:run
44+
45+
- name: Build
46+
run: npm run build
47+
48+
# Upload the built dist/ as a workflow artifact so reviewers can
49+
# download and serve it locally (e.g. `npx serve dist`) to verify the
50+
# production bundle of the PR's exact code. Retention is short to
51+
# avoid clutter; the artifact is for review-time only, not archival.
52+
- name: Upload built site
53+
uses: actions/upload-artifact@v4
54+
with:
55+
name: dist-${{ github.event.pull_request.number || github.sha }}
56+
path: dist/
57+
retention-days: 7
58+
if-no-files-found: error

Classification Harness.html

Lines changed: 338 additions & 0 deletions
Large diffs are not rendered by default.

Handoff.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# IRIS — Claude Code handoff
2+
3+
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.
4+
5+
---
6+
7+
## What's settled
8+
9+
**Direction:** Triage two-pane layout (list + detail). Mobile collapses to list + tap-to-sheet.
10+
11+
**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`.
12+
13+
**Locked tweak values from the prototype:**
14+
- Accent: `iris` (the violet)
15+
- Density: `comfy`
16+
- List width: `400px`
17+
18+
**New patterns introduced:**
19+
- **Fit meter** — three-bar visualization (Likely / May / Unclear fit) shown per row and per detail pane
20+
- **Two-stage AI pipeline** — fast classify pass, then on-demand full simplification (still being validated, see harness)
21+
- **Local-AI badge** — always-visible mono pill in header announcing on-device model
22+
- **Compare** — pin up to 3 trials; sticky bar; compare-view page is later work
23+
- **Streaming shimmer** — placeholder lines shimmer until tokens land, then fade in
24+
- **Unified search** — NL + structured form become one input with a mode toggle
25+
26+
---
27+
28+
## Files to reference
29+
30+
| File | Role |
31+
|---|---|
32+
| `IRIS Triage.html` | Final chosen direction. Reference for layout, spacing, microcopy. |
33+
| `styles/tokens.css` | Drop-in CSS variables. Colors, fonts, shadows, shimmer keyframes. |
34+
| `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. |
35+
| `Classification Harness.html` | Standalone rig for validating the two-stage classify before wiring it in. |
36+
37+
---
38+
39+
## Integration plan (recommended order)
40+
41+
### Phase 1 — Visual system (low risk, ship first)
42+
43+
1. **Add Google Fonts link** to `<head>`:
44+
```html
45+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,500;8..60,600;8..60,700&family=Inter+Tight:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap">
46+
```
47+
2. **Drop in `styles/tokens.css`** as new variables alongside existing ones; gradually replace the old palette.
48+
3. **Replace header markup** with the new dense header + privacy chip + local-AI badge.
49+
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.
50+
5. **Compress the privacy paragraph** into the on-device chip + an expandable details element with the long form.
51+
52+
Ship after Phase 1 — already a meaningful UX improvement, no logic changes.
53+
54+
### Phase 2 — Row format + accordion or two-pane
55+
56+
The current cards are full-width prose blocks. Swap for compact rows:
57+
58+
```
59+
[ ☐ ] Phase IIIb Study of Ribociclib + ET in Early Breast Cancer
60+
▌▌▌ Likely fit · 0.1 mi · Phase 3
61+
```
62+
63+
Two implementation paths — pick one based on engineering appetite:
64+
65+
- **(a) Accordion in place** — easier. Click a row, it expands inline with the detail content. Works at any width. No layout fork.
66+
- **(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).
67+
68+
Recommend **(a) for first pass**, **(b) when you're ready to invest in the layout fork.**
69+
70+
### Phase 3 — Two-stage classification
71+
72+
**Don't ship this until the harness validates it.** See [Classification harness](#classification-harness) below.
73+
74+
When ready:
75+
1. After search returns trial list, immediately render rows with title/distance/phase only — no fit meter yet.
76+
2. Kick off `classifyAll(trials, userDesc)` with concurrency 2–3.
77+
3. As each verdict returns, update that row's fit meter in place.
78+
4. Show `evaluating fit · 7 of 20` indicator in the toolbar while running.
79+
5. Once stage 1 is complete, default sort flips to "Best fit"; collapse UNLIKELY trials under a `12 less likely matches` disclosure.
80+
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.
81+
82+
### Phase 4 — Compare
83+
84+
1. `Set<nctId>` in memory, max size 3.
85+
2. Checkbox on each row.
86+
3. Sticky bar appears when set is non-empty: `[ 2 in compare ] [ Compare → ]`.
87+
4. Compare view itself is later — start with a placeholder route.
88+
89+
### Phase 5 — Mobile polish
90+
91+
1. Bottom-sheet pattern: tap row → sheet slides up with full detail. Backdrop dismiss + close button + drag handle.
92+
2. Sticky compare bar at bottom.
93+
3. Compact search summary chip replaces the full search bar on mobile (tap to expand).
94+
95+
### Phase 6 — Persistence (optional, session-only)
96+
97+
1. `sessionStorage` only — no PII to disk, in keeping with the privacy story.
98+
2. Save: search query, comparing set, currently-selected trial.
99+
3. Clear on a "Start over" button.
100+
101+
---
102+
103+
## Classification harness
104+
105+
`Classification Harness.html` is a standalone page with a mocked `classifyOne()` that simulates 200–1500ms latency and ~85% parse success.
106+
107+
**To validate the real model:**
108+
109+
1. Open the harness.
110+
2. Replace the body of `classifyOne()` with your live on-device call — the function signature is `(prompt, trial) => Promise<{ verdict, reason, raw, latencyMs }>`.
111+
3. Run with the included fixture (6 trials, with expected verdicts).
112+
4. Check the stats row: parse rate, avg latency, max latency, agreement with expected.
113+
114+
**Pass criteria for moving to Phase 3:**
115+
- Parse rate ≥ 90% on 50+ real trials
116+
- Avg latency < 1.5s per trial on a mid-range laptop
117+
- Agreement ≥ 80% on a labeled held-out set
118+
- No catastrophic UNLIKELY false-negatives (a viable trial ranked as UNLIKELY)
119+
120+
**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.`).
121+
122+
**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.
123+
124+
---
125+
126+
## Things explicitly out of scope for this pass
127+
128+
- Compare view (3-up side-by-side) — deferred
129+
- Account / login / save across sessions — deferred, conflicts with privacy story
130+
- Server-side fallback for the model — not consistent with on-device promise
131+
- Distance map view — nice-to-have, not on the critical path
132+
- Question-prep checklist — separate feature, separate PRD
133+
134+
---
135+
136+
## Open questions for product
137+
138+
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")?
139+
2. **UNLIKELY default behavior** — collapse them, or just sort to bottom? Risk of hiding viable trials if the model is wrong.
140+
3. **Compare view** — which dimensions matter most? Probably: phase, distance, drug/intervention, eligibility deltas, contact info.
141+
4. **Fit meter on mobile rows** — keep at full size or shrink to just the bars? Currently same component, both contexts.
142+
143+
---
144+
145+
## Microcopy already drafted
146+
147+
- Header sub: `clinical trial finder`
148+
- On-device chip: `on-device only`
149+
- Local-AI badge: `Gemma 2 2B · on-device`
150+
- Mode toggle: `Describe in your words` / `Structured form`
151+
- Mode toggle pill: `AI · on-device`
152+
- Understood section: `understood:` (mono, lowercase)
153+
- Section labels in detail: `What this study is testing`, `Who can join`
154+
- Fit panel caption: `based on what you described`
155+
- Toolbar count: `20 trials · near Boston · within 50 mi · recruiting`
156+
- Sort options: `Best fit`, `Distance`, `Phase`, `Most recent`
157+
- Compare bar (mobile): `**N** in compare` / `Compare →`
158+
- Sheet handle: drag affordance only, no label
159+
- Fit verdicts: `Likely fit`, `May fit`, `Unclear fit`

IRIS Redesign.html

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width,initial-scale=1" />
6+
<title>IRIS — redesign explorations</title>
7+
<link rel="stylesheet" href="styles/tokens.css" />
8+
<style>
9+
html, body { margin: 0; height: 100%; }
10+
body { font-family: 'Inter Tight', system-ui, sans-serif; background: #f0eee9; }
11+
</style>
12+
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
13+
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
14+
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
15+
</head>
16+
<body>
17+
<div id="root"></div>
18+
19+
<script type="text/babel" src="design-canvas.jsx"></script>
20+
<script type="text/babel" src="shared/iris-shared.jsx"></script>
21+
<script type="text/babel" src="variations/editorial.jsx"></script>
22+
<script type="text/babel" src="variations/triage.jsx"></script>
23+
<script type="text/babel" src="variations/dossier.jsx"></script>
24+
<script type="text/babel" src="variations/mobile-triage.jsx"></script>
25+
<script type="text/babel" src="variations/mobile-dossier.jsx"></script>
26+
<script type="text/babel" src="variations/shimmer-showcase.jsx"></script>
27+
28+
<script type="text/babel" data-presets="react">
29+
function Note({ children }) {
30+
return (
31+
<div style={{
32+
fontSize: 12, color: 'rgba(60,50,40,0.75)', lineHeight: 1.55,
33+
maxWidth: 720, marginBottom: 8,
34+
}}>{children}</div>
35+
);
36+
}
37+
38+
function App() {
39+
return (
40+
<DesignCanvas
41+
title="IRIS — redesign explorations"
42+
subtitle="Three results-layout directions on an evolved warm-parchment system. Same data, same streaming-AI behavior, different scanning models."
43+
>
44+
<DCSection
45+
id="system"
46+
title="Design system shifts"
47+
subtitle="What's the same, what's evolved"
48+
>
49+
<DCArtboard id="legend" label="System notes" width={520} height={760}>
50+
<div style={{ padding: '24px 26px', fontFamily: 'Inter Tight, sans-serif', fontSize: 13, lineHeight: 1.55, color: 'var(--p-900)', background: 'var(--p-50)', height: '100%', overflow: 'auto' }}>
51+
<h2 style={{ fontFamily: 'Source Serif 4, serif', fontSize: 22, margin: '0 0 4px', color: 'var(--p-950)', letterSpacing: '-0.01em' }}>What I changed</h2>
52+
<p style={{ color: 'var(--p-700)', margin: '0 0 18px', fontSize: 12.5 }}>Same warm parchment soul. Sharper hierarchy, an iris-violet accent, and a built-in scanning aid (the fit meter).</p>
53+
54+
<h4 style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--iris-700)', margin: '0 0 8px' }}>Type</h4>
55+
<p style={{ margin: '0 0 4px' }}>
56+
<span style={{ fontFamily: 'Source Serif 4, serif', fontSize: 22, fontWeight: 600 }}>Source Serif 4</span>
57+
<span style={{ color: 'var(--p-700)', fontSize: 12, marginLeft: 8 }}>headlines &amp; trial titles</span>
58+
</p>
59+
<p style={{ margin: '0 0 4px' }}>
60+
<span style={{ fontFamily: 'Inter Tight, sans-serif', fontSize: 16, fontWeight: 500 }}>Inter Tight</span>
61+
<span style={{ color: 'var(--p-700)', fontSize: 12, marginLeft: 8 }}>UI &amp; body</span>
62+
</p>
63+
<p style={{ margin: '0 0 18px' }}>
64+
<span style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 13 }}>JetBrains Mono</span>
65+
<span style={{ color: 'var(--p-700)', fontSize: 12, marginLeft: 8 }}>technical labels (echoes the privacy/local-AI ethos)</span>
66+
</p>
67+
68+
<h4 style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--iris-700)', margin: '0 0 8px' }}>Color</h4>
69+
<div style={{ display: 'flex', gap: 6, marginBottom: 4 }}>
70+
{['var(--p-50)','var(--p-100)','var(--p-200)','var(--p-300)','var(--p-500)','var(--p-700)','var(--p-900)','var(--p-950)'].map(c => (
71+
<div key={c} style={{ width: 36, height: 36, background: c, borderRadius: 4, border: '1px solid rgba(0,0,0,0.05)' }} />
72+
))}
73+
</div>
74+
<p style={{ margin: '0 0 12px', color: 'var(--p-700)', fontSize: 11 }}>parchment — kept &amp; lightly retuned</p>
75+
<div style={{ display: 'flex', gap: 6, marginBottom: 4 }}>
76+
{['var(--iris-50)','var(--iris-100)','var(--iris-300)','var(--iris-500)','var(--iris-700)','var(--iris-900)'].map(c => (
77+
<div key={c} style={{ width: 36, height: 36, background: c, borderRadius: 4, border: '1px solid rgba(0,0,0,0.05)' }} />
78+
))}
79+
</div>
80+
<p style={{ margin: '0 0 18px', color: 'var(--p-700)', fontSize: 11 }}>iris — new, named for the namesake. CTA + AI accent.</p>
81+
82+
<h4 style={{ fontFamily: 'JetBrains Mono, monospace', fontSize: 10, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--iris-700)', margin: '0 0 8px' }}>New patterns</h4>
83+
<ul style={{ margin: 0, paddingLeft: 18 }}>
84+
<li><b>Fit meter</b> — visualizes how the user's described situation maps to eligibility. Three states: likely / may / unclear.</li>
85+
<li><b>Streaming shimmer</b> — AI summary placeholders shimmer until tokens arrive, then fade in. Honest about latency.</li>
86+
<li><b>Local-AI badge</b> — mono-set, always visible. Pulse dot when the model is working.</li>
87+
<li><b>Unified search</b> — NL and structured form become one input with a mode toggle, not two stacked sections.</li>
88+
<li><b>Compare</b> — pin up to 3 trials. Shared toolbar pill across variations.</li>
89+
<li><b>Share / Save / Print</b> on every card. Save is session-only, no PII persisted.</li>
90+
</ul>
91+
</div>
92+
</DCArtboard>
93+
</DCSection>
94+
95+
<DCSection
96+
id="results"
97+
title="Results layout — three directions"
98+
subtitle="The primary concern was scanability. Each variation answers it differently."
99+
>
100+
<DCArtboard id="editorial" label="A · Editorial list" width={920} height={900}>
101+
<EditorialVariation />
102+
</DCArtboard>
103+
<DCArtboard id="triage" label="B · Triage two-pane" width={1180} height={900}>
104+
<TriageVariation />
105+
</DCArtboard>
106+
<DCArtboard id="dossier" label="C · Dossier grid" width={1100} height={900}>
107+
<DossierVariation />
108+
</DCArtboard>
109+
</DCSection>
110+
111+
<DCSection
112+
id="mobile"
113+
title="Mobile views — rebuilt for the form factor"
114+
subtitle="Editorial works as-is on phone (single-column already). Triage and Dossier needed mobile-native rethinks: triage → tap-to-sheet, dossier → filter chips + collapsible cards. Sticky compare bar for both."
115+
>
116+
<DCArtboard id="editorial-m" label="A · Editorial (mobile) — direct port" width={390} height={760}>
117+
<EditorialVariation />
118+
</DCArtboard>
119+
<DCArtboard id="triage-m" label="B · Triage (mobile) — list + bottom sheet" width={390} height={760}>
120+
<MobileTriageVariation />
121+
</DCArtboard>
122+
<DCArtboard id="dossier-m" label="C · Dossier (mobile) — filter chips + collapsible" width={390} height={760}>
123+
<MobileDossierVariation />
124+
</DCArtboard>
125+
</DCSection>
126+
127+
<DCSection
128+
id="shimmer"
129+
title="AI streaming — what loading looks like"
130+
subtitle="The local model evaluates trials serially, which takes time. Toggle between Queued and In-progress to see how the wait reads."
131+
>
132+
<DCArtboard id="shimmer" label="AI streaming states" width={760} height={900}>
133+
<ShimmerShowcase />
134+
</DCArtboard>
135+
</DCSection>
136+
</DesignCanvas>
137+
);
138+
}
139+
140+
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
141+
</script>
142+
</body>
143+
</html>

0 commit comments

Comments
 (0)