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 (
)
}
// ── 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 ────────────────────────────────────────────────── */}
{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 &&
}
)
}