'use client'; import { useEffect, useMemo, useState } from 'react'; import { PlayerMatchupRadar } from '@/components/PlayerMatchupRadar'; import { PropValueCard } from '@/components/PropValueCard'; import { estimatePlayerPropProbability, type PlayerMetricProfile, } from '@/lib/betting-utils'; import { calculatePlayerProps, type PlayerPropsResponse, } from '@/lib/analytics-api'; type Metric = 'shots' | 'shots_on_target' | 'passes'; type RadarMetric = { 攻擊: number; 運球: number; 組織: number; 壓迫: number; 對位完成: number; 穩定度: number; }; const metricLabelMap: Record = { shots: '射門', shots_on_target: '射正', passes: '傳球', }; export default function PropsPage() { const [playerName, setPlayerName] = useState('Kylian Mbappé'); const [opponentName, setOpponentName] = useState('墨西哥'); const [metric, setMetric] = useState('shots'); const [baselineMean, setBaselineMean] = useState(3.2); const [line, setLine] = useState(1.5); const [matchMinutes, setMatchMinutes] = useState(90); const [bookmakerOdds, setBookmakerOdds] = useState(2.15); const [loading, setLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); const [analysis, setAnalysis] = useState(null); const [radarMetric] = useState({ 攻擊: 72, 運球: 68, 組織: 74, 壓迫: 61, 對位完成: 66, 穩定度: 70, }); const profile = useMemo(() => ({ playerId: playerName, metric, baselineMean, line, matchMinutes, teamAttackFactor: 1.15, opponentDefenceFactor: 0.92, weatherFatigueFactor: 1.02, }), [playerName, metric, baselineMean, line, matchMinutes]); const estimate = useMemo(() => estimatePlayerPropProbability(profile), [profile]); const fallback = useMemo( () => ({ metric, line, over_probability: estimate.overProbability, under_probability: estimate.underProbability, expected_count: estimate.expectedCount, p5: Number((estimate.expectedCount * 0.55).toFixed(2)), p50: Number((estimate.expectedCount * 1.05).toFixed(2)), p95: Number((estimate.expectedCount * 1.65).toFixed(2)), simulation_runs: 12000, edge: null, top_edge: false, bookmaker_over_odds: bookmakerOdds, implied_prob: 1 / bookmakerOdds, }) as PlayerPropsResponse, [metric, line, estimate, bookmakerOdds], ); useEffect(() => { const signal = new AbortController(); const runAnalysis = async () => { setLoading(true); setErrorMessage(''); try { const result = await calculatePlayerProps({ player_id: playerName, player_name: playerName, metric, baseline_mean: baselineMean, line, match_minutes: matchMinutes, team_attack_factor: 1.15, opponent_defence_factor: 0.92, weather_fatigue_factor: 1.02, bookmaker_over_odds: bookmakerOdds, simulations: 12000, }); if (signal.signal.aborted) { return; } setAnalysis(result); } catch (error) { if (signal.signal.aborted) { return; } setErrorMessage(error instanceof Error ? error.message : '道具盤分析暫時無法使用'); setAnalysis(fallback); } finally { if (!signal.signal.aborted) { setLoading(false); } } }; runAnalysis(); return () => { signal.abort(); }; }, [fallback, line, metric, baselineMean, bookmakerOdds, matchMinutes, playerName]); const current = analysis ?? fallback; const edgePercent = current.edge === null ? (current.over_probability - (current.implied_prob ?? 0)) * 100 : current.edge * 100; return (

球員道具盤(Player Props)專業模組

道具盤參數

道具盤輸出

預期數:{current.expected_count}

低位分位(P5):{current.p5}

中位數(P50):{current.p50}

高位分位(P95):{current.p95}

樣本模擬:{current.simulation_runs.toLocaleString()} 次

{loading ?

更新道具盤模型中…

: null} {errorMessage ?

{errorMessage}

: null}

模擬控制

); }