'use client'; import { useMemo } from 'react'; /** * Soccer result card — renders an analyze/prop response with * soccer-specific visual treatment. We can't surface raw feature * values (the backend response carries only `reasoning.summary` + * `kill_conditions_triggered` per the engine1 → legacy adapter), so * we parse the summary for known soccer-signal phrases and surface * each as a colored chip above the prose. * * Free-tier responses already arrive gated (the Session 7h * `applyTierGating` redacts `reasoning` and `kill_conditions`); we * just need to detect the `tier_gated` / `locked` markers and show * an upgrade CTA over the blurred content. */ interface KillCondition { code: string; reason: string; locked?: boolean; } interface Reasoning { summary?: string; steps?: unknown; locked?: boolean; } export interface SoccerGradeResultProps { player: string; stat_type: string; line: number; direction: 'over' | 'under'; league: string; grade: string; confidence?: number; edge_pct?: number; reasoning?: Reasoning; kill_conditions_triggered?: KillCondition[]; tier_gated?: boolean; upgrade_hint?: string; onUpgradeClick?: () => void; onClose?: () => void; } type SignalTone = 'positive' | 'caution' | 'warning' | 'neutral'; interface ParsedSignal { icon: string; label: string; detail: string; tone: SignalTone; } const SIGNAL_TONE_STYLE: Record = { positive: { color: 'var(--grade-a)', bg: 'rgba(0,200,150,0.08)', border: 'rgba(0,200,150,0.40)' }, caution: { color: 'var(--grade-c, #B8BCC8)', bg: 'rgba(255,179,71,0.08)', border: 'rgba(255,179,71,0.40)' }, warning: { color: 'var(--grade-d, #ff5a5a)', bg: 'rgba(255,90,90,0.08)', border: 'rgba(255,90,90,0.40)' }, neutral: { color: 'var(--text-secondary)', bg: 'transparent', border: 'var(--border)' }, }; // Pattern-match the concrete sentences `buildSoccerReasoningLines` // emits in src/services/intelligence/analyzeViaEngine1.js. Order // matters — earlier patterns win when multiple match the same line. const SIGNAL_PATTERNS: Array<(line: string) => ParsedSignal | null> = [ (line) => { const m = line.match(/scores ([\d.]+) goals per 90 minutes/i); if (m) return { icon: '⚽', label: 'Goals / 90', detail: `${m[1]}`, tone: 'positive' }; return null; }, (line) => { const m = line.match(/Expected goals \(xG\): ([\d.]+) per 90 — (.+)/i); if (m) { const trend = m[2].toLowerCase(); const tone: SignalTone = trend.includes('regression') ? 'caution' : trend.includes('breakout') ? 'positive' : 'neutral'; return { icon: '📊', label: 'xG / 90', detail: `${m[1]} — ${m[2]}`, tone }; } return null; }, (line) => { if (/Designated penalty taker/i.test(line)) { return { icon: '🎯', label: 'Penalty Taker', detail: '+0.15 goals/90 boost', tone: 'positive' }; } return null; }, (line) => { if (/Direct free-kick specialist/i.test(line)) { return { icon: '🏹', label: 'Free-Kick Taker', detail: 'shot/goal probability boost', tone: 'positive' }; } return null; }, (line) => { if (/corner taker/i.test(line)) { return { icon: '⛳', label: 'Corner Taker', detail: 'assist probability boost', tone: 'positive' }; } return null; }, (line) => { const m = line.match(/Match at ([\d,]+)ft altitude\.\s*(.+)/i); if (m) { const isAcclimated = /acclimated host/i.test(m[2]); return { icon: '🏔️', label: 'Altitude', detail: `${m[1]}ft — ${isAcclimated ? 'host acclimated' : 'visitor risk'}`, tone: isAcclimated ? 'neutral' : 'warning', }; } return null; }, (line) => { const m = line.match(/(.+?) averages ([\d.]+) cards per match/i); if (m) { const cardsPerGame = parseFloat(m[2]); const tone: SignalTone = cardsPerGame >= 5 ? 'caution' : 'neutral'; return { icon: '🟨', label: `Referee: ${m[1].trim()}`, detail: `${m[2]} cards/match`, tone }; } return null; }, (line) => { const m = line.match(/Averaging only ([\d.]+) minutes per match/i); if (m) return { icon: '⏱️', label: 'Minutes', detail: `${m[1]}/90 — under-line discount`, tone: 'caution' }; return null; }, (line) => { const m = line.match(/(.+?) concedes ([\d.]+) goals per game/i); if (m) { const conceded = parseFloat(m[2]); const tone: SignalTone = conceded <= 0.8 ? 'warning' : conceded >= 1.6 ? 'positive' : 'neutral'; return { icon: '🛡️', label: `Defense: ${m[1].trim()}`, detail: `${m[2]} GA/match`, tone }; } return null; }, (line) => { const m = line.match(/Tournament pedigree: (\d+) career World Cup goals/i); if (m) return { icon: '🏆', label: 'WC Pedigree', detail: `${m[1]} career goals`, tone: 'positive' }; return null; }, ]; function parseSignals(summary: string | undefined): ParsedSignal[] { if (!summary) return []; const out: ParsedSignal[] = []; // The buildSoccerReasoningLines output is a single `lines.join(' ')`, // so split on period+space and trim. Some sentences contain periods // (e.g. "0.67 goals per 90"), so re-match conservatively. const fragments = summary.split(/(?<=\.)\s+(?=[A-Z⚽📊🎯🏹⛳🏔️🟨⏱️🛡️🏆])/); for (const frag of fragments) { for (const fn of SIGNAL_PATTERNS) { const sig = fn(frag); if (sig) { out.push(sig); break; } } } return out; } function gradeColor(grade: string): string { const g = (grade || '').trim().toUpperCase().charAt(0); if (g === 'A') return 'var(--grade-a)'; if (g === 'B') return 'var(--grade-b, #F0F0F0)'; if (g === 'C') return 'var(--grade-c, #B8BCC8)'; return 'var(--grade-d, #ff5a5a)'; } export default function SoccerGradeResult(props: SoccerGradeResultProps) { const { player, stat_type, line, direction, league, grade, confidence, edge_pct, reasoning, kill_conditions_triggered, tier_gated, upgrade_hint, onUpgradeClick, onClose, } = props; const signals = useMemo(() => parseSignals(reasoning?.summary), [reasoning?.summary]); const color = gradeColor(grade); const locked = !!tier_gated || !!reasoning?.locked; const kills = Array.isArray(kill_conditions_triggered) ? kill_conditions_triggered : []; return (
{onClose && ( )}
{player}
{direction.toUpperCase()} {line.toFixed(1)} {stat_type.replace(/_/g, ' ')} · {league.toUpperCase()}
{grade}
{typeof confidence === 'number' && (
{confidence.toFixed(0)}% conf {typeof edge_pct === 'number' && ( <> · {edge_pct >= 0 ? '+' : ''}{edge_pct.toFixed(1)}% edge )}
)}
{signals.length > 0 && !locked && (
{signals.map((sig, idx) => { const style = SIGNAL_TONE_STYLE[sig.tone]; return (
{sig.icon} {sig.label} {sig.detail}
); })}
)} {!locked && reasoning?.summary && (

{reasoning.summary}

)} {locked && (
⚽ Goals/90: 0.67 · 📊 xG: 0.52 — overperforming · 🏔️ altitude 7,349ft · 🟨 ref 4.7 cards/match · 🎯 penalty taker

{upgrade_hint || 'Unlock full intelligence — xG regression, altitude, referee, set-piece role.'}

)} {kills.length > 0 && (

Kill conditions ({kills.length})

    {kills.map((k, idx) => (
  • {k.code} {k.reason}
  • ))}
)}
); }