EpiADR-Net / frontend /src /components /PredictionPanel.tsx
ADjayantan
EpiADR-Net UI: Added Clinical Research Scope Banner, Export Clinical CSV Report, and UI reliability enhancements
11bf2dd
Raw
History Blame Contribute Delete
20.1 kB
import React, { useState } from 'react';
import { DrugRecord, ToxicityPredictionResult } from '../types';
import { SIDER_BENCHMARK_DRUGS } from '../utils/siderDataset';
import { globalEpiADREngine } from '../utils/epiAdrEngine';
import { MoleculeVisualizer } from './MoleculeVisualizer';
import { Activity, AlertTriangle, CheckCircle, Database, HelpCircle, Info, ShieldAlert, Sparkles, Zap } from 'lucide-react';
interface PredictionPanelProps {
useTissueConditioning: boolean;
onOpenAiReportWithData: (data: any) => void;
}
export const PredictionPanel: React.FC<PredictionPanelProps> = ({
useTissueConditioning,
onOpenAiReportWithData
}) => {
const [selectedDrugId, setSelectedDrugId] = useState<string>('doxorubicin');
const [customSmiles, setCustomSmiles] = useState<string>('');
const [compoundName, setCompoundName] = useState<string>('Doxorubicin');
// Select active drug record
const activeDrug = SIDER_BENCHMARK_DRUGS.find(d => d.id === selectedDrugId) || SIDER_BENCHMARK_DRUGS[0];
const activeSmiles = customSmiles.trim() !== '' ? customSmiles : activeDrug.smiles;
// Run prediction using EpiADR engine
const prediction: ToxicityPredictionResult = globalEpiADREngine.predictCompoundToxicity(
customSmiles.trim() !== '' ? 'Custom Molecule Candidate' : activeDrug.name,
activeSmiles,
useTissueConditioning
);
const handleSelectDrug = (drug: DrugRecord) => {
setSelectedDrugId(drug.id);
setCompoundName(drug.name);
setCustomSmiles('');
};
const getRiskBadgeColor = (level: string) => {
switch (level) {
case 'Severe': return 'bg-rose-500/20 text-rose-300 border-rose-500/50';
case 'High': return 'bg-amber-500/20 text-amber-300 border-amber-500/50';
case 'Moderate': return 'bg-yellow-500/20 text-yellow-300 border-yellow-500/50';
default: return 'bg-emerald-500/20 text-emerald-300 border-emerald-500/50';
}
};
const getDomainColor = (color: string) => {
if (color === 'green') return 'bg-emerald-500/10 text-emerald-400 border-emerald-500/40';
if (color === 'yellow') return 'bg-amber-500/10 text-amber-400 border-amber-500/40';
return 'bg-rose-500/10 text-rose-400 border-rose-500/40';
};
return (
<div className="space-y-6">
{/* Medical Doctor & Biological Research Scope Banner */}
<div className="bg-gradient-to-r from-rose-950/80 via-slate-900 to-slate-950 border border-rose-900/60 rounded-2xl p-4 shadow-lg flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center space-x-3">
<div className="p-2 bg-rose-900/40 rounded-xl text-rose-300 border border-rose-700/50">
<ShieldAlert className="w-5 h-5 animate-pulse" />
</div>
<div>
<div className="flex items-center space-x-2">
<span className="text-xs font-bold text-rose-200 uppercase tracking-wider">Clinical & Biological Research Scope</span>
<span className="bg-rose-900/60 text-rose-300 text-[10px] font-extrabold px-2 py-0.5 rounded-full border border-rose-700/60">
MD / Toxicologist Access Only
</span>
</div>
<p className="text-xs text-slate-300 mt-0.5">
Computational in-silico disaggregation intended for certified Medical Doctors, Clinical Pharmacologists, and Preclinical Researchers.
</p>
</div>
</div>
<button
onClick={() => {
const csvData = [
['Compound Name', prediction.compoundName],
['SMILES', activeSmiles],
['Applicability Domain', prediction.applicabilityDomain],
['Tanimoto Score', prediction.tanimotoSimilarity],
['Hepatotoxicity (Liver)', `${(prediction.organScores.liver.meanRisk * 100).toFixed(1)}% (±${prediction.organScores.liver.uncertaintySigma})`],
['Cardiotoxicity (Heart)', `${(prediction.organScores.heart.meanRisk * 100).toFixed(1)}% (±${prediction.organScores.heart.uncertaintySigma})`],
['Nephrotoxicity (Kidney)', `${(prediction.organScores.kidney.meanRisk * 100).toFixed(1)}% (±${prediction.organScores.kidney.uncertaintySigma})`],
['Neurotoxicity (Brain)', `${(prediction.organScores.brain.meanRisk * 100).toFixed(1)}% (±${prediction.organScores.brain.uncertaintySigma})`],
['Pulmotoxicity (Lung)', `${(prediction.organScores.lung.meanRisk * 100).toFixed(1)}% (±${prediction.organScores.lung.uncertaintySigma})`]
].map(e => e.join(',')).join('\n');
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', url);
link.setAttribute('download', `EpiADR_Report_${prediction.compoundName.replace(/[^a-zA-Z0-9]/g, '_')}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
className="px-3.5 py-1.5 bg-slate-950 hover:bg-slate-800 text-rose-300 border border-rose-800/80 rounded-xl text-xs font-bold transition-all flex items-center space-x-1.5 shadow"
>
<Database className="w-3.5 h-3.5 text-rose-400" />
<span>Export Clinical CSV Report</span>
</button>
</div>
{/* Molecule Input & Preset Selector */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 shadow-lg space-y-4">
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-800 pb-3">
<div className="flex items-center space-x-2">
<Database className="w-5 h-5 text-indigo-400" />
<h2 className="text-base font-bold text-white">SIDER 4.1 Benchmark Compound Library</h2>
</div>
<span className="text-xs bg-slate-950 text-slate-400 px-3 py-1 rounded-full border border-slate-800 font-mono">
7,325 Training SMILES Records
</span>
</div>
{/* Drug Selection Chips */}
<div className="flex overflow-x-auto space-x-2 pb-1 no-scrollbar">
{SIDER_BENCHMARK_DRUGS.map((drug) => {
const isSelected = selectedDrugId === drug.id && customSmiles === '';
return (
<button
key={drug.id}
onClick={() => handleSelectDrug(drug)}
className={`whitespace-nowrap px-3 py-2 rounded-xl text-xs font-bold transition-all border ${
isSelected
? 'bg-indigo-600 text-white border-indigo-400 shadow-md shadow-indigo-600/30'
: 'bg-slate-950 border-slate-800 text-slate-300 hover:bg-slate-800'
}`}
>
{drug.name}
</button>
);
})}
</div>
{/* Custom SMILES Input */}
<div className="pt-2 border-t border-slate-800 space-y-1.5">
<label className="text-xs font-semibold text-slate-400 block">
Or Paste Custom SMILES String:
</label>
<div className="flex space-x-2">
<input
type="text"
value={customSmiles}
onChange={(e) => setCustomSmiles(e.target.value)}
placeholder="e.g. CC(=O)NC1=CC=C(O)C=C1 or C1=CC=CC=C1"
className="flex-1 bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs font-mono text-indigo-300 focus:outline-none focus:border-indigo-500"
/>
{customSmiles !== '' && (
<button
onClick={() => setCustomSmiles('')}
className="px-3 py-2 bg-slate-800 text-slate-300 rounded-xl text-xs font-bold hover:bg-slate-700"
>
Reset
</button>
)}
</div>
</div>
</div>
{/* Tanimoto Structural Applicability Domain Banner */}
<div className={`p-4 rounded-2xl border flex flex-wrap items-center justify-between gap-3 shadow-md ${getDomainColor(prediction.domainColor)}`}>
<div className="flex items-center space-x-3">
<div className="p-2 bg-slate-950/80 rounded-xl border border-slate-800">
<ShieldAlert className="w-5 h-5" />
</div>
<div>
<div className="flex items-center space-x-2">
<span className="text-xs font-bold uppercase tracking-wider">Tanimoto Applicability Domain</span>
<span className="text-xs font-extrabold px-2 py-0.5 rounded-full bg-slate-950 border border-current">
Score: {prediction.tanimotoSimilarity}
</span>
</div>
<p className="text-xs font-semibold mt-0.5">{prediction.applicabilityDomain}</p>
</div>
</div>
<div className="text-xs text-right font-mono text-slate-300">
<span className="block">Morgan 128-bit Count Similarity</span>
<span className="text-[11px] opacity-80">Reference Set: SIDER 4.1</span>
</div>
</div>
{/* Main Grid: Molecular XAI + Organ Toxicity Cards */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* Left Column: 2D Molecular Hotspot Visualizer */}
<div className="lg:col-span-5 space-y-4">
<MoleculeVisualizer
smiles={activeSmiles}
compoundName={prediction.compoundName}
hotspots={prediction.atomicHotspots}
toxicophores={prediction.toxicophores}
/>
<button
onClick={() => onOpenAiReportWithData({
compoundName: prediction.compoundName,
smiles: activeSmiles,
organScores: {
liver: prediction.organScores.liver.meanRisk,
heart: prediction.organScores.heart.meanRisk,
kidney: prediction.organScores.kidney.meanRisk,
brain: prediction.organScores.brain.meanRisk,
lung: prediction.organScores.lung.meanRisk
},
tanimoto: prediction.tanimotoSimilarity,
mcUncertainty: 0.03,
useTissueConditioning
})}
className="w-full py-3 bg-gradient-to-r from-indigo-600 via-purple-600 to-pink-600 hover:opacity-90 text-white rounded-2xl font-bold text-xs shadow-xl flex items-center justify-center space-x-2 transition-all active:scale-95"
>
<Sparkles className="w-4 h-4 animate-spin-slow" />
<span>Generate Gemini Preclinical Safety Report</span>
</button>
</div>
{/* Right Column: 5 Primary Organ Zero-Shot Risk Cards */}
<div className="lg:col-span-7 space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Activity className="w-5 h-5 text-indigo-400" />
<h3 className="text-base font-bold text-white">5 Primary Organ Zero-Shot Toxicity Scores</h3>
</div>
<span className="text-xs text-slate-400 font-mono">
Bayesian MC Dropout (N=30, μ ± σ)
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3.5">
{/* Liver Card */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 space-y-3 hover:border-slate-700 transition-all">
<div className="flex items-center justify-between">
<span className="text-xs font-extrabold text-slate-200">🫀 Hepatotoxicity (Liver)</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full border ${getRiskBadgeColor(prediction.organScores.liver.riskLevel)}`}>
{prediction.organScores.liver.riskLevel} Risk
</span>
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs font-mono">
<span className="text-slate-400">Mean Risk Score (μ):</span>
<span className="font-bold text-indigo-300">{(prediction.organScores.liver.meanRisk * 100).toFixed(1)}%</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-2 overflow-hidden border border-slate-800">
<div
className="bg-gradient-to-r from-emerald-500 via-amber-500 to-rose-500 h-full transition-all duration-500"
style={{ width: `${prediction.organScores.liver.meanRisk * 100}%` }}
></div>
</div>
<span className="text-[10px] text-slate-500 font-mono block">Uncertainty σ = ±{prediction.organScores.liver.uncertaintySigma}</span>
</div>
</div>
{/* Heart Card */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 space-y-3 hover:border-slate-700 transition-all">
<div className="flex items-center justify-between">
<span className="text-xs font-extrabold text-slate-200">❤️ Cardiotoxicity (Heart)</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full border ${getRiskBadgeColor(prediction.organScores.heart.riskLevel)}`}>
{prediction.organScores.heart.riskLevel} Risk
</span>
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs font-mono">
<span className="text-slate-400">Mean Risk Score (μ):</span>
<span className="font-bold text-indigo-300">{(prediction.organScores.heart.meanRisk * 100).toFixed(1)}%</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-2 overflow-hidden border border-slate-800">
<div
className="bg-gradient-to-r from-emerald-500 via-amber-500 to-rose-500 h-full transition-all duration-500"
style={{ width: `${prediction.organScores.heart.meanRisk * 100}%` }}
></div>
</div>
<span className="text-[10px] text-slate-500 font-mono block">Uncertainty σ = ±{prediction.organScores.heart.uncertaintySigma}</span>
</div>
</div>
{/* Kidney Card */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 space-y-3 hover:border-slate-700 transition-all">
<div className="flex items-center justify-between">
<span className="text-xs font-extrabold text-slate-200">🩺 Nephrotoxicity (Kidney)</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full border ${getRiskBadgeColor(prediction.organScores.kidney.riskLevel)}`}>
{prediction.organScores.kidney.riskLevel} Risk
</span>
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs font-mono">
<span className="text-slate-400">Mean Risk Score (μ):</span>
<span className="font-bold text-indigo-300">{(prediction.organScores.kidney.meanRisk * 100).toFixed(1)}%</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-2 overflow-hidden border border-slate-800">
<div
className="bg-gradient-to-r from-emerald-500 via-amber-500 to-rose-500 h-full transition-all duration-500"
style={{ width: `${prediction.organScores.kidney.meanRisk * 100}%` }}
></div>
</div>
<span className="text-[10px] text-slate-500 font-mono block">Uncertainty σ = ±{prediction.organScores.kidney.uncertaintySigma}</span>
</div>
</div>
{/* Brain Card */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 space-y-3 hover:border-slate-700 transition-all">
<div className="flex items-center justify-between">
<span className="text-xs font-extrabold text-slate-200">🧠 Neurotoxicity (Brain)</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full border ${getRiskBadgeColor(prediction.organScores.brain.riskLevel)}`}>
{prediction.organScores.brain.riskLevel} Risk
</span>
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs font-mono">
<span className="text-slate-400">Mean Risk Score (μ):</span>
<span className="font-bold text-indigo-300">{(prediction.organScores.brain.meanRisk * 100).toFixed(1)}%</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-2 overflow-hidden border border-slate-800">
<div
className="bg-gradient-to-r from-emerald-500 via-amber-500 to-rose-500 h-full transition-all duration-500"
style={{ width: `${prediction.organScores.brain.meanRisk * 100}%` }}
></div>
</div>
<span className="text-[10px] text-slate-500 font-mono block">Uncertainty σ = ±{prediction.organScores.brain.uncertaintySigma}</span>
</div>
</div>
{/* Lung Card (Full Span on small screens) */}
<div className="sm:col-span-2 bg-slate-900 border border-slate-800 rounded-2xl p-4 space-y-3 hover:border-slate-700 transition-all">
<div className="flex items-center justify-between">
<span className="text-xs font-extrabold text-slate-200">🫁 Pulmotoxicity (Lung)</span>
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full border ${getRiskBadgeColor(prediction.organScores.lung.riskLevel)}`}>
{prediction.organScores.lung.riskLevel} Risk
</span>
</div>
<div className="space-y-1">
<div className="flex justify-between text-xs font-mono">
<span className="text-slate-400">Mean Risk Score (μ):</span>
<span className="font-bold text-indigo-300">{(prediction.organScores.lung.meanRisk * 100).toFixed(1)}%</span>
</div>
<div className="w-full bg-slate-950 rounded-full h-2 overflow-hidden border border-slate-800">
<div
className="bg-gradient-to-r from-emerald-500 via-amber-500 to-rose-500 h-full transition-all duration-500"
style={{ width: `${prediction.organScores.lung.meanRisk * 100}%` }}
></div>
</div>
<span className="text-[10px] text-slate-500 font-mono block">Uncertainty σ = ±{prediction.organScores.lung.uncertaintySigma}</span>
</div>
</div>
</div>
{/* MedDRA 10 Clinical Classes Breakdown */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 space-y-3">
<h4 className="text-xs font-bold text-slate-300">MedDRA Clinical Side Effect Classes (10 Categories)</h4>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2 text-center text-xs font-mono">
{Object.entries(prediction.meddraScores).map(([key, val]) => (
<div key={key} className="bg-slate-950 p-2 rounded-xl border border-slate-800">
<span className="text-[10px] text-slate-400 block truncate capitalize">{key.replace('_', ' ')}</span>
<span className={`font-bold ${val > 0.6 ? 'text-rose-400' : val > 0.35 ? 'text-amber-400' : 'text-emerald-400'}`}>
{(val * 100).toFixed(0)}%
</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};