fix(frontend): block/warn direct parent-offspring selections in PairingSimulator
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { FlaskConical, AlertTriangle, CheckCircle, XCircle, GitMerge } from 'lucide-react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { FlaskConical, AlertTriangle, CheckCircle, XCircle, GitMerge, ShieldAlert } from 'lucide-react'
|
||||
|
||||
export default function PairingSimulator() {
|
||||
const [dogs, setDogs] = useState([])
|
||||
@@ -9,6 +9,8 @@ export default function PairingSimulator() {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [dogsLoading, setDogsLoading] = useState(true)
|
||||
const [relationWarning, setRelationWarning] = useState(null)
|
||||
const [relationChecking, setRelationChecking] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/dogs')
|
||||
@@ -20,6 +22,38 @@ export default function PairingSimulator() {
|
||||
.catch(() => setDogsLoading(false))
|
||||
}, [])
|
||||
|
||||
// Check for direct relation whenever both sire and dam are selected
|
||||
const checkRelation = useCallback(async (sid, did) => {
|
||||
if (!sid || !did) {
|
||||
setRelationWarning(null)
|
||||
return
|
||||
}
|
||||
setRelationChecking(true)
|
||||
try {
|
||||
const res = await fetch(`/api/pedigree/relations/${sid}/${did}`)
|
||||
const data = await res.json()
|
||||
setRelationWarning(data.related ? data.relationship : null)
|
||||
} catch {
|
||||
setRelationWarning(null)
|
||||
} finally {
|
||||
setRelationChecking(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
function handleSireChange(e) {
|
||||
const val = e.target.value
|
||||
setSireId(val)
|
||||
setResult(null)
|
||||
checkRelation(val, damId)
|
||||
}
|
||||
|
||||
function handleDamChange(e) {
|
||||
const val = e.target.value
|
||||
setDamId(val)
|
||||
setResult(null)
|
||||
checkRelation(sireId, val)
|
||||
}
|
||||
|
||||
const males = dogs.filter(d => d.sex === 'male')
|
||||
const females = dogs.filter(d => d.sex === 'female')
|
||||
|
||||
@@ -84,7 +118,7 @@ export default function PairingSimulator() {
|
||||
<label className="label">Sire (Male) ♂</label>
|
||||
<select
|
||||
value={sireId}
|
||||
onChange={e => setSireId(e.target.value)}
|
||||
onChange={handleSireChange}
|
||||
required
|
||||
disabled={dogsLoading}
|
||||
>
|
||||
@@ -104,7 +138,7 @@ export default function PairingSimulator() {
|
||||
<label className="label">Dam (Female) ♀</label>
|
||||
<select
|
||||
value={damId}
|
||||
onChange={e => setDamId(e.target.value)}
|
||||
onChange={handleDamChange}
|
||||
required
|
||||
disabled={dogsLoading}
|
||||
>
|
||||
@@ -121,10 +155,30 @@ export default function PairingSimulator() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Direct-relation warning banner */}
|
||||
{relationChecking && (
|
||||
<p style={{ fontSize: '0.8125rem', color: 'var(--text-muted)', marginBottom: '1rem' }}>Checking relationship…</p>
|
||||
)}
|
||||
{!relationChecking && relationWarning && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: '0.6rem',
|
||||
background: 'rgba(234,179,8,0.12)', border: '1px solid rgba(234,179,8,0.4)',
|
||||
borderRadius: 'var(--radius-sm)', padding: '0.75rem 1rem', marginBottom: '1rem'
|
||||
}}>
|
||||
<ShieldAlert size={18} style={{ color: '#eab308', flexShrink: 0, marginTop: '0.1rem' }} />
|
||||
<div>
|
||||
<p style={{ margin: 0, fontWeight: 600, color: '#eab308', fontSize: '0.875rem' }}>Direct Relation Detected</p>
|
||||
<p style={{ margin: '0.2rem 0 0', fontSize: '0.8125rem', color: 'var(--text-secondary)' }}>
|
||||
{relationWarning}. COI will reflect the high inbreeding coefficient for this pairing.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={!sireId || !damId || loading}
|
||||
disabled={!sireId || !damId || loading || relationChecking}
|
||||
style={{ minWidth: '160px' }}
|
||||
>
|
||||
{loading ? 'Calculating…' : <><FlaskConical size={16} /> Simulate Pairing</>}
|
||||
@@ -138,6 +192,21 @@ export default function PairingSimulator() {
|
||||
{/* Results */}
|
||||
{result && (
|
||||
<div style={{ maxWidth: '720px' }}>
|
||||
{/* Direct-relation alert in results */}
|
||||
{result.directRelation && (
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: '0.6rem',
|
||||
background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.35)',
|
||||
borderRadius: 'var(--radius-sm)', padding: '0.75rem 1rem', marginBottom: '1.25rem'
|
||||
}}>
|
||||
<ShieldAlert size={18} style={{ color: 'var(--danger)', flexShrink: 0, marginTop: '0.1rem' }} />
|
||||
<div>
|
||||
<p style={{ margin: 0, fontWeight: 600, color: 'var(--danger)', fontSize: '0.875rem' }}>Direct Relation — High Inbreeding Risk</p>
|
||||
<p style={{ margin: '0.2rem 0 0', fontSize: '0.8125rem', color: 'var(--text-secondary)' }}>{result.directRelation}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* COI Summary */}
|
||||
<div className="card" style={{ marginBottom: '1.25rem' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', flexWrap: 'wrap', gap: '1rem' }}>
|
||||
@@ -152,9 +221,7 @@ export default function PairingSimulator() {
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: '0.8125rem', textTransform: 'uppercase', letterSpacing: '0.05em', fontWeight: 500, marginBottom: '0.25rem' }}>COI</p>
|
||||
<p style={{
|
||||
fontSize: '2rem',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1,
|
||||
fontSize: '2rem', fontWeight: 700, lineHeight: 1,
|
||||
color: result.coi < 5 ? 'var(--success)' : result.coi < 10 ? 'var(--warning)' : 'var(--danger)'
|
||||
}}>
|
||||
{result.coi.toFixed(2)}%
|
||||
@@ -183,7 +250,7 @@ export default function PairingSimulator() {
|
||||
|
||||
{result.commonAncestors.length === 0 ? (
|
||||
<p style={{ color: 'var(--text-muted)', textAlign: 'center', padding: '1.5rem 0', margin: 0 }}>
|
||||
No common ancestors found within 5 generations. This pairing has excellent genetic diversity.
|
||||
No common ancestors found within 6 generations. This pairing has excellent genetic diversity.
|
||||
</p>
|
||||
) : (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
|
||||
Reference in New Issue
Block a user