wuhp commited on
Commit
5dfb078
·
verified ·
1 Parent(s): 4eeb04e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +60 -118
app.py CHANGED
@@ -537,9 +537,9 @@ def _try_gemini_ground(q: str):
537
  f"Input: {q!r}"
538
  )
539
  resp = GENAI_CLIENT.models.generate_content(
540
- model="gemini-1.5-flash",
541
  contents=prompt,
542
- generation_config=cfg,
543
  )
544
  txt = (resp.text or "").strip()
545
  if txt.startswith("```"):
@@ -570,8 +570,8 @@ def _try_gemini_ground(q: str):
570
  if GENAI_LEGACY:
571
  try:
572
  model = GENAI_LEGACY.GenerativeModel(
573
- "gemini-1.5-flash",
574
- tools=[GENAI_LEGACY.Tool.from_google_search_retrieval(GENAI_LEGACY.GoogleSearch())]
575
  )
576
  prompt = (
577
  "Resolve a Clash Royale card string (typos possible). "
@@ -1483,7 +1483,7 @@ def _gemini_generate_text(prompt: str) -> str | None:
1483
  if GENAI_CLIENT:
1484
  try:
1485
  resp = GENAI_CLIENT.models.generate_content(
1486
- model="gemini-1.5-flash",
1487
  contents=prompt
1488
  )
1489
  return (resp.text or "").strip()
@@ -1492,46 +1492,12 @@ def _gemini_generate_text(prompt: str) -> str | None:
1492
  # legacy
1493
  if GENAI_LEGACY:
1494
  try:
1495
- out = GENAI_LEGACY.GenerativeModel("gemini-1.5-flash").generate_content(prompt)
1496
  return (out.text or "").strip()
1497
  except Exception as e:
1498
  _set_last_error(f"Gemini error (legacy): {e}")
1499
  return None
1500
 
1501
- # =========== NEW: Gemini helper for chat with Google Search enabled ===========
1502
- def _gemini_generate_with_search(prompt: str) -> str | None:
1503
- """Helper that enables Google Search tool for grounded, factual answers."""
1504
- # new SDK
1505
- if GENAI_CLIENT and GENAI_TYPES:
1506
- try:
1507
- Tool = GENAI_TYPES.Tool
1508
- GoogleSearch = GENAI_TYPES.GoogleSearch
1509
- generation_config = GENAI_TYPES.GenerationConfig(tools=[Tool(google_search=GoogleSearch())])
1510
-
1511
- resp = GENAI_CLIENT.models.generate_content(
1512
- model="gemini-1.5-flash",
1513
- contents=prompt,
1514
- generation_config=generation_config,
1515
- )
1516
- return (resp.text or "").strip()
1517
- except Exception as e:
1518
- _set_last_error(f"Gemini error (new SDK with search): {e}")
1519
- return "Sorry, I encountered an error while searching for an answer."
1520
- # legacy SDK
1521
- if GENAI_LEGACY:
1522
- try:
1523
- model = GENAI_LEGACY.GenerativeModel(
1524
- "gemini-1.5-flash",
1525
- tools=[GENAI_LEGACY.Tool.from_google_search_retrieval(GENAI_LEGACY.GoogleSearch())]
1526
- )
1527
- out = model.generate_content(prompt)
1528
- return (out.text or "").strip()
1529
- except Exception as e:
1530
- _set_last_error(f"Gemini error (legacy with search): {e}")
1531
- return "Sorry, I encountered an error while searching for an answer."
1532
- return "Gemini with search is not configured."
1533
-
1534
-
1535
  def gem_coach(pack: dict, history=None, question=None):
1536
  """
1537
  Coach-style, trophy-range–aware analysis. ONLY uses data in pack.
@@ -1569,8 +1535,10 @@ def gem_coach(pack: dict, history=None, question=None):
1569
  "CRITICAL: Use ONLY the cards/decks present in the JSON. Do NOT invent cards or stats. "
1570
  "Respect owned_only/min_level constraints and keep suggested average elixir reasonable.\n\n"
1571
 
 
1572
  "**IMPORTANT: Use the `deck_card_details` section to understand the function of each card in the user's deck. This is your primary source of truth for what each card does.**\n\n"
1573
-
 
1574
  "Context you MUST use:\n"
1575
  "- player.trophies and bracket.\n"
1576
  "- trophy_range_meta.* are what the user is actually facing now.\n"
@@ -1604,31 +1572,21 @@ def gem_coach(pack: dict, history=None, question=None):
1604
  _set_last_error(f"Gemini parse error: {e}")
1605
  return None
1606
  else: # Follow-up question (compose a single prompt with context + short history)
1607
- # ========= START: MODIFIED FOLLOW-UP LOGIC ===========
1608
- convo_lines = [
1609
- "You are a helpful Clash Royale coach. Concisely answer the user's follow-up question.",
1610
- "Use the provided JSON data for context about the user's deck, performance, and the initial meta analysis.",
1611
- "If the user asks a factual question about the game (e.g., card stats, damage, interactions) that is not in the provided JSON, you MUST use the search tool to find the most accurate and up-to-date information.",
1612
- "Always prioritize factual accuracy."
1613
- ]
1614
- # Seed with initial analysis if available
1615
  if history and history[0] and history[0][1]:
1616
- # Keep the initial analysis out of the prompt to save tokens, just use the JSON pack
1617
- pass
1618
- # Add last 4 user/model turns for context
1619
  for user_msg, model_msg in history[-4:]:
1620
  if user_msg:
1621
  convo_lines.append("User: " + user_msg)
1622
- if model_msg and model_msg != "Analysis complete. Ask me anything about this deck!":
1623
  convo_lines.append("Coach: " + model_msg)
 
 
1624
 
1625
- convo_lines.append("\nThis is the JSON data context for the user's deck and our initial analysis. Use it for context, but use search for new factual questions not contained within:\n" + json.dumps(pack, separators=(',',':')))
1626
- convo_lines.append("\nUser's new question: " + (question or ""))
1627
-
1628
- txt = _gemini_generate_with_search("\n\n".join(convo_lines))
1629
  return txt or "Sorry, I couldn't generate a response."
1630
- # ========= END: MODIFIED FOLLOW-UP LOGIC ===========
1631
-
1632
 
1633
  # =========================
1634
  # Smart Builder (numeric core; semantics via Gemini)
@@ -1647,15 +1605,14 @@ def smart_builder(
1647
  player_info_for_pack=None,
1648
  micro_weight=0.7,
1649
  include_top=True,
1650
- recent_matches_for_pack=None,
1651
- inv=None # Accept inventory object
1652
  ):
1653
  deck = canonicalize_card_list(list(deck_cards or []))
1654
  if not deck:
1655
  return "Pick at least one card first.", None
1656
 
1657
  # ================================================================= #
1658
- # ========= START: NEW CODE TO FETCH AND INJECT DETAILS ========= #
1659
  # ================================================================= #
1660
 
1661
  deck_card_details = []
@@ -1671,7 +1628,7 @@ def smart_builder(
1671
  })
1672
 
1673
  # =============================================================== #
1674
- # ========= END: NEW CODE TO FETCH AND INJECT DETAILS =========== #
1675
  # =============================================================== #
1676
 
1677
 
@@ -1739,8 +1696,7 @@ def smart_builder(
1739
  existing = set(deck)
1740
  base_avg = avg_elixir(deck)
1741
  elixir_low, elixir_high = 3.0, 4.5
1742
- # Use the passed inventory object for filtering candidates
1743
- pool = candidate_cards(inv or STATE.inv, min_level=min_level, owned_only=owned_only)
1744
  suggestions_seed = []
1745
 
1746
  for c in pool:
@@ -1770,9 +1726,9 @@ def smart_builder(
1770
 
1771
  # Build LLM pack (facts only) with priority policy + local meta + hard counters
1772
  priority_policy = {"micro_weight": float(micro_weight), "include_top": bool(include_top)}
1773
- pack = build_llm_pack(deck, df, ctx_notes, top_seed, inv or STATE.inv, min_level, owned_only, coo, player_info_for_pack or {}, priority_policy, recent_matches=recent_matches_for_pack)
1774
 
1775
- # Inject the details into the final pack object
1776
  pack["deck_card_details"] = deck_card_details
1777
 
1778
  lines = []
@@ -1853,7 +1809,7 @@ def smart_builder(
1853
  for r in ranked[:10]:
1854
  badges = " ".join(f"`{b}`" for b in (r.get("badges") or []))
1855
  reason = r.get('reason') or ""
1856
- lines.append(f"- **{r.get('card')}**: {reason} {badges}")
1857
  edits = result.get("edits_for_meta") or []
1858
  if edits:
1859
  lines.append("\n**Edits to beat your local meta**")
@@ -1864,12 +1820,12 @@ def smart_builder(
1864
  if tech:
1865
  lines.append("\n**Tech choices**")
1866
  for r in tech[:6]:
1867
- lines.append(f"- **{r.get('card')}**: {r.get('why')}")
1868
  qf = result.get("quick_fixes") or []
1869
  if qf:
1870
  lines.append("\n**Quick +1 fixes**")
1871
  for r in qf[:6]:
1872
- lines.append(f"- **{r.get('card')}**: {r.get('reason')}")
1873
  if result.get("warnings"):
1874
  for w in result["warnings"]:
1875
  lines.append("⚠️ " + w)
@@ -2029,7 +1985,7 @@ def player_snapshot_lines(p, tag):
2029
  for k in ("targetType","hitSpeed","dmgType"):
2030
  if k in ext: ext_bits.append(f"{k}={ext[k]}")
2031
  extra = (" | " + ", ".join(ext_bits)) if ext_bits else ""
2032
- lines.append(f"- **{nm}**: roles=`{('/'.join(r) or '—')}` | tags=`{('/'.join(sr) or '—')}`{extra}")
2033
  if champs>1: lines.append(f"⚠️ Champions in deck: {champs} (most formats allow only 1)")
2034
  if evos>0: lines.append(f"Evolution-ready in deck: {evos}")
2035
  else:
@@ -2446,8 +2402,6 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
2446
  "Tip: run **Fetch & Analyze** first to seed meta/co-occurrence for stronger suggestions."
2447
  )
2448
  use_gemini = gr.Checkbox(label="Explain & rank with Gemini", value=True)
2449
- # =========== NEW: Unlocked cards checkbox ===========
2450
- recommend_owned_only_chk = gr.Checkbox(label="Only recommend cards I have unlocked", value=False)
2451
  with gr.Row():
2452
  micro_w_slider = gr.Slider(0.0, 1.0, value=0.7, step=0.05, label="Weight on trophy-range micro-meta")
2453
  include_top_chk = gr.Checkbox(label="Blend in top-players meta", value=True)
@@ -2455,9 +2409,8 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
2455
  builder_md = gr.Markdown()
2456
 
2457
  with gr.Column(visible=False) as chat_interface:
2458
- chatbot = gr.Chatbot(label="Follow-up Coach Chat (with Google Search)", height=400)
2459
- chat_input = gr.Textbox(label="Ask a follow-up question", placeholder="e.g., How much damage does a level 11 Mega Knight do?")
2460
-
2461
 
2462
  # ================= Rankings =================
2463
  with gr.Tab("Rankings & Top Decks", id=1):
@@ -2511,11 +2464,11 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
2511
  wr_plot2 = gr.Plot()
2512
 
2513
  gr.Markdown("---")
2514
- gr.Markdown("### Card Lookup (handles typos + learns unknown cards)")
2515
  with gr.Row():
2516
- lookup_tb = gr.Textbox(label="Card lookup", placeholder="e.g., suspious bush")
2517
- lookup_btn = gr.Button("Lookup Card Info")
2518
- lookup_md = gr.Markdown()
2519
 
2520
  # ================= Player & Forecast =================
2521
  with gr.Tab("Player & Forecast", id=4):
@@ -2598,8 +2551,7 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
2598
  gem_tb.change(_set_gem, [gem_tb], [gem_info])
2599
 
2600
  # --- DECK BUILDER TAB ---
2601
- # =========== MODIFIED: Added owned_only parameter ===========
2602
- def _from_tag_rich(tag, use_gem, micro_w, include_top, owned_only):
2603
  if not tag:
2604
  return "Enter a player tag in the sidebar.", None, None, gr.update(visible=False), None
2605
  if not STATE.api_token:
@@ -2611,23 +2563,8 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
2611
  p = api.player(t)
2612
  except Exception as e:
2613
  return f"Error: {e}", None, None, gr.update(visible=False), None
2614
-
2615
- # =========== NEW: Fetch and store player's card inventory ===========
2616
- try:
2617
- STATE.inv = Inventory()
2618
- for card_data in p.get("cards", []):
2619
- cname = jname(card_data.get("name"))
2620
- if cname:
2621
- STATE.inv.card[cname] = {
2622
- "owned": True,
2623
- "level": card_data.get("level", 0)
2624
- }
2625
- except Exception as e:
2626
- # Non-critical error, can proceed without inventory
2627
- _set_last_error(f"Could not parse player inventory: {e}")
2628
 
2629
-
2630
- # Collect recent matches for LLM analysis
2631
  recent_matches = collect_recent_matches_for_llm(api, t)
2632
 
2633
  header_lines = player_snapshot_lines(p, t)
@@ -2666,35 +2603,39 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
2666
  use_bracket=True, bracket_center=p.get("trophies"),
2667
  player_info_for_pack=player_info_for_pack,
2668
  micro_weight=float(micro_w), include_top=bool(include_top),
2669
- recent_matches_for_pack=recent_matches,
2670
- owned_only=bool(owned_only), # Pass the new parameter
2671
- inv=STATE.inv # Pass the fetched inventory
2672
  )
2673
  final_md = "\n".join(header_lines + ["", *perf_lines, "", coach_block])
2674
 
2675
- initial_history_pairs = [("", "Analysis complete. Ask me anything about this deck!")]
 
 
 
2676
 
2677
- return final_md, pack, initial_history_pairs, gr.update(visible=True), initial_history_pairs
2678
 
2679
- def chat_response(message, history, pack_state, coach_hist_state):
 
2680
  if not pack_state:
2681
- history.append((message, "Please analyze a deck first."))
2682
- return history, coach_hist_state
2683
 
2684
- # Call coach with the pair-style history
2685
- reply = gem_coach(pack_state, history=coach_hist_state, question=message)
 
 
 
 
 
 
2686
 
2687
- # Update chatbot UI history
2688
- history.append((message, reply))
2689
- # Update internal state history
2690
- new_coach_hist = coach_hist_state + [(message, reply)]
2691
 
2692
- return history, new_coach_hist
2693
 
2694
- # =========== MODIFIED: Pass new checkbox state to handler ===========
2695
  build_from_tag_btn.click(
2696
  _from_tag_rich,
2697
- [player_tag_state, use_gemini, micro_w_slider, include_top_chk, recommend_owned_only_chk],
2698
  [builder_md, builder_pack_state, chatbot, chat_interface, coach_history_state]
2699
  )
2700
 
@@ -2736,6 +2677,7 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
2736
  # --- CARD STATS TAB ---
2737
  meta_btn.click(recompute_meta, [], [meta_df, meta_plot, wr_plot2])
2738
 
 
2739
  def _lookup_card_handler(q):
2740
  q = (q or "").strip()
2741
  if not q:
@@ -2745,7 +2687,7 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
2745
  info = lookup_card_details(q)
2746
  if not info:
2747
  return f"Sorry, I couldn't find info for **{q}**."
2748
- lines = [f"### {info['name']}"]
2749
  if info.get("summary"):
2750
  lines.append(info["summary"])
2751
  # show basics if we have them
@@ -2756,7 +2698,7 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
2756
  mel = info.get("maxEvolutionLevel", 0)
2757
  if mel: bits.append(f"Max Evolution Level: {mel}")
2758
  if bits:
2759
- lines.append("\n_" + " • ".join(bits) + "_")
2760
  # citations
2761
  srcs = info.get("sources") or []
2762
  if srcs:
@@ -2813,4 +2755,4 @@ with gr.Blocks(css=LIGHT_CSS, theme=gr.themes.Soft()) as demo:
2813
 
2814
 
2815
  if __name__ == "__main__":
2816
- demo.launch()
 
537
  f"Input: {q!r}"
538
  )
539
  resp = GENAI_CLIENT.models.generate_content(
540
+ model="gemini-2.5-flash",
541
  contents=prompt,
542
+ config=cfg,
543
  )
544
  txt = (resp.text or "").strip()
545
  if txt.startswith("```"):
 
570
  if GENAI_LEGACY:
571
  try:
572
  model = GENAI_LEGACY.GenerativeModel(
573
+ "gemini-2.5-flash",
574
+ tools={"google_search": {}}
575
  )
576
  prompt = (
577
  "Resolve a Clash Royale card string (typos possible). "
 
1483
  if GENAI_CLIENT:
1484
  try:
1485
  resp = GENAI_CLIENT.models.generate_content(
1486
+ model="gemini-2.5-flash",
1487
  contents=prompt
1488
  )
1489
  return (resp.text or "").strip()
 
1492
  # legacy
1493
  if GENAI_LEGACY:
1494
  try:
1495
+ out = GENAI_LEGACY.GenerativeModel("gemini-2.5-flash").generate_content(prompt)
1496
  return (out.text or "").strip()
1497
  except Exception as e:
1498
  _set_last_error(f"Gemini error (legacy): {e}")
1499
  return None
1500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1501
  def gem_coach(pack: dict, history=None, question=None):
1502
  """
1503
  Coach-style, trophy-range–aware analysis. ONLY uses data in pack.
 
1535
  "CRITICAL: Use ONLY the cards/decks present in the JSON. Do NOT invent cards or stats. "
1536
  "Respect owned_only/min_level constraints and keep suggested average elixir reasonable.\n\n"
1537
 
1538
+ # ========= START: PROMPT MODIFICATION (UPDATED CODE) ========= #
1539
  "**IMPORTANT: Use the `deck_card_details` section to understand the function of each card in the user's deck. This is your primary source of truth for what each card does.**\n\n"
1540
+ # ========= END: PROMPT MODIFICATION (UPDATED CODE) ========= #
1541
+
1542
  "Context you MUST use:\n"
1543
  "- player.trophies and bracket.\n"
1544
  "- trophy_range_meta.* are what the user is actually facing now.\n"
 
1572
  _set_last_error(f"Gemini parse error: {e}")
1573
  return None
1574
  else: # Follow-up question (compose a single prompt with context + short history)
1575
+ convo_lines = []
1576
+ # seed with initial analysis if available
 
 
 
 
 
 
1577
  if history and history[0] and history[0][1]:
1578
+ convo_lines.append("Initial analysis from coach:\n" + history[0][1])
1579
+ # add last 4 user/model turns for context
 
1580
  for user_msg, model_msg in history[-4:]:
1581
  if user_msg:
1582
  convo_lines.append("User: " + user_msg)
1583
+ if model_msg:
1584
  convo_lines.append("Coach: " + model_msg)
1585
+ convo_lines.append("\nUse ONLY data in this JSON pack for facts:\n" + json.dumps(pack, separators=(',',':')))
1586
+ convo_lines.append("\nUser question: " + (question or ""))
1587
 
1588
+ txt = _gemini_generate_text("\n\n".join(convo_lines))
 
 
 
1589
  return txt or "Sorry, I couldn't generate a response."
 
 
1590
 
1591
  # =========================
1592
  # Smart Builder (numeric core; semantics via Gemini)
 
1605
  player_info_for_pack=None,
1606
  micro_weight=0.7,
1607
  include_top=True,
1608
+ recent_matches_for_pack=None # New parameter
 
1609
  ):
