yamilsteven commited on
Commit
407acbd
·
1 Parent(s): a226f50

Enhance Gradio UI and Card Display Logic

Browse files

This commit introduces several improvements and bug fixes to the Gradio application, enhancing user experience and display reliability.

Key changes include:

- **Multiple Submissions:** Resolved an issue preventing consecutive submissions without clearing state. `GradioEventHandler` is now instantiated per submission.
- **OTS Content Reset:** Ensured the One-True-Shot (OTS) content area resets correctly to its default state for new queries.
- **UI Card Rendering:** Improved the reliability of dynamic UI card rendering (players, games, teams) by adding explicit yield statements.
- **Session Persistence:** Modified the "Clear State" functionality to preserve Zep and Freeplay session IDs, allowing users to continue their session.
- **Card Display Priority:** Corrected the logic in `on_tool_end` to ensure game and team cards are displayed correctly, not just player cards.
- **Semi-Final Game Cards:** Implemented a dedicated display for "semi-final" game cards, utilizing the existing game card format.

These changes address issues with submission flow, UI state management, and card display logic, while also introducing a new feature for specific game event cards.

Stagelink: https://huggingface.co/spaces/yamilsteven/ifx-huge-league-app

Makefile CHANGED
@@ -31,7 +31,7 @@ clean-requirements:
31
 
32
  # Build the Docker image for the 'runtime' stage in api/Dockerfile, tagged as huge-ifx-api:prod
33
  build-prod:
34
- cd api && docker build -f Dockerfile --target runtime -t huge-ifx-api:prod .
35
 
36
  # Build the prod image and run it locally, mapping ports 7860 and 8000
37
  up-build-prod: build-prod
@@ -39,5 +39,5 @@ up-build-prod: build-prod
39
 
40
  # Push the prod image to GitHub Container Registry
41
  push-prod-ghcr:
42
- docker tag huge-ifx-api:prod ghcr.io/rbalch/huge-ifx-api:prod
43
- docker push ghcr.io/rbalch/huge-ifx-api:prod
 
31
 
32
  # Build the Docker image for the 'runtime' stage in api/Dockerfile, tagged as huge-ifx-api:prod
33
  build-prod:
34
+ cd api && docker build --platform linux/amd64 -f Dockerfile --target runtime -t huge-ifx-api:prod .
35
 
36
  # Build the prod image and run it locally, mapping ports 7860 and 8000
37
  up-build-prod: build-prod
 
39
 
40
  # Push the prod image to GitHub Container Registry
41
  push-prod-ghcr:
42
+ docker tag huge-ifx-api:prod ghcr.io/ylassohugeinc/ifx-huge-league-api:prod
43
+ docker push ghcr.io/ylassohugeinc/ifx-huge-league-api:prod
api/README.md CHANGED
@@ -109,7 +109,7 @@ To deploy:
109
  2. **Deployment Targets:**
