Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions research/intents/aod-9604.json
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/ara-290.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/bpc-157.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/cagrilintide.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/dsip.json
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/ghrp-6.json
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/humanin.json
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/ipamorelin.json
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/melanotan-ii.json
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/mots-c.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/retatrutide.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"escalation",
"adverse",
"who",
Expand Down
1 change: 1 addition & 0 deletions research/intents/selank.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/semax.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/ss-31.json
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/survodutide.json
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/tb-500.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/tesamorelin.json
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/tesofensine.json
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
1 change: 1 addition & 0 deletions research/intents/thymosin-alpha-1.json
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@
"evidence",
"doses",
"dosage-chart",
"calculator",
"adverse",
"who",
"biomarkers",
Expand Down
24 changes: 24 additions & 0 deletions scripts/test_calculators.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,30 @@ if (fs.existsSync(CALC_DIR)) {
}
}

// Each compound that has a calculator page also embeds the calculator on its
// own record and links to the page. The two must agree: a calculator page with
// no inbound link from its compound is an orphan, and a compound page whose
// embedded tool does not compute is the same defect as the empty tools were.
if (fs.existsSync(CALC_DIR)) {
for (const slug of fs.readdirSync(CALC_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)) {
const file = path.join(DIST, 'compounds', slug, 'index.html');
if (!fs.existsSync(file)) { fails.push(`compounds/${slug}: no compound page for calculator page`); continue; }
const html = fs.readFileSync(file, 'utf8');
if (!html.includes(`href="/calculators/${slug}"`)) fails.push(`compounds/${slug}: no link to /calculators/${slug}`);
if (!/<section id="calculator">/.test(html)) { fails.push(`compounds/${slug}: no calculator section`); continue; }
if (!/id="r-out"/.test(html)) { fails.push(`compounds/${slug}: calculator section has no output element`); continue; }
const m = html.match(/<script type="module">([\s\S]*?)<\/script>/i);
if (!m) { fails.push(`compounds/${slug}: calculator present but no module script`); continue; }
const els = {};
for (const [id, v] of Object.entries({ 'r-mass': 5, 'r-vol': 2, 'r-want': 250, 'r-scale': 100 })) els[id] = { value: String(v), addEventListener() {} };
const out = { textContent: '' }; els['r-out'] = out;
globalThis.document = { getElementById: (id) => els[id] ?? null };
new Function(m[1])();
if (!out.textContent.includes('10 units on a U-100')) fails.push(`compounds/${slug}: embedded calculator did not compute`);
else pass++;
}
}

