import { useState, useRef } from 'react' import { BarChart, Bar, Cell, XAxis, YAxis, Tooltip, LabelList, ResponsiveContainer, ReferenceLine, } from 'recharts' import { motion } from 'framer-motion' import { Download } from 'lucide-react' import { useUsageLimiter } from '../hooks/useUsageLimiter' import AccessModal from '../components/AccessModal' import { ReportTemplate } from '../components/ReportTemplate' import DefinitionOfTerms from '../components/DefinitionOfTerms' const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' // ── Types ───────────────────────────────────────────────────────────────────── interface Ledger { intrinsic_performance_value: number category: string depreciation: number baseline_value: number external_multiplier: number hard_cap: number } interface EvalResult { ledger: Ledger nlp_results: { durability: number; recency: number; agent: number } nlp_cached: boolean nlp_found?: boolean logs: string[] shap_data: { feature: string; impact: number }[] } // ── Gauge (SVG speedometer) ──────────────────────────────────────────────────── function Gauge({ asking, hardCap }: { asking: number; hardCap: number }) { const maxVal = Math.max(asking * 1.25, hardCap * 1.25, 50) const cx = 150, cy = 138, r = 108 const toRad = (deg: number) => (deg * Math.PI) / 180 const pt = (deg: number) => ({ x: cx + r * Math.cos(toRad(deg)), y: cy + r * Math.sin(toRad(deg)), }) const frac = (v: number) => Math.min(v / maxVal, 1) const capDeg = -180 + frac(hardCap) * 180 const askDeg = -180 + frac(asking) * 180 const isOver = asking > hardCap const arc = (a1: number, a2: number) => { const s = pt(a1), e = pt(a2) const large = a2 - a1 > 180 ? 1 : 0 return `M ${s.x.toFixed(1)} ${s.y.toFixed(1)} A ${r} ${r} 0 ${large} 1 ${e.x.toFixed(1)} ${e.y.toFixed(1)}` } return ( {/* Track */} {/* Green zone: 0 → cap */} {/* Red zone: cap → asking (only if over) */} {isOver && ( )} {/* Needle */} {/* Fair Value label */} Fair Value £{hardCap.toFixed(0)}m {/* Center: asking */} £{asking.toFixed(0)}m {isOver ? `▲ £${(asking - hardCap).toFixed(1)}m OVER FAIR VALUE` : `✓ Within Fair Value`} {/* Scale ticks */} {[0, 0.25, 0.5, 0.75, 1].map(f => { const deg = -180 + f * 180 const inner = { x: cx + (r-12) * Math.cos(toRad(deg)), y: cy + (r-12) * Math.sin(toRad(deg)) } const outer = { x: cx + (r+2) * Math.cos(toRad(deg)), y: cy + (r+2) * Math.sin(toRad(deg)) } return })} ) } // ── SHAP Chart ──────────────────────────────────────────────────────────────── function ShapChart({ data }: { data: { feature: string; impact: number }[] }) { return ( [v > 0 ? `+${v.toFixed(3)}` : v.toFixed(3), 'Market Impact']} /> v > 0 ? `+${v.toFixed(2)}` : v.toFixed(2)} /> {data.map((entry, i) => ( = 0 ? 'var(--profit-color)' : 'var(--loss-color)'} fillOpacity={0.85}/> ))} ) } // ── NLP Sentiment Score ─────────────────────────────────────────────────────── function SentimentScore({ label, value, icon, found }: { label: string; value: number; icon: string, found?: boolean }) { const pct = Math.round(((value + 1) / 2) * 100) const isNeutral = value === 0 const color = value > 0.1 ? 'var(--profit-color)' : value < -0.1 ? 'var(--loss-color)' : (isNeutral && found ? 'var(--text-3)' : 'var(--accent-blue)') return (
{icon} {label} {isNeutral ? (found ? 'Neutral' : 'No Data') : (value > 0 ? '+' : '') + value.toFixed(2)}
) } // ── Custom Loaders ──────────────────────────────────────────────────────────── function AILoader() { return (
SCOUTING DATA CHANNELS...
Accessing Premier League Performance Archives
) } function ButtonPulse() { return ( ) } // ── Main Estimator Page ─────────────────────────────────────────────────────── export default function Estimator() { const { isLocked, incrementUsage } = useUsageLimiter() const reportRef = useRef(null) const [form, setForm] = useState({ selected_name: '', current_club: '', interested_club: '', contract_years: 3, age: 24, injuries_24m: 10, asking_price: 45, market_value_estimation: 40, }) const [result, setResult] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [showLog, setShowLog] = useState(false) const set = (k: string, v: string | number) => setForm(f => ({ ...f, [k]: v })) const handleSubmit = async (e?: React.FormEvent) => { if (e) e.preventDefault() // Auto-Format the Player Name so the PDF always looks professional let cleanName = form.selected_name.trim() // Apply Title Case for unrecognized players (e.g. "john doe" -> "John Doe") cleanName = cleanName.replace(/\b\w/g, c => c.toUpperCase()) if (cleanName !== form.selected_name) { setForm(prev => ({ ...prev, selected_name: cleanName })) } if (!cleanName) { setError('Player name is required.'); return } if (!incrementUsage()) return; setLoading(true); setError(null); setResult(null) try { const res = await fetch(`${API_URL}/api/evaluate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form), }) if (!res.ok) { const err = await res.json().catch(() => ({})) throw new Error(err.detail || `API error ${res.status}`) } setResult(await res.json()) } catch (e) { setError(e instanceof Error ? e.message : 'Unknown error — is the API running?') } finally { setLoading(false) } } const handleDownloadPdf = () => { // Temporarily change document title so the native Print/Save dialog // automatically uses this as the default PDF file name. const originalTitle = document.title const safeName = form.selected_name.replace(/\s+/g, '_') || 'Player' document.title = `FairValue_Report_${safeName}` // Rely on native browser print for high-fidelity, copyable vectorized PDFs window.print() // Restore the original title after the print dialog resolves setTimeout(() => { document.title = originalTitle }, 1000) } const L = result?.ledger return (
{isLocked && }
{/* Header */}
AI Valuation Engine

