// ============================================================
// CALCULATORS — incremental React port.
// Low-risk pure formulas run here; high-risk table-driven calculators
// stay in the vanilla viewer until legacy vectors land.
//
// WHY this is gated on test vectors (from the migration checkpoint):
// • AAP 2017 BP percentile uses Rosner quantile splines with long
// hard-coded coefficient arrays.
// • Fenton 2013 LMS preterm growth carries 210 validated cases.
// • AAP 2022 bilirubin phototherapy + exchange: per-week risk
// curves, 1190 validated cases.
// • Bhutani nomogram risk zones.
// • APLS + Best Guess weight-for-age.
//
// Per the checkpoint: "An LLM will sometimes 'simplify' a long array
// of numbers and silently break it — don't let that happen." Every
// calculator needs a JSON vector file (~20 known inputs + expected
// outputs captured from public/js/calculators.js) before its React
// port lands, and the port must match every vector byte-for-byte.
//
// Pill order + labels match public/components/calculators.html.
// ============================================================
import { useState } from 'react';
import {
calculateGcs,
calculateMostellerBsa,
calculateWeightBasedDose,
} from '@shared/clinical/calculators';
import { classifyBhutani, classifyAapBili, type BiliRisk } from '@shared/clinical/bilirubin';
import { fentonWeightForAge, classifySizeForAge, type Sex } from '@shared/clinical/fenton';
const card = 'rounded-lg border border-border bg-card p-5 space-y-3';
const btnPrimary = 'rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium';
const btnGhost = 'rounded-md border border-border bg-background px-4 py-2 text-sm font-medium hover:bg-muted';
const field = 'space-y-1';
const label = 'block text-xs font-medium text-muted-foreground';
const input = 'w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-ring';
const resultBox = 'rounded-lg border border-border bg-muted/40 p-4';
const errorBox = 'rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:bg-red-950/30 dark:text-red-200';
interface Pill {
id: string;
label: string;
summary: string;
source: string; // where the formulas live
ported?: boolean;
}
const PILLS: Pill[] = [
{ id: 'bp', label: 'BP Percentile', summary: 'AAP 2017 age/height/sex-adjusted BP percentiles (Rosner quantile splines).', source: 'AAP 2017 (Flynn) — Rosner splines' },
{ id: 'bmi', label: 'BMI Percentile', summary: 'BMI-for-age (CDC 2000 z-score tables).', source: 'CDC 2000 LMS' },
{ id: 'growth', label: 'Growth Charts', summary: 'Fenton 2013 preterm weight-for-GA with Z-score + percentile + SGA/AGA/LGA classification.', source: 'Fenton 2013 LMS', ported: true },
{ id: 'bili', label: 'Bilirubin', summary: 'AAP 2022 phototherapy + exchange thresholds and Bhutani nomogram risk zones.', source: 'AAP 2022 (Kemper) + Bhutani 1999', ported: true },
{ id: 'vitals', label: 'Vital Signs', summary: 'Normal HR / RR / BP ranges by age.', source: 'PALS + AHA reference' },
{ id: 'bsa', label: 'Body Surface Area', summary: 'Mosteller body surface area formula.', source: 'Mosteller 1987', ported: true },
{ id: 'dose', label: 'Weight-Based Dosing', summary: 'Generic mg/kg dosing with optional max-dose cap and concentration conversion.', source: 'Legacy calculator formula', ported: true },
{ id: 'resus', label: 'Resus Meds', summary: 'Code-cart dosing (epinephrine, amiodarone, atropine, etc.).', source: 'PALS' },
{ id: 'gcs', label: 'GCS', summary: 'Child/adult and infant Glasgow Coma Scale variants.', source: 'Teasdale + pediatric modification', ported: true },
{ id: 'equipment', label: 'Equipment', summary: 'ETT size, blade, NG, Foley, suction by age/weight.', source: 'PALS + Broselow cross-reference' },
];
function parseOptionalNumber(value: string): number | null {
if (!value.trim()) return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function FormField({
id,
labelText,
value,
onChange,
min,
max,
step = '0.1',
placeholder,
}: {
id: string;
labelText: string;
value: string;
onChange: (value: string) => void;
min?: string;
max?: string;
step?: string;
placeholder?: string;
}) {
return (
{labelText}
onChange(event.target.value)}
placeholder={placeholder}
className={input}
/>
);
}
function BsaPanel() {
const [weight, setWeight] = useState('');
const [height, setHeight] = useState('');
const [result, setResult] = useState(null);
const [error, setError] = useState('');
function calculate() {
const next = calculateMostellerBsa(Number(weight), Number(height));
if (next == null) {
setError('Enter a valid weight and height.');
setResult(null);
return;
}
setError('');
setResult(next);
}
function clear() {
setWeight('');
setHeight('');
setResult(null);
setError('');
}
return (
Body Surface Area
Mosteller formula: BSA (m2) = sqrt(height(cm) x weight(kg) / 3600).
Calculate
Clear
{error ? {error}
: null}
{result == null ? null : (
Mosteller BSA
{result.toFixed(3)} m²
{weight} kg, {height} cm
)}
);
}
function DosePanel() {
const [weight, setWeight] = useState('');
const [dosePerKg, setDosePerKg] = useState('');
const [frequency, setFrequency] = useState('1');
const [maxDose, setMaxDose] = useState('');
const [concentration, setConcentration] = useState('');
const [result, setResult] = useState>(null);
const [error, setError] = useState('');
function calculate() {
const next = calculateWeightBasedDose({
weightKg: Number(weight),
dosePerKg: Number(dosePerKg),
frequencyPerDay: Number(frequency),
maxSingleDoseMg: parseOptionalNumber(maxDose),
concentrationMgPerMl: parseOptionalNumber(concentration),
});
if (next == null) {
setError('Enter a valid weight, mg/kg dose, and frequency.');
setResult(null);
return;
}
setError('');
setResult(next);
}
function clear() {
setWeight('');
setDosePerKg('');
setFrequency('1');
setMaxDose('');
setConcentration('');
setResult(null);
setError('');
}
return (
Weight-Based Dosing
Generic mg/kg calculator. Always verify medication-specific dosing against formulary and local policy.
Frequency
setFrequency(event.target.value)} className={input}>
Once daily
Twice daily (BID)
Three times daily (TID)
Four times daily (QID)
Every 4 hours (Q4H)
Calculate
Clear
{error ? {error}
: null}
{result == null ? null : (
Single Dose
{result.singleDoseMg.toFixed(1)} mg
{result.capped ?
Capped at max dose
: null}
Daily Total
{result.dailyDoseMg.toFixed(1)} mg/day
x {result.frequencyPerDay}/day
Volume
{result.volumeMl == null ? 'n/a' : `${result.volumeMl.toFixed(1)} mL`}
per dose
)}
);
}
const GCS_OPTIONS = {
child: {
eye: [
['4', '4 - Spontaneous'],
['3', '3 - To speech'],
['2', '2 - To pain'],
['1', '1 - None'],
],
verbal: [
['5', '5 - Oriented'],
['4', '4 - Confused'],
['3', '3 - Inappropriate words'],
['2', '2 - Incomprehensible sounds'],
['1', '1 - None'],
],
motor: [
['6', '6 - Obeys commands'],
['5', '5 - Localizes pain'],
['4', '4 - Withdraws to pain'],
['3', '3 - Abnormal flexion'],
['2', '2 - Abnormal extension'],
['1', '1 - None'],
],
},
infant: {
eye: [
['4', '4 - Spontaneous'],
['3', '3 - To speech/sound'],
['2', '2 - To painful stimuli'],
['1', '1 - None'],
],
verbal: [
['5', '5 - Coos/babbles'],
['4', '4 - Irritable cry'],
['3', '3 - Cries to pain'],
['2', '2 - Moans to pain'],
['1', '1 - None'],
],
motor: [
['6', '6 - Normal spontaneous movement'],
['5', '5 - Withdraws to touch'],
['4', '4 - Withdraws to pain'],
['3', '3 - Abnormal flexion'],
['2', '2 - Abnormal extension'],
['1', '1 - None'],
],
},
} as const;
function GcsSelect({
id,
labelText,
value,
options,
onChange,
}: {
id: string;
labelText: string;
value: string;
options: readonly (readonly [string, string])[];
onChange: (value: string) => void;
}) {
return (
{labelText}
onChange(event.target.value)} className={input}>
{options.map(([optionValue, text]) => (
{text}
))}
);
}
function GcsPanel() {
const [scale, setScale] = useState<'child' | 'infant'>('child');
const [eye, setEye] = useState('4');
const [verbal, setVerbal] = useState('5');
const [motor, setMotor] = useState('6');
const result = calculateGcs(Number(eye), Number(verbal), Number(motor));
const options = GCS_OPTIONS[scale];
function switchScale(next: 'child' | 'infant') {
setScale(next);
setEye('4');
setVerbal('5');
setMotor('6');
}
return (
Glasgow Coma Scale
Select responses to calculate child/adult or infant-modified GCS. Total score 3-15.
switchScale('child')}
className={'px-3 py-1.5 rounded-full text-xs font-medium border ' + (scale === 'child' ? 'bg-primary text-primary-foreground border-primary' : 'bg-muted border-border')}
>
Child / Adult
switchScale('infant')}
className={'px-3 py-1.5 rounded-full text-xs font-medium border ' + (scale === 'infant' ? 'bg-primary text-primary-foreground border-primary' : 'bg-muted border-border')}
>
Infant
{result == null ? null : (
{scale === 'infant' ? 'Infant-modified GCS' : 'Child / adult GCS'}
GCS: {result.total}/15
{result.severity}
Interpretation: 13-15 Mild, 9-12 Moderate, 3-8 Severe/Coma.
)}
);
}
function LegacyPanel({ pill }: { pill: Pill }) {
return (
{pill.label}
{pill.summary}
Source of truth: {pill.source}.
This calculator runs in the legacy viewer. A React port is gated on capturing test vectors
from the vanilla implementation so the numerical output can be verified byte-for-byte —
the migration checkpoint specifically flags this class of data as the one an LLM is most
likely to silently simplify.
Open in legacy viewer
);
}
function BiliPanel() {
const [mode, setMode] = useState<'aap' | 'bhutani'>('aap');
const [ga, setGa] = useState('38');
const [hours, setHours] = useState('');
const [tsb, setTsb] = useState('');
const [risk, setRisk] = useState('low');
const [aapResult, setAapResult] = useState | null>(null);
const [bhutResult, setBhutResult] = useState | null>(null);
const [error, setError] = useState('');
function calc() {
const hoursNum = Number(hours);
const tsbNum = Number(tsb);
if (!Number.isFinite(hoursNum) || !Number.isFinite(tsbNum) || hoursNum <= 0 || tsbNum <= 0) {
setError('Enter hours of life and TSB (mg/dL).');
setAapResult(null);
setBhutResult(null);
return;
}
setError('');
if (mode === 'aap') {
const gaNum = Number(ga);
if (!Number.isFinite(gaNum) || gaNum < 35) {
setError('AAP 2022 thresholds apply to GA ≥35 weeks.');
setAapResult(null);
return;
}
setAapResult(classifyAapBili(gaNum, hoursNum, tsbNum, risk));
setBhutResult(null);
} else {
setBhutResult(classifyBhutani(hoursNum, tsbNum));
setAapResult(null);
}
}
const statusColor = aapResult
? aapResult.status === 'Above Exchange' ? 'text-red-800 bg-red-100'
: aapResult.status === 'Above Phototherapy' ? 'text-red-700 bg-red-50'
: 'text-green-700 bg-green-50'
: '';
const zoneColor = bhutResult
? bhutResult.zone === 'High-Risk' ? 'text-red-800 bg-red-100'
: bhutResult.zone === 'High-Intermediate' ? 'text-orange-700 bg-orange-50'
: bhutResult.zone === 'Low-Intermediate' ? 'text-amber-700 bg-amber-50'
: 'text-green-700 bg-green-50'
: '';
return (
Bilirubin
setMode('aap')} className={'px-3 py-1 rounded text-xs font-medium ' + (mode === 'aap' ? 'bg-primary text-primary-foreground' : 'bg-muted')} data-testid="bili-mode-aap">AAP 2022 Phototherapy
setMode('bhutani')} className={'px-3 py-1 rounded text-xs font-medium ' + (mode === 'bhutani' ? 'bg-primary text-primary-foreground' : 'bg-muted')} data-testid="bili-mode-bhutani">Bhutani Nomogram
{mode === 'aap' && (
<>
GA (weeks)
setGa(e.target.value)}>
{[35, 36, 37, 38, 39, 40].map((g) => {g}{g === 40 ? '+' : ''} )}
Neurotoxicity risk
setRisk(e.target.value as BiliRisk)}>
No risk factors
With risk factors
>
)}
Calculate
{ setHours(''); setTsb(''); setAapResult(null); setBhutResult(null); setError(''); }}>Clear
{error && {error}
}
{aapResult && (
{aapResult.status}
TSB {tsb} mg/dL at {hours} hours of life (GA {ga}w {risk === 'medium' ? 'with' : 'without'} risk factors)
Phototherapy {aapResult.photoThreshold.toFixed(1)} mg/dL
Exchange {aapResult.exchangeThreshold.toFixed(1)} mg/dL
AAP 2022 CPG (Kemper et al.). Always use clinical judgment.
)}
{bhutResult && (
{bhutResult.zone} Zone
TSB {tsb} mg/dL at {hours} hours of life
40th %ile {bhutResult.p40.toFixed(1)}
75th %ile {bhutResult.p75.toFixed(1)}
95th %ile {bhutResult.p95.toFixed(1)}
Bhutani 1999 hour-specific risk nomogram for infants ≥35 weeks GA.
)}
);
}
function GrowthPanel() {
const [sex, setSex] = useState('male');
const [ga, setGa] = useState('');
const [weight, setWeight] = useState('');
const [result, setResult] = useState | null>(null);
const [error, setError] = useState('');
function calc() {
const gaNum = Number(ga);
const wtNum = Number(weight);
if (!Number.isFinite(gaNum) || !Number.isFinite(wtNum) || gaNum < 22 || gaNum > 50 || wtNum <= 0) {
setError('Enter GA (22-50 weeks) and weight (grams).');
setResult(null);
return;
}
setError('');
setResult(fentonWeightForAge(gaNum, wtNum, sex));
}
const classification = result ? classifySizeForAge(result.percentile) : null;
const classColor = classification === 'SGA' ? 'text-orange-700 bg-orange-50'
: classification === 'LGA' ? 'text-amber-700 bg-amber-50'
: 'text-green-700 bg-green-50';
return (
Fenton 2013 Preterm Growth
Weight-for-gestational-age Z-score + percentile + SGA/AGA/LGA classification.
Sex
setSex(e.target.value as Sex)}>
Male
Female
Calculate
{ setGa(''); setWeight(''); setResult(null); setError(''); }}>Clear
{error && {error}
}
{result && classification && (
{classification}
Percentile {result.percentile.toFixed(1)}%
Z-score {result.z.toFixed(2)}
Median (M) {Math.round(result.M)} g
L / S {result.L.toFixed(3)} / {result.S.toFixed(3)}
Fenton TR, Kim JH. Systematic review — revised Fenton growth chart for preterm infants. BMC Pediatr 2013;13:59.
)}
);
}
function ActivePanel({ pill }: { pill: Pill }) {
if (pill.id === 'bsa') return ;
if (pill.id === 'dose') return ;
if (pill.id === 'gcs') return ;
if (pill.id === 'bili') return ;
if (pill.id === 'growth') return ;
return ;
}
export default function Calculators() {
const [active, setActive] = useState(PILLS[0].id);
const pill = PILLS.find((p) => p.id === active) ?? PILLS[0];
return (
Calculators
Pediatric calculators — BP percentiles, bilirubin thresholds, growth, dosing, equipment sizing.
Simple pure-formula calculators run in React now; high-risk table-driven calculators remain
legacy-gated until vectors are captured.
{PILLS.map((p) => (
setActive(p.id)}
className={
'px-3 py-1.5 rounded-full text-xs font-medium border transition-colors ' +
(active === p.id
? 'bg-primary text-primary-foreground border-primary'
: 'bg-muted hover:bg-muted/80 border-border')
}
data-testid={'calc-pill-' + p.id}
>
{p.label}{p.ported ? React : null}
))}
);
}