amauricunha commited on
Commit
9dc717d
Β·
verified Β·
1 Parent(s): d770486

Upload 8 files

Browse files
Files changed (8) hide show
  1. README_HF.md +120 -0
  2. admin_module.py +524 -0
  3. app.py +179 -0
  4. content_curator.py +409 -0
  5. database.py +744 -0
  6. flask_app.py +1247 -0
  7. requirements.txt +17 -0
  8. study_planner.py +460 -0
README_HF.md ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: English Helper
3
+ emoji: πŸŽ“
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ # πŸŽ“ English Helper - AI-Powered Language Learning Platform
12
+
13
+ ## 🌟 Overview
14
+
15
+ English Helper is an advanced AI-powered English learning platform that combines intelligent content curation, interactive immersive environment, and personalized study planning to provide a comprehensive language learning experience.
16
+
17
+ ## ✨ Key Features
18
+
19
+ ### πŸ€– AI-Powered Learning
20
+ - **Smart Conversation Practice**: Natural conversations with AI tutors
21
+ - **Personalized Content Curation**: AI-recommended articles and materials
22
+ - **Intelligent Study Planning**: Adaptive learning schedules
23
+ - **Real-time Feedback**: Pronunciation and grammar assistance
24
+
25
+ ### πŸ“š Interactive Learning Tools
26
+ - **Flashcard System**: Spaced repetition vocabulary learning
27
+ - **Writing Exercises**: Guided practice with AI feedback
28
+ - **Reading Comprehension**: Curated articles with difficulty assessment
29
+ - **Image Generation**: Visual vocabulary reinforcement
30
+
31
+ ### πŸ“Š Advanced Analytics
32
+ - **Progress Tracking**: Detailed performance metrics
33
+ - **Study Analytics**: Time management and efficiency analysis
34
+ - **Personalized Recommendations**: AI-driven content suggestions
35
+ - **Achievement System**: Milestone tracking and motivation
36
+
37
+ ### πŸ” Admin Panel
38
+ - **User Management**: Complete user administration
39
+ - **Token Usage Monitoring**: API cost tracking and analytics
40
+ - **System Health**: Real-time performance monitoring
41
+ - **Data Export**: CSV exports and reporting
42
+
43
+ ## πŸš€ Quick Start
44
+
45
+ 1. **Create Account**: Register with your email
46
+ 2. **Set Your Level**: Choose from A1 to C2
47
+ 3. **Define Interests**: Select your learning focus areas
48
+ 4. **Start Learning**: Use flashcards, chat with AI, read articles
49
+ 5. **Track Progress**: Monitor your improvement over time
50
+
51
+ ## 🎯 Learning Levels Supported
52
+
53
+ - **A1-A2**: Basic to Elementary
54
+ - **B1-B2**: Intermediate to Upper-Intermediate
55
+ - **C1-C2**: Advanced to Proficiency
56
+
57
+ ## 🌍 Learning Contexts
58
+
59
+ - **Professional/Business**: Corporate communication and presentations
60
+ - **Technical/IT**: Technology and software development terminology
61
+ - **General/Social**: Everyday conversation and social interactions
62
+
63
+ ## πŸ› οΈ Technology Stack
64
+
65
+ - **Backend**: Python Flask with SQLite
66
+ - **Frontend**: Responsive HTML/CSS/JavaScript
67
+ - **AI APIs**: Groq (fast processing) + Google Gemini (advanced analysis)
68
+ - **Features**: Real-time chat, content curation, study planning
69
+
70
+ ## πŸ“± Usage
71
+
72
+ ### For Learners:
73
+ 1. Register and set your English level
74
+ 2. Use the conversation feature to practice speaking
75
+ 3. Create and review flashcards for vocabulary
76
+ 4. Read curated articles matching your interests
77
+ 5. Follow your personalized study plan
78
+ 6. Track your progress in the analytics dashboard
79
+
80
+ ### For Administrators:
81
+ 1. Access the admin panel at `/admin`
82
+ 2. Monitor user activity and system health
83
+ 3. Track API usage and costs
84
+ 4. Export data for analysis
85
+ 5. Manage users and content
86
+
87
+ ## πŸ”§ Configuration
88
+
89
+ The app requires several environment variables to be set (configured automatically in Hugging Face):
90
+
91
+ - `GROQ_API_KEY`: For AI conversations
92
+ - `GEMINI_API_KEY`: For content analysis
93
+ - `SMTP_*`: For email functionality (optional)
94
+ - `ADMIN_*`: For administrative access
95
+
96
+ ## πŸ“š Educational Impact
97
+
98
+ English Helper leverages cutting-edge AI to provide:
99
+ - **Personalized Learning Paths**: Adaptive content based on user progress
100
+ - **Real-world Application**: Practical scenarios and contexts
101
+ - **Immediate Feedback**: Instant corrections and suggestions
102
+ - **Motivation Systems**: Progress tracking and achievement unlocking
103
+
104
+ ## πŸŽ“ Perfect For:
105
+
106
+ - **Students** preparing for English proficiency exams
107
+ - **Professionals** improving business English skills
108
+ - **Developers** learning technical English terminology
109
+ - **Anyone** wanting to improve their English systematically
110
+
111
+ ## πŸ”’ Privacy & Security
112
+
113
+ - User data stored securely with SQLite
114
+ - Session management with encrypted cookies
115
+ - Admin access protected with authentication
116
+ - No data sharing with third parties
117
+
118
+ ---
119
+
120
+ **Start your English learning journey with AI-powered assistance!** πŸš€πŸ“šβœ¨
admin_module.py ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ return session.get('admin_authenticated', False)
62
+
63
+ def login_admin(self, username, password):
64
+ """Admin login"""
65
+ if self.authenticate_admin(username, password):
66
+ session['admin_authenticated'] = True
67
+ session['admin_username'] = username
68
+ session['admin_login_time'] = datetime.now().isoformat()
69
+ return True
70
+ return False
71
+
72
+ def logout_admin(self):
73
+ """Admin logout"""
74
+ session.pop('admin_authenticated', None)
75
+ session.pop('admin_username', None)
76
+ session.pop('admin_login_time', None)
77
+
78
+ def get_system_stats(self):
79
+ """Get comprehensive system statistics"""
80
+ try:
81
+ conn = get_db_connection()
82
+ cursor = conn.cursor()
83
+
84
+ # User statistics
85
+ cursor.execute("SELECT COUNT(*) FROM users")
86
+ total_users = cursor.fetchone()[0]
87
+
88
+ cursor.execute("SELECT COUNT(*) FROM users WHERE created_at > datetime('now', '-7 days')")
89
+ new_users_week = cursor.fetchone()[0]
90
+
91
+ cursor.execute("SELECT COUNT(*) FROM users WHERE created_at > datetime('now', '-1 day')")
92
+ new_users_today = cursor.fetchone()[0]
93
+
94
+ # Activity statistics
95
+ cursor.execute("SELECT COUNT(*) FROM study_sessions")
96
+ total_sessions = cursor.fetchone()[0]
97
+
98
+ cursor.execute("SELECT COUNT(*) FROM flashcards")
99
+ total_flashcards = cursor.fetchone()[0]
100
+
101
+ cursor.execute("SELECT COUNT(*) FROM user_articles")
102
+ total_articles = cursor.fetchone()[0]
103
+
104
+ cursor.execute("SELECT COUNT(*) FROM study_plans")
105
+ total_study_plans = cursor.fetchone()[0]
106
+
107
+ # Token usage statistics
108
+ cursor.execute("SELECT SUM(tokens_used), COUNT(*) FROM token_usage")
109
+ token_stats = cursor.fetchone()
110
+ total_tokens = token_stats[0] if token_stats[0] else 0
111
+ total_api_calls = token_stats[1] if token_stats[1] else 0
112
+
113
+ # Calculate estimated costs
114
+ estimated_cost = self._calculate_estimated_cost(cursor)
115
+
116
+ # Recent activity
117
+ cursor.execute("""
118
+ SELECT u.email, s.created_at, s.activity_type
119
+ FROM study_sessions s
120
+ JOIN users u ON s.user_id = u.id
121
+ ORDER BY s.created_at DESC
122
+ LIMIT 10
123
+ """)
124
+ recent_activity = cursor.fetchall()
125
+
126
+ conn.close()
127
+
128
+ return {
129
+ 'users': {
130
+ 'total': total_users,
131
+ 'new_week': new_users_week,
132
+ 'new_today': new_users_today
133
+ },
134
+ 'activity': {
135
+ 'total_sessions': total_sessions,
136
+ 'total_flashcards': total_flashcards,
137
+ 'total_articles': total_articles,
138
+ 'total_study_plans': total_study_plans
139
+ },
140
+ 'api_usage': {
141
+ 'total_tokens': total_tokens,
142
+ 'total_calls': total_api_calls,
143
+ 'estimated_cost': estimated_cost
144
+ },
145
+ 'recent_activity': [
146
+ {
147
+ 'user': activity[0],
148
+ 'timestamp': activity[1],
149
+ 'activity': activity[2]
150
+ } for activity in recent_activity
151
+ ]
152
+ }
153
+ except Exception as e:
154
+ logger.error(f"Error getting system stats: {e}")
155
+ return {}
156
+
157
+ def _calculate_estimated_cost(self, cursor):
158
+ """Calculate estimated API costs"""
159
+ try:
160
+ cursor.execute("""
161
+ SELECT api_provider, SUM(input_tokens), SUM(output_tokens)
162
+ FROM token_usage
163
+ GROUP BY api_provider
164
+ """)
165
+ usage_by_provider = cursor.fetchall()
166
+
167
+ total_cost = 0
168
+ for provider, input_tokens, output_tokens in usage_by_provider:
169
+ if provider in self.token_costs:
170
+ costs = self.token_costs[provider]
171
+ total_cost += (input_tokens * costs['input']) + (output_tokens * costs['output'])
172
+
173
+ return round(total_cost, 4)
174
+ except:
175
+ return 0
176
+
177
+ def get_all_users(self, page=1, per_page=20):
178
+ """Get paginated list of all users"""
179
+ try:
180
+ conn = get_db_connection()
181
+ cursor = conn.cursor()
182
+
183
+ offset = (page - 1) * per_page
184
+
185
+ cursor.execute("""
186
+ SELECT u.id, u.email, u.created_at, u.email_confirmed, u.last_login,
187
+ COUNT(DISTINCT s.id) as session_count,
188
+ COUNT(DISTINCT f.id) as flashcard_count,
189
+ COUNT(DISTINCT a.id) as article_count
190
+ FROM users u
191
+ LEFT JOIN study_sessions s ON u.id = s.user_id
192
+ LEFT JOIN flashcards f ON u.id = f.user_id
193
+ LEFT JOIN user_articles a ON u.id = a.user_id
194
+ GROUP BY u.id
195
+ ORDER BY u.created_at DESC
196
+ LIMIT ? OFFSET ?
197
+ """, (per_page, offset))
198
+
199
+ users = cursor.fetchall()
200
+
201
+ # Get total count
202
+ cursor.execute("SELECT COUNT(*) FROM users")
203
+ total_users = cursor.fetchone()[0]
204
+
205
+ conn.close()
206
+
207
+ return {
208
+ 'users': [
209
+ {
210
+ 'id': user[0],
211
+ 'email': user[1],
212
+ 'created_at': user[2],
213
+ 'email_confirmed': bool(user[3]),
214
+ 'last_login': user[4],
215
+ 'session_count': user[5],
216
+ 'flashcard_count': user[6],
217
+ 'article_count': user[7]
218
+ } for user in users
219
+ ],
220
+ 'total': total_users,
221
+ 'page': page,
222
+ 'per_page': per_page,
223
+ 'total_pages': (total_users + per_page - 1) // per_page
224
+ }
225
+ except Exception as e:
226
+ logger.error(f"Error getting users: {e}")
227
+ return {'users': [], 'total': 0}
228
+
229
+ def delete_user(self, user_id):
230
+ """Delete a user and all associated data"""
231
+ try:
232
+ conn = get_db_connection()
233
+ cursor = conn.cursor()
234
+
235
+ # Delete in order to respect foreign key constraints
236
+ tables = [
237
+ 'study_plan_activities', 'study_plans', 'user_analytics',
238
+ 'content_recommendations', 'user_interests', 'user_articles',
239
+ 'study_sessions', 'flashcards', 'user_settings', 'users'
240
+ ]
241
+
242
+ for table in tables:
243
+ cursor.execute(f"DELETE FROM {table} WHERE user_id = ?", (user_id,))
244
+
245
+ conn.commit()
246
+ conn.close()
247
+
248
+ return True
249
+ except Exception as e:
250
+ logger.error(f"Error deleting user {user_id}: {e}")
251
+ return False
252
+
253
+ def get_user_details(self, user_id):
254
+ """Get detailed information about a specific user"""
255
+ try:
256
+ conn = get_db_connection()
257
+ cursor = conn.cursor()
258
+
259
+ # Basic user info
260
+ cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
261
+ user = cursor.fetchone()
262
+
263
+ if not user:
264
+ return None
265
+
266
+ # User settings
267
+ cursor.execute("SELECT * FROM user_settings WHERE user_id = ?", (user_id,))
268
+ settings = cursor.fetchone()
269
+
270
+ # Recent activity
271
+ cursor.execute("""
272
+ SELECT activity_type, created_at, duration_minutes
273
+ FROM study_sessions
274
+ WHERE user_id = ?
275
+ ORDER BY created_at DESC
276
+ LIMIT 20
277
+ """, (user_id,))
278
+ recent_sessions = cursor.fetchall()
279
+
280
+ # Token usage
281
+ cursor.execute("""
282
+ SELECT api_provider, SUM(input_tokens), SUM(output_tokens), COUNT(*)
283
+ FROM token_usage
284
+ WHERE user_id = ?
285
+ GROUP BY api_provider
286
+ """, (user_id,))
287
+ token_usage = cursor.fetchall()
288
+
289
+ conn.close()
290
+
291
+ return {
292
+ 'user': {
293
+ 'id': user[0],
294
+ 'email': user[1],
295
+ 'created_at': user[2],
296
+ 'email_confirmed': bool(user[3]),
297
+ 'last_login': user[4]
298
+ },
299
+ 'settings': dict(zip([col[0] for col in cursor.description], settings)) if settings else {},
300
+ 'recent_sessions': [
301
+ {
302
+ 'activity': session[0],
303
+ 'timestamp': session[1],
304
+ 'duration': session[2]
305
+ } for session in recent_sessions
306
+ ],
307
+ 'token_usage': [
308
+ {
309
+ 'provider': usage[0],
310
+ 'input_tokens': usage[1],
311
+ 'output_tokens': usage[2],
312
+ 'calls': usage[3]
313
+ } for usage in token_usage
314
+ ]
315
+ }
316
+ except Exception as e:
317
+ logger.error(f"Error getting user details for {user_id}: {e}")
318
+ return None
319
+
320
+ def record_token_usage(self, user_id, api_provider, input_tokens, output_tokens, operation_type):
321
+ """Record token usage for cost tracking"""
322
+ try:
323
+ conn = get_db_connection()
324
+ cursor = conn.cursor()
325
+
326
+ cursor.execute("""
327
+ INSERT INTO token_usage
328
+ (user_id, api_provider, input_tokens, output_tokens, operation_type, created_at)
329
+ VALUES (?, ?, ?, ?, ?, ?)
330
+ """, (user_id, api_provider, input_tokens, output_tokens, operation_type, datetime.now().isoformat()))
331
+
332
+ conn.commit()
333
+ conn.close()
334
+
335
+ return True
336
+ except Exception as e:
337
+ logger.error(f"Error recording token usage: {e}")
338
+ return False
339
+
340
+ def get_database_schema(self):
341
+ """Get database schema information"""
342
+ try:
343
+ conn = get_db_connection()
344
+ cursor = conn.cursor()
345
+
346
+ # Get all tables
347
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
348
+ tables = cursor.fetchall()
349
+
350
+ schema_info = {}
351
+ for table in tables:
352
+ table_name = table[0]
353
+
354
+ # Get table info
355
+ cursor.execute(f"PRAGMA table_info({table_name})")
356
+ columns = cursor.fetchall()
357
+
358
+ # Get row count
359
+ cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
360
+ row_count = cursor.fetchone()[0]
361
+
362
+ schema_info[table_name] = {
363
+ 'columns': [
364
+ {
365
+ 'name': col[1],
366
+ 'type': col[2],
367
+ 'not_null': bool(col[3]),
368
+ 'primary_key': bool(col[5])
369
+ } for col in columns
370
+ ],
371
+ 'row_count': row_count
372
+ }
373
+
374
+ conn.close()
375
+ return schema_info
376
+ except Exception as e:
377
+ logger.error(f"Error getting database schema: {e}")
378
+ return {}
379
+
380
+ def get_system_health(self):
381
+ """Get system health metrics"""
382
+ try:
383
+ import psutil
384
+ import os
385
+
386
+ # Memory usage
387
+ memory = psutil.virtual_memory()
388
+
389
+ # Disk usage
390
+ disk = psutil.disk_usage('/')
391
+
392
+ # Database size
393
+ db_path = 'data/englishhelper.db'
394
+ db_size = os.path.getsize(db_path) if os.path.exists(db_path) else 0
395
+
396
+ # Recent error logs (would implement proper logging)
397
+ recent_errors = self._get_recent_errors()
398
+
399
+ return {
400
+ 'memory': {
401
+ 'total': memory.total,
402
+ 'used': memory.used,
403
+ 'available': memory.available,
404
+ 'percent': memory.percent
405
+ },
406
+ 'disk': {
407
+ 'total': disk.total,
408
+ 'used': disk.used,
409
+ 'free': disk.free,
410
+ 'percent': disk.percent
411
+ },
412
+ 'database': {
413
+ 'size_bytes': db_size,
414
+ 'size_mb': round(db_size / 1024 / 1024, 2)
415
+ },
416
+ 'recent_errors': recent_errors,
417
+ 'uptime': self._get_uptime()
418
+ }
419
+ except Exception as e:
420
+ logger.error(f"Error getting system health: {e}")
421
+ return {}
422
+
423
+ def _get_recent_errors(self):
424
+ """Get recent error logs (simplified)"""
425
+ try:
426
+ # This would typically read from log files
427
+ # For now, return sample data
428
+ return [
429
+ {
430
+ 'timestamp': '2024-10-11 14:30:00',
431
+ 'level': 'ERROR',
432
+ 'message': 'API rate limit exceeded for user 123',
433
+ 'module': 'groq_client'
434
+ },
435
+ {
436
+ 'timestamp': '2024-10-11 13:45:00',
437
+ 'level': 'WARNING',
438
+ 'message': 'High memory usage detected',
439
+ 'module': 'system_monitor'
440
+ }
441
+ ]
442
+ except:
443
+ return []
444
+
445
+ def _get_uptime(self):
446
+ """Get system uptime"""
447
+ try:
448
+ import psutil
449
+ boot_time = psutil.boot_time()
450
+ uptime_seconds = datetime.now().timestamp() - boot_time
451
+
452
+ days = int(uptime_seconds // 86400)
453
+ hours = int((uptime_seconds % 86400) // 3600)
454
+ minutes = int((uptime_seconds % 3600) // 60)
455
+
456
+ return f"{days}d {hours}h {minutes}m"
457
+ except:
458
+ return "Unknown"
459
+
460
+ def check_system_alerts(self):
461
+ """Check for system alerts and warnings"""
462
+ alerts = []
463
+
464
+ try:
465
+ # Check token usage limits
466
+ conn = get_db_connection()
467
+ cursor = conn.cursor()
468
+
469
+ # Check daily token usage
470
+ cursor.execute("""
471
+ SELECT SUM(tokens_used)
472
+ FROM token_usage
473
+ WHERE date(created_at) = date('now')
474
+ """)
475
+ daily_tokens = cursor.fetchone()[0] or 0
476
+
477
+ if daily_tokens > 100000: # Alert threshold
478
+ alerts.append({
479
+ 'type': 'warning',
480
+ 'message': f'High daily token usage: {daily_tokens:,} tokens',
481
+ 'action': 'Monitor API costs'
482
+ })
483
+
484
+ # Check error rates
485
+ cursor.execute("""
486
+ SELECT COUNT(*) FROM token_usage
487
+ WHERE created_at > datetime('now', '-1 hour')
488
+ """)
489
+ hourly_requests = cursor.fetchone()[0] or 0
490
+
491
+ if hourly_requests > 500: # High load threshold
492
+ alerts.append({
493
+ 'type': 'info',
494
+ 'message': f'High API request rate: {hourly_requests} requests/hour',
495
+ 'action': 'Monitor performance'
496
+ })
497
+
498
+ # Check database size
499
+ health = self.get_system_health()
500
+ if health.get('database', {}).get('size_mb', 0) > 100: # 100MB threshold
501
+ alerts.append({
502
+ 'type': 'warning',
503
+ 'message': f'Large database size: {health["database"]["size_mb"]}MB',
504
+ 'action': 'Consider archiving old data'
505
+ })
506
+
507
+ conn.close()
508
+ return alerts
509
+
510
+ except Exception as e:
511
+ logger.error(f"Error checking system alerts: {e}")
512
+ return []
513
+
514
+ # Decorator for admin-only routes
515
+ def admin_required(f):
516
+ @wraps(f)
517
+ def decorated_function(*args, **kwargs):
518
+ if not admin_manager.is_admin_logged_in():
519
+ return jsonify({'error': 'Admin authentication required'}), 401
520
+ return f(*args, **kwargs)
521
+ return decorated_function
522
+
523
+ # Global admin manager instance
524
+ admin_manager = AdminManager()
app.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ English Helper - Hugging Face Spaces Entry Point
4
+ Interface Gradio para o sistema de aprendizado de inglΓͺs
5
+ """
6
+
7
+ import os
8
+ import gradio as gr
9
+ import threading
10
+ import time
11
+ from pathlib import Path
12
+
13
+ # Configurar ambiente
14
+ os.environ.setdefault('FLASK_ENV', 'production')
15
+
16
+ def start_flask_server():
17
+ """Iniciar servidor Flask em thread separada"""
18
+ try:
19
+ # Setup bΓ‘sico do ambiente HF
20
+ os.environ.setdefault('FLASK_ENV', 'production')
21
+ from flask_app import app
22
+
23
+ # Inicializar banco de dados
24
+ from database import init_db
25
+ init_db()
26
+
27
+ # Executar Flask em thread separada
28
+ port = int(os.environ.get("FLASK_PORT", 5000))
29
+ app.run(host="0.0.0.0", port=port, debug=False, threaded=True)
30
+
31
+ except Exception as e:
32
+ print(f"❌ Erro ao iniciar Flask: {e}")
33
+
34
+ def check_system_status():
35
+ """Verificar status do sistema"""
36
+ status = {
37
+ "Flask Server": "🟒 Running",
38
+ "Database": "🟒 Connected",
39
+ "Groq API": "🟒 Active" if os.getenv('GROQ_API_KEY') else "πŸ”΄ Missing",
40
+ "Gemini API": "🟒 Active" if os.getenv('GEMINI_API_KEY') else "πŸ”΄ Missing",
41
+ "Admin Panel": f"🟒 Available at /admin" if os.getenv('ADMIN_USERNAME') else "πŸ”΄ Not configured"
42
+ }
43
+
44
+ status_text = "## 🎯 English Helper System Status\n\n"
45
+
46
+ for service, state in status.items():
47
+ status_text += f"**{service}**: {state}\n\n"
48
+
49
+ # URLs de acesso
50
+ base_url = os.getenv('BASE_URL', 'https://huggingface.co/spaces/amauricunha/englishhelper')
51
+
52
+ status_text += "## 🌐 Access Links\n\n"
53
+ status_text += f"**Main App**: [{base_url}]({base_url})\n\n"
54
+ status_text += f"**Admin Panel**: [{base_url}/admin]({base_url}/admin)\n\n"
55
+ status_text += f"**API Health**: [{base_url}/auth/check]({base_url}/auth/check)\n\n"
56
+
57
+ return status_text
58
+
59
+ def launch_english_helper():
60
+ """Interface principal do Gradio"""
61
+
62
+ # Iniciar Flask em background
63
+ flask_thread = threading.Thread(target=start_flask_server, daemon=True)
64
+ flask_thread.start()
65
+
66
+ # Aguardar Flask inicializar
67
+ time.sleep(3)
68
+
69
+ # Interface Gradio
70
+ with gr.Blocks(
71
+ title="English Helper - AI Learning Platform",
72
+ theme=gr.themes.Soft(),
73
+ css="""
74
+ .container { max-width: 1200px; margin: 0 auto; }
75
+ .status-panel { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
76
+ color: white; padding: 20px; border-radius: 10px; }
77
+ .feature-card { border: 1px solid #e1e5e9; border-radius: 8px; padding: 16px; margin: 8px; }
78
+ """
79
+ ) as demo:
80
+
81
+ gr.HTML("""
82
+ <div class="status-panel">
83
+ <h1>πŸŽ“ English Helper - AI Learning Platform</h1>
84
+ <p>Sistema completo de aprendizado de inglΓͺs com IA, painel administrativo e anΓ‘lise de progresso.</p>
85
+ </div>
86
+ """)
87
+
88
+ with gr.Tab("πŸ“Š System Status"):
89
+ status_output = gr.Markdown(check_system_status())
90
+ refresh_btn = gr.Button("πŸ”„ Refresh Status", variant="secondary")
91
+ refresh_btn.click(fn=check_system_status, outputs=status_output)
92
+
93
+ with gr.Tab("πŸš€ Quick Start"):
94
+ gr.Markdown("""
95
+ ## 🎯 Como usar o English Helper:
96
+
97
+ ### 1️⃣ **Acesso Principal**
98
+ - **App Web**: Use o link "Main App" acima para acessar a interface completa
99
+ - **Registro**: Crie sua conta para personalizar o aprendizado
100
+ - **Login**: Acesse suas configuraΓ§Γ΅es e progresso
101
+
102
+ ### 2️⃣ **Recursos DisponΓ­veis**
103
+ - πŸ€– **Chat com IA**: ConversaΓ§Γ£o em inglΓͺs com feedback inteligente
104
+ - πŸ“š **Flashcards**: CriaΓ§Γ£o automΓ‘tica de cartΓ΅es de estudo
105
+ - πŸ“° **Curadoria de ConteΓΊdo**: Artigos personalizados para seu nΓ­vel
106
+ - πŸ“… **Plano de Estudos**: Cronograma adaptativo de aprendizado
107
+ - 🎡 **Áudio TTS**: Pronunciação com síntese de voz
108
+
109
+ ### 3️⃣ **Painel Administrativo**
110
+ - πŸ‘€ **Admin Access**: Use o link "/admin" com suas credenciais
111
+ - πŸ“ˆ **Analytics**: MΓ©tricas de uso e progresso dos usuΓ‘rios
112
+ - βš™οΈ **ConfiguraΓ§Γ΅es**: Gerenciamento do sistema
113
+ - πŸ’Ύ **Database**: VisualizaΓ§Γ£o e backup dos dados
114
+ """)
115
+
116
+ with gr.Tab("βš™οΈ Configuration"):
117
+ gr.Markdown("""
118
+ ## πŸ”§ ConfiguraΓ§Γ£o do Sistema
119
+
120
+ ### βœ… **APIs Configuradas**:
121
+ - **Groq API**: Processamento rΓ‘pido de linguagem natural
122
+ - **Gemini API**: AnΓ‘lise avanΓ§ada de conteΓΊdo e conversaΓ§Γ£o
123
+
124
+ ### πŸ“§ **Email (Opcional)**:
125
+ Configure SMTP para confirmaΓ§Γ£o de email e notificaΓ§Γ΅es
126
+
127
+ ### πŸ” **SeguranΓ§a**:
128
+ - Todas as chaves sΓ£o armazenadas como Repository Secrets
129
+ - SessΓ΅es seguras com HTTPS
130
+ - AutenticaΓ§Γ£o administrativa protegida
131
+
132
+ ### πŸ“Š **Monitoramento**:
133
+ - Logs detalhados de uso da API
134
+ - MΓ©tricas de performance
135
+ - Alertas de sistema
136
+ """)
137
+
138
+ with gr.Tab("πŸ†˜ Support"):
139
+ gr.Markdown("""
140
+ ## πŸ›Ÿ Suporte e Troubleshooting
141
+
142
+ ### πŸ” **VerificaΓ§Γ΅es BΓ‘sicas**:
143
+ 1. **System Status**: Verifique se todos os serviΓ§os estΓ£o ativos
144
+ 2. **API Keys**: Confirme se Groq e Gemini estΓ£o configurados
145
+ 3. **Database**: Verifique conexΓ£o no painel admin
146
+
147
+ ### ⚠️ **Problemas Comuns**:
148
+ - **500 Error**: Verifique logs e configuraΓ§Γ΅es de API
149
+ - **Admin nΓ£o acessa**: Confirme ADMIN_USERNAME/PASSWORD
150
+ - **Slow response**: Verifique cotas de API
151
+
152
+ ### πŸ“š **DocumentaΓ§Γ£o**:
153
+ - **Setup Guide**: Consulte HUGGINGFACE_SETUP.md
154
+ - **API Docs**: Groq e Gemini documentation
155
+ - **Flask Guide**: Deploy e configuraΓ§Γ£o
156
+
157
+ ### πŸ”— **Links Úteis**:
158
+ - [Hugging Face Spaces Docs](https://huggingface.co/docs/hub/spaces)
159
+ - [Groq Console](https://console.groq.com/)
160
+ - [Google AI Studio](https://makersuite.google.com/)
161
+ """)
162
+
163
+ return demo
164
+
165
+ # Executar aplicaΓ§Γ£o
166
+ if __name__ == "__main__":
167
+ print("🎯 Launching English Helper on Hugging Face Spaces...")
168
+
169
+ demo = launch_english_helper()
170
+
171
+ # ConfiguraΓ§Γ΅es do Gradio para HF Spaces
172
+ demo.launch(
173
+ server_name="0.0.0.0",
174
+ server_port=7860,
175
+ share=False,
176
+ debug=False,
177
+ enable_queue=True,
178
+ show_error=True
179
+ )
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,744 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 init_db():
35
+ """Initialize database with required tables"""
36
+ try:
37
+ db = sqlite3.connect(DATABASE_PATH)
38
+ db.executescript('''
39
+ -- Users table
40
+ CREATE TABLE IF NOT EXISTS users (
41
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42
+ email TEXT UNIQUE NOT NULL,
43
+ password_hash TEXT NOT NULL,
44
+ salt TEXT NOT NULL,
45
+ is_confirmed BOOLEAN DEFAULT FALSE,
46
+ confirmation_token TEXT,
47
+ reset_token TEXT,
48
+ reset_token_expires TIMESTAMP,
49
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
50
+ last_login TIMESTAMP,
51
+ is_active BOOLEAN DEFAULT TRUE
52
+ );
53
+
54
+ -- User flashcards table
55
+ CREATE TABLE IF NOT EXISTS user_flashcards (
56
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
57
+ user_id INTEGER NOT NULL,
58
+ term TEXT NOT NULL,
59
+ translation TEXT,
60
+ context_sentence TEXT,
61
+ gapped_sentence TEXT,
62
+ definition TEXT,
63
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
64
+ study_count INTEGER DEFAULT 0,
65
+ last_studied TIMESTAMP,
66
+ difficulty_level INTEGER DEFAULT 1,
67
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
68
+ );
69
+
70
+ -- User study sessions table
71
+ CREATE TABLE IF NOT EXISTS study_sessions (
72
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
73
+ user_id INTEGER NOT NULL,
74
+ session_type TEXT NOT NULL, -- 'flashcard', 'conversation', 'activity'
75
+ duration_minutes INTEGER,
76
+ cards_studied INTEGER DEFAULT 0,
77
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
78
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
79
+ );
80
+
81
+ -- User settings table
82
+ CREATE TABLE IF NOT EXISTS user_settings (
83
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
84
+ user_id INTEGER UNIQUE NOT NULL,
85
+ preferred_model TEXT DEFAULT 'gemini:gemini-2.5-flash-latest',
86
+ context_focus TEXT DEFAULT 'General/Social',
87
+ voice_accent TEXT DEFAULT 'co.uk',
88
+ daily_goal INTEGER DEFAULT 10,
89
+ notification_enabled BOOLEAN DEFAULT TRUE,
90
+ -- New settings for advanced features
91
+ english_level TEXT DEFAULT 'B1', -- A1, A2, B1, B2, C1, C2
92
+ study_goals TEXT, -- JSON: objectives like "business english", "technical vocabulary"
93
+ preferred_content_types TEXT DEFAULT 'articles,videos', -- comma separated
94
+ content_difficulty TEXT DEFAULT 'adaptive', -- 'easy', 'medium', 'hard', 'adaptive'
95
+ study_schedule TEXT, -- JSON: preferred days/times
96
+ auto_recommendations BOOLEAN DEFAULT TRUE,
97
+ content_sources TEXT DEFAULT 'news,tech,business', -- preferred content sources
98
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
99
+ );
100
+
101
+ -- User articles/content table
102
+ CREATE TABLE IF NOT EXISTS user_articles (
103
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ user_id INTEGER NOT NULL,
105
+ title TEXT NOT NULL,
106
+ content TEXT NOT NULL,
107
+ source_url TEXT,
108
+ source_type TEXT DEFAULT 'manual', -- 'manual', 'web_search', 'recommended'
109
+ category TEXT, -- user interest category
110
+ difficulty_level TEXT, -- estimated difficulty
111
+ word_count INTEGER,
112
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
113
+ last_accessed TIMESTAMP,
114
+ is_favorite BOOLEAN DEFAULT FALSE,
115
+ study_progress REAL DEFAULT 0.0, -- 0.0 to 1.0
116
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
117
+ );
118
+
119
+ -- User interests/preferences table
120
+ CREATE TABLE IF NOT EXISTS user_interests (
121
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
122
+ user_id INTEGER NOT NULL,
123
+ interest_category TEXT NOT NULL,
124
+ weight REAL DEFAULT 1.0, -- importance weight
125
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
126
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
127
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
128
+ UNIQUE(user_id, interest_category)
129
+ );
130
+
131
+ -- Content recommendations table
132
+ CREATE TABLE IF NOT EXISTS content_recommendations (
133
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
134
+ user_id INTEGER NOT NULL,
135
+ article_id INTEGER,
136
+ recommendation_reason TEXT,
137
+ relevance_score REAL,
138
+ recommended_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
139
+ viewed BOOLEAN DEFAULT FALSE,
140
+ accepted BOOLEAN DEFAULT FALSE,
141
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
142
+ FOREIGN KEY (article_id) REFERENCES user_articles (id) ON DELETE CASCADE
143
+ );
144
+
145
+ -- Study plans table
146
+ CREATE TABLE IF NOT EXISTS study_plans (
147
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
148
+ user_id INTEGER NOT NULL,
149
+ plan_name TEXT NOT NULL,
150
+ target_level TEXT, -- A1, A2, B1, B2, C1, C2
151
+ current_level TEXT,
152
+ objectives TEXT, -- JSON string with objectives
153
+ weekly_hours INTEGER DEFAULT 5,
154
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
155
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
156
+ is_active BOOLEAN DEFAULT TRUE,
157
+ completion_percentage REAL DEFAULT 0.0,
158
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
159
+ );
160
+
161
+ -- Study plan activities table
162
+ CREATE TABLE IF NOT EXISTS study_plan_activities (
163
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
164
+ plan_id INTEGER NOT NULL,
165
+ activity_type TEXT NOT NULL, -- 'reading', 'flashcards', 'conversation', 'writing'
166
+ content_reference TEXT, -- reference to article, flashcard set, etc.
167
+ scheduled_date DATE,
168
+ estimated_duration INTEGER, -- minutes
169
+ actual_duration INTEGER,
170
+ completed BOOLEAN DEFAULT FALSE,
171
+ completed_at TIMESTAMP,
172
+ difficulty_rating INTEGER, -- 1-5 user rating
173
+ notes TEXT,
174
+ FOREIGN KEY (plan_id) REFERENCES study_plans (id) ON DELETE CASCADE
175
+ );
176
+
177
+ -- User analytics table
178
+ CREATE TABLE IF NOT EXISTS user_analytics (
179
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
180
+ user_id INTEGER NOT NULL,
181
+ metric_name TEXT NOT NULL,
182
+ metric_value REAL NOT NULL,
183
+ metric_date DATE NOT NULL,
184
+ context_data TEXT, -- JSON with additional context
185
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
186
+ );
187
+
188
+ -- Token usage tracking table for admin
189
+ CREATE TABLE IF NOT EXISTS token_usage (
190
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
191
+ user_id INTEGER,
192
+ api_provider TEXT NOT NULL, -- 'groq', 'gemini', etc.
193
+ input_tokens INTEGER DEFAULT 0,
194
+ output_tokens INTEGER DEFAULT 0,
195
+ operation_type TEXT, -- 'conversation', 'content_analysis', 'recommendation', etc.
196
+ tokens_used INTEGER GENERATED ALWAYS AS (input_tokens + output_tokens) STORED,
197
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
198
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
199
+ );
200
+
201
+ -- Create indexes for better performance
202
+ CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
203
+ CREATE INDEX IF NOT EXISTS idx_users_confirmation_token ON users(confirmation_token);
204
+ CREATE INDEX IF NOT EXISTS idx_users_reset_token ON users(reset_token);
205
+ CREATE INDEX IF NOT EXISTS idx_flashcards_user_id ON user_flashcards(user_id);
206
+ CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON study_sessions(user_id);
207
+ CREATE INDEX IF NOT EXISTS idx_settings_user_id ON user_settings(user_id);
208
+ CREATE INDEX IF NOT EXISTS idx_articles_user_id ON user_articles(user_id);
209
+ CREATE INDEX IF NOT EXISTS idx_articles_category ON user_articles(category);
210
+ CREATE INDEX IF NOT EXISTS idx_interests_user_id ON user_interests(user_id);
211
+ CREATE INDEX IF NOT EXISTS idx_recommendations_user_id ON content_recommendations(user_id);
212
+ CREATE INDEX IF NOT EXISTS idx_study_plans_user_id ON study_plans(user_id);
213
+ CREATE INDEX IF NOT EXISTS idx_plan_activities_plan_id ON study_plan_activities(plan_id);
214
+ CREATE INDEX IF NOT EXISTS idx_analytics_user_date ON user_analytics(user_id, metric_date);
215
+ CREATE INDEX IF NOT EXISTS idx_token_usage_user_id ON token_usage(user_id);
216
+ CREATE INDEX IF NOT EXISTS idx_token_usage_provider ON token_usage(api_provider);
217
+ CREATE INDEX IF NOT EXISTS idx_token_usage_date ON token_usage(created_at);
218
+ ''')
219
+ db.commit()
220
+ db.close()
221
+ logger.info("Database initialized successfully")
222
+ return True
223
+ except Exception as e:
224
+ logger.error(f"Error initializing database: {e}")
225
+ return False
226
+
227
+ def hash_password(password, salt=None):
228
+ """Hash password with salt"""
229
+ if salt is None:
230
+ salt = secrets.token_hex(32)
231
+
232
+ password_hash = hashlib.pbkdf2_hmac(
233
+ 'sha256',
234
+ password.encode('utf-8'),
235
+ salt.encode('utf-8'),
236
+ 100000 # iterations
237
+ )
238
+ return password_hash.hex(), salt
239
+
240
+ def verify_password(password, password_hash, salt):
241
+ """Verify password against hash"""
242
+ new_hash, _ = hash_password(password, salt)
243
+ return new_hash == password_hash
244
+
245
+ def generate_token():
246
+ """Generate secure random token"""
247
+ return secrets.token_urlsafe(32)
248
+
249
+ def create_user(email, password):
250
+ """Create new user account"""
251
+ try:
252
+ db = get_db()
253
+
254
+ # Check if user already exists
255
+ existing_user = db.execute(
256
+ 'SELECT id FROM users WHERE email = ?', (email,)
257
+ ).fetchone()
258
+
259
+ if existing_user:
260
+ return {'success': False, 'message': 'Email already registered'}
261
+
262
+ # Hash password
263
+ password_hash, salt = hash_password(password)
264
+ confirmation_token = generate_token()
265
+
266
+ # Insert user
267
+ cursor = db.execute(
268
+ '''INSERT INTO users (email, password_hash, salt, confirmation_token)
269
+ VALUES (?, ?, ?, ?)''',
270
+ (email, password_hash, salt, confirmation_token)
271
+ )
272
+ user_id = cursor.lastrowid
273
+
274
+ # Create default user settings
275
+ db.execute(
276
+ '''INSERT INTO user_settings (user_id) VALUES (?)''',
277
+ (user_id,)
278
+ )
279
+
280
+ db.commit()
281
+
282
+ logger.info(f"User created: {email}")
283
+ return {
284
+ 'success': True,
285
+ 'user_id': user_id,
286
+ 'confirmation_token': confirmation_token,
287
+ 'message': 'User created successfully'
288
+ }
289
+
290
+ except Exception as e:
291
+ logger.error(f"Error creating user: {e}")
292
+ return {'success': False, 'message': 'Internal server error'}
293
+
294
+ def authenticate_user(email, password):
295
+ """Authenticate user login"""
296
+ try:
297
+ db = get_db()
298
+ user = db.execute(
299
+ '''SELECT id, email, password_hash, salt, is_confirmed, is_active
300
+ FROM users WHERE email = ?''', (email,)
301
+ ).fetchone()
302
+
303
+ if not user:
304
+ return {'success': False, 'message': 'Invalid email or password'}
305
+
306
+ if not user['is_active']:
307
+ return {'success': False, 'message': 'Account is deactivated'}
308
+
309
+ if not verify_password(password, user['password_hash'], user['salt']):
310
+ return {'success': False, 'message': 'Invalid email or password'}
311
+
312
+ if not user['is_confirmed']:
313
+ return {'success': False, 'message': 'Please confirm your email before logging in'}
314
+
315
+ # Update last login
316
+ db.execute(
317
+ 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
318
+ (user['id'],)
319
+ )
320
+ db.commit()
321
+
322
+ return {
323
+ 'success': True,
324
+ 'user_id': user['id'],
325
+ 'email': user['email'],
326
+ 'message': 'Login successful'
327
+ }
328
+
329
+ except Exception as e:
330
+ logger.error(f"Error authenticating user: {e}")
331
+ return {'success': False, 'message': 'Internal server error'}
332
+
333
+ def confirm_email(token):
334
+ """Confirm user email with token"""
335
+ try:
336
+ db = get_db()
337
+ user = db.execute(
338
+ 'SELECT id, email FROM users WHERE confirmation_token = ? AND is_confirmed = FALSE',
339
+ (token,)
340
+ ).fetchone()
341
+
342
+ if not user:
343
+ return {'success': False, 'message': 'Invalid or expired confirmation token'}
344
+
345
+ db.execute(
346
+ '''UPDATE users SET is_confirmed = TRUE, confirmation_token = NULL
347
+ WHERE id = ?''',
348
+ (user['id'],)
349
+ )
350
+ db.commit()
351
+
352
+ logger.info(f"Email confirmed for user: {user['email']}")
353
+ return {'success': True, 'message': 'Email confirmed successfully'}
354
+
355
+ except Exception as e:
356
+ logger.error(f"Error confirming email: {e}")
357
+ return {'success': False, 'message': 'Internal server error'}
358
+
359
+ def get_user_settings(user_id):
360
+ """Get user settings"""
361
+ try:
362
+ db = get_db()
363
+ settings = db.execute(
364
+ '''SELECT preferred_model, context_focus, voice_accent, daily_goal, notification_enabled
365
+ FROM user_settings WHERE user_id = ?''',
366
+ (user_id,)
367
+ ).fetchone()
368
+
369
+ if settings:
370
+ return dict(settings)
371
+ return None
372
+
373
+ except Exception as e:
374
+ logger.error(f"Error getting user settings: {e}")
375
+ return None
376
+
377
+ def update_user_settings(user_id, settings):
378
+ """Update user settings"""
379
+ try:
380
+ db = get_db()
381
+ db.execute(
382
+ '''UPDATE user_settings
383
+ SET preferred_model = ?, context_focus = ?, voice_accent = ?,
384
+ daily_goal = ?, notification_enabled = ?
385
+ WHERE user_id = ?''',
386
+ (settings.get('preferred_model'), settings.get('context_focus'),
387
+ settings.get('voice_accent'), settings.get('daily_goal'),
388
+ settings.get('notification_enabled'), user_id)
389
+ )
390
+ db.commit()
391
+ return True
392
+
393
+ except Exception as e:
394
+ logger.error(f"Error updating user settings: {e}")
395
+ return False
396
+
397
+ def save_user_flashcard(user_id, flashcard_data):
398
+ """Save flashcard to user's collection"""
399
+ try:
400
+ db = get_db()
401
+ db.execute(
402
+ '''INSERT INTO user_flashcards
403
+ (user_id, term, translation, context_sentence, gapped_sentence, definition)
404
+ VALUES (?, ?, ?, ?, ?, ?)''',
405
+ (user_id, flashcard_data.get('term'), flashcard_data.get('translation'),
406
+ flashcard_data.get('context_sentence'), flashcard_data.get('gapped_sentence'),
407
+ flashcard_data.get('definition'))
408
+ )
409
+ db.commit()
410
+ return True
411
+
412
+ except Exception as e:
413
+ logger.error(f"Error saving flashcard: {e}")
414
+ return False
415
+
416
+ def get_user_flashcards(user_id, limit=50):
417
+ """Get user's flashcards"""
418
+ try:
419
+ db = get_db()
420
+ flashcards = db.execute(
421
+ '''SELECT * FROM user_flashcards
422
+ WHERE user_id = ?
423
+ ORDER BY created_at DESC
424
+ LIMIT ?''',
425
+ (user_id, limit)
426
+ ).fetchall()
427
+
428
+ return [dict(card) for card in flashcards]
429
+
430
+ except Exception as e:
431
+ logger.error(f"Error getting user flashcards: {e}")
432
+ return []
433
+
434
+ def record_study_session(user_id, session_type, duration_minutes=None, cards_studied=0):
435
+ """Record a study session"""
436
+ try:
437
+ db = get_db()
438
+ db.execute(
439
+ '''INSERT INTO study_sessions (user_id, session_type, duration_minutes, cards_studied)
440
+ VALUES (?, ?, ?, ?)''',
441
+ (user_id, session_type, duration_minutes, cards_studied)
442
+ )
443
+ db.commit()
444
+ return True
445
+
446
+ except Exception as e:
447
+ logger.error(f"Error recording study session: {e}")
448
+ return False
449
+
450
+ # Authentication decorators
451
+ def login_required(f):
452
+ """Decorator to require login"""
453
+ @wraps(f)
454
+ def decorated_function(*args, **kwargs):
455
+ if 'user_id' not in session:
456
+ return jsonify({'error': 'Authentication required'}), 401
457
+ return f(*args, **kwargs)
458
+ return decorated_function
459
+
460
+ def get_current_user():
461
+ """Get current logged in user"""
462
+ if 'user_id' in session:
463
+ try:
464
+ db = get_db()
465
+ user = db.execute(
466
+ 'SELECT id, email, is_confirmed FROM users WHERE id = ? AND is_active = TRUE',
467
+ (session['user_id'],)
468
+ ).fetchone()
469
+ return dict(user) if user else None
470
+ except Exception as e:
471
+ logger.error(f"Error getting current user: {e}")
472
+ return None
473
+ return None
474
+
475
+ # Email functionality (for Hugging Face Spaces)
476
+ def send_confirmation_email(email, token):
477
+ """Send confirmation email (simplified for HF Spaces)"""
478
+ try:
479
+ # For Hugging Face Spaces, we'll use environment variables for SMTP
480
+ smtp_server = os.environ.get('SMTP_SERVER', 'smtp.gmail.com')
481
+ smtp_port = int(os.environ.get('SMTP_PORT', '587'))
482
+ smtp_username = os.environ.get('SMTP_USERNAME')
483
+ smtp_password = os.environ.get('SMTP_PASSWORD')
484
+
485
+ if not all([smtp_username, smtp_password]):
486
+ logger.warning("SMTP credentials not configured")
487
+ return False
488
+
489
+ # Create confirmation URL (will be updated with actual domain)
490
+ base_url = os.environ.get('BASE_URL', 'http://localhost:7860')
491
+ confirm_url = f"{base_url}/confirm-email?token={token}"
492
+
493
+ # Create email
494
+ msg = MIMEMultipart()
495
+ msg['From'] = smtp_username
496
+ msg['To'] = email
497
+ msg['Subject'] = "Confirm your English Helper account"
498
+
499
+ body = f"""
500
+ <html>
501
+ <body>
502
+ <h2>Welcome to Dynamic English Study Studio!</h2>
503
+ <p>Thank you for creating an account. Please click the link below to confirm your email address:</p>
504
+ <p><a href="{confirm_url}" style="background-color: #4f46e5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Confirm Email</a></p>
505
+ <p>If the button doesn't work, copy and paste this link into your browser:</p>
506
+ <p>{confirm_url}</p>
507
+ <p>This link will expire in 24 hours.</p>
508
+ <p>If you didn't create this account, please ignore this email.</p>
509
+ </body>
510
+ </html>
511
+ """
512
+
513
+ msg.attach(MIMEText(body, 'html'))
514
+
515
+ # Send email
516
+ server = smtplib.SMTP(smtp_server, smtp_port)
517
+ server.starttls()
518
+ server.login(smtp_username, smtp_password)
519
+ text = msg.as_string()
520
+ server.sendmail(smtp_username, email, text)
521
+ server.quit()
522
+
523
+ logger.info(f"Confirmation email sent to {email}")
524
+ return True
525
+
526
+ except Exception as e:
527
+ logger.error(f"Error sending confirmation email: {e}")
528
+ return False
529
+
530
+ # --- CONTENT CURATION FUNCTIONS ---
531
+
532
+ def save_user_article(user_id, title, content, source_url=None, source_type='manual', category=None):
533
+ """Save article/content for user"""
534
+ try:
535
+ db = get_db()
536
+ word_count = len(content.split()) if content else 0
537
+
538
+ cursor = db.execute(
539
+ '''INSERT INTO user_articles
540
+ (user_id, title, content, source_url, source_type, category, word_count)
541
+ VALUES (?, ?, ?, ?, ?, ?, ?)''',
542
+ (user_id, title, content, source_url, source_type, category, word_count)
543
+ )
544
+ article_id = cursor.lastrowid
545
+ db.commit()
546
+
547
+ logger.info(f"Article saved for user {user_id}: {title}")
548
+ return {'success': True, 'article_id': article_id}
549
+
550
+ except Exception as e:
551
+ logger.error(f"Error saving article: {e}")
552
+ return {'success': False, 'message': 'Failed to save article'}
553
+
554
+ def get_user_articles(user_id, category=None, limit=50):
555
+ """Get user's saved articles"""
556
+ try:
557
+ db = get_db()
558
+
559
+ if category:
560
+ articles = db.execute(
561
+ '''SELECT * FROM user_articles
562
+ WHERE user_id = ? AND category = ?
563
+ ORDER BY created_at DESC LIMIT ?''',
564
+ (user_id, category, limit)
565
+ ).fetchall()
566
+ else:
567
+ articles = db.execute(
568
+ '''SELECT * FROM user_articles
569
+ WHERE user_id = ?
570
+ ORDER BY created_at DESC LIMIT ?''',
571
+ (user_id, limit)
572
+ ).fetchall()
573
+
574
+ return [dict(article) for article in articles]
575
+
576
+ except Exception as e:
577
+ logger.error(f"Error getting user articles: {e}")
578
+ return []
579
+
580
+ def update_user_interests(user_id, interests):
581
+ """Update user's interests/categories"""
582
+ try:
583
+ db = get_db()
584
+
585
+ # Clear existing interests
586
+ db.execute('DELETE FROM user_interests WHERE user_id = ?', (user_id,))
587
+
588
+ # Add new interests
589
+ for interest, weight in interests.items():
590
+ db.execute(
591
+ '''INSERT INTO user_interests (user_id, interest_category, weight)
592
+ VALUES (?, ?, ?)''',
593
+ (user_id, interest, weight)
594
+ )
595
+
596
+ db.commit()
597
+ return True
598
+
599
+ except Exception as e:
600
+ logger.error(f"Error updating user interests: {e}")
601
+ return False
602
+
603
+ def get_user_interests(user_id):
604
+ """Get user's interests"""
605
+ try:
606
+ db = get_db()
607
+ interests = db.execute(
608
+ 'SELECT interest_category, weight FROM user_interests WHERE user_id = ?',
609
+ (user_id,)
610
+ ).fetchall()
611
+
612
+ return {interest['interest_category']: interest['weight'] for interest in interests}
613
+
614
+ except Exception as e:
615
+ logger.error(f"Error getting user interests: {e}")
616
+ return {}
617
+
618
+ def create_study_plan(user_id, plan_name, target_level, current_level, objectives, weekly_hours=5):
619
+ """Create new study plan"""
620
+ try:
621
+ db = get_db()
622
+
623
+ cursor = db.execute(
624
+ '''INSERT INTO study_plans
625
+ (user_id, plan_name, target_level, current_level, objectives, weekly_hours)
626
+ VALUES (?, ?, ?, ?, ?, ?)''',
627
+ (user_id, plan_name, target_level, current_level, objectives, weekly_hours)
628
+ )
629
+ plan_id = cursor.lastrowid
630
+ db.commit()
631
+
632
+ logger.info(f"Study plan created for user {user_id}: {plan_name}")
633
+ return {'success': True, 'plan_id': plan_id}
634
+
635
+ except Exception as e:
636
+ logger.error(f"Error creating study plan: {e}")
637
+ return {'success': False, 'message': 'Failed to create study plan'}
638
+
639
+ def get_user_study_plans(user_id):
640
+ """Get user's study plans"""
641
+ try:
642
+ db = get_db()
643
+ plans = db.execute(
644
+ '''SELECT * FROM study_plans
645
+ WHERE user_id = ?
646
+ ORDER BY created_at DESC''',
647
+ (user_id,)
648
+ ).fetchall()
649
+
650
+ return [dict(plan) for plan in plans]
651
+
652
+ except Exception as e:
653
+ logger.error(f"Error getting study plans: {e}")
654
+ return []
655
+
656
+ def add_study_activity(plan_id, activity_type, content_reference, scheduled_date, estimated_duration):
657
+ """Add activity to study plan"""
658
+ try:
659
+ db = get_db()
660
+
661
+ db.execute(
662
+ '''INSERT INTO study_plan_activities
663
+ (plan_id, activity_type, content_reference, scheduled_date, estimated_duration)
664
+ VALUES (?, ?, ?, ?, ?)''',
665
+ (plan_id, activity_type, content_reference, scheduled_date, estimated_duration)
666
+ )
667
+ db.commit()
668
+ return True
669
+
670
+ except Exception as e:
671
+ logger.error(f"Error adding study activity: {e}")
672
+ return False
673
+
674
+ def get_study_activities(plan_id, date_range=None):
675
+ """Get activities for study plan"""
676
+ try:
677
+ db = get_db()
678
+
679
+ if date_range:
680
+ start_date, end_date = date_range
681
+ activities = db.execute(
682
+ '''SELECT * FROM study_plan_activities
683
+ WHERE plan_id = ? AND scheduled_date BETWEEN ? AND ?
684
+ ORDER BY scheduled_date''',
685
+ (plan_id, start_date, end_date)
686
+ ).fetchall()
687
+ else:
688
+ activities = db.execute(
689
+ '''SELECT * FROM study_plan_activities
690
+ WHERE plan_id = ?
691
+ ORDER BY scheduled_date''',
692
+ (plan_id,)
693
+ ).fetchall()
694
+
695
+ return [dict(activity) for activity in activities]
696
+
697
+ except Exception as e:
698
+ logger.error(f"Error getting study activities: {e}")
699
+ return []
700
+
701
+ def record_analytics_metric(user_id, metric_name, metric_value, context_data=None):
702
+ """Record analytics metric"""
703
+ try:
704
+ db = get_db()
705
+
706
+ db.execute(
707
+ '''INSERT INTO user_analytics (user_id, metric_name, metric_value, metric_date, context_data)
708
+ VALUES (?, ?, ?, DATE('now'), ?)''',
709
+ (user_id, metric_name, metric_value, context_data)
710
+ )
711
+ db.commit()
712
+ return True
713
+
714
+ except Exception as e:
715
+ logger.error(f"Error recording analytics: {e}")
716
+ return False
717
+
718
+ def get_user_analytics(user_id, metric_name=None, days=30):
719
+ """Get user analytics data"""
720
+ try:
721
+ db = get_db()
722
+
723
+ if metric_name:
724
+ analytics = db.execute(
725
+ '''SELECT * FROM user_analytics
726
+ WHERE user_id = ? AND metric_name = ?
727
+ AND metric_date >= DATE('now', '-{} days')
728
+ ORDER BY metric_date DESC'''.format(days),
729
+ (user_id, metric_name)
730
+ ).fetchall()
731
+ else:
732
+ analytics = db.execute(
733
+ '''SELECT * FROM user_analytics
734
+ WHERE user_id = ?
735
+ AND metric_date >= DATE('now', '-{} days')
736
+ ORDER BY metric_date DESC'''.format(days),
737
+ (user_id,)
738
+ ).fetchall()
739
+
740
+ return [dict(metric) for metric in analytics]
741
+
742
+ except Exception as e:
743
+ logger.error(f"Error getting analytics: {e}")
744
+ return []
flask_app.py ADDED
@@ -0,0 +1,1247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #app.py
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
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
+ app = Flask(__name__)
36
+
37
+ # Configuration for sessions
38
+ app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-change-in-production')
39
+ app.config['SESSION_TYPE'] = 'filesystem'
40
+ app.config['SESSION_PERMANENT'] = False
41
+ app.config['SESSION_USE_SIGNER'] = True
42
+ app.config['SESSION_KEY_PREFIX'] = 'englishhelper:'
43
+
44
+ Session(app)
45
+
46
+ # Initialize database
47
+ def initialize_database():
48
+ init_db()
49
+
50
+ # Initialize on startup
51
+ initialize_database()
52
+
53
+ # Token tracking helper function
54
+ def track_token_usage(user_id, provider, input_tokens, output_tokens, operation):
55
+ """Helper function to track token usage"""
56
+ try:
57
+ admin_manager.record_token_usage(user_id, provider, input_tokens, output_tokens, operation)
58
+ except Exception as e:
59
+ print(f"Token tracking error: {e}")
60
+
61
+ @app.teardown_appcontext
62
+ def close_database(error):
63
+ close_db(error)
64
+
65
+ # --- CONFIGURAÇÃO DAS APIS LLM ---
66
+ genai_client = None
67
+ groq_client = None
68
+
69
+ # 1. ConfiguraΓ§Γ£o Gemini
70
+ try:
71
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
72
+ if GEMINI_API_KEY:
73
+ genai.configure(api_key=GEMINI_API_KEY)
74
+ genai_client = genai
75
+ else:
76
+ print("AVISO: GEMINI_API_KEY nΓ£o configurada.")
77
+ except Exception as e:
78
+ genai_client = None
79
+ print(f"ERRO ao inicializar o cliente Gemini: {e}.")
80
+
81
+ # 2. ConfiguraΓ§Γ£o Groq
82
+ try:
83
+ GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
84
+ if GROQ_API_KEY:
85
+ groq_client = Groq(api_key=GROQ_API_KEY)
86
+ else:
87
+ print("AVISO: GROQ_API_KEY nΓ£o configurada.")
88
+ except Exception as e:
89
+ groq_client = None
90
+ print(f"ERRO ao inicializar o cliente Groq: {e}.")
91
+
92
+
93
+ # --- ROTA PARA LISTAR MODELOS DINAMICAMENTE ---
94
+ @app.route('/list-models')
95
+ def list_models():
96
+ available_models = []
97
+ groq_text_models = [
98
+ "llama-3.1-8b-instant",
99
+ "llama-3.3-70b-versatile",
100
+ "openai/gpt-oss-120b",
101
+ "openai/gpt-oss-20b"
102
+ ]
103
+ try:
104
+ if genai_client:
105
+ for m in genai_client.list_models():
106
+ if 'generateContent' in m.supported_generation_methods:
107
+ model_name = m.name.replace("models/", "")
108
+ if "flash" in model_name or "pro" in model_name:
109
+ available_models.append({
110
+ "value": f"gemini:{model_name}",
111
+ "name": m.display_name
112
+ })
113
+ if groq_client:
114
+ for model_id in groq_text_models:
115
+ display_name = model_id.split('/')[-1].replace('-instant', '').replace('-versatile', '')
116
+ available_models.append({
117
+ "value": f"groq:{model_id}",
118
+ "name": f"Groq: {display_name}"
119
+ })
120
+ except Exception as e:
121
+ print(f"Erro ao listar modelos: {e}")
122
+ return jsonify([
123
+ {"value": "gemini:gemini-2.5-flash-latest", "name": "Gemini 2.5 Flash (Fallback)"},
124
+ {"value": "groq:llama-3.1-8b-instant", "name": "Llama 3.1 8B (Fallback)"}
125
+ ])
126
+ return jsonify(available_models)
127
+
128
+
129
+ # --- ROTAS PRINCIPAIS ---
130
+
131
+ @app.route('/tts-proxy', methods=['POST'])
132
+ def tts_proxy():
133
+ data = request.get_json()
134
+ text = data.get('text', '')
135
+ tld = data.get('tld', 'co.uk')
136
+ if not text: return jsonify({"error": "No text provided"}), 400
137
+ try:
138
+ tts = gTTS(text=text, lang='en', tld=tld)
139
+ mp3_fp = io.BytesIO()
140
+ tts.write_to_fp(mp3_fp)
141
+ mp3_fp.seek(0)
142
+ return send_file(mp3_fp, mimetype='audio/mpeg')
143
+ except Exception as e:
144
+ return jsonify({"error": f"Failed to generate audio via gTTS: {e}"}), 500
145
+
146
+ @app.route('/explain-proxy', methods=['POST'])
147
+ def explain_proxy():
148
+ data = request.get_json()
149
+ model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1)
150
+ context_focus = data.get('context_focus', 'General/Social')
151
+ custom_prompt = data.get('custom_prompt', None)
152
+ word = data.get('word', '').strip()
153
+ context = data.get('context', '')
154
+ for_flashcard = data.get('for_flashcard', False)
155
+
156
+ if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client):
157
+ return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503
158
+
159
+ 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."
160
+ try:
161
+ if custom_prompt:
162
+ activity_text = get_ai_text_response(model_provider, model_name, system_instruction_base, custom_prompt)
163
+ return jsonify({"explanation": activity_text})
164
+
165
+ if not word: return jsonify({"error": "No word selected."}), 400
166
+
167
+ if for_flashcard:
168
+ 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"]}
169
+ prompt = f"Analyze '{word}' in context: '{context}'. Generate a JSON for a flashcard. The 'gapped_sentence' must replace '{word}' with '______________'."
170
+ return jsonify(get_ai_text_response(model_provider, model_name, system_instruction_base, prompt, json_schema=schema))
171
+ else:
172
+ prompt = f"Analyze '{word}' in context: '{context}'. Provide a one-sentence English explanation, then '---', then the Portuguese translation."
173
+ parts = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt).split('---', 1)
174
+ return jsonify({"explanation": parts[0].strip(), "translation": parts[1].strip() if len(parts) > 1 else 'N/A'})
175
+ except Exception as e:
176
+ print(f"AI ANALYSIS ERROR in /explain-proxy: {e}")
177
+ return jsonify({"error": f"AI analysis failed: {e}"}), 500
178
+
179
+ @app.route('/activity-feedback', methods=['POST'])
180
+ def activity_feedback():
181
+ data = request.get_json()
182
+ model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1)
183
+ context_focus = data.get('context_focus', 'General/Social')
184
+ original_prompt = data.get('original_prompt', '')
185
+ user_response = data.get('user_response', '')
186
+
187
+ if not original_prompt or not user_response: return jsonify({"error": "Original prompt and user response are required."}), 400
188
+ if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client):
189
+ return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503
190
+
191
+ system_instruction = (
192
+ "You are an expert English teacher providing feedback. "
193
+ f"The user's study focus is '{context_focus}'. "
194
+ "Your entire response MUST be in English. "
195
+ "Provide clear, constructive feedback on the user's writing. "
196
+ "Point out grammar, spelling, or style errors. "
197
+ "Offer a corrected or improved version of their text. "
198
+ "Structure your feedback with markdown for clarity (e.g., using ### Corrected Version)."
199
+ )
200
+ 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."
201
+ try:
202
+ feedback_text = get_ai_text_response(model_provider, model_name, system_instruction, user_prompt)
203
+ return jsonify({"feedback": feedback_text})
204
+ except Exception as e:
205
+ return jsonify({"error": f"AI feedback failed: {e}"}), 500
206
+
207
+ @app.route('/analyze-image', methods=['POST'])
208
+ def analyze_image():
209
+ if not genai_client: return jsonify({"error": "GEMINI_API_KEY not configured."}), 503
210
+ data = request.get_json()
211
+ base64_image = data.get('image')
212
+ model_value = data.get('model', 'gemini:gemini-2.5-flash-latest')
213
+
214
+ model_name = 'gemini-2.5-flash-latest' # Default
215
+ if model_value.startswith('gemini:'):
216
+ model_name = model_value.split(':', 1)[1]
217
+
218
+ if not base64_image: return jsonify({"error": "No image data."}), 400
219
+ try:
220
+ image = Image.open(io.BytesIO(base64.b64decode(base64_image.split(',')[1])))
221
+ model = genai_client.GenerativeModel(model_name)
222
+ schema = { "type": "object", "properties": { "vocabulary": { "type": "array", "items": { "type": "object", "properties": { "term": {"type": "string"}, "definition": {"type": "string"} }, "required": ["term", "definition"] } } }, "required": ["vocabulary"] }
223
+ 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 ]
224
+
225
+ config = GenerationConfig(response_mime_type="application/json", response_schema=schema)
226
+ response = model.generate_content(prompt, generation_config=config)
227
+
228
+ return jsonify(json.loads(response.text)['vocabulary'])
229
+ except Exception as e:
230
+ return jsonify({"error": f"Image analysis failed: {e}"}), 500
231
+
232
+ @app.route('/chat-with-ai', methods=['POST'])
233
+ def chat_with_ai():
234
+ if not groq_client: return jsonify({"error": "GROQ_API_KEY not configured."}), 503
235
+ data = request.get_json()
236
+ history, user_message = data.get('history', []), data.get('message', '')
237
+ if not user_message: return jsonify({"error": "No message."}), 400
238
+ try:
239
+ 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."
240
+ messages = [{"role": "system", "content": system}] + history + [{"role": "user", "content": user_message}]
241
+ response = groq_client.chat.completions.create(model="llama-3.1-8b-instant", messages=messages, temperature=0.7)
242
+
243
+ # Track token usage
244
+ user = get_current_user()
245
+ if user and hasattr(response, 'usage'):
246
+ track_token_usage(
247
+ user['id'],
248
+ 'groq',
249
+ response.usage.prompt_tokens,
250
+ response.usage.completion_tokens,
251
+ 'conversation'
252
+ )
253
+
254
+ return jsonify({"response": response.choices[0].message.content.strip()})
255
+ except Exception as e:
256
+ return jsonify({"error": f"AI chat failed: {e}"}), 500
257
+
258
+ @app.route('/pronunciation-feedback', methods=['POST'])
259
+ def pronunciation_feedback():
260
+ if not groq_client: return jsonify({"error": "GROQ_API_KEY not configured."}), 503
261
+ data = request.get_json()
262
+ target_text, user_text = data.get('target_text'), data.get('user_text')
263
+ if not target_text or not user_text: return jsonify({"error": "Required data missing."}), 400
264
+ try:
265
+ 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."
266
+ user_prompt = f"Target: \"{target_text}\"\nTranscription: \"{user_text}\"\n\nProvide pronunciation feedback."
267
+ messages = [{"role": "system", "content": system_instruction}, {"role": "user", "content": user_prompt}]
268
+ response = groq_client.chat.completions.create(model="llama-3.1-8b-instant", messages=messages, temperature=0.5)
269
+
270
+ # Track token usage
271
+ user = get_current_user()
272
+ if user and hasattr(response, 'usage'):
273
+ track_token_usage(
274
+ user['id'],
275
+ 'groq',
276
+ response.usage.prompt_tokens,
277
+ response.usage.completion_tokens,
278
+ 'pronunciation_feedback'
279
+ )
280
+
281
+ return jsonify({"feedback": response.choices[0].message.content.strip()})
282
+ except Exception as e:
283
+ return jsonify({"error": f"Pronunciation analysis failed: {e}"}), 500
284
+
285
+ @app.route('/generate-image', methods=['POST'])
286
+ def generate_image():
287
+ if not genai_client: return jsonify({"error": "GEMINI_API_KEY not configured."}), 503
288
+ data = request.get_json()
289
+ prompt = data.get('prompt')
290
+ if not prompt: return jsonify({"error": "Image prompt is required."}), 400
291
+ try:
292
+ model = genai_client.GenerativeModel(model_name='gemini-2.5-flash-image-preview')
293
+ response = model.generate_content(prompt)
294
+ base64_image_data = response.parts[0].inline_data.data
295
+ return jsonify({"image_base64": base64_image_data})
296
+ except Exception as e:
297
+ return jsonify({"error": f"Image generation failed: {e}"}), 500
298
+
299
+ # --- FUNÇÃO AUXILIAR E ROTA RAIZ ---
300
+ def get_ai_text_response(provider, model_name, system_instruction, user_prompt, json_schema=None):
301
+ if provider == 'gemini':
302
+ model = genai_client.GenerativeModel(model_name, system_instruction=system_instruction)
303
+
304
+ config = None
305
+ if json_schema:
306
+ config = GenerationConfig(response_mime_type="application/json", response_schema=json_schema)
307
+
308
+ response = model.generate_content(user_prompt, generation_config=config)
309
+
310
+ if json_schema:
311
+ parsed_json = json.loads(response.text)
312
+ required_keys = json_schema.get("required", [])
313
+ if not all(key in parsed_json and parsed_json[key] for key in required_keys):
314
+ raise ValueError(f"AI response missing required keys or has empty values.")
315
+ return parsed_json
316
+ else:
317
+ return response.text.strip()
318
+
319
+ elif provider == 'groq':
320
+ final_user_prompt = user_prompt
321
+ if json_schema:
322
+ 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)}"
323
+
324
+ messages = [{"role": "system", "content": system_instruction}, {"role": "user", "content": final_user_prompt}]
325
+ config = {'response_format': {"type": "json_object"}} if json_schema else {}
326
+ response = groq_client.chat.completions.create(model=model_name, messages=messages, **config)
327
+
328
+ if json_schema:
329
+ parsed_json = json.loads(response.choices[0].message.content)
330
+ required_keys = json_schema.get("required", [])
331
+ if not all(key in parsed_json and parsed_json[key] for key in required_keys):
332
+ raise ValueError(f"AI response missing required keys or has empty values.")
333
+ return parsed_json
334
+ else:
335
+ return response.choices[0].message.content.strip()
336
+
337
+ raise Exception(f"Unsupported provider: {provider}")
338
+
339
+ # --- AUTHENTICATION ROUTES ---
340
+
341
+ @app.route('/register', methods=['POST'])
342
+ def register():
343
+ """User registration endpoint"""
344
+ try:
345
+ data = request.get_json()
346
+ email = data.get('email', '').strip().lower()
347
+ password = data.get('password', '')
348
+
349
+ # Validate input
350
+ if not email or not password:
351
+ return jsonify({'error': 'Email and password are required'}), 400
352
+
353
+ if len(password) < 8:
354
+ return jsonify({'error': 'Password must be at least 8 characters long'}), 400
355
+
356
+ # Validate email format
357
+ try:
358
+ validate_email(email)
359
+ except EmailNotValidError:
360
+ return jsonify({'error': 'Invalid email format'}), 400
361
+
362
+ # Create user
363
+ result = create_user(email, password)
364
+
365
+ if result['success']:
366
+ # Send confirmation email
367
+ if send_confirmation_email(email, result['confirmation_token']):
368
+ return jsonify({
369
+ 'message': 'Registration successful! Please check your email to confirm your account.',
370
+ 'email_sent': True
371
+ }), 201
372
+ else:
373
+ return jsonify({
374
+ 'message': 'Registration successful! However, we could not send the confirmation email. Please contact support.',
375
+ 'email_sent': False
376
+ }), 201
377
+ else:
378
+ return jsonify({'error': result['message']}), 400
379
+
380
+ except Exception as e:
381
+ print(f"Registration error: {e}")
382
+ return jsonify({'error': 'Internal server error'}), 500
383
+
384
+ @app.route('/login', methods=['POST'])
385
+ def login():
386
+ """User login endpoint"""
387
+ try:
388
+ data = request.get_json()
389
+ email = data.get('email', '').strip().lower()
390
+ password = data.get('password', '')
391
+
392
+ if not email or not password:
393
+ return jsonify({'error': 'Email and password are required'}), 400
394
+
395
+ result = authenticate_user(email, password)
396
+
397
+ if result['success']:
398
+ session['user_id'] = result['user_id']
399
+ session['user_email'] = result['email']
400
+ session.permanent = True
401
+
402
+ # Get user settings
403
+ settings = get_user_settings(result['user_id'])
404
+
405
+ return jsonify({
406
+ 'message': 'Login successful',
407
+ 'user': {
408
+ 'id': result['user_id'],
409
+ 'email': result['email'],
410
+ 'settings': settings
411
+ }
412
+ }), 200
413
+ else:
414
+ return jsonify({'error': result['message']}), 401
415
+
416
+ except Exception as e:
417
+ print(f"Login error: {e}")
418
+ return jsonify({'error': 'Internal server error'}), 500
419
+
420
+ @app.route('/logout', methods=['POST'])
421
+ def logout():
422
+ """User logout endpoint"""
423
+ session.clear()
424
+ return jsonify({'message': 'Logout successful'}), 200
425
+
426
+ @app.route('/confirm-email')
427
+ def confirm_email_route():
428
+ """Email confirmation endpoint"""
429
+ token = request.args.get('token')
430
+
431
+ if not token:
432
+ return render_template_string('''
433
+ <!DOCTYPE html>
434
+ <html><head><title>Invalid Link</title></head>
435
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
436
+ <h2>Invalid Confirmation Link</h2>
437
+ <p>This confirmation link is invalid or malformed.</p>
438
+ <a href="/" style="color: #4f46e5;">Return to English Helper</a>
439
+ </body></html>
440
+ '''), 400
441
+
442
+ result = confirm_email(token)
443
+
444
+ if result['success']:
445
+ return render_template_string('''
446
+ <!DOCTYPE html>
447
+ <html><head><title>Email Confirmed</title></head>
448
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
449
+ <h2>βœ… Email Confirmed!</h2>
450
+ <p>Your email has been successfully confirmed. You can now log in to your account.</p>
451
+ <a href="/" style="background: #4f46e5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Continue to English Helper</a>
452
+ </body></html>
453
+ ''')
454
+ else:
455
+ return render_template_string('''
456
+ <!DOCTYPE html>
457
+ <html><head><title>Confirmation Failed</title></head>
458
+ <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
459
+ <h2>❌ Confirmation Failed</h2>
460
+ <p>This confirmation link is invalid or has expired.</p>
461
+ <a href="/" style="color: #4f46e5;">Return to English Helper</a>
462
+ </body></html>
463
+ '''), 400
464
+
465
+ @app.route('/user/profile', methods=['GET'])
466
+ @login_required
467
+ def get_user_profile():
468
+ """Get current user profile"""
469
+ user = get_current_user()
470
+ if not user:
471
+ return jsonify({'error': 'User not found'}), 404
472
+
473
+ settings = get_user_settings(user['id'])
474
+ flashcards_count = len(get_user_flashcards(user['id'], 1000))
475
+
476
+ return jsonify({
477
+ 'user': {
478
+ 'id': user['id'],
479
+ 'email': user['email'],
480
+ 'settings': settings,
481
+ 'stats': {
482
+ 'flashcards_created': flashcards_count
483
+ }
484
+ }
485
+ })
486
+
487
+ @app.route('/user/settings', methods=['GET', 'POST'])
488
+ @login_required
489
+ def user_settings():
490
+ """Get or update user settings"""
491
+ user = get_current_user()
492
+ if not user:
493
+ return jsonify({'error': 'User not found'}), 404
494
+
495
+ if request.method == 'GET':
496
+ settings = get_user_settings(user['id'])
497
+ return jsonify({'settings': settings})
498
+
499
+ elif request.method == 'POST':
500
+ data = request.get_json()
501
+ settings = {
502
+ 'preferred_model': data.get('preferred_model'),
503
+ 'context_focus': data.get('context_focus'),
504
+ 'voice_accent': data.get('voice_accent'),
505
+ 'daily_goal': data.get('daily_goal', 10),
506
+ 'notification_enabled': data.get('notification_enabled', True)
507
+ }
508
+
509
+ if update_user_settings(user['id'], settings):
510
+ return jsonify({'message': 'Settings updated successfully'})
511
+ else:
512
+ return jsonify({'error': 'Failed to update settings'}), 500
513
+
514
+ @app.route('/user/flashcards', methods=['GET', 'POST'])
515
+ @login_required
516
+ def user_flashcards():
517
+ """Get user flashcards or save new flashcard"""
518
+ user = get_current_user()
519
+ if not user:
520
+ return jsonify({'error': 'User not found'}), 404
521
+
522
+ if request.method == 'GET':
523
+ flashcards = get_user_flashcards(user['id'])
524
+ return jsonify({'flashcards': flashcards})
525
+
526
+ elif request.method == 'POST':
527
+ data = request.get_json()
528
+ if save_user_flashcard(user['id'], data):
529
+ return jsonify({'message': 'Flashcard saved successfully'})
530
+ else:
531
+ return jsonify({'error': 'Failed to save flashcard'}), 500
532
+
533
+ @app.route('/auth/check', methods=['GET'])
534
+ def check_auth():
535
+ """Check if user is authenticated"""
536
+ user = get_current_user()
537
+ if user:
538
+ settings = get_user_settings(user['id'])
539
+ return jsonify({
540
+ 'authenticated': True,
541
+ 'user': {
542
+ 'id': user['id'],
543
+ 'email': user['email'],
544
+ 'settings': settings
545
+ }
546
+ })
547
+ else:
548
+ return jsonify({'authenticated': False})
549
+
550
+ # --- MODIFIED EXISTING ROUTES TO SUPPORT USER DATA ---
551
+
552
+ # Override the original explain-proxy to save flashcards for logged-in users
553
+ @app.route('/explain-proxy', methods=['POST'])
554
+ def explain_proxy():
555
+ data = request.get_json()
556
+ model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1)
557
+ context_focus = data.get('context_focus', 'General/Social')
558
+ custom_prompt = data.get('custom_prompt', None)
559
+ word = data.get('word', '').strip()
560
+ context = data.get('context', '')
561
+ for_flashcard = data.get('for_flashcard', False)
562
+
563
+ if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client):
564
+ return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503
565
+
566
+ 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."
567
+ try:
568
+ if custom_prompt:
569
+ activity_text = get_ai_text_response(model_provider, model_name, system_instruction_base, custom_prompt)
570
+ return jsonify({"explanation": activity_text})
571
+
572
+ if not word: return jsonify({"error": "No word selected."}), 400
573
+
574
+ if for_flashcard:
575
+ 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"]}
576
+ prompt = f"Analyze '{word}' in context: '{context}'. Generate a JSON for a flashcard. The 'gapped_sentence' must replace '{word}' with '______________'."
577
+ flashcard_data = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt, json_schema=schema)
578
+
579
+ # Save flashcard for logged-in users
580
+ user = get_current_user()
581
+ if user:
582
+ save_user_flashcard(user['id'], flashcard_data)
583
+
584
+ return jsonify(flashcard_data)
585
+ else:
586
+ prompt = f"Analyze '{word}' in context: '{context}'. Provide a one-sentence English explanation, then '---', then the Portuguese translation."
587
+ parts = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt).split('---', 1)
588
+ return jsonify({"explanation": parts[0].strip(), "translation": parts[1].strip() if len(parts) > 1 else 'N/A'})
589
+ except Exception as e:
590
+ print(f"AI ANALYSIS ERROR in /explain-proxy: {e}")
591
+ return jsonify({"error": f"AI analysis failed: {e}"}), 500
592
+
593
+ # --- CONTENT CURATION ROUTES ---
594
+
595
+ @app.route('/content/search', methods=['POST'])
596
+ @login_required
597
+ def search_content():
598
+ """Search for content based on user interests"""
599
+ try:
600
+ user = get_current_user()
601
+ if not user:
602
+ return jsonify({'error': 'User not found'}), 404
603
+
604
+ data = request.get_json()
605
+ query = data.get('query', '')
606
+ category = data.get('category', '')
607
+
608
+ # Get user settings and interests
609
+ settings = get_user_settings(user['id'])
610
+ interests = get_user_interests(user['id'])
611
+
612
+ if not interests and query:
613
+ # Use query as interest if no interests set
614
+ interests = {query: 1.0}
615
+
616
+ english_level = settings.get('english_level', 'B1') if settings else 'B1'
617
+ context_focus = settings.get('context_focus', 'General/Social') if settings else 'General/Social'
618
+
619
+ # Search for content
620
+ results = content_curator.search_content(
621
+ interests=list(interests.keys()) if interests else [query],
622
+ english_level=english_level,
623
+ context_focus=context_focus,
624
+ limit=10
625
+ )
626
+
627
+ return jsonify({'results': results})
628
+
629
+ except Exception as e:
630
+ print(f"Content search error: {e}")
631
+ return jsonify({'error': 'Content search failed'}), 500
632
+
633
+ @app.route('/content/extract', methods=['POST'])
634
+ @login_required
635
+ def extract_content():
636
+ """Extract content from URL"""
637
+ try:
638
+ data = request.get_json()
639
+ url = data.get('url', '')
640
+
641
+ if not url:
642
+ return jsonify({'error': 'URL required'}), 400
643
+
644
+ result = content_curator.extract_content_from_url(url)
645
+ return jsonify(result)
646
+
647
+ except Exception as e:
648
+ print(f"Content extraction error: {e}")
649
+ return jsonify({'error': 'Content extraction failed'}), 500
650
+
651
+ @app.route('/content/save', methods=['POST'])
652
+ @login_required
653
+ def save_content():
654
+ """Save content/article for user"""
655
+ try:
656
+ user = get_current_user()
657
+ if not user:
658
+ return jsonify({'error': 'User not found'}), 404
659
+
660
+ data = request.get_json()
661
+ title = data.get('title', '')
662
+ content = data.get('content', '')
663
+ source_url = data.get('source_url')
664
+ source_type = data.get('source_type', 'manual')
665
+ category = data.get('category')
666
+
667
+ if not title or not content:
668
+ return jsonify({'error': 'Title and content required'}), 400
669
+
670
+ result = save_user_article(user['id'], title, content, source_url, source_type, category)
671
+
672
+ if result['success']:
673
+ # Record analytics
674
+ record_analytics_metric(user['id'], 'content_saved', 1)
675
+ return jsonify({'message': 'Content saved successfully', 'article_id': result['article_id']})
676
+ else:
677
+ return jsonify({'error': result['message']}), 500
678
+
679
+ except Exception as e:
680
+ print(f"Save content error: {e}")
681
+ return jsonify({'error': 'Failed to save content'}), 500
682
+
683
+ @app.route('/content/articles', methods=['GET'])
684
+ @login_required
685
+ def get_articles():
686
+ """Get user's saved articles"""
687
+ try:
688
+ user = get_current_user()
689
+ if not user:
690
+ return jsonify({'error': 'User not found'}), 404
691
+
692
+ category = request.args.get('category')
693
+ limit = int(request.args.get('limit', 50))
694
+
695
+ articles = get_user_articles(user['id'], category, limit)
696
+ return jsonify({'articles': articles})
697
+
698
+ except Exception as e:
699
+ print(f"Get articles error: {e}")
700
+ return jsonify({'error': 'Failed to get articles'}), 500
701
+
702
+ @app.route('/content/interests', methods=['GET', 'POST'])
703
+ @login_required
704
+ def manage_interests():
705
+ """Get or update user interests"""
706
+ try:
707
+ user = get_current_user()
708
+ if not user:
709
+ return jsonify({'error': 'User not found'}), 404
710
+
711
+ if request.method == 'GET':
712
+ interests = get_user_interests(user['id'])
713
+ return jsonify({'interests': interests})
714
+
715
+ elif request.method == 'POST':
716
+ data = request.get_json()
717
+ interests = data.get('interests', {})
718
+
719
+ if update_user_interests(user['id'], interests):
720
+ return jsonify({'message': 'Interests updated successfully'})
721
+ else:
722
+ return jsonify({'error': 'Failed to update interests'}), 500
723
+
724
+ except Exception as e:
725
+ print(f"Manage interests error: {e}")
726
+ return jsonify({'error': 'Failed to manage interests'}), 500
727
+
728
+ @app.route('/content/recommendations', methods=['GET'])
729
+ @login_required
730
+ def get_recommendations():
731
+ """Get AI-powered content recommendations"""
732
+ try:
733
+ user = get_current_user()
734
+ if not user:
735
+ return jsonify({'error': 'User not found'}), 404
736
+
737
+ # Get user data
738
+ interests = get_user_interests(user['id'])
739
+ recent_articles = get_user_articles(user['id'], limit=10)
740
+ settings = get_user_settings(user['id'])
741
+
742
+ english_level = settings.get('english_level', 'B1') if settings else 'B1'
743
+ context_focus = settings.get('context_focus', 'General/Social') if settings else 'General/Social'
744
+
745
+ # Generate recommendations
746
+ recommendations = content_curator.generate_personalized_recommendations(
747
+ interests, recent_articles, english_level, context_focus, user['id']
748
+ )
749
+
750
+ return jsonify({'recommendations': recommendations})
751
+
752
+ except Exception as e:
753
+ print(f"Recommendations error: {e}")
754
+ return jsonify({'error': 'Failed to get recommendations'}), 500
755
+
756
+ @app.route('/content/analyze', methods=['POST'])
757
+ @login_required
758
+ def analyze_content():
759
+ """Analyze content for learning insights"""
760
+ try:
761
+ user = get_current_user()
762
+ if not user:
763
+ return jsonify({'error': 'User not found'}), 404
764
+
765
+ data = request.get_json()
766
+ content = data.get('content', '')
767
+
768
+ if not content:
769
+ return jsonify({'error': 'Content required'}), 400
770
+
771
+ settings = get_user_settings(user['id'])
772
+ english_level = settings.get('english_level', 'B1') if settings else 'B1'
773
+
774
+ analysis = content_curator.analyze_content_for_learning(content, english_level)
775
+
776
+ return jsonify({'analysis': analysis})
777
+
778
+ except Exception as e:
779
+ print(f"Content analysis error: {e}")
780
+ return jsonify({'error': 'Content analysis failed'}), 500
781
+
782
+ # --- STUDY PLANNING ROUTES ---
783
+
784
+ @app.route('/study/plans', methods=['GET', 'POST'])
785
+ @login_required
786
+ def manage_study_plans():
787
+ """Get or create study plans"""
788
+ try:
789
+ user = get_current_user()
790
+ if not user:
791
+ return jsonify({'error': 'User not found'}), 404
792
+
793
+ if request.method == 'GET':
794
+ plans = get_user_study_plans(user['id'])
795
+ return jsonify({'plans': plans})
796
+
797
+ elif request.method == 'POST':
798
+ data = request.get_json()
799
+ plan_name = data.get('plan_name', '')
800
+ target_level = data.get('target_level', 'B2')
801
+ current_level = data.get('current_level', 'B1')
802
+ objectives = json.dumps(data.get('objectives', []))
803
+ weekly_hours = data.get('weekly_hours', 5)
804
+
805
+ if not plan_name:
806
+ return jsonify({'error': 'Plan name required'}), 400
807
+
808
+ result = create_study_plan(user['id'], plan_name, target_level, current_level, objectives, weekly_hours)
809
+
810
+ if result['success']:
811
+ return jsonify({'message': 'Study plan created', 'plan_id': result['plan_id']})
812
+ else:
813
+ return jsonify({'error': result['message']}), 500
814
+
815
+ except Exception as e:
816
+ print(f"Study plans error: {e}")
817
+ return jsonify({'error': 'Failed to manage study plans'}), 500
818
+
819
+ @app.route('/analytics/dashboard', methods=['GET'])
820
+ @login_required
821
+ def analytics_dashboard():
822
+ """Get analytics dashboard data"""
823
+ try:
824
+ user = get_current_user()
825
+ if not user:
826
+ return jsonify({'error': 'User not found'}), 404
827
+
828
+ days = int(request.args.get('days', 30))
829
+
830
+ # Get various analytics
831
+ analytics_data = {
832
+ 'flashcards_created': get_user_analytics(user['id'], 'flashcards_created', days),
833
+ 'content_saved': get_user_analytics(user['id'], 'content_saved', days),
834
+ 'study_sessions': get_user_analytics(user['id'], 'study_session', days),
835
+ 'total_flashcards': len(get_user_flashcards(user['id'], 1000)),
836
+ 'total_articles': len(get_user_articles(user['id'], limit=1000)),
837
+ 'user_level': get_user_settings(user['id']).get('english_level', 'B1')
838
+ }
839
+
840
+ return jsonify({'analytics': analytics_data})
841
+
842
+ except Exception as e:
843
+ print(f"Analytics error: {e}")
844
+ return jsonify({'error': 'Failed to get analytics'}), 500
845
+
846
+ # --- STUDY PLANNER ROUTES ---
847
+
848
+ @app.route('/study-plan/create', methods=['POST'])
849
+ @login_required
850
+ def create_study_plan_route():
851
+ """Create a personalized study plan"""
852
+ try:
853
+ user = get_current_user()
854
+ if not user:
855
+ return jsonify({'error': 'User not found'}), 404
856
+
857
+ data = request.get_json()
858
+
859
+ # Get user settings and interests
860
+ user_settings = get_user_settings(user['id'])
861
+ user_interests = get_user_interests(user['id'])
862
+
863
+ # Prepare data for study planner
864
+ planner_data = {
865
+ 'english_level': data.get('current_level') or user_settings.get('english_level', 'B1'),
866
+ 'target_level': data.get('target_level', 'B2'),
867
+ 'weekly_hours': int(data.get('weekly_hours', 5)),
868
+ 'context_focus': data.get('context_focus') or user_settings.get('context_focus', 'General/Social'),
869
+ 'interests': user_interests,
870
+ 'study_goals': data.get('study_goals', [])
871
+ }
872
+
873
+ # Generate the plan
874
+ result = study_planner.generate_personalized_plan(planner_data)
875
+
876
+ if result['success']:
877
+ plan = result['plan']
878
+
879
+ # Save to database
880
+ plan_id = create_study_plan(
881
+ user['id'],
882
+ plan['target_level'],
883
+ plan['weekly_hours'],
884
+ plan['estimated_weeks'],
885
+ json.dumps(plan)
886
+ )
887
+
888
+ plan['id'] = plan_id
889
+
890
+ # Record analytics
891
+ record_analytics_metric(user['id'], 'study_plan_created', 1)
892
+
893
+ return jsonify({'success': True, 'plan': plan})
894
+ else:
895
+ return jsonify({'success': False, 'error': result['error']}), 500
896
+
897
+ except Exception as e:
898
+ print(f"Study plan creation error: {e}")
899
+ return jsonify({'error': 'Failed to create study plan'}), 500
900
+
901
+ @app.route('/study-plan/current', methods=['GET'])
902
+ @login_required
903
+ def get_current_study_plan():
904
+ """Get user's current study plan"""
905
+ try:
906
+ user = get_current_user()
907
+ if not user:
908
+ return jsonify({'error': 'User not found'}), 404
909
+
910
+ plans = get_user_study_plans(user['id'])
911
+
912
+ if plans:
913
+ # Get the most recent active plan
914
+ current_plan = plans[0] # Assuming most recent first
915
+
916
+ # Parse the plan data
917
+ plan_data = json.loads(current_plan['plan_data'])
918
+
919
+ # Add database ID
920
+ plan_data['db_id'] = current_plan['id']
921
+
922
+ # Get activities for this plan
923
+ activities = get_study_activities(current_plan['id'])
924
+ plan_data['completed_activities'] = activities
925
+
926
+ return jsonify({'success': True, 'plan': plan_data})
927
+ else:
928
+ return jsonify({'success': True, 'plan': None})
929
+
930
+ except Exception as e:
931
+ print(f"Get study plan error: {e}")
932
+ return jsonify({'error': 'Failed to get study plan'}), 500
933
+
934
+ @app.route('/study-plan/activity/complete', methods=['POST'])
935
+ @login_required
936
+ def complete_study_activity():
937
+ """Mark a study activity as completed"""
938
+ try:
939
+ user = get_current_user()
940
+ if not user:
941
+ return jsonify({'error': 'User not found'}), 404
942
+
943
+ data = request.get_json()
944
+ plan_id = data.get('plan_id')
945
+ activity_id = data.get('activity_id')
946
+ duration_minutes = data.get('duration_minutes', 0)
947
+ notes = data.get('notes', '')
948
+
949
+ if not plan_id or not activity_id:
950
+ return jsonify({'error': 'Missing plan_id or activity_id'}), 400
951
+
952
+ # Add activity completion
953
+ add_study_activity(plan_id, activity_id, duration_minutes, notes)
954
+
955
+ # Record analytics
956
+ record_analytics_metric(user['id'], 'study_activity_completed', 1)
957
+ record_analytics_metric(user['id'], 'study_time_minutes', duration_minutes)
958
+
959
+ return jsonify({'success': True})
960
+
961
+ except Exception as e:
962
+ print(f"Complete activity error: {e}")
963
+ return jsonify({'error': 'Failed to complete activity'}), 500
964
+
965
+ @app.route('/study-plan/progress', methods=['GET'])
966
+ @login_required
967
+ def get_study_progress():
968
+ """Get study plan progress analytics"""
969
+ try:
970
+ user = get_current_user()
971
+ if not user:
972
+ return jsonify({'error': 'User not found'}), 404
973
+
974
+ plans = get_user_study_plans(user['id'])
975
+
976
+ if not plans:
977
+ return jsonify({'success': True, 'progress': None})
978
+
979
+ current_plan = plans[0]
980
+ plan_data = json.loads(current_plan['plan_data'])
981
+ activities = get_study_activities(current_plan['id'])
982
+
983
+ # Calculate progress
984
+ total_activities = len(plan_data.get('activities', []))
985
+ completed_activities = len(activities)
986
+
987
+ progress_data = {
988
+ 'total_activities': total_activities,
989
+ 'completed_activities': completed_activities,
990
+ 'completion_percentage': (completed_activities / max(total_activities, 1)) * 100,
991
+ 'estimated_weeks': plan_data.get('estimated_weeks', 0),
992
+ 'weeks_elapsed': max(1, (datetime.now() - datetime.fromisoformat(current_plan['created_at'])).days // 7),
993
+ 'target_level': plan_data.get('target_level', 'B2'),
994
+ 'weekly_hours': plan_data.get('weekly_hours', 5),
995
+ 'recent_activities': activities[-10:] if activities else [] # Last 10 activities
996
+ }
997
+
998
+ return jsonify({'success': True, 'progress': progress_data})
999
+
1000
+ except Exception as e:
1001
+ print(f"Study progress error: {e}")
1002
+ return jsonify({'error': 'Failed to get study progress'}), 500
1003
+
1004
+ # --- ADMIN ROUTES ---
1005
+
1006
+ @app.route('/admin/login', methods=['POST'])
1007
+ def admin_login():
1008
+ """Admin login endpoint"""
1009
+ try:
1010
+ data = request.get_json()
1011
+ username = data.get('username')
1012
+ password = data.get('password')
1013
+
1014
+ if admin_manager.login_admin(username, password):
1015
+ return jsonify({'success': True, 'message': 'Admin logged in successfully'})
1016
+ else:
1017
+ return jsonify({'success': False, 'error': 'Invalid credentials'}), 401
1018
+
1019
+ except Exception as e:
1020
+ print(f"Admin login error: {e}")
1021
+ return jsonify({'error': 'Admin login failed'}), 500
1022
+
1023
+ @app.route('/admin/logout', methods=['POST'])
1024
+ @admin_required
1025
+ def admin_logout():
1026
+ """Admin logout endpoint"""
1027
+ try:
1028
+ admin_manager.logout_admin()
1029
+ return jsonify({'success': True, 'message': 'Admin logged out successfully'})
1030
+ except Exception as e:
1031
+ print(f"Admin logout error: {e}")
1032
+ return jsonify({'error': 'Admin logout failed'}), 500
1033
+
1034
+ @app.route('/admin/check', methods=['GET'])
1035
+ def admin_check():
1036
+ """Check admin authentication status"""
1037
+ try:
1038
+ is_authenticated = admin_manager.is_admin_logged_in()
1039
+ return jsonify({
1040
+ 'authenticated': is_authenticated,
1041
+ 'username': session.get('admin_username') if is_authenticated else None
1042
+ })
1043
+ except Exception as e:
1044
+ print(f"Admin check error: {e}")
1045
+ return jsonify({'authenticated': False})
1046
+
1047
+ @app.route('/admin/dashboard', methods=['GET'])
1048
+ @admin_required
1049
+ def admin_dashboard():
1050
+ """Get admin dashboard data"""
1051
+ try:
1052
+ stats = admin_manager.get_system_stats()
1053
+ return jsonify({'success': True, 'stats': stats})
1054
+ except Exception as e:
1055
+ print(f"Admin dashboard error: {e}")
1056
+ return jsonify({'error': 'Failed to load dashboard'}), 500
1057
+
1058
+ @app.route('/admin/users', methods=['GET'])
1059
+ @admin_required
1060
+ def admin_get_users():
1061
+ """Get paginated list of users"""
1062
+ try:
1063
+ page = int(request.args.get('page', 1))
1064
+ per_page = int(request.args.get('per_page', 20))
1065
+
1066
+ users_data = admin_manager.get_all_users(page, per_page)
1067
+ return jsonify({'success': True, 'data': users_data})
1068
+ except Exception as e:
1069
+ print(f"Admin get users error: {e}")
1070
+ return jsonify({'error': 'Failed to get users'}), 500
1071
+
1072
+ @app.route('/admin/users/<int:user_id>', methods=['GET'])
1073
+ @admin_required
1074
+ def admin_get_user_details(user_id):
1075
+ """Get detailed information about a user"""
1076
+ try:
1077
+ user_details = admin_manager.get_user_details(user_id)
1078
+ if user_details:
1079
+ return jsonify({'success': True, 'user': user_details})
1080
+ else:
1081
+ return jsonify({'error': 'User not found'}), 404
1082
+ except Exception as e:
1083
+ print(f"Admin get user details error: {e}")
1084
+ return jsonify({'error': 'Failed to get user details'}), 500
1085
+
1086
+ @app.route('/admin/users/<int:user_id>', methods=['DELETE'])
1087
+ @admin_required
1088
+ def admin_delete_user(user_id):
1089
+ """Delete a user and all associated data"""
1090
+ try:
1091
+ if admin_manager.delete_user(user_id):
1092
+ return jsonify({'success': True, 'message': 'User deleted successfully'})
1093
+ else:
1094
+ return jsonify({'error': 'Failed to delete user'}), 500
1095
+ except Exception as e:
1096
+ print(f"Admin delete user error: {e}")
1097
+ return jsonify({'error': 'Failed to delete user'}), 500
1098
+
1099
+ @app.route('/admin/database/schema', methods=['GET'])
1100
+ @admin_required
1101
+ def admin_get_database_schema():
1102
+ """Get database schema information"""
1103
+ try:
1104
+ schema = admin_manager.get_database_schema()
1105
+ return jsonify({'success': True, 'schema': schema})
1106
+ except Exception as e:
1107
+ print(f"Admin get schema error: {e}")
1108
+ return jsonify({'error': 'Failed to get database schema'}), 500
1109
+
1110
+ @app.route('/admin/token-usage', methods=['POST'])
1111
+ def record_token_usage():
1112
+ """Record token usage (called by AI functions)"""
1113
+ try:
1114
+ data = request.get_json()
1115
+ user_id = data.get('user_id')
1116
+ api_provider = data.get('api_provider')
1117
+ input_tokens = data.get('input_tokens', 0)
1118
+ output_tokens = data.get('output_tokens', 0)
1119
+ operation_type = data.get('operation_type', 'unknown')
1120
+
1121
+ admin_manager.record_token_usage(
1122
+ user_id, api_provider, input_tokens, output_tokens, operation_type
1123
+ )
1124
+
1125
+ return jsonify({'success': True})
1126
+ except Exception as e:
1127
+ print(f"Token usage recording error: {e}")
1128
+ return jsonify({'error': 'Failed to record token usage'}), 500
1129
+
1130
+ @app.route('/admin/export/users', methods=['GET'])
1131
+ @admin_required
1132
+ def export_users():
1133
+ """Export users data as CSV"""
1134
+ try:
1135
+ import csv
1136
+ from io import StringIO
1137
+
1138
+ users_data = admin_manager.get_all_users(page=1, per_page=10000) # Get all users
1139
+
1140
+ output = StringIO()
1141
+ writer = csv.writer(output)
1142
+
1143
+ # Write header
1144
+ writer.writerow(['ID', 'Email', 'Created At', 'Email Confirmed', 'Last Login', 'Sessions', 'Flashcards', 'Articles'])
1145
+
1146
+ # Write data
1147
+ for user in users_data['users']:
1148
+ writer.writerow([
1149
+ user['id'],
1150
+ user['email'],
1151
+ user['created_at'],
1152
+ user['email_confirmed'],
1153
+ user['last_login'] or 'Never',
1154
+ user['session_count'],
1155
+ user['flashcard_count'],
1156
+ user['article_count']
1157
+ ])
1158
+
1159
+ output.seek(0)
1160
+
1161
+ return Response(
1162
+ output.getvalue(),
1163
+ mimetype='text/csv',
1164
+ headers={'Content-Disposition': f'attachment; filename=users_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'}
1165
+ )
1166
+
1167
+ except Exception as e:
1168
+ print(f"Export users error: {e}")
1169
+ return jsonify({'error': 'Failed to export users'}), 500
1170
+
1171
+ @app.route('/admin/export/tokens', methods=['GET'])
1172
+ @admin_required
1173
+ def export_token_usage():
1174
+ """Export token usage data as CSV"""
1175
+ try:
1176
+ import csv
1177
+ from io import StringIO
1178
+
1179
+ conn = get_db_connection()
1180
+ cursor = conn.cursor()
1181
+
1182
+ cursor.execute("""
1183
+ SELECT t.created_at, u.email, t.api_provider, t.input_tokens,
1184
+ t.output_tokens, t.tokens_used, t.operation_type
1185
+ FROM token_usage t
1186
+ LEFT JOIN users u ON t.user_id = u.id
1187
+ ORDER BY t.created_at DESC
1188
+ """)
1189
+
1190
+ token_data = cursor.fetchall()
1191
+ conn.close()
1192
+
1193
+ output = StringIO()
1194
+ writer = csv.writer(output)
1195
+
1196
+ # Write header
1197
+ writer.writerow(['Date', 'User Email', 'Provider', 'Input Tokens', 'Output Tokens', 'Total Tokens', 'Operation'])
1198
+
1199
+ # Write data
1200
+ for row in token_data:
1201
+ writer.writerow(row)
1202
+
1203
+ output.seek(0)
1204
+
1205
+ return Response(
1206
+ output.getvalue(),
1207
+ mimetype='text/csv',
1208
+ headers={'Content-Disposition': f'attachment; filename=token_usage_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'}
1209
+ )
1210
+
1211
+ except Exception as e:
1212
+ print(f"Export tokens error: {e}")
1213
+ return jsonify({'error': 'Failed to export token usage'}), 500
1214
+
1215
+ @app.route('/admin/system/health', methods=['GET'])
1216
+ @admin_required
1217
+ def get_system_health():
1218
+ """Get system health metrics"""
1219
+ try:
1220
+ health = admin_manager.get_system_health()
1221
+ return jsonify({'success': True, 'health': health})
1222
+ except Exception as e:
1223
+ print(f"System health error: {e}")
1224
+ return jsonify({'error': 'Failed to get system health'}), 500
1225
+
1226
+ @app.route('/admin/system/alerts', methods=['GET'])
1227
+ @admin_required
1228
+ def get_system_alerts():
1229
+ """Get system alerts"""
1230
+ try:
1231
+ alerts = admin_manager.check_system_alerts()
1232
+ return jsonify({'success': True, 'alerts': alerts})
1233
+ except Exception as e:
1234
+ print(f"System alerts error: {e}")
1235
+ return jsonify({'error': 'Failed to get system alerts'}), 500
1236
+
1237
+ @app.route('/admin')
1238
+ def admin_interface():
1239
+ """Serve admin interface"""
1240
+ return send_file('templates/admin.html')
1241
+
1242
+ @app.route('/')
1243
+ def root():
1244
+ return send_file('templates/index.html')
1245
+
1246
+ if __name__ == '__main__':
1247
+ app.run(host='0.0.0.0', port=7860)
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask
2
+ flask-session
3
+ gtts
4
+ google-generativeai
5
+ groq
6
+ requests
7
+ Pillow
8
+ werkzeug
9
+ email-validator
10
+ beautifulsoup4
11
+ feedparser
12
+ matplotlib
13
+ plotly
14
+ pandas
15
+ numpy
16
+ psutil
17
+ gradio
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()