EnglishStudyHelper / flask_app.py
amauricunha's picture
Upload 6 files
625c7c9 verified
Raw
History Blame Contribute Delete
54.8 kB
# 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('''
<!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
@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/<int:user_id>', methods=['GET'])
@admin_required
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
@app.route('/admin/users/<int:user_id>', methods=['DELETE'])
@admin_required
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
@app.route('/admin/database/schema', methods=['GET'])
@admin_required
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
@app.route('/admin/token-usage', methods=['POST'])
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
@app.route('/admin/export/users', methods=['GET'])
@admin_required
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
@app.route('/admin/export/tokens', methods=['GET'])
@admin_required
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
@app.route('/admin/system/health', methods=['GET'])
@admin_required
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
@app.route('/admin/system/alerts', methods=['GET'])
@admin_required
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
@app.route('/admin')
def admin_interface():
"""Serve admin interface"""
return send_file('templates/admin.html')
@app.route('/')
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)