Spaces:
Sleeping
Sleeping
FairValue
feat: upgrade NLP engine with deep search, fallbacks, and neutral signal detection
778ab74 | import React, { forwardRef } from 'react' | |
| export interface ReportProps { | |
| form: { | |
| selected_name: string | |
| current_club: string | |
| interested_club: string | |
| contract_years: number | |
| age: number | |
| injuries_24m: number | |
| asking_price: number | |
| market_value_estimation: number | |
| } | |
| result: { | |
| ledger: { | |
| intrinsic_performance_value: number | |
| category: string | |
| depreciation: number | |
| baseline_value: number | |
| external_multiplier: number | |
| hard_cap: number | |
| } | |
| nlp_results?: { | |
| durability: number | |
| recency: number | |
| agent: number | |
| } | |
| nlp_found?: boolean | |
| } | |
| } | |
| export const ReportTemplate = forwardRef<HTMLDivElement, ReportProps>(({ form, result }, ref) => { | |
| const L = result.ledger | |
| const isOverpay = form.asking_price > L.hard_cap | |
| const dateStr = new Date().toLocaleString('en-US', { dateStyle: 'long', timeStyle: 'short' }) | |
| return ( | |
| <div | |
| ref={ref} | |
| className="printable-report" | |
| style={{ | |
| backgroundColor: '#ffffff', | |
| color: '#0f172a', | |
| fontFamily: "'Inter', sans-serif", | |
| boxSizing: 'border-box', | |
| display: 'none', // Hidden on screen, shown in print via CSS | |
| }} | |
| > | |
| {/* HEADER */} | |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', borderBottom: '2px solid #e2e8f0', paddingBottom: '24px', marginBottom: '32px' }}> | |
| <div> | |
| <h1 style={{ fontSize: '32px', fontWeight: 800, margin: 0, color: '#020617', letterSpacing: '-0.03em' }}> | |
| FairValue Strategic Report | |
| </h1> | |
| <p style={{ fontSize: '14px', color: '#64748b', marginTop: '4px' }}> | |
| AI-Driven Transfer Valuation Intelligence | |
| </p> | |
| </div> | |
| <div style={{ textAlign: 'right', fontSize: '12px', color: '#475569', lineHeight: 1.6 }}> | |
| <div style={{ fontWeight: 600, color: '#0f172a' }}>Lawrence Oladeji</div> | |
| <div>oladeji.lawrence@gmail.com</div> | |
| <div><a href="https://wa.me/2349038819790" style={{ color: '#0f172a', textDecoration: 'none' }}>WhatsApp</a></div> | |
| <div><a href="https://premiership-player-fair-value.vercel.app/" style={{ color: '#2563eb', textDecoration: 'none' }}>Website</a></div> | |
| <div style={{ marginTop: '8px', color: '#94a3b8' }}>Generated: {dateStr}</div> | |
| </div> | |
| </div> | |
| {/* PLAYER PROFILE */} | |
| <div style={{ marginBottom: '32px', pageBreakInside: 'avoid', breakInside: 'avoid' }}> | |
| <h2 style={{ fontSize: '18px', fontWeight: 700, color: '#334155', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '16px' }}> | |
| Player Profile & Parameters | |
| </h2> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', background: '#f8fafc', padding: '24px', borderRadius: '12px', border: '1px solid #e2e8f0' }}> | |
| <div> | |
| <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Target Player</div> | |
| <div style={{ fontSize: '20px', fontWeight: 700, color: '#0f172a' }}>{form.selected_name || 'N/A'}</div> | |
| </div> | |
| <div> | |
| <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Transfer Direction</div> | |
| <div style={{ fontSize: '16px', fontWeight: 600, color: '#0f172a' }}> | |
| {form.current_club || 'Unknown'} → {form.interested_club || 'Unknown'} | |
| </div> | |
| </div> | |
| <div> | |
| <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Age</div> | |
| <div style={{ fontSize: '16px', fontWeight: 600, color: '#0f172a' }}>{form.age} years</div> | |
| </div> | |
| <div> | |
| <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Contract Remaining</div> | |
| <div style={{ fontSize: '16px', fontWeight: 600, color: '#0f172a' }}>{form.contract_years} years</div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* FINANCIAL VALUATION LEDGER */} | |
| <div style={{ marginBottom: '32px', pageBreakInside: 'avoid', breakInside: 'avoid' }}> | |
| <h2 style={{ fontSize: '18px', fontWeight: 700, color: '#334155', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '16px' }}> | |
| AI Valuation Ledger | |
| </h2> | |
| <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '14px' }}> | |
| <colgroup> | |
| <col style={{ width: '70%' }} /> | |
| <col style={{ width: '30%' }} /> | |
| </colgroup> | |
| <tbody> | |
| <tr style={{ borderBottom: '1px solid #e2e8f0' }}> | |
| <td style={{ padding: '12px 0', color: '#475569', fontWeight: 500 }}>Intrinsic Performance Value</td> | |
| <td style={{ padding: '12px 0', textAlign: 'right', fontWeight: 700, color: '#0f172a' }}>£{L.intrinsic_performance_value.toFixed(1)}m</td> | |
| </tr> | |
| <tr style={{ borderBottom: '1px solid #e2e8f0' }}> | |
| <td style={{ padding: '12px 0', color: '#475569', fontWeight: 500 }}> | |
| Age & Contract Impact (SHAP) — {L.depreciation > 0 ? '📉 Depreciation' : '📈 Appreciation'} | |
| {L.depreciation > 0 && form.age <= 23 && ( | |
| <div style={{ fontSize: '11px', color: '#94a3b8', marginTop: '2px' }}> | |
| * Model reflects current output, not future potential | |
| </div> | |
| )} | |
| </td> | |
| <td style={{ padding: '12px 0', textAlign: 'right', fontWeight: 700, color: L.depreciation > 0 ? '#ef4444' : '#22c55e' }}> | |
| {L.depreciation > 0 ? '-' : '+'}£{Math.abs(L.depreciation).toFixed(1)}m | |
| </td> | |
| </tr> | |
| <tr style={{ borderBottom: '1px solid #e2e8f0' }}> | |
| <td style={{ padding: '12px 0', color: '#475569', fontWeight: 500 }}>ML Baseline Value</td> | |
| <td style={{ padding: '12px 0', textAlign: 'right', fontWeight: 700, color: '#0f172a' }}>£{L.baseline_value.toFixed(1)}m</td> | |
| </tr> | |
| <tr style={{ borderBottom: '1px solid #e2e8f0' }}> | |
| <td style={{ padding: '12px 0', color: '#475569', fontWeight: 500 }}>Live NLP Market Multiplier</td> | |
| <td style={{ padding: '12px 0', textAlign: 'right', fontWeight: 700, color: L.external_multiplier > 1 ? '#22c55e' : '#ef4444' }}> | |
| ×{L.external_multiplier.toFixed(3)} | |
| </td> | |
| </tr> | |
| <tr style={{ backgroundColor: '#f0fdf4', border: '1px solid #bbf7d0' }}> | |
| <td style={{ padding: '16px 12px', color: '#166534', fontWeight: 700, fontSize: '16px' }}>Calculated Fair Value (Ceiling)</td> | |
| <td style={{ padding: '16px 12px', textAlign: 'right', fontWeight: 800, fontSize: '20px', color: '#16a34a' }}> | |
| £{L.hard_cap.toFixed(1)}m | |
| </td> | |
| </tr> | |
| <tr style={{ backgroundColor: '#fefce8', border: '1px solid #fde68a' }}> | |
| <td style={{ padding: '12px 12px', color: '#92400e', fontWeight: 600, fontSize: '13px' }}> | |
| 🎯 Recommended Opening Bid | |
| </td> | |
| <td style={{ padding: '12px 12px', textAlign: 'right', fontWeight: 700, fontSize: '15px', color: '#b45309' }}> | |
| £{(L.hard_cap * 0.85).toFixed(1)}m | |
| </td> | |
| </tr> | |
| <tr style={{ backgroundColor: '#fff7ed', border: '1px solid #fed7aa' }}> | |
| <td style={{ padding: '12px 12px', color: '#7c2d12', fontWeight: 600, fontSize: '13px' }}> | |
| ⚠️ Walk-Away Ceiling | |
| </td> | |
| <td style={{ padding: '12px 12px', textAlign: 'right', fontWeight: 700, fontSize: '15px', color: '#dc2626' }}> | |
| £{L.hard_cap.toFixed(1)}m | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| {/* NLP INTELLIGENCE BREAKDOWN */} | |
| {result.nlp_results && ( | |
| <div style={{ marginBottom: '32px', pageBreakInside: 'avoid', breakInside: 'avoid' }}> | |
| <h2 style={{ fontSize: '18px', fontWeight: 700, color: '#334155', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '16px' }}> | |
| Live Market Intelligence (NLP Breakdown) | |
| </h2> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '16px', background: '#f8fafc', padding: '24px', borderRadius: '12px', border: '1px solid #e2e8f0' }}> | |
| <div> | |
| <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Recent Form & Impact</div> | |
| <div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.recency ?? 0) === 0 ? (result.nlp_found ? '#475569' : '#94a3b8') : (result.nlp_results?.recency ?? 0) < 0 ? '#ef4444' : '#22c55e' }}> | |
| {(result.nlp_results?.recency ?? 0) === 0 ? (result.nlp_found ? 'Neutral / Factual' : 'No signals detected') : `${(result.nlp_results?.recency ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.recency ?? 0).toFixed(2)}`} | |
| </div> | |
| </div> | |
| <div> | |
| <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Injury / Availability</div> | |
| <div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.durability ?? 0) === 0 ? (result.nlp_found ? '#475569' : '#94a3b8') : (result.nlp_results?.durability ?? 0) < 0 ? '#ef4444' : '#0f172a' }}> | |
| {(result.nlp_results?.durability ?? 0) === 0 ? (result.nlp_found ? 'Neutral / Factual' : 'No signals detected') : `${(result.nlp_results?.durability ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.durability ?? 0).toFixed(2)}`} | |
| </div> | |
| </div> | |
| <div> | |
| <div style={{ fontSize: '11px', color: '#64748b', textTransform: 'uppercase' }}>Transfer Speculation</div> | |
| <div style={{ fontSize: '18px', fontWeight: 700, color: (result.nlp_results?.agent ?? 0) === 0 ? (result.nlp_found ? '#475569' : '#94a3b8') : (result.nlp_results?.agent ?? 0) < 0 ? '#ef4444' : '#22c55e' }}> | |
| {(result.nlp_results?.agent ?? 0) === 0 ? (result.nlp_found ? 'Neutral / Factual' : 'No signals detected') : `${(result.nlp_results?.agent ?? 0) > 0 ? '+' : ''}${(result.nlp_results?.agent ?? 0).toFixed(2)}`} | |
| </div> | |
| </div> | |
| </div> | |
| <div style={{ fontSize: '13px', color: '#64748b', marginTop: '12px', lineHeight: 1.5 }}> | |
| <strong>How this works:</strong> The Live NLP Market Multiplier (currently <strong>×{L.external_multiplier.toFixed(3)}</strong>) is calculated from the sentiment scores above. A multiplier of exactly 1.000 means perfectly neutral market hype. A score above 1.0 indicates high demand and positive news, allowing the selling club to charge a premium. A score below 1.0 indicates negative press (e.g. poor form or injury history), creating leverage to negotiate a discount. | |
| </div> | |
| </div> | |
| )} | |
| {/* VERDICT & SUMMARY */} | |
| <div style={{ marginBottom: '40px', pageBreakBefore: 'always', breakBefore: 'page' }}> | |
| <h2 style={{ fontSize: '18px', fontWeight: 700, color: '#334155', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '16px' }}> | |
| Executive Summary & Verdict | |
| </h2> | |
| <div style={{ padding: '24px', background: isOverpay ? '#fef2f2' : '#f0fdf4', border: `1px solid ${isOverpay ? '#fecaca' : '#bbf7d0'}`, borderRadius: '12px' }}> | |
| <p style={{ fontSize: '15px', color: isOverpay ? '#991b1b' : '#166534', lineHeight: 1.6, marginBottom: '16px' }}> | |
| <strong>Negotiation Intel:</strong> Based on the real-time evaluation engine, {form.selected_name}'s intrinsic value sits at £{L.intrinsic_performance_value.toFixed(1)}m. | |
| Age & contract dynamics apply a {L.depreciation > 0 ? `depreciation of -£${Math.abs(L.depreciation).toFixed(1)}m` : `appreciation of +£${Math.abs(L.depreciation).toFixed(1)}m`}, | |
| with live NLP market sentiment at ×{L.external_multiplier.toFixed(3)}. The absolute financial ceiling (Fair Value) is <strong>£{L.hard_cap.toFixed(1)}m</strong>. | |
| <br/><br/> | |
| <strong>Validation:</strong> {(result.nlp_results?.recency ?? 0) > 0 ? "Player's recent form commands a market premium." : "Recent form is suboptimal — leverage this to negotiate a lower fee."} {L.depreciation > 0 ? `Age & contract profile adds £${Math.abs(L.depreciation).toFixed(1)}m in depreciation risk — use this as a negotiation lever.` : `Young age and contract security add £${Math.abs(L.depreciation).toFixed(1)}m in appreciation value, justifying a strong offer.`} | |
| {L.depreciation > 0 && form.age <= 23 && ( | |
| <span style={{ display: 'block', marginTop: '8px', fontSize: '12px', color: '#64748b' }}> | |
| ℹ️ Note: Depreciation reflects the player's current statistical output stage vs. their peak. Young players may command a future-value premium not captured in this model. | |
| </span> | |
| )} | |
| </p> | |
| <div style={{ fontSize: '18px', fontWeight: 800, color: isOverpay ? '#dc2626' : '#16a34a', borderTop: `1px solid ${isOverpay ? '#fca5a5' : '#86efac'}`, paddingTop: '16px' }}> | |
| VERDICT: {isOverpay ? 'OVERPAY RISK' : 'FAIR DEAL - PROCEED WITH CONFIDENCE'} | |
| </div> | |
| <div style={{ fontSize: '14px', color: isOverpay ? '#b91c1c' : '#15803d', marginTop: '6px' }}> | |
| The selling club's asking price of £{form.asking_price}m {isOverpay ? 'exceeds' : 'is safely within'} our calculated Fair Value. | |
| </div> | |
| </div> | |
| </div> | |
| {/* DEFINITION OF TERMS */} | |
| <div style={{ borderTop: '2px solid #e2e8f0', paddingTop: '24px', pageBreakInside: 'avoid', breakInside: 'avoid' }}> | |
| <h2 style={{ fontSize: '14px', fontWeight: 700, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '12px' }}> | |
| Definition of Terms | |
| </h2> | |
| <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', fontSize: '12px', color: '#475569', lineHeight: 1.5 }}> | |
| <div> | |
| <strong style={{ color: '#0f172a' }}>Intrinsic Performance Value:</strong> Player's raw talent value derived purely from on-pitch statistics. | |
| </div> | |
| <div> | |
| <strong style={{ color: '#0f172a' }}>Age & Contract Impact:</strong> Financial adjustment based on age and remaining contract years. | |
| </div> | |
| <div> | |
| <strong style={{ color: '#0f172a' }}>NLP Multiplier:</strong> Live web-scraping adjustment factor. Values {'>'} 1.0 indicate positive hype (premium), while values {'<'} 1.0 indicate negative press (discount). | |
| </div> | |
| <div> | |
| <strong style={{ color: '#0f172a' }}>Fair Value:</strong> The absolute maximum recommended price (financial ceiling). | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ) | |
| }) | |