if (fails.length) {
console.error(`calculator tests: ${pass} passed, ${fails.length} FAILED\n`);
for (const f of fails) console.error(` ${f}\n`);
Expand Down
16 changes: 12 additions & 4 deletions src/components/Calculators.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@
// Nothing here accepts a body weight, an age or a condition, and nothing
// returns a dose. Each tool states its formula above the inputs, so the
// arithmetic can be checked by hand.
interface Props { slug: string }
const { slug } = Astro.props;
interface Props {
slug: string;
/** The reconstitution tool's section id and heading, overridable so the same
* component can sit inside a compound page under that page's own H2. */
id?: string;
heading?: string;
headingLevel?: 2 | 3;
}
const { slug, id = 'calc-recon', heading = 'Work out concentration and what to draw', headingLevel = 2 } = Astro.props;
const H = headingLevel === 3 ? 'h3' : 'h2';
---
{slug === 'reconstitution-calculator' && (
<section id="calc-recon">
<h2>Work out concentration and what to draw</h2>
<section id={id}>
<H>{heading}</H>
<p class="prose">Two divisions, both shown. <b>Concentration</b> = mass in the vial &divide; diluent volume. <b>Volume to draw</b> = mass you want &divide; that concentration. Syringe units are that volume &times; the units per millilitre on your syringe's own scale.</p>
<form class="calc" onsubmit="return false">
<label for="r-mass">Mass in vial (mg)</label>
Expand Down
17 changes: 15 additions & 2 deletions src/layouts/CompoundV2.astro
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import LandscapeTable from '../components/v2/LandscapeTable.astro';
import HumanTrials from '../components/v2/HumanTrials.astro';
import CombinationEvidence from '../components/v2/CombinationEvidence.astro';
import EditorialSection from '../components/v2/EditorialSection.astro';
import Calculators from '../components/Calculators.astro';
import { qualifies as hasCalculator } from '../lib/calculators';
import { entityLinker } from '../lib/entities';
import ReconstitutionMath from '../components/shared/ReconstitutionMath.astro';
import EquipmentInStudies from '../components/shared/EquipmentInStudies.astro';
Expand Down Expand Up @@ -120,7 +122,7 @@ const ledeFor = (f: string) => [boundaryFor(intent, f), defaultLedes[f]].filter(
// claims but with mapped demand renders as an explicit absence statement.
type Guide = { id: string; heading: string; lede?: string; html: string; pending?: boolean };
const guide: Guide[] = ((d as any).guide ?? []) as Guide[];
type Block = { id: string; kind: 'human' | 'table' | 'landscape' | 'claims' | 'absence' | 'method' | 'compare' | 'combination' | 'regulatory' | 'open' | 'faq' | 'guide'; field?: string; label: string; group: string; guide?: Guide };
type Block = { id: string; kind: 'human' | 'table' | 'landscape' | 'claims' | 'absence' | 'method' | 'calculator' | 'compare' | 'combination' | 'regulatory' | 'open' | 'faq' | 'guide'; field?: string; label: string; group: string; guide?: Guide };
// Links the first mention of each other record in this page's own prose.
const linkEntities = entityLinker([d.slug], 10);
const blocks: Block[] = [];
Expand All @@ -133,6 +135,11 @@ for (const [f, id, label] of evidence) {
if (has(f)) blocks.push({ id, kind: 'claims', field: f, label: headingFor(intent, f, label), group: 'Evidence' });
else if (queriesFor(intent, f).some((q) => q.policy !== 'redirect')) blocks.push({ id, kind: 'absence', field: f, label: headingFor(intent, f, label), group: 'Evidence' });
}
// The compound's own calculator, only where it has one: a record qualifies when
// it is finished and carries at least two distinct published dose figures, the
// same rule that decides whether /calculators/<slug> exists at all.
const calculatorHere = hasCalculator(entry);
if (calculatorHere) blocks.push({ id: 'calculator', kind: 'calculator', label: headingFor(intent, 'calculator', `${d.preferred_name} dosage calculator`), group: 'Evidence' });
if (hasAnyEvidence) blocks.push({ id: 'method', kind: 'method', label: 'Method notes', group: 'Method' });
if (peerEntries.length > 0) blocks.push({ id: 'compare', kind: 'compare', label: headingFor(intent, 'compare', 'Other compounds in its class'), group: 'Context' });
if (inStacks.length > 0) blocks.push({ id: 'combination', kind: 'combination', label: headingFor(intent, 'combination', 'Studied in combination'), group: 'Context' });
Expand Down Expand Up @@ -228,6 +235,11 @@ const toc = [secs[0], secs[1], secs[2], ...tocBody, ...secs.filter((s) => ['sour
<EvidenceTable rows={A.evidence_table} />
</section>
);
case 'calculator': return (
<section id="calculator"><div><h2>{b.label}</h2><p class="lede">Enter a vial and a dose and it returns the concentration, the volume and the units on your syringe. It converts the numbers you give it and suggests none. The doses studies of {d.preferred_name} actually used, with a units chart per vial size, are on the <a href={`/calculators/${d.slug}`}>full {d.preferred_name} calculator page</a>.</p></div>
<Calculators slug="reconstitution-calculator" id="calculator-tool" heading="Work out concentration and what to draw" headingLevel={3} />
</section>
);
case 'claims': return (
<section id={b.id}><div><h2>{b.label}</h2><p class="lede">{ledeFor(b.field!)}</p></div><ClaimCards claims={A[b.field!]} sources={d.sources} />{b.field === 'weight_normalized_doses' && <WeightCaveat />}</section>
);
Expand Down Expand Up @@ -260,8 +272,9 @@ const toc = [secs[0], secs[1], secs[2], ...tocBody, ...secs.filter((s) => ['sour

{d.sources.length > 0 && <section id="sources"><div><h2>Sources</h2><p class="lede">Full citations. Every claim above links to one of these by its id.</p></div><Sources sources={d.sources} /></section>}
<section id="card"><h2>Reference card</h2><RefCard record={d} reg={reg} path={path} /></section>
{(inStacks.length > 0 || inComparisons.length > 0 || peerEntries.length > 0) && (
{(inStacks.length > 0 || inComparisons.length > 0 || peerEntries.length > 0 || calculatorHere) && (
<section id="related"><h2>Related records</h2><ul class="cards">
{calculatorHere && <li><a href={`/calculators/${d.slug}`}>{d.preferred_name} dosage calculator</a><div class="sub">Units, mg and reconstitution, with the doses studies used</div></li>}
{peerEntries.map((p) => <li><a href={`/compounds/${p.data.slug}`}>{p.data.preferred_name}</a><div class="sub">Peer compound</div></li>)}
{inStacks.map((s) => <li><a href={`/stacks/${s.slug}`}>{s.name}</a><div class="sub">Stack 路 wave {s.pilot_wave}</div></li>)}
{inComparisons.map((c) => <li><a href={`/compare/${c.slug}`}>{c.sides.join(' vs ')}</a><div class="sub">Comparison 路 wave {c.pilot_wave}</div></li>)}
Expand Down
Loading