# 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 @app.before_request 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]}...") @app.after_request 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}") @app.teardown_appcontext 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 --- @app.route('/list-models') 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 = {} @app.route('/tts-proxy', methods=['POST']) 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 @app.route('/activity-feedback', methods=['POST']) 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 @app.route('/analyze-image', methods=['POST']) 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 @app.route('/chat-with-ai', methods=['POST']) 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 @app.route('/pronunciation-feedback', methods=['POST']) 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 @app.route('/generate-image', methods=['POST']) 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 --- @app.route('/register', methods=['POST']) 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 @app.route('/login', methods=['POST']) 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 @app.route('/logout', methods=['POST']) def logout(): """User logout endpoint""" session.clear() return jsonify({'message': 'Logout successful'}), 200 @app.route('/confirm-email') def confirm_email_route(): """Email confirmation endpoint""" token = request.args.get('token') if not token: return render_template_string('''
This confirmation link is invalid or malformed.
Return to English Helper '''), 400 result = confirm_email(token) if result['success']: return render_template_string('''Your email has been successfully confirmed. You can now log in to your account.
Continue to English Helper ''') else: return render_template_string('''This confirmation link is invalid or has expired.
Return to English Helper '''), 400 @app.route('/user/profile', methods=['GET']) @login_required 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 } } }) @app.route('/user/settings', methods=['GET', 'POST']) @login_required 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 @app.route('/user/flashcards', methods=['GET', 'POST']) @login_required 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 @app.route('/auth/check', methods=['GET']) 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 @app.route('/explain-proxy', methods=['POST']) 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 --- @app.route('/content/search', methods=['POST']) @login_required 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 @app.route('/content/extract', methods=['POST']) @login_required 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 @app.route('/content/save', methods=['POST']) @login_required 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 @app.route('/content/articles', methods=['GET']) @login_required 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 @app.route('/content/interests', methods=['GET', 'POST']) @login_required 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 @app.route('/content/recommendations', methods=['GET']) @login_required 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 @app.route('/content/analyze', methods=['POST']) @login_required 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 --- @app.route('/study/plans', methods=['GET', 'POST']) @login_required 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 @app.route('/analytics/dashboard', methods=['GET']) @login_required 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 --- @app.route('/study-plan/create', methods=['POST']) @login_required 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 @app.route('/study-plan/current', methods=['GET']) @login_required 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 @app.route('/study-plan/activity/complete', methods=['POST']) @login_required 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 @app.route('/study-plan/progress', methods=['GET']) @login_required 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 --- @app.route('/admin/login', methods=['POST']) 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 @app.route('/admin/logout', methods=['POST']) @admin_required 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 @app.route('/admin/check', methods=['GET']) 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 @app.route('/admin/debug-session', methods=['GET', 'POST']) 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') }) @app.route('/admin/dashboard', methods=['GET']) @admin_required 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 @app.route('/admin/users', methods=['GET']) @admin_required 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 @app.route('/admin/users/