Design

ConsultBridge.

A family physician gets two or three specialist consult letters a day, and each one costs about fourteen minutes of reading, reconciling and re-typing. ConsultBridge is my team's answer from the Hackers & Healers AI in Healthcare Co-Design Hackathon (June 2026): open a letter, and a model turns it into medication changes carrying their own evidence, follow-up tasks, an EMR note and a patient letter — every one of them reviewed, edited and committed by the physician. My teammates built the pipeline; I built the interface. This page takes the interface apart.

The components — rebuilt from source, at the app's own proportions 21 pieces · desktop app, no phone layout — the rail scrollsso this one plays
The components, filmed at the size the app draws them. Everything moving in it is the piece's own animation, captured as it ran.
WordmarkChrome
3processed 40msaved
Throughput statChrome
Operating · Cloud processing
Status pillChrome
ModelMistral NeMo
EndpointRailway · OpenRouter
PrivacyCloud · TLS encrypted
Processing statusChrome
Search fieldChrome
Filter menuChrome
Inbox 20 Letters
Loading library…
InboxLeft pane
Consult - History and Phy. — Heart Failure (HFrEF) - Cardiology Consult 205 words · #900

Thank you for referring Mrs. Thompson, a 74-year-old female with a longstanding history of hypertension, type 2 diabetes (on metformin 1000mg BID), and newly diagnosed heart failure with reduced ejection fraction (HFrEF, EF 35%).

ASSESSMENT & PLAN

- Initiating Entresto (sacubitril/valsartan) 49/51mg BID. Discontinue her current lisinopril 10mg — do not overlap, wait 36 hours before starting Entresto.

- Adding empagliflozin (Jardiance) 10mg daily — excellent evidence in HFrEF and also addresses her diabetes.

- Increasing furosemide from 20mg to 40mg daily for volume management.

- Please monitor potassium and creatinine in 2 weeks given medication changes.

Consult letterMiddle pane
Summarize 2.0s
Medication reconciliation
Follow-up tasks
Section shellRight pane
Generated Result Reconciling medications…
…"action": "START", "drug_name": "Entresto (sacubitril/valsartan)", "dosage": "49/51mg BID"
StreamingRight pane
Generated Result Queued
Queued for processing — another letter is being analyzed. It'll start automatically.
QueuedRight pane
STOP lisinopril(Zestril)
OLD
—
NEW
10mg daily
REF
lisinopril 10mg — do not overlap, wait 36 hours before starting Entresto.
START Entresto (sacubitril/valsartan)(Diovan)
OLD
—
NEW
49/51mg BID
REF
ASSESSMENT & PLAN: - Initiating Entresto (sacubitril/valsartan) 49/51mg BID.
DIN copied to clipboard
Medication reconciliationRight pane
SkeletonRight pane
Generated Result Backend error
Couldn't reach the backend
Failed to fetch — consultbridge-production.up.railway.app
Backend errorRight pane
Patient Letter 2.0s
Dear Margaret Thompson, Thank you for coming in today. We've made some adjustments to your medications based on your heart condition. You'll now take Entresto instead of lisinopril, but remember not to overlap them – wait 36 hours before starting Entresto. We're also adding Jardiance to help manage both your diabetes and heart failure. Your furosemide dose has been increased for better fluid management. Please keep an eye on your potassium levels over the next two weeks due to these changes.
Mrs. Thompson is a 74-year-old female with hypertension, type 2 diabetes, and newly diagnosed heart failure with reduced ejection fraction (HFrEF). On examination today, she was hemodynamically stable with BP 138/82, HR 76 bpm regular. Lisinopril has been discontinued and Entresto started at 49/51mg BID after a 36-hour washout period.
Patient letterRight pane
  • @Lab · in 2 weeksdue Jul 4, 2026
    Task
    Book potassium + creatinine blood work within 2 weeks
  • @MOA · in 12 weeksdue Sep 12, 2026
    Task
    Schedule repeat echocardiogram in 3 months
Follow-up tasksRight pane
Medication reconciliation
No related information detected
Empty stateRight pane
74-year-old female with hypertension, type 2 diabetes, and newly diagnosed heart failure with
SummarizeRight pane
Commit CTARight pane
Commit dialogRight pane
inbox4 Changes3 tasks14 mins
med actionsSTOPSTARTMODIFYINCREASEDECREASE
diff rowsOLDNEWREF
tasksTaskDIN #02049333
Chip familyTokens

Three panes