1610
  deck = canonicalize_card_list(list(deck_cards or []))
1611
  if not deck:
1612
  return "Pick at least one card first.", None
1613
 
1614
  # ================================================================= #
1615
+ # ========= START: NEW CODE TO FETCH AND INJECT DETAILS (UPDATED CODE) ========= #
1616
  # ================================================================= #
1617
 
1618
  deck_card_details = []
 
1628
  })
1629
 
1630
  # =============================================================== #
1631
+ # ========= END: NEW CODE TO FETCH AND INJECT DETAILS (UPDATED CODE) ========= #
1632
  # =============================================================== #
1633
 
1634
 
 
1696
  existing = set(deck)
1697
  base_avg = avg_elixir(deck)
1698
  elixir_low, elixir_high = 3.0, 4.5
1699
+ pool = candidate_cards(STATE.inv, min_level=min_level, owned_only=owned_only)
 
1700
  suggestions_seed = []
1701
 
1702
  for c in pool:
 
1726
 
1727
  # Build LLM pack (facts only) with priority policy + local meta + hard counters
1728
  priority_policy = {"micro_weight": float(micro_weight), "include_top": bool(include_top)}
1729
+ pack = build_llm_pack(deck, df, ctx_notes, top_seed, STATE.inv, min_level, owned_only, coo, player_info_for_pack or {}, priority_policy, recent_matches=recent_matches_for_pack)
1730
 
