samiee2213 commited on
Commit
62e9a25
Β·
verified Β·
1 Parent(s): db09b16

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +864 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,866 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import requests
3
+ import time
4
+ import json
5
+ from datetime import datetime
6
+ import os
7
+ import base64
8
+ from typing import Dict, Any
9
+ import io
10
+ from PIL import Image
11
 
12
+ # Configuration - FIXED FOR HUGGING FACE DEPLOYMENT
13
+ BJJ_BACKEND_URL = os.getenv("BJJ_BACKEND_URL", "http://127.0.0.1:8000")
14
+
15
+ # Page configuration
16
+ st.set_page_config(
17
+ page_title="BJJ AI Coach - Pro Analysis",
18
+ page_icon="πŸ₯‹",
19
+ layout="wide",
20
+ initial_sidebar_state="expanded"
21
+ )
22
+
23
+ # Enhanced Custom CSS with animated loader
24
+ st.markdown("""
25
+ <style>
26
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&display=swap');
27
+
28
+ html, body, [class*="css"] {
29
+ font-family: 'Inter', sans-serif;
30
+ background-color: #0a0a0a;
31
+ }
32
+
33
+ .main-title {
34
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
35
+ -webkit-background-clip: text;
36
+ -webkit-text-fill-color: transparent;
37
+ font-size: 3.5rem;
38
+ font-weight: 800;
39
+ text-align: center;
40
+ margin-bottom: 0.5rem;
41
+ letter-spacing: -1px;
42
+ }
43
+
44
+ .subtitle {
45
+ text-align: center;
46
+ color: #888;
47
+ font-size: 1.2rem;
48
+ margin-bottom: 2rem;
49
+ font-weight: 300;
50
+ }
51
+
52
+ /* Animated Loader */
53
+ .analysis-loader-container {
54
+ display: flex;
55
+ flex-direction: column;
56
+ align-items: center;
57
+ justify-content: center;
58
+ padding: 60px 20px;
59
+ background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
60
+ border-radius: 20px;
61
+ margin: 40px 0;
62
+ border: 2px solid #667eea;
63
+ box-shadow: 0 10px 40px rgba(102, 126, 234, 0.3);
64
+ }
65
+
66
+ .spinner {
67
+ width: 120px;
68
+ height: 120px;
69
+ border: 8px solid #2a2a2a;
70
+ border-top: 8px solid #667eea;
71
+ border-right: 8px solid #764ba2;
72
+ border-radius: 50%;
73
+ animation: spin 1.5s linear infinite;
74
+ margin-bottom: 30px;
75
+ position: relative;
76
+ }
77
+
78
+ .spinner::before {
79
+ content: "πŸ₯‹";
80
+ position: absolute;
81
+ top: 50%;
82
+ left: 50%;
83
+ transform: translate(-50%, -50%);
84
+ font-size: 2.5rem;
85
+ }
86
+
87
+ @keyframes spin {
88
+ 0% { transform: rotate(0deg); }
89
+ 100% { transform: rotate(360deg); }
90
+ }
91
+
92
+ .loader-title {
93
+ color: #667eea;
94
+ font-size: 2rem;
95
+ font-weight: 800;
96
+ text-align: center;
97
+ margin-bottom: 15px;
98
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
99
+ -webkit-background-clip: text;
100
+ -webkit-text-fill-color: transparent;
101
+ }
102
+
103
+ .loader-stage {
104
+ color: #aaa;
105
+ font-size: 1.2rem;
106
+ text-align: center;
107
+ margin-bottom: 10px;
108
+ font-weight: 600;
109
+ animation: pulse 2s ease-in-out infinite;
110
+ }
111
+
112
+ @keyframes pulse {
113
+ 0%, 100% { opacity: 0.6; }
114
+ 50% { opacity: 1; }
115
+ }
116
+
117
+ .loader-details {
118
+ color: #666;
119
+ font-size: 0.9rem;
120
+ text-align: center;
121
+ font-family: 'Courier New', monospace;
122
+ }
123
+
124
+ .overall-performance {
125
+ background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
126
+ border-radius: 20px;
127
+ padding: 40px;
128
+ text-align: center;
129
+ margin: 30px 0;
130
+ border: 2px solid #333;
131
+ box-shadow: 0 10px 40px rgba(0,0,0,0.5);
132
+ }
133
+
134
+ .performance-score {
135
+ font-size: 5rem;
136
+ font-weight: 800;
137
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
138
+ -webkit-background-clip: text;
139
+ -webkit-text-fill-color: transparent;
140
+ margin: 20px 0;
141
+ }
142
+
143
+ .performance-label {
144
+ color: #aaa;
145
+ font-size: 1.5rem;
146
+ font-weight: 600;
147
+ text-transform: uppercase;
148
+ letter-spacing: 2px;
149
+ margin-top: 10px;
150
+ }
151
+
152
+ .grade-badge {
153
+ display: inline-block;
154
+ padding: 8px 20px;
155
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
156
+ border-radius: 12px;
157
+ color: white;
158
+ font-weight: 700;
159
+ font-size: 1.2rem;
160
+ margin: 5px;
161
+ }
162
+
163
+ .skill-card {
164
+ background: #1a1a1a;
165
+ border-radius: 15px;
166
+ padding: 25px;
167
+ margin: 10px 0;
168
+ border-left: 4px solid #667eea;
169
+ box-shadow: 0 4px 15px rgba(0,0,0,0.3);
170
+ }
171
+
172
+ .skill-label {
173
+ color: #aaa;
174
+ font-size: 0.9rem;
175
+ text-transform: uppercase;
176
+ font-weight: 600;
177
+ letter-spacing: 1px;
178
+ }
179
+
180
+ .skill-score {
181
+ font-size: 2.5rem;
182
+ font-weight: 800;
183
+ color: white;
184
+ margin: 10px 0;
185
+ }
186
+
187
+ .progress-bar-custom {
188
+ background: #2a2a2a;
189
+ border-radius: 10px;
190
+ height: 12px;
191
+ overflow: hidden;
192
+ margin-top: 10px;
193
+ }
194
+
195
+ .progress-fill {
196
+ height: 100%;
197
+ background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
198
+ border-radius: 10px;
199
+ transition: width 0.5s ease;
200
+ }
201
+
202
+ .strength-card {
203
+ background: linear-gradient(135deg, #1a4d2e 0%, #1e5f3a 100%);
204
+ border-radius: 12px;
205
+ padding: 20px;
206
+ margin: 10px 0;
207
+ border-left: 4px solid #38ef7d;
208
+ }
209
+
210
+ .weakness-card {
211
+ background: linear-gradient(135deg, #4d1a1a 0%, #5f1e1e 100%);
212
+ border-radius: 12px;
213
+ padding: 20px;
214
+ margin: 10px 0;
215
+ border-left: 4px solid #ff6a00;
216
+ }
217
+
218
+ .opportunity-card {
219
+ background: #1a1a2e;
220
+ border-radius: 12px;
221
+ padding: 20px;
222
+ margin: 10px 0;
223
+ border-left: 4px solid #e94560;
224
+ }
225
+
226
+ .timestamp-badge {
227
+ background: #667eea;
228
+ color: white;
229
+ padding: 4px 12px;
230
+ border-radius: 8px;
231
+ font-weight: 600;
232
+ font-family: 'Courier New', monospace;
233
+ font-size: 0.9rem;
234
+ }
235
+
236
+ .category-badge {
237
+ background: #764ba2;
238
+ color: white;
239
+ padding: 4px 10px;
240
+ border-radius: 6px;
241
+ font-size: 0.75rem;
242
+ font-weight: 600;
243
+ text-transform: uppercase;
244
+ margin-left: 10px;
245
+ }
246
+
247
+ .drill-card {
248
+ background: linear-gradient(135deg, #1a1a2e 0%, #2d2d40 100%);
249
+ border-radius: 12px;
250
+ padding: 25px;
251
+ margin: 15px 0;
252
+ border: 2px solid #333;
253
+ }
254
+
255
+ .drill-header {
256
+ color: #667eea;
257
+ font-size: 1.3rem;
258
+ font-weight: 700;
259
+ margin-bottom: 10px;
260
+ }
261
+
262
+ .drill-meta {
263
+ display: inline-block;
264
+ background: #2a2a3e;
265
+ padding: 6px 12px;
266
+ border-radius: 8px;
267
+ margin-right: 10px;
268
+ font-size: 0.85rem;
269
+ color: #aaa;
270
+ }
271
+
272
+ .coach-notes-section {
273
+ background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
274
+ border-radius: 15px;
275
+ padding: 30px;
276
+ margin: 30px 0;
277
+ border: 2px solid #667eea;
278
+ box-shadow: 0 8px 30px rgba(102, 126, 234, 0.2);
279
+ }
280
+
281
+ .stButton>button {
282
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
283
+ color: white;
284
+ border: none;
285
+ border-radius: 25px;
286
+ font-weight: 700;
287
+ padding: 15px 40px;
288
+ font-size: 1.1rem;
289
+ transition: all 0.3s ease;
290
+ }
291
+
292
+ .stButton>button:hover {
293
+ transform: translateY(-2px);
294
+ box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
295
+ }
296
+
297
+ .fallback-warning {
298
+ background: linear-gradient(135deg, #4d3d1a 0%, #5f4a1e 100%);
299
+ border-left: 4px solid #ffb800;
300
+ padding: 20px;
301
+ border-radius: 10px;
302
+ margin: 20px 0;
303
+ }
304
+
305
+ .connection-status {
306
+ background: #1a1a1a;
307
+ border-radius: 10px;
308
+ padding: 15px;
309
+ margin: 10px 0;
310
+ border-left: 4px solid #667eea;
311
+ }
312
+ </style>
313
+ """, unsafe_allow_html=True)
314
+
315
+ def check_backend_health(url: str) -> tuple[bool, str]:
316
+ """Check if backend is online and get version"""
317
+ try:
318
+ response = requests.get(f"{url}/health", timeout=10)
319
+ if response.status_code == 200:
320
+ data = response.json()
321
+ version = data.get("version", "Unknown")
322
+ return True, f"v{version} βœ…"
323
+ return False, f"Status {response.status_code} ❌"
324
+ except requests.exceptions.Timeout:
325
+ return False, "Timeout (>10s) ⏱️"
326
+ except requests.exceptions.ConnectionError:
327
+ return False, "Connection refused πŸ”Œ"
328
+ except Exception as e:
329
+ return False, f"Error: {str(e)[:50]}"
330
+
331
+ def render_animated_loader(stage: str, details: str, progress: int):
332
+ """Render animated loading screen with technical details"""
333
+
334
+ st.markdown(f"""
335
+ <div class="analysis-loader-container">
336
+ <div class="spinner"></div>
337
+ <div class="loader-title">BJJ AI Analyst Thinking...</div>
338
+ <div class="loader-stage">{stage}</div>
339
+ <div class="loader-details">{details}</div>
340
+ </div>
341
+ """, unsafe_allow_html=True)
342
+
343
+ def render_overall_performance(result: Dict[str, Any]):
344
+ """Render the overall performance card matching client UI"""
345
+ overall_score = result.get("overall_score", 0)
346
+ performance_label = result.get("performance_label", "PERFORMANCE")
347
+ grades = result.get("performance_grades", {})
348
+
349
+ st.markdown(f"""
350
+ <div class="overall-performance">
351
+ <div style="color: #aaa; font-size: 1rem; text-transform: uppercase; letter-spacing: 3px;">
352
+ OVERALL PERFORMANCE
353
+ </div>
354
+ <div class="performance-score">{overall_score}</div>
355
+ <div class="performance-label">{performance_label}</div>
356
+ <div style="margin-top: 30px;">
357
+ <span class="grade-badge">πŸ›‘οΈ DEFENSE: {grades.get('defense_grade', 'B')}</span>
358
+ <span class="grade-badge">βš”οΈ OFFENSE: {grades.get('offense_grade', 'B')}</span>
359
+ <span class="grade-badge">πŸ“ CONTROL: {grades.get('control_grade', 'B')}</span>
360
+ </div>
361
+ </div>
362
+ """, unsafe_allow_html=True)
363
+
364
+ def render_skill_breakdown(skills: Dict[str, int]):
365
+ """Render detailed skill breakdown with progress bars"""
366
+ st.markdown("### πŸ“Š SKILL BREAKDOWN")
367
+ st.markdown("<br>", unsafe_allow_html=True)
368
+
369
+ skill_labels = {
370
+ "offense": ("βš”οΈ", "OFFENSE"),
371
+ "defense": ("πŸ›‘οΈ", "DEFENSE"),
372
+ "guard": ("πŸ”’", "GUARD"),
373
+ "passing": ("🚢", "PASSING"),
374
+ "standup": ("🧍", "STANDUP")
375
+ }
376
+
377
+ for key, (icon, label) in skill_labels.items():
378
+ if key in skills:
379
+ value = skills[key]
380
+
381
+ if value >= 85:
382
+ color = "linear-gradient(90deg, #11998e 0%, #38ef7d 100%)"
383
+ elif value >= 70:
384
+ color = "linear-gradient(90deg, #667eea 0%, #764ba2 100%)"
385
+ else:
386
+ color = "linear-gradient(90deg, #ee0979 0%, #ff6a00 100%)"
387
+
388
+ st.markdown(f"""
389
+ <div class="skill-card">
390
+ <div class="skill-label">{icon} {label}</div>
391
+ <div style="display: flex; justify-content: space-between; align-items: center;">
392
+ <div class="skill-score">{value}%</div>
393
+ </div>
394
+ <div class="progress-bar-custom">
395
+ <div class="progress-fill" style="width: {value}%; background: {color};"></div>
396
+ </div>
397
+ </div>
398
+ """, unsafe_allow_html=True)
399
+
400
+ def render_strengths_weaknesses(strengths: list, weaknesses: list):
401
+ """Render strengths and weaknesses side by side"""
402
+ col1, col2 = st.columns(2)
403
+
404
+ with col1:
405
+ st.markdown("### βœ… STRENGTHS")
406
+ for i, strength in enumerate(strengths, 1):
407
+ st.markdown(f"""
408
+ <div class="strength-card">
409
+ <div style="color: #38ef7d; font-weight: 700; margin-bottom: 8px;">
410
+ βœ“ Strength {i}
411
+ </div>
412
+ <div style="color: #ddd;">
413
+ {strength}
414
+ </div>
415
+ </div>
416
+ """, unsafe_allow_html=True)
417
+
418
+ with col2:
419
+ st.markdown("### ⚠️ WEAKNESSES")
420
+ for i, weakness in enumerate(weaknesses, 1):
421
+ st.markdown(f"""
422
+ <div class="weakness-card">
423
+ <div style="color: #ff6a00; font-weight: 700; margin-bottom: 8px;">
424
+ βœ— Weakness {i}
425
+ </div>
426
+ <div style="color: #ddd;">
427
+ {weakness}
428
+ </div>
429
+ </div>
430
+ """, unsafe_allow_html=True)
431
+
432
+ def render_opportunities(opportunities: list):
433
+ if not opportunities:
434
+ return
435
+
436
+ st.markdown("### πŸ’‘ OPPORTUNITIES MISSED")
437
+ st.markdown("<br>", unsafe_allow_html=True)
438
+
439
+ for i, opp in enumerate(opportunities, 1):
440
+ timestamp = opp.get("time", "00:00")
441
+ title = opp.get("title", "Opportunity")
442
+ description = opp.get("description", "")
443
+ category = opp.get("category", "GENERAL")
444
+
445
+ st.markdown(f"""
446
+ <div class="opportunity-card">
447
+ <div style="display: flex; align-items: center; margin-bottom: 12px;">
448
+ <div style="background: #e94560; color: white; width: 35px; height: 35px;
449
+ border-radius: 50%; display: flex; align-items: center;
450
+ justify-content: center; font-weight: 800; margin-right: 15px;">
451
+ {i}
452
+ </div>
453
+ <span class="timestamp-badge">{timestamp}</span>
454
+ <span class="category-badge">{category}</span>
455
+ </div>
456
+ <div style="color: #e94560; font-weight: 700; font-size: 1.1rem; margin-bottom: 8px;">
457
+ {title}
458
+ </div>
459
+ <div style="color: #ccc;">
460
+ {description}
461
+ </div>
462
+ </div>
463
+ """, unsafe_allow_html=True)
464
+
465
+ if opp.get("frame_image"):
466
+ try:
467
+ st.image(
468
+ base64.b64decode(opp["frame_image"]),
469
+ caption=f"Frame @ {timestamp}",
470
+ use_container_width=True
471
+ )
472
+ except:
473
+ pass
474
+
475
+ def render_key_moments(moments: list):
476
+ if not moments:
477
+ return
478
+
479
+ st.markdown("### ⭐ KEY MOMENTS")
480
+ st.markdown("<br>", unsafe_allow_html=True)
481
+
482
+ for i, moment in enumerate(moments, 1):
483
+ timestamp = moment.get("time", "00:00")
484
+ title = moment.get("title", "Event")
485
+ description = moment.get("description", "")
486
+ category = moment.get("category", "GENERAL")
487
+
488
+ with st.expander(f"⏱️ {timestamp} - {title}", expanded=False):
489
+ st.markdown(f"""
490
+ <div style="padding: 10px;">
491
+ <span class="category-badge">{category}</span>
492
+ <div style="color: #ddd; margin-top: 15px; line-height: 1.6;">
493
+ {description}
494
+ </div>
495
+ </div>
496
+ """, unsafe_allow_html=True)
497
+
498
+ if moment.get("frame_image"):
499
+ try:
500
+ st.image(
501
+ base64.b64decode(moment["frame_image"]),
502
+ caption=f"Frame @ {timestamp}",
503
+ use_container_width=True
504
+ )
505
+ except:
506
+ pass
507
+
508
+ def render_coach_notes(notes: str):
509
+ """Render coach's insights section"""
510
+ st.markdown("""
511
+ <div class="coach-notes-section">
512
+ <div style="display: flex; align-items: center; margin-bottom: 20px;">
513
+ <div style="font-size: 2rem; margin-right: 15px;">πŸ§‘β€πŸ«</div>
514
+ <div style="font-size: 1.5rem; font-weight: 700; color: #667eea;">
515
+ COACH'S INSIGHTS
516
+ </div>
517
+ </div>
518
+ <div style="color: #ddd; line-height: 1.8; font-size: 1.05rem;">
519
+ """, unsafe_allow_html=True)
520
+
521
+ st.markdown(notes)
522
+
523
+ st.markdown("</div></div>", unsafe_allow_html=True)
524
+
525
+ def render_recommended_drills(drills: list):
526
+ """Render training drills with detailed information"""
527
+ st.markdown("### πŸŽ“ RECOMMENDED DRILLS")
528
+ st.markdown("<br>", unsafe_allow_html=True)
529
+
530
+ for i, drill in enumerate(drills, 1):
531
+ name = drill.get("name", "Training Drill")
532
+ focus = drill.get("focus_area", "General")
533
+ reason = drill.get("reason", "")
534
+ duration = drill.get("duration", "15 min/day")
535
+ frequency = drill.get("frequency", "5x/week")
536
+
537
+ st.markdown(f"""
538
+ <div class="drill-card">
539
+ <div class="drill-header">
540
+ {i}. {name}
541
+ </div>
542
+ <div style="margin: 15px 0;">
543
+ <span class="drill-meta">⏱️ {duration}</span>
544
+ <span class="drill-meta">πŸ“… {frequency}</span>
545
+ </div>
546
+ <div style="color: #aaa; font-size: 0.9rem; margin-bottom: 8px;">
547
+ <strong>Focus Area:</strong> {focus}
548
+ </div>
549
+ <div style="color: #ddd; line-height: 1.6;">
550
+ <strong>Why:</strong> {reason}
551
+ </div>
552
+ </div>
553
+ """, unsafe_allow_html=True)
554
+
555
+ def display_analysis_results(results: Dict[str, Any]):
556
+ """Main function to display all analysis results"""
557
+ if not results:
558
+ st.warning("No results to display")
559
+ return
560
+
561
+ used_fallback = results.get("used_fallback", False)
562
+ if used_fallback:
563
+ st.markdown("""
564
+ <div class="fallback-warning">
565
+ <div style="font-weight: 700; font-size: 1.1rem; margin-bottom: 10px;">
566
+ ⚠️ Analysis Used Fallback Mode
567
+ </div>
568
+ <div style="color: #ddd;">
569
+ The AI encountered processing difficulties. Results are based on general BJJ principles
570
+ and may be less specific than usual. For best results, ensure good lighting and clear footage.
571
+ </div>
572
+ </div>
573
+ """, unsafe_allow_html=True)
574
+
575
+ data = results.get("data", {})
576
+ if not data:
577
+ data = results
578
+
579
+ if "processing_time" in results:
580
+ st.caption(f"⚑ Analysis completed in {results['processing_time']}")
581
+
582
+ st.markdown("<br>", unsafe_allow_html=True)
583
+
584
+ render_overall_performance(data)
585
+ st.markdown("<br><br>", unsafe_allow_html=True)
586
+
587
+ if "skill_breakdown" in data:
588
+ render_skill_breakdown(data["skill_breakdown"])
589
+
590
+ st.markdown("<br><br>", unsafe_allow_html=True)
591
+
592
+ if "strengths" in data and "weaknesses" in data:
593
+ render_strengths_weaknesses(data["strengths"], data["weaknesses"])
594
+
595
+ st.markdown("<br><br>", unsafe_allow_html=True)
596
+
597
+ if "missed_opportunities" in data:
598
+ render_opportunities(data["missed_opportunities"])
599
+
600
+ st.markdown("<br><br>", unsafe_allow_html=True)
601
+
602
+ if "key_moments" in data:
603
+ render_key_moments(data["key_moments"])
604
+
605
+ st.markdown("<br><br>", unsafe_allow_html=True)
606
+
607
+ if "coach_notes" in data:
608
+ render_coach_notes(data["coach_notes"])
609
+
610
+ st.markdown("<br><br>", unsafe_allow_html=True)
611
+
612
+ if "recommended_drills" in data:
613
+ render_recommended_drills(data["recommended_drills"])
614
+
615
+ def main():
616
+ if "analysis_running" not in st.session_state:
617
+ st.session_state.analysis_running = False
618
+
619
+ if "analysis_result" not in st.session_state:
620
+ st.session_state.analysis_result = None
621
+
622
+ st.markdown('<h1 class="main-title">πŸ₯‹ BJJ AI COACH</h1>', unsafe_allow_html=True)
623
+ st.markdown('<p class="subtitle">Advanced Performance Analysis Powered by AI</p>', unsafe_allow_html=True)
624
+
625
+ with st.spinner("Checking backend connection..."):
626
+ online, status_msg = check_backend_health(BJJ_BACKEND_URL)
627
+
628
+ if not online:
629
+ st.error("⚠️ Backend Connection Failed")
630
+ st.markdown(f"""
631
+ <div class="connection-status">
632
+ <div style="color: #ff6a00; font-weight: 700; margin-bottom: 10px;">
633
+ Status: {status_msg}
634
+ </div>
635
+ <div style="color: #aaa; font-size: 0.9rem;">
636
+ Backend URL: <code>{BJJ_BACKEND_URL}</code>
637
+ </div>
638
+ </div>
639
+ """, unsafe_allow_html=True)
640
+
641
+ if st.button("πŸ”„ Retry Connection"):
642
+ st.rerun()
643
+
644
+ st.info("πŸ’‘ **Troubleshooting:**\n- Check if the backend is deployed\n- Verify the URL is correct\n- Check Hugging Face Space logs")
645
+ return
646
+ else:
647
+ st.success(f"βœ… Backend Online: {status_msg}")
648
+
649
+ st.markdown("---")
650
+
651
+ col1, col2 = st.columns([1.2, 1])
652
+
653
+ with col1:
654
+ st.markdown("### πŸ“Ή UPLOAD VIDEO")
655
+ uploaded_file = st.file_uploader(
656
+ "Select sparring footage",
657
+ type=["mp4", "avi", "mov", "mkv", "webm"],
658
+ help="Supported formats: MP4, AVI, MOV, MKV, WebM"
659
+ )
660
+
661
+ if uploaded_file:
662
+ st.success(f"βœ“ Loaded: {uploaded_file.name} ({uploaded_file.size / 1024 / 1024:.2f} MB)")
663
+
664
+ with col2:
665
+ st.markdown("### βš™οΈ MATCH CONFIGURATION")
666
+ user_desc = st.text_input(
667
+ "πŸ‘€ Your Description",
668
+ value="Player with dreadlocks",
669
+ help="Describe yourself (hair, gi color, appearance)"
670
+ )
671
+ opp_desc = st.text_input(
672
+ "πŸ₯Š Opponent Description",
673
+ value="Player with short hair",
674
+ help="Describe your opponent"
675
+ )
676
+ activity = st.selectbox(
677
+ "πŸ₯‹ Activity Type",
678
+ ["Brazilian Jiu-Jitsu", "No-Gi Grappling", "Judo", "Wrestling", "MMA Grappling"]
679
+ )
680
+
681
+ st.markdown("---")
682
+
683
+ if uploaded_file and online:
684
+ col1, col2, col3 = st.columns([1, 2, 1])
685
+ with col2:
686
+ analyze_button = st.button(
687
+ "πŸš€ START ANALYSIS",
688
+ type="primary",
689
+ use_container_width=True,
690
+ disabled=st.session_state.analysis_running
691
+ )
692
+
693
+ if analyze_button and not st.session_state.analysis_running:
694
+ st.session_state.analysis_running = True
695
+ st.session_state.analysis_result = None
696
+
697
+ # Create containers
698
+ loader_container = st.empty()
699
+
700
+ try:
701
+ # Stage 1: Upload
702
+ with loader_container:
703
+ render_animated_loader(
704
+ "Uploading Video...",
705
+ "Establishing secure connection to AI backend",
706
+ 15
707
+ )
708
+ time.sleep(0.8)
709
+
710
+ # Stage 2: Frame Extraction (Simulated for visual flow)
711
+ with loader_container:
712
+ render_animated_loader(
713
+ "Extracting Frames...",
714
+ "Weighted extraction: 40% from ending (submission detection)",
715
+ 30
716
+ )
717
+
718
+ # Stage 3: Analysis
719
+ with loader_container:
720
+ render_animated_loader(
721
+ "AI Analysis in Progress...",
722
+ "Gemini Vision: Frame-by-frame position detection | CrewAI: Technical analysis",
723
+ 55
724
+ )
725
+
726
+ # Prepare request
727
+ uploaded_file.seek(0) # Reset file pointer
728
+ files = {
729
+ "file": (uploaded_file.name, uploaded_file.getvalue(), uploaded_file.type)
730
+ }
731
+ data = {
732
+ "user_description": user_desc,
733
+ "opponent_description": opp_desc,
734
+ "activity_type": activity
735
+ }
736
+
737
+ with loader_container:
738
+ render_animated_loader(
739
+ "Submission Detection Active...",
740
+ "Analyzing: Leg locks | Chokes | Armbars | Tap detection",
741
+ 70
742
+ )
743
+
744
+ start_time = time.time()
745
+
746
+ response = requests.post(
747
+ f"{BJJ_BACKEND_URL}/analyze-complete",
748
+ files=files,
749
+ data=data,
750
+ timeout=180
751
+ )
752
+
753
+ end_time = time.time()
754
+ processing_duration = f"{end_time - start_time:.2f}s"
755
+
756
+ with loader_container:
757
+ render_animated_loader(
758
+ "Validating Results...",
759
+ "Generic feedback filtering | Frame attachment | Quality check",
760
+ 90
761
+ )
762
+
763
+ time.sleep(0.5)
764
+
765
+ if response.status_code == 200:
766
+ result = response.json()
767
+
768
+ if result.get("status") in ["completed", "completed_with_fallback"]:
769
+ with loader_container:
770
+ render_animated_loader(
771
+ "Complete!",
772
+ "Analysis ready for review",
773
+ 100
774
+ )
775
+
776
+ time.sleep(1)
777
+
778
+ # Clear loaders
779
+ loader_container.empty()
780
+
781
+ if "processing_time" not in result:
782
+ result["processing_time"] = processing_duration
783
+
784
+ st.markdown("---")
785
+
786
+ st.session_state.analysis_result = result
787
+
788
+ st.markdown("<br><br>", unsafe_allow_html=True)
789
+ col1, col2, col3 = st.columns([1, 2, 1])
790
+ with col2:
791
+ json_str = json.dumps(result.get("data", {}), indent=2)
792
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
793
+ st.download_button(
794
+ "πŸ“₯ Download Full Analysis (JSON)",
795
+ json_str,
796
+ file_name=f"bjj_analysis_{timestamp}.json",
797
+ mime="application/json",
798
+ use_container_width=True
799
+ )
800
+ else:
801
+ st.error(f"❌ Analysis failed: {result.get('error', 'Unknown error')}")
802
+
803
+ else:
804
+ st.error(f"❌ Server error {response.status_code}")
805
+ with st.expander("Show error details"):
806
+ st.code(response.text)
807
+
808
+ except requests.exceptions.Timeout:
809
+ st.error("⏱️ Request timed out (>3 min). Try a shorter video clip.")
810
+ except requests.exceptions.ConnectionError:
811
+ st.error("πŸ”Œ Connection error. Backend may be down.")
812
+ except Exception as e:
813
+ st.error(f"❌ Error: {str(e)}")
814
+ finally:
815
+ st.session_state.analysis_running = False
816
+ loader_container.empty()
817
+
818
+ if st.session_state.analysis_result:
819
+ st.markdown("---")
820
+ display_analysis_results(st.session_state.analysis_result)
821
+
822
+ with st.sidebar:
823
+ st.markdown("### βš™οΈ SYSTEM INFO")
824
+ st.markdown(f"""
825
+ <div class="connection-status">
826
+ <div style="color: #aaa; font-size: 0.85rem; margin-bottom: 5px;">Backend:</div>
827
+ <div style="color: #667eea; font-size: 0.9rem; word-break: break-all;">
828
+ {BJJ_BACKEND_URL}
829
+ </div>
830
+ </div>
831
+ """, unsafe_allow_html=True)
832
+
833
+ st.markdown("---")
834
+ st.markdown("### πŸ“– HOW IT WORKS")
835
+ st.markdown("""
836
+ 1. **Upload** your sparring video (15-90s)
837
+ 2. **Describe** who's who clearly
838
+ 3. **Wait** 30-50s for AI analysis
839
+ 4. **Review** detailed performance breakdown
840
+ 5. **Download** JSON for records
841
+ """)
842
+
843
+ st.markdown("---")
844
+ st.markdown("### πŸ’‘ BEST PRACTICES")
845
+ st.markdown("""
846
+ - βœ… Good lighting
847
+ - βœ… Side angle camera
848
+ - βœ… Both athletes in frame
849
+ - βœ… Clear athlete distinctions
850
+ - βœ… 15-60 second clips ideal
851
+ """)
852
+
853
+ st.markdown("---")
854
+ st.markdown("### 🎯 FEATURES")
855
+ st.markdown("""
856
+ - **Smart Frame Extraction**: 40% from ending
857
+ - **Submission Detection**: Tap recognition
858
+ - **Weighted Scoring**: Outcome-based
859
+ - **Frame Attachments**: Visual evidence
860
+ """)
861
+
862
+ st.markdown("---")
863
+ st.caption("Built with ❀️ for the BJJ community")
864
+
865
+ if __name__ == "__main__":
866
+ main()