rairo commited on
Commit
5df17b9
Β·
verified Β·
1 Parent(s): 957d8fd

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +428 -90
src/streamlit_app.py CHANGED
@@ -4,6 +4,34 @@ import numpy as np
4
  import requests
5
  import os
6
  from datetime import datetime
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
9
  # CSS (black & white theme)
@@ -15,11 +43,14 @@ st.markdown("""
15
  </style>
16
  """, unsafe_allow_html=True)
17
 
18
- # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
19
- import requests
20
- from bs4 import BeautifulSoup
21
 
22
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
 
 
 
23
  @st.cache_data(ttl=3600)
24
  def fetch_html(url):
25
  """Fetch raw HTML for a URL (with error handling)."""
@@ -27,31 +58,57 @@ def fetch_html(url):
27
  resp = requests.get(url, timeout=20)
28
  resp.raise_for_status()
29
  return resp.text
30
- except Exception as e:
31
  st.error(f"Failed to fetch {url}: {e}")
32
  return ""
 
 
 
33
 
34
  def parse_table(html, table_id=None):
35
  """
36
- Given raw HTML and optional table_id, locate that <table> via BeautifulSoup,
37
- then parse it with pandas.read_html.
38
  """
39
- soup = BeautifulSoup(html, "html.parser")
 
 
40
  if table_id:
 
41
  tbl = soup.find("table", {"id": table_id})
42
- if not tbl:
43
- return pd.DataFrame()
44
- tbl_html = str(tbl)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  else:
46
- # fallback: first table on page
47
  first = soup.find("table")
48
- if not first:
49
- return pd.DataFrame()
50
- tbl_html = str(first)
 
 
51
 
52
  try:
 
53
  return pd.read_html(tbl_html)[0]
54
- except ValueError:
 
 
 
55
  return pd.DataFrame()
56
 
57
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
@@ -67,13 +124,16 @@ def get_player_index():
67
  for letter in map(chr, range(ord('a'), ord('z')+1)):
68
  url = f"{base}{letter}/"
69
  html = fetch_html(url)
70
- df = parse_table(html, table_id="players")
71
- if df.empty:
 
 
 
 
 
72
  continue
73
 
74
- # each row: <th data-stat="player"><a href="/players/x/xxxxx.html">Name</a></th>
75
- soup = BeautifulSoup(html, "html.parser")
76
- for row in soup.select("table#players tbody tr"):
77
  th = row.find("th", {"data-stat": "player"})
78
  if not th:
79
  continue
@@ -95,18 +155,36 @@ def player_season_stats(bbr_url):
95
  Returns cleaned DataFrame.
96
  """
97
  html = fetch_html(bbr_url)
 
 
 
98
  df = parse_table(html, table_id="per_game")
99
  if df.empty or "Season" not in df.columns:
100
  return pd.DataFrame()
101
 
102
- # drop repeated header rows
103
  df = df[df["Season"] != "Season"].copy()
104
- df["Season"] = df["Season"].astype(str)
105
- # coerce all numeric columns
106
- nonnum = {"Season", "Tm", "Lg", "Pos"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  for col in df.columns:
108
- if col not in nonnum:
109
  df[col] = pd.to_numeric(df[col], errors="coerce")
 
110
  return df
111
 
112
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
@@ -119,23 +197,39 @@ def team_per_game(year):
119
  """
120
  url = f"https://www.basketball-reference.com/leagues/NBA_{year}_per_game.html"
121
  html = fetch_html(url)
122
- df = parse_table(html, table_id="per_game-team")
 
 
 
123
  if df.empty or "Team" not in df.columns:
124
  return pd.DataFrame()
125
 
126
  # drop repeated headers & rename
127
  df = df[df["Team"] != "Team"].copy()