A specialist's consult letter is a wall of prose hiding the same few questions every time: what changed, what do I need to do, and what does the patient need to hear. ConsultBridge answers them on one screen, and the layout is the argument. The inbox on the left is the letter library — rows carry a specialty and a word count until they've been processed, and swap to their outcome (4 Changes, 3 tasks) once they have. The letter in the middle is the source of truth, untouched. The generated result on the right is the work: a summary, the medication changes, the patient letter, the tasks.

The right pane shows its work at every step. Every medication change is a diff — OLD, NEW, and a REF row whose text is the specialist's own sentence; hover it and that sentence lights up in the middle pane, click and the letter scrolls to it. Every change takes an explicit Approve / Defer / Reject, the patient letter is editable in place, and the commit button won't enable until each change has a decision. The model drafts; the physician decides. That division of labour is the whole product.

One honest detail in that diff: the OLD row is always an em dash. The backend reports the action to take, and only that — so the row stays empty and the REF line carries the whole justification. It's exactly the kind of gap a demo is tempted to paper over.

One token file

Every colour, radius, shadow and easing in the pieces above comes from one file: tokens.css, 64 custom properties, the app's single source of truth. Every value in it has a provenance. The surfaces and chip pairings are transcribed from the Figma spec; the four motion curves were measured frame-by-frame from reference captures the repo keeps notes on — a voice-recording pill, a sandbox boot card, a recurrence picker, a delete dialog, each broken down to per-frame timings before any component borrowed its curve.

tokens.csscss
/* tokens.css — motion (measured from replicas). Every curve in the app   is one of these four; nothing animates on an ad-hoc bezier. */--out: cubic-bezier(.16, 1, .3, 1);     /* entrances / open / reveal */--in: cubic-bezier(.4, 0, 1, 1);        /* exits / close */--spring: cubic-bezier(.34, 1.56, .64, 1);   /* pop overshoot */--spring-soft: cubic-bezier(.34, 1.3, .5, 1);

This page ships that exact file. The shelf's pieces are scoped under a single block that is tokens.css verbatim, Inter and all. If a chip colour looks slightly off to you here, it's off in the app too.

Grounding

The colour system's real job is cross-pane linking. A highlight ships as a triad — a resting wash, a -strong hover shade, an -ink for text — and the medication chips take their colours straight from those triads:

tokens.csscss
/* tokens.css — the letter <-> AI link colours. A highlight is a triad:   the resting wash, a -strong hover/flash shade, an -ink for chip text.   The med chips own no colours at all — they alias the triads, so a STOP   chip and the sentence it cites can never drift apart. */--hl-red: #ffd6d9;    --hl-red-strong: #ffb3b9;    --hl-red-ink: #b23b36;--hl-green: #e9f8f1;  --hl-green-strong: #c8eedd;  --hl-green-ink: #0a8a5e;--hl-cream: #fef7eb;  --hl-cream-strong: #fbe9c8;  --hl-cream-ink: #9a6a13;--hl-blue: #8dc6f9;   --hl-blue-strong: #6fb4f4;   --hl-blue-ink: #0f3f6b;​--med-stop-bg: var(--hl-red);     --med-stop-ink: var(--hl-red-ink);--med-start-bg: var(--hl-green);  --med-start-ink: var(--hl-green-ink);--med-modify-bg: var(--hl-cream); --med-modify-ink: var(--hl-cream-ink);--med-dose-bg: var(--hl-blue);    --med-dose-ink: var(--hl-blue-ink);

So the red STOP chip, the red wash under "lisinopril 10mg — do not overlap", and the red ink that chip types in are one decision, aliased into three places.

The more interesting half is that each highlight is made the moment the model answers. A letter arrives as an undifferentiated blob of text — real mtsamples transcriptions have no paragraph breaks at all, so they're first split into sections by anchoring on ALL-CAPS headings followed by a colon. Then, for each medication change, the app goes looking for the evidence the model quoted and marks it in place:

letters.jsjs
// letters.js — attachHighlights, compressed. Nothing in the letter is// pre-marked: for each medication the model returns, its evidence is// located in the letter text and a highlight is injected there. Matching// is best-first — the quoted phrase, then the raw evidence, then the drug// name alone, because mtsamples evidence often spans a section heading// and won't survive re-blocking, while the drug name still will.const candidates = [m.phrase, m.evidence, m.drug_name].filter(Boolean)​for (const cand of candidates) {  const idx = block.text.toLowerCase().indexOf(cand.toLowerCase())  if (idx === -1) continue  const overlap = used[bi].find((u) => start < u.end && end > u.start)  if (overlap) idRemap[id] = overlap.id   // two meds, one sentence  else block.hl.push({ id, phrase: block.text.slice(start, end), color })  placed = true}