Strategic Transfer Estimator

Evaluate a transfer ceiling guided by ML valuations and multi-axis NLP intelligence.

{/* ── Left: Form ────────────────────────────────────────────────── */}

Player Profile

set('selected_name', e.target.value)}/> Use the Transfermarkt name for best accuracy.
set('current_club', e.target.value)}/>
set('interested_club', e.target.value)}/>

Financial Parameters

{/* Contract Years */}
set('contract_years', parseFloat(e.target.value))}/>
0.5yr7yr
{/* Age */}
set('age', parseInt(e.target.value))}/>
1640
{/* Injury days */}
set('injuries_24m', parseInt(e.target.value)||0)}/>
{/* Market value */}
set('market_value_estimation', parseFloat(e.target.value)||0)}/>
{/* Asking price */}
set('asking_price', parseFloat(e.target.value)||0)}/>
{error &&
{error}
}
{/* ── Right: Results ────────────────────────────────────────────── */}
{!result && !loading && (
📊

Results will appear here after evaluation.

)} {loading && (

Running ML inference + scraping live market signals…

Live DDGS intel can take 15–30 seconds first time.

)} {result && L && ( {/* Verdict alert */}
L.hard_cap ? 'alert-danger' : 'alert-success'}`} style={{ flex: 1, margin: 0 }}> {form.asking_price > L.hard_cap ? `⚠️ OVERPAY RISK — Asking price £${form.asking_price}m exceeds our Fair Value ceiling of £${L.hard_cap.toFixed(1)}m by £${(form.asking_price - L.hard_cap).toFixed(1)}m.` : `✅ FAIR DEAL — Asking price £${form.asking_price}m is within the £${L.hard_cap.toFixed(1)}m Fair Value. Proceed with confidence.`}
💡 In the print dialog, set Destination → Save as PDF
{/* Gauge + Ledger */}

XAI Valuation Ledger

Intrinsic Performance Value
£{L.intrinsic_performance_value.toFixed(1)}m
{L.category}
Age & Contract Impact — {L.depreciation > 0 ? '📉 Depreciation' : '📈 Appreciation'}
0 ? 'var(--loss-color)' : 'var(--profit-color)' }}> {L.depreciation > 0 ? '-' : '+'}£{Math.abs(L.depreciation).toFixed(1)}m
SHAP Calculated
ML Baseline
£{L.baseline_value.toFixed(1)}m
NLP Multiplier
1 ? 'var(--profit-color)' : 'var(--loss-color)' }}> ×{L.external_multiplier.toFixed(3)}
🎯 Fair Value Ceiling
£{L.hard_cap.toFixed(1)}m
{/* SHAP Chart */}

SHAP Deep Dive — Top 10 Valuation Drivers

Green = value driver. Red = value drag. Measured in log-price space.

{/* NLP Results */}

Live Market Intelligence

{result.nlp_cached && Cached}
{showLog && (
{result.logs.map((l, i) => (
{l}
))}
)}
)}
{/* Definition of Terms Section */}
{/* Hidden Report Template for PDF Generation */} {result && }
) }