1731
+ # Inject the details into the final pack object (UPDATED CODE)
1732
  pack["deck_card_details"] = deck_card_details
1733
 
1734
  lines = []
 
1809
  for r in ranked[:10]:
1810
  badges = " ".join(f"`{b}`" for b in (r.get("badges") or []))
1811
  reason = r.get('reason') or ""
1812
+ lines.append(f"- {r.get('card')}: {reason} {badges}")
1813
  edits = result.get("edits_for_meta") or []
1814
  if edits:
1815
  lines.append("\n**Edits to beat your local meta**")
 
1820
  if tech:
1821
  lines.append("\n**Tech choices**")
1822
  for r in tech[:6]:
1823
+ lines.append(f"- {r.get('card')}: {r.get('why')}")
1824
  qf = result.get("quick_fixes") or []
1825
  if qf:
1826
  lines.append("\n**Quick +1 fixes**")
1827
  for r in qf[:6]:
1828
+ lines.append(f"- {r.get('card')}: {r.get('reason')}")
1829
  if result.get("warnings"):
1830
  for w in result["warnings"]:
1831
  lines.append("⚠️ " + w)
 
1985
  for k in ("targetType","hitSpeed","dmgType"):
1986
  if k in ext: ext_bits.append(f"{k}={ext[k]}")
