MrSimple07 commited on
Commit
e530bd7
·
1 Parent(s): 40821e9
Files changed (1) hide show
  1. app.py +247 -101
app.py CHANGED
@@ -9,6 +9,8 @@ from collections import Counter, defaultdict
9
  import os
10
  import time
11
  import re
 
 
12
 
13
  GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY')
14
  genai.configure(api_key=GEMINI_API_KEY)
@@ -16,13 +18,16 @@ model = genai.GenerativeModel('gemini-2.0-flash-exp')
16
 
17
  def get_user_games_from_chess_com(username):
18
  try:
19
- username = username.strip().lower()
 
 
20
 
21
  user_url = f"https://api.chess.com/pub/player/{username}"
22
  response = requests.get(user_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
23
 
24
  if response.status_code != 200:
25
- return None, f"❌ Foydalanuvchi topilmadi: {username}. Chess.com'da mavjudligini tekshiring."
 
26
 
27
  archives_url = f"https://api.chess.com/pub/player/{username}/games/archives"
28
  response = requests.get(archives_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
@@ -80,6 +85,66 @@ def parse_pgn_content(pgn_content):
80
  break
81
  return games
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  def detect_opening(game):
84
  """Detect opening from ECO code or Opening header"""
85
  opening = game.headers.get("Opening", "")
@@ -88,23 +153,31 @@ def detect_opening(game):
88
  if opening:
89
  return opening
90
  elif eco:
91
- return f"ECO {eco}"
92
  else:
93
  return "Unknown Opening"
94
 
95
  def analyze_game_detailed(game, username):
96
  board = game.board()
97
  mistakes = []
 
 
 
 
 
 
 
98
  move_number = 0
99
 
100
- white_player = game.headers.get("White", "").lower()
101
- black_player = game.headers.get("Black", "").lower()
102
- username_lower = username.lower()
 
103
 
104
  user_color = None
105
- if username_lower in white_player:
106
  user_color = chess.WHITE
107
- elif username_lower in black_player:
108
  user_color = chess.BLACK
109
 
110
  result = game.headers.get("Result", "*")
@@ -170,68 +243,161 @@ def analyze_game_detailed(game, username):
170
 
171
  return {
172
  'mistakes': mistakes,
 
 
 
 
 
 
173
  'opening': opening,
174
  'result': result,
175
  'user_color': user_color,
176
  'user_result': user_result
177
  }
178
 
179
- def categorize_mistakes(all_mistakes):
180
- if not all_mistakes:
 
181
  return []
182
 
183
- types = []
184
- for m in all_mistakes:
185
- types.append(m['type'])
186
- types.append(m['phase'])
 
 
 
187
 
188
- counts = Counter(types)
189
 
190
- categories_map = {
191
- 'blunder': "Qo'pol xatolar",
192
- 'mistake': 'Kichik xatolar',
193
- 'hanging_piece': 'Himoyasiz qoldirish',
194
- 'opening_mistake': 'Debyut xatolari',
195
- 'middlegame_mistake': "O'rta o'yin xatolari",
196
- 'endgame_mistake': 'Endshpil xatolari'
197
- }
198
 
199
  weaknesses = []
200
- for mistake_type, count in counts.most_common(6):
201
- if mistake_type in categories_map:
202
- weaknesses.append({
203
- 'category': categories_map[mistake_type],
204
- 'count': count,
205
- 'percentage': (count / len(all_mistakes) * 100)
206
- })
207
-
208
- return weaknesses[:3]
209
-
210
- def fetch_lichess_puzzles(count=5):
211
- """Fetch real puzzles from Lichess API"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  puzzles = []
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  try:
215
- url = "https://lichess.org/api/puzzle/daily"
216
- response = requests.get(url, timeout=10)
 
 
 
 
 
 
217
 
218
  if response.status_code == 200:
219
- data = response.json()
220
- puzzle = data['puzzle']
221
- game = data['game']
222
-
223
- puzzles.append({
224
- 'id': puzzle['id'],
225
- 'fen': puzzle['initialPly']['fen'] if 'initialPly' in puzzle else game['fen'],
226
- 'moves': puzzle['solution'],
227
- 'rating': puzzle['rating'],
228
- 'themes': puzzle.get('themes', ['tactics'])
229
- })
230
- except:
231
- pass
232
-
233
- # If we couldn't fetch puzzles, return empty list
234
- return puzzles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
  def create_board_svg(fen, size=400):
237
  """Create SVG representation of chess position"""
@@ -292,12 +458,15 @@ MUHIM: Javobni FAQAT O'ZBEK TILIDA yozing! Aniq va amaliy maslahatlar bering."""
292
  return f"AI tahlil hozircha mavjud emas: {str(e)}"
293
 
294
  def analyze_games(username, pgn_file):
 
 
295
  if username:
296
- pgn_content, error = get_user_games_from_chess_com(username)
297
  if error:
298
  return error, "", "", "", None, None, None, None, None
 
299
  elif pgn_file:
300
- username = "Player"
301
  pgn_content = pgn_file.decode('utf-8') if isinstance(pgn_file, bytes) else pgn_file
302
  else:
303
  return "❌ Foydalanuvchi nomini kiriting yoki PGN faylni yuklang", "", "", "", None, None, None, None, None
@@ -307,7 +476,7 @@ def analyze_games(username, pgn_file):
307
  if not games:
308
  return "❌ O'yinlar topilmadi yoki tahlil qilinmadi", "", "", "", None, None, None, None, None
309
 
310
- all_mistakes = []
311
  opening_stats = defaultdict(lambda: {'wins': 0, 'losses': 0, 'draws': 0, 'total': 0})
312
  color_stats = {
313
  'white': {'wins': 0, 'losses': 0, 'draws': 0},
@@ -315,30 +484,30 @@ def analyze_games(username, pgn_file):
315
  }
316
 
317
  for game in games[:30]:
318
- analysis = analyze_game_detailed(game, username)
319
- all_mistakes.extend(analysis['mistakes'])
320
 
321
  opening = analysis['opening']
322
  user_result = analysis.get('user_result')
323
  user_color = analysis['user_color']
324
 
325
- opening_stats[opening]['total'] += 1
326
-
327
- # Fix: Only increment win/loss/draw if user_result is not None
328
- if user_result == 'win':
329
- opening_stats[opening]['wins'] += 1
330
- color_key = 'white' if user_color == chess.WHITE else 'black'
331
- color_stats[color_key]['wins'] += 1
332
- elif user_result == 'loss':
333
- opening_stats[opening]['losses'] += 1
334
- color_key = 'white' if user_color == chess.WHITE else 'black'
335
- color_stats[color_key]['losses'] += 1
336
- elif user_result == 'draw':
337
- opening_stats[opening]['draws'] += 1
338
- color_key = 'white' if user_color == chess.WHITE else 'black'
339
- color_stats[color_key]['draws'] += 1
340
-
341
- weaknesses = categorize_mistakes(all_mistakes)
342
 
343
  # Statistics Report
344
  stats_report = f"## 📊 {len(games)} ta o'yin tahlili\n\n"
@@ -388,8 +557,9 @@ def analyze_games(username, pgn_file):
388
  ai_analysis = get_comprehensive_analysis(weaknesses, opening_stats, color_stats, len(games))
389
  ai_report = f"## 🤖 AI Murabbiy: To'liq Tahlil va O'quv Rejasi\n\n{ai_analysis}"
390
 
391
- # Fetch puzzles
392
- puzzles = fetch_lichess_puzzles(5)
 
393
 
394
  # Fix: Format puzzle output as requested
395
  puzzle_text = "## 🧩 Sizning shaxsiy masalalaringiz\n\n"
@@ -416,30 +586,6 @@ def analyze_games(username, pgn_file):
416
  "",
417
  None
418
  )
419
-
420
- puzzle_svgs = []
421
- puzzle_info = []
422
-
423
- for i, puzzle in enumerate(puzzles):
424
- svg = create_board_svg(puzzle['fen'], size=300)
425
- puzzle_svgs.append(svg)
426
-
427
- themes = ", ".join(puzzle['themes'][:2]) if 'themes' in puzzle else "Taktika"
428
- info = f"**Masala {i+1}**: {themes.title()} (Reyting: {puzzle['rating']})"
429
- puzzle_info.append(info)
430
-
431
- return (
432
- full_report,
433
- ai_report,
434
- "## 🧩 Sizning shaxsiy masalalaringiz\n\nQuyidagi pozitsiyalarda eng yaxshi yurishni toping:",
435
- puzzle_info[0] if len(puzzle_info) > 0 else "",
436
- puzzle_svgs[0] if len(puzzle_svgs) > 0 else None,
437
- puzzle_info[1] if len(puzzle_info) > 1 else "",
438
- puzzle_svgs[1] if len(puzzle_svgs) > 1 else None,
439
- puzzle_info[2] if len(puzzle_info) > 2 else "",
440
- puzzle_svgs[2] if len(puzzle_svgs) > 2 else None
441
- )
442
-
443
  with gr.Blocks(title="Chess Study Plan Pro", theme=gr.themes.Soft()) as demo:
444
  gr.Markdown("""
445
  # ♟️ Professional Шахмат O'quv Rejasi
 
9
  import os
10
  import time
11
  import re
12
+ import chess.polyglot
13
+
14
 
15
  GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY')
16
  genai.configure(api_key=GEMINI_API_KEY)
 
18
 
19
  def get_user_games_from_chess_com(username):
20
  try:
21
+ # Keep original case but normalize for API
22
+ username_original = username.strip()
23
+ username = username_original.lower()
24
 
25
  user_url = f"https://api.chess.com/pub/player/{username}"
26
  response = requests.get(user_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
27
 
28
  if response.status_code != 200:
29
+ return None, None, f"❌ Foydalanuvchi topilmadi: {username}. Chess.com'da mavjudligini tekshiring."
30
+
31
 
32
  archives_url = f"https://api.chess.com/pub/player/{username}/games/archives"
33
  response = requests.get(archives_url, timeout=10, headers={'User-Agent': 'Mozilla/5.0'})
 
85
  break
86
  return games
87
 
88
+ def get_opening_name_from_eco(eco_code):
89
+ """Convert ECO code to opening name using chess library"""
90
+ eco_openings = {
91
+ 'A00': 'Uncommon Opening',
92
+ 'A01': "Nimzowitsch-Larsen Attack",
93
+ 'A02': "Bird's Opening",
94
+ 'A03': "Bird's Opening: Dutch Variation",
95
+ 'A04': 'Reti Opening',
96
+ 'A05': 'Reti Opening: 1...Nf6',
97
+ 'A06': 'Reti Opening: 2.b3',
98
+ 'A10': 'English Opening',
99
+ 'A15': 'English Opening: Anglo-Indian Defense',
100
+ 'A20': 'English Opening: 1...e5',
101
+ 'A30': 'English Opening: Symmetrical Variation',
102
+ 'A40': 'Queen Pawn Game',
103
+ 'A45': 'Indian Defense',
104
+ 'A46': 'Indian Defense: 2.Nf3',
105
+ 'A50': 'Indian Defense: Normal Variation',
106
+ 'B00': 'Uncommon King Pawn Opening',
107
+ 'B01': 'Scandinavian Defense',
108
+ 'B02': "Alekhine's Defense",
109
+ 'B03': "Alekhine's Defense: Four Pawns Attack",
110
+ 'B10': 'Caro-Kann Defense',
111
+ 'B12': 'Caro-Kann Defense: Advance Variation',
112
+ 'B20': 'Sicilian Defense',
113
+ 'B22': 'Sicilian Defense: Alapin Variation',
114
+ 'B23': 'Sicilian Defense: Closed',
115
+ 'B30': 'Sicilian Defense: 2...Nc6',
116
+ 'B40': 'Sicilian Defense: French Variation',
117
+ 'B50': 'Sicilian Defense: 2...d6',
118
+ 'B90': 'Sicilian Defense: Najdorf',
119
+ 'C00': 'French Defense',
120
+ 'C02': 'French Defense: Advance Variation',
121
+ 'C10': 'French Defense: Rubinstein Variation',
122
+ 'C20': 'King Pawn Game',
123
+ 'C30': "King's Gambit",
124
+ 'C40': "King's Knight Opening",
125
+ 'C41': 'Philidor Defense',
126
+ 'C42': 'Russian Game (Petrov Defense)',
127
+ 'C44': 'Scotch Game',
128
+ 'C50': 'Italian Game',
129
+ 'C60': 'Spanish Opening (Ruy Lopez)',
130
+ 'C65': 'Spanish Opening: Berlin Defense',
131
+ 'D00': 'Queen Pawn Game',
132
+ 'D02': 'Queen Pawn Game: 2.Nf3',
133
+ 'D10': 'Slav Defense',
134
+ 'D20': "Queen's Gambit Accepted",
135
+ 'D30': "Queen's Gambit Declined",
136
+ 'D50': "Queen's Gambit Declined: 4.Bg5",
137
+ 'E00': 'Indian Defense',
138
+ 'E10': 'Indian Defense: 3.Nf3',
139
+ 'E20': 'Nimzo-Indian Defense',
140
+ 'E30': 'Nimzo-Indian Defense: Leningrad Variation',
141
+ 'E60': "King's Indian Defense",
142
+ 'E70': "King's Indian Defense: Normal Variation",
143
+ 'E90': "King's Indian Defense: Orthodox Variation",
144
+ }
145
+
146
+ return eco_openings.get(eco_code, f"ECO {eco_code}")
147
+
148
  def detect_opening(game):
149
  """Detect opening from ECO code or Opening header"""
150
  opening = game.headers.get("Opening", "")
 
153
  if opening:
154
  return opening
155
  elif eco:
156
+ return get_opening_name_from_eco(eco)
157
  else:
158
  return "Unknown Opening"
159
 
160
  def analyze_game_detailed(game, username):
161
  board = game.board()
162
  mistakes = []
163
+ blunders = []
164
+ mistake_moves = []
165
+ hanging_pieces = []
166
+ opening_mistakes = []
167
+ middlegame_mistakes = []
168
+ endgame_mistakes = []
169
+
170
  move_number = 0
171
 
172
+ # Normalize all player names to lowercase
173
+ white_player = game.headers.get("White", "").strip().lower()
174
+ black_player = game.headers.get("Black", "").strip().lower()
175
+ username_lower = username.strip().lower()
176
 
177
  user_color = None
178
+ if username_lower == white_player:
179
  user_color = chess.WHITE
180
+ elif username_lower == black_player:
181
  user_color = chess.BLACK
182
 
183
  result = game.headers.get("Result", "*")
 
243
 
244
  return {
245
  'mistakes': mistakes,
246
+ 'blunders': blunders,
247
+ 'mistake_moves': mistake_moves,
248
+ 'hanging_pieces': hanging_pieces,
249
+ 'opening_mistakes': opening_mistakes,
250
+ 'middlegame_mistakes': middlegame_mistakes,
251
+ 'endgame_mistakes': endgame_mistakes,
252
  'opening': opening,
253
  'result': result,
254
  'user_color': user_color,
255
  'user_result': user_result
256
  }
257
 
258
+ def categorize_mistakes(all_analyses):
259
+ """Categorize all types of mistakes with proper counting"""
260
+ if not all_analyses:
261
  return []
262
 
263
+ # Count all mistake types
264
+ blunders = sum(len(a['blunders']) for a in all_analyses)
265
+ mistakes = sum(len(a['mistake_moves']) for a in all_analyses)
266
+ hanging = sum(len(a['hanging_pieces']) for a in all_analyses)
267
+ opening = sum(len(a['opening_mistakes']) for a in all_analyses)
268
+ middlegame = sum(len(a['middlegame_mistakes']) for a in all_analyses)
269
+ endgame = sum(len(a['endgame_mistakes']) for a in all_analyses)
270
 
271
+ total = blunders + mistakes + hanging + opening + middlegame + endgame
272
 
273
+ if total == 0:
274
+ return []
 
 
 
 
 
 
275
 
276
  weaknesses = []
277
+
278
+ if blunders > 0:
279
+ weaknesses.append({
280
+ 'category': "Qo'pol xatolar",
281
+ 'count': blunders,
282
+ 'percentage': (blunders / total * 100)
283
+ })
284
+
285
+ if mistakes > 0:
286
+ weaknesses.append({
287
+ 'category': 'Kichik xatolar',
288
+ 'count': mistakes,
289
+ 'percentage': (mistakes / total * 100)
290
+ })
291
+
292
+ if hanging > 0:
293
+ weaknesses.append({
294
+ 'category': 'Himoyasiz qoldirish',
295
+ 'count': hanging,
296
+ 'percentage': (hanging / total * 100)
297
+ })
298
+
299
+ if opening > 0:
300
+ weaknesses.append({
301
+ 'category': 'Debyut xatolari',
302
+ 'count': opening,
303
+ 'percentage': (opening / total * 100)
304
+ })
305
+
306
+ if middlegame > 0:
307
+ weaknesses.append({
308
+ 'category': "O'rta o'yin xatolari",
309
+ 'count': middlegame,
310
+ 'percentage': (middlegame / total * 100)
311
+ })
312
+
313
+ if endgame > 0:
314
+ weaknesses.append({
315
+ 'category': 'Endshpil xatolari',
316
+ 'count': endgame,
317
+ 'percentage': (endgame / total * 100)
318
+ })
319
+
320
+ # Sort by count descending
321
+ weaknesses.sort(key=lambda x: x['count'], reverse=True)
322
+
323
+ return weaknesses
324
+ def fetch_lichess_puzzles(themes, count=5):
325
+ """Fetch puzzles from Lichess based on detected weakness themes"""
326
  puzzles = []
327
 
328
+ # Map weakness categories to Lichess puzzle themes
329
+ theme_map = {
330
+ "Qo'pol xatolar": ['blunder', 'hangingPiece', 'sacrifice'],
331
+ 'Kichik xatolar': ['advantage', 'endgame', 'middlegame'],
332
+ 'Himoyasiz qoldirish': ['hangingPiece', 'attraction', 'exposedKing'],
333
+ 'Debyut xatolari': ['opening', 'short'],
334
+ "O'rta o'yin xatolari": ['middlegame', 'attackingF2F7', 'advancedPawn'],
335
+ 'Endshpil xatolari': ['endgame', 'queenEndgame', 'rookEndgame']
336
+ }
337
+
338
+ # Collect all relevant themes
339
+ lichess_themes = []
340
+ for theme_name in themes:
341
+ if theme_name in theme_map:
342
+ lichess_themes.extend(theme_map[theme_name])
343
+
344
+ # Remove duplicates
345
+ lichess_themes = list(set(lichess_themes))[:3]
346
+
347
  try:
348
+ # Use batch API to get multiple puzzles
349
+ angle = ','.join(lichess_themes) if lichess_themes else 'mix'
350
+ url = f"https://lichess.org/api/puzzle/batch/{angle}"
351
+
352
+ response = requests.get(url, timeout=15, headers={
353
+ 'User-Agent': 'Mozilla/5.0',
354
+ 'Accept': 'application/x-ndjson'
355
+ })
356
 
357
  if response.status_code == 200:
358
+ # Parse NDJSON response
359
+ lines = response.text.strip().split('\n')
360
+ for line in lines[:count]:
361
+ try:
362
+ puzzle_data = eval(line) # or use json.loads(line)
363
+ puzzle_game = puzzle_data['game']
364
+ puzzle_info = puzzle_data['puzzle']
365
+
366
+ puzzles.append({
367
+ 'id': puzzle_info['id'],
368
+ 'fen': puzzle_game.get('fen', ''),
369
+ 'moves': puzzle_game.get('pgn', '').split()[-1],
370
+ 'solution': puzzle_info.get('solution', []),
371
+ 'rating': puzzle_info.get('rating', 1500),
372
+ 'themes': puzzle_info.get('themes', []),
373
+ 'url': f"https://lichess.org/training/{puzzle_info['id']}"
374
+ })
375
+ except:
376
+ continue
377
+ except Exception as e:
378
+ print(f"Error fetching puzzles: {e}")
379
+
380
+ # Fallback if not enough puzzles
381
+ if len(puzzles) < count:
382
+ try:
383
+ # Try daily puzzle as backup
384
+ url = "https://lichess.org/api/puzzle/daily"
385
+ response = requests.get(url, timeout=10)
386
+ if response.status_code == 200:
387
+ daily = response.json()
388
+ puzzles.append({
389
+ 'id': daily['puzzle']['id'],
390
+ 'fen': daily['game'].get('fen', ''),
391
+ 'moves': daily['game'].get('pgn', '').split()[-1],
392
+ 'solution': daily['puzzle'].get('solution', []),
393
+ 'rating': daily['puzzle'].get('rating', 1500),
394
+ 'themes': daily['puzzle'].get('themes', []),
395
+ 'url': f"https://lichess.org/training/{daily['puzzle']['id']}"
396
+ })
397
+ except:
398
+ pass
399
+
400
+ return puzzles[:count]
401
 
402
  def create_board_svg(fen, size=400):
403
  """Create SVG representation of chess position"""
 
458
  return f"AI tahlil hozircha mavjud emas: {str(e)}"
459
 
460
  def analyze_games(username, pgn_file):
461
+ actual_username = username # Store for later use
462
+
463
  if username:
464
+ pgn_content, returned_username, error = get_user_games_from_chess_com(username)
465
  if error:
466
  return error, "", "", "", None, None, None, None, None
467
+ actual_username = returned_username
468
  elif pgn_file:
469
+ actual_username = "Player"
470
  pgn_content = pgn_file.decode('utf-8') if isinstance(pgn_file, bytes) else pgn_file
471
  else:
472
  return "❌ Foydalanuvchi nomini kiriting yoki PGN faylni yuklang", "", "", "", None, None, None, None, None
 
476
  if not games:
477
  return "❌ O'yinlar topilmadi yoki tahlil qilinmadi", "", "", "", None, None, None, None, None
478
 
479
+ all_analyses = []
480
  opening_stats = defaultdict(lambda: {'wins': 0, 'losses': 0, 'draws': 0, 'total': 0})
481
  color_stats = {
482
  'white': {'wins': 0, 'losses': 0, 'draws': 0},
 
484
  }
485
 
486
  for game in games[:30]:
487
+ analysis = analyze_game_detailed(game, actual_username)
488
+ all_analyses.append(analysis)
489
 
490
  opening = analysis['opening']
491
  user_result = analysis.get('user_result')
492
  user_color = analysis['user_color']
493
 
494
+ if user_color is not None:
495
+ opening_stats[opening]['total'] += 1
496
+
497
+ if user_result == 'win':
498
+ opening_stats[opening]['wins'] += 1
499
+ color_key = 'white' if user_color == chess.WHITE else 'black'
500
+ color_stats[color_key]['wins'] += 1
501
+ elif user_result == 'loss':
502
+ opening_stats[opening]['losses'] += 1
503
+ color_key = 'white' if user_color == chess.WHITE else 'black'
504
+ color_stats[color_key]['losses'] += 1
505
+ elif user_result == 'draw':
506
+ opening_stats[opening]['draws'] += 1
507
+ color_key = 'white' if user_color == chess.WHITE else 'black'
508
+ color_stats[color_key]['draws'] += 1
509
+
510
+ weaknesses = categorize_mistakes(all_analyses)
511
 
512
  # Statistics Report
513
  stats_report = f"## 📊 {len(games)} ta o'yin tahlili\n\n"
 
557
  ai_analysis = get_comprehensive_analysis(weaknesses, opening_stats, color_stats, len(games))
558
  ai_report = f"## 🤖 AI Murabbiy: To'liq Tahlil va O'quv Rejasi\n\n{ai_analysis}"
559
 
560
+ # Get puzzles based on top 3 weaknesses
561
+ weakness_themes = [w['category'] for w in weaknesses[:3]]
562
+ puzzles = fetch_lichess_puzzles(weakness_themes, count=5)
563
 
564
  # Fix: Format puzzle output as requested
565
  puzzle_text = "## 🧩 Sizning shaxsiy masalalaringiz\n\n"
 
586
  "",
587
  None
588
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
589
  with gr.Blocks(title="Chess Study Plan Pro", theme=gr.themes.Soft()) as demo:
590
  gr.Markdown("""
591
  # ♟️ Professional Шахмат O'quv Rejasi