The fallback ladder is the tell of something built against real data. A model quoting a clean referral letter gives you a sentence you can find verbatim; a model quoting mtsamples gives you a fragment that spans a heading the re-blocker just moved. Falling back to the drug name alone means the link degrades to roughly here instead of vanishing. And when two medications cite the same sentence — an Entresto start and a lisinopril stop, in one line — the second can't get its own span, so it's remapped onto the first and both rows still scroll to the same evidence.

Designing the wait

The pipeline is slow in a way a demo can't hide: a long letter through a 12B model is a 60-second stare. Almost every interesting decision in the right pane is about that minute. Four sections share one clock and three states — queued, processing, done — with a grey ring while waiting, a spinning arc and a live rolling-digit timer while working, a springing check when finished. When a newer section completes the older one demotes: its ring slides away, its timer fades, and the title glides left, so only the most recently finished section keeps its time on screen as a receipt. The Section shell piece on the shelf plays that loop live.

Two states exist purely because the backend is real. Only one letter is processed at a time, so selecting a second one puts you in an explicit Queued state that names the wait and promises to start itself. And when the backend simply isn't there, the pane names the endpoint it couldn't reach and offers Retry — during a hackathon demo, "couldn't reach the backend" is a state you will be in, on stage.

My favourite detail is the progress bar, which has to span two incompatible kinds of waiting — a slow, real, server-side analysis, then a fast, cosmetic reveal:

AIPane.jsxjsx
// AIPane.jsx. The backend takes 60s+ on a long letter; the reveal// choreography takes ~8. Giving each its own bar would rewind the// progress the reader had already watched fill, so the analyze phase// owns 0–90% and the reveal owns the last 10% — one monotonic bar// across two completely different kinds of waiting.const barPct = liveError || queued  ? 0  : preResult    ? (live?.progress || 0)    : 0.9 + progress * 0.1

Giving each phase its own bar would rewind progress the reader had already watched fill. Ninety percent for the wait, ten for the show, and the bar only ever moves right. The same instinct runs through the rest: the leading section's ring spins the instant analysis starts, rather than showing four idle grey rings for a minute; and a cached result skips the reveal animation entirely, because nothing was recomputed and pretending otherwise would be a small lie the interface told every time you revisited a letter.

Where it stands

It's a hackathon build, and this page says so plainly. The default build runs on a bundled corpus rather than a server — but it's worth being precise about what that means, because the app is precise about it in code:

api.jsjs
/* api.js — the app's single door to backend data.​   Two implementations sit behind it:     · MOCK (default) — src/lib/mockApi.js, served from the bundled corpus of       real letters + real pipeline outputs. No backend, no Ollama, no network.     · LIVE — the ConsultBridge FastAPI backend at VITE_API_BASE.​   Flip with VITE_USE_MOCK in app/.env (`false` → live backend). */​export const USE_MOCK = String(import.meta.env.VITE_USE_MOCK ?? 'true') !== 'false'

One door, two implementations, one environment variable. And the corpus behind the mock is the real thing: 31 mtsamples letters carrying 27 medication changes and 62 follow-up tasks, generated by actual mistral-nemo:12b runs, with every evidence phrase re-derived through the backend's own grounding logic so it is a real substring of its own letter. That's why every value in the shelf's live-looking result came out of the model itself — and why the demo survives a hotel network.

The privacy story deserves the same precision. The team designed for local inference — Ontario's PHIPA is the reason, Ollama on a clinic-owned GPU is the mechanism, and the README argues it well. But the shipped toolbar reads Operating · Cloud processing, with the endpoint showing Railway · OpenRouter: for the demo it ran on the cloud fallback. The pill tells the truth about which one you're on, which is the right call — but "no patient data leaves the clinic" is a promise the architecture makes and the demo has yet to keep.

Beyond that: the app is desktop-only, and this page defends that as a decision — squeezing a three-pane reconciliation surface to phone width gives you a different product, so the rail above scrolls sideways instead of pretending. The app lives in the repo. And nothing here has been near a real patient or a real physician's EMR; every result the app produces carries the line "AI-assisted draft — physician review required before clinical use" in its metadata, which is the only claim any of this is entitled to make.

Source: github.com/Adithiyan/ConsultBridge — a team build from the Hackers & Healers AI in Healthcare Co-Design Hackathon, June 2026. The pipeline (FastAPI · Ollama · mistral-nemo:12b) is my teammates' work. Every value in the pieces above is transcribed from the shipped app source, showing letter #900 of its bundled corpus; patients and physicians in that corpus are fictional, and the DINs are real Health Canada records.