1987
  extra = (" | " + ", ".join(ext_bits)) if ext_bits else ""
1988
+ lines.append(f"- {nm}: roles={('/'.join(r) or '—')} | tags={('/'.join(sr) or '—')}{extra}")
1989
  if champs>1: lines.append(f"⚠️ Champions in deck: {champs} (most formats allow only 1)")
1990
  if evos>0: lines.append(f"Evolution-ready in deck: {evos}")
1991
  else:
 
2402
  "Tip: run **Fetch & Analyze** first to seed meta/co-occurrence for stronger suggestions."
2403
  )
2404
  use_gemini = gr.Checkbox(label="Explain & rank with Gemini", value=True)
 
 
2405
  with gr.Row():
2406
  micro_w_slider = gr.Slider(0.0, 1.0, value=0.7, step=0.05, label="Weight on trophy-range micro-meta")
2407
  include_top_chk = gr.Checkbox(label="Blend in top-players meta", value=True)
 
2409
  builder_md = gr.Markdown()
2410
 
2411
  with gr.Column(visible=False) as chat_interface:
2412
+ chatbot = gr.Chatbot(label="Follow-up Coach Chat", height=400, type="messages")
2413
+ chat_input = gr.Textbox(label="Ask a follow-up question", placeholder="e.g., What's a good replacement for my Knight?")
 
