import { useState, useRef, useCallback, useEffect } from 'react'
import './App.css'
const API = 'http://localhost:8000'
const MODELS = ['Baseline', 'GAN', 'EBM']
const MODEL_META = {
GAN: { color: '#4f8ef7', desc: 'ConvTranspose2d Generator + Spectral Norm Discriminator' },
EBM: { color: '#a855f7', desc: 'Energy-Based Model via Langevin Dynamics (MCMC sampling)' },
}
// ── Upload Zone ───────────────────────────────────────────────────────────────
function UploadZone({ onUpload, preview }) {
const [dragging, setDragging] = useState(false)
const inputRef = useRef()
const handle = useCallback((file) => {
if (file && file.type.startsWith('image/')) onUpload(file)
}, [onUpload])
return (
{ e.preventDefault(); setDragging(true) }}
onDragLeave={() => setDragging(false)}
onDrop={e => { e.preventDefault(); setDragging(false); handle(e.dataTransfer.files[0]) }}
onClick={() => inputRef.current.click()}
>
handle(e.target.files[0])} />
{preview ? (
Click to change
) : (
<>
🫁
Drop your chest X-ray here
or click to browse — PNG, JPG, JPEG
>
)}
)
}
// ── Augmentation Grid ─────────────────────────────────────────────────────────
function AugGrid({ items, loading }) {
if (loading) return
if (!items.length) return Upload an image to see augmented versions.
return (
{items.map((item, i) => (
{item.label}
))}
)
}
// ── Model Sample Column ───────────────────────────────────────────────────────
function ModelColumn({ name, images }) {
const meta = MODEL_META[name]
return (
{name}
{meta.desc}
{images.length === 0
? [0].map(i =>
)
: images.map((img, i) => (
Sample
))
}
)
}
// ── Confidence Bar ────────────────────────────────────────────────────────────
function ConfidenceBar({ label, value, color }) {
return (
{label}
{(value * 100).toFixed(1)}%
)
}
// ── Dashboard Component ───────────────────────────────────────────────────────
function Dashboard({ navigate }) {
const [metrics, setMetrics] = useState(null)
const [samples, setSamples] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
Promise.all([
fetch(`${API}/metrics_images`).then(r => r.json()),
fetch(`${API}/dataset_samples`).then(r => r.json())
])
.then(([metricsData, samplesData]) => {
setMetrics(metricsData)
setSamples(samplesData)
setLoading(false)
})
.catch(() => setLoading(false))
}, [])
return (
{loading ? (
Loading metrics and dataset samples...
) : !metrics ? (
Failed to load metrics. Check backend.
) : (
{/* Dataset Samples Section */}
{samples && (
PneumoniaMNIST Dataset Samples
Real examples from the test set used to evaluate the models.
Normal (Healthy)
The minority class in training (Needs augmentation)
{samples.normal.map((img, i) => (
))}
Pneumonia
The majority class (Notice the cloudy opacities)
{samples.pneumonia.map((img, i) => (
))}
)}
{/* Explanation Section */}
Why did EBM outperform GAN?
1. No Mode Collapse: GANs often suffer from mode collapse, where the generator finds a few realistic examples and repeats them. This creates sharp but repetitive images, failing to capture the full natural diversity of human lungs.
2. Diverse Distribution Modeling: EBMs (via Langevin dynamics) explore the entire continuous space of the data distribution. Because they assign low "energy" to all realistic lung states, EBMs generate a massive, diverse variety of healthy lungs including subtle variations in rib cage shapes and tissue densities.
Conclusion: When the EBM-generated "Normal" images were added to the dataset, they provided the classifier with a much wider, richer spectrum of healthy lungs to learn from. This led to a more robust decision boundary, drastically reducing False Positives from 10 (Baseline) to 5 (EBM), pushing Specificity to 96.30%.
{/* Metrics Section */}
Model Metrics
{['Baseline', 'GAN', 'EBM'].map(model => (
{model} Metrics
Confusion Matrix
{metrics[model]?.cm ? (
) :
}
ROC Curve
{metrics[model]?.roc ? (
) :
}
))}
)}
)
}
// ── Main App ──────────────────────────────────────────────────────────────────
export default function App() {
const [currentRoute, setCurrentRoute] = useState(window.location.pathname)
const [file, setFile] = useState(null)
const [preview, setPreview] = useState('')
const [augments, setAugments] = useState([])
const [genData, setGenData] = useState({})
const [result, setResult] = useState(null)
const [model, setModel] = useState('Baseline')
const [loadingAug, setLoadingAug] = useState(false)
const [loadingGen, setLoadingGen] = useState(false)
const [loadingClf, setLoadingClf] = useState(false)
useEffect(() => {
const handleLocationChange = () => setCurrentRoute(window.location.pathname)
window.addEventListener('popstate', handleLocationChange)
return () => window.removeEventListener('popstate', handleLocationChange)
}, [])
const navigate = (path, e) => {
if (e) e.preventDefault()
window.history.pushState({}, '', path)
setCurrentRoute(path)
}
const handleUpload = async (f) => {
setFile(f)
setPreview(URL.createObjectURL(f))
setAugments([]); setGenData({}); setResult(null)
setLoadingAug(true)
setLoadingGen(true)
try {
const fd = new FormData(); fd.append('file', f)
const [augRes, genRes] = await Promise.all([
fetch(`${API}/augment`, { method: 'POST', body: fd }),
fetch(`${API}/generate?n=1`, { method: 'POST' })
])
const [augData, genData] = await Promise.all([augRes.json(), genRes.json()])
setAugments(augData.augmentations)
setGenData(genData)
} catch { alert('Backend not reachable. Make sure FastAPI is running on port 8000.') }
finally { setLoadingAug(false); setLoadingGen(false) }
}
const handleClassify = async () => {
if (!file) return
setLoadingClf(true); setResult(null)
try {
const fd = new FormData(); fd.append('file', file); fd.append('model_name', model)
const res = await fetch(`${API}/classify`, { method: 'POST', body: fd })
setResult(await res.json())
} catch { alert('Classification failed. Check backend.') }
finally { setLoadingClf(false) }
}
if (currentRoute === '/dashboard') {
return
}
return (
Upload Chest X-ray
Step 1
{/* Traditional Augmentations */}
Traditional Augmentations
Row 1
Standard image transforms applied to the uploaded X-ray — flips, rotations, brightness, blur, and cropping.
{/* Generative Samples — GAN & EBM */}
Generative Model Samples
Row 2 — GAN & EBM
Synthetic Normal lung images auto-generated when you upload — these are what each model added to the training set to fix class imbalance.
{loadingGen && (
Generating GAN & EBM samples…
)}
{!loadingGen && Object.keys(genData).length === 0 && (
Upload an image above to auto-generate GAN & EBM samples.
)}
{Object.keys(genData).length > 0 && (
)}
{/* Classification */}
Classification
Step 2 — Run Inference
Select a ResNet50 model trained with each augmentation strategy and classify your uploaded X-ray.
setModel(e.target.value)}>
{MODELS.map(m => {m} )}
{loadingClf ? 'Classifying…' : '🔬 Classify'}
{loadingClf && }
{result && (
{result.prediction === 'Normal' ? '✅' : '⚠️'}
Prediction
{result.prediction}
{result.model}
)}
{/* Results Summary Table */}
Model Performance Summary
PneumoniaMNIST Test Set
Model Accuracy Sensitivity Specificity AUC FP ↓ FN ↓
{[
{ name: 'Baseline', acc: 97.90, sens: 99.74, spec: 92.59, auc: 0.9983, fp: 10, fn: 1 },
{ name: 'GAN', acc: 98.47, sens: 99.74, spec: 94.81, auc: 0.9979, fp: 7, fn: 1 },
{ name: 'EBM', acc: 98.85, sens: 99.74, spec: 96.30, auc: 0.9993, fp: 5, fn: 1, best: true },
].map(r => (
{r.name !== 'Baseline' && (
)}
{r.name}
{r.best && 🏆 Best }
{r.acc.toFixed(2)}%
{r.sens.toFixed(2)}%
{r.spec.toFixed(2)}%
{r.auc.toFixed(4)}
= 10 ? 'cell-worst' : ''}>{r.fp}
= 4 ? 'cell-worst' : ''}>{r.fn}
))}
)
}