Update pharmacokinetic parameters/calculations, add advanced settings, add disclaimer/citations, many improvements
This commit is contained in:
45
src/App.tsx
45
src/App.tsx
@@ -16,6 +16,7 @@ import DaySchedule from './components/day-schedule';
|
||||
import SimulationChart from './components/simulation-chart';
|
||||
import Settings from './components/settings';
|
||||
import LanguageSelector from './components/language-selector';
|
||||
import DisclaimerModal from './components/disclaimer-modal';
|
||||
import { Button } from './components/ui/button';
|
||||
|
||||
// Custom Hooks
|
||||
@@ -27,6 +28,25 @@ import { useLanguage } from './hooks/useLanguage';
|
||||
const MedPlanAssistant = () => {
|
||||
const { currentLanguage, t, changeLanguage } = useLanguage();
|
||||
|
||||
// Disclaimer modal state
|
||||
const [showDisclaimer, setShowDisclaimer] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const hasAccepted = localStorage.getItem('medPlanDisclaimerAccepted_v1');
|
||||
if (!hasAccepted) {
|
||||
setShowDisclaimer(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAcceptDisclaimer = () => {
|
||||
localStorage.setItem('medPlanDisclaimerAccepted_v1', 'true');
|
||||
setShowDisclaimer(false);
|
||||
};
|
||||
|
||||
const handleOpenDisclaimer = () => {
|
||||
setShowDisclaimer(true);
|
||||
};
|
||||
|
||||
const {
|
||||
appState,
|
||||
updateNestedState,
|
||||
@@ -66,6 +86,15 @@ const MedPlanAssistant = () => {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-4 sm:p-6 lg:p-8">
|
||||
{/* Disclaimer Modal */}
|
||||
<DisclaimerModal
|
||||
isOpen={showDisclaimer}
|
||||
onAccept={handleAcceptDisclaimer}
|
||||
currentLanguage={currentLanguage}
|
||||
onLanguageChange={changeLanguage}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<header className="mb-8">
|
||||
<div className="flex justify-between items-start">
|
||||
@@ -151,8 +180,20 @@ const MedPlanAssistant = () => {
|
||||
</div>
|
||||
|
||||
<footer className="mt-8 p-4 bg-muted rounded-lg text-sm text-muted-foreground border">
|
||||
<h3 className="font-semibold mb-2 text-foreground">{t('importantNote')}</h3>
|
||||
<p>{t('disclaimer')}</p>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="font-semibold mb-2 text-foreground">{t('importantNote')}</h3>
|
||||
<p>{t('disclaimer')}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleOpenDisclaimer}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{t('disclaimerModalFooterLink')}
|
||||
</Button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
287
src/components/disclaimer-modal.tsx
Normal file
287
src/components/disclaimer-modal.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Disclaimer Modal Component
|
||||
*
|
||||
* Displays FDA/TGA-derived medical disclaimer on first app load.
|
||||
* Users must acknowledge before using simulation features.
|
||||
* Tracks dismissal in localStorage and provides language selection.
|
||||
*
|
||||
* @author Andreas Weyer
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import LanguageSelector from './language-selector';
|
||||
|
||||
interface DisclaimerModalProps {
|
||||
isOpen: boolean;
|
||||
onAccept: () => void;
|
||||
currentLanguage: string;
|
||||
onLanguageChange: (lang: string) => void;
|
||||
t: (key: string) => string;
|
||||
}
|
||||
|
||||
const DisclaimerModal: React.FC<DisclaimerModalProps> = ({
|
||||
isOpen,
|
||||
onAccept,
|
||||
currentLanguage,
|
||||
onLanguageChange,
|
||||
t
|
||||
}) => {
|
||||
const [sourcesExpanded, setSourcesExpanded] = useState(false);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||
<div className="max-w-3xl max-h-[90vh] overflow-y-auto bg-background rounded-lg shadow-xl">
|
||||
<Card className="border-0">
|
||||
<CardHeader className="bg-destructive/10 border-b">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-2xl font-bold">
|
||||
{t('disclaimerModalTitle')}
|
||||
</CardTitle>
|
||||
<p className="text-center text-muted-foreground mt-2">
|
||||
{t('disclaimerModalSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<LanguageSelector
|
||||
currentLanguage={currentLanguage}
|
||||
onLanguageChange={onLanguageChange}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 pt-6">
|
||||
{/* Purpose */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
|
||||
<span className="text-2xl">ℹ️</span>
|
||||
{t('disclaimerModalPurpose')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('disclaimerModalPurposeText')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Variability */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
|
||||
<span className="text-2xl">⚠️</span>
|
||||
{t('disclaimerModalVariability')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('disclaimerModalVariabilityText')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Medical Advice */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
|
||||
<span className="text-2xl">🩺</span>
|
||||
{t('disclaimerModalMedicalAdvice')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('disclaimerModalMedicalAdviceText')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Schedule II Warning */}
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4">
|
||||
<h3 className="text-lg font-semibold mb-2 flex items-center gap-2 text-red-900 dark:text-red-200">
|
||||
<span className="text-2xl">⛔</span>
|
||||
{t('disclaimerModalScheduleII')}
|
||||
</h3>
|
||||
<p className="text-sm text-red-800 dark:text-red-300">
|
||||
{t('disclaimerModalScheduleIIText')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Data Sources */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
|
||||
<span className="text-2xl">📚</span>
|
||||
{t('disclaimerModalDataSources')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('disclaimerModalDataSourcesText')}
|
||||
</p>
|
||||
|
||||
{/* Collapsible Sources List */}
|
||||
<div className="mt-3 border rounded-md">
|
||||
<button
|
||||
onClick={() => setSourcesExpanded(!sourcesExpanded)}
|
||||
className="w-full px-4 py-2 flex items-center justify-between hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<span className="text-sm font-medium">
|
||||
{sourcesExpanded ? '▼' : '▶'} Key References & Full Citation List
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{sourcesExpanded && (
|
||||
<div className="px-4 pb-4 pt-2 border-t bg-muted/20 space-y-2 text-sm">
|
||||
<p className="text-muted-foreground font-semibold mb-3">Primary regulatory sources:</p>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<a
|
||||
href="https://www.accessdata.fda.gov/drugsatfda_docs/label/2017/208510lbl.pdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
FDA Prescribing Information: Vyvanse (2017)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.tga.gov.au/sites/default/files/auspar-lisdexamfetamine-dimesilate-131023.pdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
TGA AusPAR: Lisdexamfetamine dimesylate (2013)
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-muted-foreground font-semibold mt-4 mb-3">Pharmacokinetic & mechanism studies:</p>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<a
|
||||
href="https://pmc.ncbi.nlm.nih.gov/articles/PMC4257105/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
RBC hydrolysis mechanism of lisdexamfetamine (NIH)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://pmc.ncbi.nlm.nih.gov/articles/PMC4823324/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Lisdexamfetamine: Prodrug Delivery & Exposure (Ermer et al.)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://pmc.ncbi.nlm.nih.gov/articles/PMC5594082/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Pharmacokinetics & Pharmacodynamics in Healthy Subjects (NIH)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://pmc.ncbi.nlm.nih.gov/articles/PMC3575217/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Double-blind study in healthy older adults (NIH)
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-muted-foreground font-semibold mt-4 mb-3">Therapeutic & reference ranges:</p>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<a
|
||||
href="https://www.thieme-connect.com/products/ejournals/pdf/10.1055/a-2689-4911.pdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Therapeutic Reference Ranges for ADHD Drugs (Thieme Connect)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.frontiersin.org/journals/pharmacology/articles/10.3389/fphar.2022.881198/full"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Oral solution vs. capsules bioavailability comparison (Frontiers, 2022)
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-muted-foreground font-semibold mt-4 mb-3">General references & overviews:</p>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/Lisdexamfetamine"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Lisdexamfetamine — Wikipedia
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://www.ncbi.nlm.nih.gov/books/NBK507808/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Dextroamphetamine-Amphetamine — StatPearls (NCBI Bookshelf)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://pubchem.ncbi.nlm.nih.gov/compound/Lisdexamfetamine"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Lisdexamfetamine chemistry & properties — PubChem (NIH)
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p className="text-xs text-muted-foreground mt-4 pt-3 border-t">
|
||||
All sources accessed January 8–9, 2026. For complete citation details, see the project documentation at the end of this app.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Liability */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2 flex items-center gap-2">
|
||||
<span className="text-2xl">⚖️</span>
|
||||
{t('disclaimerModalLiability')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('disclaimerModalLiabilityText')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Accept Button */}
|
||||
<div className="pt-4 border-t">
|
||||
<Button
|
||||
onClick={onAccept}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{t('disclaimerModalAccept')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisclaimerModal;
|
||||
@@ -19,6 +19,91 @@ import { Separator } from './ui/separator';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { getDefaultState } from '../constants/defaults';
|
||||
|
||||
/**
|
||||
* Helper function to create translation interpolation values for defaults.
|
||||
* Derives default values dynamically from hardcoded defaults.
|
||||
*/
|
||||
const getDefaultsForTranslation = (pkParams: any, therapeuticRange: any, uiSettings: any) => {
|
||||
const defaults = getDefaultState();
|
||||
|
||||
return {
|
||||
// UI Settings
|
||||
simulationDays: defaults.uiSettings.simulationDays,
|
||||
displayedDays: defaults.uiSettings.displayedDays,
|
||||
therapeuticRangeMin: defaults.therapeuticRange.min,
|
||||
therapeuticRangeMax: defaults.therapeuticRange.max,
|
||||
|
||||
// PK Parameters
|
||||
damphHalfLife: defaults.pkParams.damph.halfLife,
|
||||
ldxHalfLife: defaults.pkParams.ldx.halfLife,
|
||||
ldxAbsorptionHalfLife: defaults.pkParams.ldx.absorptionHalfLife,
|
||||
|
||||
// Advanced Settings
|
||||
bodyWeight: defaults.pkParams.advanced.weightBasedVd.bodyWeight,
|
||||
tmaxDelay: defaults.pkParams.advanced.foodEffect.tmaxDelay,
|
||||
phTendency: defaults.pkParams.advanced.urinePh.phTendency,
|
||||
fOral: defaults.pkParams.advanced.fOral,
|
||||
fOralPercent: String((parseFloat(defaults.pkParams.advanced.fOral) * 100).toFixed(1)),
|
||||
steadyStateDays: defaults.pkParams.advanced.steadyStateDays,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to interpolate translation strings with default values.
|
||||
* @example t('simulationDurationTooltip', defaultsForT) → "...Default: 5 days."
|
||||
*/
|
||||
const tWithDefaults = (translationFn: any, key: string, defaults: Record<string, string>) => {
|
||||
const translated = translationFn(key);
|
||||
let result = translated;
|
||||
|
||||
// Replace all {{placeholder}} patterns with values from defaults object
|
||||
Object.entries(defaults).forEach(([placeholder, value]) => {
|
||||
result = result.replace(new RegExp(`{{${placeholder}}}`, 'g'), String(value));
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to render tooltip content with inline source links.
|
||||
* Parses [link text](url) markdown-style syntax and renders as clickable links.
|
||||
* @example "See [this study](https://example.com)" → clickable link within tooltip
|
||||
*/
|
||||
const renderTooltipWithLinks = (text: string): React.ReactNode => {
|
||||
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
||||
const parts: React.ReactNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = linkRegex.exec(text)) !== null) {
|
||||
// Add text before link
|
||||
if (match.index > lastIndex) {
|
||||
parts.push(text.substring(lastIndex, match.index));
|
||||
}
|
||||
// Add link
|
||||
parts.push(
|
||||
<a
|
||||
key={`link-${match.index}`}
|
||||
href={match[2]}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline italic text-yellow-300 hover:text-yellow-200 cursor-pointer"
|
||||
>
|
||||
{match[1]}
|
||||
</a>
|
||||
);
|
||||
lastIndex = linkRegex.lastIndex;
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(text.substring(lastIndex));
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts : text;
|
||||
};
|
||||
|
||||
const Settings = ({
|
||||
pkParams,
|
||||
@@ -35,8 +120,40 @@ const Settings = ({
|
||||
const showTherapeuticRange = (uiSettings as any).showTherapeuticRange ?? true;
|
||||
|
||||
const [isDiagramExpanded, setIsDiagramExpanded] = React.useState(true);
|
||||
const [isSimulationExpanded, setIsSimulationExpanded] = React.useState(true);
|
||||
const [isPharmacokineticExpanded, setIsPharmacokineticExpanded] = React.useState(true);
|
||||
const [isAdvancedExpanded, setIsAdvancedExpanded] = React.useState(false);
|
||||
|
||||
// Create defaults object for translation interpolation
|
||||
const defaultsForT = getDefaultsForTranslation(pkParams, therapeuticRange, uiSettings);
|
||||
|
||||
// Helper to update nested advanced settings
|
||||
const updateAdvanced = (category: string, key: string, value: any) => {
|
||||
onUpdatePkParams('advanced', {
|
||||
...pkParams.advanced,
|
||||
[category]: {
|
||||
...pkParams.advanced[category],
|
||||
[key]: value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateAdvancedDirect = (key: string, value: any) => {
|
||||
onUpdatePkParams('advanced', {
|
||||
...pkParams.advanced,
|
||||
[key]: value
|
||||
});
|
||||
};
|
||||
|
||||
// Check for out-of-range warnings
|
||||
const absorptionHL = parseFloat(pkParams.ldx.absorptionHalfLife);
|
||||
const conversionHL = parseFloat(pkParams.ldx.halfLife);
|
||||
const eliminationHL = parseFloat(pkParams.damph.halfLife);
|
||||
|
||||
const absorptionWarning = (absorptionHL < 0.7 || absorptionHL > 1.2);
|
||||
const conversionWarning = (conversionHL < 0.7 || conversionHL > 1.2);
|
||||
const eliminationWarning = (eliminationHL < 9 || eliminationHL > 12);
|
||||
const eliminationExtreme = (eliminationHL < 7 || eliminationHL > 15);
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Diagram Settings Card */}
|
||||
@@ -49,6 +166,172 @@ const Settings = ({
|
||||
</CardHeader>
|
||||
{isDiagramExpanded && (
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="showTemplateDay"
|
||||
checked={showTemplateDay}
|
||||
onCheckedChange={checked => onUpdateUiSetting('showTemplateDay', checked)}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label htmlFor="showTemplateDay" className="font-medium cursor-help">
|
||||
{t('showTemplateDayInChart')}
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showTemplateDayTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="showDayReferenceLines"
|
||||
checked={showDayReferenceLines}
|
||||
onCheckedChange={checked => onUpdateUiSetting('showDayReferenceLines', checked)}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label htmlFor="showDayReferenceLines" className="font-medium cursor-help">
|
||||
{t('showDayReferenceLines')}
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showDayReferenceLinesTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="showTherapeuticRange"
|
||||
checked={showTherapeuticRange}
|
||||
onCheckedChange={checked => onUpdateUiSetting('showTherapeuticRange', checked)}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label htmlFor="showTherapeuticRange" className="font-medium cursor-help">
|
||||
{t('showTherapeuticRangeLines')}
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'showTherapeuticRangeLinesTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{showTherapeuticRange && (
|
||||
<div className="ml-8 space-y-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="font-medium cursor-help">
|
||||
{t('therapeuticRange')}
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'therapeuticRangeTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormNumericInput
|
||||
value={therapeuticRange.min}
|
||||
onChange={val => onUpdateTherapeuticRange('min', val)}
|
||||
increment={0.5}
|
||||
min={0}
|
||||
placeholder={t('min')}
|
||||
required={true}
|
||||
errorMessage={t('errorTherapeuticRangeMinRequired') || 'Minimum therapeutic range is required'}
|
||||
/>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
<FormNumericInput
|
||||
value={therapeuticRange.max}
|
||||
onChange={val => onUpdateTherapeuticRange('max', val)}
|
||||
increment={0.5}
|
||||
min={0}
|
||||
placeholder={t('max')}
|
||||
unit="ng/ml"
|
||||
required={true}
|
||||
errorMessage={t('errorTherapeuticRangeMaxRequired') || 'Maximum therapeutic range is required'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="font-medium cursor-help">{t('displayedDays')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'displayedDaysTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormNumericInput
|
||||
value={displayedDays}
|
||||
onChange={val => onUpdateUiSetting('displayedDays', val)}
|
||||
increment={1}
|
||||
min={1}
|
||||
max={parseInt(simulationDays, 10) || 3}
|
||||
unit={t('unitDays')}
|
||||
required={true}
|
||||
errorMessage={t('errorNumberRequired')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="font-medium cursor-help">{t('yAxisRange')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'yAxisRangeTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormNumericInput
|
||||
value={yAxisMin}
|
||||
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
||||
increment={1}
|
||||
min={0}
|
||||
placeholder={t('auto')}
|
||||
allowEmpty={true}
|
||||
clearButton={true}
|
||||
/>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
<FormNumericInput
|
||||
value={yAxisMax}
|
||||
onChange={val => onUpdateUiSetting('yAxisMax', val)}
|
||||
increment={1}
|
||||
min={0}
|
||||
placeholder={t('auto')}
|
||||
unit="ng/ml"
|
||||
allowEmpty={true}
|
||||
clearButton={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium">{t('xAxisTimeFormat')}</Label>
|
||||
<TooltipProvider>
|
||||
@@ -94,70 +377,31 @@ const Settings = ({
|
||||
</Select>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="showTemplateDay"
|
||||
checked={showTemplateDay}
|
||||
onCheckedChange={checked => onUpdateUiSetting('showTemplateDay', checked)}
|
||||
/>
|
||||
<Label htmlFor="showTemplateDay" className="font-regular">
|
||||
{t('showTemplateDayInChart')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="showDayReferenceLines"
|
||||
checked={showDayReferenceLines}
|
||||
onCheckedChange={checked => onUpdateUiSetting('showDayReferenceLines', checked)}
|
||||
/>
|
||||
<Label htmlFor="showDayReferenceLines" className="font-regular">
|
||||
{t('showDayReferenceLines')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="showTherapeuticRange"
|
||||
checked={showTherapeuticRange}
|
||||
onCheckedChange={checked => onUpdateUiSetting('showTherapeuticRange', checked)}
|
||||
/>
|
||||
<Label htmlFor="showTherapeuticRange" className="font-regular">
|
||||
{t('showTherapeuticRangeLines')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{/* Simulation Settings Card */}
|
||||
<Card>
|
||||
<CardHeader className="cursor-pointer pb-3" onClick={() => setIsSimulationExpanded(!isSimulationExpanded)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">{t('simulationSettings')}</CardTitle>
|
||||
{isSimulationExpanded ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||
</div>
|
||||
</CardHeader>
|
||||
{isSimulationExpanded && (
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
{ /* <Label className={`font-medium ${!showTherapeuticRange ? 'text-muted-foreground' : ''}`}>{t('therapeuticRange')}</Label> */ }
|
||||
<div className="flex items-center gap-2">
|
||||
<FormNumericInput
|
||||
value={therapeuticRange.min}
|
||||
onChange={val => onUpdateTherapeuticRange('min', val)}
|
||||
increment={0.5}
|
||||
min={0}
|
||||
placeholder={t('min')}
|
||||
required={showTherapeuticRange}
|
||||
disabled={!showTherapeuticRange}
|
||||
errorMessage={t('therapeuticRangeMinRequired') || 'Minimum therapeutic range is required'}
|
||||
/>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
<FormNumericInput
|
||||
value={therapeuticRange.max}
|
||||
onChange={val => onUpdateTherapeuticRange('max', val)}
|
||||
increment={0.5}
|
||||
min={0}
|
||||
placeholder={t('max')}
|
||||
unit="ng/ml"
|
||||
required={showTherapeuticRange}
|
||||
disabled={!showTherapeuticRange}
|
||||
errorMessage={t('therapeuticRangeMaxRequired') || 'Maximum therapeutic range is required'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium">{t('simulationDuration')}</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="font-medium cursor-help">{t('simulationDuration')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'simulationDurationTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormNumericInput
|
||||
value={simulationDays}
|
||||
onChange={val => onUpdateUiSetting('simulationDays', val)}
|
||||
@@ -171,44 +415,26 @@ const Settings = ({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium">{t('displayedDays')}</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="font-medium cursor-help">{t('steadyStateDays')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'steadyStateDaysTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormNumericInput
|
||||
value={displayedDays}
|
||||
onChange={val => onUpdateUiSetting('displayedDays', val)}
|
||||
value={pkParams.advanced.steadyStateDays}
|
||||
onChange={val => updateAdvancedDirect('steadyStateDays', val)}
|
||||
increment={1}
|
||||
min={1}
|
||||
max={parseInt(simulationDays, 10) || 3}
|
||||
min={0}
|
||||
max={7}
|
||||
unit={t('unitDays')}
|
||||
required={true}
|
||||
errorMessage={t('errorNumberRequired')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium">{t('yAxisRange')}</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormNumericInput
|
||||
value={yAxisMin}
|
||||
onChange={val => onUpdateUiSetting('yAxisMin', val)}
|
||||
increment={1}
|
||||
min={0}
|
||||
placeholder={t('auto')}
|
||||
allowEmpty={true}
|
||||
clearButton={true}
|
||||
/>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
<FormNumericInput
|
||||
value={yAxisMax}
|
||||
onChange={val => onUpdateUiSetting('yAxisMax', val)}
|
||||
increment={1}
|
||||
min={0}
|
||||
placeholder={t('auto')}
|
||||
unit="ng/ml"
|
||||
allowEmpty={true}
|
||||
clearButton={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
@@ -225,15 +451,28 @@ const Settings = ({
|
||||
<CardContent className="space-y-4">
|
||||
<h3 className="text-lg font-semibold">{t('dAmphetamineParameters')}</h3>
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium">{t('halfLife')}</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="font-medium cursor-help">{t('halfLife')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'halfLifeTooltip', defaultsForT))}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormNumericInput
|
||||
value={pkParams.damph.halfLife}
|
||||
onChange={val => onUpdatePkParams('damph', { ...pkParams.damph, halfLife: val })}
|
||||
increment={0.5}
|
||||
min={0.1}
|
||||
min={5}
|
||||
max={34}
|
||||
unit="h"
|
||||
required={true}
|
||||
errorMessage={t('halfLifeRequired') || 'Half-life is required'}
|
||||
warning={eliminationWarning && !eliminationExtreme}
|
||||
error={eliminationExtreme}
|
||||
warningMessage={t('warningEliminationOutOfRange')}
|
||||
errorMessage={t('errorEliminationHalfLifeRequired')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -241,28 +480,235 @@ const Settings = ({
|
||||
|
||||
<h3 className="text-lg font-semibold">{t('lisdexamfetamineParameters')}</h3>
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium">{t('conversionHalfLife')}</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="font-medium cursor-help">{t('conversionHalfLife')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'conversionHalfLifeTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormNumericInput
|
||||
value={pkParams.ldx.halfLife}
|
||||
onChange={val => onUpdatePkParams('ldx', { ...pkParams.ldx, halfLife: val })}
|
||||
increment={0.1}
|
||||
min={0.1}
|
||||
min={0.5}
|
||||
max={2}
|
||||
unit="h"
|
||||
required={true}
|
||||
errorMessage={t('conversionHalfLifeRequired') || 'Conversion half-life is required'}
|
||||
warning={conversionWarning}
|
||||
warningMessage={t('warningConversionOutOfRange')}
|
||||
errorMessage={t('errorConversionHalfLifeRequired')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="font-medium">{t('absorptionRate')}</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="font-medium cursor-help">{t('absorptionHalfLife')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'absorptionHalfLifeTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormNumericInput
|
||||
value={pkParams.ldx.absorptionRate}
|
||||
onChange={val => onUpdatePkParams('ldx', { ...pkParams.ldx, absorptionRate: val })}
|
||||
value={pkParams.ldx.absorptionHalfLife}
|
||||
onChange={val => onUpdatePkParams('ldx', { ...pkParams.ldx, absorptionHalfLife: val })}
|
||||
increment={0.1}
|
||||
min={0.1}
|
||||
unit={t('faster')}
|
||||
min={0.5}
|
||||
max={2}
|
||||
unit="h"
|
||||
required={true}
|
||||
warning={absorptionWarning}
|
||||
warningMessage={t('warningAbsorptionOutOfRange')}
|
||||
errorMessage={t('errorAbsorptionRateRequired')}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Advanced Settings Card */}
|
||||
<Card>
|
||||
<CardHeader className="cursor-pointer pb-3" onClick={() => setIsAdvancedExpanded(!isAdvancedExpanded)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">{t('advancedSettings')}</CardTitle>
|
||||
{isAdvancedExpanded ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||
</div>
|
||||
</CardHeader>
|
||||
{isAdvancedExpanded && (
|
||||
<CardContent className="space-y-4">
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md p-3 text-sm">
|
||||
<p className="text-yellow-800 dark:text-yellow-200">{t('advancedSettingsWarning')}</p>
|
||||
</div>
|
||||
|
||||
{/* Weight-Based Vd */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="weightBasedVdEnabled"
|
||||
checked={pkParams.advanced.weightBasedVd.enabled}
|
||||
onCheckedChange={checked => updateAdvanced('weightBasedVd', 'enabled', checked)}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label htmlFor="weightBasedVdEnabled" className="font-medium cursor-help">
|
||||
{t('weightBasedVdScaling')}
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'weightBasedVdTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{pkParams.advanced.weightBasedVd.enabled && (
|
||||
<div className="ml-8 space-y-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="text-sm font-medium cursor-help">{t('bodyWeight')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'bodyWeightTooltip', defaultsForT))}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormNumericInput
|
||||
value={pkParams.advanced.weightBasedVd.bodyWeight}
|
||||
onChange={val => updateAdvanced('weightBasedVd', 'bodyWeight', val)}
|
||||
increment={1}
|
||||
min={20}
|
||||
max={150}
|
||||
unit={t('bodyWeightUnit')}
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
{/* Food Effect */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="foodEffectEnabled"
|
||||
checked={pkParams.advanced.foodEffect.enabled}
|
||||
onCheckedChange={checked => updateAdvanced('foodEffect', 'enabled', checked)}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label htmlFor="foodEffectEnabled" className="font-medium cursor-help">
|
||||
{t('foodEffectEnabled')}
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'foodEffectTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{pkParams.advanced.foodEffect.enabled && (
|
||||
<div className="ml-8 space-y-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="text-sm font-medium cursor-help">{t('tmaxDelay')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'tmaxDelayTooltip', defaultsForT))}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormNumericInput
|
||||
value={pkParams.advanced.foodEffect.tmaxDelay}
|
||||
onChange={val => updateAdvanced('foodEffect', 'tmaxDelay', val)}
|
||||
increment={0.1}
|
||||
min={0}
|
||||
max={2}
|
||||
unit={t('tmaxDelayUnit')}
|
||||
required={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
{/* Urine pH */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch
|
||||
id="urinePHEnabled"
|
||||
checked={pkParams.advanced.urinePh.enabled}
|
||||
onCheckedChange={checked => updateAdvanced('urinePh', 'enabled', checked)}
|
||||
/>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label htmlFor="urinePHEnabled" className="font-medium cursor-help">
|
||||
{t('urinePHTendency')}
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'urinePHTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
{pkParams.advanced.urinePh.enabled && (
|
||||
<div className="ml-8 space-y-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="text-sm font-medium cursor-help">{t('urinePHValue')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{tWithDefaults(t, 'urinePHValueTooltip', defaultsForT)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormNumericInput
|
||||
value={pkParams.advanced.urinePh.phTendency}
|
||||
onChange={val => updateAdvanced('urinePh', 'phTendency', val)}
|
||||
increment={0.1}
|
||||
min={5.5}
|
||||
max={8.0}
|
||||
required={true}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t('phUnit')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
{/* Oral Bioavailability */}
|
||||
<div className="space-y-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Label className="font-medium cursor-help">{t('oralBioavailability')}</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p className="text-xs max-w-xs">{renderTooltipWithLinks(tWithDefaults(t, 'oralBioavailabilityTooltip', defaultsForT))}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<FormNumericInput
|
||||
value={pkParams.advanced.fOral}
|
||||
onChange={val => updateAdvancedDirect('fOral', val)}
|
||||
increment={0.01}
|
||||
min={0.5}
|
||||
max={1.0}
|
||||
required={true}
|
||||
errorMessage={t('absorptionRateRequired') || 'Absorption rate is required'}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -421,7 +421,7 @@ const SimulationChart = ({
|
||||
key={`day-${day+1}`}
|
||||
x={24 * (day+1)}
|
||||
label={{
|
||||
value: t('refLineDayX', { x: day+1 }) + '→' + getDayLabel(day + 1),
|
||||
value: t('refLineDayX', { x: day+1 }) + ' ' + getDayLabel(day + 1),
|
||||
position: 'insideTopRight',
|
||||
style: {
|
||||
fontSize: '0.75rem',
|
||||
|
||||
@@ -140,18 +140,6 @@ const FormNumericInput = React.forwardRef<HTMLInputElement, NumericInputProps>(
|
||||
setShowWarning(hasWarning)
|
||||
}
|
||||
|
||||
// Ensure value is consistently formatted to the required decimal places
|
||||
React.useEffect(() => {
|
||||
const strVal = String(value)
|
||||
if (strVal === '') return
|
||||
const num = Number(strVal)
|
||||
if (isNaN(num)) return
|
||||
const formatted = num.toFixed(decimalPlaces)
|
||||
if (strVal !== formatted) {
|
||||
onChange(formatted)
|
||||
}
|
||||
}, [value, decimalPlaces, onChange])
|
||||
|
||||
const getAlignmentClass = () => {
|
||||
switch (align) {
|
||||
case 'left': return 'text-left'
|
||||
|
||||
@@ -8,13 +8,27 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export const LOCAL_STORAGE_KEY = 'medPlanAssistantState_v6';
|
||||
export const LDX_TO_DAMPH_CONVERSION_FACTOR = 0.2948;
|
||||
export const LOCAL_STORAGE_KEY = 'medPlanAssistantState_v7';
|
||||
|
||||
// Pharmacokinetic Constants (from research literature)
|
||||
// MW ratio: 135.21 (d-amphetamine) / 455.60 (LDX dimesylate) = 0.29677
|
||||
export const LDX_TO_DAMPH_SALT_FACTOR = 0.29677;
|
||||
// Oral bioavailability for LDX (FDA label, 96.4%)
|
||||
export const DEFAULT_F_ORAL = 0.96;
|
||||
|
||||
// Type definitions
|
||||
export interface AdvancedSettings {
|
||||
weightBasedVd: { enabled: boolean; bodyWeight: string }; // kg
|
||||
foodEffect: { enabled: boolean; tmaxDelay: string }; // hours
|
||||
urinePh: { enabled: boolean; phTendency: string }; // 5.5-8.0 range
|
||||
fOral: string; // bioavailability fraction
|
||||
steadyStateDays: string; // days of medication history to simulate
|
||||
}
|
||||
|
||||
export interface PkParams {
|
||||
damph: { halfLife: string };
|
||||
ldx: { halfLife: string; absorptionRate: string };
|
||||
ldx: { halfLife: string; absorptionHalfLife: string }; // renamed from absorptionRate
|
||||
advanced: AdvancedSettings;
|
||||
}
|
||||
|
||||
export interface DayDose {
|
||||
@@ -82,7 +96,17 @@ export interface ConcentrationPoint {
|
||||
export const getDefaultState = (): AppState => ({
|
||||
pkParams: {
|
||||
damph: { halfLife: '11' },
|
||||
ldx: { halfLife: '0.8', absorptionRate: '1.5' },
|
||||
ldx: {
|
||||
halfLife: '0.8',
|
||||
absorptionHalfLife: '0.9' // changed from 1.5, better reflects ~1h Tmax
|
||||
},
|
||||
advanced: {
|
||||
weightBasedVd: { enabled: false, bodyWeight: '70' }, // kg, adult average
|
||||
foodEffect: { enabled: false, tmaxDelay: '1.0' }, // hours delay
|
||||
urinePh: { enabled: false, phTendency: '6.0' }, // pH scale (5.5-8.0)
|
||||
fOral: String(DEFAULT_F_ORAL), // 0.96 bioavailability
|
||||
steadyStateDays: '7' // days of prior medication history
|
||||
}
|
||||
},
|
||||
days: [
|
||||
{
|
||||
@@ -96,15 +120,15 @@ export const getDefaultState = (): AppState => ({
|
||||
]
|
||||
}
|
||||
],
|
||||
steadyStateConfig: { daysOnMedication: '7' },
|
||||
therapeuticRange: { min: '10.5', max: '11.5' },
|
||||
steadyStateConfig: { daysOnMedication: '7' }, // kept for backwards compatibility, now sourced from pkParams.advanced
|
||||
therapeuticRange: { min: '5', max: '25' }, // widened from 10.5-11.5 to general adult range
|
||||
doseIncrement: '2.5',
|
||||
uiSettings: {
|
||||
showDayTimeOnXAxis: '24h',
|
||||
showTemplateDay: true,
|
||||
chartView: 'damph',
|
||||
yAxisMin: '8',
|
||||
yAxisMax: '13',
|
||||
yAxisMin: '',
|
||||
yAxisMax: '',
|
||||
simulationDays: '5',
|
||||
displayedDays: '2',
|
||||
showTherapeuticRange: true,
|
||||
|
||||
@@ -43,12 +43,11 @@ export const de = {
|
||||
axisLabelHours: "Stunden (h)",
|
||||
axisLabelTimeOfDay: "Tageszeit (h)",
|
||||
tickNoon: "Mittag",
|
||||
refLineRegularPlan: "Regulärer Plan",
|
||||
refLineDeviatingPlan: "Abweichung vom Plan",
|
||||
refLineNoDeviation: "Keine Abweichung",
|
||||
refLineRegularPlan: "Regulär",
|
||||
refLineNoDeviation: "Regulär",
|
||||
refLineRecovering: "Erholung",
|
||||
refLineIrregularIntake: "Irreguläre Einnahme",
|
||||
refLineDayX: "Tag {{x}}",
|
||||
refLineIrregularIntake: "Irregulär",
|
||||
refLineDayX: "T{{x}}",
|
||||
refLineMin: "Min",
|
||||
refLineMax: "Max",
|
||||
tooltipHour: "Stunde",
|
||||
@@ -56,6 +55,8 @@ export const de = {
|
||||
// Settings
|
||||
diagramSettings: "Diagramm-Einstellungen",
|
||||
pharmacokineticsSettings: "Pharmakokinetik-Einstellungen",
|
||||
advancedSettings: "Erweiterte Einstellungen",
|
||||
advancedSettingsWarning: "⚠️ Diese Parameter beeinflussen die Simulationsgenauigkeit und können von Bevölkerungsdurchschnitten abweichen. Nur anpassen, wenn spezifische klinische Daten oder Forschungsreferenzen vorliegen.",
|
||||
xAxisTimeFormat: "Zeitformat",
|
||||
xAxisFormatContinuous: "Fortlaufend",
|
||||
xAxisFormatContinuousDesc: "Endlose Sequenz (0h, 6h, 12h...)",
|
||||
@@ -63,26 +64,83 @@ export const de = {
|
||||
xAxisFormat24hDesc: "Wiederholender 0-24h Zyklus",
|
||||
xAxisFormat12h: "Tageszeit (12h AM/PM)",
|
||||
xAxisFormat12hDesc: "Wiederholend 12h Zyklus im AM/PM Format",
|
||||
showTemplateDayInChart: "Regulären Plan einblenden (nur bei abweichenden Tagen)",
|
||||
showTemplateDayInChart: "Regulären Plan kontinuierlich anzeigen",
|
||||
showTemplateDayTooltip: "Medikationsplan als Referenz-Overlay jederzeit anzeigen (Standard: aktiviert).",
|
||||
simulationSettings: "Simulations-Einstellungen",
|
||||
|
||||
showDayReferenceLines: "Tagestrenner anzeigen (Vertikale Referenzlinien und Status)",
|
||||
showTherapeuticRangeLines: "Therapeutischen Bereich anzeigen (Horizontale Min/MaxReferenzlinien)",
|
||||
showDayReferenceLines: "Tagestrenner anzeigen",
|
||||
showDayReferenceLinesTooltip: "Vertikale Linien und Statusanzeigen zwischen Tagen anzeigen (Standard: aktiviert).",
|
||||
showTherapeuticRangeLines: "Therapeutischen Bereich anzeigen",
|
||||
showTherapeuticRangeLinesTooltip: "Horizontale Referenzlinien für therapeutisches Min/Max anzeigen (Standard: aktiviert).",
|
||||
simulationDuration: "Simulationsdauer",
|
||||
simulationDurationTooltip: "Anzahl der zu simulierenden Tage. Längere Zeiträume zeigen Steady-State. Standard: {{simulationDays}} Tage.",
|
||||
displayedDays: "Sichtbare Tage (im Fokus)",
|
||||
yAxisRange: "Y-Achsen-Bereich (Zoom)",
|
||||
displayedDaysTooltip: "Wie viele Tage auf einmal angezeigt werden. Kleinere Werte zoomen in Details. Standard: {{displayedDays}} Tag(e).",
|
||||
yAxisRange: "Y-Achsen-Bereich (Konzentrations-Zoom)",
|
||||
yAxisRangeTooltip: "Vertikale Achse manuell festlegen (Konzentrationsskala). Leer lassen für automatische Anpassung. Standard: auto.",
|
||||
yAxisRangeAutoButton: "A",
|
||||
yAxisRangeAutoButtonTitle: "Bereich automatisch anhand des Datenbereichs bestimmen",
|
||||
auto: "Auto",
|
||||
therapeuticRange: "Therapeutischer Bereich (Referenzlinien)",
|
||||
therapeuticRange: "Therapeutischer Bereich",
|
||||
therapeuticRangeTooltip: "Referenzkonzentrationen für Medikamentenwirksamkeit. Typischer Bereich für Erwachsene: 5-25 ng/mL. Individuelle therapeutische Fenster variieren erheblich. Standard: {{therapeuticRangeMin}}-{{therapeuticRangeMax}} ng/mL. Konsultiere deinen Arzt.",
|
||||
dAmphetamineParameters: "d-Amphetamin Parameter",
|
||||
halfLife: "Halbwertszeit",
|
||||
lisdexamfetamineParameters: "Lisdexamfetamin Parameter",
|
||||
conversionHalfLife: "Umwandlungs-Halbwertszeit",
|
||||
absorptionRate: "Absorptionsrate",
|
||||
halfLife: "Eliminations-Halbwertszeit",
|
||||
halfLifeTooltip: "Zeit bis der Körper die Hälfte des d-Amphetamins aus dem Blut ausscheidet. Beeinflusst durch Urin-pH: sauer (<6) → 7-9h, neutral (6-7,5) → 10-12h, alkalisch (>7,5) → 13-15h. Siehe [therapeutische Referenzbereiche](https://www.thieme-connect.com/products/ejournals/pdf/10.1055/a-2689-4911.pdf). Standard: {{damphHalfLife}}h.",
|
||||
lisdexamfetamineParameters: "Lisdexamfetamin (LDX) Parameter",
|
||||
conversionHalfLife: "LDX→d-Amph Umwandlungs-Halbwertszeit",
|
||||
conversionHalfLifeTooltip: "Zeit bis rote Blutkörperchen die Hälfte des inaktiven LDX-Prodrugs in aktives d-Amphetamin umwandeln. Typisch: 0,7-1,2h. Standard: {{ldxHalfLife}}h.",
|
||||
absorptionHalfLife: "Absorptions-Halbwertszeit",
|
||||
absorptionHalfLifeTooltip: "Zeit bis der Darm die Hälfte des LDX vom Magen ins Blut aufnimmt. Durch Nahrung verzögert (~1h Verschiebung). Typisch: 0,7-1,2h. Standard: {{ldxAbsorptionHalfLife}}h.",
|
||||
faster: "(schneller >)",
|
||||
resetAllSettings: "Alle Einstellungen zurücksetzen", resetDiagramSettings: "Diagramm-Einstellungen zurücksetzen",
|
||||
resetPharmacokineticSetting: "Pharmakokinetik-Einstellungen zurücksetzen",
|
||||
|
||||
// Advanced Settings
|
||||
weightBasedVdScaling: "Gewichtsbasiertes Verteilungsvolumen",
|
||||
weightBasedVdTooltip: "Passt Plasmakonzentrationen basierend auf Körpergewicht an (proportional zu ~5,4 L/kg). Leichtere → höhere Spitzen, schwerere → niedrigere. Bei Deaktivierung: 70 kg Erwachsener.",
|
||||
bodyWeight: "Körpergewicht",
|
||||
bodyWeightTooltip: "Dein Körpergewicht für Konzentrationsanpassung. Verwendet zur Berechnung des Verteilungsvolumens (Vd = Gewicht × 5,4). Siehe [Populations-Pharmakokinetik](https://pmc.ncbi.nlm.nih.gov/articles/PMC5572767/). Standard: {{bodyWeight}} kg.",
|
||||
bodyWeightUnit: "kg",
|
||||
|
||||
foodEffectEnabled: "Mit Mahlzeit eingenommen",
|
||||
foodEffectTooltip: "Fettreiche Mahlzeiten verzögern die Absorption ohne Gesamtaufnahme zu ändern. Verlangsamt Wirkungseintritt (~1h Verzögerung). Bei Deaktivierung: Nüchterner Zustand.",
|
||||
tmaxDelay: "Absorptionsverzögerung",
|
||||
tmaxDelayTooltip: "Wie viel die Mahlzeit die Absorption verzögert (Tmax-Verschiebung). Siehe [Nahrungseffekt-Studie](https://pmc.ncbi.nlm.nih.gov/articles/PMC4823324/) von Ermer et al. Typisch: 1,0h für fettreiche Mahlzeit. Standard: {{tmaxDelay}}h.",
|
||||
tmaxDelayUnit: "h",
|
||||
|
||||
urinePHTendency: "Urin-pH-Effekte",
|
||||
urinePHTooltip: "Urin-pH beeinflusst Nierenrückresorption von Amphetamin. Ermöglicht pH-abhängige Halbwertszeit-Variation (7-15h Bereich). Bei Deaktivierung: neutraler pH (~11h HWZ).",
|
||||
urinePHValue: "pH-Wert",
|
||||
urinePHValueTooltip: "Dein typischer Urin-pH (sauer=schnellere Ausscheidung, alkalisch=langsamer). Standard: {{phTendency}}. Bereich: 5,5-8,0.",
|
||||
phValue: "pH-Wert",
|
||||
phUnit: "(5,5-8,0)",
|
||||
|
||||
oralBioavailability: "Orale Bioverfügbarkeit",
|
||||
oralBioavailabilityTooltip: "Anteil der LDX-Dosis, der ins Blut gelangt. Siehe [Bioverfügbarkeitsstudie](https://www.frontiersin.org/journals/pharmacology/articles/10.3389/fphar.2022.881198/full) (FDA-Label: 96,4%). Selten Anpassung nötig, außer bei dokumentierten Absorptionsproblemen. Standard: {{fOral}} ({{fOralPercent}}%).",
|
||||
|
||||
steadyStateDays: "Medikationshistorie",
|
||||
steadyStateDaysTooltip: "Anzahl vorheriger Tage stabiler Medikamentendosis zur Simulation der Akkumulation/Steady-State. 0 setzen für \"erster Tag ohne Vorgeschichte.\" Standard: {{steadyStateDays}} Tage. Max: 7.",
|
||||
|
||||
resetAllSettings: "Alle Einstellungen zurücksetzen",
|
||||
resetDiagramSettings: "Diagramm-Einstellungen zurücksetzen",
|
||||
resetPharmacokineticSettings: "Pharmakokinetik-Einstellungen zurücksetzen",
|
||||
resetPlan: "Plan zurücksetzen",
|
||||
|
||||
// Disclaimer Modal
|
||||
disclaimerModalTitle: "Wichtiger medizinischer Haftungsausschluss",
|
||||
disclaimerModalSubtitle: "Bitte sorgfältig lesen vor Nutzung dieses Simulationstools",
|
||||
disclaimerModalPurpose: "Zweck & Einschränkungen",
|
||||
disclaimerModalPurposeText: "Diese Anwendung bietet theoretische pharmakokinetische Simulationen basierend auf Bevölkerungsdurchschnitten. Sie ist KEIN medizinisches Gerät und dient ausschließlich zu Bildungs- und Informationszwecken.",
|
||||
disclaimerModalVariability: "Individuelle Variabilität",
|
||||
disclaimerModalVariabilityText: "Arzneimittelmetabolismus variiert erheblich aufgrund von Körpergewicht, Nierenfunktion, Urin-pH, Genetik und anderen Faktoren. Tatsächliche Plasmakonzentrationen können um 30-40% oder mehr von diesen Schätzungen abweichen.",
|
||||
disclaimerModalMedicalAdvice: "Ärztliche Konsultation erforderlich",
|
||||
disclaimerModalMedicalAdviceText: "Verwende diese Daten NICHT zur Anpassung deiner Medikamentendosis. Konsultiere immer deinen verschreibenden Arzt für medizinische Entscheidungen. Unsachgemäße Dosisanpassungen können ernsthaften Schaden verursachen.",
|
||||
disclaimerModalDataSources: "Datenquellen",
|
||||
disclaimerModalDataSourcesText: "Simulationen nutzen etablierte pharmakokinetische Modelle mit Parametern aus: Ermer et al. (2016), Boellner et al. (2010), Roberts et al. (2015), und FDA Verschreibungsinformationen für Vyvanse®/Elvanse®.",
|
||||
disclaimerModalScheduleII: "Warnung zu kontrollierter Substanz",
|
||||
disclaimerModalScheduleIIText: "Lisdexamfetamin ist eine kontrollierte Substanz (Betäubungsmittel) mit, im Vergleich zum aktiven Dexamfetamin, moderatem Missbrauchs- und Abhängigkeitspotenzial. Unsachgemäßer oder missbräuchlicher Gebrauch kann schwerwiegende gesundheitliche Folgen so wie strafrechtliche konsequenzen haben.",
|
||||
disclaimerModalLiability: "Keine Garantien oder Gewährleistungen",
|
||||
disclaimerModalLiabilityText: "Dies ist ein Hobby-/Bildungsprojekt ohne kommerzielle Absicht. Der Entwickler übernimmt keine Garantien, Gewährleistungen oder Haftung. Nutzung erfolgt vollständig auf eigenes Risiko.",
|
||||
disclaimerModalAccept: "Verstanden - Weiter zur App",
|
||||
disclaimerModalFooterLink: "Medizinischer Haftungsausschluss & Datenquellen",
|
||||
// Units
|
||||
unitMg: "mg",
|
||||
unitNgml: "ng/ml",
|
||||
@@ -99,16 +157,23 @@ export const de = {
|
||||
// Number input field
|
||||
buttonClear: "Feld löschen",
|
||||
|
||||
// Field validation
|
||||
errorNumberRequired: "Bitte gib eine gültige Zahl ein.",
|
||||
errorTimeRequired: "Bitte gib eine gültige Zeitangabe ein.",
|
||||
warningDuplicateTime: "Mehrere Dosen zur gleichen Zeit.",
|
||||
warningZeroDose: "Nulldosis hat keine Auswirkung auf die Simulation.",
|
||||
halfLifeRequired: "Halbwertszeit ist erforderlich.",
|
||||
conversionHalfLifeRequired: "Umwandlungs-Halbwertszeit ist erforderlich.",
|
||||
absorptionRateRequired: "Absorptionsrate ist erforderlich.",
|
||||
therapeuticRangeMinRequired: "Minimaler therapeutischer Bereich ist erforderlich.",
|
||||
therapeuticRangeMaxRequired: "Maximaler therapeutischer Bereich ist erforderlich.",
|
||||
// Field validation - Errors
|
||||
errorNumberRequired: "⛔ Bitte gib eine gültige Zahl ein.",
|
||||
errorTimeRequired: "⛔ Bitte gib eine gültige Zeitangabe ein.",
|
||||
errorHalfLifeRequired: "⛔ Halbwertszeit ist erforderlich.",
|
||||
errorAbsorptionRateRequired: "⛔ Absorptionsrate ist erforderlich.",
|
||||
errorConversionHalfLifeRequired: "⛔ Umwandlungs-Halbwertszeit ist erforderlich.",
|
||||
errorTherapeuticRangeMinRequired: "⛔ Minimaler therapeutischer Bereich ist erforderlich.",
|
||||
errorTherapeuticRangeMaxRequired: "⛔ Maximaler therapeutischer Bereich ist erforderlich.",
|
||||
errorEliminationHalfLifeRequired: "⛔ Eliminations-Halbwertszeit ist erforderlich.",
|
||||
|
||||
// Field validation - Warnings
|
||||
warningDuplicateTime: "⚠️ Mehrere Dosen zur gleichen Zeit.",
|
||||
warningZeroDose: "⚠️ Nulldosis hat keine Auswirkung auf die Simulation.",
|
||||
warningAbsorptionOutOfRange: "⚠️ Typischer Bereich: 0,7-1,2h. Aktueller Wert könnte außerhalb klinischer Normen liegen.",
|
||||
warningConversionOutOfRange: "⚠️ Typischer Bereich: 0,7-1,2h. Aktueller Wert könnte außerhalb klinischer Normen liegen.",
|
||||
warningEliminationOutOfRange: "⚠️ Typischer Bereich: 9-12h (normaler pH). Erweiterter Bereich 7-15h (pH-Effekte). Aktueller Wert ist ungewöhnlich.",
|
||||
warningDoseAbove70mg: "⚠️ FDA-zugelassenes Maximum: 70 mg. Höhere Dosen haben keine Sicherheitsdaten und erhöhen kardiovaskuläre Risiken.",
|
||||
|
||||
// Day-based schedule
|
||||
regularPlan: "Regulärer Plan",
|
||||
|
||||
@@ -43,18 +43,19 @@ export const en = {
|
||||
axisLabelHours: "Hours (h)",
|
||||
axisLabelTimeOfDay: "Time of Day (h)",
|
||||
tickNoon: "Noon",
|
||||
refLineRegularPlan: "Regular Plan",
|
||||
refLineDeviatingPlan: "Deviation from Plan",
|
||||
refLineNoDeviation: "No Deviation",
|
||||
refLineRegularPlan: "Regular",
|
||||
refLineNoDeviation: "Regular",
|
||||
refLineRecovering: "Recovering",
|
||||
refLineIrregularIntake: "Irregular Intake",
|
||||
refLineDayX: "Day {{x}}",
|
||||
refLineIrregularIntake: "Irregular",
|
||||
refLineDayX: "D{{x}}",
|
||||
refLineMin: "Min",
|
||||
refLineMax: "Max",
|
||||
|
||||
// Settings
|
||||
diagramSettings: "Diagram Settings",
|
||||
pharmacokineticsSettings: "Pharmacokinetics Settings",
|
||||
advancedSettings: "Advanced Settings",
|
||||
advancedSettingsWarning: "⚠️ These parameters affect simulation accuracy and may deviate from population averages. Adjust only if you have specific clinical data or research references.",
|
||||
xAxisTimeFormat: "Time Format",
|
||||
xAxisFormatContinuous: "Continuous",
|
||||
xAxisFormatContinuousDesc: "Endless sequence (0h, 6h, 12h...)",
|
||||
@@ -62,25 +63,82 @@ export const en = {
|
||||
xAxisFormat24hDesc: "Repeating 0-24h cycle",
|
||||
xAxisFormat12h: "Time of Day (12h AM/PM)",
|
||||
xAxisFormat12hDesc: "Repeating 12h cycle in AM/PM format",
|
||||
showTemplateDayInChart: "Show Regular Plan (Only for Deviating Days)",
|
||||
showDayReferenceLines: "Show Day Separators (Vertical Reference Lines and Status)",
|
||||
showTherapeuticRangeLines: "Show Therapeutic Range (Horizontal Min/Max Reference Lines)",
|
||||
showTemplateDayInChart: "Continuously Show Regular Plan",
|
||||
showTemplateDayTooltip: "Display the regular medication plan as reference overlay at all times (default: enabled).",
|
||||
simulationSettings: "Simulation Settings",
|
||||
showDayReferenceLines: "Show Day Separators",
|
||||
showDayReferenceLinesTooltip: "Display vertical lines and status indicators separating days (default: enabled).",
|
||||
showTherapeuticRangeLines: "Show Therapeutic Range",
|
||||
showTherapeuticRangeLinesTooltip: "Display horizontal reference lines for therapeutic min/max concentrations (default: enabled).",
|
||||
simulationDuration: "Simulation Duration",
|
||||
simulationDurationTooltip: "Number of days to simulate. Longer periods allow steady-state observation. Default: {{simulationDays}} days.",
|
||||
displayedDays: "Visible Days (in Focus)",
|
||||
yAxisRange: "Y-Axis Range (Zoom)",
|
||||
displayedDaysTooltip: "How many days to display on screen at once. Smaller values zoom in on details. Default: {{displayedDays}} day(s).",
|
||||
yAxisRange: "Y-Axis Range (Concentration Zoom)",
|
||||
yAxisRangeTooltip: "Manually set vertical axis limits (concentration scale). Leave empty for automatic scaling based on data. Default: auto.",
|
||||
yAxisRangeAutoButton: "A",
|
||||
yAxisRangeAutoButtonTitle: "Determine range automatically based on data range",
|
||||
auto: "Auto",
|
||||
therapeuticRange: "Therapeutic Range (Reference Lines)",
|
||||
therapeuticRange: "Therapeutic Range",
|
||||
therapeuticRangeTooltip: "Reference concentrations for medication efficacy. Typical adult range: 5-25 ng/mL. Individual therapeutic windows vary significantly. Default: {{therapeuticRangeMin}}-{{therapeuticRangeMax}} ng/mL. Consult your physician.",
|
||||
dAmphetamineParameters: "d-Amphetamine Parameters",
|
||||
halfLife: "Half-life",
|
||||
lisdexamfetamineParameters: "Lisdexamfetamine Parameters",
|
||||
conversionHalfLife: "Conversion Half-life",
|
||||
absorptionRate: "Absorption Rate",
|
||||
halfLife: "Elimination Half-life",
|
||||
halfLifeTooltip: "Time for body to clear half the d-amphetamine from blood. Affected by urine pH: acidic (<6) → 7-9h, neutral (6-7.5) → 10-12h, alkaline (>7.5) → 13-15h. See [therapeutic reference ranges](https://www.thieme-connect.com/products/ejournals/pdf/10.1055/a-2689-4911.pdf). Default: {{damphHalfLife}}h.",
|
||||
lisdexamfetamineParameters: "Lisdexamfetamine (LDX) Parameters",
|
||||
conversionHalfLife: "LDX→d-Amph Conversion Half-life",
|
||||
conversionHalfLifeTooltip: "Time for red blood cells to convert half the inactive LDX prodrug into active d-amphetamine. Typical: 0.7-1.2h. Default: {{ldxHalfLife}}h.",
|
||||
absorptionHalfLife: "Absorption Half-life",
|
||||
absorptionHalfLifeTooltip: "Time for intestines to absorb half the LDX from stomach to blood. Delayed by food (~1h shift). Typical: 0.7-1.2h. Default: {{ldxAbsorptionHalfLife}}h.",
|
||||
faster: "(faster >)",
|
||||
resetAllSettings: "Reset All Settings", resetDiagramSettings: "Reset Diagram Settings",
|
||||
|
||||
// Advanced Settings
|
||||
weightBasedVdScaling: "Weight-Based Volume of Distribution",
|
||||
weightBasedVdTooltip: "Adjusts plasma concentrations based on body weight (proportional to ~5.4 L/kg). Lighter persons → higher peaks, heavier → lower peaks. When disabled, assumes 70 kg adult.",
|
||||
bodyWeight: "Body Weight",
|
||||
bodyWeightTooltip: "Your body weight for concentration scaling. Used to calculate volume of distribution (Vd = weight × 5.4). See [population PK analysis](https://pmc.ncbi.nlm.nih.gov/articles/PMC5572767/). Default: {{bodyWeight}} kg.",
|
||||
bodyWeightUnit: "kg",
|
||||
|
||||
foodEffectEnabled: "Taken With Meal",
|
||||
foodEffectTooltip: "High-fat meals delay absorption without changing total exposure. Slows onset of effects (~1h delay). When disabled, assumes fasted state.",
|
||||
tmaxDelay: "Absorption Delay",
|
||||
tmaxDelayTooltip: "How much the meal delays absorption (Tmax shift). See [food effect study](https://pmc.ncbi.nlm.nih.gov/articles/PMC4823324/) by Ermer et al. Typical: 1.0h for high-fat meal. Default: {{tmaxDelay}}h.",
|
||||
tmaxDelayUnit: "h",
|
||||
|
||||
urinePHTendency: "Urine pH Effects",
|
||||
urinePHTooltip: "Urine pH affects kidney reabsorption of amphetamine. Enables pH-dependent half-life variation (7-15h range). When disabled, assumes neutral pH (~11h HL).",
|
||||
urinePHValue: "pH Value",
|
||||
urinePHValueTooltip: "Your typical urine pH (acidic=faster clearance, alkaline=slower). Default: {{phTendency}}. Range: 5.5-8.0.",
|
||||
phValue: "pH Value",
|
||||
phUnit: "(5.5-8.0)",
|
||||
|
||||
oralBioavailability: "Oral Bioavailability",
|
||||
oralBioavailabilityTooltip: "Fraction of LDX dose that reaches bloodstream. See [bioavailability study](https://www.frontiersin.org/journals/pharmacology/articles/10.3389/fphar.2022.881198/full) (FDA label: 96.4%). Rarely needs adjustment unless you have documented absorption issues. Default: {{fOral}} ({{fOralPercent}}%).",
|
||||
|
||||
steadyStateDays: "Medication History",
|
||||
steadyStateDaysTooltip: "Number of prior days on stable medication dose to simulate accumulation/steady-state. Set 0 for \"first day from scratch.\" Default: {{steadyStateDays}} days. Max: 7.",
|
||||
|
||||
resetAllSettings: "Reset All Settings",
|
||||
resetDiagramSettings: "Reset Diagram Settings",
|
||||
resetPharmacokineticSettings: "Reset Pharmacokinetic Settings",
|
||||
resetPlan: "Reset Plan",
|
||||
|
||||
// Disclaimer Modal
|
||||
disclaimerModalTitle: "Important Medical Disclaimer",
|
||||
disclaimerModalSubtitle: "Please read carefully before using this simulation tool",
|
||||
disclaimerModalPurpose: "Purpose & Limitations",
|
||||
disclaimerModalPurposeText: "This application provides theoretical pharmacokinetic simulations based on population average parameters. It is NOT a medical device and is for educational and informational purposes only.",
|
||||
disclaimerModalVariability: "Individual Variability",
|
||||
disclaimerModalVariabilityText: "Drug metabolism varies significantly due to body weight, kidney function, urine pH, genetics, and other factors. Real-world plasma concentrations may differ by 30-40% or more from these estimates.",
|
||||
disclaimerModalMedicalAdvice: "Medical Consultation Required",
|
||||
disclaimerModalMedicalAdviceText: "Do NOT use this data to adjust your medication dosage. Always consult your prescribing physician for medical decisions. Improper dose adjustments can cause serious harm.",
|
||||
disclaimerModalDataSources: "Data Sources",
|
||||
disclaimerModalDataSourcesText: "Simulations utilize established pharmacokinetic models incorporating parameters from: Ermer et al. (2016), Boellner et al. (2010), Roberts et al. (2015), and FDA Prescribing Information for Vyvanse®/Elvanse®.",
|
||||
disclaimerModalScheduleII: "Controlled Substance Warning",
|
||||
disclaimerModalScheduleIIText: "Lisdexamfetamine is a controlled substance (Schedule II) with moderate abuse and dependence potential compared to active dexamphetamine. Improper or abusive use can lead to serious health consequences as well as legal repercussions.",
|
||||
disclaimerModalLiability: "No Warranties or Guarantees",
|
||||
disclaimerModalLiabilityText: "This is a hobbyist/educational project with no commercial intent. The developer provides no warranties, guarantees, or liability. Use entirely at your own risk.",
|
||||
disclaimerModalAccept: "I Understand - Continue to App",
|
||||
disclaimerModalFooterLink: "Medical Disclaimer & Data Sources",
|
||||
// Units
|
||||
unitMg: "mg",
|
||||
unitNgml: "ng/ml",
|
||||
@@ -97,16 +155,25 @@ export const en = {
|
||||
// Number input field
|
||||
buttonClear: "Clear field",
|
||||
|
||||
// Field validation
|
||||
errorNumberRequired: "Please enter a valid number.",
|
||||
errorTimeRequired: "Please enter a valid time.",
|
||||
warningDuplicateTime: "Multiple doses at same time.",
|
||||
warningZeroDose: "Zero dose has no effect on simulation.",
|
||||
halfLifeRequired: "Half-life is required.",
|
||||
conversionHalfLifeRequired: "Conversion half-life is required.",
|
||||
absorptionRateRequired: "Absorption rate is required.",
|
||||
therapeuticRangeMinRequired: "Minimum therapeutic range is required.",
|
||||
therapeuticRangeMaxRequired: "Maximum therapeutic range is required.",
|
||||
// Field validation - Errors
|
||||
errorNumberRequired: "⛔ Please enter a valid number.",
|
||||
errorTimeRequired: "⛔ Please enter a valid time.",
|
||||
|
||||
errorHalfLifeRequired: "⛔ Half-life is required.",
|
||||
errorConversionHalfLifeRequired: "⛔ Conversion half-life is required.",
|
||||
errorAbsorptionRateRequired: "⛔ Absorption rate is required.",
|
||||
errorTherapeuticRangeMinRequired: "⛔ Minimum therapeutic range is required.",
|
||||
errorTherapeuticRangeMaxRequired: "⛔ Maximum therapeutic range is required.",
|
||||
errorEliminationHalfLifeRequired: "⛔ Elimination half-life is required.",
|
||||
|
||||
|
||||
// Field validation - Warnings
|
||||
warningDuplicateTime: "⚠️ Multiple doses at same time.",
|
||||
warningZeroDose: "⚠️ Zero dose has no effect on simulation.",
|
||||
warningAbsorptionOutOfRange: "⚠️ Typical range: 0.7-1.2h. Current value may be outside clinical norms.",
|
||||
warningConversionOutOfRange: "⚠️ Typical range: 0.7-1.2h. Current value may be outside clinical norms.",
|
||||
warningEliminationOutOfRange: "⚠️ Typical range: 9-12h (normal pH). Extended range 7-15h (pH effects). Current value is unusual.",
|
||||
warningDoseAbove70mg: "⚠️ FDA-approved maximum: 70 mg. Higher doses lack safety data and increase cardiovascular risk.",
|
||||
|
||||
// Time picker
|
||||
timePickerHour: "Hour",
|
||||
|
||||
@@ -28,7 +28,12 @@ export const calculateCombinedProfile = (
|
||||
const timeStepHours = 0.25;
|
||||
const totalDays = days.length;
|
||||
const totalHours = totalDays * 24;
|
||||
const daysToSimulate = Math.min(parseInt(steadyStateConfig.daysOnMedication, 10) || 0, 5);
|
||||
|
||||
// Use steadyStateDays from advanced settings (allows 0 for "first day" simulation)
|
||||
const daysToSimulate = Math.min(
|
||||
parseInt(pkParams.advanced.steadyStateDays, 10) || 0,
|
||||
7 // cap at 7 days for performance
|
||||
);
|
||||
|
||||
// Convert days to processed doses with absolute time
|
||||
const allDoses: ProcessedDose[] = [];
|
||||
@@ -36,7 +41,7 @@ export const calculateCombinedProfile = (
|
||||
// Add steady-state doses (days before simulation period)
|
||||
// Use template day (first day) for steady state
|
||||
const templateDay = days[0];
|
||||
if (templateDay) {
|
||||
if (templateDay && daysToSimulate > 0) {
|
||||
for (let steadyDay = -daysToSimulate; steadyDay < 0; steadyDay++) {
|
||||
const dayOffsetMinutes = steadyDay * 24 * 60;
|
||||
templateDay.doses.forEach(dose => {
|
||||
|
||||
@@ -3,19 +3,22 @@
|
||||
*
|
||||
* Implements single-dose concentration calculations for lisdexamfetamine (LDX)
|
||||
* and its active metabolite dextroamphetamine (d-amph). Uses first-order
|
||||
* absorption and elimination kinetics.
|
||||
* absorption and elimination kinetics with optional advanced modifiers.
|
||||
*
|
||||
* @author Andreas Weyer
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { LDX_TO_DAMPH_CONVERSION_FACTOR, type PkParams } from '../constants/defaults';
|
||||
import { LDX_TO_DAMPH_SALT_FACTOR, DEFAULT_F_ORAL, type PkParams } from '../constants/defaults';
|
||||
|
||||
interface ConcentrationResult {
|
||||
ldx: number;
|
||||
damph: number;
|
||||
}
|
||||
|
||||
// Standard adult volume of distribution (Roberts et al. 2015): 377 L
|
||||
const STANDARD_VD_ADULT = 377.0;
|
||||
|
||||
// Pharmacokinetic calculations
|
||||
export const calculateSingleDoseConcentration = (
|
||||
dose: string,
|
||||
@@ -25,27 +28,61 @@ export const calculateSingleDoseConcentration = (
|
||||
const numDose = parseFloat(dose) || 0;
|
||||
if (timeSinceDoseHours < 0 || numDose <= 0) return { ldx: 0, damph: 0 };
|
||||
|
||||
const absorptionRate = parseFloat(pkParams.ldx.absorptionRate);
|
||||
// Extract base parameters
|
||||
const absorptionHalfLife = parseFloat(pkParams.ldx.absorptionHalfLife);
|
||||
const conversionHalfLife = parseFloat(pkParams.ldx.halfLife);
|
||||
const damphHalfLife = parseFloat(pkParams.damph.halfLife);
|
||||
|
||||
// Validate parameters to avoid division by zero or invalid calculations
|
||||
if (isNaN(absorptionRate) || absorptionRate <= 0 ||
|
||||
// Extract advanced parameters
|
||||
const fOral = parseFloat(pkParams.advanced.fOral) || DEFAULT_F_ORAL;
|
||||
const foodEnabled = pkParams.advanced.foodEffect.enabled;
|
||||
const tmaxDelay = foodEnabled ? parseFloat(pkParams.advanced.foodEffect.tmaxDelay) : 0;
|
||||
const urinePHEnabled = pkParams.advanced.urinePh.enabled;
|
||||
const phTendency = urinePHEnabled ? parseFloat(pkParams.advanced.urinePh.phTendency) : 6.0;
|
||||
|
||||
// Validate base parameters
|
||||
if (isNaN(absorptionHalfLife) || absorptionHalfLife <= 0 ||
|
||||
isNaN(conversionHalfLife) || conversionHalfLife <= 0 ||
|
||||
isNaN(damphHalfLife) || damphHalfLife <= 0) {
|
||||
return { ldx: 0, damph: 0 };
|
||||
}
|
||||
|
||||
const ka_ldx = Math.log(2) / absorptionRate;
|
||||
const k_conv = Math.log(2) / conversionHalfLife;
|
||||
const ke_damph = Math.log(2) / damphHalfLife;
|
||||
// Apply food effect: high-fat meal delays absorption by slowing rate (~+1h to Tmax)
|
||||
// Approximate by increasing absorption half-life proportionally
|
||||
const adjustedAbsorptionHL = absorptionHalfLife * (1 + (tmaxDelay / 1.5));
|
||||
|
||||
// Apply urine pH effect on elimination half-life
|
||||
// pH < 6: acidic (faster elimination, HL ~7-9h)
|
||||
// pH 6-7: normal (HL ~10-12h)
|
||||
// pH > 7: alkaline (slower elimination, HL ~13-15h up to 34h extreme)
|
||||
let adjustedDamphHL = damphHalfLife;
|
||||
if (urinePHEnabled) {
|
||||
if (phTendency < 6.0) {
|
||||
// Acidic: reduce HL by ~30%
|
||||
adjustedDamphHL = damphHalfLife * 0.7;
|
||||
} else if (phTendency > 7.5) {
|
||||
// Alkaline: increase HL by ~30-40%
|
||||
adjustedDamphHL = damphHalfLife * 1.35;
|
||||
}
|
||||
// else: normal pH 6-7.5, no adjustment
|
||||
}
|
||||
|
||||
// Calculate rate constants
|
||||
const ka_ldx = Math.log(2) / adjustedAbsorptionHL;
|
||||
const k_conv = Math.log(2) / conversionHalfLife;
|
||||
const ke_damph = Math.log(2) / adjustedDamphHL;
|
||||
|
||||
// Apply stoichiometric conversion and bioavailability
|
||||
const effectiveDose = numDose * LDX_TO_DAMPH_SALT_FACTOR * fOral;
|
||||
|
||||
// Calculate LDX concentration (prodrug)
|
||||
let ldxConcentration = 0;
|
||||
if (Math.abs(ka_ldx - k_conv) > 0.0001) {
|
||||
ldxConcentration = (numDose * ka_ldx / (ka_ldx - k_conv)) *
|
||||
(Math.exp(-k_conv * timeSinceDoseHours) - Math.exp(-ka_ldx * timeSinceDoseHours));
|
||||
}
|
||||
|
||||
// Calculate d-amphetamine concentration (active metabolite)
|
||||
let damphConcentration = 0;
|
||||
if (Math.abs(ka_ldx - ke_damph) > 0.0001 &&
|
||||
Math.abs(k_conv - ke_damph) > 0.0001 &&
|
||||
@@ -53,7 +90,20 @@ export const calculateSingleDoseConcentration = (
|
||||
const term1 = Math.exp(-ke_damph * timeSinceDoseHours) / ((ka_ldx - ke_damph) * (k_conv - ke_damph));
|
||||
const term2 = Math.exp(-k_conv * timeSinceDoseHours) / ((ka_ldx - k_conv) * (ke_damph - k_conv));
|
||||
const term3 = Math.exp(-ka_ldx * timeSinceDoseHours) / ((k_conv - ka_ldx) * (ke_damph - ka_ldx));
|
||||
damphConcentration = LDX_TO_DAMPH_CONVERSION_FACTOR * numDose * ka_ldx * k_conv * (term1 + term2 + term3);
|
||||
damphConcentration = effectiveDose * ka_ldx * k_conv * (term1 + term2 + term3);
|
||||
}
|
||||
|
||||
// Apply weight-based Vd scaling if enabled
|
||||
// Standard adult Vd = 377 L; weight-normalized ~5.4 L/kg
|
||||
// Concentration inversely proportional to Vd: C = Amount / Vd
|
||||
if (pkParams.advanced.weightBasedVd.enabled) {
|
||||
const bodyWeight = parseFloat(pkParams.advanced.weightBasedVd.bodyWeight);
|
||||
if (!isNaN(bodyWeight) && bodyWeight > 0) {
|
||||
const weightBasedVd = bodyWeight * 5.4; // L/kg factor from literature
|
||||
const scalingFactor = STANDARD_VD_ADULT / weightBasedVd;
|
||||
damphConcentration *= scalingFactor;
|
||||
ldxConcentration *= scalingFactor;
|
||||
}
|
||||
}
|
||||
|
||||
return { ldx: Math.max(0, ldxConcentration), damph: Math.max(0, damphConcentration) };
|
||||
|
||||
Reference in New Issue
Block a user