110
  - GitHub: [aliss77777/IFX-sandbox](https://github.com/aliss77777/IFX-sandbox/tree/huge-league)
111
  - HuggingFace: [ryanbalch/IFX-huge-league](https://huggingface.co/spaces/ryanbalch/IFX-huge-league/tree/main)
112
- - Docker images: `ghcr.io/rbalch/huge-ifx-api:prod`
113
 
114
  3. **Deployment Steps:**
115
 
@@ -196,7 +196,7 @@ The following `make` commands are available for development, build, and deployme
196
  | `make extract-lock` | Extract the `poetry.lock` file from a built container to your local directory. **Note:** This is only needed if you've been deleting the lock file because build will not have access to local lock file. |
197
  | `make build-prod` | Build the Docker image for the `runtime` stage in `api/Dockerfile`, tagged as `huge-ifx-api:prod`. Used for production deploys. |
198
  | `make up-build-prod` | Build and run the production image locally, mapping ports 7860 and 8000, with `.env` and `DEV_MODE=true`. |
199
- | `make push-prod-ghcr` | Tag and push the production image to GitHub Container Registry at `ghcr.io/rbalch/huge-ifx-api:prod`. |
200
 
201
  **Typical workflow:**
202
  - Use `make build` and `make up` for local development.
 
109
  2. **Deployment Targets:**
110
  - GitHub: [aliss77777/IFX-sandbox](https://github.com/aliss77777/IFX-sandbox/tree/huge-league)
111
  - HuggingFace: [ryanbalch/IFX-huge-league](https://huggingface.co/spaces/ryanbalch/IFX-huge-league/tree/main)
112
+ - Docker images: `ghcr.io/ylassohugeinc/ifx-huge-league-api:prod`
113
 
114
  3. **Deployment Steps:**
115
 
 
196
  | `make extract-lock` | Extract the `poetry.lock` file from a built container to your local directory. **Note:** This is only needed if you've been deleting the lock file because build will not have access to local lock file. |
197
  | `make build-prod` | Build the Docker image for the `runtime` stage in `api/Dockerfile`, tagged as `huge-ifx-api:prod`. Used for production deploys. |
198
  | `make up-build-prod` | Build and run the production image locally, mapping ports 7860 and 8000, with `.env` and `DEV_MODE=true`. |
199
+ | `make push-prod-ghcr` | Tag and push the production image to GitHub Container Registry at `ghcr.io/ylassohugeinc/ifx-huge-league-api:prod`. |
200
 
201
  **Typical workflow:**
202
  - Use `make build` and `make up` for local development.
api/components_bugfix_README.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Technical Design Document: Gradio UI Enhancements & Card Display Logic
2
+
3
+ **Version:** 1.0
4
+ **Date:** 6/3/2025
5
+ **Author:** @ylasso
6
+
7
+ ## 1. Overview
8
+
9
+ This document outlines a series of enhancements and bug fixes applied to the Gradio-based application. The primary goals of these changes were to improve user experience by enabling seamless multiple submissions, ensuring correct UI state resets, enhancing the reliability of dynamic card displays (for players, games, teams), and introducing new features like session persistence on state clear and specific handling for semi-final game cards.
10
+
11
+ ## 2. Problems Addressed and Solutions Implemented
12
+
13
+ ### 2.1. Consecutive Submissions Requiring "Clear State"
14
+
15
+ * **Problem:** Users could not make a second successful submission without first clicking the "Clear State" button. The first submission would work, but subsequent ones would fail to produce the expected output or behavior.
16
+ * **Root Cause:** The `GradioEventHandler` instance, which manages a queue for streaming responses, was initialized globally once. Its internal queue was not reset between submissions. If a previous workflow ended by placing a `None` (end-of-stream marker) in the queue, subsequent submissions would immediately read this `None` and terminate prematurely.
17
+ * **Solution:**
18
+ * The `GradioEventHandler` instantiation was moved from a global scope within the Gradio Blocks definition to occur locally within the `submit` and `user_query_submit` event handler functions in `api/server_gradio.py`.
19
+ * This ensures that a fresh `GradioEventHandler` (and thus a new, empty queue) is created for each user submission, preventing interference from previous submission states.
20
+
21
+ ### 2.2. Persistent OTS (One-True-Shot) Content
22
+
23
+ * **Problem:** After a player card (or other specialized content in the "OTS" display area) was shown, it would remain visible even after a new, unrelated query was submitted. The OTS area was not resetting to its default landing state.
24
+ * **Root Cause:** The `ots_content` attribute in the `AppState` was not being explicitly reset at the beginning of a new submission.
25
+ * **Solution:**
26
+ * A constant `INITIAL_OTS_IMAGE_HTML` was defined in `api/server_gradio.py` holding the HTML for the default landing image.
27
+ * The `AppState` model's `ots_content` field now defaults to this initial image.
28
+ * Crucially, at the beginning of the `submit_helper` function (called by both submit actions), `state.ots_content` is explicitly reset to `ots_default.format(content=INITIAL_OTS_IMAGE_HTML)`. This ensures the OTS display area starts fresh with the landing image for every new query.
29
+
30
+ ### 2.3. Intermittent Failure to Render UI Cards (OTS Content)
31
+
32
+ * **Problem:** Even when a tool was correctly invoked (indicated by a diagnostic message), the corresponding UI card (e.g., player card, game card) would sometimes fail to render, and only a text response would be shown. This behavior was inconsistent.
33
+ * **Root Cause:** The Gradio UI might not have been updated with the new `state.ots_content` if the "ots" token from the `handler.queue` was the last meaningful token before the stream ended, or if no further text tokens followed to trigger another `yield`. The state update with the card HTML might have been "missed."
34
+ * **Solution:**
35
+ * In the `submit_helper` function in `api/server_gradio.py`, an explicit `yield state, result` was added immediately after an "ots" type token is processed and `state.ots_content` is updated. This forces Gradio to update with the card content as soon as it's ready.
36
+ * An additional final `yield state, result` was added at the very end of the `submit_helper` function, after the main processing loop, to ensure the absolute final state (including any last-minute `ots_content` or the complete `result`) is always rendered.
37
+
38
+ ### 2.4. Session ID Reset on "Clear State"
39
+
40
+ * **Problem:** Clicking the "Clear State" button would reset all application state, including Zep and Freeplay session IDs. This meant subsequent interactions would start entirely new sessions, losing context from the prior interaction if the user only intended to clear the current query and history.
41
+ * **Desired Behavior:** Preserve Zep and Freeplay session IDs when "Clear State" is used, allowing the user to continue the same underlying session while clearing the immediate UI and chat history.
42
+ * **Root Cause:** The `clear_state` function created a completely new `AppState()` instance, which initializes session IDs to empty strings by default.
43
+ * **Solution:**
44
+ * The `clear_state` function in `api/server_gradio.py` was modified to accept the current `state` (as `current_app_state: AppState`) as an input.
45
+ * Before creating the `new_state`, it now extracts `existing_zep_session_id` and `existing_freeplay_session_id` from `current_app_state`.
46
+ * These preserved session IDs are then passed explicitly when instantiating the `new_state = AppState(zep_session_id=..., freeplay_session_id=...)`. Other fields are reset to their defaults.
47
+
48
+ ### 2.5. Game and Team Cards Not Displaying (Player Card Priority Issue)
49
+
50
+ * **Problem:** Only player cards were being displayed. Queries that should have resulted in game cards or team cards were not showing these specific UIs.
51
+ * **Root Cause:** Within the `on_tool_end` method of `api/event_handlers/gradio_handler.py`, the logic for displaying player cards was checked first. If a player card was generated, the method would `return` immediately, preventing the subsequent `if/elif` blocks for game cards and team cards from being evaluated.
52
+ * **Solution:**
53
+ * The conditional logic in `on_tool_end` was restructured to prioritize more specific card types. The new order of checks is:
54
+ 1. (New) Specific check for "Semi-Final" game cards (see section 3.1).
55
+ 2. Generic Game Cards (based on `output[0].metadata.get('type') == 'event'`).
56
+ 3. Team Cards (based on `output[0].metadata.get("show_team_card")`).
57
+ 4. Player Cards (iterating `output` for `doc.metadata.get("show_profile_card")`).
58
+ * Each primary card-type block still `return`s after successfully queuing its card via `self.ots_box()`, assuming only one card is displayed per tool end.
59
+
60
+ ## 3. New Features
61
+
62
+ ### 3.1. Dedicated Semi-Final Game Card Display
63
+
64
+ * **Requirement:** Provide a distinct card display for queries related to "semi-finals," similar to the existing game card.
65
+ * **Implementation:**
66
+ * An `if` condition was added at the beginning of the `on_tool_end` method in `api/event_handlers/gradio_handler.py`.
67
+ * This condition specifically checks if `output[0].metadata.get('game_name')` (case-insensitive) contains the string "semi-final".
68
+ * If true, the existing game card generation logic (`game_card_html` and associated data extraction) is used to render the semi-final information.
69
+ * This ensures that semi-final games are explicitly identified and displayed using the game card format.
70
+ * **Dependency:** This relies on the upstream tool providing `game_name` in `output[0].metadata` for semi-final related queries.
71
+
72
+ ## 4. Affected Files
73
+
74
+ * `api/server_gradio.py`:
75
+ * Modified `submit` and `user_query_submit` event handlers.
76
+ * Modified `submit_helper` function.
77
+ * Modified `clear_state` function and its Gradio registration.
78
+ * Added `INITIAL_OTS_IMAGE_HTML` constant and updated `AppState` default for `ots_content`.
79
+ * `api/event_handlers/gradio_handler.py`:
80
+ * Significantly refactored the `on_tool_end` method's conditional logic for card display.
api/event_handlers/gradio_handler.py CHANGED
@@ -115,52 +115,86 @@ class GradioEventHandler(AsyncCallbackHandler):
115
 
116
  async def on_tool_end(self, output: any, **kwargs):
117
  print(f"\n{Fore.CYAN}[TOOL END] {output}{Style.RESET_ALL}")
118
- for doc in output:
119
- if doc.metadata.get("show_profile_card"):
120
- raw_player_name = doc.metadata.get("name", "unknown-player")
121
- player_name = "Unknown Player"
122
- if raw_player_name != "unknown-player":
123
- parts = raw_player_name.split('-')
124
- if len(parts) == 2:
125
- first_name = parts[0].capitalize()
126
- last_name = parts[1].capitalize()
127
- player_name = f"{last_name} {first_name}"
128
- else:
129
- player_name = raw_player_name.replace('-', ' ').title()
130
- else:
131
- player_name = "Unknown Player"
132
 
133
- team_slug = doc.metadata.get("team", "unknown-team")
134
- formatted_team_name = team_slug.replace('-', ' ').title()
135
- if team_slug == "unknown-team":
136
- formatted_team_name = "Unknown Team"
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
- player_number = doc.metadata.get("number", "N/A")
139
- height = doc.metadata.get("height", "175")
140
- weight = doc.metadata.get("weight", "150")
141
- instagram_url = doc.metadata.get("instagram_url", raw_player_name)
142
-
143
- img = image_base.format(
144
- filename=self.get_image_filename(doc),
145
- player_name=player_name,
146
- team_name=formatted_team_name,
147
- player_number=player_number,
148
- height=height,
149
- weight=weight,
150
- instagram_url=instagram_url
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  )
152
- self.ots_box(img)
153
  return
 
 
154
 
155
- if output and isinstance(output[0], object) and hasattr(output[0], 'metadata') and output[0].metadata.get('type') == 'event':
 
156
  game_teams_set = set()
157
  game_events_details = []
158
  raw_game_name = "Unknown Game"
159
 
160
- for i, doc in enumerate(output):
161
  if hasattr(doc, 'metadata') and doc.metadata.get('team'):
162
  game_teams_set.add(doc.metadata.get('team'))
163
- if i == 0:
164
  raw_game_name = doc.metadata.get('game_name', raw_game_name)
165
 
166
  if hasattr(doc, 'metadata') and doc.metadata.get('event') == 'Goal':
@@ -169,14 +203,14 @@ class GradioEventHandler(AsyncCallbackHandler):
169
  event_team = doc.metadata.get('team', '')
170
  game_events_details.append(f"({event_minute}') {event_team}: {event_desc}")
171
 
172
- if len(game_teams_set) >= 1:
173
  team_list = list(game_teams_set)
174
  team1_name_str = team_list[0]
175
  team2_name_str = team_list[1] if len(team_list) > 1 else "(opponent not specified)"
176
 
177
  score_team1 = 0
178
  score_team2 = 0
179
- for doc in output:
180
  if hasattr(doc, 'metadata') and doc.metadata.get('event') == 'Goal':
181
  scoring_team = doc.metadata.get('team')
182
  if scoring_team == team1_name_str:
@@ -191,8 +225,7 @@ class GradioEventHandler(AsyncCallbackHandler):
191
  highlights_html_str += "</ul>"
192
  if not game_events_details:
193
  highlights_html_str = "<p style='font-size: 14px;'>No goal highlights available.</p>"
194
-
195
- log_msg = f"Game: {game_title_str} | {team1_name_str} {score_team1} - {score_team2} {team2_name_str}"
196
  team1_logo_filename = GradioEventHandler.get_team_logo_slug(team1_name_str)
197
  team2_logo_filename = GradioEventHandler.get_team_logo_slug(team2_name_str)
198
 
@@ -212,34 +245,76 @@ class GradioEventHandler(AsyncCallbackHandler):
212
  default_logo_url=default_logo_url
213
  )
214
  self.ots_box(formatted_game_html)
215
- return
216
  else:
217
- print(f"\n{Fore.RED}[TOOL END - GAME CARD] Not enough team data found in events.{Style.RESET_ALL}")
218
-
219
- elif isinstance(output, list) and output:
 
220
  doc_for_team_card = output[0]
221
- if hasattr(doc_for_team_card, 'metadata') and doc_for_team_card.metadata.get("show_team_card"):
222
- team_name_from_meta = doc_for_team_card.metadata.get("team_name", "Unknown Team")
223
- city_raw = doc_for_team_card.metadata.get("city", "N/A")
224
- display_team_name = str(team_name_from_meta).replace('-', ' ').replace('_', ' ').title()
225
- city_display = city_raw.title() if city_raw != "N/A" else "Location N/A"
226
- logo_filename = GradioEventHandler.get_team_logo_slug(team_name_from_meta)
227
- team_specific_logo_url = f"{TEAM_LOGO_BASE_URL}{logo_filename}"
228
- current_default_logo_url = f"{TEAM_LOGO_BASE_URL}default.png"
229
- team_id_slug = doc_for_team_card.metadata.get("team_id", "")
230
- team_page_url = f"https://www.team.com/{team_id_slug}" if team_id_slug else "#"
231
- team_page_cta_html = f'''<a href="{team_page_url}" target="_blank" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block; font-size: 14px; margin-top: 10px;">Visit Team Page</a>'''
232
-
233
- formatted_team_card_html = team_info_card_html.format(
234
- team_logo_url=team_specific_logo_url,
235
- team_display_name=display_team_name,
236
- city_display=city_display,
237
- default_logo_url=current_default_logo_url,
238
- team_page_cta_html=team_page_cta_html
239
- )
240
-
241
- self.ots_box(formatted_team_card_html)
242
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
 
244
  async def on_tool_start(self, input: any, *args, **kwargs):
245
  self.info_box(input.get("name", "[TOOL START]"))
 
115
 
116
  async def on_tool_end(self, output: any, **kwargs):
117
  print(f"\n{Fore.CYAN}[TOOL END] {output}{Style.RESET_ALL}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ # Check for Semi-Final Game Card first, based on game_name
120
+ if output and isinstance(output, list) and len(output) > 0 and \
121
+ isinstance(output[0], object) and hasattr(output[0], 'metadata') and \
122
+ isinstance(output[0].metadata.get('game_name'), str) and \
123
+ "semi-final" in output[0].metadata.get('game_name', '').lower():
124
+
125
+ # This is largely the same logic as the generic game card below
126
+ game_teams_set = set()
127
+ game_events_details = []
128
+ # Ensure raw_game_name uses the game_name from metadata if available
129
+ raw_game_name = output[0].metadata.get('game_name', "Unknown Semi-Final")
130
+
131
+ for i, doc in enumerate(output): # Iterate through all docs for game details
132
+ if hasattr(doc, 'metadata') and doc.metadata.get('team'):
133
+ game_teams_set.add(doc.metadata.get('team'))
134
+ # game_name is taken from output[0] already
135
 
136
+ if hasattr(doc, 'metadata') and doc.metadata.get('event') == 'Goal':
137
+ event_desc = doc.metadata.get('description', 'Goal scored')
138
+ event_minute = doc.metadata.get('minute', '')
139
+ event_team = doc.metadata.get('team', '')
140
+ game_events_details.append(f"({event_minute}') {event_team}: {event_desc}")
141
+
142
+ if len(game_teams_set) >= 1:
143
+ team_list = list(game_teams_set)
144
+ team1_name_str = team_list[0]
145
+ team2_name_str = team_list[1] if len(team_list) > 1 else "(opponent not specified)"
146
+
147
+ score_team1 = 0
148
+ score_team2 = 0
149
+ for doc in output:
150
+ if hasattr(doc, 'metadata') and doc.metadata.get('event') == 'Goal':
151
+ scoring_team = doc.metadata.get('team')
152
+ if scoring_team == team1_name_str:
153
+ score_team1 += 1
154
+ elif scoring_team == team2_name_str:
155
+ score_team2 += 1
156
+
157
+ game_title_str = raw_game_name.replace('_', ' ').title()
158
+ highlights_html_str = "<ul style='list-style-type: none; padding-left: 0; text-align: left;'>"
159
+ for highlight in game_events_details[:3]:
160
+ highlights_html_str += f"<li style='margin-bottom: 5px; font-size: 14px;'>{highlight}</li>"
161
+ highlights_html_str += "</ul>"
162
+ if not game_events_details:
163
+ highlights_html_str = "<p style='font-size: 14px;'>No goal highlights available.</p>"
164
+
165
+ team1_logo_filename = GradioEventHandler.get_team_logo_slug(team1_name_str)
166
+ team2_logo_filename = GradioEventHandler.get_team_logo_slug(team2_name_str)
167
+
168
+ team1_logo_url = f"{TEAM_LOGO_BASE_URL}{team1_logo_filename}"
169
+ team2_logo_url = f"{TEAM_LOGO_BASE_URL}{team2_logo_filename}"
170
+ default_logo_url = f"{TEAM_LOGO_BASE_URL}default.png"
171
+
172
+ formatted_game_html = game_card_html.format(
173
+ game_title=game_title_str,
174
+ team_home=team1_name_str,
175
+ team_away=team2_name_str,
176
+ team1_score=score_team1,
177
+ team2_score=score_team2,
178
+ highlights=highlights_html_str,
179
+ team1_logo_url=team1_logo_url,
180
+ team2_logo_url=team2_logo_url,
181
+ default_logo_url=default_logo_url
182
  )
183
+ self.ots_box(formatted_game_html)
184
  return
185
+ else:
186
+ print(f"\n{Fore.RED}[TOOL END - SEMI-FINAL CARD] Not enough team data or structure not as expected for {raw_game_name}.{Style.RESET_ALL}")
187
 
188
+ # Attempt to process Generic Game Card (e.g., type 'event') if not a Semi-Final
189
+ elif output and isinstance(output, list) and len(output) > 0 and isinstance(output[0], object) and hasattr(output[0], 'metadata') and output[0].metadata.get('type') == 'event':
190
  game_teams_set = set()
191
  game_events_details = []
192
  raw_game_name = "Unknown Game"
193
 
194
+ for i, doc in enumerate(output):
195
  if hasattr(doc, 'metadata') and doc.metadata.get('team'):
196
  game_teams_set.add(doc.metadata.get('team'))
197
+ if i == 0:
198
  raw_game_name = doc.metadata.get('game_name', raw_game_name)
199
 
200
  if hasattr(doc, 'metadata') and doc.metadata.get('event') == 'Goal':
 
203
  event_team = doc.metadata.get('team', '')
204
  game_events_details.append(f"({event_minute}') {event_team}: {event_desc}")
205
 
206
+ if len(game_teams_set) >= 1:
207
  team_list = list(game_teams_set)
208
  team1_name_str = team_list[0]
209
  team2_name_str = team_list[1] if len(team_list) > 1 else "(opponent not specified)"
210
 
211
  score_team1 = 0
212
  score_team2 = 0
213
+ for doc in output:
214
  if hasattr(doc, 'metadata') and doc.metadata.get('event') == 'Goal':
215
  scoring_team = doc.metadata.get('team')
216
  if scoring_team == team1_name_str:
 
225
  highlights_html_str += "</ul>"
226
  if not game_events_details:
227
  highlights_html_str = "<p style='font-size: 14px;'>No goal highlights available.</p>"
228
+
 
229
  team1_logo_filename = GradioEventHandler.get_team_logo_slug(team1_name_str)
230
  team2_logo_filename = GradioEventHandler.get_team_logo_slug(team2_name_str)
231
 
 
245
  default_logo_url=default_logo_url
246
  )
247
  self.ots_box(formatted_game_html)
248
+ return
249
  else:
250
+ print(f"\n{Fore.RED}[TOOL END - GAME CARD] Not enough team data found in events or output structure not as expected.{Style.RESET_ALL}")
251
+
252
+ # Attempt to process Team Card if no Game Card was processed
253
+ elif output and isinstance(output, list) and len(output) > 0 and isinstance(output[0], object) and hasattr(output[0], 'metadata') and output[0].metadata.get("show_team_card"):
254
  doc_for_team_card = output[0]
255
+ team_name_from_meta = doc_for_team_card.metadata.get("team_name", "Unknown Team")
256
+ city_raw = doc_for_team_card.metadata.get("city", "N/A")
257
+ display_team_name = str(team_name_from_meta).replace('-', ' ').replace('_', ' ').title()
258
+ city_display = city_raw.title() if city_raw != "N/A" else "Location N/A"
259
+ logo_filename = GradioEventHandler.get_team_logo_slug(team_name_from_meta)
260
+ team_specific_logo_url = f"{TEAM_LOGO_BASE_URL}{logo_filename}"
261
+ current_default_logo_url = f"{TEAM_LOGO_BASE_URL}default.png"
262
+ team_id_slug = doc_for_team_card.metadata.get("team_id", "")
263
+ team_page_url = f"https://www.team.com/{team_id_slug}" if team_id_slug else "#"
264
+ team_page_cta_html = f'''<a href="{team_page_url}" target="_blank" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block; font-size: 14px; margin-top: 10px;">Visit Team Page</a>'''
265
+
266
+ formatted_team_card_html = team_info_card_html.format(
267
+ team_logo_url=team_specific_logo_url,
268
+ team_display_name=display_team_name,
269
+ city_display=city_display,
270
+ default_logo_url=current_default_logo_url,
271
+ team_page_cta_html=team_page_cta_html
272
+ )
273
+ self.ots_box(formatted_team_card_html)
274
+ return
275
+
276
+ # Attempt to process Player Card if no Game or Team Card was processed
277
+ # This iterates through all docs in output, so it's more general
278
+ elif output and isinstance(output, list):
279
+ for doc in output:
280
+ if hasattr(doc, 'metadata') and doc.metadata.get("show_profile_card"):
281
+ raw_player_name = doc.metadata.get("name", "unknown-player")
282
+ player_name = "Unknown Player"
283
+ if raw_player_name != "unknown-player":
284
+ parts = raw_player_name.split('-')
285
+ if len(parts) == 2:
286
+ first_name = parts[0].capitalize()
287
+ last_name = parts[1].capitalize()
288
+ player_name = f"{last_name} {first_name}"
289
+ else:
290
+ player_name = raw_player_name.replace('-', ' ').title()
291
+ else:
292
+ player_name = "Unknown Player"
293
+
294
+ team_slug = doc.metadata.get("team", "unknown-team")
295
+ formatted_team_name = team_slug.replace('-', ' ').title()
296
+ if team_slug == "unknown-team":
297
+ formatted_team_name = "Unknown Team"
298
+
299
+ player_number = doc.metadata.get("number", "N/A")
300
+ height = doc.metadata.get("height", "175")
301
+ weight = doc.metadata.get("weight", "150")
302
+ instagram_url = doc.metadata.get("instagram_url", raw_player_name)
303
+
304
+ img = image_base.format(
305
+ filename=self.get_image_filename(doc),
306
+ player_name=player_name,
307
+ team_name=formatted_team_name,
308
+ player_number=player_number,
309
+ height=height,
310
+ weight=weight,
311
+ instagram_url=instagram_url
312
+ )
313
+ self.ots_box(img)
314
+ return # Return after the first player card is found
315
+
316
+ # Fallback if no specific card type was identified
317
+ print(f"\n{Fore.YELLOW}[TOOL END] No specific card type processed for OTS.{Style.RESET_ALL}")
318
 
319
  async def on_tool_start(self, input: any, *args, **kwargs):
320
  self.info_box(input.get("name", "[TOOL START]"))
api/server_gradio.py CHANGED
@@ -18,6 +18,12 @@ MESSAGE_TYPE_MAP = {
18
  "ai": AIMessage,
19
  # Add other message types as needed
20
  }
 
 
 
 
 
 
21
  ots_default = """
22
  <div style="display: flex; justify-content: center; align-items: center; width: 100%; max-width: 727px; height: 363px; margin: 0 auto;">
23
  {content}
@@ -34,12 +40,7 @@ class AppState(BaseModel):
34
  history: list = []
35
  zep_session_id: str = ""
36
  freeplay_session_id: str = ""
37
- ots_content: str = ots_default.format(content="""
38
- <img
39
- src="https://huggingface.co/spaces/ryanbalch/IFX-huge-league/resolve/main/assets/huge_landing.png"
40
- style="max-width: 100%; max-height: 100%; object-fit: contain; display: block; margin: 0 auto;"
41
- />
42
- """)
43
 
44
  def ensure_sessions(self):
45
  if not self.zep_session_id:
@@ -67,6 +68,7 @@ class AppState(BaseModel):
67
  ### Helpers ###
68
 
69
  def submit_helper(state, handler, user_query):
 
70
  state.count += 1
71
  state.ensure_sessions()
72
  message = HumanMessage(content=user_query)
@@ -117,17 +119,18 @@ def submit_helper(state, handler, user_query):
117
  print('OTS: ' + token["message"])
118
  state.ots_content = ots_default.format(content=token["message"])
119
  state = AppState(**state.model_dump())
 
120
  continue
121
  result += token
122
  yield state, result
123
 
124
  state.history.append(AIMessage(content=result))
 
125
 
126
  ### Interface ###
127
 
128
  with gr.Blocks(theme="soft") as demo:
129
  state = gr.State(AppState())
130
- handler = GradioEventHandler()
131
 
132
  gr.Markdown("# Huge League Soccer")
133
  with gr.Row():
@@ -207,21 +210,33 @@ with gr.Blocks(theme="soft") as demo:
207
  def state_change(state):
208
  return state.count, state.persona, state.zep_session_id, state.freeplay_session_id, "", state.ots_content
209
 
210
- @clear_state_btn.click(outputs=[state, llm_response, persona, user_query, email, first_name, last_name])
211
- def clear_state():
212
- new_state = AppState()
 
 
 
 
 
 
 
 
 
 
213
  return new_state, "", new_state.persona, "", new_state.email, new_state.first_name, new_state.last_name
214
 
215
  @submit_btn.click(inputs=[state, user_query], outputs=[state, llm_response])
216
  def submit(state, user_query):
217
  # user_query = user_query or "tell me about some players in everglade fc"
218
  user_query = user_query or "tell me about Ryan Martinez of everglade fc"
 
219
  yield from submit_helper(state, handler, user_query)
220
 
221
  @user_query.submit(inputs=[state, user_query], outputs=[state, llm_response])
222
  def user_query_change(state, user_query):
223
  # user_query = user_query or "tell me about some players in everglade fc"
224
  user_query = user_query or "tell me about Ryan Martinez of everglade fc"
 
225
  yield from submit_helper(state, handler, user_query)
226
 
227
  @persona.change(inputs=[persona, state], outputs=[persona_disp])
 
18
  "ai": AIMessage,
19
  # Add other message types as needed
20
  }
21
+ INITIAL_OTS_IMAGE_HTML = """
22
+ <img
23
+ src="https://huggingface.co/spaces/ryanbalch/IFX-huge-league/resolve/main/assets/huge_landing.png"
24
+ style="max-width: 100%; max-height: 100%; object-fit: contain; display: block; margin: 0 auto;"
25
+ />
26
+ """
27
  ots_default = """
28
  <div style="display: flex; justify-content: center; align-items: center; width: 100%; max-width: 727px; height: 363px; margin: 0 auto;">
29
  {content}
 
40
  history: list = []
41
  zep_session_id: str = ""
42
  freeplay_session_id: str = ""
43
+ ots_content: str = ots_default.format(content=INITIAL_OTS_IMAGE_HTML)
 
 
 
 
 
44
 
45
  def ensure_sessions(self):
46
  if not self.zep_session_id:
 
68
  ### Helpers ###
69
 
70
  def submit_helper(state, handler, user_query):
71
+ state.ots_content = ots_default.format(content=INITIAL_OTS_IMAGE_HTML)
72
  state.count += 1
73
  state.ensure_sessions()
74
  message = HumanMessage(content=user_query)
 
119
  print('OTS: ' + token["message"])
120
  state.ots_content = ots_default.format(content=token["message"])
121
  state = AppState(**state.model_dump())
122
+ yield state, result
123
  continue
124
  result += token
125
  yield state, result
126
 
127
  state.history.append(AIMessage(content=result))
128
+ yield state, result
129
 
130
  ### Interface ###
131
 
132
  with gr.Blocks(theme="soft") as demo:
133
  state = gr.State(AppState())
 
134
 
135
  gr.Markdown("# Huge League Soccer")
136
  with gr.Row():
 
210
  def state_change(state):
211
  return state.count, state.persona, state.zep_session_id, state.freeplay_session_id, "", state.ots_content
212
 
213
+ @clear_state_btn.click(inputs=[state], outputs=[state, llm_response, persona, user_query, email, first_name, last_name])
214
+ def clear_state(current_app_state: AppState):
215
+ # Preserve session IDs
216
+ existing_zep_session_id = current_app_state.zep_session_id
217
+ existing_freeplay_session_id = current_app_state.freeplay_session_id
218
+
219
+ # Create new AppState, passing preserved session IDs
220
+ # Other fields will get their default values from AppState definition
221
+ new_state = AppState(
222
+ zep_session_id=existing_zep_session_id,
223
+ freeplay_session_id=existing_freeplay_session_id
224
+ )
225
+ # The email, first_name, last_name, persona will be the defaults from new_state
226
  return new_state, "", new_state.persona, "", new_state.email, new_state.first_name, new_state.last_name
227
 
228
  @submit_btn.click(inputs=[state, user_query], outputs=[state, llm_response])
229
  def submit(state, user_query):
230
  # user_query = user_query or "tell me about some players in everglade fc"
231
  user_query = user_query or "tell me about Ryan Martinez of everglade fc"
232
+ handler = GradioEventHandler()
233
  yield from submit_helper(state, handler, user_query)
234
 
235
  @user_query.submit(inputs=[state, user_query], outputs=[state, llm_response])
236
  def user_query_change(state, user_query):
237
  # user_query = user_query or "tell me about some players in everglade fc"
238
  user_query = user_query or "tell me about Ryan Martinez of everglade fc"
239
+ handler = GradioEventHandler()
240
  yield from submit_helper(state, handler, user_query)
241
 
242
  @persona.change(inputs=[persona, state], outputs=[persona_disp])