amauricunha commited on
Commit
757abab
·
verified ·
1 Parent(s): 3a8424f

Delete flask_app.py

Browse files
Files changed (1) hide show
  1. flask_app.py +0 -1356
flask_app.py DELETED
@@ -1,1356 +0,0 @@
1
- # flask_app.py - English Helper Flask Application
2
- import os
3
- import io
4
- import json
5
- import base64
6
- from datetime import datetime
7
- from PIL import Image
8
- from email_validator import validate_email, EmailNotValidError
9
-
10
- from flask import Flask, request, jsonify, send_file, session, render_template_string, redirect, url_for, Response
11
- # from flask_session import Session # Removido para usar sessões nativas do Flask
12
- from gtts import gTTS
13
- from groq import Groq
14
- import google.generativeai as genai
15
- from google.generativeai.types import GenerationConfig
16
-
17
- # Import database functions
18
- from database import (
19
- init_db, close_db, create_user, authenticate_user, confirm_email,
20
- get_user_settings, update_user_settings, save_user_flashcard,
21
- get_user_flashcards, record_study_session, login_required,
22
- get_current_user, send_confirmation_email, save_user_article,
23
- get_user_articles, update_user_interests, get_user_interests,
24
- create_study_plan, get_user_study_plans, add_study_activity,
25
- get_study_activities, record_analytics_metric, get_user_analytics,
26
- get_db_connection
27
- )
28
-
29
- # Import content curation and study planner
30
- from content_curator import content_curator
31
- from study_planner import study_planner
32
- from admin_module import admin_manager, admin_required
33
-
34
- # --- CONFIGURAÇÃO INICIAL ---
35
- # Evitar múltiplas instâncias do Flask
36
- if 'app' not in globals():
37
- app = Flask(__name__)
38
-
39
- # Configuration for sessions - Simplificado para HF Spaces
40
- app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-change-in-production-hf-spaces')
41
- app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24 hours
42
-
43
- # Configurações específicas para HF Spaces
44
- app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
45
- app.config['TEMPLATES_AUTO_RELOAD'] = True
46
- app.config['SESSION_COOKIE_SECURE'] = False # HF Spaces pode ter problemas com HTTPS interno
47
- app.config['SESSION_COOKIE_HTTPONLY'] = True
48
- app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Mais permissivo para HF Spaces
49
- app.config['SESSION_COOKIE_NAME'] = 'englishhelper_session'
50
-
51
- # Usar sessões nativas do Flask ao invés de Flask-Session
52
- # Session(app) # Comentado para usar sessões nativas
53
-
54
- # Print session config for debugging
55
- print(f"✅ Flask app inicializado - SECRET_KEY length: {len(app.config['SECRET_KEY'])}")
56
- print(f"✅ Session config - Usando sessões nativas do Flask")
57
- print(f"✅ Working directory: {os.getcwd()}")
58
- else:
59
- print("✅ Flask app já existe - reutilizando instância")
60
-
61
- # Adicionar middleware para debug de sessão
62
- @app.before_request
63
- def debug_session():
64
- if request.endpoint and 'admin' in request.endpoint:
65
- print(f"🔍 Session Debug - Endpoint: {request.endpoint}")
66
- print(f"🔍 Session Data: {dict(session)}")
67
- print(f"🔍 All Cookies: {dict(request.cookies)}")
68
- print(f"🔍 Session ID: {request.cookies.get('englishhelper_session', 'no-session')}")
69
- print(f"🔍 User Agent: {request.headers.get('User-Agent', 'unknown')[:50]}...")
70
-
71
- @app.after_request
72
- def ensure_session_saved(response):
73
- """Garantir que a sessão seja salva"""
74
- try:
75
- if hasattr(session, 'accessed') and session.accessed:
76
- session.permanent = True
77
- except Exception as e:
78
- print(f"Session save error: {e}")
79
- return response
80
-
81
- # Database initialization (handled by app.py)
82
- def initialize_database():
83
- init_db()
84
-
85
- # Note: Database initialization moved to app.py to avoid conflicts
86
-
87
- # Token tracking helper function
88
- def track_token_usage(user_id, provider, input_tokens, output_tokens, operation):
89
- """Helper function to track token usage"""
90
- try:
91
- admin_manager.record_token_usage(user_id, provider, input_tokens, output_tokens, operation)
92
- except Exception as e:
93
- print(f"Token tracking error: {e}")
94
-
95
- @app.teardown_appcontext
96
- def close_database(error):
97
- close_db(error)
98
-
99
- # --- CONFIGURAÇÃO DAS APIS LLM ---
100
- genai_client = None
101
- groq_client = None
102
-
103
- # 1. Configuração Gemini
104
- try:
105
- GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
106
- if GEMINI_API_KEY:
107
- genai.configure(api_key=GEMINI_API_KEY)
108
- genai_client = genai
109
- else:
110
- print("AVISO: GEMINI_API_KEY não configurada.")
111
- except Exception as e:
112
- genai_client = None
113
- print(f"ERRO ao inicializar o cliente Gemini: {e}.")
114
-
115
- # 2. Configuração Groq
116
- try:
117
- GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
118
- if GROQ_API_KEY:
119
- groq_client = Groq(api_key=GROQ_API_KEY)
120
- else:
121
- print("AVISO: GROQ_API_KEY não configurada.")
122
- except Exception as e:
123
- groq_client = None
124
- print(f"ERRO ao inicializar o cliente Groq: {e}.")
125
-
126
-
127
- # --- ROTA PARA LISTAR MODELOS DINAMICAMENTE ---
128
- @app.route('/list-models')
129
- def list_models():
130
- available_models = []
131
- groq_text_models = [
132
- "llama-3.1-8b-instant",
133
- "llama-3.3-70b-versatile",
134
- "openai/gpt-oss-120b",
135
- "openai/gpt-oss-20b"
136
- ]
137
- try:
138
- if genai_client:
139
- for m in genai_client.list_models():
140
- if 'generateContent' in m.supported_generation_methods:
141
- model_name = m.name.replace("models/", "")
142
- if "flash" in model_name or "pro" in model_name:
143
- available_models.append({
144
- "value": f"gemini:{model_name}",
145
- "name": m.display_name
146
- })
147
- if groq_client:
148
- for model_id in groq_text_models:
149
- display_name = model_id.split('/')[-1].replace('-instant', '').replace('-versatile', '')
150
- available_models.append({
151
- "value": f"groq:{model_id}",
152
- "name": f"Groq: {display_name}"
153
- })
154
- except Exception as e:
155
- print(f"Erro ao listar modelos: {e}")
156
- return jsonify([
157
- {"value": "gemini:gemini-2.5-flash-latest", "name": "Gemini 2.5 Flash (Fallback)"},
158
- {"value": "groq:llama-3.1-8b-instant", "name": "Llama 3.1 8B (Fallback)"}
159
- ])
160
- return jsonify(available_models)
161
-
162
-
163
- # --- ROTAS PRINCIPAIS ---
164
-
165
- # Cache de áudio TTS em memória
166
- tts_cache = {}
167
-
168
- @app.route('/tts-proxy', methods=['POST'])
169
- def tts_proxy():
170
- data = request.get_json()
171
- text = data.get('text', '')
172
- tld = data.get('tld', 'co.uk')
173
- if not text: return jsonify({"error": "No text provided"}), 400
174
-
175
- # Validar comprimento do texto (10000 caracteres max)
176
- if len(text) > 10000:
177
- return jsonify({"error": "Text is too long. Maximum 10,000 characters allowed."}), 400
178
-
179
- # Criar chave de cache baseada no texto e TLD
180
- import hashlib
181
- cache_key = hashlib.md5(f"{text}_{tld}".encode()).hexdigest()
182
-
183
- try:
184
- # Verificar se está no cache
185
- if cache_key in tts_cache:
186
- print(f"🎵 TTS Cache HIT: {len(text)} chars")
187
- cached_audio = tts_cache[cache_key]
188
- audio_fp = io.BytesIO(cached_audio)
189
- return send_file(audio_fp, mimetype='audio/mpeg', as_attachment=False)
190
-
191
- # Gerar novo áudio
192
- print(f"🎵 TTS Cache MISS: Gerando áudio para {len(text)} chars, tld: {tld}")
193
-
194
- tts = gTTS(text=text, lang='en', tld=tld)
195
- mp3_fp = io.BytesIO()
196
- tts.write_to_fp(mp3_fp)
197
- mp3_fp.seek(0)
198
-
199
- # Salvar no cache
200
- audio_data = mp3_fp.read()
201
- tts_cache[cache_key] = audio_data
202
-
203
- # Limitar cache a 50 entradas
204
- if len(tts_cache) > 50:
205
- oldest_key = next(iter(tts_cache))
206
- del tts_cache[oldest_key]
207
-
208
- # Retornar áudio
209
- audio_fp = io.BytesIO(audio_data)
210
- return send_file(audio_fp, mimetype='audio/mpeg', as_attachment=False)
211
-
212
- except Exception as e:
213
- print(f"❌ TTS Error: {e}")
214
- return jsonify({"error": f"Failed to generate audio via gTTS: {e}"}), 500
215
-
216
- # Primeira função explain_proxy removida - duplicata
217
-
218
- @app.route('/activity-feedback', methods=['POST'])
219
- def activity_feedback():
220
- data = request.get_json()
221
- model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1)
222
- context_focus = data.get('context_focus', 'General/Social')
223
- original_prompt = data.get('original_prompt', '')
224
- user_response = data.get('user_response', '')
225
-
226
- if not original_prompt or not user_response: return jsonify({"error": "Original prompt and user response are required."}), 400
227
- if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client):
228
- return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503
229
-
230
- system_instruction = (
231
- "You are an expert English teacher providing feedback. "
232
- f"The user's study focus is '{context_focus}'. "
233
- "Your entire response MUST be in English. "
234
- "Provide clear, constructive feedback on the user's writing. "
235
- "Point out grammar, spelling, or style errors. "
236
- "Offer a corrected or improved version of their text. "
237
- "Structure your feedback with markdown for clarity (e.g., using ### Corrected Version)."
238
- )
239
- user_prompt = f"The original task was: \"{original_prompt}\"\n\nHere is the user's response:\n---\n{user_response}\n---\nPlease provide your feedback."
240
- try:
241
- feedback_text = get_ai_text_response(model_provider, model_name, system_instruction, user_prompt)
242
- return jsonify({"feedback": feedback_text})
243
- except Exception as e:
244
- return jsonify({"error": f"AI feedback failed: {e}"}), 500
245
-
246
- @app.route('/analyze-image', methods=['POST'])
247
- def analyze_image():
248
- if not genai_client: return jsonify({"error": "GEMINI_API_KEY not configured."}), 503
249
- data = request.get_json()
250
- base64_image = data.get('image')
251
- model_value = data.get('model', 'gemini:gemini-2.5-flash-latest')
252
-
253
- model_name = 'gemini-2.5-flash-latest' # Default
254
- if model_value.startswith('gemini:'):
255
- model_name = model_value.split(':', 1)[1]
256
-
257
- if not base64_image: return jsonify({"error": "No image data."}), 400
258
- try:
259
- image = Image.open(io.BytesIO(base64.b64decode(base64_image.split(',')[1])))
260
- model = genai_client.GenerativeModel(model_name)
261
- schema = { "type": "object", "properties": { "vocabulary": { "type": "array", "items": { "type": "object", "properties": { "term": {"type": "string"}, "definition": {"type": "string"} }, "required": ["term", "definition"] } } }, "required": ["vocabulary"] }
262
- prompt = [ "Act as an English teacher. Identify 5-7 key objects/concepts in this image. For each, provide its English name and a simple definition. Return a single JSON object conforming to the schema.", image ]
263
-
264
- config = GenerationConfig(response_mime_type="application/json", response_schema=schema)
265
- response = model.generate_content(prompt, generation_config=config)
266
-
267
- return jsonify(json.loads(response.text)['vocabulary'])
268
- except Exception as e:
269
- return jsonify({"error": f"Image analysis failed: {e}"}), 500
270
-
271
- @app.route('/chat-with-ai', methods=['POST'])
272
- def chat_with_ai():
273
- if not groq_client: return jsonify({"error": "GROQ_API_KEY not configured."}), 503
274
- data = request.get_json()
275
- history, user_message = data.get('history', []), data.get('message', '')
276
- if not user_message: return jsonify({"error": "No message."}), 400
277
- try:
278
- system = "You are 'Groq Chat', a friendly English tutor. Keep responses concise (1-2 sentences). If the user makes a grammar mistake, gently correct it. Ask questions to keep the conversation flowing. Always respond in English."
279
- messages = [{"role": "system", "content": system}] + history + [{"role": "user", "content": user_message}]
280
- response = groq_client.chat.completions.create(model="llama-3.1-8b-instant", messages=messages, temperature=0.7)
281
-
282
- # Track token usage
283
- user = get_current_user()
284
- if user and hasattr(response, 'usage'):
285
- track_token_usage(
286
- user['id'],
287
- 'groq',
288
- response.usage.prompt_tokens,
289
- response.usage.completion_tokens,
290
- 'conversation'
291
- )
292
-
293
- return jsonify({"response": response.choices[0].message.content.strip()})
294
- except Exception as e:
295
- return jsonify({"error": f"AI chat failed: {e}"}), 500
296
-
297
- @app.route('/pronunciation-feedback', methods=['POST'])
298
- def pronunciation_feedback():
299
- if not groq_client: return jsonify({"error": "GROQ_API_KEY not configured."}), 503
300
- data = request.get_json()
301
- target_text, user_text = data.get('target_text'), data.get('user_text')
302
- if not target_text or not user_text: return jsonify({"error": "Required data missing."}), 400
303
- try:
304
- system_instruction = "You are an expert American English pronunciation coach. The user tried to say a target sentence, and their speech was transcribed. Based on the likely pronunciation differences, provide brief, friendly, and actionable feedback in Portuguese. Focus on 1-2 key points. If it's very close, praise the user."
305
- user_prompt = f"Target: \"{target_text}\"\nTranscription: \"{user_text}\"\n\nProvide pronunciation feedback."
306
- messages = [{"role": "system", "content": system_instruction}, {"role": "user", "content": user_prompt}]
307
- response = groq_client.chat.completions.create(model="llama-3.1-8b-instant", messages=messages, temperature=0.5)
308
-
309
- # Track token usage
310
- user = get_current_user()
311
- if user and hasattr(response, 'usage'):
312
- track_token_usage(
313
- user['id'],
314
- 'groq',
315
- response.usage.prompt_tokens,
316
- response.usage.completion_tokens,
317
- 'pronunciation_feedback'
318
- )
319
-
320
- return jsonify({"feedback": response.choices[0].message.content.strip()})
321
- except Exception as e:
322
- return jsonify({"error": f"Pronunciation analysis failed: {e}"}), 500
323
-
324
- @app.route('/generate-image', methods=['POST'])
325
- def generate_image():
326
- if not genai_client: return jsonify({"error": "GEMINI_API_KEY not configured."}), 503
327
- data = request.get_json()
328
- prompt = data.get('prompt')
329
- if not prompt: return jsonify({"error": "Image prompt is required."}), 400
330
- try:
331
- model = genai_client.GenerativeModel(model_name='gemini-2.5-flash-image-preview')
332
- response = model.generate_content(prompt)
333
- base64_image_data = response.parts[0].inline_data.data
334
- return jsonify({"image_base64": base64_image_data})
335
- except Exception as e:
336
- return jsonify({"error": f"Image generation failed: {e}"}), 500
337
-
338
- # --- FUNÇÃO AUXILIAR E ROTA RAIZ ---
339
- def get_ai_text_response(provider, model_name, system_instruction, user_prompt, json_schema=None):
340
- if provider == 'gemini':
341
- model = genai_client.GenerativeModel(model_name, system_instruction=system_instruction)
342
-
343
- config = None
344
- if json_schema:
345
- config = GenerationConfig(response_mime_type="application/json", response_schema=json_schema)
346
-
347
- response = model.generate_content(user_prompt, generation_config=config)
348
-
349
- if json_schema:
350
- parsed_json = json.loads(response.text)
351
- required_keys = json_schema.get("required", [])
352
- if not all(key in parsed_json and parsed_json[key] for key in required_keys):
353
- raise ValueError(f"AI response missing required keys or has empty values.")
354
- return parsed_json
355
- else:
356
- return response.text.strip()
357
-
358
- elif provider == 'groq':
359
- final_user_prompt = user_prompt
360
- if json_schema:
361
- final_user_prompt += f"\n\nYou MUST respond with a single JSON object that strictly follows this schema. Do not add any other text before or after the JSON object:\n{json.dumps(json_schema)}"
362
-
363
- messages = [{"role": "system", "content": system_instruction}, {"role": "user", "content": final_user_prompt}]
364
- config = {'response_format': {"type": "json_object"}} if json_schema else {}
365
- response = groq_client.chat.completions.create(model=model_name, messages=messages, **config)
366
-
367
- if json_schema:
368
- parsed_json = json.loads(response.choices[0].message.content)
369
- required_keys = json_schema.get("required", [])
370
- if not all(key in parsed_json and parsed_json[key] for key in required_keys):
371
- raise ValueError(f"AI response missing required keys or has empty values.")
372
- return parsed_json
373
- else:
374
- return response.choices[0].message.content.strip()
375
-
376
- raise Exception(f"Unsupported provider: {provider}")
377
-
378
- # --- AUTHENTICATION ROUTES ---
379
-
380
- @app.route('/register', methods=['POST'])
381
- def register():
382
- """User registration endpoint"""
383
- try:
384
- data = request.get_json()
385
- email = data.get('email', '').strip().lower()
386
- password = data.get('password', '')
387
-
388
- # Validate input
389
- if not email or not password:
390
- return jsonify({'error': 'Email and password are required'}), 400
391
-
392
- if len(password) < 8:
393
- return jsonify({'error': 'Password must be at least 8 characters long'}), 400
394
-
395
- # Validate email format
396
- try:
397
- validate_email(email)
398
- except EmailNotValidError:
399
- return jsonify({'error': 'Invalid email format'}), 400
400
-
401
- # Create user
402
- result = create_user(email, password)
403
-
404
- if result['success']:
405
- # Try to send confirmation email (non-blocking for HF Spaces)
406
- email_sent = False
407
- try:
408
- # Use a timeout to prevent hanging
409
- import threading
410
- import time
411
-
412
- def send_email_async():
413
- nonlocal email_sent
414
- try:
415
- email_sent = send_confirmation_email(email, result['confirmation_token'])
416
- except:
417
- email_sent = False
418
-
419
- # Start email sending in background with timeout
420
- email_thread = threading.Thread(target=send_email_async)
421
- email_thread.daemon = True
422
- email_thread.start()
423
- email_thread.join(timeout=5) # 5 second timeout
424
-
425
- except Exception as e:
426
- print(f"Email sending timeout or error: {e}")
427
- email_sent = False
428
-
429
- # Return success message based on auto-confirmation and email status
430
- auto_confirmed = result.get('auto_confirmed', False)
431
-
432
- if auto_confirmed:
433
- return jsonify({
434
- 'message': 'Registration successful! Your account is ready to use - you can log in immediately.',
435
- 'email_sent': email_sent,
436
- 'auto_confirmed': True,
437
- 'note': 'Email confirmation is disabled in demo mode.'
438
- }), 201
439
- elif email_sent:
440
- return jsonify({
441
- 'message': 'Registration successful! Please check your email to confirm your account.',
442
- 'email_sent': True,
443
- 'auto_confirmed': False
444
- }), 201
445
- else:
446
- return jsonify({
447
- 'message': 'Registration successful! However, we could not send the confirmation email. Please contact support.',
448
- 'email_sent': False,
449
- 'auto_confirmed': False
450
- }), 201
451
- else:
452
- return jsonify({'error': result['message']}), 400
453
-
454
- except Exception as e:
455
- print(f"Registration error: {e}")
456
- return jsonify({'error': 'Internal server error'}), 500
457
-
458
- @app.route('/login', methods=['POST'])
459
- def login():
460
- """User login endpoint"""
461
- try:
462
- data = request.get_json()
463
- email = data.get('email', '').strip().lower()
464
- password = data.get('password', '')
465
-
466
- if not email or not password:
467
- return jsonify({'error': 'Email and password are required'}), 400
468
-
469
- result = authenticate_user(email, password)
470
-
471
- if result['success']:
472
- session['user_id'] = result['user_id']
473
- session['user_email'] = result['email']
474
- session.permanent = True
475
-
476
- # Get user settings
477
- settings = get_user_settings(result['user_id'])
478
-
479
- return jsonify({
480
- 'message': 'Login successful',
481
- 'user': {
482
- 'id': result['user_id'],
483
- 'email': result['email'],
484
- 'settings': settings
485
- }
486
- }), 200
487
- else:
488
- return jsonify({'error': result['message']}), 401
489
-
490
- except Exception as e:
491
- print(f"Login error: {e}")
492
- return jsonify({'error': 'Internal server error'}), 500
493
-
494
- @app.route('/logout', methods=['POST'])
495
- def logout():
496
- """User logout endpoint"""
497
- session.clear()
498
- return jsonify({'message': 'Logout successful'}), 200
499
-
500
- @app.route('/confirm-email')
501
- def confirm_email_route():
502
- """Email confirmation endpoint"""
503
- token = request.args.get('token')
504
-
505
- if not token:
506
- return render_template_string('''
507
- <!DOCTYPE html>
508
- <html><head><title>Invalid Link</title></head>
509
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
510
- <h2>Invalid Confirmation Link</h2>
511
- <p>This confirmation link is invalid or malformed.</p>
512
- <a href="/" style="color: #4f46e5;">Return to English Helper</a>
513
- </body></html>
514
- '''), 400
515
-
516
- result = confirm_email(token)
517
-
518
- if result['success']:
519
- return render_template_string('''
520
- <!DOCTYPE html>
521
- <html><head><title>Email Confirmed</title></head>
522
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
523
- <h2>✅ Email Confirmed!</h2>
524
- <p>Your email has been successfully confirmed. You can now log in to your account.</p>
525
- <a href="/" style="background: #4f46e5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Continue to English Helper</a>
526
- </body></html>
527
- ''')
528
- else:
529
- return render_template_string('''
530
- <!DOCTYPE html>
531
- <html><head><title>Confirmation Failed</title></head>
532
- <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;">
533
- <h2>❌ Confirmation Failed</h2>
534
- <p>This confirmation link is invalid or has expired.</p>
535
- <a href="/" style="color: #4f46e5;">Return to English Helper</a>
536
- </body></html>
537
- '''), 400
538
-
539
- @app.route('/user/profile', methods=['GET'])
540
- @login_required
541
- def get_user_profile():
542
- """Get current user profile"""
543
- user = get_current_user()
544
- if not user:
545
- return jsonify({'error': 'User not found'}), 404
546
-
547
- settings = get_user_settings(user['id'])
548
- flashcards_count = len(get_user_flashcards(user['id'], 1000))
549
-
550
- return jsonify({
551
- 'user': {
552
- 'id': user['id'],
553
- 'email': user['email'],
554
- 'settings': settings,
555
- 'stats': {
556
- 'flashcards_created': flashcards_count
557
- }
558
- }
559
- })
560
-
561
- @app.route('/user/settings', methods=['GET', 'POST'])
562
- @login_required
563
- def user_settings():
564
- """Get or update user settings"""
565
- user = get_current_user()
566
- if not user:
567
- return jsonify({'error': 'User not found'}), 404
568
-
569
- if request.method == 'GET':
570
- settings = get_user_settings(user['id'])
571
- return jsonify({'settings': settings})
572
-
573
- elif request.method == 'POST':
574
- data = request.get_json()
575
- settings = {
576
- 'preferred_model': data.get('preferred_model'),
577
- 'context_focus': data.get('context_focus'),
578
- 'voice_accent': data.get('voice_accent'),
579
- 'daily_goal': data.get('daily_goal', 10),
580
- 'notification_enabled': data.get('notification_enabled', True)
581
- }
582
-
583
- if update_user_settings(user['id'], settings):
584
- return jsonify({'message': 'Settings updated successfully'})
585
- else:
586
- return jsonify({'error': 'Failed to update settings'}), 500
587
-
588
- @app.route('/user/flashcards', methods=['GET', 'POST'])
589
- @login_required
590
- def user_flashcards():
591
- """Get user flashcards or save new flashcard"""
592
- user = get_current_user()
593
- if not user:
594
- return jsonify({'error': 'User not found'}), 404
595
-
596
- if request.method == 'GET':
597
- flashcards = get_user_flashcards(user['id'])
598
- return jsonify({'flashcards': flashcards})
599
-
600
- elif request.method == 'POST':
601
- data = request.get_json()
602
- if save_user_flashcard(user['id'], data):
603
- return jsonify({'message': 'Flashcard saved successfully'})
604
- else:
605
- return jsonify({'error': 'Failed to save flashcard'}), 500
606
-
607
- @app.route('/auth/check', methods=['GET'])
608
- def check_auth():
609
- """Check if user is authenticated"""
610
- user = get_current_user()
611
- if user:
612
- settings = get_user_settings(user['id'])
613
- return jsonify({
614
- 'authenticated': True,
615
- 'user': {
616
- 'id': user['id'],
617
- 'email': user['email'],
618
- 'settings': settings
619
- }
620
- })
621
- else:
622
- return jsonify({'authenticated': False})
623
-
624
- # --- MODIFIED EXISTING ROUTES TO SUPPORT USER DATA ---
625
-
626
- # Override the original explain-proxy to save flashcards for logged-in users
627
- @app.route('/explain-proxy', methods=['POST'])
628
- def explain_proxy():
629
- data = request.get_json()
630
- model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1)
631
- context_focus = data.get('context_focus', 'General/Social')
632
- custom_prompt = data.get('custom_prompt', None)
633
- word = data.get('word', '').strip()
634
- context = data.get('context', '')
635
- for_flashcard = data.get('for_flashcard', False)
636
-
637
- if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client):
638
- return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503
639
-
640
- system_instruction_base = f"You are a professional English tutor. The user's study focus is '{context_focus}'. All your responses must be in ENGLISH."
641
- try:
642
- if custom_prompt:
643
- activity_text = get_ai_text_response(model_provider, model_name, system_instruction_base, custom_prompt)
644
- return jsonify({"explanation": activity_text})
645
-
646
- if not word: return jsonify({"error": "No word selected."}), 400
647
-
648
- if for_flashcard:
649
- schema = {"type": "object", "properties": {"term": {"type": "string"}, "translation": {"type": "string"}, "context_sentence": {"type": "string"}, "gapped_sentence": {"type": "string"}, "definition": {"type": "string"}}, "required": ["term", "translation", "context_sentence", "gapped_sentence", "definition"]}
650
- prompt = f"Analyze '{word}' in context: '{context}'. Generate a JSON for a flashcard. The 'gapped_sentence' must replace '{word}' with '______________'."
651
- flashcard_data = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt, json_schema=schema)
652
-
653
- # Save flashcard for logged-in users
654
- user = get_current_user()
655
- if user:
656
- save_user_flashcard(user['id'], flashcard_data)
657
-
658
- return jsonify(flashcard_data)
659
- else:
660
- prompt = f"Analyze '{word}' in context: '{context}'. Provide a one-sentence English explanation, then '---', then the Portuguese translation."
661
- parts = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt).split('---', 1)
662
- return jsonify({"explanation": parts[0].strip(), "translation": parts[1].strip() if len(parts) > 1 else 'N/A'})
663
- except Exception as e:
664
- print(f"AI ANALYSIS ERROR in /explain-proxy: {e}")
665
- return jsonify({"error": f"AI analysis failed: {e}"}), 500
666
-
667
- # --- CONTENT CURATION ROUTES ---
668
-
669
- @app.route('/content/search', methods=['POST'])
670
- @login_required
671
- def search_content():
672
- """Search for content based on user interests"""
673
- try:
674
- user = get_current_user()
675
- if not user:
676
- return jsonify({'error': 'User not found'}), 404
677
-
678
- data = request.get_json()
679
- query = data.get('query', '')
680
- category = data.get('category', '')
681
-
682
- # Get user settings and interests
683
- settings = get_user_settings(user['id'])
684
- interests = get_user_interests(user['id'])
685
-
686
- if not interests and query:
687
- # Use query as interest if no interests set
688
- interests = {query: 1.0}
689
-
690
- english_level = settings.get('english_level', 'B1') if settings else 'B1'
691
- context_focus = settings.get('context_focus', 'General/Social') if settings else 'General/Social'
692
-
693
- # Search for content
694
- results = content_curator.search_content(
695
- interests=list(interests.keys()) if interests else [query],
696
- english_level=english_level,
697
- context_focus=context_focus,
698
- limit=10
699
- )
700
-
701
- return jsonify({'results': results})
702
-
703
- except Exception as e:
704
- print(f"Content search error: {e}")
705
- return jsonify({'error': 'Content search failed'}), 500
706
-
707
- @app.route('/content/extract', methods=['POST'])
708
- @login_required
709
- def extract_content():
710
- """Extract content from URL"""
711
- try:
712
- data = request.get_json()
713
- url = data.get('url', '')
714
-
715
- if not url:
716
- return jsonify({'error': 'URL required'}), 400
717
-
718
- result = content_curator.extract_content_from_url(url)
719
- return jsonify(result)
720
-
721
- except Exception as e:
722
- print(f"Content extraction error: {e}")
723
- return jsonify({'error': 'Content extraction failed'}), 500
724
-
725
- @app.route('/content/save', methods=['POST'])
726
- @login_required
727
- def save_content():
728
- """Save content/article for user"""
729
- try:
730
- user = get_current_user()
731
- if not user:
732
- return jsonify({'error': 'User not found'}), 404
733
-
734
- data = request.get_json()
735
- title = data.get('title', '')
736
- content = data.get('content', '')
737
- source_url = data.get('source_url')
738
- source_type = data.get('source_type', 'manual')
739
- category = data.get('category')
740
-
741
- if not title or not content:
742
- return jsonify({'error': 'Title and content required'}), 400
743
-
744
- result = save_user_article(user['id'], title, content, source_url, source_type, category)
745
-
746
- if result['success']:
747
- # Record analytics
748
- record_analytics_metric(user['id'], 'content_saved', 1)
749
- return jsonify({'message': 'Content saved successfully', 'article_id': result['article_id']})
750
- else:
751
- return jsonify({'error': result['message']}), 500
752
-
753
- except Exception as e:
754
- print(f"Save content error: {e}")
755
- return jsonify({'error': 'Failed to save content'}), 500
756
-
757
- @app.route('/content/articles', methods=['GET'])
758
- @login_required
759
- def get_articles():
760
- """Get user's saved articles"""
761
- try:
762
- user = get_current_user()
763
- if not user:
764
- return jsonify({'error': 'User not found'}), 404
765
-
766
- category = request.args.get('category')
767
- limit = int(request.args.get('limit', 50))
768
-
769
- articles = get_user_articles(user['id'], category, limit)
770
- return jsonify({'articles': articles})
771
-
772
- except Exception as e:
773
- print(f"Get articles error: {e}")
774
- return jsonify({'error': 'Failed to get articles'}), 500
775
-
776
- @app.route('/content/interests', methods=['GET', 'POST'])
777
- @login_required
778
- def manage_interests():
779
- """Get or update user interests"""
780
- try:
781
- user = get_current_user()
782
- if not user:
783
- return jsonify({'error': 'User not found'}), 404
784
-
785
- if request.method == 'GET':
786
- interests = get_user_interests(user['id'])
787
- return jsonify({'interests': interests})
788
-
789
- elif request.method == 'POST':
790
- data = request.get_json()
791
- interests = data.get('interests', {})
792
-
793
- if update_user_interests(user['id'], interests):
794
- return jsonify({'message': 'Interests updated successfully'})
795
- else:
796
- return jsonify({'error': 'Failed to update interests'}), 500
797
-
798
- except Exception as e:
799
- print(f"Manage interests error: {e}")
800
- return jsonify({'error': 'Failed to manage interests'}), 500
801
-
802
- @app.route('/content/recommendations', methods=['GET'])
803
- @login_required
804
- def get_recommendations():
805
- """Get AI-powered content recommendations"""
806
- try:
807
- user = get_current_user()
808
- if not user:
809
- return jsonify({'error': 'User not found'}), 404
810
-
811
- # Get user data
812
- interests = get_user_interests(user['id'])
813
- recent_articles = get_user_articles(user['id'], limit=10)
814
- settings = get_user_settings(user['id'])
815
-
816
- english_level = settings.get('english_level', 'B1') if settings else 'B1'
817
- context_focus = settings.get('context_focus', 'General/Social') if settings else 'General/Social'
818
-
819
- # Generate recommendations
820
- recommendations = content_curator.generate_personalized_recommendations(
821
- interests, recent_articles, english_level, context_focus, user['id']
822
- )
823
-
824
- return jsonify({'recommendations': recommendations})
825
-
826
- except Exception as e:
827
- print(f"Recommendations error: {e}")
828
- return jsonify({'error': 'Failed to get recommendations'}), 500
829
-
830
- @app.route('/content/analyze', methods=['POST'])
831
- @login_required
832
- def analyze_content():
833
- """Analyze content for learning insights"""
834
- try:
835
- user = get_current_user()
836
- if not user:
837
- return jsonify({'error': 'User not found'}), 404
838
-
839
- data = request.get_json()
840
- content = data.get('content', '')
841
-
842
- if not content:
843
- return jsonify({'error': 'Content required'}), 400
844
-
845
- settings = get_user_settings(user['id'])
846
- english_level = settings.get('english_level', 'B1') if settings else 'B1'
847
-
848
- analysis = content_curator.analyze_content_for_learning(content, english_level)
849
-
850
- return jsonify({'analysis': analysis})
851
-
852
- except Exception as e:
853
- print(f"Content analysis error: {e}")
854
- return jsonify({'error': 'Content analysis failed'}), 500
855
-
856
- # --- STUDY PLANNING ROUTES ---
857
-
858
- @app.route('/study/plans', methods=['GET', 'POST'])
859
- @login_required
860
- def manage_study_plans():
861
- """Get or create study plans"""
862
- try:
863
- user = get_current_user()
864
- if not user:
865
- return jsonify({'error': 'User not found'}), 404
866
-
867
- if request.method == 'GET':
868
- plans = get_user_study_plans(user['id'])
869
- return jsonify({'plans': plans})
870
-
871
- elif request.method == 'POST':
872
- data = request.get_json()
873
- plan_name = data.get('plan_name', '')
874
- target_level = data.get('target_level', 'B2')
875
- current_level = data.get('current_level', 'B1')
876
- objectives = json.dumps(data.get('objectives', []))
877
- weekly_hours = data.get('weekly_hours', 5)
878
-
879
- if not plan_name:
880
- return jsonify({'error': 'Plan name required'}), 400
881
-
882
- result = create_study_plan(user['id'], plan_name, target_level, current_level, objectives, weekly_hours)
883
-
884
- if result['success']:
885
- return jsonify({'message': 'Study plan created', 'plan_id': result['plan_id']})
886
- else:
887
- return jsonify({'error': result['message']}), 500
888
-
889
- except Exception as e:
890
- print(f"Study plans error: {e}")
891
- return jsonify({'error': 'Failed to manage study plans'}), 500
892
-
893
- @app.route('/analytics/dashboard', methods=['GET'])
894
- @login_required
895
- def analytics_dashboard():
896
- """Get analytics dashboard data"""
897
- try:
898
- user = get_current_user()
899
- if not user:
900
- return jsonify({'error': 'User not found'}), 404
901
-
902
- days = int(request.args.get('days', 30))
903
-
904
- # Get various analytics
905
- analytics_data = {
906
- 'flashcards_created': get_user_analytics(user['id'], 'flashcards_created', days),
907
- 'content_saved': get_user_analytics(user['id'], 'content_saved', days),
908
- 'study_sessions': get_user_analytics(user['id'], 'study_session', days),
909
- 'total_flashcards': len(get_user_flashcards(user['id'], 1000)),
910
- 'total_articles': len(get_user_articles(user['id'], limit=1000)),
911
- 'user_level': get_user_settings(user['id']).get('english_level', 'B1')
912
- }
913
-
914
- return jsonify({'analytics': analytics_data})
915
-
916
- except Exception as e:
917
- print(f"Analytics error: {e}")
918
- return jsonify({'error': 'Failed to get analytics'}), 500
919
-
920
- # --- STUDY PLANNER ROUTES ---
921
-
922
- @app.route('/study-plan/create', methods=['POST'])
923
- @login_required
924
- def create_study_plan_route():
925
- """Create a personalized study plan"""
926
- try:
927
- user = get_current_user()
928
- if not user:
929
- return jsonify({'error': 'User not found'}), 404
930
-
931
- data = request.get_json()
932
-
933
- # Get user settings and interests
934
- user_settings = get_user_settings(user['id'])
935
- user_interests = get_user_interests(user['id'])
936
-
937
- # Prepare data for study planner
938
- planner_data = {
939
- 'english_level': data.get('current_level') or user_settings.get('english_level', 'B1'),
940
- 'target_level': data.get('target_level', 'B2'),
941
- 'weekly_hours': int(data.get('weekly_hours', 5)),
942
- 'context_focus': data.get('context_focus') or user_settings.get('context_focus', 'General/Social'),
943
- 'interests': user_interests,
944
- 'study_goals': data.get('study_goals', [])
945
- }
946
-
947
- # Generate the plan
948
- result = study_planner.generate_personalized_plan(planner_data)
949
-
950
- if result['success']:
951
- plan = result['plan']
952
-
953
- # Save to database
954
- plan_id = create_study_plan(
955
- user['id'],
956
- plan['target_level'],
957
- plan['weekly_hours'],
958
- plan['estimated_weeks'],
959
- json.dumps(plan)
960
- )
961
-
962
- plan['id'] = plan_id
963
-
964
- # Record analytics
965
- record_analytics_metric(user['id'], 'study_plan_created', 1)
966
-
967
- return jsonify({'success': True, 'plan': plan})
968
- else:
969
- return jsonify({'success': False, 'error': result['error']}), 500
970
-
971
- except Exception as e:
972
- print(f"Study plan creation error: {e}")
973
- return jsonify({'error': 'Failed to create study plan'}), 500
974
-
975
- @app.route('/study-plan/current', methods=['GET'])
976
- @login_required
977
- def get_current_study_plan():
978
- """Get user's current study plan"""
979
- try:
980
- user = get_current_user()
981
- if not user:
982
- return jsonify({'error': 'User not found'}), 404
983
-
984
- plans = get_user_study_plans(user['id'])
985
-
986
- if plans:
987
- # Get the most recent active plan
988
- current_plan = plans[0] # Assuming most recent first
989
-
990
- # Parse the plan data
991
- plan_data = json.loads(current_plan['plan_data'])
992
-
993
- # Add database ID
994
- plan_data['db_id'] = current_plan['id']
995
-
996
- # Get activities for this plan
997
- activities = get_study_activities(current_plan['id'])
998
- plan_data['completed_activities'] = activities
999
-
1000
- return jsonify({'success': True, 'plan': plan_data})
1001
- else:
1002
- return jsonify({'success': True, 'plan': None})
1003
-
1004
- except Exception as e:
1005
- print(f"Get study plan error: {e}")
1006
- return jsonify({'error': 'Failed to get study plan'}), 500
1007
-
1008
- @app.route('/study-plan/activity/complete', methods=['POST'])
1009
- @login_required
1010
- def complete_study_activity():
1011
- """Mark a study activity as completed"""
1012
- try:
1013
- user = get_current_user()
1014
- if not user:
1015
- return jsonify({'error': 'User not found'}), 404
1016
-
1017
- data = request.get_json()
1018
- plan_id = data.get('plan_id')
1019
- activity_id = data.get('activity_id')
1020
- duration_minutes = data.get('duration_minutes', 0)
1021
- notes = data.get('notes', '')
1022
-
1023
- if not plan_id or not activity_id:
1024
- return jsonify({'error': 'Missing plan_id or activity_id'}), 400
1025
-
1026
- # Add activity completion
1027
- add_study_activity(plan_id, activity_id, duration_minutes, notes)
1028
-
1029
- # Record analytics
1030
- record_analytics_metric(user['id'], 'study_activity_completed', 1)
1031
- record_analytics_metric(user['id'], 'study_time_minutes', duration_minutes)
1032
-
1033
- return jsonify({'success': True})
1034
-
1035
- except Exception as e:
1036
- print(f"Complete activity error: {e}")
1037
- return jsonify({'error': 'Failed to complete activity'}), 500
1038
-
1039
- @app.route('/study-plan/progress', methods=['GET'])
1040
- @login_required
1041
- def get_study_progress():
1042
- """Get study plan progress analytics"""
1043
- try:
1044
- user = get_current_user()
1045
- if not user:
1046
- return jsonify({'error': 'User not found'}), 404
1047
-
1048
- plans = get_user_study_plans(user['id'])
1049
-
1050
- if not plans:
1051
- return jsonify({'success': True, 'progress': None})
1052
-
1053
- current_plan = plans[0]
1054
- plan_data = json.loads(current_plan['plan_data'])
1055
- activities = get_study_activities(current_plan['id'])
1056
-
1057
- # Calculate progress
1058
- total_activities = len(plan_data.get('activities', []))
1059
- completed_activities = len(activities)
1060
-
1061
- progress_data = {
1062
- 'total_activities': total_activities,
1063
- 'completed_activities': completed_activities,
1064
- 'completion_percentage': (completed_activities / max(total_activities, 1)) * 100,
1065
- 'estimated_weeks': plan_data.get('estimated_weeks', 0),
1066
- 'weeks_elapsed': max(1, (datetime.now() - datetime.fromisoformat(current_plan['created_at'])).days // 7),
1067
- 'target_level': plan_data.get('target_level', 'B2'),
1068
- 'weekly_hours': plan_data.get('weekly_hours', 5),
1069
- 'recent_activities': activities[-10:] if activities else [] # Last 10 activities
1070
- }
1071
-
1072
- return jsonify({'success': True, 'progress': progress_data})
1073
-
1074
- except Exception as e:
1075
- print(f"Study progress error: {e}")
1076
- return jsonify({'error': 'Failed to get study progress'}), 500
1077
-
1078
- # --- ADMIN ROUTES ---
1079
-
1080
- @app.route('/admin/login', methods=['POST'])
1081
- def admin_login():
1082
- """Admin login endpoint"""
1083
- try:
1084
- data = request.get_json()
1085
- username = data.get('username')
1086
- password = data.get('password')
1087
-
1088
- print(f"Admin login attempt - Username: {username}")
1089
-
1090
- if admin_manager.login_admin(username, password):
1091
- print(f"Admin login successful - Session ID: {session.get('_id', 'no-id')}")
1092
- print(f"Session data after login: {dict(session)}")
1093
- return jsonify({'success': True, 'message': 'Admin logged in successfully'})
1094
- else:
1095
- print(f"Admin login failed - Invalid credentials for: {username}")
1096
- return jsonify({'success': False, 'error': 'Invalid credentials'}), 401
1097
-
1098
- except Exception as e:
1099
- print(f"Admin login error: {e}")
1100
- return jsonify({'error': 'Admin login failed'}), 500
1101
-
1102
- @app.route('/admin/logout', methods=['POST'])
1103
- @admin_required
1104
- def admin_logout():
1105
- """Admin logout endpoint"""
1106
- try:
1107
- admin_manager.logout_admin()
1108
- return jsonify({'success': True, 'message': 'Admin logged out successfully'})
1109
- except Exception as e:
1110
- print(f"Admin logout error: {e}")
1111
- return jsonify({'error': 'Admin logout failed'}), 500
1112
-
1113
- @app.route('/admin/check', methods=['GET'])
1114
- def admin_check():
1115
- """Check admin authentication status"""
1116
- try:
1117
- is_authenticated = admin_manager.is_admin_logged_in()
1118
- username = session.get('admin_username')
1119
-
1120
- print(f"Admin check - Authenticated: {is_authenticated}, Username: {username}")
1121
- print(f"Current session data: {dict(session)}")
1122
- print(f"Session ID: {session.get('_id', 'no-session-id')}")
1123
-
1124
- return jsonify({
1125
- 'authenticated': is_authenticated,
1126
- 'username': username if is_authenticated else None,
1127
- 'session_id': session.get('_id', 'no-session-id'),
1128
- 'debug_session_keys': list(session.keys())
1129
- })
1130
- except Exception as e:
1131
- print(f"Admin check error: {e}")
1132
- return jsonify({'authenticated': False})
1133
-
1134
- # Debug route to test sessions
1135
- @app.route('/admin/debug-session', methods=['GET', 'POST'])
1136
- def debug_session():
1137
- """Debug session functionality"""
1138
- if request.method == 'POST':
1139
- session['debug_test'] = 'session_working'
1140
- session.permanent = True
1141
- return jsonify({
1142
- 'message': 'Session test value set',
1143
- 'session_data': dict(session)
1144
- })
1145
- else:
1146
- test_value = session.get('debug_test', 'not_found')
1147
- return jsonify({
1148
- 'test_value': test_value,
1149
- 'session_data': dict(session),
1150
- 'session_id': session.get('_id', 'no-session-id')
1151
- })
1152
-
1153
- @app.route('/admin/dashboard', methods=['GET'])
1154
- @admin_required
1155
- def admin_dashboard():
1156
- """Get admin dashboard data"""
1157
- try:
1158
- stats = admin_manager.get_system_stats()
1159
- return jsonify({'success': True, 'stats': stats})
1160
- except Exception as e:
1161
- print(f"Admin dashboard error: {e}")
1162
- return jsonify({'error': 'Failed to load dashboard'}), 500
1163
-
1164
- @app.route('/admin/users', methods=['GET'])
1165
- @admin_required
1166
- def admin_get_users():
1167
- """Get paginated list of users"""
1168
- try:
1169
- page = int(request.args.get('page', 1))
1170
- per_page = int(request.args.get('per_page', 20))
1171
-
1172
- users_data = admin_manager.get_all_users(page, per_page)
1173
- return jsonify({'success': True, 'data': users_data})
1174
- except Exception as e:
1175
- print(f"Admin get users error: {e}")
1176
- return jsonify({'error': 'Failed to get users'}), 500
1177
-
1178
- @app.route('/admin/users/<int:user_id>', methods=['GET'])
1179
- @admin_required
1180
- def admin_get_user_details(user_id):
1181
- """Get detailed information about a user"""
1182
- try:
1183
- user_details = admin_manager.get_user_details(user_id)
1184
- if user_details:
1185
- return jsonify({'success': True, 'user': user_details})
1186
- else:
1187
- return jsonify({'error': 'User not found'}), 404
1188
- except Exception as e:
1189
- print(f"Admin get user details error: {e}")
1190
- return jsonify({'error': 'Failed to get user details'}), 500
1191
-
1192
- @app.route('/admin/users/<int:user_id>', methods=['DELETE'])
1193
- @admin_required
1194
- def admin_delete_user(user_id):
1195
- """Delete a user and all associated data"""
1196
- try:
1197
- if admin_manager.delete_user(user_id):
1198
- return jsonify({'success': True, 'message': 'User deleted successfully'})
1199
- else:
1200
- return jsonify({'error': 'Failed to delete user'}), 500
1201
- except Exception as e:
1202
- print(f"Admin delete user error: {e}")
1203
- return jsonify({'error': 'Failed to delete user'}), 500
1204
-
1205
- @app.route('/admin/database/schema', methods=['GET'])
1206
- @admin_required
1207
- def admin_get_database_schema():
1208
- """Get database schema information"""
1209
- try:
1210
- schema = admin_manager.get_database_schema()
1211
- return jsonify({'success': True, 'schema': schema})
1212
- except Exception as e:
1213
- print(f"Admin get schema error: {e}")
1214
- return jsonify({'error': 'Failed to get database schema'}), 500
1215
-
1216
- @app.route('/admin/token-usage', methods=['POST'])
1217
- def record_token_usage():
1218
- """Record token usage (called by AI functions)"""
1219
- try:
1220
- data = request.get_json()
1221
- user_id = data.get('user_id')
1222
- api_provider = data.get('api_provider')
1223
- input_tokens = data.get('input_tokens', 0)
1224
- output_tokens = data.get('output_tokens', 0)
1225
- operation_type = data.get('operation_type', 'unknown')
1226
-
1227
- admin_manager.record_token_usage(
1228
- user_id, api_provider, input_tokens, output_tokens, operation_type
1229
- )
1230
-
1231
- return jsonify({'success': True})
1232
- except Exception as e:
1233
- print(f"Token usage recording error: {e}")
1234
- return jsonify({'error': 'Failed to record token usage'}), 500
1235
-
1236
- @app.route('/admin/export/users', methods=['GET'])
1237
- @admin_required
1238
- def export_users():
1239
- """Export users data as CSV"""
1240
- try:
1241
- import csv
1242
- from io import StringIO
1243
-
1244
- users_data = admin_manager.get_all_users(page=1, per_page=10000) # Get all users
1245
-
1246
- output = StringIO()
1247
- writer = csv.writer(output)
1248
-
1249
- # Write header
1250
- writer.writerow(['ID', 'Email', 'Created At', 'Email Confirmed', 'Last Login', 'Sessions', 'Flashcards', 'Articles'])
1251
-
1252
- # Write data
1253
- for user in users_data['users']:
1254
- writer.writerow([
1255
- user['id'],
1256
- user['email'],
1257
- user['created_at'],
1258
- user['email_confirmed'],
1259
- user['last_login'] or 'Never',
1260
- user['session_count'],
1261
- user['flashcard_count'],
1262
- user['article_count']
1263
- ])
1264
-
1265
- output.seek(0)
1266
-
1267
- return Response(
1268
- output.getvalue(),
1269
- mimetype='text/csv',
1270
- headers={'Content-Disposition': f'attachment; filename=users_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'}
1271
- )
1272
-
1273
- except Exception as e:
1274
- print(f"Export users error: {e}")
1275
- return jsonify({'error': 'Failed to export users'}), 500
1276
-
1277
- @app.route('/admin/export/tokens', methods=['GET'])
1278
- @admin_required
1279
- def export_token_usage():
1280
- """Export token usage data as CSV"""
1281
- try:
1282
- import csv
1283
- from io import StringIO
1284
-
1285
- conn = get_db_connection()
1286
- cursor = conn.cursor()
1287
-
1288
- cursor.execute("""
1289
- SELECT t.created_at, u.email, t.api_provider, t.input_tokens,
1290
- t.output_tokens, t.tokens_used, t.operation_type
1291
- FROM token_usage t
1292
- LEFT JOIN users u ON t.user_id = u.id
1293
- ORDER BY t.created_at DESC
1294
- """)
1295
-
1296
- token_data = cursor.fetchall()
1297
- conn.close()
1298
-
1299
- output = StringIO()
1300
- writer = csv.writer(output)
1301
-
1302
- # Write header
1303
- writer.writerow(['Date', 'User Email', 'Provider', 'Input Tokens', 'Output Tokens', 'Total Tokens', 'Operation'])
1304
-
1305
- # Write data
1306
- for row in token_data:
1307
- writer.writerow(row)
1308
-
1309
- output.seek(0)
1310
-
1311
- return Response(
1312
- output.getvalue(),
1313
- mimetype='text/csv',
1314
- headers={'Content-Disposition': f'attachment; filename=token_usage_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'}
1315
- )
1316
-
1317
- except Exception as e:
1318
- print(f"Export tokens error: {e}")
1319
- return jsonify({'error': 'Failed to export token usage'}), 500
1320
-
1321
- @app.route('/admin/system/health', methods=['GET'])
1322
- @admin_required
1323
- def get_system_health():
1324
- """Get system health metrics"""
1325
- try:
1326
- health = admin_manager.get_system_health()
1327
- return jsonify({'success': True, 'health': health})
1328
- except Exception as e:
1329
- print(f"System health error: {e}")
1330
- return jsonify({'error': 'Failed to get system health'}), 500
1331
-
1332
- @app.route('/admin/system/alerts', methods=['GET'])
1333
- @admin_required
1334
- def get_system_alerts():
1335
- """Get system alerts"""
1336
- try:
1337
- alerts = admin_manager.check_system_alerts()
1338
- return jsonify({'success': True, 'alerts': alerts})
1339
- except Exception as e:
1340
- print(f"System alerts error: {e}")
1341
- return jsonify({'error': 'Failed to get system alerts'}), 500
1342
-
1343
- @app.route('/admin')
1344
- def admin_interface():
1345
- """Serve admin interface"""
1346
- return send_file('templates/admin.html')
1347
-
1348
- @app.route('/')
1349
- def root():
1350
- return send_file('templates/index.html')
1351
-
1352
- # Evitar execução automática quando importado
1353
- if __name__ == '__main__':
1354
- print("⚠️ flask_app.py executado diretamente")
1355
- print("💡 Use app.py para HF Spaces ou execute como módulo")
1356
- app.run(host='0.0.0.0', port=5000, debug=True)