pediatric-ai-scribe-v3/client/src/pages/Bedside.tsx
Daniel 314b256160 feat(client): port Bedside sub-nav shell (15 pills linked to legacy)
First Bedside commit. Delivers the /app/bedside page with the full
15-pill sub-nav in the same order + labels as the vanilla app, and
switches the sidebar Bedside link to available.

client/src/pages/Bedside.tsx
  Single-file shell. PILLS array is the single source of truth for
  pill ID / label / icon / summary, ordered to match
  public/components/bedside.html (neonatal, airway, cardiac,
  respiratory, ventilation, seizure, sepsis, anaphylaxis, sedation,
  agitation, antiemetics, antimicrobials, burns, toxicology, trauma).
  Clicking a pill flips useState<active>, and LegacyPanel renders a
  summary of that module + a button to open the legacy Bedside tab.

What is intentionally NOT in this commit
  Each pill's actual clinical dosing panel stays in vanilla for now.
  Those panels encode weight-based dosing, syndrome-keyed
  antimicrobials, and PALS / ALS formulas — exactly the class of
  content the migration checkpoint memory flags as must-not-be-
  "simplified" by an LLM. They belong in per-module commits that
  land alongside the calculators port (APLS + Best Guess weight,
  Fenton 2013 LMS, AAP 2022 bilirubin, Rosner BP splines) where
  test vectors can verify byte-for-byte parity with the vanilla
  output.

  The top-level age → weight estimator also waits on calculators —
  it calls window._PED_MATH.estimateWeightFromAgeMonths in the
  vanilla module, which is defined in public/js/calculators.js.

e2e/tests/bedside-react.spec.js — three smoke tests
  All 15 pills render by data-testid, clicking a pill swaps the
  panel, and the legacy-viewer link is present. The pill-order list
  is hard-coded in the spec so re-ordering or dropping a pill trips
  a loud failure.

Client tsc -b + vite build clean. Bundle 456.71 kB / 130.53 kB gz.
2026-04-23 23:59:34 +02:00

109 lines
5.7 KiB
TypeScript

// ============================================================
// BEDSIDE — emergency + rapid-reference pediatric tools. This first
// port ships the sub-nav shell at /app/bedside so the sidebar links
// light up and users land on a page that mirrors the legacy layout.
//
// The 15 clinical sub-modules (neonatal, airway, cardiac, respiratory,
// ventilation, seizures, sepsis, anaphylaxis, sedation, agitation,
// antiemetics, antimicrobials, burns, toxicology, trauma) stay in the
// vanilla viewer for now. Each one carries weight-based dosing +
// clinical decision content the migration checkpoint explicitly
// flagged as must-not-be-"simplified" by an LLM — they belong in
// dedicated per-module commits alongside the calculators port (Rosner
// BP splines, Fenton LMS, AAP 2022 bilirubin, APLS weights) where
// test vectors can verify byte-for-byte parity.
//
// The top-level age → weight estimator (APLS / Best Guess formulas)
// also depends on public/js/calculators.js which has not been ported
// yet. It will appear at the top of this page when calculators port.
// ============================================================
import { useState } from 'react';
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';
interface Pill {
id: string;
label: string;
icon?: string; // emoji stand-in; font-awesome lives in the legacy shell
summary: string;
}
// Order and labels match public/components/bedside.html exactly.
const PILLS: Pill[] = [
{ id: 'neonatal', label: 'Neonatal', icon: '👶', summary: 'GA classification, AGA/SGA/LGA, prematurity category (Fenton 2013 / WHO).' },
{ id: 'airway', label: 'Airway / RSI', icon: '💨', summary: 'ETT size + depth, RSI induction + paralytic dosing by weight.' },
{ id: 'cardiac', label: 'Cardiac Arrest', icon: '❤️', summary: 'PALS dosing (epinephrine, amiodarone, lidocaine), defibrillation J/kg.' },
{ id: 'respiratory', label: 'Respiratory', icon: '🫁', summary: 'Asthma, bronchiolitis, croup severity + dosing.' },
{ id: 'ventilation', label: 'O₂ & Ventilation', icon: '🌀', summary: 'NC / HFNC / CPAP / BiPAP flow + FiO₂ targets by age.' },
{ id: 'seizure', label: 'Seizures', icon: '🧠', summary: 'Benzodiazepine + second/third-line weight-based dosing.' },
{ id: 'sepsis', label: 'Sepsis & Fever', icon: '🦠', summary: 'Empirical antibiotics + fluid bolus dosing by weight.' },
{ id: 'anaphylaxis', label: 'Anaphylaxis', icon: '💉', summary: 'Epinephrine IM, IV infusion, steroid + antihistamine dosing.' },
{ id: 'sedation', label: 'Sedation', icon: '🛌', summary: 'Procedural sedation regimens — ketamine, propofol, midazolam.' },
{ id: 'agitation', label: 'Agitation', icon: '😤', summary: 'Weight-based haloperidol, olanzapine, lorazepam.' },
{ id: 'antiemetics', label: 'Antiemetics', icon: '💊', summary: 'Ondansetron, metoclopramide, promethazine dosing.' },
{ id: 'antimicrobials', label: 'Antimicrobials', icon: '🧫', summary: 'Common empirical regimens keyed to syndrome + weight.' },
{ id: 'burns', label: 'Burns', icon: '🔥', summary: 'TBSA % (Lund-Browder, Rule of Nines-children), Parkland fluids.' },
{ id: 'toxicology', label: 'Toxicology', icon: '☠️', summary: 'Common toxidromes + antidotes + decontamination windows.' },
{ id: 'trauma', label: 'Trauma', icon: '🩹', summary: 'PECARN, c-spine, blood-product dosing, TXA.' },
];
function LegacyPanel({ pill }: { pill: Pill }) {
return (
<section className={card} data-testid={'bedside-panel-' + pill.id}>
<div className="flex items-center gap-3">
<span className="text-2xl" aria-hidden>{pill.icon}</span>
<h2 className="text-lg font-semibold">{pill.label}</h2>
</div>
<p className="text-sm text-muted-foreground">{pill.summary}</p>
<div className="rounded-md border border-amber-300 bg-amber-50 dark:bg-amber-950/30 p-3 text-sm space-y-2">
<p className="text-amber-900 dark:text-amber-100">
Weight-based calculators for this module run in the legacy viewer while the clinical data is
verified for a direct React port. Open the legacy Bedside tab to use the full dosing flow.
</p>
</div>
<a href="/#bedside" className={btnPrimary + ' inline-block'}>
Open in legacy viewer
</a>
</section>
);
}
export default function Bedside() {
const [active, setActive] = useState<string>(PILLS[0].id);
const pill = PILLS.find((p) => p.id === active) ?? PILLS[0];
return (
<div className="max-w-5xl mx-auto p-6 space-y-4">
<header>
<h1 className="text-2xl font-semibold">Bedside</h1>
<p className="text-sm text-muted-foreground">
Emergency and rapid-reference pediatric tools. Weight-based dosing throughout always verify against institutional protocols.
</p>
</header>
<div className="flex flex-wrap gap-2" data-testid="bedside-subnav">
{PILLS.map((p) => (
<button
key={p.id}
type="button"
onClick={() => 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={'bedside-pill-' + p.id}
>
<span className="mr-1" aria-hidden>{p.icon}</span>
{p.label}
</button>
))}
</div>
<LegacyPanel pill={pill} />
</div>
);
}