amauricunha commited on
Commit
625c7c9
Β·
verified Β·
1 Parent(s): 6f713c1

Upload 6 files

Browse files
Files changed (6) hide show
  1. admin_module.py +562 -0
  2. app.py +17 -17
  3. content_curator.py +409 -0
  4. database.py +785 -0
  5. flask_app.py +1356 -0
  6. study_planner.py +460 -0
admin_module.py ADDED
@@ -0,0 +1,562 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # admin_module.py - Administrative interface for English Helper
2
+ import os
3
+ import json
4
+ import hashlib
5
+ from datetime import datetime, timedelta
6
+ from functools import wraps
7
+ from flask import session, request, jsonify, redirect, url_for
8
+ import sqlite3
9
+ from database import get_db_connection
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class AdminManager:
15
+ def __init__(self):
16
+ self.admin_credentials = self._load_admin_credentials()
17
+ self.token_costs = {
18
+ 'groq': {'input': 0.00000059, 'output': 0.00000079}, # per token
19
+ 'gemini': {'input': 0.00000125, 'output': 0.00000375} # per token
20
+ }
21
+
22
+ def _load_admin_credentials(self):
23
+ """Load admin credentials from environment variables (Hugging Face secrets)"""
24
+ try:
25
+ # Try to load from Hugging Face secrets format
26
+ admin_user = os.environ.get('ADMIN_USERNAME', 'admin')
27
+ admin_pass = os.environ.get('ADMIN_PASSWORD', 'admin123')
28
+
29
+ # For security, hash the password
30
+ admin_pass_hash = hashlib.sha256(admin_pass.encode()).hexdigest()
31
+
32
+ return {
33
+ 'username': admin_user,
34
+ 'password_hash': admin_pass_hash,
35
+ 'original_password': admin_pass # Store for initial comparison
36
+ }
37
+ except Exception as e:
38
+ logger.error(f"Error loading admin credentials: {e}")
39
+ # Fallback credentials
40
+ return {
41
+ 'username': 'admin',
42
+ 'password_hash': hashlib.sha256('admin123'.encode()).hexdigest(),
43
+ 'original_password': 'admin123'
44
+ }
45
+
46
+ def authenticate_admin(self, username, password):
47
+ """Authenticate admin user"""
48
+ try:
49
+ if username != self.admin_credentials['username']:
50
+ return False
51
+
52
+ # Check password hash
53
+ password_hash = hashlib.sha256(password.encode()).hexdigest()
54
+ return password_hash == self.admin_credentials['password_hash']
55
+ except Exception as e:
56
+ logger.error(f"Admin authentication error: {e}")
57
+ return False
58
+
59
+ def is_admin_logged_in(self):
60
+ """Check if admin is logged in"""
61
+ authenticated = session.get('admin_authenticated', False)
62
+ username = session.get('admin_username')
63
+ login_time = session.get('admin_login_time')
64
+
65
+ # Debug logging
66
+ logger.info(f"Admin auth check: authenticated={authenticated}, username={username}, login_time={login_time}")
67
+
68
+ # Check if session has expired (24 hours)
69
+ if authenticated and login_time:
70
+ try:
71
+ login_datetime = datetime.fromisoformat(login_time)
72
+ if datetime.now() - login_datetime > timedelta(hours=24):
73
+ logger.info("Admin session expired, logging out")
74
+ self.logout_admin()
75
+ return False
76
+ except Exception as e:
77
+ logger.error(f"Error checking session expiry: {e}")
78
+
79
+ return authenticated
80
+
81
+ def login_admin(self, username, password):
82
+ """Admin login"""
83
+ if self.authenticate_admin(username, password):
84
+ session['admin_authenticated'] = True
85
+ session['admin_username'] = username
86
+ session['admin_login_time'] = datetime.now().isoformat()
87
+ session.permanent = True # Make session permanent
88
+
89
+ logger.info(f"Admin login successful: {username}")
90
+ return True
91
+ else:
92
+ logger.warning(f"Admin login failed for username: {username}")
93
+ return False
94
+
95
+ def logout_admin(self):
96
+ """Admin logout"""
97
+ session.pop('admin_authenticated', None)
98
+ session.pop('admin_username', None)
99
+ session.pop('admin_login_time', None)
100
+
101
+ def get_system_stats(self):
102
+ """Get comprehensive system statistics"""
103
+ try:
104
+ conn = get_db_connection()
105
+ cursor = conn.cursor()
106
+
107
+ # User statistics
108
+ cursor.execute("SELECT COUNT(*) FROM users")
109
+ total_users = cursor.fetchone()[0]
110
+
111
+ cursor.execute("SELECT COUNT(*) FROM users WHERE created_at > datetime('now', '-7 days')")
112
+ new_users_week = cursor.fetchone()[0]
113
+
114
+ cursor.execute("SELECT COUNT(*) FROM users WHERE created_at > datetime('now', '-1 day')")
115
+ new_users_today = cursor.fetchone()[0]
116
+
117
+ # Activity statistics
118
+ cursor.execute("SELECT COUNT(*) FROM study_sessions")
119
+ total_sessions = cursor.fetchone()[0]
120
+
121
+ cursor.execute("SELECT COUNT(*) FROM flashcards")
122
+ total_flashcards = cursor.fetchone()[0]
123
+
124
+ cursor.execute("SELECT COUNT(*) FROM user_articles")
125
+ total_articles = cursor.fetchone()[0]
126
+
127
+ cursor.execute("SELECT COUNT(*) FROM study_plans")
128
+ total_study_plans = cursor.fetchone()[0]
129
+
130
+ # Token usage statistics
131
+ cursor.execute("SELECT SUM(tokens_used), COUNT(*) FROM token_usage")
132
+ token_stats = cursor.fetchone()
133
+ total_tokens = token_stats[0] if token_stats[0] else 0
134
+ total_api_calls = token_stats[1] if token_stats[1] else 0
135
+
136
+ # Calculate estimated costs
137
+ estimated_cost = self._calculate_estimated_cost(cursor)
138
+
139
+ # Recent activity
140
+ cursor.execute("""
141
+ SELECT u.email, s.created_at, s.activity_type
142
+ FROM study_sessions s
143
+ JOIN users u ON s.user_id = u.id
144
+ ORDER BY s.created_at DESC
145
+ LIMIT 10
146
+ """)
147
+ recent_activity = cursor.fetchall()
148
+
149
+ conn.close()
150
+
151
+ return {
152
+ 'users': {
153
+ 'total': total_users,
154
+ 'new_week': new_users_week,
155
+ 'new_today': new_users_today
156
+ },
157
+ 'activity': {
158
+ 'total_sessions': total_sessions,
159
+ 'total_flashcards': total_flashcards,
160
+ 'total_articles': total_articles,
161
+ 'total_study_plans': total_study_plans
162
+ },
163
+ 'api_usage': {
164
+ 'total_tokens': total_tokens,
165
+ 'total_calls': total_api_calls,
166
+ 'estimated_cost': estimated_cost
167
+ },
168
+ 'recent_activity': [
169
+ {
170
+ 'user': activity[0],
171
+ 'timestamp': activity[1],
172
+ 'activity': activity[2]
173
+ } for activity in recent_activity
174
+ ]
175
+ }
176
+ except Exception as e:
177
+ logger.error(f"Error getting system stats: {e}")
178
+ return {}
179
+
180
+ def _calculate_estimated_cost(self, cursor):
181
+ """Calculate estimated API costs"""
182
+ try:
183
+ cursor.execute("""
184
+ SELECT api_provider, SUM(input_tokens), SUM(output_tokens)
185
+ FROM token_usage
186
+ GROUP BY api_provider
187
+ """)
188
+ usage_by_provider = cursor.fetchall()
189
+
190
+ total_cost = 0
191
+ for provider, input_tokens, output_tokens in usage_by_provider:
192
+ if provider in self.token_costs:
193
+ costs = self.token_costs[provider]
194
+ total_cost += (input_tokens * costs['input']) + (output_tokens * costs['output'])
195
+
196
+ return round(total_cost, 4)
197
+ except:
198
+ return 0
199
+
200
+ def get_all_users(self, page=1, per_page=20):
201
+ """Get paginated list of all users"""
202
+ try:
203
+ conn = get_db_connection()
204
+ cursor = conn.cursor()
205
+
206
+ offset = (page - 1) * per_page
207
+
208
+ cursor.execute("""
209
+ SELECT u.id, u.email, u.created_at, u.email_confirmed, u.last_login,
210
+ COUNT(DISTINCT s.id) as session_count,
211
+ COUNT(DISTINCT f.id) as flashcard_count,
212
+ COUNT(DISTINCT a.id) as article_count
213
+ FROM users u
214
+ LEFT JOIN study_sessions s ON u.id = s.user_id
215
+ LEFT JOIN flashcards f ON u.id = f.user_id
216
+ LEFT JOIN user_articles a ON u.id = a.user_id
217
+ GROUP BY u.id
218
+ ORDER BY u.created_at DESC
219
+ LIMIT ? OFFSET ?
220
+ """, (per_page, offset))
221
+
222
+ users = cursor.fetchall()
223
+
224
+ # Get total count
225
+ cursor.execute("SELECT COUNT(*) FROM users")
226
+ total_users = cursor.fetchone()[0]
227
+
228
+ conn.close()
229
+
230
+ return {
231
+ 'users': [
232
+ {
233
+ 'id': user[0],
234
+ 'email': user[1],
235
+ 'created_at': user[2],
236
+ 'email_confirmed': bool(user[3]),
237
+ 'last_login': user[4],
238
+ 'session_count': user[5],
239
+ 'flashcard_count': user[6],
240
+ 'article_count': user[7]
241
+ } for user in users
242
+ ],
243
+ 'total': total_users,
244
+ 'page': page,
245
+ 'per_page': per_page,
246
+ 'total_pages': (total_users + per_page - 1) // per_page
247
+ }
248
+ except Exception as e:
249
+ logger.error(f"Error getting users: {e}")
250
+ return {'users': [], 'total': 0}
251
+
252
+ def delete_user(self, user_id):
253
+ """Delete a user and all associated data"""
254
+ try:
255
+ conn = get_db_connection()
256
+ cursor = conn.cursor()
257
+
258
+ # Delete in order to respect foreign key constraints
259
+ tables = [
260
+ 'study_plan_activities', 'study_plans', 'user_analytics',
261
+ 'content_recommendations', 'user_interests', 'user_articles',
262
+ 'study_sessions', 'flashcards', 'user_settings', 'users'
263
+ ]
264
+
265
+ for table in tables:
266
+ cursor.execute(f"DELETE FROM {table} WHERE user_id = ?", (user_id,))
267
+
268
+ conn.commit()
269
+ conn.close()
270
+
271
+ return True
272
+ except Exception as e:
273
+ logger.error(f"Error deleting user {user_id}: {e}")
274
+ return False
275
+
276
+ def get_user_details(self, user_id):
277
+ """Get detailed information about a specific user"""
278
+ try:
279
+ conn = get_db_connection()
280
+ cursor = conn.cursor()
281
+
282
+ # Basic user info
283
+ cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
284
+ user = cursor.fetchone()
285
+
286
+ if not user:
287
+ return None
288
+
289
+ # User settings
290
+ cursor.execute("SELECT * FROM user_settings WHERE user_id = ?", (user_id,))
291
+ settings = cursor.fetchone()
292
+
293
+ # Recent activity
294
+ cursor.execute("""
295
+ SELECT activity_type, created_at, duration_minutes
296
+ FROM study_sessions
297
+ WHERE user_id = ?
298
+ ORDER BY created_at DESC
299
+ LIMIT 20
300
+ """, (user_id,))
301
+ recent_sessions = cursor.fetchall()
302
+
303
+ # Token usage
304
+ cursor.execute("""
305
+ SELECT api_provider, SUM(input_tokens), SUM(output_tokens), COUNT(*)
306
+ FROM token_usage
307
+ WHERE user_id = ?
308
+ GROUP BY api_provider
309
+ """, (user_id,))
310
+ token_usage = cursor.fetchall()
311
+
312
+ conn.close()
313
+
314
+ return {
315
+ 'user': {
316
+ 'id': user[0],
317
+ 'email': user[1],
318
+ 'created_at': user[2],
319
+ 'email_confirmed': bool(user[3]),
320
+ 'last_login': user[4]
321
+ },
322
+ 'settings': dict(zip([col[0] for col in cursor.description], settings)) if settings else {},
323
+ 'recent_sessions': [
324
+ {
325
+ 'activity': session[0],
326
+ 'timestamp': session[1],
327
+ 'duration': session[2]
328
+ } for session in recent_sessions
329
+ ],
330
+ 'token_usage': [
331
+ {
332
+ 'provider': usage[0],
333
+ 'input_tokens': usage[1],
334
+ 'output_tokens': usage[2],
335
+ 'calls': usage[3]
336
+ } for usage in token_usage
337
+ ]
338
+ }
339
+ except Exception as e:
340
+ logger.error(f"Error getting user details for {user_id}: {e}")
341
+ return None
342
+
343
+ def record_token_usage(self, user_id, api_provider, input_tokens, output_tokens, operation_type):
344
+ """Record token usage for cost tracking"""
345
+ try:
346
+ conn = get_db_connection()
347
+ cursor = conn.cursor()
348
+
349
+ cursor.execute("""
350
+ INSERT INTO token_usage
351
+ (user_id, api_provider, input_tokens, output_tokens, operation_type, created_at)
352
+ VALUES (?, ?, ?, ?, ?, ?)
353
+ """, (user_id, api_provider, input_tokens, output_tokens, operation_type, datetime.now().isoformat()))
354
+
355
+ conn.commit()
356
+ conn.close()
357
+
358
+ return True
359
+ except Exception as e:
360
+ logger.error(f"Error recording token usage: {e}")
361
+ return False
362
+
363
+ def get_database_schema(self):
364
+ """Get database schema information"""
365
+ try:
366
+ conn = get_db_connection()
367
+ cursor = conn.cursor()
368
+
369
+ # Get all tables
370
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
371
+ tables = cursor.fetchall()
372
+
373
+ schema_info = {}
374
+ for table in tables:
375
+ table_name = table[0]
376
+
377
+ # Get table info
378
+ cursor.execute(f"PRAGMA table_info({table_name})")
379
+ columns = cursor.fetchall()
380
+
381
+ # Get row count
382
+ cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
383
+ row_count = cursor.fetchone()[0]
384
+
385
+ schema_info[table_name] = {
386
+ 'columns': [
387
+ {
388
+ 'name': col[1],
389
+ 'type': col[2],
390
+ 'not_null': bool(col[3]),
391
+ 'primary_key': bool(col[5])
392
+ } for col in columns
393
+ ],
394
+ 'row_count': row_count
395
+ }
396
+
397
+ conn.close()
398
+ return schema_info
399
+ except Exception as e:
400
+ logger.error(f"Error getting database schema: {e}")
401
+ return {}
402
+
403
+ def get_system_health(self):
404
+ """Get system health metrics"""
405
+ try:
406
+ import psutil
407
+ import os
408
+
409
+ # Memory usage
410
+ memory = psutil.virtual_memory()
411
+
412
+ # Disk usage
413
+ disk = psutil.disk_usage('/')
414
+
415
+ # Database size
416
+ db_path = 'data/englishhelper.db'
417
+ db_size = os.path.getsize(db_path) if os.path.exists(db_path) else 0
418
+
419
+ # Recent error logs (would implement proper logging)
420
+ recent_errors = self._get_recent_errors()
421
+
422
+ return {
423
+ 'memory': {
424
+ 'total': memory.total,
425
+ 'used': memory.used,
426
+ 'available': memory.available,
427
+ 'percent': memory.percent
428
+ },
429
+ 'disk': {
430
+ 'total': disk.total,
431
+ 'used': disk.used,
432
+ 'free': disk.free,
433
+ 'percent': disk.percent
434
+ },
435
+ 'database': {
436
+ 'size_bytes': db_size,
437
+ 'size_mb': round(db_size / 1024 / 1024, 2)
438
+ },
439
+ 'recent_errors': recent_errors,
440
+ 'uptime': self._get_uptime()
441
+ }
442
+ except Exception as e:
443
+ logger.error(f"Error getting system health: {e}")
444
+ return {}
445
+
446
+ def _get_recent_errors(self):
447
+ """Get recent error logs (simplified)"""
448
+ try:
449
+ # This would typically read from log files
450
+ # For now, return sample data
451
+ return [
452
+ {
453
+ 'timestamp': '2024-10-11 14:30:00',
454
+ 'level': 'ERROR',
455
+ 'message': 'API rate limit exceeded for user 123',
456
+ 'module': 'groq_client'
457
+ },
458
+ {
459
+ 'timestamp': '2024-10-11 13:45:00',
460
+ 'level': 'WARNING',
461
+ 'message': 'High memory usage detected',
462
+ 'module': 'system_monitor'
463
+ }
464
+ ]
465
+ except:
466
+ return []
467
+
468
+ def _get_uptime(self):
469
+ """Get system uptime"""
470
+ try:
471
+ import psutil
472
+ boot_time = psutil.boot_time()
473
+ uptime_seconds = datetime.now().timestamp() - boot_time
474
+
475
+ days = int(uptime_seconds // 86400)
476
+ hours = int((uptime_seconds % 86400) // 3600)
477
+ minutes = int((uptime_seconds % 3600) // 60)
478
+
479
+ return f"{days}d {hours}h {minutes}m"
480
+ except:
481
+ return "Unknown"
482
+
483
+ def check_system_alerts(self):
484
+ """Check for system alerts and warnings"""
485
+ alerts = []
486
+
487
+ try:
488
+ # Check token usage limits
489
+ conn = get_db_connection()
490
+ cursor = conn.cursor()
491
+
492
+ # Check daily token usage
493
+ cursor.execute("""
494
+ SELECT SUM(tokens_used)
495
+ FROM token_usage
496
+ WHERE date(created_at) = date('now')
497
+ """)
498
+ daily_tokens = cursor.fetchone()[0] or 0
499
+
500
+ if daily_tokens > 100000: # Alert threshold
501
+ alerts.append({
502
+ 'type': 'warning',
503
+ 'message': f'High daily token usage: {daily_tokens:,} tokens',
504
+ 'action': 'Monitor API costs'
505
+ })
506
+
507
+ # Check error rates
508
+ cursor.execute("""
509
+ SELECT COUNT(*) FROM token_usage
510
+ WHERE created_at > datetime('now', '-1 hour')
511
+ """)
512
+ hourly_requests = cursor.fetchone()[0] or 0
513
+
514
+ if hourly_requests > 500: # High load threshold
515
+ alerts.append({
516
+ 'type': 'info',
517
+ 'message': f'High API request rate: {hourly_requests} requests/hour',
518
+ 'action': 'Monitor performance'
519
+ })
520
+
521
+ # Check database size
522
+ health = self.get_system_health()
523
+ if health.get('database', {}).get('size_mb', 0) > 100: # 100MB threshold
524
+ alerts.append({
525
+ 'type': 'warning',
526
+ 'message': f'Large database size: {health["database"]["size_mb"]}MB',
527
+ 'action': 'Consider archiving old data'
528
+ })
529
+
530
+ conn.close()
531
+ return alerts
532
+
533
+ except Exception as e:
534
+ logger.error(f"Error checking system alerts: {e}")
535
+ return []
536
+
537
+ # Decorator for admin-only routes
538
+ def admin_required(f):
539
+ @wraps(f)
540
+ def decorated_function(*args, **kwargs):
541
+ is_authenticated = admin_manager.is_admin_logged_in()
542
+
543
+ # Enhanced logging for debugging
544
+ from flask import session, request
545
+ logger.info(f"Admin required check for {f.__name__}: authenticated={is_authenticated}")
546
+ logger.info(f"Session keys: {list(session.keys())}")
547
+ logger.info(f"Request URL: {request.url}")
548
+
549
+ if not is_authenticated:
550
+ logger.warning(f"Admin authentication failed for {f.__name__}")
551
+ return jsonify({
552
+ 'error': 'Admin authentication required',
553
+ 'authenticated': False,
554
+ 'endpoint': f.__name__
555
+ }), 401
556
+
557
+ logger.info(f"Admin access granted to {f.__name__}")
558
+ return f(*args, **kwargs)
559
+ return decorated_function
560
+
561
+ # Global admin manager instance
562
+ admin_manager = AdminManager()
app.py CHANGED
@@ -1,21 +1,20 @@
1
  #!/usr/bin/env python3
2
  """
3
- English Helper - HF Spaces Version (Simplified)
4
- Sistema de aprendizado de inglΓͺs sem autenticaΓ§Γ£o para mΓ‘xima compatibilidade
5
  """
6
 
7
  import os
8
- import json
9
- from datetime import datetime
10
 
11
- def init_simple_storage():
12
- """Inicializar armazenamento simples baseado em arquivos"""
13
  try:
14
- os.makedirs('hf_data', exist_ok=True)
15
- os.makedirs('hf_data/flashcards', exist_ok=True)
16
- os.makedirs('hf_data/conversations', exist_ok=True)
17
- os.makedirs('hf_data/analytics', exist_ok=True)
18
- print("βœ… Sistema de armazenamento HF inicializado")
19
  return True
20
  except Exception as e:
21
  print(f"❌ Erro na inicializaΓ§Γ£o: {e}")
@@ -23,18 +22,19 @@ def init_simple_storage():
23
 
24
  # Executar aplicaΓ§Γ£o
25
  if __name__ == "__main__":
26
- print("🎯 English Helper HF - Versão Simplificada")
27
 
28
  # Inicializar sistema
29
- if not init_simple_storage():
30
  print("❌ Falha na inicializaΓ§Γ£o")
31
  exit(1)
32
 
33
  try:
34
- # Importar Flask app simplificado
35
- import flask_app_hf
36
- app = flask_app_hf.app
37
 
 
38
  print("πŸš€ Executando Flask na porta 7860 (HF Spaces)")
39
  app.run(
40
  host="0.0.0.0",
@@ -47,4 +47,4 @@ if __name__ == "__main__":
47
  except Exception as e:
48
  print(f"❌ Erro: {e}")
49
  import traceback
50
- traceback.print_exc()
 
1
  #!/usr/bin/env python3
2
  """
3
+ English Helper - Hugging Face Spaces
4
+ Sistema completo de aprendizado de inglΓͺs com IA
5
  """
6
 
7
  import os
8
+ import threading
9
+ import time
10
 
11
+ def init_system():
12
+ """Inicializar sistema completo"""
13
  try:
14
+ os.environ.setdefault('FLASK_ENV', 'production')
15
+ from database import init_db
16
+ init_db()
17
+ print("βœ… Banco de dados inicializado")
 
18
  return True
19
  except Exception as e:
20
  print(f"❌ Erro na inicializaΓ§Γ£o: {e}")
 
22
 
23
  # Executar aplicaΓ§Γ£o
24
  if __name__ == "__main__":
25
+ print("🎯 English Helper - Iniciando...")
26
 
27
  # Inicializar sistema
28
+ if not init_system():
29
  print("❌ Falha na inicializaΓ§Γ£o")
30
  exit(1)
31
 
32
  try:
33
+ # Importar Flask app
34
+ import flask_app
35
+ app = flask_app.app
36
 
37
+ # Executar na porta correta para HF Spaces
38
  print("πŸš€ Executando Flask na porta 7860 (HF Spaces)")
39
  app.run(
40
  host="0.0.0.0",
 
47
  except Exception as e:
48
  print(f"❌ Erro: {e}")
49
  import traceback
50
+ traceback.print_exc()
content_curator.py ADDED
@@ -0,0 +1,409 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # content_curator.py
2
+ import requests
3
+ import json
4
+ import re
5
+ from datetime import datetime, timedelta
6
+ from urllib.parse import urlparse, urljoin
7
+ from bs4 import BeautifulSoup
8
+ import feedparser
9
+ import logging
10
+ from groq import Groq
11
+ import google.generativeai as genai
12
+ import os
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Initialize AI clients
17
+ groq_client = None
18
+ genai_client = None
19
+
20
+ try:
21
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
22
+ if GROQ_API_KEY:
23
+ groq_client = Groq(api_key=GROQ_API_KEY)
24
+ except Exception as e:
25
+ logger.warning(f"Groq client not available: {e}")
26
+
27
+ try:
28
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
29
+ if GEMINI_API_KEY:
30
+ genai.configure(api_key=GEMINI_API_KEY)
31
+ genai_client = genai
32
+ except Exception as e:
33
+ logger.warning(f"Gemini client not available: {e}")
34
+
35
+ class ContentCurator:
36
+ def __init__(self):
37
+ # Import here to avoid circular imports
38
+ self.track_tokens = None
39
+ try:
40
+ from admin_module import admin_manager
41
+ self.admin_manager = admin_manager
42
+ except ImportError:
43
+ self.admin_manager = None
44
+
45
+ def _track_token_usage(self, user_id, provider, input_tokens, output_tokens, operation):
46
+ """Track token usage for admin monitoring"""
47
+ try:
48
+ if self.admin_manager:
49
+ self.admin_manager.record_token_usage(user_id, provider, input_tokens, output_tokens, operation)
50
+ except Exception as e:
51
+ logger.error(f"Error tracking tokens: {e}")
52
+
53
+ self.search_engines = {
54
+ # Using free APIs and web scraping
55
+ 'news_sources': {
56
+ 'bbc': 'https://feeds.bbci.co.uk/news/rss.xml',
57
+ 'reuters': 'https://www.reuters.com/arcio/rss/',
58
+ 'techcrunch': 'https://techcrunch.com/feed/',
59
+ 'medium': 'https://medium.com/feed/tag/{topic}',
60
+ },
61
+ 'content_categories': {
62
+ 'technology': ['tech', 'software', 'AI', 'cybersecurity', 'automotive'],
63
+ 'business': ['business', 'management', 'leadership', 'finance'],
64
+ 'science': ['science', 'research', 'innovation'],
65
+ 'professional': ['career', 'professional-development', 'skills']
66
+ }
67
+ }
68
+
69
+ def search_content(self, interests, english_level, context_focus, limit=10):
70
+ """Search for content based on user interests and level"""
71
+ try:
72
+ results = []
73
+
74
+ for interest in interests:
75
+ # Search RSS feeds
76
+ rss_results = self._search_rss_feeds(interest, limit=3)
77
+ results.extend(rss_results)
78
+
79
+ # Search Medium articles
80
+ medium_results = self._search_medium(interest, limit=2)
81
+ results.extend(medium_results)
82
+
83
+ # Filter and rank results
84
+ filtered_results = self._filter_by_level_and_context(
85
+ results, english_level, context_focus
86
+ )
87
+
88
+ return filtered_results[:limit]
89
+
90
+ except Exception as e:
91
+ logger.error(f"Error searching content: {e}")
92
+ return []
93
+
94
+ def _search_rss_feeds(self, topic, limit=5):
95
+ """Search RSS feeds for relevant content"""
96
+ results = []
97
+
98
+ try:
99
+ # Map topic to appropriate RSS feeds
100
+ relevant_feeds = []
101
+
102
+ if any(keyword in topic.lower() for keyword in ['tech', 'software', 'cyber', 'adas', 'automotive']):
103
+ relevant_feeds.extend([
104
+ 'https://feeds.bbci.co.uk/news/technology/rss.xml',
105
+ 'https://techcrunch.com/feed/',
106
+ 'https://www.wired.com/feed/rss'
107
+ ])
108
+
109
+ if any(keyword in topic.lower() for keyword in ['business', 'management', 'product']):
110
+ relevant_feeds.extend([
111
+ 'https://feeds.bbci.co.uk/news/business/rss.xml',
112
+ 'https://feeds.harvard.edu/news/rss/business.xml'
113
+ ])
114
+
115
+ # Default to general news if no specific match
116
+ if not relevant_feeds:
117
+ relevant_feeds = ['https://feeds.bbci.co.uk/news/rss.xml']
118
+
119
+ for feed_url in relevant_feeds[:2]: # Limit to 2 feeds to avoid timeout
120
+ try:
121
+ feed = feedparser.parse(feed_url)
122
+
123
+ for entry in feed.entries[:limit]:
124
+ if self._is_relevant_to_topic(entry.title + " " + entry.get('summary', ''), topic):
125
+ results.append({
126
+ 'title': entry.title,
127
+ 'url': entry.link,
128
+ 'summary': entry.get('summary', '')[:200] + '...',
129
+ 'source': urlparse(feed_url).netloc,
130
+ 'published': entry.get('published', ''),
131
+ 'relevance_score': self._calculate_relevance(entry.title, topic)
132
+ })
133
+
134
+ except Exception as e:
135
+ logger.warning(f"Error parsing feed {feed_url}: {e}")
136
+ continue
137
+
138
+ except Exception as e:
139
+ logger.error(f"Error in RSS search: {e}")
140
+
141
+ return sorted(results, key=lambda x: x['relevance_score'], reverse=True)
142
+
143
+ def _search_medium(self, topic, limit=3):
144
+ """Search Medium articles (simplified approach)"""
145
+ results = []
146
+
147
+ try:
148
+ # Use Medium's RSS feed for topics
149
+ search_terms = topic.lower().replace(' ', '-')
150
+ medium_url = f"https://medium.com/feed/tag/{search_terms}"
151
+
152
+ feed = feedparser.parse(medium_url)
153
+
154
+ for entry in feed.entries[:limit]:
155
+ results.append({
156
+ 'title': entry.title,
157
+ 'url': entry.link,
158
+ 'summary': entry.get('summary', '')[:200] + '...',
159
+ 'source': 'Medium',
160
+ 'published': entry.get('published', ''),
161
+ 'relevance_score': self._calculate_relevance(entry.title, topic)
162
+ })
163
+
164
+ except Exception as e:
165
+ logger.warning(f"Error searching Medium: {e}")
166
+
167
+ return results
168
+
169
+ def _is_relevant_to_topic(self, text, topic):
170
+ """Check if text is relevant to topic"""
171
+ text_lower = text.lower()
172
+ topic_words = topic.lower().split()
173
+
174
+ # Simple relevance check
175
+ matches = sum(1 for word in topic_words if word in text_lower)
176
+ return matches >= len(topic_words) * 0.5 # At least 50% of topic words present
177
+
178
+ def _calculate_relevance(self, title, topic):
179
+ """Calculate relevance score between title and topic"""
180
+ title_lower = title.lower()
181
+ topic_lower = topic.lower()
182
+
183
+ # Simple scoring based on word matches
184
+ topic_words = topic_lower.split()
185
+ score = 0
186
+
187
+ for word in topic_words:
188
+ if word in title_lower:
189
+ score += 1
190
+
191
+ return score / len(topic_words) if topic_words else 0
192
+
193
+ def _filter_by_level_and_context(self, results, english_level, context_focus):
194
+ """Filter results by English level and context"""
195
+ # Level difficulty mapping
196
+ level_complexity = {
197
+ 'A1': 1, 'A2': 2, 'B1': 3, 'B2': 4, 'C1': 5, 'C2': 6
198
+ }
199
+
200
+ user_level = level_complexity.get(english_level, 3)
201
+
202
+ filtered = []
203
+ for result in results:
204
+ # Estimate content difficulty (simplified)
205
+ difficulty = self._estimate_content_difficulty(result['title'] + " " + result['summary'])
206
+
207
+ # Filter by level (allow content slightly above user level)
208
+ if difficulty <= user_level + 1:
209
+ result['estimated_difficulty'] = difficulty
210
+ filtered.append(result)
211
+
212
+ return filtered
213
+
214
+ def _estimate_content_difficulty(self, text):
215
+ """Estimate content difficulty (1-6 scale)"""
216
+ # Simple heuristics for difficulty estimation
217
+ word_count = len(text.split())
218
+ avg_word_length = sum(len(word) for word in text.split()) / word_count if word_count > 0 else 0
219
+
220
+ # Technical terms increase difficulty
221
+ technical_terms = ['algorithm', 'implementation', 'architecture', 'methodology', 'paradigm']
222
+ tech_score = sum(1 for term in technical_terms if term in text.lower())
223
+
224
+ # Calculate difficulty score
225
+ difficulty = 1
226
+ if avg_word_length > 6:
227
+ difficulty += 1
228
+ if tech_score > 0:
229
+ difficulty += 1
230
+ if word_count > 200:
231
+ difficulty += 1
232
+
233
+ return min(difficulty, 6)
234
+
235
+ def extract_content_from_url(self, url):
236
+ """Extract readable content from URL"""
237
+ try:
238
+ headers = {
239
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
240
+ }
241
+
242
+ response = requests.get(url, headers=headers, timeout=10)
243
+ response.raise_for_status()
244
+
245
+ soup = BeautifulSoup(response.content, 'html.parser')
246
+
247
+ # Remove unwanted elements
248
+ for element in soup(['script', 'style', 'nav', 'header', 'footer', 'aside']):
249
+ element.decompose()
250
+
251
+ # Extract main content
252
+ main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=re.compile(r'content|article|post'))
253
+
254
+ if main_content:
255
+ text = main_content.get_text(separator=' ', strip=True)
256
+ else:
257
+ # Fallback to body text
258
+ text = soup.get_text(separator=' ', strip=True)
259
+
260
+ # Clean up text
261
+ text = re.sub(r'\s+', ' ', text) # Multiple spaces to single
262
+ text = text[:5000] # Limit length
263
+
264
+ return {
265
+ 'success': True,
266
+ 'content': text,
267
+ 'title': soup.find('title').text if soup.find('title') else '',
268
+ 'word_count': len(text.split())
269
+ }
270
+
271
+ except Exception as e:
272
+ logger.error(f"Error extracting content from {url}: {e}")
273
+ return {
274
+ 'success': False,
275
+ 'error': str(e)
276
+ }
277
+
278
+ def generate_personalized_recommendations(self, user_interests, recent_articles, english_level, context_focus, user_id=None):
279
+ """Generate AI-powered content recommendations"""
280
+ try:
281
+ if not groq_client and not genai_client:
282
+ return []
283
+
284
+ # Prepare context for AI
285
+ interests_text = ', '.join(user_interests.keys())
286
+ recent_titles = [article.get('title', '') for article in recent_articles[-5:]]
287
+ recent_text = '; '.join(recent_titles)
288
+
289
+ prompt = f"""
290
+ User Profile:
291
+ - English Level: {english_level}
292
+ - Context Focus: {context_focus}
293
+ - Interests: {interests_text}
294
+ - Recently read: {recent_text}
295
+
296
+ Recommend 5 specific article topics or search terms that would be perfect for this user's English learning journey.
297
+ Consider their level and interests. Focus on practical, engaging content.
298
+
299
+ Format as JSON array: [
300
+ {{"topic": "topic name", "reason": "why this is good for the user", "difficulty": "estimated level"}},
301
+ ...
302
+ ]
303
+ """
304
+
305
+ # Try Groq first, then Gemini
306
+ response_text = None
307
+ if groq_client:
308
+ response = groq_client.chat.completions.create(
309
+ model="llama-3.1-8b-instant",
310
+ messages=[{"role": "user", "content": prompt}],
311
+ temperature=0.7
312
+ )
313
+ response_text = response.choices[0].message.content
314
+
315
+ # Track token usage
316
+ if user_id and hasattr(response, 'usage'):
317
+ self._track_token_usage(
318
+ user_id, 'groq',
319
+ response.usage.prompt_tokens,
320
+ response.usage.completion_tokens,
321
+ 'content_recommendations'
322
+ )
323
+ elif genai_client:
324
+ model = genai_client.GenerativeModel('gemini-2.5-flash-latest')
325
+ response = model.generate_content(prompt)
326
+ response_text = response.text
327
+
328
+ # Track token usage for Gemini (estimated)
329
+ if user_id:
330
+ # Estimate tokens (rough approximation: 1 token β‰ˆ 4 characters)
331
+ input_tokens = len(prompt) // 4
332
+ output_tokens = len(response_text) // 4
333
+ self._track_token_usage(
334
+ user_id, 'gemini',
335
+ input_tokens,
336
+ output_tokens,
337
+ 'content_recommendations'
338
+ )
339
+
340
+ if response_text:
341
+ # Extract JSON from response
342
+ json_match = re.search(r'\[.*\]', response_text, re.DOTALL)
343
+ if json_match:
344
+ recommendations = json.loads(json_match.group())
345
+ return recommendations
346
+
347
+ return []
348
+
349
+ except Exception as e:
350
+ logger.error(f"Error generating recommendations: {e}")
351
+ return []
352
+
353
+ def analyze_content_for_learning(self, content, user_level):
354
+ """Analyze content and suggest learning points"""
355
+ try:
356
+ if not groq_client and not genai_client:
357
+ return {}
358
+
359
+ # Truncate content for analysis
360
+ analysis_content = content[:2000] + "..." if len(content) > 2000 else content
361
+
362
+ prompt = f"""
363
+ Analyze this English text for a {user_level} level English learner:
364
+
365
+ "{analysis_content}"
366
+
367
+ Provide:
368
+ 1. Key vocabulary words (5-8 words) with definitions
369
+ 2. Important grammar patterns used
370
+ 3. Main topics/themes
371
+ 4. Difficulty assessment (1-10)
372
+ 5. Learning suggestions for this level
373
+
374
+ Format as JSON: {{
375
+ "vocabulary": [{{"word": "...", "definition": "..."}}, ...],
376
+ "grammar_patterns": ["pattern1", "pattern2", ...],
377
+ "topics": ["topic1", "topic2", ...],
378
+ "difficulty": 7,
379
+ "learning_suggestions": ["suggestion1", "suggestion2", ...]
380
+ }}
381
+ """
382
+
383
+ response_text = None
384
+ if groq_client:
385
+ response = groq_client.chat.completions.create(
386
+ model="llama-3.1-8b-instant",
387
+ messages=[{"role": "user", "content": prompt}],
388
+ temperature=0.3
389
+ )
390
+ response_text = response.choices[0].message.content
391
+ elif genai_client:
392
+ model = genai_client.GenerativeModel('gemini-2.5-flash-latest')
393
+ response = model.generate_content(prompt)
394
+ response_text = response.text
395
+
396
+ if response_text:
397
+ json_match = re.search(r'\{.*\}', response_text, re.DOTALL)
398
+ if json_match:
399
+ analysis = json.loads(json_match.group())
400
+ return analysis
401
+
402
+ return {}
403
+
404
+ except Exception as e:
405
+ logger.error(f"Error analyzing content: {e}")
406
+ return {}
407
+
408
+ # Global instance
409
+ content_curator = ContentCurator()
database.py ADDED
@@ -0,0 +1,785 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # database.py
2
+ import sqlite3
3
+ import os
4
+ import hashlib
5
+ import secrets
6
+ import smtplib
7
+ from email.mime.text import MIMEText
8
+ from email.mime.multipart import MIMEMultipart
9
+ from datetime import datetime, timedelta
10
+ from functools import wraps
11
+ from flask import g, session, jsonify, request
12
+ import logging
13
+
14
+ # Configure logging
15
+ logging.basicConfig(level=logging.INFO)
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Database configuration
19
+ DATABASE_PATH = 'users.db'
20
+
21
+ def get_db():
22
+ """Get database connection"""
23
+ if 'db' not in g:
24
+ g.db = sqlite3.connect(DATABASE_PATH)
25
+ g.db.row_factory = sqlite3.Row
26
+ return g.db
27
+
28
+ def close_db(e=None):
29
+ """Close database connection"""
30
+ db = g.pop('db', None)
31
+ if db is not None:
32
+ db.close()
33
+
34
+ def get_db_connection():
35
+ """Get a new database connection (for use outside Flask context)"""
36
+ conn = sqlite3.connect(DATABASE_PATH)
37
+ conn.row_factory = sqlite3.Row
38
+ return conn
39
+
40
+ def init_db():
41
+ """Initialize database with required tables"""
42
+ try:
43
+ db = sqlite3.connect(DATABASE_PATH)
44
+ db.executescript('''
45
+ -- Users table
46
+ CREATE TABLE IF NOT EXISTS users (
47
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ email TEXT UNIQUE NOT NULL,
49
+ password_hash TEXT NOT NULL,
50
+ salt TEXT NOT NULL,
51
+ is_confirmed BOOLEAN DEFAULT FALSE,
52
+ confirmation_token TEXT,
53
+ reset_token TEXT,
54
+ reset_token_expires TIMESTAMP,
55
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
56
+ last_login TIMESTAMP,
57
+ is_active BOOLEAN DEFAULT TRUE
58
+ );
59
+
60
+ -- User flashcards table
61
+ CREATE TABLE IF NOT EXISTS user_flashcards (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ user_id INTEGER NOT NULL,
64
+ term TEXT NOT NULL,
65
+ translation TEXT,
66
+ context_sentence TEXT,
67
+ gapped_sentence TEXT,
68
+ definition TEXT,
69
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
70
+ study_count INTEGER DEFAULT 0,
71
+ last_studied TIMESTAMP,
72
+ difficulty_level INTEGER DEFAULT 1,
73
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
74
+ );
75
+
76
+ -- User study sessions table
77
+ CREATE TABLE IF NOT EXISTS study_sessions (
78
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
79
+ user_id INTEGER NOT NULL,
80
+ session_type TEXT NOT NULL, -- 'flashcard', 'conversation', 'activity'
81
+ duration_minutes INTEGER,
82
+ cards_studied INTEGER DEFAULT 0,
83
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
84
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
85
+ );
86
+
87
+ -- User settings table
88
+ CREATE TABLE IF NOT EXISTS user_settings (
89
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
90
+ user_id INTEGER UNIQUE NOT NULL,
91
+ preferred_model TEXT DEFAULT 'gemini:gemini-2.5-flash-latest',
92
+ context_focus TEXT DEFAULT 'General/Social',
93
+ voice_accent TEXT DEFAULT 'co.uk',
94
+ daily_goal INTEGER DEFAULT 10,
95
+ notification_enabled BOOLEAN DEFAULT TRUE,
96
+ -- New settings for advanced features
97
+ english_level TEXT DEFAULT 'B1', -- A1, A2, B1, B2, C1, C2
98
+ study_goals TEXT, -- JSON: objectives like "business english", "technical vocabulary"
99
+ preferred_content_types TEXT DEFAULT 'articles,videos', -- comma separated
100
+ content_difficulty TEXT DEFAULT 'adaptive', -- 'easy', 'medium', 'hard', 'adaptive'
101
+ study_schedule TEXT, -- JSON: preferred days/times
102
+ auto_recommendations BOOLEAN DEFAULT TRUE,
103
+ content_sources TEXT DEFAULT 'news,tech,business', -- preferred content sources
104
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
105
+ );
106
+
107
+ -- User articles/content table
108
+ CREATE TABLE IF NOT EXISTS user_articles (
109
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ user_id INTEGER NOT NULL,
111
+ title TEXT NOT NULL,
112
+ content TEXT NOT NULL,
113
+ source_url TEXT,
114
+ source_type TEXT DEFAULT 'manual', -- 'manual', 'web_search', 'recommended'
115
+ category TEXT, -- user interest category
116
+ difficulty_level TEXT, -- estimated difficulty
117
+ word_count INTEGER,
118
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
119
+ last_accessed TIMESTAMP,
120
+ is_favorite BOOLEAN DEFAULT FALSE,
121
+ study_progress REAL DEFAULT 0.0, -- 0.0 to 1.0
122
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
123
+ );
124
+
125
+ -- User interests/preferences table
126
+ CREATE TABLE IF NOT EXISTS user_interests (
127
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
128
+ user_id INTEGER NOT NULL,
129
+ interest_category TEXT NOT NULL,
130
+ weight REAL DEFAULT 1.0, -- importance weight
131
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
132
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
133
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
134
+ UNIQUE(user_id, interest_category)
135
+ );
136
+
137
+ -- Content recommendations table
138
+ CREATE TABLE IF NOT EXISTS content_recommendations (
139
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
140
+ user_id INTEGER NOT NULL,
141
+ article_id INTEGER,
142
+ recommendation_reason TEXT,
143
+ relevance_score REAL,
144
+ recommended_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
145
+ viewed BOOLEAN DEFAULT FALSE,
146
+ accepted BOOLEAN DEFAULT FALSE,
147
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
148
+ FOREIGN KEY (article_id) REFERENCES user_articles (id) ON DELETE CASCADE
149
+ );
150
+
151
+ -- Study plans table
152
+ CREATE TABLE IF NOT EXISTS study_plans (
153
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
154
+ user_id INTEGER NOT NULL,
155
+ plan_name TEXT NOT NULL,
156
+ target_level TEXT, -- A1, A2, B1, B2, C1, C2
157
+ current_level TEXT,
158
+ objectives TEXT, -- JSON string with objectives
159
+ weekly_hours INTEGER DEFAULT 5,
160
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
161
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
162
+ is_active BOOLEAN DEFAULT TRUE,
163
+ completion_percentage REAL DEFAULT 0.0,
164
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
165
+ );
166
+
167
+ -- Study plan activities table
168
+ CREATE TABLE IF NOT EXISTS study_plan_activities (
169
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
170
+ plan_id INTEGER NOT NULL,
171
+ activity_type TEXT NOT NULL, -- 'reading', 'flashcards', 'conversation', 'writing'
172
+ content_reference TEXT, -- reference to article, flashcard set, etc.
173
+ scheduled_date DATE,
174
+ estimated_duration INTEGER, -- minutes
175
+ actual_duration INTEGER,
176
+ completed BOOLEAN DEFAULT FALSE,
177
+ completed_at TIMESTAMP,
178
+ difficulty_rating INTEGER, -- 1-5 user rating
179
+ notes TEXT,
180
+ FOREIGN KEY (plan_id) REFERENCES study_plans (id) ON DELETE CASCADE
181
+ );
182
+
183
+ -- User analytics table
184
+ CREATE TABLE IF NOT EXISTS user_analytics (
185
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
186
+ user_id INTEGER NOT NULL,
187
+ metric_name TEXT NOT NULL,
188
+ metric_value REAL NOT NULL,
189
+ metric_date DATE NOT NULL,
190
+ context_data TEXT, -- JSON with additional context
191
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
192
+ );
193
+
194
+ -- Token usage tracking table for admin
195
+ CREATE TABLE IF NOT EXISTS token_usage (
196
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
197
+ user_id INTEGER,
198
+ api_provider TEXT NOT NULL, -- 'groq', 'gemini', etc.
199
+ input_tokens INTEGER DEFAULT 0,
200
+ output_tokens INTEGER DEFAULT 0,
201
+ operation_type TEXT, -- 'conversation', 'content_analysis', 'recommendation', etc.
202
+ tokens_used INTEGER GENERATED ALWAYS AS (input_tokens + output_tokens) STORED,
203
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
204
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
205
+ );
206
+
207
+ -- Create indexes for better performance
208
+ CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
209
+ CREATE INDEX IF NOT EXISTS idx_users_confirmation_token ON users(confirmation_token);
210
+ CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(reset_token);
211
+ CREATE INDEX IF NOT EXISTS idx_flashcards_user_id ON user_flashcards(user_id);
212
+ CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON study_sessions(user_id);
213
+ CREATE INDEX IF NOT EXISTS idx_settings_user_id ON user_settings(user_id);
214
+ CREATE INDEX IF NOT EXISTS idx_articles_user_id ON user_articles(user_id);
215
+ CREATE INDEX IF NOT EXISTS idx_articles_category ON user_articles(category);
216
+ CREATE INDEX IF NOT EXISTS idx_interests_user_id ON user_interests(user_id);
217
+ CREATE INDEX IF NOT EXISTS idx_recommendations_user_id ON content_recommendations(user_id);
218
+ CREATE INDEX IF NOT EXISTS idx_study_plans_user_id ON study_plans(user_id);
219
+ CREATE INDEX IF NOT EXISTS idx_plan_activities_plan_id ON study_plan_activities(plan_id);
220
+ CREATE INDEX IF NOT EXISTS idx_analytics_user_date ON user_analytics(user_id, metric_date);
221
+ CREATE INDEX IF NOT EXISTS idx_token_usage_user_id ON token_usage(user_id);
222
+ CREATE INDEX IF NOT EXISTS idx_token_usage_provider ON token_usage(api_provider);
223
+ CREATE INDEX IF NOT EXISTS idx_token_usage_date ON token_usage(created_at);
224
+ ''')
225
+ db.commit()
226
+ db.close()
227
+ logger.info("Database initialized successfully")
228
+ return True
229
+ except Exception as e:
230
+ logger.error(f"Error initializing database: {e}")
231
+ return False
232
+
233
+ def hash_password(password, salt=None):
234
+ """Hash password with salt"""
235
+ if salt is None:
236
+ salt = secrets.token_hex(32)
237
+
238
+ password_hash = hashlib.pbkdf2_hmac(
239
+ 'sha256',
240
+ password.encode('utf-8'),
241
+ salt.encode('utf-8'),
242
+ 100000 # iterations
243
+ )
244
+ return password_hash.hex(), salt
245
+
246
+ def verify_password(password, password_hash, salt):
247
+ """Verify password against hash"""
248
+ new_hash, _ = hash_password(password, salt)
249
+ return new_hash == password_hash
250
+
251
+ def generate_token():
252
+ """Generate secure random token"""
253
+ return secrets.token_urlsafe(32)
254
+
255
+ def create_user(email, password):
256
+ """Create new user account"""
257
+ try:
258
+ db = get_db()
259
+
260
+ # Check if user already exists
261
+ existing_user = db.execute(
262
+ 'SELECT id FROM users WHERE email = ?', (email,)
263
+ ).fetchone()
264
+
265
+ if existing_user:
266
+ return {'success': False, 'message': 'Email already registered'}
267
+
268
+ # Hash password
269
+ password_hash, salt = hash_password(password)
270
+ confirmation_token = generate_token()
271
+
272
+ # For HF Spaces demo mode, auto-confirm emails
273
+ is_hf_spaces = os.environ.get('SPACE_ID') is not None
274
+ has_smtp_username = os.environ.get('SMTP_USERNAME') is not None
275
+ has_smtp_password = os.environ.get('SMTP_PASSWORD') is not None
276
+ has_smtp = has_smtp_username and has_smtp_password
277
+
278
+ # Sempre auto-confirmar no HF Spaces ou quando SMTP nΓ£o estΓ‘ configurado
279
+ auto_confirm = is_hf_spaces or not has_smtp
280
+
281
+ # Debug logging
282
+ logger.info(f"Registration debug - SPACE_ID: {os.environ.get('SPACE_ID')}")
283
+ logger.info(f"HF_Spaces: {is_hf_spaces}, SMTP_USER: {has_smtp_username}, SMTP_PASS: {has_smtp_password}")
284
+ logger.info(f"SMTP configured: {has_smtp}, Auto-confirm: {auto_confirm}")
285
+
286
+ # Insert user
287
+ cursor = db.execute(
288
+ '''INSERT INTO users (email, password_hash, salt, confirmation_token, is_confirmed)
289
+ VALUES (?, ?, ?, ?, ?)''',
290
+ (email, password_hash, salt, confirmation_token, auto_confirm)
291
+ )
292
+ user_id = cursor.lastrowid
293
+
294
+ # Create default user settings
295
+ db.execute(
296
+ '''INSERT INTO user_settings (user_id) VALUES (?)''',
297
+ (user_id,)
298
+ )
299
+
300
+ db.commit()
301
+
302
+ if auto_confirm:
303
+ logger.info(f"User created and auto-confirmed: {email} (HF Spaces demo mode)")
304
+ message = 'Account created and ready to use! (Demo mode - no email confirmation needed)'
305
+ else:
306
+ logger.info(f"User created: {email}")
307
+ message = 'User created successfully. Please check your email for confirmation.'
308
+
309
+ return {
310
+ 'success': True,
311
+ 'user_id': user_id,
312
+ 'confirmation_token': confirmation_token,
313
+ 'message': message,
314
+ 'auto_confirmed': auto_confirm
315
+ }
316
+
317
+ except Exception as e:
318
+ logger.error(f"Error creating user: {e}")
319
+ return {'success': False, 'message': 'Internal server error'}
320
+
321
+ def authenticate_user(email, password):
322
+ """Authenticate user login"""
323
+ try:
324
+ db = get_db()
325
+ user = db.execute(
326
+ '''SELECT id, email, password_hash, salt, is_confirmed, is_active
327
+ FROM users WHERE email = ?''', (email,)
328
+ ).fetchone()
329
+
330
+ if not user:
331
+ return {'success': False, 'message': 'Invalid email or password'}
332
+
333
+ if not user['is_active']:
334
+ return {'success': False, 'message': 'Account is deactivated'}
335
+
336
+ if not verify_password(password, user['password_hash'], user['salt']):
337
+ return {'success': False, 'message': 'Invalid email or password'}
338
+
339
+ if not user['is_confirmed']:
340
+ return {'success': False, 'message': 'Please confirm your email before logging in'}
341
+
342
+ # Update last login
343
+ db.execute(
344
+ 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
345
+ (user['id'],)
346
+ )
347
+ db.commit()
348
+
349
+ return {
350
+ 'success': True,
351
+ 'user_id': user['id'],
352
+ 'email': user['email'],
353
+ 'message': 'Login successful'
354
+ }
355
+
356
+ except Exception as e:
357
+ logger.error(f"Error authenticating user: {e}")
358
+ return {'success': False, 'message': 'Internal server error'}
359
+
360
+ def confirm_email(token):
361
+ """Confirm user email with token"""
362
+ try:
363
+ db = get_db()
364
+ user = db.execute(
365
+ 'SELECT id, email FROM users WHERE confirmation_token = ? AND is_confirmed = FALSE',
366
+ (token,)
367
+ ).fetchone()
368
+
369
+ if not user:
370
+ return {'success': False, 'message': 'Invalid or expired confirmation token'}
371
+
372
+ db.execute(
373
+ '''UPDATE users SET is_confirmed = TRUE, confirmation_token = NULL
374
+ WHERE id = ?''',
375
+ (user['id'],)
376
+ )
377
+ db.commit()
378
+
379
+ logger.info(f"Email confirmed for user: {user['email']}")
380
+ return {'success': True, 'message': 'Email confirmed successfully'}
381
+
382
+ except Exception as e:
383
+ logger.error(f"Error confirming email: {e}")
384
+ return {'success': False, 'message': 'Internal server error'}
385
+
386
+ def get_user_settings(user_id):
387
+ """Get user settings"""
388
+ try:
389
+ db = get_db()
390
+ settings = db.execute(
391
+ '''SELECT preferred_model, context_focus, voice_accent, daily_goal, notification_enabled
392
+ FROM user_settings WHERE user_id = ?''',
393
+ (user_id,)
394
+ ).fetchone()
395
+
396
+ if settings:
397
+ return dict(settings)
398
+ return None
399
+
400
+ except Exception as e:
401
+ logger.error(f"Error getting user settings: {e}")
402
+ return None
403
+
404
+ def update_user_settings(user_id, settings):
405
+ """Update user settings"""
406
+ try:
407
+ db = get_db()
408
+ db.execute(
409
+ '''UPDATE user_settings
410
+ SET preferred_model = ?, context_focus = ?, voice_accent = ?,
411
+ daily_goal = ?, notification_enabled = ?
412
+ WHERE user_id = ?''',
413
+ (settings.get('preferred_model'), settings.get('context_focus'),
414
+ settings.get('voice_accent'), settings.get('daily_goal'),
415
+ settings.get('notification_enabled'), user_id)
416
+ )
417
+ db.commit()
418
+ return True
419
+
420
+ except Exception as e:
421
+ logger.error(f"Error updating user settings: {e}")
422
+ return False
423
+
424
+ def save_user_flashcard(user_id, flashcard_data):
425
+ """Save flashcard to user's collection"""
426
+ try:
427
+ db = get_db()
428
+ db.execute(
429
+ '''INSERT INTO user_flashcards
430
+ (user_id, term, translation, context_sentence, gapped_sentence, definition)
431
+ VALUES (?, ?, ?, ?, ?, ?)''',
432
+ (user_id, flashcard_data.get('term'), flashcard_data.get('translation'),
433
+ flashcard_data.get('context_sentence'), flashcard_data.get('gapped_sentence'),
434
+ flashcard_data.get('definition'))
435
+ )
436
+ db.commit()
437
+ return True
438
+
439
+ except Exception as e:
440
+ logger.error(f"Error saving flashcard: {e}")
441
+ return False
442
+
443
+ def get_user_flashcards(user_id, limit=50):
444
+ """Get user's flashcards"""
445
+ try:
446
+ db = get_db()
447
+ flashcards = db.execute(
448
+ '''SELECT * FROM user_flashcards
449
+ WHERE user_id = ?
450
+ ORDER BY created_at DESC
451
+ LIMIT ?''',
452
+ (user_id, limit)
453
+ ).fetchall()
454
+
455
+ return [dict(card) for card in flashcards]
456
+
457
+ except Exception as e:
458
+ logger.error(f"Error getting user flashcards: {e}")
459
+ return []
460
+
461
+ def record_study_session(user_id, session_type, duration_minutes=None, cards_studied=0):
462
+ """Record a study session"""
463
+ try:
464
+ db = get_db()
465
+ db.execute(
466
+ '''INSERT INTO study_sessions (user_id, session_type, duration_minutes, cards_studied)
467
+ VALUES (?, ?, ?, ?)''',
468
+ (user_id, session_type, duration_minutes, cards_studied)
469
+ )
470
+ db.commit()
471
+ return True
472
+
473
+ except Exception as e:
474
+ logger.error(f"Error recording study session: {e}")
475
+ return False
476
+
477
+ # Authentication decorators
478
+ def login_required(f):
479
+ """Decorator to require login"""
480
+ @wraps(f)
481
+ def decorated_function(*args, **kwargs):
482
+ if 'user_id' not in session:
483
+ return jsonify({'error': 'Authentication required'}), 401
484
+ return f(*args, **kwargs)
485
+ return decorated_function
486
+
487
+ def get_current_user():
488
+ """Get current logged in user"""
489
+ if 'user_id' in session:
490
+ try:
491
+ db = get_db()
492
+ user = db.execute(
493
+ 'SELECT id, email, is_confirmed FROM users WHERE id = ? AND is_active = TRUE',
494
+ (session['user_id'],)
495
+ ).fetchone()
496
+ return dict(user) if user else None
497
+ except Exception as e:
498
+ logger.error(f"Error getting current user: {e}")
499
+ return None
500
+ return None
501
+
502
+ # Email functionality (for Hugging Face Spaces)
503
+ def send_confirmation_email(email, token):
504
+ """Send confirmation email (simplified for HF Spaces with timeout)"""
505
+ try:
506
+ # For Hugging Face Spaces, we'll use environment variables for SMTP
507
+ smtp_server = os.environ.get('SMTP_SERVER', 'smtp.gmail.com')
508
+ smtp_port = int(os.environ.get('SMTP_PORT', '587'))
509
+ smtp_username = os.environ.get('SMTP_USERNAME')
510
+ smtp_password = os.environ.get('SMTP_PASSWORD')
511
+
512
+ if not all([smtp_username, smtp_password]):
513
+ logger.warning("SMTP credentials not configured - skipping email")
514
+ return False
515
+
516
+ # Create confirmation URL (will be updated with actual domain)
517
+ base_url = os.environ.get('BASE_URL', 'http://localhost:7860')
518
+ confirm_url = f"{base_url}/confirm-email?token={token}"
519
+
520
+ # Create email
521
+ msg = MIMEMultipart()
522
+ msg['From'] = smtp_username
523
+ msg['To'] = email
524
+ msg['Subject'] = "Confirm your English Helper account"
525
+
526
+ body = f"""
527
+ <html>
528
+ <body>
529
+ <h2>Welcome to Dynamic English Study Studio!</h2>
530
+ <p>Thank you for creating an account. Please click the link below to confirm your email address:</p>
531
+ <p><a href="{confirm_url}" style="background-color: #4f46e5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Confirm Email</a></p>
532
+ <p>If the button doesn't work, copy and paste this link into your browser:</p>
533
+ <p>{confirm_url}</p>
534
+ <p>This link will expire in 24 hours.</p>
535
+ <p>If you didn't create this account, please ignore this email.</p>
536
+ </body>
537
+ </html>
538
+ """
539
+
540
+ msg.attach(MIMEText(body, 'html'))
541
+
542
+ # Send email with timeout
543
+ import socket
544
+
545
+ # Set socket timeout to prevent hanging
546
+ socket.setdefaulttimeout(10)
547
+
548
+ server = smtplib.SMTP(smtp_server, smtp_port)
549
+ server.starttls()
550
+ server.login(smtp_username, smtp_password)
551
+ text = msg.as_string()
552
+ server.sendmail(smtp_username, email, text)
553
+ server.quit()
554
+
555
+ # Reset socket timeout
556
+ socket.setdefaulttimeout(None)
557
+
558
+ logger.info(f"Confirmation email sent to {email}")
559
+ return True
560
+
561
+ except Exception as e:
562
+ logger.error(f"Error sending confirmation email: {e}")
563
+ # Reset socket timeout on error
564
+ try:
565
+ import socket
566
+ socket.setdefaulttimeout(None)
567
+ except:
568
+ pass
569
+ return False
570
+
571
+ # --- CONTENT CURATION FUNCTIONS ---
572
+
573
+ def save_user_article(user_id, title, content, source_url=None, source_type='manual', category=None):
574
+ """Save article/content for user"""
575
+ try:
576
+ db = get_db()
577
+ word_count = len(content.split()) if content else 0
578
+
579
+ cursor = db.execute(
580
+ '''INSERT INTO user_articles
581
+ (user_id, title, content, source_url, source_type, category, word_count)
582
+ VALUES (?, ?, ?, ?, ?, ?, ?)''',
583
+ (user_id, title, content, source_url, source_type, category, word_count)
584
+ )
585
+ article_id = cursor.lastrowid
586
+ db.commit()
587
+
588
+ logger.info(f"Article saved for user {user_id}: {title}")
589
+ return {'success': True, 'article_id': article_id}
590
+
591
+ except Exception as e:
592
+ logger.error(f"Error saving article: {e}")
593
+ return {'success': False, 'message': 'Failed to save article'}
594
+
595
+ def get_user_articles(user_id, category=None, limit=50):
596
+ """Get user's saved articles"""
597
+ try:
598
+ db = get_db()
599
+
600
+ if category:
601
+ articles = db.execute(
602
+ '''SELECT * FROM user_articles
603
+ WHERE user_id = ? AND category = ?
604
+ ORDER BY created_at DESC LIMIT ?''',
605
+ (user_id, category, limit)
606
+ ).fetchall()
607
+ else:
608
+ articles = db.execute(
609
+ '''SELECT * FROM user_articles
610
+ WHERE user_id = ?
611
+ ORDER BY created_at DESC LIMIT ?''',
612
+ (user_id, limit)
613
+ ).fetchall()
614
+
615
+ return [dict(article) for article in articles]
616
+
617
+ except Exception as e:
618
+ logger.error(f"Error getting user articles: {e}")
619
+ return []
620
+
621
+ def update_user_interests(user_id, interests):
622
+ """Update user's interests/categories"""
623
+ try:
624
+ db = get_db()
625
+
626
+ # Clear existing interests
627
+ db.execute('DELETE FROM user_interests WHERE user_id = ?', (user_id,))
628
+
629
+ # Add new interests
630
+ for interest, weight in interests.items():
631
+ db.execute(
632
+ '''INSERT INTO user_interests (user_id, interest_category, weight)
633
+ VALUES (?, ?, ?)''',
634
+ (user_id, interest, weight)
635
+ )
636
+
637
+ db.commit()
638
+ return True
639
+
640
+ except Exception as e:
641
+ logger.error(f"Error updating user interests: {e}")
642
+ return False
643
+
644
+ def get_user_interests(user_id):
645
+ """Get user's interests"""
646
+ try:
647
+ db = get_db()
648
+ interests = db.execute(
649
+ 'SELECT interest_category, weight FROM user_interests WHERE user_id = ?',
650
+ (user_id,)
651
+ ).fetchall()
652
+
653
+ return {interest['interest_category']: interest['weight'] for interest in interests}
654
+
655
+ except Exception as e:
656
+ logger.error(f"Error getting user interests: {e}")
657
+ return {}
658
+
659
+ def create_study_plan(user_id, plan_name, target_level, current_level, objectives, weekly_hours=5):
660
+ """Create new study plan"""
661
+ try:
662
+ db = get_db()
663
+
664
+ cursor = db.execute(
665
+ '''INSERT INTO study_plans
666
+ (user_id, plan_name, target_level, current_level, objectives, weekly_hours)
667
+ VALUES (?, ?, ?, ?, ?, ?)''',
668
+ (user_id, plan_name, target_level, current_level, objectives, weekly_hours)
669
+ )
670
+ plan_id = cursor.lastrowid
671
+ db.commit()
672
+
673
+ logger.info(f"Study plan created for user {user_id}: {plan_name}")
674
+ return {'success': True, 'plan_id': plan_id}
675
+
676
+ except Exception as e:
677
+ logger.error(f"Error creating study plan: {e}")
678
+ return {'success': False, 'message': 'Failed to create study plan'}
679
+
680
+ def get_user_study_plans(user_id):
681
+ """Get user's study plans"""
682
+ try:
683
+ db = get_db()
684
+ plans = db.execute(
685
+ '''SELECT * FROM study_plans
686
+ WHERE user_id = ?
687
+ ORDER BY created_at DESC''',
688
+ (user_id,)
689
+ ).fetchall()
690
+
691
+ return [dict(plan) for plan in plans]
692
+
693
+ except Exception as e:
694
+ logger.error(f"Error getting study plans: {e}")
695
+ return []
696
+
697
+ def add_study_activity(plan_id, activity_type, content_reference, scheduled_date, estimated_duration):
698
+ """Add activity to study plan"""
699
+ try:
700
+ db = get_db()
701
+
702
+ db.execute(
703
+ '''INSERT INTO study_plan_activities
704
+ (plan_id, activity_type, content_reference, scheduled_date, estimated_duration)
705
+ VALUES (?, ?, ?, ?, ?)''',
706
+ (plan_id, activity_type, content_reference, scheduled_date, estimated_duration)
707
+ )
708
+ db.commit()
709
+ return True
710
+
711
+ except Exception as e:
712
+ logger.error(f"Error adding study activity: {e}")
713
+ return False
714
+
715
+ def get_study_activities(plan_id, date_range=None):
716
+ """Get activities for study plan"""
717
+ try:
718
+ db = get_db()
719
+
720
+ if date_range:
721
+ start_date, end_date = date_range
722
+ activities = db.execute(
723
+ '''SELECT * FROM study_plan_activities
724
+ WHERE plan_id = ? AND scheduled_date BETWEEN ? AND ?
725
+ ORDER BY scheduled_date''',
726
+ (plan_id, start_date, end_date)
727
+ ).fetchall()
728
+ else:
729
+ activities = db.execute(
730
+ '''SELECT * FROM study_plan_activities
731
+ WHERE plan_id = ?
732
+ ORDER BY scheduled_date''',
733
+ (plan_id,)
734
+ ).fetchall()
735
+
736
+ return [dict(activity) for activity in activities]
737
+
738
+ except Exception as e:
739
+ logger.error(f"Error getting study activities: {e}")
740
+ return []
741
+
742
+ def record_analytics_metric(user_id, metric_name, metric_value, context_data=None):
743
+ """Record analytics metric"""
744
+ try:
745
+ db = get_db()
746
+
747
+ db.execute(
748
+ '''INSERT INTO user_analytics (user_id, metric_name, metric_value, metric_date, context_data)
749
+ VALUES (?, ?, ?, DATE('now'), ?)''',
750
+ (user_id, metric_name, metric_value, context_data)
751
+ )
752
+ db.commit()
753
+ return True
754
+
755
+ except Exception as e:
756
+ logger.error(f"Error recording analytics: {e}")
757
+ return False
758
+
759
+ def get_user_analytics(user_id, metric_name=None, days=30):
760
+ """Get user analytics data"""
761
+ try:
762
+ db = get_db()
763
+
764
+ if metric_name:
765
+ analytics = db.execute(
766
+ '''SELECT * FROM user_analytics
767
+ WHERE user_id = ? AND metric_name = ?
768
+ AND metric_date >= DATE('now', '-{} days')
769
+ ORDER BY metric_date DESC'''.format(days),
770
+ (user_id, metric_name)
771
+ ).fetchall()
772
+ else:
773
+ analytics = db.execute(
774
+ '''SELECT * FROM user_analytics
775
+ WHERE user_id = ?
776
+ AND metric_date >= DATE('now', '-{} days')
777
+ ORDER BY metric_date DESC'''.format(days),
778
+ (user_id,)
779
+ ).fetchall()
780
+
781
+ return [dict(metric) for metric in analytics]
782
+
783
+ except Exception as e:
784
+ logger.error(f"Error getting analytics: {e}")
785
+ return []
flask_app.py ADDED
@@ -0,0 +1,1356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # flask_app.py - English Helper Flask Application
2
+ import os
3
+ import io
4
+ import json
5
+ import base64
6
+ from datetime import datetime
7
+ from PIL import Image
8
+ from email_validator import validate_email, EmailNotValidError
9
+
10
+ from flask import Flask, request, jsonify, send_file, session, render_template_string, redirect, url_for, Response
11
+ # from flask_session import Session # Removido para usar sessΓ΅es nativas do Flask
12
+ from gtts import gTTS
13
+ from groq import Groq
14
+ import google.generativeai as genai
15
+ from google.generativeai.types import GenerationConfig
16
+
17
+ # Import database functions
18
+ from database import (
19
+ init_db, close_db, create_user, authenticate_user, confirm_email,
20
+ get_user_settings, update_user_settings, save_user_flashcard,
21
+ get_user_flashcards, record_study_session, login_required,
22
+ get_current_user, send_confirmation_email, save_user_article,
23
+ get_user_articles, update_user_interests, get_user_interests,
24
+ create_study_plan, get_user_study_plans, add_study_activity,
25
+ get_study_activities, record_analytics_metric, get_user_analytics,
26
+ get_db_connection
27
+ )
28
+
29
+ # Import content curation and study planner
30
+ from content_curator import content_curator
31
+ from study_planner import study_planner
32
+ from admin_module import admin_manager, admin_required
33
+
34
+ # --- CONFIGURAÇÃO INICIAL ---
35
+ # Evitar mΓΊltiplas instΓ’ncias do Flask
36
+ if 'app' not in globals():
37
+ app = Flask(__name__)
38
+
39
+ # Configuration for sessions - Simplificado para HF Spaces
40
+ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-change-in-production-hf-spaces')
41
+ app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24 hours
42
+
43
+ # ConfiguraΓ§Γ΅es especΓ­ficas para HF Spaces
44
+ app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
45
+ app.config['TEMPLATES_AUTO_RELOAD'] = True
46
+ app.config['SESSION_COOKIE_SECURE'] = False # HF Spaces pode ter problemas com HTTPS interno
47
+ app.config['SESSION_COOKIE_HTTPONLY'] = True
48
+ app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Mais permissivo para HF Spaces
49
+ app.config['SESSION_COOKIE_NAME'] = 'englishhelper_session'
50
+
51
+ # Usar sessΓ΅es nativas do Flask ao invΓ©s de Flask-Session
52
+ # Session(app) # Comentado para usar sessΓ΅es nativas
53
+
54
+ # Print session config for debugging
55
+ print(f"βœ… Flask app inicializado - SECRET_KEY length: {len(app.config['SECRET_KEY'])}")
56
+ print(f"βœ… Session config - Usando sessΓ΅es nativas do Flask")
57
+ print(f"βœ… Working directory: {os.getcwd()}")
58
+ else:
59
+ print("βœ… Flask app jΓ‘ existe - reutilizando instΓ’ncia")
60
+
61
+ # Adicionar middleware para debug de sessΓ£o
62
+ @app.before_request
63
+ def debug_session():
64
+ if request.endpoint and 'admin' in request.endpoint:
65
+ print(f"πŸ” Session Debug - Endpoint: {request.endpoint}")
66
+ print(f"πŸ” Session Data: {dict(session)}")
67
+ print(f"πŸ” All Cookies: {dict(request.cookies)}")
68
+ print(f"πŸ” Session ID: {request.cookies.get('englishhelper_session', 'no-session')}")
69
+ print(f"πŸ” User Agent: {request.headers.get('User-Agent', 'unknown')[:50]}...")
70
+
71
+ @app.after_request
72
+ def ensure_session_saved(response):
73
+ """Garantir que a sessΓ£o seja salva"""
74
+ try:
75
+ if hasattr(session, 'accessed') and session.accessed:
76
+ session.permanent = True
77
+ except Exception as e:
78
+ print(f"Session save error: {e}")
79
+ return response
80
+
81
+ # Database initialization (handled by app.py)
82
+ def initialize_database():
83
+ init_db()
84
+
85
+ # Note: Database initialization moved to app.py to avoid conflicts
86
+
87
+ # Token tracking helper function
88
+ def track_token_usage(user_id, provider, input_tokens, output_tokens, operation):
89
+ """Helper function to track token usage"""
90
+ try:
91
+ admin_manager.record_token_usage(user_id, provider, input_tokens, output_tokens, operation)
92
+ except Exception as e:
93
+ print(f"Token tracking error: {e}")
94
+
95
+ @app.teardown_appcontext
96
+ def close_database(error):
97
+ close_db(error)
98
+
99
+ # --- CONFIGURAÇÃO DAS APIS LLM ---
100
+ genai_client = None
101
+ groq_client = None
102
+
103
+ # 1. ConfiguraΓ§Γ£o Gemini
104
+ try:
105
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
106
+ if GEMINI_API_KEY:
107
+ genai.configure(api_key=GEMINI_API_KEY)
108
+ genai_client = genai
109
+ else:
110
+ print("AVISO: GEMINI_API_KEY nΓ£o configurada.")
111
+ except Exception as e:
112
+ genai_client = None
113
+ print(f"ERRO ao inicializar o cliente Gemini: {e}.")
114
+
115
+ # 2. ConfiguraΓ§Γ£o Groq
116
+ try:
117
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
118
+ if GROQ_API_KEY:
119
+ groq_client = Groq(api_key=GROQ_API_KEY)
120
+ else:
121
+ print("AVISO: GROQ_API_KEY nΓ£o configurada.")
122
+ except Exception as e:
123
+ groq_client = None
124
+ print(f"ERRO ao inicializar o cliente Groq: {e}.")
125
+
126
+
127
+ # --- ROTA PARA LISTAR MODELOS DINAMICAMENTE ---
128
+ @app.route('/list-models')
129
+ def list_models():
130
+ available_models = []
131
+ groq_text_models = [
132
+ "llama-3.1-8b-instant",
133
+ "llama-3.3-70b-versatile",
134
+ "openai/gpt-oss-120b",
135
+ "openai/gpt-oss-20b"
136
+ ]
137
+ try:
138
+ if genai_client:
139
+ for m in genai_client.list_models():
140
+ if 'generateContent' in m.supported_generation_methods:
141
+ model_name = m.name.replace("models/", "")
142
+ if "flash" in model_name or "pro" in model_name:
143
+ available_models.append({
144
+ "value": f"gemini:{model_name}",
145
+ "name": m.display_name
146
+ })
147
+ if groq_client:
148
+ for model_id in groq_text_models:
149
+ display_name = model_id.split('/')[-1].replace('-instant', '').replace('-versatile', '')
150
+ available_models.append({
151
+ "value": f"groq:{model_id}",
152
+ "name": f"Groq: {display_name}"
153
+ })
154
+ except Exception as e:
155
+ print(f"Erro ao listar modelos: {e}")
156
+ return jsonify([
157
+ {"value": "gemini:gemini-2.5-flash-latest", "name": "Gemini 2.5 Flash (Fallback)"},
158
+ {"value": "groq:llama-3.1-8b-instant", "name": "Llama 3.1 8B (Fallback)"}
159
+ ])
160
+ return jsonify(available_models)
161
+
162
+
163
+ # --- ROTAS PRINCIPAIS ---
164
+
165
+ # Cache de Γ‘udio TTS em memΓ³ria
166
+ tts_cache = {}
167
+
168
+ @app.route('/tts-proxy', methods=['POST'])
169
+ def tts_proxy():
170
+ data = request.get_json()
171
+ text = data.get('text', '')
172
+ tld = data.get('tld', 'co.uk')
173
+ if not text: return jsonify({"error": "No text provided"}), 400
174
+
175
+ # Validar comprimento do texto (10000 caracteres max)
176
+ if len(text) > 10000:
177
+ return jsonify({"error": "Text is too long. Maximum 10,000 characters allowed."}), 400
178
+
179
+ # Criar chave de cache baseada no texto e TLD
180
+ import hashlib
181
+ cache_key = hashlib.md5(f"{text}_{tld}".encode()).hexdigest()
182
+
183
+ try:
184
+ # Verificar se estΓ‘ no cache
185
+ if cache_key in tts_cache:
186
+ print(f"🎡 TTS Cache HIT: {len(text)} chars")
187
+ cached_audio = tts_cache[cache_key]
188
+ audio_fp = io.BytesIO(cached_audio)
189
+ return send_file(audio_fp, mimetype='audio/mpeg', as_attachment=False)
190
+
191
+ # Gerar novo Γ‘udio
192
+ print(f"🎡 TTS Cache MISS: Gerando Ñudio para {len(text)} chars, tld: {tld}")
193
+
194
+ tts = gTTS(text=text, lang='en', tld=tld)
195
+ mp3_fp = io.BytesIO()
196
+ tts.write_to_fp(mp3_fp)
197
+ mp3_fp.seek(0)
198
+
199
+ # Salvar no cache
200
+ audio_data = mp3_fp.read()
201
+ tts_cache[cache_key] = audio_data
202
+
203
+ # Limitar cache a 50 entradas
204
+ if len(tts_cache) > 50:
205
+ oldest_key = next(iter(tts_cache))
206
+ del tts_cache[oldest_key]
207
+
208
+ # Retornar Γ‘udio
209
+ audio_fp = io.BytesIO(audio_data)
210
+ return send_file(audio_fp, mimetype='audio/mpeg', as_attachment=False)
211
+
212
+ except Exception as e:
213
+ print(f"❌ TTS Error: {e}")
214
+ return jsonify({"error": f"Failed to generate audio via gTTS: {e}"}), 500
215
+
216
+ # Primeira funΓ§Γ£o explain_proxy removida - duplicata
217
+
218
+ @app.route('/activity-feedback', methods=['POST'])
219
+ def activity_feedback():
220
+ data = request.get_json()
221
+ model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1)
222
+ context_focus = data.get('context_focus', 'General/Social')
223
+ original_prompt = data.get('original_prompt', '')
224
+ user_response = data.get('user_response', '')
225
+
226
+ if not original_prompt or not user_response: return jsonify({"error": "Original prompt and user response are required."}), 400
227
+ if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client):
228
+ return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503
229
+
230
+ system_instruction = (
231
+ "You are an expert English teacher providing feedback. "
232
+ f"The user's study focus is '{context_focus}'. "
233
+ "Your entire response MUST be in English. "
234
+ "Provide clear, constructive feedback on the user's writing. "
235
+ "Point out grammar, spelling, or style errors. "
236
+ "Offer a corrected or improved version of their text. "
237
+ "Structure your feedback with markdown for clarity (e.g., using ### Corrected Version)."
238
+ )
239
+ user_prompt = f"The original task was: \"{original_prompt}\"\n\nHere is the user's response:\n---\n{user_response}\n---\nPlease provide your feedback."
240
+ try:
241
+ feedback_text = get_ai_text_response(model_provider, model_name, system_instruction, user_prompt)
242
+ return jsonify({"feedback": feedback_text})
243
+ except Exception as e:
244
+ return jsonify({"error": f"AI feedback failed: {e}"}), 500
245
+
246
+ @app.route('/analyze-image', methods=['POST'])
247
+ def analyze_image():
248
+ if not genai_client: return jsonify({"error": "GEMINI_API_KEY not configured."}), 503
249
+ data = request.get_json()
250
+ base64_image = data.get('image')
251
+ model_value = data.get('model', 'gemini:gemini-2.5-flash-latest')
252
+
253
+ model_name = 'gemini-2.5-flash-latest' # Default
254
+ if model_value.startswith('gemini:'):
255
+ model_name = model_value.split(':', 1)[1]
256
+
257
+ if not base64_image: return jsonify({"error": "No image data."}), 400
258
+ try:
259
+ image = Image.open(io.BytesIO(base64.b64decode(base64_image.split(',')[1])))
260
+ model = genai_client.GenerativeModel(model_name)
261
+ schema = { "type": "object", "properties": { "vocabulary": { "type": "array", "items": { "type": "object", "properties": { "term": {"type": "string"}, "definition": {"type": "string"} }, "required": ["term", "definition"] } } }, "required": ["vocabulary"] }
262
+ prompt = [ "Act as an English teacher. Identify 5-7 key objects/concepts in this image. For each, provide its English name and a simple definition. Return a single JSON object conforming to the schema.", image ]
263
+
264
+ config = GenerationConfig(response_mime_type="application/json", response_schema=schema)
265
+ response = model.generate_content(prompt, generation_config=config)
266
+
267
+ return jsonify(json.loads(response.text)['vocabulary'])
268
+ except Exception as e:
269
+ return jsonify({"error": f"Image analysis failed: {e}"}), 500
270
+
271
+ @app.route('/chat-with-ai', methods=['POST'])
272
+ def chat_with_ai():
273
+ if not groq_client: return jsonify({"error": "GROQ_API_KEY not configured."}), 503
274
+ data = request.get_json()
275
+ history, user_message = data.get('history', []), data.get('message', '')
276
+ if not user_message: return jsonify({"error": "No message."}), 400
277
+ try:
278
+ system = "You are 'Groq Chat', a friendly English tutor. Keep responses concise (1-2 sentences). If the user makes a grammar mistake, gently correct it. Ask questions to keep the conversation flowing. Always respond in English."
279
+ messages = [{"role": "system", "content": system}] + history + [{"role": "user", "content": user_message}]
280
+ response = groq_client.chat.completions.create(model="llama-3.1-8b-instant", messages=messages, temperature=0.7)
281
+
282
+ # Track token usage
283
+ user = get_current_user()
284
+ if user and hasattr(response, 'usage'):
285
+ track_token_usage(
286
+ user['id'],
287
+ 'groq',
288
+ response.usage.prompt_tokens,
289
+ response.usage.completion_tokens,
290
+ 'conversation'
291
+ )
292
+
293
+ return jsonify({"response": response.choices[0].message.content.strip()})
294
+ except Exception as e:
295
+ return jsonify({"error": f"AI chat failed: {e}"}), 500
296
+
297
+ @app.route('/pronunciation-feedback', methods=['POST'])
298
+ def pronunciation_feedback():
299
+ if not groq_client: return jsonify({"error": "GROQ_API_KEY not configured."}), 503
300
+ data = request.get_json()
301
+ target_text, user_text = data.get('target_text'), data.get('user_text')
302
+ if not target_text or not user_text: return jsonify({"error": "Required data missing."}), 400
303
+ try:
304
+ system_instruction = "You are an expert American English pronunciation coach. The user tried to say a target sentence, and their speech was transcribed. Based on the likely pronunciation differences, provide brief, friendly, and actionable feedback in Portuguese. Focus on 1-2 key points. If it's very close, praise the user."
305
+ user_prompt = f"Target: \"{target_text}\"\nTranscription: \"{user_text}\"\n\nProvide pronunciation feedback."
306
+ messages = [{"role": "system", "content": system_instruction}, {"role": "user", "content": user_prompt}]
307
+ response = groq_client.chat.completions.create(model="llama-3.1-8b-instant", messages=messages, temperature=0.5)
308
+
309
+ # Track token usage
310
+ user = get_current_user()
311
+ if user and hasattr(response, 'usage'):
312
+ track_token_usage(
313
+ user['id'],
314
+ 'groq',
315
+ response.usage.prompt_tokens,
316
+ response.usage.completion_tokens,
317
+ 'pronunciation_feedback'
318
+ )
319
+
320
+ return jsonify({"feedback": response.choices[0].message.content.strip()})
321
+ except Exception as e:
322
+ return jsonify({"error": f"Pronunciation analysis failed: {e}"}), 500
323
+
324
+ @app.route('/generate-image', methods=['POST'])
325
+ def generate_image():
326
+ if not genai_client: return jsonify({"error": "GEMINI_API_KEY not configured."}), 503
327
+ data = request.get_json()
328
+ prompt = data.get('prompt')
329
+ if not prompt: return jsonify({"error": "Image prompt is required."}), 400
330
+ try:
331
+ model = genai_client.GenerativeModel(model_name='gemini-2.5-flash-image-preview')
332
+ response = model.generate_content(prompt)
333
+ base64_image_data = response.parts[0].inline_data.data
334
+ return jsonify({"image_base64": base64_image_data})
335
+ except Exception as e:
336
+ return jsonify({"error": f"Image generation failed: {e}"}), 500
337
+
338
+ # --- FUNÇÃO AUXILIAR E ROTA RAIZ ---
339
+ def get_ai_text_response(provider, model_name, system_instruction, user_prompt, json_schema=None):
340
+ if provider == 'gemini':
341
+ model = genai_client.GenerativeModel(model_name, system_instruction=system_instruction)
342
+
343
+ config = None
344
+ if json_schema:
345
+ config = GenerationConfig(response_mime_type="application/json", response_schema=json_schema)
346
+
347
+ response = model.generate_content(user_prompt, generation_config=config)
348
+
349
+ if json_schema:
350
+ parsed_json = json.loads(response.text)
351
+ required_keys = json_schema.get("required", [])
352
+ if not all(key in parsed_json and parsed_json[key] for key in required_keys):
353
+ raise ValueError(f"AI response missing required keys or has empty values.")
354
+ return parsed_json
355
+ else:
356
+ return response.text.strip()
357
+
358
+ elif provider == 'groq':
359
+ final_user_prompt = user_prompt
360
+ if json_schema:
361
+ final_user_prompt += f"\n\nYou MUST respond with a single JSON object that strictly follows this schema. Do not add any other text before or after the JSON object:\n{json.dumps(json_schema)}"
362
+
363
+ messages = [{"role": "system", "content": system_instruction}, {"role": "user", "content": final_user_prompt}]
364
+ config = {'response_format': {"type": "json_object"}} if json_schema else {}
365
+ response = groq_client.chat.completions.create(model=model_name, messages=messages, **config)
366
+
367
+ if json_schema:
368
+ parsed_json = json.loads(response.choices[0].message.content)
369
+ required_keys = json_schema.get("required", [])
370
+ if not all(key in parsed_json and parsed_json[key] for key in required_keys):
371
+ raise ValueError(f"AI response missing required keys or has empty values.")
372
+ return parsed_json
373
+ else:
374
+ return response.choices[0].message.content.strip()
375
+
376
+ raise Exception(f"Unsupported provider: {provider}")
377
+
378
+ # --- AUTHENTICATION ROUTES ---
379
+
380
+ @app.route('/register', methods=['POST'])
381
+ def register():
382
+ """User registration endpoint"""
383
+ try:
384
+ data = request.get_json()
385
+ email = data.get('email', '').strip().lower()
386
+ password = data.get('password', '')
387
+
388
+ # Validate input
389
+ if not email or not password:
390
+ return jsonify({'error': 'Email and password are required'}), 400
391
+
392
+ if len(password) < 8:
393
+ return jsonify({'error': 'Password must be at least 8 characters long'}), 400
394
+
395
+ # Validate email format
396
+ try:
397
+ validate_email(email)
398
+ except EmailNotValidError:
399
+ return jsonify({'error': 'Invalid email format'}), 400
400
+
401
+ # Create user
402
+ result = create_user(email, password)
403
+
404
+ if result['success']:
405
+ # Try to send confirmation email (non-blocking for HF Spaces)
406
+ email_sent = False
407
+ try:
408
+ # Use a timeout to prevent hanging
409
+ import threading
410
+ import time
411
+
412
+ def send_email_async():
413
+ nonlocal email_sent
414
+ try:
415
+ email_sent = send_confirmation_email(email, result['confirmation_token'])
416
+ except:
417
+ email_sent = False
418
+
419
+ # Start email sending in background with timeout
420
+ email_thread = threading.Thread(target=send_email_async)
421
+ email_thread.daemon = True
422
+ email_thread.start()
423
+ email_thread.join(timeout=5) # 5 second timeout
424
+
425
+ except Exception as e:
426
+ print(f"Email sending timeout or error: {e}")
427
+ email_sent = False
428
+
429
+ # Return success message based on auto-confirmation and email status
430
+ auto_confirmed = result.get('auto_confirmed', False)
431
+
432
+ if auto_confirmed:
433
+ return jsonify({
434
+ 'message': 'Registration successful! Your account is ready to use - you can log in immediately.',
435
+ 'email_sent': email_sent,
436
+ 'auto_confirmed': True,
437
+ 'note': 'Email confirmation is disabled in demo mode.'
438
+ }), 201
439
+ elif email_sent:
440
+ return jsonify({
441
+ 'message': 'Registration successful! Please check your email to confirm your account.',
442
+ 'email_sent': True,
443
+ 'auto_confirmed': False
444
+ }), 201
445
+ else:
446
+ return jsonify({
447
+ 'message': 'Registration successful! However, we could not send the confirmation email. Please contact support.',
448
+ 'email_sent': False,
449
+ 'auto_confirmed': False
450
+ }), 201
451
+ else:
452
+ return jsonify({'error': result['message']}), 400
453
+
454
+ except Exception as e:
455
+ print(f"Registration error: {e}")
456
+ return jsonify({'error': 'Internal server error'}), 500
457
+
458
+ @app.route('/login', methods=['POST'])
459
+ def login():
460
+ """User login endpoint"""
461
+ try:
462
+ data = request.get_json()
463
+ email = data.get('email', '').strip().lower()
464
+ password = data.get('password', '')
465
+
466
+ if not email or not password:
467
+ return jsonify({'error': 'Email and password are required'}), 400
468
+
469
+ result = authenticate_user(email, password)
470
+
471
+ if result['success']:
472
+ session['user_id'] = result['user_id']
473
+ session['user_email'] = result['email']
474
+ session.permanent = True
475
+
476
+ # Get user settings
477
+ settings = get_user_settings(result['user_id'])
478
+
479
+ return jsonify({
480
+ 'message': 'Login successful',
481
+ 'user': {
482
+ 'id': result['user_id'],
483
+ 'email': result['email'],
484
+ 'settings': settings
485
+ }
486
+ }), 200
487
+ else:
488
+ return jsonify({'error': result['message']}), 401
489
+
490
+ except Exception as e:
491
+ print(f"Login error: {e}")
492
+ return jsonify({'error': 'Internal server error'}), 500
493
+
494
+ @app.route('/logout', methods=['POST'])
495
+ def logout():
496
+ """User logout endpoint"""
497
+ session.clear()
498
+ return jsonify({'message': 'Logout successful'}), 200
499
+
500
+ @app.route('/confirm-email')
501
+ def confirm_email_route():
502
+ """Email confirmation endpoint"""
503
+ token = request.args.get('token')
504
+
505
+ if not token:
506
+ return render_template_string('''
507
+ <!DOCTYPE html>
508
+ <html><head><title>Invalid Link</title></head>
509
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
510
+ <h2>Invalid Confirmation Link</h2>
511
+ <p>This confirmation link is invalid or malformed.</p>
512
+ <a href="/" style="color: #4f46e5;">Return to English Helper</a>
513
+ </body></html>
514
+ '''), 400
515
+
516
+ result = confirm_email(token)
517
+
518
+ if result['success']:
519
+ return render_template_string('''
520
+ <!DOCTYPE html>
521
+ <html><head><title>Email Confirmed</title></head>
522
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
523
+ <h2>βœ… Email Confirmed!</h2>
524
+ <p>Your email has been successfully confirmed. You can now log in to your account.</p>
525
+ <a href="/" style="background: #4f46e5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Continue to English Helper</a>
526
+ </body></html>
527
+ ''')
528
+ else:
529
+ return render_template_string('''
530
+ <!DOCTYPE html>
531
+ <html><head><title>Confirmation Failed</title></head>
532
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
533
+ <h2>❌ Confirmation Failed</h2>
534
+ <p>This confirmation link is invalid or has expired.</p>
535
+ <a href="/" style="color: #4f46e5;">Return to English Helper</a>
536
+ </body></html>
537
+ '''), 400
538
+
539
+ @app.route('/user/profile', methods=['GET'])
540
+ @login_required
541
+ def get_user_profile():
542
+ """Get current user profile"""
543
+ user = get_current_user()
544
+ if not user:
545
+ return jsonify({'error': 'User not found'}), 404
546
+
547
+ settings = get_user_settings(user['id'])
548
+ flashcards_count = len(get_user_flashcards(user['id'], 1000))
549
+
550
+ return jsonify({
551
+ 'user': {
552
+ 'id': user['id'],
553
+ 'email': user['email'],
554
+ 'settings': settings,
555
+ 'stats': {
556
+ 'flashcards_created': flashcards_count
557
+ }
558
+ }
559
+ })
560
+
561
+ @app.route('/user/settings', methods=['GET', 'POST'])
562
+ @login_required
563
+ def user_settings():
564
+ """Get or update user settings"""
565
+ user = get_current_user()
566
+ if not user:
567
+ return jsonify({'error': 'User not found'}), 404
568
+
569
+ if request.method == 'GET':
570
+ settings = get_user_settings(user['id'])
571
+ return jsonify({'settings': settings})
572
+
573
+ elif request.method == 'POST':
574
+ data = request.get_json()
575
+ settings = {
576
+ 'preferred_model': data.get('preferred_model'),
577
+ 'context_focus': data.get('context_focus'),
578
+ 'voice_accent': data.get('voice_accent'),
579
+ 'daily_goal': data.get('daily_goal', 10),
580
+ 'notification_enabled': data.get('notification_enabled', True)
581
+ }
582
+
583
+ if update_user_settings(user['id'], settings):
584
+ return jsonify({'message': 'Settings updated successfully'})
585
+ else:
586
+ return jsonify({'error': 'Failed to update settings'}), 500
587
+
588
+ @app.route('/user/flashcards', methods=['GET', 'POST'])
589
+ @login_required
590
+ def user_flashcards():
591
+ """Get user flashcards or save new flashcard"""
592
+ user = get_current_user()
593
+ if not user:
594
+ return jsonify({'error': 'User not found'}), 404
595
+
596
+ if request.method == 'GET':
597
+ flashcards = get_user_flashcards(user['id'])
598
+ return jsonify({'flashcards': flashcards})
599
+
600
+ elif request.method == 'POST':
601
+ data = request.get_json()
602
+ if save_user_flashcard(user['id'], data):
603
+ return jsonify({'message': 'Flashcard saved successfully'})
604
+ else:
605
+ return jsonify({'error': 'Failed to save flashcard'}), 500
606
+
607
+ @app.route('/auth/check', methods=['GET'])
608
+ def check_auth():
609
+ """Check if user is authenticated"""
610
+ user = get_current_user()
611
+ if user:
612
+ settings = get_user_settings(user['id'])
613
+ return jsonify({
614
+ 'authenticated': True,
615
+ 'user': {
616
+ 'id': user['id'],
617
+ 'email': user['email'],
618
+ 'settings': settings
619
+ }
620
+ })
621
+ else:
622
+ return jsonify({'authenticated': False})
623
+
624
+ # --- MODIFIED EXISTING ROUTES TO SUPPORT USER DATA ---
625
+
626
+ # Override the original explain-proxy to save flashcards for logged-in users
627
+ @app.route('/explain-proxy', methods=['POST'])
628
+ def explain_proxy():
629
+ data = request.get_json()
630
+ model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1)
631
+ context_focus = data.get('context_focus', 'General/Social')
632
+ custom_prompt = data.get('custom_prompt', None)
633
+ word = data.get('word', '').strip()
634
+ context = data.get('context', '')
635
+ for_flashcard = data.get('for_flashcard', False)
636
+
637
+ if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client):
638
+ return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503
639
+
640
+ system_instruction_base = f"You are a professional English tutor. The user's study focus is '{context_focus}'. All your responses must be in ENGLISH."
641
+ try:
642
+ if custom_prompt:
643
+ activity_text = get_ai_text_response(model_provider, model_name, system_instruction_base, custom_prompt)
644
+ return jsonify({"explanation": activity_text})
645
+
646
+ if not word: return jsonify({"error": "No word selected."}), 400
647
+
648
+ if for_flashcard:
649
+ schema = {"type": "object", "properties": {"term": {"type": "string"}, "translation": {"type": "string"}, "context_sentence": {"type": "string"}, "gapped_sentence": {"type": "string"}, "definition": {"type": "string"}}, "required": ["term", "translation", "context_sentence", "gapped_sentence", "definition"]}
650
+ prompt = f"Analyze '{word}' in context: '{context}'. Generate a JSON for a flashcard. The 'gapped_sentence' must replace '{word}' with '______________'."
651
+ flashcard_data = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt, json_schema=schema)
652
+
653
+ # Save flashcard for logged-in users
654
+ user = get_current_user()
655
+ if user:
656
+ save_user_flashcard(user['id'], flashcard_data)
657
+
658
+ return jsonify(flashcard_data)
659
+ else:
660
+ prompt = f"Analyze '{word}' in context: '{context}'. Provide a one-sentence English explanation, then '---', then the Portuguese translation."
661
+ parts = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt).split('---', 1)
662
+ return jsonify({"explanation": parts[0].strip(), "translation": parts[1].strip() if len(parts) > 1 else 'N/A'})
663
+ except Exception as e:
664
+ print(f"AI ANALYSIS ERROR in /explain-proxy: {e}")
665
+ return jsonify({"error": f"AI analysis failed: {e}"}), 500
666
+
667
+ # --- CONTENT CURATION ROUTES ---
668
+
669
+ @app.route('/content/search', methods=['POST'])
670
+ @login_required
671
+ def search_content():
672
+ """Search for content based on user interests"""
673
+ try:
674
+ user = get_current_user()
675
+ if not user:
676
+ return jsonify({'error': 'User not found'}), 404
677
+
678
+ data = request.get_json()
679
+ query = data.get('query', '')
680
+ category = data.get('category', '')
681
+
682
+ # Get user settings and interests
683
+ settings = get_user_settings(user['id'])
684
+ interests = get_user_interests(user['id'])
685
+
686
+ if not interests and query:
687
+ # Use query as interest if no interests set
688
+ interests = {query: 1.0}
689
+
690
+ english_level = settings.get('english_level', 'B1') if settings else 'B1'
691
+ context_focus = settings.get('context_focus', 'General/Social') if settings else 'General/Social'
692
+
693
+ # Search for content
694
+ results = content_curator.search_content(
695
+ interests=list(interests.keys()) if interests else [query],
696
+ english_level=english_level,
697
+ context_focus=context_focus,
698
+ limit=10
699
+ )
700
+
701
+ return jsonify({'results': results})
702
+
703
+ except Exception as e:
704
+ print(f"Content search error: {e}")
705
+ return jsonify({'error': 'Content search failed'}), 500
706
+
707
+ @app.route('/content/extract', methods=['POST'])
708
+ @login_required
709
+ def extract_content():
710
+ """Extract content from URL"""
711
+ try:
712
+ data = request.get_json()
713
+ url = data.get('url', '')
714
+
715
+ if not url:
716
+ return jsonify({'error': 'URL required'}), 400
717
+
718
+ result = content_curator.extract_content_from_url(url)
719
+ return jsonify(result)
720
+
721
+ except Exception as e:
722
+ print(f"Content extraction error: {e}")
723
+ return jsonify({'error': 'Content extraction failed'}), 500
724
+
725
+ @app.route('/content/save', methods=['POST'])
726
+ @login_required
727
+ def save_content():
728
+ """Save content/article for user"""
729
+ try:
730
+ user = get_current_user()
731
+ if not user:
732
+ return jsonify({'error': 'User not found'}), 404
733
+
734
+ data = request.get_json()
735
+ title = data.get('title', '')
736
+ content = data.get('content', '')
737
+ source_url = data.get('source_url')
738
+ source_type = data.get('source_type', 'manual')
739
+ category = data.get('category')
740
+
741
+ if not title or not content:
742
+ return jsonify({'error': 'Title and content required'}), 400
743
+
744
+ result = save_user_article(user['id'], title, content, source_url, source_type, category)
745
+
746
+ if result['success']:
747
+ # Record analytics
748
+ record_analytics_metric(user['id'], 'content_saved', 1)
749
+ return jsonify({'message': 'Content saved successfully', 'article_id': result['article_id']})
750
+ else:
751
+ return jsonify({'error': result['message']}), 500
752
+
753
+ except Exception as e:
754
+ print(f"Save content error: {e}")
755
+ return jsonify({'error': 'Failed to save content'}), 500
756
+
757
+ @app.route('/content/articles', methods=['GET'])
758
+ @login_required
759
+ def get_articles():
760
+ """Get user's saved articles"""
761
+ try:
762
+ user = get_current_user()
763
+ if not user:
764
+ return jsonify({'error': 'User not found'}), 404
765
+
766
+ category = request.args.get('category')
767
+ limit = int(request.args.get('limit', 50))
768
+
769
+ articles = get_user_articles(user['id'], category, limit)
770
+ return jsonify({'articles': articles})
771
+
772
+ except Exception as e:
773
+ print(f"Get articles error: {e}")
774
+ return jsonify({'error': 'Failed to get articles'}), 500
775
+
776
+ @app.route('/content/interests', methods=['GET', 'POST'])
777
+ @login_required
778
+ def manage_interests():
779
+ """Get or update user interests"""
780
+ try:
781
+ user = get_current_user()
782
+ if not user:
783
+ return jsonify({'error': 'User not found'}), 404
784
+
785
+ if request.method == 'GET':
786
+ interests = get_user_interests(user['id'])
787
+ return jsonify({'interests': interests})
788
+
789
+ elif request.method == 'POST':
790
+ data = request.get_json()
791
+ interests = data.get('interests', {})
792
+
793
+ if update_user_interests(user['id'], interests):
794
+ return jsonify({'message': 'Interests updated successfully'})
795
+ else:
796
+ return jsonify({'error': 'Failed to update interests'}), 500
797
+
798
+ except Exception as e:
799
+ print(f"Manage interests error: {e}")
800
+ return jsonify({'error': 'Failed to manage interests'}), 500
801
+
802
+ @app.route('/content/recommendations', methods=['GET'])
803
+ @login_required
804
+ def get_recommendations():
805
+ """Get AI-powered content recommendations"""
806
+ try:
807
+ user = get_current_user()
808
+ if not user:
809
+ return jsonify({'error': 'User not found'}), 404
810
+
811
+ # Get user data
812
+ interests = get_user_interests(user['id'])
813
+ recent_articles = get_user_articles(user['id'], limit=10)
814
+ settings = get_user_settings(user['id'])
815
+
816
+ english_level = settings.get('english_level', 'B1') if settings else 'B1'
817
+ context_focus = settings.get('context_focus', 'General/Social') if settings else 'General/Social'
818
+
819
+ # Generate recommendations
820
+ recommendations = content_curator.generate_personalized_recommendations(
821
+ interests, recent_articles, english_level, context_focus, user['id']
822
+ )
823
+
824
+ return jsonify({'recommendations': recommendations})
825
+
826
+ except Exception as e:
827
+ print(f"Recommendations error: {e}")
828
+ return jsonify({'error': 'Failed to get recommendations'}), 500
829
+
830
+ @app.route('/content/analyze', methods=['POST'])
831
+ @login_required
832
+ def analyze_content():
833
+ """Analyze content for learning insights"""
834
+ try:
835
+ user = get_current_user()
836
+ if not user:
837
+ return jsonify({'error': 'User not found'}), 404
838
+
839
+ data = request.get_json()
840
+ content = data.get('content', '')
841
+
842
+ if not content:
843
+ return jsonify({'error': 'Content required'}), 400
844
+
845
+ settings = get_user_settings(user['id'])
846
+ english_level = settings.get('english_level', 'B1') if settings else 'B1'
847
+
848
+ analysis = content_curator.analyze_content_for_learning(content, english_level)
849
+
850
+ return jsonify({'analysis': analysis})
851
+
852
+ except Exception as e:
853
+ print(f"Content analysis error: {e}")
854
+ return jsonify({'error': 'Content analysis failed'}), 500
855
+
856
+ # --- STUDY PLANNING ROUTES ---
857
+
858
+ @app.route('/study/plans', methods=['GET', 'POST'])
859
+ @login_required
860
+ def manage_study_plans():
861
+ """Get or create study plans"""
862
+ try:
863
+ user = get_current_user()
864
+ if not user:
865
+ return jsonify({'error': 'User not found'}), 404
866
+
867
+ if request.method == 'GET':
868
+ plans = get_user_study_plans(user['id'])
869
+ return jsonify({'plans': plans})
870
+
871
+ elif request.method == 'POST':
872
+ data = request.get_json()
873
+ plan_name = data.get('plan_name', '')
874
+ target_level = data.get('target_level', 'B2')
875
+ current_level = data.get('current_level', 'B1')
876
+ objectives = json.dumps(data.get('objectives', []))
877
+ weekly_hours = data.get('weekly_hours', 5)
878
+
879
+ if not plan_name:
880
+ return jsonify({'error': 'Plan name required'}), 400
881
+
882
+ result = create_study_plan(user['id'], plan_name, target_level, current_level, objectives, weekly_hours)
883
+
884
+ if result['success']:
885
+ return jsonify({'message': 'Study plan created', 'plan_id': result['plan_id']})
886
+ else:
887
+ return jsonify({'error': result['message']}), 500
888
+
889
+ except Exception as e:
890
+ print(f"Study plans error: {e}")
891
+ return jsonify({'error': 'Failed to manage study plans'}), 500
892
+
893
+ @app.route('/analytics/dashboard', methods=['GET'])
894
+ @login_required
895
+ def analytics_dashboard():
896
+ """Get analytics dashboard data"""
897
+ try:
898
+ user = get_current_user()
899
+ if not user:
900
+ return jsonify({'error': 'User not found'}), 404
901
+
902
+ days = int(request.args.get('days', 30))
903
+
904
+ # Get various analytics
905
+ analytics_data = {
906
+ 'flashcards_created': get_user_analytics(user['id'], 'flashcards_created', days),
907
+ 'content_saved': get_user_analytics(user['id'], 'content_saved', days),
908
+ 'study_sessions': get_user_analytics(user['id'], 'study_session', days),
909
+ 'total_flashcards': len(get_user_flashcards(user['id'], 1000)),
910
+ 'total_articles': len(get_user_articles(user['id'], limit=1000)),
911
+ 'user_level': get_user_settings(user['id']).get('english_level', 'B1')
912
+ }
913
+
914
+ return jsonify({'analytics': analytics_data})
915
+
916
+ except Exception as e:
917
+ print(f"Analytics error: {e}")
918
+ return jsonify({'error': 'Failed to get analytics'}), 500
919
+
920
+ # --- STUDY PLANNER ROUTES ---
921
+
922
+ @app.route('/study-plan/create', methods=['POST'])
923
+ @login_required
924
+ def create_study_plan_route():
925
+ """Create a personalized study plan"""
926
+ try:
927
+ user = get_current_user()
928
+ if not user:
929
+ return jsonify({'error': 'User not found'}), 404
930
+
931
+ data = request.get_json()
932
+
933
+ # Get user settings and interests
934
+ user_settings = get_user_settings(user['id'])
935
+ user_interests = get_user_interests(user['id'])
936
+
937
+ # Prepare data for study planner
938
+ planner_data = {
939
+ 'english_level': data.get('current_level') or user_settings.get('english_level', 'B1'),
940
+ 'target_level': data.get('target_level', 'B2'),
941
+ 'weekly_hours': int(data.get('weekly_hours', 5)),
942
+ 'context_focus': data.get('context_focus') or user_settings.get('context_focus', 'General/Social'),
943
+ 'interests': user_interests,
944
+ 'study_goals': data.get('study_goals', [])
945
+ }
946
+
947
+ # Generate the plan
948
+ result = study_planner.generate_personalized_plan(planner_data)
949
+
950
+ if result['success']:
951
+ plan = result['plan']
952
+
953
+ # Save to database
954
+ plan_id = create_study_plan(
955
+ user['id'],
956
+ plan['target_level'],
957
+ plan['weekly_hours'],
958
+ plan['estimated_weeks'],
959
+ json.dumps(plan)
960
+ )
961
+
962
+ plan['id'] = plan_id
963
+
964
+ # Record analytics
965
+ record_analytics_metric(user['id'], 'study_plan_created', 1)
966
+
967
+ return jsonify({'success': True, 'plan': plan})
968
+ else:
969
+ return jsonify({'success': False, 'error': result['error']}), 500
970
+
971
+ except Exception as e:
972
+ print(f"Study plan creation error: {e}")
973
+ return jsonify({'error': 'Failed to create study plan'}), 500
974
+
975
+ @app.route('/study-plan/current', methods=['GET'])
976
+ @login_required
977
+ def get_current_study_plan():
978
+ """Get user's current study plan"""
979
+ try:
980
+ user = get_current_user()
981
+ if not user:
982
+ return jsonify({'error': 'User not found'}), 404
983
+
984
+ plans = get_user_study_plans(user['id'])
985
+
986
+ if plans:
987
+ # Get the most recent active plan
988
+ current_plan = plans[0] # Assuming most recent first
989
+
990
+ # Parse the plan data
991
+ plan_data = json.loads(current_plan['plan_data'])
992
+
993
+ # Add database ID
994
+ plan_data['db_id'] = current_plan['id']
995
+
996
+ # Get activities for this plan
997
+ activities = get_study_activities(current_plan['id'])
998
+ plan_data['completed_activities'] = activities
999
+
1000
+ return jsonify({'success': True, 'plan': plan_data})
1001
+ else:
1002
+ return jsonify({'success': True, 'plan': None})
1003
+
1004
+ except Exception as e:
1005
+ print(f"Get study plan error: {e}")
1006
+ return jsonify({'error': 'Failed to get study plan'}), 500
1007
+
1008
+ @app.route('/study-plan/activity/complete', methods=['POST'])
1009
+ @login_required
1010
+ def complete_study_activity():
1011
+ """Mark a study activity as completed"""
1012
+ try:
1013
+ user = get_current_user()
1014
+ if not user:
1015
+ return jsonify({'error': 'User not found'}), 404
1016
+
1017
+ data = request.get_json()
1018
+ plan_id = data.get('plan_id')
1019
+ activity_id = data.get('activity_id')
1020
+ duration_minutes = data.get('duration_minutes', 0)
1021
+ notes = data.get('notes', '')
1022
+
1023
+ if not plan_id or not activity_id:
1024
+ return jsonify({'error': 'Missing plan_id or activity_id'}), 400
1025
+
1026
+ # Add activity completion
1027
+ add_study_activity(plan_id, activity_id, duration_minutes, notes)
1028
+
1029
+ # Record analytics
1030
+ record_analytics_metric(user['id'], 'study_activity_completed', 1)
1031
+ record_analytics_metric(user['id'], 'study_time_minutes', duration_minutes)
1032
+
1033
+ return jsonify({'success': True})
1034
+
1035
+ except Exception as e:
1036
+ print(f"Complete activity error: {e}")
1037
+ return jsonify({'error': 'Failed to complete activity'}), 500
1038
+
1039
+ @app.route('/study-plan/progress', methods=['GET'])
1040
+ @login_required
1041
+ def get_study_progress():
1042
+ """Get study plan progress analytics"""
1043
+ try:
1044
+ user = get_current_user()
1045
+ if not user:
1046
+ return jsonify({'error': 'User not found'}), 404
1047
+
1048
+ plans = get_user_study_plans(user['id'])
1049
+
1050
+ if not plans:
1051
+ return jsonify({'success': True, 'progress': None})
1052
+
1053
+ current_plan = plans[0]
1054
+ plan_data = json.loads(current_plan['plan_data'])
1055
+ activities = get_study_activities(current_plan['id'])
1056
+
1057
+ # Calculate progress
1058
+ total_activities = len(plan_data.get('activities', []))
1059
+ completed_activities = len(activities)
1060
+
1061
+ progress_data = {
1062
+ 'total_activities': total_activities,
1063
+ 'completed_activities': completed_activities,
1064
+ 'completion_percentage': (completed_activities / max(total_activities, 1)) * 100,
1065
+ 'estimated_weeks': plan_data.get('estimated_weeks', 0),
1066
+ 'weeks_elapsed': max(1, (datetime.now() - datetime.fromisoformat(current_plan['created_at'])).days // 7),
1067
+ 'target_level': plan_data.get('target_level', 'B2'),
1068
+ 'weekly_hours': plan_data.get('weekly_hours', 5),
1069
+ 'recent_activities': activities[-10:] if activities else [] # Last 10 activities
1070
+ }
1071
+
1072
+ return jsonify({'success': True, 'progress': progress_data})
1073
+
1074
+ except Exception as e:
1075
+ print(f"Study progress error: {e}")
1076
+ return jsonify({'error': 'Failed to get study progress'}), 500
1077
+
1078
+ # --- ADMIN ROUTES ---
1079
+
1080
+ @app.route('/admin/login', methods=['POST'])
1081
+ def admin_login():
1082
+ """Admin login endpoint"""
1083
+ try:
1084
+ data = request.get_json()
1085
+ username = data.get('username')
1086
+ password = data.get('password')
1087
+
1088
+ print(f"Admin login attempt - Username: {username}")
1089
+
1090
+ if admin_manager.login_admin(username, password):
1091
+ print(f"Admin login successful - Session ID: {session.get('_id', 'no-id')}")
1092
+ print(f"Session data after login: {dict(session)}")
1093
+ return jsonify({'success': True, 'message': 'Admin logged in successfully'})
1094
+ else:
1095
+ print(f"Admin login failed - Invalid credentials for: {username}")
1096
+ return jsonify({'success': False, 'error': 'Invalid credentials'}), 401
1097
+
1098
+ except Exception as e:
1099
+ print(f"Admin login error: {e}")
1100
+ return jsonify({'error': 'Admin login failed'}), 500
1101
+
1102
+ @app.route('/admin/logout', methods=['POST'])
1103
+ @admin_required
1104
+ def admin_logout():
1105
+ """Admin logout endpoint"""
1106
+ try:
1107
+ admin_manager.logout_admin()
1108
+ return jsonify({'success': True, 'message': 'Admin logged out successfully'})
1109
+ except Exception as e:
1110
+ print(f"Admin logout error: {e}")
1111
+ return jsonify({'error': 'Admin logout failed'}), 500
1112
+
1113
+ @app.route('/admin/check', methods=['GET'])
1114
+ def admin_check():
1115
+ """Check admin authentication status"""
1116
+ try:
1117
+ is_authenticated = admin_manager.is_admin_logged_in()
1118
+ username = session.get('admin_username')
1119
+
1120
+ print(f"Admin check - Authenticated: {is_authenticated}, Username: {username}")
1121
+ print(f"Current session data: {dict(session)}")
1122
+ print(f"Session ID: {session.get('_id', 'no-session-id')}")
1123
+
1124
+ return jsonify({
1125
+ 'authenticated': is_authenticated,
1126
+ 'username': username if is_authenticated else None,
1127
+ 'session_id': session.get('_id', 'no-session-id'),
1128
+ 'debug_session_keys': list(session.keys())
1129
+ })
1130
+ except Exception as e:
1131
+ print(f"Admin check error: {e}")
1132
+ return jsonify({'authenticated': False})
1133
+
1134
+ # Debug route to test sessions
1135
+ @app.route('/admin/debug-session', methods=['GET', 'POST'])
1136
+ def debug_session():
1137
+ """Debug session functionality"""
1138
+ if request.method == 'POST':
1139
+ session['debug_test'] = 'session_working'
1140
+ session.permanent = True
1141
+ return jsonify({
1142
+ 'message': 'Session test value set',
1143
+ 'session_data': dict(session)
1144
+ })
1145
+ else:
1146
+ test_value = session.get('debug_test', 'not_found')
1147
+ return jsonify({
1148
+ 'test_value': test_value,
1149
+ 'session_data': dict(session),
1150
+ 'session_id': session.get('_id', 'no-session-id')
1151
+ })
1152
+
1153
+ @app.route('/admin/dashboard', methods=['GET'])
1154
+ @admin_required
1155
+ def admin_dashboard():
1156
+ """Get admin dashboard data"""
1157
+ try:
1158
+ stats = admin_manager.get_system_stats()
1159
+ return jsonify({'success': True, 'stats': stats})
1160
+ except Exception as e:
1161
+ print(f"Admin dashboard error: {e}")
1162
+ return jsonify({'error': 'Failed to load dashboard'}), 500
1163
+
1164
+ @app.route('/admin/users', methods=['GET'])
1165
+ @admin_required
1166
+ def admin_get_users():
1167
+ """Get paginated list of users"""
1168
+ try:
1169
+ page = int(request.args.get('page', 1))
1170
+ per_page = int(request.args.get('per_page', 20))
1171
+
1172
+ users_data = admin_manager.get_all_users(page, per_page)
1173
+ return jsonify({'success': True, 'data': users_data})
1174
+ except Exception as e:
1175
+ print(f"Admin get users error: {e}")
1176
+ return jsonify({'error': 'Failed to get users'}), 500
1177
+
1178
+ @app.route('/admin/users/<int:user_id>', methods=['GET'])
1179
+ @admin_required
1180
+ def admin_get_user_details(user_id):
1181
+ """Get detailed information about a user"""
1182
+ try:
1183
+ user_details = admin_manager.get_user_details(user_id)
1184
+ if user_details:
1185
+ return jsonify({'success': True, 'user': user_details})
1186
+ else:
1187
+ return jsonify({'error': 'User not found'}), 404
1188
+ except Exception as e:
1189
+ print(f"Admin get user details error: {e}")
1190
+ return jsonify({'error': 'Failed to get user details'}), 500
1191
+
1192
+ @app.route('/admin/users/<int:user_id>', methods=['DELETE'])
1193
+ @admin_required
1194
+ def admin_delete_user(user_id):
1195
+ """Delete a user and all associated data"""
1196
+ try:
1197
+ if admin_manager.delete_user(user_id):
1198
+ return jsonify({'success': True, 'message': 'User deleted successfully'})
1199
+ else:
1200
+ return jsonify({'error': 'Failed to delete user'}), 500
1201
+ except Exception as e:
1202
+ print(f"Admin delete user error: {e}")
1203
+ return jsonify({'error': 'Failed to delete user'}), 500
1204
+
1205
+ @app.route('/admin/database/schema', methods=['GET'])
1206
+ @admin_required
1207
+ def admin_get_database_schema():
1208
+ """Get database schema information"""
1209
+ try:
1210
+ schema = admin_manager.get_database_schema()
1211
+ return jsonify({'success': True, 'schema': schema})
1212
+ except Exception as e:
1213
+ print(f"Admin get schema error: {e}")
1214
+ return jsonify({'error': 'Failed to get database schema'}), 500
1215
+
1216
+ @app.route('/admin/token-usage', methods=['POST'])
1217
+ def record_token_usage():
1218
+ """Record token usage (called by AI functions)"""
1219
+ try:
1220
+ data = request.get_json()
1221
+ user_id = data.get('user_id')
1222
+ api_provider = data.get('api_provider')
1223
+ input_tokens = data.get('input_tokens', 0)
1224
+ output_tokens = data.get('output_tokens', 0)
1225
+ operation_type = data.get('operation_type', 'unknown')
1226
+
1227
+ admin_manager.record_token_usage(
1228
+ user_id, api_provider, input_tokens, output_tokens, operation_type
1229
+ )
1230
+
1231
+ return jsonify({'success': True})
1232
+ except Exception as e:
1233
+ print(f"Token usage recording error: {e}")
1234
+ return jsonify({'error': 'Failed to record token usage'}), 500
1235
+
1236
+ @app.route('/admin/export/users', methods=['GET'])
1237
+ @admin_required
1238
+ def export_users():
1239
+ """Export users data as CSV"""
1240
+ try:
1241
+ import csv
1242
+ from io import StringIO
1243
+
1244
+ users_data = admin_manager.get_all_users(page=1, per_page=10000) # Get all users
1245
+
1246
+ output = StringIO()
1247
+ writer = csv.writer(output)
1248
+
1249
+ # Write header
1250
+ writer.writerow(['ID', 'Email', 'Created At', 'Email Confirmed', 'Last Login', 'Sessions', 'Flashcards', 'Articles'])
1251
+
1252
+ # Write data
1253
+ for user in users_data['users']:
1254
+ writer.writerow([
1255
+ user['id'],
1256
+ user['email'],
1257
+ user['created_at'],
1258
+ user['email_confirmed'],
1259
+ user['last_login'] or 'Never',
1260
+ user['session_count'],
1261
+ user['flashcard_count'],
1262
+ user['article_count']
1263
+ ])
1264
+
1265
+ output.seek(0)
1266
+
1267
+ return Response(
1268
+ output.getvalue(),
1269
+ mimetype='text/csv',
1270
+ headers={'Content-Disposition': f'attachment; filename=users_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'}
1271
+ )
1272
+
1273
+ except Exception as e:
1274
+ print(f"Export users error: {e}")
1275
+ return jsonify({'error': 'Failed to export users'}), 500
1276
+
1277
+ @app.route('/admin/export/tokens', methods=['GET'])
1278
+ @admin_required
1279
+ def export_token_usage():
1280
+ """Export token usage data as CSV"""
1281
+ try:
1282
+ import csv
1283
+ from io import StringIO
1284
+
1285
+ conn = get_db_connection()
1286
+ cursor = conn.cursor()
1287
+
1288
+ cursor.execute("""
1289
+ SELECT t.created_at, u.email, t.api_provider, t.input_tokens,
1290
+ t.output_tokens, t.tokens_used, t.operation_type
1291
+ FROM token_usage t
1292
+ LEFT JOIN users u ON t.user_id = u.id
1293
+ ORDER BY t.created_at DESC
1294
+ """)
1295
+
1296
+ token_data = cursor.fetchall()
1297
+ conn.close()
1298
+
1299
+ output = StringIO()
1300
+ writer = csv.writer(output)
1301
+
1302
+ # Write header
1303
+ writer.writerow(['Date', 'User Email', 'Provider', 'Input Tokens', 'Output Tokens', 'Total Tokens', 'Operation'])
1304
+
1305
+ # Write data
1306
+ for row in token_data:
1307
+ writer.writerow(row)
1308
+
1309
+ output.seek(0)
1310
+
1311
+ return Response(
1312
+ output.getvalue(),
1313
+ mimetype='text/csv',
1314
+ headers={'Content-Disposition': f'attachment; filename=token_usage_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'}
1315
+ )
1316
+
1317
+ except Exception as e:
1318
+ print(f"Export tokens error: {e}")
1319
+ return jsonify({'error': 'Failed to export token usage'}), 500
1320
+
1321
+ @app.route('/admin/system/health', methods=['GET'])
1322
+ @admin_required
1323
+ def get_system_health():
1324
+ """Get system health metrics"""
1325
+ try:
1326
+ health = admin_manager.get_system_health()
1327
+ return jsonify({'success': True, 'health': health})
1328
+ except Exception as e:
1329
+ print(f"System health error: {e}")
1330
+ return jsonify({'error': 'Failed to get system health'}), 500
1331
+
1332
+ @app.route('/admin/system/alerts', methods=['GET'])
1333
+ @admin_required
1334
+ def get_system_alerts():
1335
+ """Get system alerts"""
1336
+ try:
1337
+ alerts = admin_manager.check_system_alerts()
1338
+ return jsonify({'success': True, 'alerts': alerts})
1339
+ except Exception as e:
1340
+ print(f"System alerts error: {e}")
1341
+ return jsonify({'error': 'Failed to get system alerts'}), 500
1342
+
1343
+ @app.route('/admin')
1344
+ def admin_interface():
1345
+ """Serve admin interface"""
1346
+ return send_file('templates/admin.html')
1347
+
1348
+ @app.route('/')
1349
+ def root():
1350
+ return send_file('templates/index.html')
1351
+
1352
+ # Evitar execuΓ§Γ£o automΓ‘tica quando importado
1353
+ if __name__ == '__main__':
1354
+ print("⚠️ flask_app.py executado diretamente")
1355
+ print("πŸ’‘ Use app.py para HF Spaces ou execute como mΓ³dulo")
1356
+ app.run(host='0.0.0.0', port=5000, debug=True)
study_planner.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # study_planner.py
2
+ import json
3
+ from datetime import datetime, timedelta, date
4
+ from typing import Dict, List, Any
5
+ import logging
6
+ from groq import Groq
7
+ import google.generativeai as genai
8
+ import os
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Initialize AI clients (reuse from main app)
13
+ groq_client = None
14
+ genai_client = None
15
+
16
+ try:
17
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
18
+ if GROQ_API_KEY:
19
+ groq_client = Groq(api_key=GROQ_API_KEY)
20
+ except Exception as e:
21
+ logger.warning(f"Groq client not available: {e}")
22
+
23
+ try:
24
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
25
+ if GEMINI_API_KEY:
26
+ genai.configure(api_key=GEMINI_API_KEY)
27
+ genai_client = genai
28
+ except Exception as e:
29
+ logger.warning(f"Gemini client not available: {e}")
30
+
31
+ class StudyPlanner:
32
+ def __init__(self):
33
+ self.level_progression = {
34
+ 'A1': {'next': 'A2', 'weeks': 12, 'focus': ['basic vocabulary', 'present tense', 'introductions']},
35
+ 'A2': {'next': 'B1', 'weeks': 16, 'focus': ['past tense', 'future tense', 'everyday situations']},
36
+ 'B1': {'next': 'B2', 'weeks': 20, 'focus': ['conditional', 'complex sentences', 'opinions']},
37
+ 'B2': {'next': 'C1', 'weeks': 24, 'focus': ['subjunctive', 'formal writing', 'presentations']},
38
+ 'C1': {'next': 'C2', 'weeks': 28, 'focus': ['nuanced expressions', 'academic writing', 'debates']},
39
+ 'C2': {'next': 'C2', 'weeks': 32, 'focus': ['native-like fluency', 'specialized topics', 'literature']}
40
+ }
41
+
42
+ self.activity_types = {
43
+ 'reading': {
44
+ 'icon': 'πŸ“š',
45
+ 'min_duration': 20,
46
+ 'max_duration': 45,
47
+ 'difficulty_scaling': True,
48
+ 'description': 'Read articles and texts'
49
+ },
50
+ 'flashcards': {
51
+ 'icon': 'πŸƒ',
52
+ 'min_duration': 10,
53
+ 'max_duration': 25,
54
+ 'difficulty_scaling': False,
55
+ 'description': 'Review vocabulary flashcards'
56
+ },
57
+ 'conversation': {
58
+ 'icon': 'πŸ’¬',
59
+ 'min_duration': 15,
60
+ 'max_duration': 30,
61
+ 'difficulty_scaling': True,
62
+ 'description': 'Practice speaking and conversation'
63
+ },
64
+ 'writing': {
65
+ 'icon': '✍️',
66
+ 'min_duration': 15,
67
+ 'max_duration': 40,
68
+ 'difficulty_scaling': True,
69
+ 'description': 'Complete writing exercises'
70
+ },
71
+ 'listening': {
72
+ 'icon': '🎧',
73
+ 'min_duration': 15,
74
+ 'max_duration': 30,
75
+ 'difficulty_scaling': True,
76
+ 'description': 'Listen to audio content'
77
+ },
78
+ 'grammar': {
79
+ 'icon': 'πŸ“',
80
+ 'min_duration': 10,
81
+ 'max_duration': 25,
82
+ 'difficulty_scaling': True,
83
+ 'description': 'Study grammar rules and patterns'
84
+ }
85
+ }
86
+
87
+ def generate_personalized_plan(self, user_data: Dict[str, Any]) -> Dict[str, Any]:
88
+ """Generate a comprehensive study plan based on user data"""
89
+ try:
90
+ current_level = user_data.get('english_level', 'B1')
91
+ target_level = user_data.get('target_level', 'B2')
92
+ weekly_hours = user_data.get('weekly_hours', 5)
93
+ interests = user_data.get('interests', {})
94
+ context_focus = user_data.get('context_focus', 'General/Social')
95
+ study_goals = user_data.get('study_goals', [])
96
+
97
+ # Calculate timeline
98
+ timeline = self._calculate_study_timeline(current_level, target_level, weekly_hours)
99
+
100
+ # Generate weekly structure
101
+ weekly_structure = self._create_weekly_structure(weekly_hours, current_level, context_focus)
102
+
103
+ # Create specific activities
104
+ activities = self._generate_weekly_activities(
105
+ weekly_structure, interests, current_level, context_focus, study_goals
106
+ )
107
+
108
+ # Generate AI-powered study tips
109
+ study_tips = self._generate_ai_study_tips(user_data)
110
+
111
+ plan = {
112
+ 'id': f"plan_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
113
+ 'created_at': datetime.now().isoformat(),
114
+ 'current_level': current_level,
115
+ 'target_level': target_level,
116
+ 'weekly_hours': weekly_hours,
117
+ 'estimated_weeks': timeline['weeks'],
118
+ 'completion_date': timeline['completion_date'],
119
+ 'weekly_structure': weekly_structure,
120
+ 'activities': activities,
121
+ 'study_tips': study_tips,
122
+ 'milestones': self._create_milestones(current_level, target_level, timeline['weeks']),
123
+ 'adaptations': self._suggest_adaptations(user_data)
124
+ }
125
+
126
+ return {'success': True, 'plan': plan}
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error generating study plan: {e}")
130
+ return {'success': False, 'error': str(e)}
131
+
132
+ def _calculate_study_timeline(self, current_level: str, target_level: str, weekly_hours: int) -> Dict[str, Any]:
133
+ """Calculate realistic timeline for reaching target level"""
134
+ try:
135
+ current_info = self.level_progression.get(current_level, self.level_progression['B1'])
136
+ base_weeks = current_info['weeks']
137
+
138
+ # Adjust based on weekly hours (baseline is 5 hours/week)
139
+ hour_multiplier = 5 / max(weekly_hours, 1)
140
+ adjusted_weeks = int(base_weeks * hour_multiplier)
141
+
142
+ # If targeting multiple levels ahead, add additional time
143
+ level_order = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
144
+ current_idx = level_order.index(current_level) if current_level in level_order else 2
145
+ target_idx = level_order.index(target_level) if target_level in level_order else 3
146
+
147
+ if target_idx > current_idx + 1:
148
+ # Multiple levels - add 20% more time
149
+ adjusted_weeks = int(adjusted_weeks * 1.2 * (target_idx - current_idx))
150
+
151
+ completion_date = (datetime.now() + timedelta(weeks=adjusted_weeks)).date()
152
+
153
+ return {
154
+ 'weeks': adjusted_weeks,
155
+ 'completion_date': completion_date.isoformat(),
156
+ 'intensity': 'High' if weekly_hours > 7 else 'Medium' if weekly_hours > 4 else 'Light'
157
+ }
158
+
159
+ except Exception as e:
160
+ logger.error(f"Error calculating timeline: {e}")
161
+ return {'weeks': 16, 'completion_date': (datetime.now() + timedelta(weeks=16)).date().isoformat()}
162
+
163
+ def _create_weekly_structure(self, weekly_hours: int, level: str, context: str) -> Dict[str, Any]:
164
+ """Create optimal weekly study structure"""
165
+ try:
166
+ # Base distribution percentages
167
+ distributions = {
168
+ 'A1': {'reading': 0.25, 'flashcards': 0.30, 'conversation': 0.20, 'writing': 0.15, 'grammar': 0.10},
169
+ 'A2': {'reading': 0.30, 'flashcards': 0.25, 'conversation': 0.20, 'writing': 0.15, 'grammar': 0.10},
170
+ 'B1': {'reading': 0.30, 'flashcards': 0.20, 'conversation': 0.25, 'writing': 0.20, 'listening': 0.05},
171
+ 'B2': {'reading': 0.25, 'flashcards': 0.15, 'conversation': 0.25, 'writing': 0.25, 'listening': 0.10},
172
+ 'C1': {'reading': 0.30, 'flashcards': 0.10, 'conversation': 0.25, 'writing': 0.25, 'listening': 0.10},
173
+ 'C2': {'reading': 0.35, 'flashcards': 0.05, 'conversation': 0.25, 'writing': 0.25, 'listening': 0.10}
174
+ }
175
+
176
+ base_dist = distributions.get(level, distributions['B1'])
177
+
178
+ # Adjust based on context
179
+ if context == 'Professional/Business':
180
+ base_dist['writing'] = min(base_dist['writing'] + 0.10, 0.40)
181
+ base_dist['reading'] = max(base_dist['reading'] - 0.05, 0.15)
182
+ base_dist['conversation'] = max(base_dist['conversation'] - 0.05, 0.15)
183
+ elif context == 'Technical/IT':
184
+ base_dist['reading'] = min(base_dist['reading'] + 0.10, 0.45)
185
+ base_dist['flashcards'] = min(base_dist['flashcards'] + 0.05, 0.35)
186
+ base_dist['conversation'] = max(base_dist['conversation'] - 0.10, 0.15)
187
+
188
+ # Convert to actual hours
189
+ weekly_structure = {}
190
+ total_minutes = weekly_hours * 60
191
+
192
+ for activity, percentage in base_dist.items():
193
+ minutes = int(total_minutes * percentage)
194
+ if minutes >= self.activity_types[activity]['min_duration']:
195
+ weekly_structure[activity] = {
196
+ 'minutes_per_week': minutes,
197
+ 'sessions_per_week': max(1, minutes // 30), # Aim for 30-min sessions
198
+ 'minutes_per_session': minutes // max(1, minutes // 30)
199
+ }
200
+
201
+ return weekly_structure
202
+
203
+ except Exception as e:
204
+ logger.error(f"Error creating weekly structure: {e}")
205
+ return {}
206
+
207
+ def _generate_weekly_activities(self, structure: Dict, interests: Dict, level: str, context: str, goals: List) -> List[Dict]:
208
+ """Generate specific weekly activities"""
209
+ activities = []
210
+
211
+ try:
212
+ for activity_type, schedule in structure.items():
213
+ activity_info = self.activity_types[activity_type]
214
+
215
+ for session in range(schedule['sessions_per_week']):
216
+ activity = {
217
+ 'id': f"{activity_type}_{session + 1}",
218
+ 'type': activity_type,
219
+ 'icon': activity_info['icon'],
220
+ 'title': f"{activity_info['description']}",
221
+ 'duration_minutes': schedule['minutes_per_session'],
222
+ 'difficulty': level,
223
+ 'context': context,
224
+ 'day_of_week': (session * 2) % 7, # Spread throughout week
225
+ 'specific_tasks': self._generate_specific_tasks(activity_type, level, context, interests, goals)
226
+ }
227
+ activities.append(activity)
228
+
229
+ # Sort by day of week
230
+ activities.sort(key=lambda x: x['day_of_week'])
231
+
232
+ return activities
233
+
234
+ except Exception as e:
235
+ logger.error(f"Error generating activities: {e}")
236
+ return []
237
+
238
+ def _generate_specific_tasks(self, activity_type: str, level: str, context: str, interests: Dict, goals: List) -> List[str]:
239
+ """Generate specific tasks for each activity type"""
240
+ tasks = []
241
+
242
+ try:
243
+ interest_topics = list(interests.keys())[:3] if interests else ['general topics']
244
+
245
+ if activity_type == 'reading':
246
+ tasks = [
247
+ f"Read a {context.lower()} article about {topic}" for topic in interest_topics
248
+ ] + [
249
+ f"Practice reading comprehension with {level}-level texts",
250
+ "Identify new vocabulary and create flashcards"
251
+ ]
252
+
253
+ elif activity_type == 'flashcards':
254
+ tasks = [
255
+ "Review previous day's vocabulary",
256
+ "Practice new words from recent reading",
257
+ f"Focus on {context.lower()} terminology"
258
+ ]
259
+
260
+ elif activity_type == 'conversation':
261
+ tasks = [
262
+ f"Discuss {topic} using {level}-level vocabulary" for topic in interest_topics[:2]
263
+ ] + [
264
+ "Practice pronunciation with AI feedback",
265
+ f"Role-play {context.lower()} scenarios"
266
+ ]
267
+
268
+ elif activity_type == 'writing':
269
+ tasks = [
270
+ f"Write a short text about {topic}" for topic in interest_topics[:1]
271
+ ] + [
272
+ f"Practice {context.lower()} writing format s",
273
+ "Get AI feedback on grammar and style"
274
+ ]
275
+
276
+ elif activity_type == 'listening':
277
+ tasks = [
278
+ f"Listen to content about {topic}" for topic in interest_topics[:2]
279
+ ] + [
280
+ "Practice with different accents",
281
+ "Take notes while listening"
282
+ ]
283
+
284
+ elif activity_type == 'grammar':
285
+ level_grammar = {
286
+ 'A1': ['present tense', 'basic sentence structure', 'personal pronouns'],
287
+ 'A2': ['past tense', 'future tense', 'comparatives'],
288
+ 'B1': ['present perfect', 'conditional sentences', 'passive voice'],
289
+ 'B2': ['subjunctive mood', 'complex sentences', 'reported speech'],
290
+ 'C1': ['advanced tenses', 'nuanced expressions', 'formal structures'],
291
+ 'C2': ['idiomatic expressions', 'stylistic variations', 'literary devices']
292
+ }
293
+
294
+ tasks = [f"Study {topic}" for topic in level_grammar.get(level, level_grammar['B1'])]
295
+
296
+ return tasks[:3] # Limit to 3 tasks per activity
297
+
298
+ except Exception as e:
299
+ logger.error(f"Error generating specific tasks: {e}")
300
+ return ["Complete activity as planned"]
301
+
302
+ def _generate_ai_study_tips(self, user_data: Dict) -> List[str]:
303
+ """Generate personalized study tips using AI"""
304
+ try:
305
+ if not groq_client and not genai_client:
306
+ return self._get_default_tips(user_data.get('english_level', 'B1'))
307
+
308
+ prompt = f"""
309
+ Generate 5 personalized English study tips for a user with these characteristics:
310
+ - Current Level: {user_data.get('english_level', 'B1')}
311
+ - Target Level: {user_data.get('target_level', 'B2')}
312
+ - Weekly Study Time: {user_data.get('weekly_hours', 5)} hours
313
+ - Context Focus: {user_data.get('context_focus', 'General/Social')}
314
+ - Interests: {', '.join(user_data.get('interests', {}).keys())}
315
+
316
+ Provide practical, actionable tips that are specific to their level and interests.
317
+ Format as a simple list of tips, each starting with an emoji.
318
+ """
319
+
320
+ response_text = None
321
+ if groq_client:
322
+ response = groq_client.chat.completions.create(
323
+ model="llama-3.1-8b-instant",
324
+ messages=[{"role": "user", "content": prompt}],
325
+ temperature=0.7
326
+ )
327
+ response_text = response.choices[0].message.content
328
+ elif genai_client:
329
+ model = genai_client.GenerativeModel('gemini-2.5-flash-latest')
330
+ response = model.generate_content(prompt)
331
+ response_text = response.text
332
+
333
+ if response_text:
334
+ # Extract tips from response
335
+ tips = [line.strip() for line in response_text.split('\n') if line.strip() and ('πŸ“š' in line or 'πŸ’‘' in line or '🎯' in line or '⭐' in line or 'πŸš€' in line)]
336
+ return tips[:5] if tips else self._get_default_tips(user_data.get('english_level', 'B1'))
337
+
338
+ return self._get_default_tips(user_data.get('english_level', 'B1'))
339
+
340
+ except Exception as e:
341
+ logger.error(f"Error generating AI study tips: {e}")
342
+ return self._get_default_tips(user_data.get('english_level', 'B1'))
343
+
344
+ def _get_default_tips(self, level: str) -> List[str]:
345
+ """Get default study tips based on level"""
346
+ tips_by_level = {
347
+ 'A1': [
348
+ "πŸ“š Start with basic vocabulary - 10 new words daily",
349
+ "🎯 Focus on present tense in daily conversations",
350
+ "πŸ’‘ Use picture dictionaries for visual learning",
351
+ "⭐ Practice pronunciation with simple audio materials",
352
+ "πŸš€ Don't worry about mistakes - communication is key!"
353
+ ],
354
+ 'A2': [
355
+ "πŸ“š Read simple news articles and stories",
356
+ "🎯 Practice past and future tenses regularly",
357
+ "πŸ’‘ Join basic English conversation groups",
358
+ "⭐ Use language learning apps for daily practice",
359
+ "πŸš€ Watch movies with subtitles in your language"
360
+ ],
361
+ 'B1': [
362
+ "πŸ“š Read intermediate articles on topics you enjoy",
363
+ "🎯 Practice expressing opinions and preferences",
364
+ "πŸ’‘ Start writing short paragraphs daily",
365
+ "⭐ Listen to podcasts at normal speed",
366
+ "πŸš€ Try to think in English for simple tasks"
367
+ ],
368
+ 'B2': [
369
+ "πŸ“š Read longer articles and opinion pieces",
370
+ "🎯 Practice formal and informal writing styles",
371
+ "πŸ’‘ Engage in debates and discussions",
372
+ "⭐ Watch news programs without subtitles",
373
+ "πŸš€ Set specific goals for each study session"
374
+ ],
375
+ 'C1': [
376
+ "πŸ“š Read academic and professional texts",
377
+ "🎯 Practice nuanced expressions and idioms",
378
+ "πŸ’‘ Write formal reports and presentations",
379
+ "⭐ Listen to academic lectures and conferences",
380
+ "πŸš€ Focus on specialized vocabulary for your field"
381
+ ],
382
+ 'C2': [
383
+ "πŸ“š Read literature and complex analytical texts",
384
+ "🎯 Master subtle language differences",
385
+ "πŸ’‘ Write with stylistic sophistication",
386
+ "⭐ Engage with native speakers in professional contexts",
387
+ "πŸš€ Aim for native-like fluency in all skills"
388
+ ]
389
+ }
390
+
391
+ return tips_by_level.get(level, tips_by_level['B1'])
392
+
393
+ def _create_milestones(self, current_level: str, target_level: str, weeks: int) -> List[Dict]:
394
+ """Create progress milestones"""
395
+ milestones = []
396
+
397
+ try:
398
+ milestone_intervals = max(2, weeks // 4) # Create 4 milestones
399
+
400
+ for i in range(1, 5):
401
+ week = milestone_intervals * i
402
+ if week <= weeks:
403
+ milestone = {
404
+ 'week': week,
405
+ 'title': f"Milestone {i}",
406
+ 'description': self._get_milestone_description(i, current_level, target_level),
407
+ 'target_date': (datetime.now() + timedelta(weeks=week)).date().isoformat(),
408
+ 'completed': False
409
+ }
410
+ milestones.append(milestone)
411
+
412
+ return milestones
413
+
414
+ except Exception as e:
415
+ logger.error(f"Error creating milestones: {e}")
416
+ return []
417
+
418
+ def _get_milestone_description(self, milestone_num: int, current_level: str, target_level: str) -> str:
419
+ """Get description for milestone"""
420
+ descriptions = {
421
+ 1: f"Complete foundation review and establish study routine",
422
+ 2: f"Reach intermediate proficiency between {current_level} and {target_level}",
423
+ 3: f"Demonstrate advanced skills approaching {target_level} level",
424
+ 4: f"Achieve {target_level} level proficiency in all skills"
425
+ }
426
+
427
+ return descriptions.get(milestone_num, f"Progress checkpoint {milestone_num}")
428
+
429
+ def _suggest_adaptations(self, user_data: Dict) -> List[str]:
430
+ """Suggest plan adaptations based on user data"""
431
+ adaptations = []
432
+
433
+ try:
434
+ weekly_hours = user_data.get('weekly_hours', 5)
435
+ context = user_data.get('context_focus', 'General/Social')
436
+ level = user_data.get('english_level', 'B1')
437
+
438
+ if weekly_hours < 4:
439
+ adaptations.append("πŸ’‘ Consider increasing study time to 4+ hours/week for faster progress")
440
+
441
+ if weekly_hours > 8:
442
+ adaptations.append("⚠️ Ensure you don't burn out - quality over quantity")
443
+
444
+ if context == 'Professional/Business':
445
+ adaptations.append("πŸ“Š Focus extra time on business writing and presentation skills")
446
+
447
+ if context == 'Technical/IT':
448
+ adaptations.append("πŸ’» Include technical documentation reading in your routine")
449
+
450
+ if level in ['C1', 'C2']:
451
+ adaptations.append("🎯 Consider specialized courses or certification preparation")
452
+
453
+ return adaptations[:3] # Limit to 3 adaptations
454
+
455
+ except Exception as e:
456
+ logger.error(f"Error suggesting adaptations: {e}")
457
+ return []
458
+
459
+ # Global instance
460
+ study_planner = StudyPlanner()