/**
* ComparisonMode — Feature 3.6
*
* Side-by-side comparison of two prediction scenarios (Before/After).
* Users pick two mock patients (or two sets of parameters) and the component
* runs both predictions and highlights the differences.
*
* Works with Engineering Mode style (no auth required) so it uses the
* public /predict endpoint via the existing api utility.
*/
import { useState, useCallback } from 'react'
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Cell, ResponsiveContainer,
} from 'recharts'
import {
ArrowLeftRight, TrendingUp, TrendingDown, Minus, Loader2, AlertCircle,
ChevronDown, CheckCircle2, XCircle,
} from 'lucide-react'
import { api } from '../api'
import { useDisease } from '../context/DiseaseContext'
import { useDiseaseSchema } from '../hooks/useDiseaseSchema'
import { getPatientsForDisease } from '../mockPatients'
// ── Helpers ───────────────────────────────────────────────────────────────────
function riskScore(result) {
if (!result) return null
return result.prediction === 1 ? result.confidence : 1 - result.confidence
}
function DeltaBadge({ before, after }) {
if (before == null || after == null) return null
const delta = after - before
const pct = Math.round(Math.abs(delta) * 100)
if (pct === 0) return No change
const improving = delta < 0
return (
{improving ? : }
{improving ? '↓' : '↑'}{pct}% risk
)
}
// ── Patient picker ────────────────────────────────────────────────────────────
function PatientPicker({ label, patients, selected, onSelect, color }) {
const [open, setOpen] = useState(false)
return (
{label}
{open && (
<>
setOpen(false)} />
{patients.map(p => (
))}
>
)}
)
}
// ── Result column ─────────────────────────────────────────────────────────────
function ResultColumn({ label, result, loading, error, color, colorLight }) {
if (loading) return (
)
if (error) return (
)
if (!result) return (
No result yet
)
const isPositive = result.prediction === 1
const conf = Math.round((result.confidence ?? 0) * 100)
return (
{/* Badge */}
{isPositive
?
: }
{result.diagnosis ?? (isPositive ? 'Positive' : 'Negative')}
Confidence
{conf}%
{/* Risk score */}
Risk Score
{Math.round(riskScore(result) * 100)}%
)
}
// ── Feature diff table ────────────────────────────────────────────────────────
function FeatureDiffTable({ beforePatient, afterPatient, fields }) {
if (!beforePatient || !afterPatient) return null
const b = beforePatient.data || {}
const a = afterPatient.data || {}
const diffs = fields
.filter(f => String(b[f.name]) !== String(a[f.name]))
.map(f => ({ name: f.name, label: f.label || f.name, before: b[f.name], after: a[f.name] }))
if (diffs.length === 0) {
return (
Both patients have identical feature values.
)
}
return (
| Feature |
Before |
After |
Change |
{diffs.map(d => {
const numBefore = parseFloat(d.before)
const numAfter = parseFloat(d.after)
const isNumeric = !isNaN(numBefore) && !isNaN(numAfter)
const delta = isNumeric ? numAfter - numBefore : null
return (
| {d.label} |
{String(d.before ?? '—')} |
{String(d.after ?? '—')} |
{delta != null ? (
0 ? 'text-red-500' : delta < 0 ? 'text-green-600' : 'text-gray-400'}`}>
{delta > 0 ? `+${delta.toFixed(1)}` : delta < 0 ? delta.toFixed(1) : '='}
) : (
{d.before} → {d.after}
)}
|
)
})}
)
}
// ── Side-by-side confidence chart ─────────────────────────────────────────────
function ComparisonChart({ beforeResult, afterResult, beforeName, afterName }) {
if (!beforeResult || !afterResult) return null
const data = [
{
name: 'Risk Score',
Before: Math.round(riskScore(beforeResult) * 100),
After: Math.round(riskScore(afterResult) * 100),
},
{
name: 'Confidence',
Before: Math.round((beforeResult.confidence ?? 0) * 100),
After: Math.round((afterResult.confidence ?? 0) * 100),
},
]
return (
Side-by-Side Metrics
`${v}%`} tick={{ fontSize: 10 }} />
[`${v}%`]} />
)
}
// ── Main ComparisonMode ───────────────────────────────────────────────────────
export default function ComparisonMode() {
const { selectedDisease } = useDisease()
const { fields } = useDiseaseSchema(selectedDisease)
const patients = getPatientsForDisease(selectedDisease)
const [beforePatient, setBeforePatient] = useState(patients[0] ?? null)
const [afterPatient, setAfterPatient] = useState(patients[1] ?? patients[0] ?? null)
const [beforeResult, setBeforeResult] = useState(null)
const [afterResult, setAfterResult] = useState(null)
const [beforeLoading, setBeforeLoading] = useState(false)
const [afterLoading, setAfterLoading] = useState(false)
const [beforeError, setBeforeError] = useState(null)
const [afterError, setAfterError] = useState(null)
const [ran, setRan] = useState(false)
const runComparison = useCallback(async () => {
if (!beforePatient || !afterPatient || !selectedDisease) return
setRan(true)
setBeforeResult(null)
setAfterResult(null)
setBeforeError(null)
setAfterError(null)
setBeforeLoading(true)
setAfterLoading(true)
const [bRes, aRes] = await Promise.allSettled([
api.predict(selectedDisease, beforePatient.data),
api.predict(selectedDisease, afterPatient.data),
])
setBeforeLoading(false)
setAfterLoading(false)
if (bRes.status === 'fulfilled') setBeforeResult(bRes.value)
else setBeforeError(bRes.reason?.message ?? 'Prediction failed')
if (aRes.status === 'fulfilled') setAfterResult(aRes.value)
else setAfterError(aRes.reason?.message ?? 'Prediction failed')
}, [beforePatient, afterPatient, selectedDisease])
const riskBefore = riskScore(beforeResult)
const riskAfter = riskScore(afterResult)
return (
{/* Header */}
Before / After Comparison
Compare two patient profiles side by side to evaluate risk changes
{/* Patient selectors + compare button */}
{ setBeforePatient(p); setRan(false) }}
color="text-blue-600"
/>
{ setAfterPatient(p); setRan(false) }}
color="text-violet-600"
/>
{/* Results panel */}
{ran && (
<>
{/* Side-by-side result columns */}
Prediction Results
{riskBefore != null && riskAfter != null && (
)}
Before — {beforePatient?.name}
After — {afterPatient?.name}
{/* Chart comparison */}
{/* Feature diff table */}
>
)}
{/* Empty state */}
{!ran && (
Select two patients and click Compare to see the results.
)}
)
}