128
  df.rename(columns={"Team": "Tm"}, inplace=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  # coerce numeric columns
130
- nonnum = {"Player", "Pos", "Tm"}
131
  for col in df.columns:
132
- if col not in nonnum:
133
  df[col] = pd.to_numeric(df[col], errors="coerce")
134
 
135
  return df
136
 
137
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
138
- # Perplexity integration (unchanged)
139
  PERP_KEY = os.getenv("PERPLEXITY_API_KEY")
140
  PERP_URL = "https://api.perplexity.ai/chat/completions"
141
 
@@ -145,7 +239,7 @@ def ask_perp(prompt, system="You are a helpful NBA analyst AI.", max_tokens=500,
145
  return ""
146
  hdr = {'Authorization':f'Bearer {PERP_KEY}','Content-Type':'application/json'}
147
  payload = {
148
- "model":"sonar-pro",
149
  "messages":[{"role":"system","content":system},{"role":"user","content":prompt}],
150
  "max_tokens":max_tokens, "temperature":temp
151
  }
@@ -153,10 +247,44 @@ def ask_perp(prompt, system="You are a helpful NBA analyst AI.", max_tokens=500,
153
  r = requests.post(PERP_URL, json=payload, headers=hdr, timeout=45)
154
  r.raise_for_status()
155
  return r.json().get("choices", [{}])[0].get("message",{}).get("content","")
 
 
 
 
 
 
 
 
 
 
156
  except Exception as e:
157
- st.error(f"Perplexity error: {e}")
158
  return ""
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
161
  def main():
162
  st.markdown('<h1 class="main-header">πŸ€ NBA Analytics Hub (BBR Edition)</h1>', unsafe_allow_html=True)
@@ -177,51 +305,236 @@ def main():
177
  elif page == "Roster Builder": roster_builder()
178
  else: trade_analyzer()
179
 
 
 
180
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
181
  def player_vs_player():
182
  st.markdown('<h2 class="section-header">Player vs Player Comparison</h2>', unsafe_allow_html=True)
183
  idx = get_player_index()
184
  names = idx['name'].tolist()
185
- sel = st.multiselect("Select Players (up to 4)", names, max_selections=4)
186
- seasons = st.multiselect("Select Seasons", ["2023–24","2022–23","2021–22","2020–21"], default=["2023–24"])
 
 
187
 
188
  if st.button("Run Comparison"):
189
- if not sel: return st.warning("Pick at least one player.")
190
- stats = []
191
- for p in sel:
192
- url = idx.loc[idx.name==p,'url'].iat[0]
193
- df = player_season_stats(url)
194
- df['Season'] = df['Season'].str.replace('-','–')
195
- df = df[df['Season'].isin(seasons)]
196
- if df.empty: continue
197
- avg = df.mean(numeric_only=True).to_frame().T
198
- avg['Player'] = p
199
- stats.append(avg)
200
- if not stats: return st.info("No data.")
201
- comp = pd.concat(stats, ignore_index=True)
202
- cols = ['Player','PTS','TRB','AST','STL','BLK','FG%','3P%','FT%']
203
- st.dataframe(comp[cols].round(2), use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  def team_vs_team():
206
  st.markdown('<h2 class="section-header">Team vs Team Analysis</h2>', unsafe_allow_html=True)
207
- year = st.selectbox("Season End Year", [2024,2023,2022,2021], index=0)
208
- tm_df = team_per_game(year)
 
 
 
 
 
 
 
 
 
 
209
  teams = tm_df['Tm'].unique().tolist()
210
- sel = st.multiselect("Select Teams (up to 4)", teams, max_selections=4)
211
 
212
  if st.button("Run Comparison"):
213
- if not sel: return st.warning("Pick at least one team.")
 
 
 
214
  stats = []
215
- for t in sel:
216
- df = tm_df[tm_df.Tm==t]
217
- if df.empty: continue
218
- avg = df.mean(numeric_only=True).to_frame().T
219
- avg['Team'] = t
220
- stats.append(avg)
221
- if not stats: return st.info("No data.")
222
- comp = pd.concat(stats, ignore_index=True)
223
- cols = ['Team','PTS','TRB','AST','STL','BLK','FG%','3P%','FT%']
224
- st.dataframe(comp[cols].round(2), use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
  def awards_predictor():
227
  st.markdown('<h2 class="section-header">NBA Awards Predictor</h2>', unsafe_allow_html=True)
@@ -258,28 +571,37 @@ def awards_predictor():
258
 
259
  def ai_chat():
260
  st.markdown('<h2 class="section-header">AI Chat & Insights</h2>', unsafe_allow_html=True)
261
- if 'history' not in st.session_state: st.session_state.history=[]
262
- for msg in st.session_state.history:
263
  with st.chat_message(msg["role"]):
264
  st.write(msg["content"])
265
  if prompt:=st.chat_input("Ask me anything about NBA…"):
266
- st.session_state.history.append({"role":"user","content":prompt})
 
 
267
  with st.chat_message("assistant"):
268
  ans = ask_perp(prompt, system="You are an NBA expert analyst AI.", max_tokens=700)
269
  st.write(ans)
270
- st.session_state.history.append({"role":"assistant","content":ans})
271
  st.subheader("Quick Actions")
272
  c1,c2,c3 = st.columns(3)
273
  if c1.button("πŸ† Contenders"):
274
- st.write(ask_perp("Top 5 championship contenders for 2024 and why?"))
 
 
275
  if c2.button("⭐ Rising Stars"):
276
- st.write(ask_perp("Most promising NBA players age ≀23 in 2024?"))
 
 
277
  if c3.button("πŸ“Š Trades"):
278
- st.write(ask_perp("Potential NBA trades this season with analysis."))
 
 
279
 
280
  def young_projections():
281
  st.markdown('<h2 class="section-header">Young Player Projections</h2>', unsafe_allow_html=True)
282
- all_p = get_player_index()['name'].tolist()
 
283
  sp = st.selectbox("Select or enter player", [""]+all_p)
284
  if not sp:
285
  sp = st.text_input("Enter player name manually")
@@ -299,28 +621,32 @@ def young_projections():
299
  st.write(out)
300
  yrs_lbl = [f"Year {i+1}" for i in range(5)]
301
  vals = [ppg*(1+0.1*i) for i in range(5)]
302
- st.line_chart({'PPG':vals}, x=yrs_lbl)
 
 
 
303
 
304
  def similar_players():
305
  st.markdown('<h2 class="section-header">Similar Players Finder</h2>', unsafe_allow_html=True)
306
- all_p = get_player_index()['name'].tolist()
 
307
  tp = st.selectbox("Target Player", all_p)
308
- crit = st.multiselect("Criteria",["Position","Height/Weight","Style","Stats","Experience"],default=["Style","Stats"])
309
  if tp and crit and st.button("Find Similar"):
310
- prompt = f"Find top 5 current and top 3 historical similar to {tp} by {crit}."
311
  st.write(ask_perp(prompt, system="You are a similarity expert AI.", max_tokens=800))
312
  st.subheader("Manual Compare")
313
  p1 = st.selectbox("Player 1", all_p, key="p1")
314
  p2 = st.selectbox("Player 2", all_p, key="p2")
315
  if p1 and p2 and p1!=p2 and st.button("Compare Players"):
316
- prompt = f"Compare {p1} vs {p2} on stats, style, strengths/weaknesses, team impact."
317
  st.write(ask_perp(prompt, system="You are a comparison expert AI.", max_tokens=700))
318
 
319
  def roster_builder():
320
  st.markdown('<h2 class="section-header">NBA Roster Builder</h2>', unsafe_allow_html=True)
321
- cap = st.number_input("Salary Cap (M)",100,200,136)
322
- strat = st.selectbox("Strategy",["Champ Contender","Young Development","Balanced","Small Ball","Defense First"])
323
- pos = st.multiselect("Priority Positions",["PG","SG","SF","PF","C"],default=["PG","C"])
324
  st.subheader("Budget Allocation")
325
  cols = st.columns(5)
326
  alloc = {}
@@ -328,28 +654,40 @@ def roster_builder():
328
  for i,p in enumerate(["PG","SG","SF","PF","C"]):
329
  val = cols[i].number_input(f"{p} Budget ($M)",0,50,20, key=f"b{p}")
330
  alloc[p]=val; total+=val
331
- st.write(f"Total: ${total}M / ${cap}M")
332
- if total>cap: st.error("Over cap!")
333
 
334
- if st.button("Generate Roster"):
335
  if total<=cap:
336
  prompt = (
337
- f"Build roster with cap=${cap}M, strat={strat}, "
338
- f"priority={pos}, budgets={alloc}."
 
 
 
 
 
 
 
 
 
339
  )
340
- st.markdown("### Suggestions")
341
- st.write(ask_perp(prompt, system="You are a roster builder AI.", max_tokens=900))
342
  else:
343
- st.warning("Adjust budgets under cap.")
344
 
345
  def trade_analyzer():
346
  st.markdown('<h2 class="section-header">Trade Scenario Analyzer</h2>', unsafe_allow_html=True)
347
- t1 = st.text_input("Team 1 trades")
348
- t2 = st.text_input("Team 2 trades")
349
  if t1 and t2 and st.button("Analyze Trade"):
350
  prompt = (
351
- f"Team1 trades: {t1}. Team2 trades: {t2}. "
352
- "Assess fairness, cap, impact, chemistry, likelihood, alternatives."
 
 
 
353
  )
354
  st.write(ask_perp(prompt, system="You are a trade analysis AI.", max_tokens=700))
355
 
 
4
  import requests
5
  import os
6
  from datetime import datetime
7
+ from bs4 import BeautifulSoup
8
+ import re # New import for regex operations
9
+
10
+ # --- IMPORTANT: Addressing PermissionError in Containerized Environments ---
11
+ # The error "PermissionError: [Errno 13] Permission denied: '/.streamlit'"
12
+ # occurs because Streamlit tries to write to a non-writable directory.
13
+ # To fix this in your Dockerfile or when running your Docker container,
14
+ # you should set one of the following environment variables:
15
+ #
16
+ # Option 1 (Recommended): Set the HOME environment variable to a writable path.
17
+ # In your Dockerfile: ENV HOME /tmp
18
+ # Or when running: docker run -e HOME=/tmp your_image_name
19
+ #
20
+ # Option 2: Disable Streamlit's usage statistics gathering.
21
+ # In your Dockerfile: ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=False
22
+ # Or when running: docker run -e STREAMLIT_BROWSER_GATHER_USAGE_STATS=False your_image_name
23
+ #
24
+ # Option 1 is generally more robust as it provides a writable home directory
25
+ # for any application that might need it.
26
+ # -----------------------------------------------------------------------
27
+
28
+ # Page configuration
29
+ st.set_page_config(
30
+ page_title="NBA Analytics Hub",
31
+ page_icon="πŸ€",
32
+ layout="wide",
33
+ initial_sidebar_state="expanded"
34
+ )
35
 
36
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
37
  # CSS (black & white theme)
 
43
  </style>
44
  """, unsafe_allow_html=True)
45
 
46
+ # Initialize session state for chat history
47
+ if 'chat_history' not in st.session_state:
48
+ st.session_state.chat_history = []
49
 
50
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
51
+ # Basketball-Reference Data Fetching Utilities
52
+ # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
53
+
54
  @st.cache_data(ttl=3600)
55
  def fetch_html(url):
56
  """Fetch raw HTML for a URL (with error handling)."""
 
58
  resp = requests.get(url, timeout=20)
59
  resp.raise_for_status()
60
  return resp.text
61
+ except requests.exceptions.RequestException as e:
62
  st.error(f"Failed to fetch {url}: {e}")
63
  return ""
64
+ except Exception as e:
65
+ st.error(f"An unexpected error occurred while fetching {url}: {e}")
66
+ return ""
67
 
68
  def parse_table(html, table_id=None):
69
  """
70
+ Given raw HTML and optional table_id, locate that <table>,
71
+ handling cases where it's commented out, then parse it with pandas.read_html.
72
  """
73
+ soup = BeautifulSoup(html, "lxml") # Using lxml for potentially faster parsing
74
+ tbl_html = ""
75
+
76
  if table_id:
77
+ # First, try to find the table directly
78
  tbl = soup.find("table", {"id": table_id})
79
+ if tbl:
80
+ tbl_html = str(tbl)
81
+ else:
82
+ # If not found directly, search for it within HTML comments
83
+ # Basketball-Reference often comments out tables
84
+ comment_pattern = re.compile(r'<!--.*?<table[^>]*id="%s"[^>]*>.*?</table>.*?-->' % table_id, re.DOTALL)
85
+ comment_match = comment_pattern.search(html)
86
+ if comment_match:
87
+ # Extract the content of the comment
88
+ comment_content = comment_match.group(0)
89
+ # Remove the comment tags
90
+ comment_content = comment_content.replace('<!--', '').replace('-->', '')
91
+ # Parse the comment content as new HTML
92
+ comment_soup = BeautifulSoup(comment_content, 'lxml')
93
+ tbl = comment_soup.find('table', {'id': table_id})
94
+ if tbl:
95
+ tbl_html = str(tbl)
96
  else:
97
+ # fallback: first table on page (only if no table_id specified)
98
  first = soup.find("table")
99
+ if first:
100
+ tbl_html = str(first)
101
+
102
+ if not tbl_html:
103
+ return pd.DataFrame()
104
 
105
  try:
106
+ # pd.read_html returns a list of DataFrames, we want the first one
107
  return pd.read_html(tbl_html)[0]
108
+ except ValueError: # No tables found in the provided HTML string
109
+ return pd.DataFrame()
110
+ except Exception as e:
111
+ st.error(f"Error parsing table with pandas: {e}")
112
  return pd.DataFrame()
113
 
114
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
 
124
  for letter in map(chr, range(ord('a'), ord('z')+1)):
125
  url = f"{base}{letter}/"
126
  html = fetch_html(url)
127
+ if not html:
128
+ continue
129
+
130
+ soup = BeautifulSoup(html, "lxml")
131
+ # The players table is usually directly available, not commented out.
132
+ table = soup.find("table", {"id": "players"})
133
+ if not table:
134
  continue
135
 
136
+ for row in table.select("tbody tr"):
 
 
137
  th = row.find("th", {"data-stat": "player"})
138
  if not th:
139
  continue
 
155
  Returns cleaned DataFrame.
156
  """
157
  html = fetch_html(bbr_url)
158
+ if not html:
159
+ return pd.DataFrame()
160
+
161
  df = parse_table(html, table_id="per_game")
162
  if df.empty or "Season" not in df.columns:
163
  return pd.DataFrame()
164
 
165
+ # drop repeated header rows (e.g., rows where 'Season' is literally 'Season')
166
  df = df[df["Season"] != "Season"].copy()
167
+ df["Season"] = df["Season"].astype(str) # Ensure season is string
168
+
169
+ # Standardize column names to match previous nba_api output expectations
170
+ df = df.rename(columns={
171
+ 'G': 'GP', 'GS': 'GS', 'MP': 'MIN',
172
+ 'FG%': 'FG_PCT', '3P%': 'FG3_PCT', 'FT%': 'FT_PCT',
173
+ 'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO',
174
+ 'PF': 'PF', 'PTS': 'PTS',
175
+ 'Age': 'AGE', 'Tm': 'TEAM_ABBREVIATION', 'Lg': 'LEAGUE_ID', 'Pos': 'POSITION',
176
+ 'FG': 'FGM', 'FGA': 'FGA', '3P': 'FG3M', '3PA': 'FG3A',
177
+ '2P': 'FGM2', '2PA': 'FGA2', '2P%': 'FG2_PCT', 'eFG%': 'EFG_PCT',
178
+ 'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB'
179
+ })
180
+
181
+ # Coerce all numeric columns
182
+ # Exclude columns that are definitely not numeric or are identifiers
183
+ non_numeric_cols = {'Season', 'TEAM_ABBREVIATION', 'LEAGUE_ID', 'POSITION', 'Player'}
184
  for col in df.columns:
185
+ if col not in non_numeric_cols:
186
  df[col] = pd.to_numeric(df[col], errors="coerce")
187
+
188
  return df
189
 
190
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
 
197
  """
198
  url = f"https://www.basketball-reference.com/leagues/NBA_{year}_per_game.html"
199
  html = fetch_html(url)
200
+ if not html:
201
+ return pd.DataFrame()
202
+
203
+ df = parse_table(html, table_id="per_game-team") # Correct table ID for team stats
204
  if df.empty or "Team" not in df.columns:
205
  return pd.DataFrame()
206
 
207
  # drop repeated headers & rename
208
  df = df[df["Team"] != "Team"].copy()
209
  df.rename(columns={"Team": "Tm"}, inplace=True)
210
+
211
+ # Standardize column names
212
+ df = df.rename(columns={
213
+ 'G': 'GP', 'MP': 'MIN',
214
+ 'FG%': 'FG_PCT', '3P%': 'FG3_PCT', 'FT%': 'FT_PCT',
215
+ 'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO',
216
+ 'PF': 'PF', 'PTS': 'PTS',
217
+ 'Rk': 'RANK', 'W': 'WINS', 'L': 'LOSSES', 'W/L%': 'WIN_LOSS_PCT',
218
+ 'FG': 'FGM', 'FGA': 'FGA', '3P': 'FG3M', '3PA': 'FG3A',
219
+ '2P': 'FGM2', '2PA': 'FGA2', '2P%': 'FG2_PCT', 'eFG%': 'EFG_PCT',
220
+ 'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB'
221
+ })
222
+
223
  # coerce numeric columns
224
+ non_numeric_cols = {"Tm", "RANK"} # 'RANK' is usually numeric, but 'Tm' is not
225
  for col in df.columns:
226
+ if col not in non_numeric_cols:
227
  df[col] = pd.to_numeric(df[col], errors="coerce")
228
 
229
  return df
230
 
231
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
232
+ # Perplexity integration
233
  PERP_KEY = os.getenv("PERPLEXITY_API_KEY")
234
  PERP_URL = "https://api.perplexity.ai/chat/completions"
235
 
 
239
  return ""
240
  hdr = {'Authorization':f'Bearer {PERP_KEY}','Content-Type':'application/json'}
241
  payload = {
242
+ "model":"sonar-medium-online", # Changed to a commonly available online model
243
  "messages":[{"role":"system","content":system},{"role":"user","content":prompt}],
244
  "max_tokens":max_tokens, "temperature":temp
245
  }
 
247
  r = requests.post(PERP_URL, json=payload, headers=hdr, timeout=45)
248
  r.raise_for_status()
249
  return r.json().get("choices", [{}])[0].get("message",{}).get("content","")
250
+ except requests.exceptions.RequestException as e:
251
+ error_message = f"Error communicating with Perplexity API: {e}"
252
+ if r.response is not None:
253
+ try:
254
+ error_detail = r.response.json().get("error", {}).get("message", r.response.text)
255
+ error_message = f"Perplexity API error: {error_detail}"
256
+ except ValueError:
257
+ error_message = f"Perplexity API error: {r.response.status_code} - {r.response.reason}"
258
+ st.error(error_message)
259
+ return ""
260
  except Exception as e:
261
+ st.error(f"An unexpected error occurred with Perplexity API: {e}")
262
  return ""
263
 
264
+ # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
265
+ # Helper for dynamic season generation
266
+ def get_available_seasons(num_seasons=6):
267
+ """Generates a list of recent NBA seasons in 'YYYY–YY' format."""
268
+ current_year = datetime.now().year
269
+ current_month = datetime.now().month
270
+
271
+ # Determine the latest season end year.
272
+ # If it's before July (e.g., May 2025), the current season is 2024-25 (ends 2025).
273
+ # If it's July or later (e.g., July 2025), the 2024-25 season just finished,
274
+ # and the next season (2025-26) is considered the "current" one for future projections.
275
+ latest_season_end_year = current_year
276
+ if current_month >= 7: # NBA season typically ends in June
277
+ latest_season_end_year += 1
278
+
279
+ seasons_list = []
280
+ for i in range(num_seasons):
281
+ end_year = latest_season_end_year - i
282
+ start_year = end_year - 1
283
+ seasons_list.append(f"{start_year}–{end_year}")
284
+ return sorted(seasons_list, reverse=True) # Sort to show most recent first
285
+
286
+ # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
287
+ # Main App Structure
288
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
289
  def main():
290
  st.markdown('<h1 class="main-header">πŸ€ NBA Analytics Hub (BBR Edition)</h1>', unsafe_allow_html=True)
 
305
  elif page == "Roster Builder": roster_builder()
306
  else: trade_analyzer()
307
 
308
+ # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
309
+ # Page Implementations
310
  # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
311
  def player_vs_player():
312
  st.markdown('<h2 class="section-header">Player vs Player Comparison</h2>', unsafe_allow_html=True)
313
  idx = get_player_index()
314
  names = idx['name'].tolist()
315
+ selected_players = st.multiselect("Select Players (up to 4)", names, max_selections=4)
316
+
317
+ available_seasons = get_available_seasons()
318
+ selected_seasons = st.multiselect("Select Seasons", available_seasons, default=[available_seasons[0]])
319
 
320
  if st.button("Run Comparison"):
321
+ if not selected_players:
322
+ st.warning("Please select at least one player to compare.")
323
+ return
324
+
325
+ stats_tabs = st.tabs(["Basic Stats", "Advanced Stats", "Visualizations"])
326
+ all_player_season_data = [] # To store individual season rows for each player
327
+
328
+ with st.spinner("Fetching player data..."):
329
+ for player_name in selected_players:
330
+ player_url = idx.loc[idx.name == player_name, 'url'].iat[0]
331
+ df_player_career = player_season_stats(player_url)
332
+
333
+ if not df_player_career.empty:
334
+ # Filter for selected seasons and ensure 'Season' column is consistent
335
+ df_player_career['Season'] = df_player_career['Season'].str.replace('-', '–')
336
+ filtered_df = df_player_career[df_player_career['Season'].isin(selected_seasons)].copy()
337
+
338
+ if not filtered_df.empty:
339
+ filtered_df['Player'] = player_name # Add player name for identification
340
+ all_player_season_data.append(filtered_df)
341
+ else:
342
+ st.info(f"No data found for {player_name} in selected seasons.")
343
+ else:
344
+ st.info(f"Could not fetch career stats for {player_name}.")
345
+
346
+ if not all_player_season_data:
347
+ st.info("No data available for the selected players and seasons.")
348
+ return
349
+
350
+ # Concatenate all collected season data into one DataFrame
351
+ comparison_df_raw = pd.concat(all_player_season_data, ignore_index=True)
352
+
353
+ with stats_tabs[0]:
354
+ st.subheader("Basic Statistics")
355
+ # Group by player and average for the basic stats table if multiple seasons are selected
356
+ # Otherwise, show individual season stats if only one season is selected
357
+ if len(selected_seasons) > 1:
358
+ basic_display_df = comparison_df_raw.groupby('Player').mean(numeric_only=True).reset_index()
359
+ basic_cols = ['Player', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
360
+ else:
361
+ basic_display_df = comparison_df_raw.copy()
362
+ basic_cols = ['Player', 'Season', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
363
+
364
+ display_cols = [col for col in basic_cols if col in basic_display_df.columns]
365
+ st.dataframe(basic_display_df[display_cols].round(2), use_container_width=True)
366
+
367
+ with stats_tabs[1]:
368
+ st.subheader("Advanced Statistics")
369
+ if not comparison_df_raw.empty:
370
+ advanced_df = comparison_df_raw.copy()
371
+
372
+ # Calculate TS% (True Shooting Percentage)
373
+ # Ensure FGA and FTA are numeric and not zero to avoid division by zero
374
+ advanced_df['FGA'] = pd.to_numeric(advanced_df['FGA'], errors='coerce').fillna(0)
375
+ advanced_df['FTA'] = pd.to_numeric(advanced_df['FTA'], errors='coerce').fillna(0)
376
+ advanced_df['PTS'] = pd.to_numeric(advanced_df['PTS'], errors='coerce').fillna(0)
377
+
378
+ advanced_df['TS_PCT'] = advanced_df.apply(
379
+ lambda row: row['PTS'] / (2 * (row['FGA'] + 0.44 * row['FTA'])) if (row['FGA'] + 0.44 * row['FTA']) != 0 else 0,
380
+ axis=1
381
+ )
382
+
383
+ if len(selected_seasons) > 1:
384
+ advanced_display_df = advanced_df.groupby('Player').mean(numeric_only=True).reset_index()
385
+ advanced_cols = ['Player', 'PTS', 'REB', 'AST', 'FG_PCT', 'TS_PCT']
386
+ else:
387
+ advanced_display_df = advanced_df.copy()
388
+ advanced_cols = ['Player', 'Season', 'PTS', 'REB', 'AST', 'FG_PCT', 'TS_PCT']
389
+
390
+ display_cols = [col for col in advanced_cols if col in advanced_display_df.columns]
391
+ st.dataframe(advanced_display_df[display_cols].round(3), use_container_width=True)
392
+ else:
393
+ st.info("No data available for advanced statistics.")
394
+
395
+ with stats_tabs[2]:
396
+ st.subheader("Player Comparison Charts")
397
+
398
+ if not comparison_df_raw.empty:
399
+ metrics = ['PTS', 'REB', 'AST', 'FG_PCT', '3P_PCT', 'FT_PCT', 'STL', 'BLK']
400
+ available_metrics = [m for m in metrics if m in comparison_df_raw.columns]
401
+
402
+ if available_metrics:
403
+ selected_metric = st.selectbox("Select Metric to Visualize", available_metrics)
404
+
405
+ if selected_metric:
406
+ # Determine if we are showing a trend for one player or comparison for multiple
407
+ if len(selected_players) == 1 and len(selected_seasons) > 1:
408
+ # Show trend over seasons for one player
409
+ player_trend_df = comparison_df_raw[comparison_df_raw['Player'] == selected_players[0]].sort_values(by='Season')
410
+ fig = px.line(
411
+ player_trend_df,
412
+ x='Season',
413
+ y=selected_metric,
414
+ title=f"{selected_players[0]} - {selected_metric} Trend",
415
+ markers=True
416
+ )
417
+ else:
418
+ # Average over selected seasons for multiple players for bar chart
419
+ avg_comparison_df = comparison_df_raw.groupby('Player')[available_metrics].mean().reset_index()
420
+ fig = px.bar(
421
+ avg_comparison_df,
422
+ x='Player',
423
+ y=selected_metric,
424
+ title=f"Average {selected_metric} Comparison (Selected Seasons)",
425
+ color='Player'
426
+ )
427
+ st.plotly_chart(fig, use_container_width=True)
428
+
429
+ # Radar chart for multi-metric comparison
430
+ radar_metrics_for_chart = ['PTS', 'REB', 'AST', 'STL', 'BLK']
431
+ radar_metrics_for_chart = [m for m in radar_metrics_for_chart if m in comparison_df_raw.columns]
432
+
433
+ if len(radar_metrics_for_chart) >= 3:
434
+ radar_data = {}
435
+ # Use the averaged data for radar chart if multiple seasons
436
+ if len(selected_seasons) > 1:
437
+ radar_source_df = comparison_df_raw.groupby('Player')[radar_metrics_for_chart].mean(numeric_only=True).reset_index()
438
+ else:
439
+ radar_source_df = comparison_df_raw.copy()
440
+
441
+ scaled_radar_df = radar_source_df.copy()
442
+
443
+ # Simple min-max scaling for radar chart visualization (0-100)
444
+ for col in radar_metrics_for_chart:
445
+ min_val = scaled_radar_df[col].min()
446
+ max_val = scaled_radar_df[col].max()
447
+ if max_val > min_val:
448
+ scaled_radar_df[col] = ((scaled_radar_df[col] - min_val) / (max_val - min_val)) * 100
449
+ else:
450
+ scaled_radar_df[col] = 0 # Default if all values are the same
451
+
452
+ for _, row in scaled_radar_df.iterrows():
453
+ radar_data[row['Player']] = {
454
+ metric: row[metric] for metric in radar_metrics_for_chart
455
+ }
456
+
457
+ if radar_data:
458
+ radar_fig = create_radar_chart(radar_data, radar_metrics_for_chart)
459
+ st.plotly_chart(radar_fig, use_container_width=True)
460
+ else:
461
+ st.info("Could not generate radar chart data.")
462
+ else:
463
+ st.info("Select at least 3 common metrics for a radar chart (e.g., PTS, REB, AST, STL, BLK).")
464
+ else:
465
+ st.info("No common metrics available for visualization.")
466
+ else:
467
+ st.info("No data available for visualizations.")
468
+
469
 
470
  def team_vs_team():
471
  st.markdown('<h2 class="section-header">Team vs Team Analysis</h2>', unsafe_allow_html=True)
472
+
473
+ available_seasons = get_available_seasons()
474
+ selected_season_str = st.selectbox("Select Season", available_seasons, index=0)
475
+
476
+ # Extract the end year from the season string (e.g., "2024–25" -> 2025)
477
+ year_for_team_stats = int(selected_season_str.split('–')[1])
478
+
479
+ tm_df = team_per_game(year_for_team_stats)
480
+ if tm_df.empty:
481
+ st.info(f"No team data available for the {selected_season_str} season.")
482
+ return
483
+
484
  teams = tm_df['Tm'].unique().tolist()
485
+ selected_teams = st.multiselect("Select Teams (up to 4)", teams, max_selections=4)
486
 
487
  if st.button("Run Comparison"):
488
+ if not selected_teams:
489
+ st.warning("Please select at least one team.")
490
+ return
491
+
492
  stats = []
493
+ for t in selected_teams:
494
+ df = tm_df[tm_df.Tm == t].copy() # Use .copy() to avoid SettingWithCopyWarning
495
+ if not df.empty:
496
+ # For team stats, we usually get one row per team per season from team_per_game
497
+ # So, no need for .mean() here, just take the row.
498
+ df['Team'] = t # Add 'Team' column for consistency
499
+ df['Season'] = selected_season_str # Add 'Season' column
500
+ stats.append(df.iloc[0].to_dict()) # Convert the single row to dict
501
+ else:
502
+ st.info(f"No data found for team {t} in {selected_season_str}.")
503
+
504
+ if not stats:
505
+ st.info("No data available for the selected teams.")
506
+ return
507
+
508
+ comp = pd.DataFrame(stats)
509
+ # Ensure numeric columns are actually numeric for display and calculations
510
+ for col in ['PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', '3P_PCT', 'FT_PCT']:
511
+ if col in comp.columns:
512
+ comp[col] = pd.to_numeric(comp[col], errors='coerce')
513
+
514
+ st.subheader("Team Statistics Comparison")
515
+ cols = ['Team', 'Season', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', '3P_PCT', 'FT_PCT']
516
+ display_cols = [col for col in cols if col in comp.columns]
517
+ st.dataframe(comp[display_cols].round(2), use_container_width=True)
518
+
519
+ st.subheader("Team Performance Visualization")
520
+ metric_options = ['PTS', 'REB', 'AST', 'FG_PCT', '3P_PCT', 'FT_PCT']
521
+ available_metrics = [m for m in metric_options if m in comp.columns]
522
+
523
+ if available_metrics:
524
+ selected_metric = st.selectbox("Select Metric", available_metrics)
525
+
526
+ fig = px.bar(
527
+ comp,
528
+ x='Team',
529
+ y=selected_metric,
530
+ color='Team', # Color by team for clarity
531
+ title=f"Team {selected_metric} Comparison ({selected_season_str} Season)",
532
+ barmode='group'
533
+ )
534
+ st.plotly_chart(fig, use_container_width=True)
535
+ else:
536
+ st.info("No common metrics available for visualization.")
537
+
538
 
539
  def awards_predictor():
540
  st.markdown('<h2 class="section-header">NBA Awards Predictor</h2>', unsafe_allow_html=True)
 
571
 
572
  def ai_chat():
573
  st.markdown('<h2 class="section-header">AI Chat & Insights</h2>', unsafe_allow_html=True)
574
+ if 'chat_history' not in st.session_state: st.session_state.chat_history=[]
575
+ for msg in st.session_state.chat_history:
576
  with st.chat_message(msg["role"]):
577
  st.write(msg["content"])
578
  if prompt:=st.chat_input("Ask me anything about NBA…"):
579
+ st.session_state.chat_history.append({"role":"user","content":prompt})
580
+ with st.chat_message("user"):
581
+ st.write(prompt) # Display user message immediately
582
  with st.chat_message("assistant"):
583
  ans = ask_perp(prompt, system="You are an NBA expert analyst AI.", max_tokens=700)
584
  st.write(ans)
585
+ st.session_state.chat_history.append({"role":"assistant","content":ans})
586
  st.subheader("Quick Actions")
587
  c1,c2,c3 = st.columns(3)
588
  if c1.button("πŸ† Contenders"):
589
+ prompt = "Analyze the current NBA championship contenders for 2024. Who are the top 5 teams and why?"
590
+ response = ask_perp(prompt)
591
+ if response: st.write(response)
592
  if c2.button("⭐ Rising Stars"):
593
+ prompt = "Who are the most promising young NBA players to watch in 2024? Focus on players 23 and under."
594
+ response = ask_perp(prompt)
595
+ if response: st.write(response)
596
  if c3.button("πŸ“Š Trades"):
597
+ prompt = "What are some potential NBA trades that could happen this season? Analyze team needs and available players."
598
+ response = ask_perp(prompt)
599
+ if response: st.write(response)
600
 
601
  def young_projections():
602
  st.markdown('<h2 class="section-header">Young Player Projections</h2>', unsafe_allow_html=True)
603
+ all_p_df = get_player_index()
604
+ all_p = all_p_df['name'].tolist()
605
  sp = st.selectbox("Select or enter player", [""]+all_p)
606
  if not sp:
607
  sp = st.text_input("Enter player name manually")
 
621
  st.write(out)
622
  yrs_lbl = [f"Year {i+1}" for i in range(5)]
623
  vals = [ppg*(1+0.1*i) for i in range(5)]
624
+ fig = go.Figure()
625
+ fig.add_trace(go.Scatter(x=yrs_lbl, y=vals, mode='lines+markers', name='Projected PPG'))
626
+ fig.update_layout(title=f"{sp} - PPG Projection", xaxis_title="Years", yaxis_title="Points Per Game")
627
+ st.plotly_chart(fig, use_container_width=True)
628
 
629
  def similar_players():
630
  st.markdown('<h2 class="section-header">Similar Players Finder</h2>', unsafe_allow_html=True)
631
+ all_p_df = get_player_index()
632
+ all_p = all_p_df['name'].tolist()
633
  tp = st.selectbox("Target Player", all_p)
634
+ crit = st.multiselect("Criteria",["Position","Height/Weight","Playing Style","Statistical Profile","Age/Experience"],default=["Playing Style","Statistical Profile"])
635
  if tp and crit and st.button("Find Similar"):
636
+ prompt = f"Find top 5 current and top 3 historical similar to {tp} by {', '.join(crit)}. Provide detailed reasoning."
637
  st.write(ask_perp(prompt, system="You are a similarity expert AI.", max_tokens=800))
638
  st.subheader("Manual Compare")
639
  p1 = st.selectbox("Player 1", all_p, key="p1")
640
  p2 = st.selectbox("Player 2", all_p, key="p2")
641
  if p1 and p2 and p1!=p2 and st.button("Compare Players"):
642
+ prompt = f"Compare {p1} vs {p2} on statistical comparison (current season), playing style similarities and differences, strengths and weaknesses, team impact and role, and overall similarity score (1-10)."
643
  st.write(ask_perp(prompt, system="You are a comparison expert AI.", max_tokens=700))
644
 
645
  def roster_builder():
646
  st.markdown('<h2 class="section-header">NBA Roster Builder</h2>', unsafe_allow_html=True)
647
+ cap = st.number_input("Salary Cap (Millions)",100,200,136)
648
+ strat = st.selectbox("Strategy",["Championship Contender","Young Core Development","Balanced Veteran Mix","Small Ball","Defense First"])
649
+ pos = st.multiselect("Priority Positions",["Point Guard","Shooting Guard","Small Forward","Power Forward","Center"],default=["Point Guard","Center"])
650
  st.subheader("Budget Allocation")
651
  cols = st.columns(5)
652
  alloc = {}
 
654
  for i,p in enumerate(["PG","SG","SF","PF","C"]):
655
  val = cols[i].number_input(f"{p} Budget ($M)",0,50,20, key=f"b{p}")
656
  alloc[p]=val; total+=val
657
+ st.write(f"Total Allocated: ${total}M / ${cap}M")
658
+ if total>cap: st.error("Budget exceeds salary cap!")
659
 
660
+ if st.button("Generate Roster Suggestions"):
661
  if total<=cap:
662
  prompt = (
663
+ f"Build an NBA roster with the following constraints: "
664
+ f"Salary Cap: ${cap} million, Team Strategy: {strat}, "
665
+ f"Priority Positions: {', '.join(pos)}, "
666
+ f"Position Budgets: {alloc}. "
667
+ "Please provide: 1. Starting lineup with specific player recommendations. "
668
+ "2. Key bench players (6th man, backup center, etc.). "
669
+ "3. Total estimated salary breakdown. "
670
+ "4. Rationale for each major signing. "
671
+ "5. How this roster fits the chosen strategy. "
672
+ "6. Potential weaknesses and how to address them. "
673
+ "Focus on realistic player availability and current market values."
674
  )
675
+ st.markdown("### AI Roster Recommendations")
676
+ st.write(ask_perp(prompt, system="You are an NBA roster building expert AI.", max_tokens=900))
677
  else:
678
+ st.warning("Please adjust your budget to be within the salary cap before generating suggestions.")
679
 
680
  def trade_analyzer():
681
  st.markdown('<h2 class="section-header">Trade Scenario Analyzer</h2>', unsafe_allow_html=True)
682
+ t1 = st.text_input("Team 1 trades (e.g., 'LeBron James for Anthony Davis')")
683
+ t2 = st.text_input("Team 2 trades (e.g., 'Anthony Davis for LeBron James')")
684
  if t1 and t2 and st.button("Analyze Trade"):
685
  prompt = (
686
+ f"Analyze this potential NBA trade: Team 1 trades: {t1}. Team 2 trades: {t2}. "
687
+ "Please evaluate: 1. Fair value assessment. 2. How this trade helps each team. "
688
+ "3. Salary cap implications. 4. Impact on team chemistry and performance. "
689
+ "5. Likelihood of this trade happening. 6. Alternative trade suggestions. "
690
+ "Consider current team needs and player contracts."
691
  )
692
  st.write(ask_perp(prompt, system="You are a trade analysis AI.", max_tokens=700))
693