Spaces:
Sleeping
Sleeping
| # flask_app.py - English Helper Flask Application | |
| import os | |
| import io | |
| import json | |
| import base64 | |
| from datetime import datetime | |
| from PIL import Image | |
| from email_validator import validate_email, EmailNotValidError | |
| from flask import Flask, request, jsonify, send_file, session, render_template_string, redirect, url_for, Response | |
| # from flask_session import Session # Removido para usar sessões nativas do Flask | |
| from gtts import gTTS | |
| from groq import Groq | |
| import google.generativeai as genai | |
| from google.generativeai.types import GenerationConfig | |
| # Import database functions | |
| from database import ( | |
| init_db, close_db, create_user, authenticate_user, confirm_email, | |
| get_user_settings, update_user_settings, save_user_flashcard, | |
| get_user_flashcards, record_study_session, login_required, | |
| get_current_user, send_confirmation_email, save_user_article, | |
| get_user_articles, update_user_interests, get_user_interests, | |
| create_study_plan, get_user_study_plans, add_study_activity, | |
| get_study_activities, record_analytics_metric, get_user_analytics, | |
| get_db_connection | |
| ) | |
| # Import content curation and study planner | |
| from content_curator import content_curator | |
| from study_planner import study_planner | |
| from admin_module import admin_manager, admin_required | |
| # --- CONFIGURAÇÃO INICIAL --- | |
| # Evitar múltiplas instâncias do Flask | |
| if 'app' not in globals(): | |
| app = Flask(__name__) | |
| # Configuration for sessions - Simplificado para HF Spaces | |
| app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-change-in-production-hf-spaces') | |
| app.config['PERMANENT_SESSION_LIFETIME'] = 86400 # 24 hours | |
| # Configurações específicas para HF Spaces | |
| app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 | |
| app.config['TEMPLATES_AUTO_RELOAD'] = True | |
| app.config['SESSION_COOKIE_SECURE'] = False # HF Spaces pode ter problemas com HTTPS interno | |
| app.config['SESSION_COOKIE_HTTPONLY'] = True | |
| app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # Mais permissivo para HF Spaces | |
| app.config['SESSION_COOKIE_NAME'] = 'englishhelper_session' | |
| # Usar sessões nativas do Flask ao invés de Flask-Session | |
| # Session(app) # Comentado para usar sessões nativas | |
| # Print session config for debugging | |
| print(f"✅ Flask app inicializado - SECRET_KEY length: {len(app.config['SECRET_KEY'])}") | |
| print(f"✅ Session config - Usando sessões nativas do Flask") | |
| print(f"✅ Working directory: {os.getcwd()}") | |
| else: | |
| print("✅ Flask app já existe - reutilizando instância") | |
| # Adicionar middleware para debug de sessão | |
| def debug_session(): | |
| if request.endpoint and 'admin' in request.endpoint: | |
| print(f"🔍 Session Debug - Endpoint: {request.endpoint}") | |
| print(f"🔍 Session Data: {dict(session)}") | |
| print(f"🔍 All Cookies: {dict(request.cookies)}") | |
| print(f"🔍 Session ID: {request.cookies.get('englishhelper_session', 'no-session')}") | |
| print(f"🔍 User Agent: {request.headers.get('User-Agent', 'unknown')[:50]}...") | |
| def ensure_session_saved(response): | |
| """Garantir que a sessão seja salva""" | |
| try: | |
| if hasattr(session, 'accessed') and session.accessed: | |
| session.permanent = True | |
| except Exception as e: | |
| print(f"Session save error: {e}") | |
| return response | |
| # Database initialization (handled by app.py) | |
| def initialize_database(): | |
| init_db() | |
| # Note: Database initialization moved to app.py to avoid conflicts | |
| # Token tracking helper function | |
| def track_token_usage(user_id, provider, input_tokens, output_tokens, operation): | |
| """Helper function to track token usage""" | |
| try: | |
| admin_manager.record_token_usage(user_id, provider, input_tokens, output_tokens, operation) | |
| except Exception as e: | |
| print(f"Token tracking error: {e}") | |
| def close_database(error): | |
| close_db(error) | |
| # --- CONFIGURAÇÃO DAS APIS LLM --- | |
| genai_client = None | |
| groq_client = None | |
| # 1. Configuração Gemini | |
| try: | |
| GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") | |
| if GEMINI_API_KEY: | |
| genai.configure(api_key=GEMINI_API_KEY) | |
| genai_client = genai | |
| else: | |
| print("AVISO: GEMINI_API_KEY não configurada.") | |
| except Exception as e: | |
| genai_client = None | |
| print(f"ERRO ao inicializar o cliente Gemini: {e}.") | |
| # 2. Configuração Groq | |
| try: | |
| GROQ_API_KEY = os.environ.get("GROQ_API_KEY") | |
| if GROQ_API_KEY: | |
| groq_client = Groq(api_key=GROQ_API_KEY) | |
| else: | |
| print("AVISO: GROQ_API_KEY não configurada.") | |
| except Exception as e: | |
| groq_client = None | |
| print(f"ERRO ao inicializar o cliente Groq: {e}.") | |
| # --- ROTA PARA LISTAR MODELOS DINAMICAMENTE --- | |
| def list_models(): | |
| available_models = [] | |
| groq_text_models = [ | |
| "llama-3.1-8b-instant", | |
| "llama-3.3-70b-versatile", | |
| "openai/gpt-oss-120b", | |
| "openai/gpt-oss-20b" | |
| ] | |
| try: | |
| if genai_client: | |
| for m in genai_client.list_models(): | |
| if 'generateContent' in m.supported_generation_methods: | |
| model_name = m.name.replace("models/", "") | |
| if "flash" in model_name or "pro" in model_name: | |
| available_models.append({ | |
| "value": f"gemini:{model_name}", | |
| "name": m.display_name | |
| }) | |
| if groq_client: | |
| for model_id in groq_text_models: | |
| display_name = model_id.split('/')[-1].replace('-instant', '').replace('-versatile', '') | |
| available_models.append({ | |
| "value": f"groq:{model_id}", | |
| "name": f"Groq: {display_name}" | |
| }) | |
| except Exception as e: | |
| print(f"Erro ao listar modelos: {e}") | |
| return jsonify([ | |
| {"value": "gemini:gemini-2.5-flash-latest", "name": "Gemini 2.5 Flash (Fallback)"}, | |
| {"value": "groq:llama-3.1-8b-instant", "name": "Llama 3.1 8B (Fallback)"} | |
| ]) | |
| return jsonify(available_models) | |
| # --- ROTAS PRINCIPAIS --- | |
| # Cache de áudio TTS em memória | |
| tts_cache = {} | |
| def tts_proxy(): | |
| data = request.get_json() | |
| text = data.get('text', '') | |
| tld = data.get('tld', 'co.uk') | |
| if not text: return jsonify({"error": "No text provided"}), 400 | |
| # Validar comprimento do texto (10000 caracteres max) | |
| if len(text) > 10000: | |
| return jsonify({"error": "Text is too long. Maximum 10,000 characters allowed."}), 400 | |
| # Criar chave de cache baseada no texto e TLD | |
| import hashlib | |
| cache_key = hashlib.md5(f"{text}_{tld}".encode()).hexdigest() | |
| try: | |
| # Verificar se está no cache | |
| if cache_key in tts_cache: | |
| print(f"🎵 TTS Cache HIT: {len(text)} chars") | |
| cached_audio = tts_cache[cache_key] | |
| audio_fp = io.BytesIO(cached_audio) | |
| return send_file(audio_fp, mimetype='audio/mpeg', as_attachment=False) | |
| # Gerar novo áudio | |
| print(f"🎵 TTS Cache MISS: Gerando áudio para {len(text)} chars, tld: {tld}") | |
| tts = gTTS(text=text, lang='en', tld=tld) | |
| mp3_fp = io.BytesIO() | |
| tts.write_to_fp(mp3_fp) | |
| mp3_fp.seek(0) | |
| # Salvar no cache | |
| audio_data = mp3_fp.read() | |
| tts_cache[cache_key] = audio_data | |
| # Limitar cache a 50 entradas | |
| if len(tts_cache) > 50: | |
| oldest_key = next(iter(tts_cache)) | |
| del tts_cache[oldest_key] | |
| # Retornar áudio | |
| audio_fp = io.BytesIO(audio_data) | |
| return send_file(audio_fp, mimetype='audio/mpeg', as_attachment=False) | |
| except Exception as e: | |
| print(f"❌ TTS Error: {e}") | |
| return jsonify({"error": f"Failed to generate audio via gTTS: {e}"}), 500 | |
| # Primeira função explain_proxy removida - duplicata | |
| def activity_feedback(): | |
| data = request.get_json() | |
| model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1) | |
| context_focus = data.get('context_focus', 'General/Social') | |
| original_prompt = data.get('original_prompt', '') | |
| user_response = data.get('user_response', '') | |
| if not original_prompt or not user_response: return jsonify({"error": "Original prompt and user response are required."}), 400 | |
| if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client): | |
| return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503 | |
| system_instruction = ( | |
| "You are an expert English teacher providing feedback. " | |
| f"The user's study focus is '{context_focus}'. " | |
| "Your entire response MUST be in English. " | |
| "Provide clear, constructive feedback on the user's writing. " | |
| "Point out grammar, spelling, or style errors. " | |
| "Offer a corrected or improved version of their text. " | |
| "Structure your feedback with markdown for clarity (e.g., using ### Corrected Version)." | |
| ) | |
| 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." | |
| try: | |
| feedback_text = get_ai_text_response(model_provider, model_name, system_instruction, user_prompt) | |
| return jsonify({"feedback": feedback_text}) | |
| except Exception as e: | |
| return jsonify({"error": f"AI feedback failed: {e}"}), 500 | |
| def analyze_image(): | |
| if not genai_client: return jsonify({"error": "GEMINI_API_KEY not configured."}), 503 | |
| data = request.get_json() | |
| base64_image = data.get('image') | |
| model_value = data.get('model', 'gemini:gemini-2.5-flash-latest') | |
| model_name = 'gemini-2.5-flash-latest' # Default | |
| if model_value.startswith('gemini:'): | |
| model_name = model_value.split(':', 1)[1] | |
| if not base64_image: return jsonify({"error": "No image data."}), 400 | |
| try: | |
| image = Image.open(io.BytesIO(base64.b64decode(base64_image.split(',')[1]))) | |
| model = genai_client.GenerativeModel(model_name) | |
| schema = { "type": "object", "properties": { "vocabulary": { "type": "array", "items": { "type": "object", "properties": { "term": {"type": "string"}, "definition": {"type": "string"} }, "required": ["term", "definition"] } } }, "required": ["vocabulary"] } | |
| 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 ] | |
| config = GenerationConfig(response_mime_type="application/json", response_schema=schema) | |
| response = model.generate_content(prompt, generation_config=config) | |
| return jsonify(json.loads(response.text)['vocabulary']) | |
| except Exception as e: | |
| return jsonify({"error": f"Image analysis failed: {e}"}), 500 | |
| def chat_with_ai(): | |
| if not groq_client: return jsonify({"error": "GROQ_API_KEY not configured."}), 503 | |
| data = request.get_json() | |
| history, user_message = data.get('history', []), data.get('message', '') | |
| if not user_message: return jsonify({"error": "No message."}), 400 | |
| try: | |
| 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." | |
| messages = [{"role": "system", "content": system}] + history + [{"role": "user", "content": user_message}] | |
| response = groq_client.chat.completions.create(model="llama-3.1-8b-instant", messages=messages, temperature=0.7) | |
| # Track token usage | |
| user = get_current_user() | |
| if user and hasattr(response, 'usage'): | |
| track_token_usage( | |
| user['id'], | |
| 'groq', | |
| response.usage.prompt_tokens, | |
| response.usage.completion_tokens, | |
| 'conversation' | |
| ) | |
| return jsonify({"response": response.choices[0].message.content.strip()}) | |
| except Exception as e: | |
| return jsonify({"error": f"AI chat failed: {e}"}), 500 | |
| def pronunciation_feedback(): | |
| if not groq_client: return jsonify({"error": "GROQ_API_KEY not configured."}), 503 | |
| data = request.get_json() | |
| target_text, user_text = data.get('target_text'), data.get('user_text') | |
| if not target_text or not user_text: return jsonify({"error": "Required data missing."}), 400 | |
| try: | |
| 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." | |
| user_prompt = f"Target: \"{target_text}\"\nTranscription: \"{user_text}\"\n\nProvide pronunciation feedback." | |
| messages = [{"role": "system", "content": system_instruction}, {"role": "user", "content": user_prompt}] | |
| response = groq_client.chat.completions.create(model="llama-3.1-8b-instant", messages=messages, temperature=0.5) | |
| # Track token usage | |
| user = get_current_user() | |
| if user and hasattr(response, 'usage'): | |
| track_token_usage( | |
| user['id'], | |
| 'groq', | |
| response.usage.prompt_tokens, | |
| response.usage.completion_tokens, | |
| 'pronunciation_feedback' | |
| ) | |
| return jsonify({"feedback": response.choices[0].message.content.strip()}) | |
| except Exception as e: | |
| return jsonify({"error": f"Pronunciation analysis failed: {e}"}), 500 | |
| def generate_image(): | |
| if not genai_client: return jsonify({"error": "GEMINI_API_KEY not configured."}), 503 | |
| data = request.get_json() | |
| prompt = data.get('prompt') | |
| if not prompt: return jsonify({"error": "Image prompt is required."}), 400 | |
| try: | |
| model = genai_client.GenerativeModel(model_name='gemini-2.5-flash-image-preview') | |
| response = model.generate_content(prompt) | |
| base64_image_data = response.parts[0].inline_data.data | |
| return jsonify({"image_base64": base64_image_data}) | |
| except Exception as e: | |
| return jsonify({"error": f"Image generation failed: {e}"}), 500 | |
| # --- FUNÇÃO AUXILIAR E ROTA RAIZ --- | |
| def get_ai_text_response(provider, model_name, system_instruction, user_prompt, json_schema=None): | |
| if provider == 'gemini': | |
| model = genai_client.GenerativeModel(model_name, system_instruction=system_instruction) | |
| config = None | |
| if json_schema: | |
| config = GenerationConfig(response_mime_type="application/json", response_schema=json_schema) | |
| response = model.generate_content(user_prompt, generation_config=config) | |
| if json_schema: | |
| parsed_json = json.loads(response.text) | |
| required_keys = json_schema.get("required", []) | |
| if not all(key in parsed_json and parsed_json[key] for key in required_keys): | |
| raise ValueError(f"AI response missing required keys or has empty values.") | |
| return parsed_json | |
| else: | |
| return response.text.strip() | |
| elif provider == 'groq': | |
| final_user_prompt = user_prompt | |
| if json_schema: | |
| 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)}" | |
| messages = [{"role": "system", "content": system_instruction}, {"role": "user", "content": final_user_prompt}] | |
| config = {'response_format': {"type": "json_object"}} if json_schema else {} | |
| response = groq_client.chat.completions.create(model=model_name, messages=messages, **config) | |
| if json_schema: | |
| parsed_json = json.loads(response.choices[0].message.content) | |
| required_keys = json_schema.get("required", []) | |
| if not all(key in parsed_json and parsed_json[key] for key in required_keys): | |
| raise ValueError(f"AI response missing required keys or has empty values.") | |
| return parsed_json | |
| else: | |
| return response.choices[0].message.content.strip() | |
| raise Exception(f"Unsupported provider: {provider}") | |
| # --- AUTHENTICATION ROUTES --- | |
| def register(): | |
| """User registration endpoint""" | |
| try: | |
| data = request.get_json() | |
| email = data.get('email', '').strip().lower() | |
| password = data.get('password', '') | |
| # Validate input | |
| if not email or not password: | |
| return jsonify({'error': 'Email and password are required'}), 400 | |
| if len(password) < 8: | |
| return jsonify({'error': 'Password must be at least 8 characters long'}), 400 | |
| # Validate email format | |
| try: | |
| validate_email(email) | |
| except EmailNotValidError: | |
| return jsonify({'error': 'Invalid email format'}), 400 | |
| # Create user | |
| result = create_user(email, password) | |
| if result['success']: | |
| # Try to send confirmation email (non-blocking for HF Spaces) | |
| email_sent = False | |
| try: | |
| # Use a timeout to prevent hanging | |
| import threading | |
| import time | |
| def send_email_async(): | |
| nonlocal email_sent | |
| try: | |
| email_sent = send_confirmation_email(email, result['confirmation_token']) | |
| except: | |
| email_sent = False | |
| # Start email sending in background with timeout | |
| email_thread = threading.Thread(target=send_email_async) | |
| email_thread.daemon = True | |
| email_thread.start() | |
| email_thread.join(timeout=5) # 5 second timeout | |
| except Exception as e: | |
| print(f"Email sending timeout or error: {e}") | |
| email_sent = False | |
| # Return success message based on auto-confirmation and email status | |
| auto_confirmed = result.get('auto_confirmed', False) | |
| if auto_confirmed: | |
| return jsonify({ | |
| 'message': 'Registration successful! Your account is ready to use - you can log in immediately.', | |
| 'email_sent': email_sent, | |
| 'auto_confirmed': True, | |
| 'note': 'Email confirmation is disabled in demo mode.' | |
| }), 201 | |
| elif email_sent: | |
| return jsonify({ | |
| 'message': 'Registration successful! Please check your email to confirm your account.', | |
| 'email_sent': True, | |
| 'auto_confirmed': False | |
| }), 201 | |
| else: | |
| return jsonify({ | |
| 'message': 'Registration successful! However, we could not send the confirmation email. Please contact support.', | |
| 'email_sent': False, | |
| 'auto_confirmed': False | |
| }), 201 | |
| else: | |
| return jsonify({'error': result['message']}), 400 | |
| except Exception as e: | |
| print(f"Registration error: {e}") | |
| return jsonify({'error': 'Internal server error'}), 500 | |
| def login(): | |
| """User login endpoint""" | |
| try: | |
| data = request.get_json() | |
| email = data.get('email', '').strip().lower() | |
| password = data.get('password', '') | |
| if not email or not password: | |
| return jsonify({'error': 'Email and password are required'}), 400 | |
| result = authenticate_user(email, password) | |
| if result['success']: | |
| session['user_id'] = result['user_id'] | |
| session['user_email'] = result['email'] | |
| session.permanent = True | |
| # Get user settings | |
| settings = get_user_settings(result['user_id']) | |
| return jsonify({ | |
| 'message': 'Login successful', | |
| 'user': { | |
| 'id': result['user_id'], | |
| 'email': result['email'], | |
| 'settings': settings | |
| } | |
| }), 200 | |
| else: | |
| return jsonify({'error': result['message']}), 401 | |
| except Exception as e: | |
| print(f"Login error: {e}") | |
| return jsonify({'error': 'Internal server error'}), 500 | |
| def logout(): | |
| """User logout endpoint""" | |
| session.clear() | |
| return jsonify({'message': 'Logout successful'}), 200 | |
| def confirm_email_route(): | |
| """Email confirmation endpoint""" | |
| token = request.args.get('token') | |
| if not token: | |
| return render_template_string(''' | |
| <!DOCTYPE html> | |
| <html><head><title>Invalid Link</title></head> | |
| <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> | |
| <h2>Invalid Confirmation Link</h2> | |
| <p>This confirmation link is invalid or malformed.</p> | |
| <a href="/" style="color: #4f46e5;">Return to English Helper</a> | |
| </body></html> | |
| '''), 400 | |
| result = confirm_email(token) | |
| if result['success']: | |
| return render_template_string(''' | |
| <!DOCTYPE html> | |
| <html><head><title>Email Confirmed</title></head> | |
| <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> | |
| <h2>✅ Email Confirmed!</h2> | |
| <p>Your email has been successfully confirmed. You can now log in to your account.</p> | |
| <a href="/" style="background: #4f46e5; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Continue to English Helper</a> | |
| </body></html> | |
| ''') | |
| else: | |
| return render_template_string(''' | |
| <!DOCTYPE html> | |
| <html><head><title>Confirmation Failed</title></head> | |
| <body style="font-family: Arial, sans-serif; text-align: center; padding: 50px;"> | |
| <h2>❌ Confirmation Failed</h2> | |
| <p>This confirmation link is invalid or has expired.</p> | |
| <a href="/" style="color: #4f46e5;">Return to English Helper</a> | |
| </body></html> | |
| '''), 400 | |
| def get_user_profile(): | |
| """Get current user profile""" | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| settings = get_user_settings(user['id']) | |
| flashcards_count = len(get_user_flashcards(user['id'], 1000)) | |
| return jsonify({ | |
| 'user': { | |
| 'id': user['id'], | |
| 'email': user['email'], | |
| 'settings': settings, | |
| 'stats': { | |
| 'flashcards_created': flashcards_count | |
| } | |
| } | |
| }) | |
| def user_settings(): | |
| """Get or update user settings""" | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| if request.method == 'GET': | |
| settings = get_user_settings(user['id']) | |
| return jsonify({'settings': settings}) | |
| elif request.method == 'POST': | |
| data = request.get_json() | |
| settings = { | |
| 'preferred_model': data.get('preferred_model'), | |
| 'context_focus': data.get('context_focus'), | |
| 'voice_accent': data.get('voice_accent'), | |
| 'daily_goal': data.get('daily_goal', 10), | |
| 'notification_enabled': data.get('notification_enabled', True) | |
| } | |
| if update_user_settings(user['id'], settings): | |
| return jsonify({'message': 'Settings updated successfully'}) | |
| else: | |
| return jsonify({'error': 'Failed to update settings'}), 500 | |
| def user_flashcards(): | |
| """Get user flashcards or save new flashcard""" | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| if request.method == 'GET': | |
| flashcards = get_user_flashcards(user['id']) | |
| return jsonify({'flashcards': flashcards}) | |
| elif request.method == 'POST': | |
| data = request.get_json() | |
| if save_user_flashcard(user['id'], data): | |
| return jsonify({'message': 'Flashcard saved successfully'}) | |
| else: | |
| return jsonify({'error': 'Failed to save flashcard'}), 500 | |
| def check_auth(): | |
| """Check if user is authenticated""" | |
| user = get_current_user() | |
| if user: | |
| settings = get_user_settings(user['id']) | |
| return jsonify({ | |
| 'authenticated': True, | |
| 'user': { | |
| 'id': user['id'], | |
| 'email': user['email'], | |
| 'settings': settings | |
| } | |
| }) | |
| else: | |
| return jsonify({'authenticated': False}) | |
| # --- MODIFIED EXISTING ROUTES TO SUPPORT USER DATA --- | |
| # Override the original explain-proxy to save flashcards for logged-in users | |
| def explain_proxy(): | |
| data = request.get_json() | |
| model_provider, model_name = data.get('model', 'gemini:gemini-2.5-flash-latest').split(':', 1) | |
| context_focus = data.get('context_focus', 'General/Social') | |
| custom_prompt = data.get('custom_prompt', None) | |
| word = data.get('word', '').strip() | |
| context = data.get('context', '') | |
| for_flashcard = data.get('for_flashcard', False) | |
| if (model_provider == 'gemini' and not genai_client) or (model_provider == 'groq' and not groq_client): | |
| return jsonify({"error": f"{model_provider.upper()}_API_KEY not configured."}), 503 | |
| 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." | |
| try: | |
| if custom_prompt: | |
| activity_text = get_ai_text_response(model_provider, model_name, system_instruction_base, custom_prompt) | |
| return jsonify({"explanation": activity_text}) | |
| if not word: return jsonify({"error": "No word selected."}), 400 | |
| if for_flashcard: | |
| 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"]} | |
| prompt = f"Analyze '{word}' in context: '{context}'. Generate a JSON for a flashcard. The 'gapped_sentence' must replace '{word}' with '______________'." | |
| flashcard_data = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt, json_schema=schema) | |
| # Save flashcard for logged-in users | |
| user = get_current_user() | |
| if user: | |
| save_user_flashcard(user['id'], flashcard_data) | |
| return jsonify(flashcard_data) | |
| else: | |
| prompt = f"Analyze '{word}' in context: '{context}'. Provide a one-sentence English explanation, then '---', then the Portuguese translation." | |
| parts = get_ai_text_response(model_provider, model_name, system_instruction_base, prompt).split('---', 1) | |
| return jsonify({"explanation": parts[0].strip(), "translation": parts[1].strip() if len(parts) > 1 else 'N/A'}) | |
| except Exception as e: | |
| print(f"AI ANALYSIS ERROR in /explain-proxy: {e}") | |
| return jsonify({"error": f"AI analysis failed: {e}"}), 500 | |
| # --- CONTENT CURATION ROUTES --- | |
| def search_content(): | |
| """Search for content based on user interests""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| data = request.get_json() | |
| query = data.get('query', '') | |
| category = data.get('category', '') | |
| # Get user settings and interests | |
| settings = get_user_settings(user['id']) | |
| interests = get_user_interests(user['id']) | |
| if not interests and query: | |
| # Use query as interest if no interests set | |
| interests = {query: 1.0} | |
| english_level = settings.get('english_level', 'B1') if settings else 'B1' | |
| context_focus = settings.get('context_focus', 'General/Social') if settings else 'General/Social' | |
| # Search for content | |
| results = content_curator.search_content( | |
| interests=list(interests.keys()) if interests else [query], | |
| english_level=english_level, | |
| context_focus=context_focus, | |
| limit=10 | |
| ) | |
| return jsonify({'results': results}) | |
| except Exception as e: | |
| print(f"Content search error: {e}") | |
| return jsonify({'error': 'Content search failed'}), 500 | |
| def extract_content(): | |
| """Extract content from URL""" | |
| try: | |
| data = request.get_json() | |
| url = data.get('url', '') | |
| if not url: | |
| return jsonify({'error': 'URL required'}), 400 | |
| result = content_curator.extract_content_from_url(url) | |
| return jsonify(result) | |
| except Exception as e: | |
| print(f"Content extraction error: {e}") | |
| return jsonify({'error': 'Content extraction failed'}), 500 | |
| def save_content(): | |
| """Save content/article for user""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| data = request.get_json() | |
| title = data.get('title', '') | |
| content = data.get('content', '') | |
| source_url = data.get('source_url') | |
| source_type = data.get('source_type', 'manual') | |
| category = data.get('category') | |
| if not title or not content: | |
| return jsonify({'error': 'Title and content required'}), 400 | |
| result = save_user_article(user['id'], title, content, source_url, source_type, category) | |
| if result['success']: | |
| # Record analytics | |
| record_analytics_metric(user['id'], 'content_saved', 1) | |
| return jsonify({'message': 'Content saved successfully', 'article_id': result['article_id']}) | |
| else: | |
| return jsonify({'error': result['message']}), 500 | |
| except Exception as e: | |
| print(f"Save content error: {e}") | |
| return jsonify({'error': 'Failed to save content'}), 500 | |
| def get_articles(): | |
| """Get user's saved articles""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| category = request.args.get('category') | |
| limit = int(request.args.get('limit', 50)) | |
| articles = get_user_articles(user['id'], category, limit) | |
| return jsonify({'articles': articles}) | |
| except Exception as e: | |
| print(f"Get articles error: {e}") | |
| return jsonify({'error': 'Failed to get articles'}), 500 | |
| def manage_interests(): | |
| """Get or update user interests""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| if request.method == 'GET': | |
| interests = get_user_interests(user['id']) | |
| return jsonify({'interests': interests}) | |
| elif request.method == 'POST': | |
| data = request.get_json() | |
| interests = data.get('interests', {}) | |
| if update_user_interests(user['id'], interests): | |
| return jsonify({'message': 'Interests updated successfully'}) | |
| else: | |
| return jsonify({'error': 'Failed to update interests'}), 500 | |
| except Exception as e: | |
| print(f"Manage interests error: {e}") | |
| return jsonify({'error': 'Failed to manage interests'}), 500 | |
| def get_recommendations(): | |
| """Get AI-powered content recommendations""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| # Get user data | |
| interests = get_user_interests(user['id']) | |
| recent_articles = get_user_articles(user['id'], limit=10) | |
| settings = get_user_settings(user['id']) | |
| english_level = settings.get('english_level', 'B1') if settings else 'B1' | |
| context_focus = settings.get('context_focus', 'General/Social') if settings else 'General/Social' | |
| # Generate recommendations | |
| recommendations = content_curator.generate_personalized_recommendations( | |
| interests, recent_articles, english_level, context_focus, user['id'] | |
| ) | |
| return jsonify({'recommendations': recommendations}) | |
| except Exception as e: | |
| print(f"Recommendations error: {e}") | |
| return jsonify({'error': 'Failed to get recommendations'}), 500 | |
| def analyze_content(): | |
| """Analyze content for learning insights""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| data = request.get_json() | |
| content = data.get('content', '') | |
| if not content: | |
| return jsonify({'error': 'Content required'}), 400 | |
| settings = get_user_settings(user['id']) | |
| english_level = settings.get('english_level', 'B1') if settings else 'B1' | |
| analysis = content_curator.analyze_content_for_learning(content, english_level) | |
| return jsonify({'analysis': analysis}) | |
| except Exception as e: | |
| print(f"Content analysis error: {e}") | |
| return jsonify({'error': 'Content analysis failed'}), 500 | |
| # --- STUDY PLANNING ROUTES --- | |
| def manage_study_plans(): | |
| """Get or create study plans""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| if request.method == 'GET': | |
| plans = get_user_study_plans(user['id']) | |
| return jsonify({'plans': plans}) | |
| elif request.method == 'POST': | |
| data = request.get_json() | |
| plan_name = data.get('plan_name', '') | |
| target_level = data.get('target_level', 'B2') | |
| current_level = data.get('current_level', 'B1') | |
| objectives = json.dumps(data.get('objectives', [])) | |
| weekly_hours = data.get('weekly_hours', 5) | |
| if not plan_name: | |
| return jsonify({'error': 'Plan name required'}), 400 | |
| result = create_study_plan(user['id'], plan_name, target_level, current_level, objectives, weekly_hours) | |
| if result['success']: | |
| return jsonify({'message': 'Study plan created', 'plan_id': result['plan_id']}) | |
| else: | |
| return jsonify({'error': result['message']}), 500 | |
| except Exception as e: | |
| print(f"Study plans error: {e}") | |
| return jsonify({'error': 'Failed to manage study plans'}), 500 | |
| def analytics_dashboard(): | |
| """Get analytics dashboard data""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| days = int(request.args.get('days', 30)) | |
| # Get various analytics | |
| analytics_data = { | |
| 'flashcards_created': get_user_analytics(user['id'], 'flashcards_created', days), | |
| 'content_saved': get_user_analytics(user['id'], 'content_saved', days), | |
| 'study_sessions': get_user_analytics(user['id'], 'study_session', days), | |
| 'total_flashcards': len(get_user_flashcards(user['id'], 1000)), | |
| 'total_articles': len(get_user_articles(user['id'], limit=1000)), | |
| 'user_level': get_user_settings(user['id']).get('english_level', 'B1') | |
| } | |
| return jsonify({'analytics': analytics_data}) | |
| except Exception as e: | |
| print(f"Analytics error: {e}") | |
| return jsonify({'error': 'Failed to get analytics'}), 500 | |
| # --- STUDY PLANNER ROUTES --- | |
| def create_study_plan_route(): | |
| """Create a personalized study plan""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| data = request.get_json() | |
| # Get user settings and interests | |
| user_settings = get_user_settings(user['id']) | |
| user_interests = get_user_interests(user['id']) | |
| # Prepare data for study planner | |
| planner_data = { | |
| 'english_level': data.get('current_level') or user_settings.get('english_level', 'B1'), | |
| 'target_level': data.get('target_level', 'B2'), | |
| 'weekly_hours': int(data.get('weekly_hours', 5)), | |
| 'context_focus': data.get('context_focus') or user_settings.get('context_focus', 'General/Social'), | |
| 'interests': user_interests, | |
| 'study_goals': data.get('study_goals', []) | |
| } | |
| # Generate the plan | |
| result = study_planner.generate_personalized_plan(planner_data) | |
| if result['success']: | |
| plan = result['plan'] | |
| # Save to database | |
| plan_id = create_study_plan( | |
| user['id'], | |
| plan['target_level'], | |
| plan['weekly_hours'], | |
| plan['estimated_weeks'], | |
| json.dumps(plan) | |
| ) | |
| plan['id'] = plan_id | |
| # Record analytics | |
| record_analytics_metric(user['id'], 'study_plan_created', 1) | |
| return jsonify({'success': True, 'plan': plan}) | |
| else: | |
| return jsonify({'success': False, 'error': result['error']}), 500 | |
| except Exception as e: | |
| print(f"Study plan creation error: {e}") | |
| return jsonify({'error': 'Failed to create study plan'}), 500 | |
| def get_current_study_plan(): | |
| """Get user's current study plan""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| plans = get_user_study_plans(user['id']) | |
| if plans: | |
| # Get the most recent active plan | |
| current_plan = plans[0] # Assuming most recent first | |
| # Parse the plan data | |
| plan_data = json.loads(current_plan['plan_data']) | |
| # Add database ID | |
| plan_data['db_id'] = current_plan['id'] | |
| # Get activities for this plan | |
| activities = get_study_activities(current_plan['id']) | |
| plan_data['completed_activities'] = activities | |
| return jsonify({'success': True, 'plan': plan_data}) | |
| else: | |
| return jsonify({'success': True, 'plan': None}) | |
| except Exception as e: | |
| print(f"Get study plan error: {e}") | |
| return jsonify({'error': 'Failed to get study plan'}), 500 | |
| def complete_study_activity(): | |
| """Mark a study activity as completed""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| data = request.get_json() | |
| plan_id = data.get('plan_id') | |
| activity_id = data.get('activity_id') | |
| duration_minutes = data.get('duration_minutes', 0) | |
| notes = data.get('notes', '') | |
| if not plan_id or not activity_id: | |
| return jsonify({'error': 'Missing plan_id or activity_id'}), 400 | |
| # Add activity completion | |
| add_study_activity(plan_id, activity_id, duration_minutes, notes) | |
| # Record analytics | |
| record_analytics_metric(user['id'], 'study_activity_completed', 1) | |
| record_analytics_metric(user['id'], 'study_time_minutes', duration_minutes) | |
| return jsonify({'success': True}) | |
| except Exception as e: | |
| print(f"Complete activity error: {e}") | |
| return jsonify({'error': 'Failed to complete activity'}), 500 | |
| def get_study_progress(): | |
| """Get study plan progress analytics""" | |
| try: | |
| user = get_current_user() | |
| if not user: | |
| return jsonify({'error': 'User not found'}), 404 | |
| plans = get_user_study_plans(user['id']) | |
| if not plans: | |
| return jsonify({'success': True, 'progress': None}) | |
| current_plan = plans[0] | |
| plan_data = json.loads(current_plan['plan_data']) | |
| activities = get_study_activities(current_plan['id']) | |
| # Calculate progress | |
| total_activities = len(plan_data.get('activities', [])) | |
| completed_activities = len(activities) | |
| progress_data = { | |
| 'total_activities': total_activities, | |
| 'completed_activities': completed_activities, | |
| 'completion_percentage': (completed_activities / max(total_activities, 1)) * 100, | |
| 'estimated_weeks': plan_data.get('estimated_weeks', 0), | |
| 'weeks_elapsed': max(1, (datetime.now() - datetime.fromisoformat(current_plan['created_at'])).days // 7), | |
| 'target_level': plan_data.get('target_level', 'B2'), | |
| 'weekly_hours': plan_data.get('weekly_hours', 5), | |
| 'recent_activities': activities[-10:] if activities else [] # Last 10 activities | |
| } | |
| return jsonify({'success': True, 'progress': progress_data}) | |
| except Exception as e: | |
| print(f"Study progress error: {e}") | |
| return jsonify({'error': 'Failed to get study progress'}), 500 | |
| # --- ADMIN ROUTES --- | |
| def admin_login(): | |
| """Admin login endpoint""" | |
| try: | |
| data = request.get_json() | |
| username = data.get('username') | |
| password = data.get('password') | |
| print(f"Admin login attempt - Username: {username}") | |
| if admin_manager.login_admin(username, password): | |
| print(f"Admin login successful - Session ID: {session.get('_id', 'no-id')}") | |
| print(f"Session data after login: {dict(session)}") | |
| return jsonify({'success': True, 'message': 'Admin logged in successfully'}) | |
| else: | |
| print(f"Admin login failed - Invalid credentials for: {username}") | |
| return jsonify({'success': False, 'error': 'Invalid credentials'}), 401 | |
| except Exception as e: | |
| print(f"Admin login error: {e}") | |
| return jsonify({'error': 'Admin login failed'}), 500 | |
| def admin_logout(): | |
| """Admin logout endpoint""" | |
| try: | |
| admin_manager.logout_admin() | |
| return jsonify({'success': True, 'message': 'Admin logged out successfully'}) | |
| except Exception as e: | |
| print(f"Admin logout error: {e}") | |
| return jsonify({'error': 'Admin logout failed'}), 500 | |
| def admin_check(): | |
| """Check admin authentication status""" | |
| try: | |
| is_authenticated = admin_manager.is_admin_logged_in() | |
| username = session.get('admin_username') | |
| print(f"Admin check - Authenticated: {is_authenticated}, Username: {username}") | |
| print(f"Current session data: {dict(session)}") | |
| print(f"Session ID: {session.get('_id', 'no-session-id')}") | |
| return jsonify({ | |
| 'authenticated': is_authenticated, | |
| 'username': username if is_authenticated else None, | |
| 'session_id': session.get('_id', 'no-session-id'), | |
| 'debug_session_keys': list(session.keys()) | |
| }) | |
| except Exception as e: | |
| print(f"Admin check error: {e}") | |
| return jsonify({'authenticated': False}) | |
| # Debug route to test sessions | |
| def debug_session(): | |
| """Debug session functionality""" | |
| if request.method == 'POST': | |
| session['debug_test'] = 'session_working' | |
| session.permanent = True | |
| return jsonify({ | |
| 'message': 'Session test value set', | |
| 'session_data': dict(session) | |
| }) | |
| else: | |
| test_value = session.get('debug_test', 'not_found') | |
| return jsonify({ | |
| 'test_value': test_value, | |
| 'session_data': dict(session), | |
| 'session_id': session.get('_id', 'no-session-id') | |
| }) | |
| def admin_dashboard(): | |
| """Get admin dashboard data""" | |
| try: | |
| stats = admin_manager.get_system_stats() | |
| return jsonify({'success': True, 'stats': stats}) | |
| except Exception as e: | |
| print(f"Admin dashboard error: {e}") | |
| return jsonify({'error': 'Failed to load dashboard'}), 500 | |
| def admin_get_users(): | |
| """Get paginated list of users""" | |
| try: | |
| page = int(request.args.get('page', 1)) | |
| per_page = int(request.args.get('per_page', 20)) | |
| users_data = admin_manager.get_all_users(page, per_page) | |
| return jsonify({'success': True, 'data': users_data}) | |
| except Exception as e: | |
| print(f"Admin get users error: {e}") | |
| return jsonify({'error': 'Failed to get users'}), 500 | |
| def admin_get_user_details(user_id): | |
| """Get detailed information about a user""" | |
| try: | |
| user_details = admin_manager.get_user_details(user_id) | |
| if user_details: | |
| return jsonify({'success': True, 'user': user_details}) | |
| else: | |
| return jsonify({'error': 'User not found'}), 404 | |
| except Exception as e: | |
| print(f"Admin get user details error: {e}") | |
| return jsonify({'error': 'Failed to get user details'}), 500 | |
| def admin_delete_user(user_id): | |
| """Delete a user and all associated data""" | |
| try: | |
| if admin_manager.delete_user(user_id): | |
| return jsonify({'success': True, 'message': 'User deleted successfully'}) | |
| else: | |
| return jsonify({'error': 'Failed to delete user'}), 500 | |
| except Exception as e: | |
| print(f"Admin delete user error: {e}") | |
| return jsonify({'error': 'Failed to delete user'}), 500 | |
| def admin_get_database_schema(): | |
| """Get database schema information""" | |
| try: | |
| schema = admin_manager.get_database_schema() | |
| return jsonify({'success': True, 'schema': schema}) | |
| except Exception as e: | |
| print(f"Admin get schema error: {e}") | |
| return jsonify({'error': 'Failed to get database schema'}), 500 | |
| def record_token_usage(): | |
| """Record token usage (called by AI functions)""" | |
| try: | |
| data = request.get_json() | |
| user_id = data.get('user_id') | |
| api_provider = data.get('api_provider') | |
| input_tokens = data.get('input_tokens', 0) | |
| output_tokens = data.get('output_tokens', 0) | |
| operation_type = data.get('operation_type', 'unknown') | |
| admin_manager.record_token_usage( | |
| user_id, api_provider, input_tokens, output_tokens, operation_type | |
| ) | |
| return jsonify({'success': True}) | |
| except Exception as e: | |
| print(f"Token usage recording error: {e}") | |
| return jsonify({'error': 'Failed to record token usage'}), 500 | |
| def export_users(): | |
| """Export users data as CSV""" | |
| try: | |
| import csv | |
| from io import StringIO | |
| users_data = admin_manager.get_all_users(page=1, per_page=10000) # Get all users | |
| output = StringIO() | |
| writer = csv.writer(output) | |
| # Write header | |
| writer.writerow(['ID', 'Email', 'Created At', 'Email Confirmed', 'Last Login', 'Sessions', 'Flashcards', 'Articles']) | |
| # Write data | |
| for user in users_data['users']: | |
| writer.writerow([ | |
| user['id'], | |
| user['email'], | |
| user['created_at'], | |
| user['email_confirmed'], | |
| user['last_login'] or 'Never', | |
| user['session_count'], | |
| user['flashcard_count'], | |
| user['article_count'] | |
| ]) | |
| output.seek(0) | |
| return Response( | |
| output.getvalue(), | |
| mimetype='text/csv', | |
| headers={'Content-Disposition': f'attachment; filename=users_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'} | |
| ) | |
| except Exception as e: | |
| print(f"Export users error: {e}") | |
| return jsonify({'error': 'Failed to export users'}), 500 | |
| def export_token_usage(): | |
| """Export token usage data as CSV""" | |
| try: | |
| import csv | |
| from io import StringIO | |
| conn = get_db_connection() | |
| cursor = conn.cursor() | |
| cursor.execute(""" | |
| SELECT t.created_at, u.email, t.api_provider, t.input_tokens, | |
| t.output_tokens, t.tokens_used, t.operation_type | |
| FROM token_usage t | |
| LEFT JOIN users u ON t.user_id = u.id | |
| ORDER BY t.created_at DESC | |
| """) | |
| token_data = cursor.fetchall() | |
| conn.close() | |
| output = StringIO() | |
| writer = csv.writer(output) | |
| # Write header | |
| writer.writerow(['Date', 'User Email', 'Provider', 'Input Tokens', 'Output Tokens', 'Total Tokens', 'Operation']) | |
| # Write data | |
| for row in token_data: | |
| writer.writerow(row) | |
| output.seek(0) | |
| return Response( | |
| output.getvalue(), | |
| mimetype='text/csv', | |
| headers={'Content-Disposition': f'attachment; filename=token_usage_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'} | |
| ) | |
| except Exception as e: | |
| print(f"Export tokens error: {e}") | |
| return jsonify({'error': 'Failed to export token usage'}), 500 | |
| def get_system_health(): | |
| """Get system health metrics""" | |
| try: | |
| health = admin_manager.get_system_health() | |
| return jsonify({'success': True, 'health': health}) | |
| except Exception as e: | |
| print(f"System health error: {e}") | |
| return jsonify({'error': 'Failed to get system health'}), 500 | |
| def get_system_alerts(): | |
| """Get system alerts""" | |
| try: | |
| alerts = admin_manager.check_system_alerts() | |
| return jsonify({'success': True, 'alerts': alerts}) | |
| except Exception as e: | |
| print(f"System alerts error: {e}") | |
| return jsonify({'error': 'Failed to get system alerts'}), 500 | |
| def admin_interface(): | |
| """Serve admin interface""" | |
| return send_file('templates/admin.html') | |
| def root(): | |
| return send_file('templates/index.html') | |
| # Evitar execução automática quando importado | |
| if __name__ == '__main__': | |
| print("⚠️ flask_app.py executado diretamente") | |
| print("💡 Use app.py para HF Spaces ou execute como módulo") | |
| app.run(host='0.0.0.0', port=5000, debug=True) |