2414
 
2415
  # ================= Rankings =================
2416
  with gr.Tab("Rankings & Top Decks", id=1):
 
2464
  wr_plot2 = gr.Plot()
2465
 
2466
  gr.Markdown("---")
2467
+ gr.Markdown("### Card Lookup (handles typos + learns unknown cards)") # NEW
2468
  with gr.Row():
2469
+ lookup_tb = gr.Textbox(label="Card lookup", placeholder="e.g., suspious bush") # NEW
2470
+ lookup_btn = gr.Button("Lookup Card Info") # NEW
2471
+ lookup_md = gr.Markdown() # NEW
2472
 
2473
  # ================= Player & Forecast =================
2474
  with gr.Tab("Player & Forecast", id=4):
 
2551
  gem_tb.change(_set_gem, [gem_tb], [gem_info])
2552
 
2553
  # --- DECK BUILDER TAB ---
2554
+ def _from_tag_rich(tag, use_gem, micro_w, include_top):
 
2555
  if not tag:
2556
  return "Enter a player tag in the sidebar.", None, None, gr.update(visible=False), None
2557
  if not STATE.api_token:
 
2563
  p = api.player(t)
2564
  except Exception as e:
2565
  return f"Error: {e}", None, None, gr.update(visible=False), None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2566
 
