NBA_PREDICTOR / web /src /pages /Accuracy.jsx
jashdoshi77's picture
Initial commit: NBA Sage Predictor for Hugging Face Spaces (with LFS for large files)
c095e08
import { useState, useEffect } from 'react'
import { getAccuracy } from '../api'
import { IconRefresh } from '../icons'
function Accuracy() {
const [data, setData] = useState({ stats: {}, recent_predictions: [] })
const [loading, setLoading] = useState(true)
const fetchData = async () => {
setLoading(true)
try {
const result = await getAccuracy()
setData(result)
} catch (err) {
console.error('Failed to load accuracy:', err)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchData()
// Auto-refresh every 60 seconds
const interval = setInterval(fetchData, 60000)
return () => clearInterval(interval)
}, [])
if (loading) {
return (
<div className="loading">
<div className="spinner"></div>
<p className="loading-text">Loading accuracy stats...</p>
</div>
)
}
const { stats, recent_predictions } = data
const overallAccuracy = stats.overall_accuracy || 0
const byConfidence = stats.by_confidence || {}
return (
<div className="animate-fadeIn">
<div className="page-header">
<div className="flex items-center justify-between">
<div>
<h1 className="page-title">Model Accuracy</h1>
<p className="page-description">Track prediction performance and model reliability</p>
</div>
<button className="btn btn-secondary" onClick={fetchData}>
<IconRefresh className="nav-icon" />
Refresh
</button>
</div>
</div>
{/* Primary Stats Grid */}
<div className="stats-grid" style={{ marginBottom: 'var(--space-6)' }}>
<div className="stat-card">
<div className="stat-label">Total Predictions</div>
<div className="stat-value">{stats.total_predictions || 0}</div>
</div>
<div className="stat-card">
<div className="stat-label">Completed Games</div>
<div className="stat-value">{stats.completed_games || 0}</div>
</div>
<div className="stat-card">
<div className="stat-label">Correct Predictions</div>
<div className="stat-value accent">{stats.correct_predictions || 0}</div>
</div>
<div className="stat-card">
<div className="stat-label">Overall Accuracy</div>
<div className="stat-value accent" style={{ fontSize: '2.5rem' }}>
{(overallAccuracy * 100).toFixed(1)}%
</div>
</div>
</div>
{/* Detailed Metrics */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-6)', marginBottom: 'var(--space-8)' }}>
{/* Performance Metrics */}
<div className="card">
<h3 style={{ marginBottom: 'var(--space-4)' }}>Performance Metrics</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-4)' }}>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Current Streak</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: stats.streak_type === 'W' ? 'var(--accent-success)' : 'var(--accent-danger)' }}>
{stats.current_streak || 0}{stats.streak_type || ''}
</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Last 10 Games</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700' }}>
{stats.last_10_record || '0-0'}
<span style={{ fontSize: '0.875rem', color: 'var(--text-muted)', marginLeft: 'var(--space-2)' }}>
({((stats.last_10_accuracy || 0) * 100).toFixed(0)}%)
</span>
</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Pending Predictions</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--accent-warning)' }}>
{stats.pending_predictions || 0}
</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Avg Probability</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700' }}>
{((stats.avg_probability_correct || 0) * 100).toFixed(1)}%
</div>
</div>
</div>
</div>
{/* Home vs Away */}
<div className="card">
<h3 style={{ marginBottom: 'var(--space-4)' }}>Home vs Away Picks</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-4)' }}>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Home Team Picks</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--accent-primary)' }}>
{((stats.home_pick_accuracy || 0) * 100).toFixed(1)}%
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
{stats.home_picks_total || 0} picks
</div>
</div>
<div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Away Team Picks</div>
<div style={{ fontSize: '1.5rem', fontWeight: '700', color: 'var(--accent-secondary)' }}>
{((stats.away_pick_accuracy || 0) * 100).toFixed(1)}%
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
{stats.away_picks_total || 0} picks
</div>
</div>
</div>
{/* Visual bar */}
<div style={{ marginTop: 'var(--space-4)' }}>
<div style={{ display: 'flex', height: '8px', borderRadius: '4px', overflow: 'hidden' }}>
<div style={{
flex: stats.home_picks_total || 1,
background: 'var(--accent-primary)',
opacity: stats.home_pick_accuracy > stats.away_pick_accuracy ? 1 : 0.5
}}></div>
<div style={{
flex: stats.away_picks_total || 1,
background: 'var(--accent-secondary)',
opacity: stats.away_pick_accuracy > stats.home_pick_accuracy ? 1 : 0.5
}}></div>
</div>
</div>
</div>
</div>
{/* Accuracy by Confidence */}
{Object.keys(byConfidence).length > 0 && (
<div style={{ marginBottom: 'var(--space-8)' }}>
<h3 style={{ marginBottom: 'var(--space-4)' }}>Accuracy by Confidence Level</h3>
<div className="stats-grid" style={{ gridTemplateColumns: 'repeat(3, 1fr)' }}>
{['high', 'medium', 'low'].map((conf) => {
const confData = byConfidence[conf] || { accuracy: 0, correct: 0, total: 0 }
const accuracyPercent = (confData.accuracy * 100).toFixed(1)
return (
<div key={conf} className="stat-card" style={{ textAlign: 'center' }}>
<span className={`badge confidence-${conf}`} style={{ marginBottom: 'var(--space-3)' }}>
{conf.toUpperCase()}
</span>
<div className="stat-value" style={{ fontSize: '2rem' }}>{accuracyPercent}%</div>
<div className="stat-label" style={{ marginTop: 'var(--space-2)' }}>
{confData.correct}/{confData.total} correct
</div>
{/* Progress ring visual */}
<div style={{
marginTop: 'var(--space-3)',
height: '4px',
background: 'var(--bg-tertiary)',
borderRadius: '2px',
overflow: 'hidden'
}}>
<div style={{
height: '100%',
width: `${accuracyPercent}%`,
background: conf === 'high' ? 'var(--accent-success)' : conf === 'medium' ? 'var(--accent-warning)' : 'var(--accent-danger)',
transition: 'width 0.3s ease'
}}></div>
</div>
</div>
)
})}
</div>
</div>
)}
{/* Accuracy by Team */}
{stats.by_team && Object.keys(stats.by_team).length > 0 && (
<div style={{ marginBottom: 'var(--space-8)' }}>
<h3 style={{ marginBottom: 'var(--space-4)' }}>Accuracy by Team Predicted</h3>
<div className="table-container">
<table className="data-table">
<thead>
<tr>
<th>Team</th>
<th style={{ textAlign: 'center' }}>Correct</th>
<th style={{ textAlign: 'center' }}>Total</th>
<th style={{ textAlign: 'right' }}>Accuracy</th>
</tr>
</thead>
<tbody>
{Object.entries(stats.by_team)
.sort((a, b) => b[1].accuracy - a[1].accuracy)
.map(([team, data]) => (
<tr key={team}>
<td style={{ fontWeight: '500' }}>{team}</td>
<td style={{ textAlign: 'center' }}>{data.correct}</td>
<td style={{ textAlign: 'center', color: 'var(--text-muted)' }}>{data.total}</td>
<td style={{ textAlign: 'right', color: 'var(--accent-primary)', fontWeight: '600', fontFamily: 'var(--font-mono)' }}>
{(data.accuracy * 100).toFixed(1)}%
</td>
</tr>
))
}
</tbody>
</table>
</div>
</div>
)}
{/* Recent Predictions */}
<div className="table-container">
<div style={{ padding: 'var(--space-4) var(--space-4) 0', borderBottom: 'var(--border-subtle)' }}>
<h3 style={{ marginBottom: 'var(--space-4)' }}>Recent Predictions</h3>
</div>
{recent_predictions.length === 0 ? (
<div className="empty-state">
<p className="empty-state-title">No Predictions Yet</p>
<p className="empty-state-text">Visit the Live Games page to start tracking predictions.</p>
</div>
) : (
<table className="data-table">
<thead>
<tr>
<th>Date</th>
<th>Matchup</th>
<th>Prediction</th>
<th style={{ textAlign: 'center' }}>Confidence</th>
<th style={{ textAlign: 'center' }}>Result</th>
</tr>
</thead>
<tbody>
{recent_predictions.map((pred, idx) => {
const isPending = pred.is_correct === -1
const isCorrect = pred.is_correct === 1
return (
<tr key={idx}>
<td style={{ color: 'var(--text-muted)', fontSize: '0.8125rem' }}>
{pred.game_date || 'N/A'}
</td>
<td>
<span style={{ fontWeight: '500' }}>{pred.away_team || 'N/A'}</span>
<span style={{ color: 'var(--text-muted)' }}> @ </span>
<span style={{ fontWeight: '500' }}>{pred.home_team || 'N/A'}</span>
</td>
<td style={{ color: 'var(--accent-primary)', fontWeight: '500' }}>
{pred.predicted_winner || 'N/A'}
<span style={{ color: 'var(--text-muted)', fontSize: '0.75rem', marginLeft: 'var(--space-2)' }}>
({((pred.home_win_prob > 0.5 ? pred.home_win_prob : pred.away_win_prob) * 100 || 50).toFixed(0)}%)
</span>
</td>
<td style={{ textAlign: 'center' }}>
<span className={`badge confidence-${pred.confidence || 'medium'}`}>
{(pred.confidence || 'medium').toUpperCase()}
</span>
</td>
<td style={{ textAlign: 'center' }}>
{isPending ? (
<span className="badge badge-neutral">PENDING</span>
) : isCorrect ? (
<span className="badge badge-success">CORRECT</span>
) : (
<span className="badge badge-danger">WRONG</span>
)}
</td>
</tr>
)
})}
</tbody>
</table>
)}
</div>
</div>
)
}
export default Accuracy