2567
+ # NEW: Collect recent matches
 
2568
  recent_matches = collect_recent_matches_for_llm(api, t)
2569
 
2570
  header_lines = player_snapshot_lines(p, t)
 
2603
  use_bracket=True, bracket_center=p.get("trophies"),
2604
  player_info_for_pack=player_info_for_pack,
2605
  micro_weight=float(micro_w), include_top=bool(include_top),
2606
+ recent_matches_for_pack=recent_matches # Pass new data
 
 
2607
  )
2608
  final_md = "\n".join(header_lines + ["", *perf_lines, "", coach_block])
2609
 
2610
+ initial_messages = [
2611
+ {"role": "assistant", "content": "Analysis complete. Ask me anything about this deck!"}
2612
+ ]
2613
+ initial_pairs = [("", "Analysis complete. Ask me anything about this deck!")]
2614
 
2615
+ return final_md, pack, initial_messages, gr.update(visible=True), initial_pairs
2616
 
2617
+ def chat_response(message, messages, pack_state, coach_hist):
2618
+ # messages: [{"role":"user"|"assistant","content":"..."}]
2619
  if not pack_state:
2620
+ return messages + [{"role":"assistant","content":"Please analyze a deck first."}], coach_hist
 
2621
 
2622
+ # Call your coach with the pair-style history it expects
2623
+ reply = gem_coach(pack_state, history=coach_hist, question=message)
2624
+
2625
+ # Update UI messages
2626
+ new_messages = messages + [
2627
+ {"role":"user", "content": message},
2628
+ {"role":"assistant", "content": reply}
2629
+ ]
2630
 
2631
+ # Update your pair history
2632
+ new_pairs = coach_hist + [(message, reply)]
 
 
2633
 
2634
+ return new_messages, new_pairs
2635
 
 
2636
  build_from_tag_btn.click(
2637
  _from_tag_rich,
2638
+ [player_tag_state, use_gemini, micro_w_slider, include_top_chk],
2639
  [builder_md, builder_pack_state, chatbot, chat_interface, coach_history_state]
2640
  )
2641
 
 
2677
  # --- CARD STATS TAB ---
2678
  meta_btn.click(recompute_meta, [], [meta_df, meta_plot, wr_plot2])
2679
 
2680
+ # NEW: Card Lookup handler (grounded search + caching)
2681
  def _lookup_card_handler(q):
2682
  q = (q or "").strip()
2683
  if not q:
 
2687
  info = lookup_card_details(q)
2688
  if not info:
2689
  return f"Sorry, I couldn't find info for **{q}**."
2690
+ lines = [f"**{info['name']}**"]
2691
  if info.get("summary"):
2692
  lines.append(info["summary"])
2693
  # show basics if we have them
 
2698
  mel = info.get("maxEvolutionLevel", 0)
2699
  if mel: bits.append(f"Max Evolution Level: {mel}")
2700
  if bits:
2701
+ lines.append("_" + " • ".join(bits) + "_")
2702
  # citations
2703
  srcs = info.get("sources") or []
2704
  if srcs:
 
2755
 
2756
 
2757
  if __name__ == "__main__":
2758
+ demo.launch()