Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- README.md +51 -52
- flask_app_hf.py +393 -44
- study_plan.py +16 -13
README.md
CHANGED
|
@@ -1,53 +1,52 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: English Helper
|
| 3 |
-
emoji: 🎓
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: purple
|
| 6 |
-
sdk: gradio
|
| 7 |
-
app_file: app.py
|
| 8 |
-
pinned: false
|
| 9 |
-
license: mit
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
- **
|
| 20 |
-
- **
|
| 21 |
-
- **
|
| 22 |
-
- **
|
| 23 |
-
- **
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
- **
|
| 29 |
-
- **No
|
| 30 |
-
- **
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
├──
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
**Versão simplificada para máxima compatibilidade com HF Spaces**
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: English Helper
|
| 3 |
+
emoji: 🎓
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
app_file: app.py
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# English Helper - HF Spaces
|
| 13 |
+
|
| 14 |
+
Sistema simplificado de aprendizado de inglês para Hugging Face Spaces.
|
| 15 |
+
|
| 16 |
+
## 🚀 Funcionalidades
|
| 17 |
+
|
| 18 |
+
- **Seleção de Usuário**: Escolha ou crie usuário (sem autenticação)
|
| 19 |
+
- **Text-to-Speech**: Áudio dos textos com gTTS
|
| 20 |
+
- **Flashcards**: Criação automática com IA
|
| 21 |
+
- **Chat**: Conversação básica
|
| 22 |
+
- **Admin Panel**: Gestão de usuários em `/admin`
|
| 23 |
+
- **Status Page**: Monitoramento em `/status`
|
| 24 |
+
|
| 25 |
+
## 🏗️ Arquitetura Simplificada
|
| 26 |
+
|
| 27 |
+
- **Storage**: Arquivos JSON em `hf_data/`
|
| 28 |
+
- **No Database**: Sistema baseado em arquivos
|
| 29 |
+
- **No Authentication**: Seleção de usuário apenas
|
| 30 |
+
- **Minimal Dependencies**: Flask + requests + gTTS
|
| 31 |
+
|
| 32 |
+
## 📁 Estrutura
|
| 33 |
+
|
| 34 |
+
```
|
| 35 |
+
app.py # Entry point HF
|
| 36 |
+
flask_app_hf.py # Flask simplificado
|
| 37 |
+
requirements.txt # Dependências mínimas
|
| 38 |
+
templates/ # Interface HTML
|
| 39 |
+
├── index.html # Interface principal
|
| 40 |
+
├── admin.html # Painel admin
|
| 41 |
+
└── status.html # Status do sistema
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
## 🎯 Deploy
|
| 45 |
+
|
| 46 |
+
1. Faça upload de todos os arquivos desta pasta para seu HF Space
|
| 47 |
+
2. O app roda automaticamente na porta 7860
|
| 48 |
+
3. Acesse a interface principal, admin (/admin) e status (/status)
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
|
|
|
| 52 |
**Versão simplificada para máxima compatibilidade com HF Spaces**
|
flask_app_hf.py
CHANGED
|
@@ -5,10 +5,21 @@ import json
|
|
| 5 |
import base64
|
| 6 |
from datetime import datetime
|
| 7 |
from flask import Flask, request, jsonify, send_file, render_template, render_template_string, redirect, url_for, Response
|
| 8 |
-
|
| 9 |
-
from
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# --- CONFIGURAÇÃO INICIAL ---
|
| 14 |
app = Flask(__name__)
|
|
@@ -19,62 +30,72 @@ app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
|
|
| 19 |
|
| 20 |
print(f"✅ Flask HF app inicializado")
|
| 21 |
print(f"✅ Working directory: {os.getcwd()}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
# --- UTILITÁRIOS DE ARMAZENAMENTO ---
|
| 24 |
|
| 25 |
def save_user_data(user_id, data_type, data):
|
| 26 |
-
"""Salvar dados do usuário em
|
| 27 |
try:
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
data['timestamp'] = datetime.now().isoformat()
|
| 41 |
-
existing_data.append(data)
|
| 42 |
-
|
| 43 |
-
# Manter apenas os últimos 100 itens
|
| 44 |
-
if len(existing_data) > 100:
|
| 45 |
-
existing_data = existing_data[-100:]
|
| 46 |
-
|
| 47 |
-
# Salvar
|
| 48 |
-
with open(file_path, 'w', encoding='utf-8') as f:
|
| 49 |
-
json.dump(existing_data, f, ensure_ascii=False, indent=2)
|
| 50 |
-
|
| 51 |
return True
|
| 52 |
except Exception as e:
|
| 53 |
-
print(f"Erro ao salvar dados: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
return False
|
| 55 |
|
| 56 |
def load_user_data(user_id, data_type):
|
| 57 |
-
"""
|
| 58 |
try:
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
with open(file_path, 'r', encoding='utf-8') as f:
|
| 62 |
-
return json.load(f)
|
| 63 |
-
return []
|
| 64 |
except Exception as e:
|
| 65 |
-
print(f"Erro ao carregar dados: {e}")
|
| 66 |
return []
|
| 67 |
|
| 68 |
def get_all_users():
|
| 69 |
-
"""
|
| 70 |
users = set()
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
if filename.endswith('.json'):
|
| 76 |
-
users.add(filename[:-5]) # Remove .json
|
| 77 |
-
return sorted(list(users))
|
| 78 |
|
| 79 |
# --- CONFIGURAÇÃO DE APIs ---
|
| 80 |
|
|
@@ -259,6 +280,31 @@ def get_users():
|
|
| 259 |
users = get_all_users()
|
| 260 |
return jsonify({'users': users, 'total': len(users)})
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
@app.route('/user/<user_id>/flashcards', methods=['GET', 'POST'])
|
| 263 |
def user_flashcards(user_id):
|
| 264 |
"""Gerenciar flashcards do usuário"""
|
|
@@ -335,6 +381,304 @@ def system_status():
|
|
| 335 |
'auth': 'disabled'
|
| 336 |
})
|
| 337 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
from study_plan import save_study_plan, load_study_plan, generate_study_plan
|
| 339 |
|
| 340 |
@app.route('/study-plan', methods=['GET', 'POST'])
|
|
@@ -343,8 +687,13 @@ def study_plan():
|
|
| 343 |
if request.method == 'POST':
|
| 344 |
try:
|
| 345 |
data = request.get_json()
|
|
|
|
| 346 |
plan = generate_study_plan(data)
|
|
|
|
| 347 |
save_study_plan(plan)
|
|
|
|
|
|
|
|
|
|
| 348 |
return jsonify({'success': True, 'message': 'Study plan generated and saved', 'plan': plan})
|
| 349 |
except Exception as e:
|
| 350 |
print(f"Erro ao salvar study plan: {e}")
|
|
|
|
| 5 |
import base64
|
| 6 |
from datetime import datetime
|
| 7 |
from flask import Flask, request, jsonify, send_file, render_template, render_template_string, redirect, url_for, Response
|
| 8 |
+
try:
|
| 9 |
+
from gtts import gTTS
|
| 10 |
+
except Exception:
|
| 11 |
+
gTTS = None
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
from groq import Groq
|
| 15 |
+
except Exception:
|
| 16 |
+
Groq = None
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
import google.generativeai as genai
|
| 20 |
+
from google.generativeai.types import GenerationConfig
|
| 21 |
+
except Exception:
|
| 22 |
+
genai = None
|
| 23 |
|
| 24 |
# --- CONFIGURAÇÃO INICIAL ---
|
| 25 |
app = Flask(__name__)
|
|
|
|
| 30 |
|
| 31 |
print(f"✅ Flask HF app inicializado")
|
| 32 |
print(f"✅ Working directory: {os.getcwd()}")
|
| 33 |
+
# In-memory storage (suitable for HF Spaces testing only)
|
| 34 |
+
IN_MEMORY = {
|
| 35 |
+
'users': {}, # user_id -> record dict
|
| 36 |
+
'flashcards': {}, # user_id -> [items]
|
| 37 |
+
'conversations': {}, # user_id -> [items]
|
| 38 |
+
'analytics': {}, # user_id -> [items]
|
| 39 |
+
'study_plans': {}, # user_id or 'global' -> plan dict/list
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
# Keep legacy DATA_ROOT variable for compatibility checks (unused in-memory)
|
| 43 |
+
_module_dir = os.path.dirname(os.path.abspath(__file__))
|
| 44 |
+
DATA_ROOT = os.environ.get('EH_DATA_ROOT', os.path.join(_module_dir, 'hf_data'))
|
| 45 |
|
| 46 |
# --- UTILITÁRIOS DE ARMAZENAMENTO ---
|
| 47 |
|
| 48 |
def save_user_data(user_id, data_type, data):
|
| 49 |
+
"""Salvar dados do usuário em memória."""
|
| 50 |
try:
|
| 51 |
+
store = IN_MEMORY.setdefault(data_type, {})
|
| 52 |
+
lst = store.setdefault(user_id, [])
|
| 53 |
+
# attach timestamp
|
| 54 |
+
if isinstance(data, dict):
|
| 55 |
+
data = dict(data)
|
| 56 |
+
entry = data
|
| 57 |
+
if isinstance(entry, dict):
|
| 58 |
+
entry.setdefault('timestamp', datetime.now().isoformat())
|
| 59 |
+
lst.append(entry)
|
| 60 |
+
# keep last 100
|
| 61 |
+
if len(lst) > 100:
|
| 62 |
+
store[user_id] = lst[-100:]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
return True
|
| 64 |
except Exception as e:
|
| 65 |
+
print(f"Erro ao salvar dados (in-memory): {e}")
|
| 66 |
+
return False
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def save_user_record(user_id, metadata=None):
|
| 70 |
+
"""Create or update a simple user record in memory."""
|
| 71 |
+
try:
|
| 72 |
+
record = IN_MEMORY['users'].get(user_id, {})
|
| 73 |
+
record['id'] = user_id
|
| 74 |
+
record.setdefault('created_at', datetime.now().isoformat())
|
| 75 |
+
if metadata and isinstance(metadata, dict):
|
| 76 |
+
record.update(metadata)
|
| 77 |
+
IN_MEMORY['users'][user_id] = record
|
| 78 |
+
return True
|
| 79 |
+
except Exception as e:
|
| 80 |
+
print(f"save_user_record error (in-memory): {e}")
|
| 81 |
return False
|
| 82 |
|
| 83 |
def load_user_data(user_id, data_type):
|
| 84 |
+
"""Load user data from memory."""
|
| 85 |
try:
|
| 86 |
+
store = IN_MEMORY.get(data_type, {})
|
| 87 |
+
return list(store.get(user_id, []))
|
|
|
|
|
|
|
|
|
|
| 88 |
except Exception as e:
|
| 89 |
+
print(f"Erro ao carregar dados (in-memory): {e}")
|
| 90 |
return []
|
| 91 |
|
| 92 |
def get_all_users():
|
| 93 |
+
"""Return sorted list of all user ids known in memory."""
|
| 94 |
users = set()
|
| 95 |
+
users.update(IN_MEMORY.get('users', {}).keys())
|
| 96 |
+
for data_type in ['flashcards', 'conversations', 'analytics', 'study_plans']:
|
| 97 |
+
users.update(IN_MEMORY.get(data_type, {}).keys())
|
| 98 |
+
return sorted([u for u in users if u])
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
# --- CONFIGURAÇÃO DE APIs ---
|
| 101 |
|
|
|
|
| 280 |
users = get_all_users()
|
| 281 |
return jsonify({'users': users, 'total': len(users)})
|
| 282 |
|
| 283 |
+
|
| 284 |
+
@app.route('/user/create', methods=['POST'])
|
| 285 |
+
def create_user():
|
| 286 |
+
"""Create a lightweight user record for HF Spaces (no auth)."""
|
| 287 |
+
try:
|
| 288 |
+
data = request.get_json() or {}
|
| 289 |
+
user_id = data.get('user_id') or data.get('email') or data.get('name')
|
| 290 |
+
if not user_id:
|
| 291 |
+
return jsonify({'success': False, 'error': 'user_id (or email/name) required'}), 400
|
| 292 |
+
|
| 293 |
+
# sanitize user_id to a filename-friendly string
|
| 294 |
+
safe_id = ''.join(c for c in user_id if c.isalnum() or c in ('-', '_')).lower()
|
| 295 |
+
if not safe_id:
|
| 296 |
+
return jsonify({'success': False, 'error': 'invalid user_id'}), 400
|
| 297 |
+
|
| 298 |
+
ok = save_user_record(safe_id, {'raw': user_id})
|
| 299 |
+
if not ok:
|
| 300 |
+
return jsonify({'success': False, 'error': 'failed to save user record'}), 500
|
| 301 |
+
|
| 302 |
+
users = get_all_users()
|
| 303 |
+
return jsonify({'success': True, 'user_id': safe_id, 'users': users})
|
| 304 |
+
except Exception as e:
|
| 305 |
+
print(f"create_user error: {e}")
|
| 306 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 307 |
+
|
| 308 |
@app.route('/user/<user_id>/flashcards', methods=['GET', 'POST'])
|
| 309 |
def user_flashcards(user_id):
|
| 310 |
"""Gerenciar flashcards do usuário"""
|
|
|
|
| 381 |
'auth': 'disabled'
|
| 382 |
})
|
| 383 |
|
| 384 |
+
|
| 385 |
+
# --- ADMIN / HEALTH / EXPORT (file-based implementations for HF Spaces) ---
|
| 386 |
+
@app.route('/admin/system/health', methods=['GET'])
|
| 387 |
+
def admin_system_health():
|
| 388 |
+
"""Return simple system health info using file-based storage (no external deps)."""
|
| 389 |
+
try:
|
| 390 |
+
import shutil
|
| 391 |
+
data_root = DATA_ROOT
|
| 392 |
+
schema = {}
|
| 393 |
+
total_rows = 0
|
| 394 |
+
db_size = 0
|
| 395 |
+
|
| 396 |
+
if os.path.exists(data_root):
|
| 397 |
+
for folder in os.listdir(data_root):
|
| 398 |
+
folder_path = os.path.join(data_root, folder)
|
| 399 |
+
if os.path.isdir(folder_path):
|
| 400 |
+
files = [f for f in os.listdir(folder_path) if f.endswith('.json')]
|
| 401 |
+
row_count = len(files)
|
| 402 |
+
total_rows += row_count
|
| 403 |
+
# approximate size
|
| 404 |
+
size = 0
|
| 405 |
+
for fn in files:
|
| 406 |
+
p = os.path.join(folder_path, fn)
|
| 407 |
+
try:
|
| 408 |
+
size += os.path.getsize(p)
|
| 409 |
+
except Exception:
|
| 410 |
+
pass
|
| 411 |
+
db_size += size
|
| 412 |
+
schema[folder] = {'row_count': row_count, 'size_bytes': size}
|
| 413 |
+
|
| 414 |
+
# disk usage for current filesystem
|
| 415 |
+
try:
|
| 416 |
+
du = shutil.disk_usage('.')
|
| 417 |
+
disk = {
|
| 418 |
+
'total': du.total,
|
| 419 |
+
'used': du.used,
|
| 420 |
+
'free': du.free,
|
| 421 |
+
'percent': round(du.used / du.total * 100, 2) if du.total else 0
|
| 422 |
+
}
|
| 423 |
+
except Exception:
|
| 424 |
+
disk = {}
|
| 425 |
+
|
| 426 |
+
health = {
|
| 427 |
+
'memory': None,
|
| 428 |
+
'disk': disk,
|
| 429 |
+
'database': {
|
| 430 |
+
'data_root': data_root,
|
| 431 |
+
'total_rows': total_rows,
|
| 432 |
+
'estimated_size_bytes': db_size,
|
| 433 |
+
'tables': schema
|
| 434 |
+
},
|
| 435 |
+
'uptime': datetime.now().isoformat()
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
return jsonify({'success': True, 'health': health})
|
| 439 |
+
except Exception as e:
|
| 440 |
+
print(f"Health endpoint error: {e}")
|
| 441 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
@app.route('/admin/database/schema', methods=['GET'])
|
| 445 |
+
def admin_database_schema():
|
| 446 |
+
"""Return a simple schema overview derived from hf_data folders."""
|
| 447 |
+
try:
|
| 448 |
+
data_root = DATA_ROOT
|
| 449 |
+
schema = {}
|
| 450 |
+
if os.path.exists(data_root):
|
| 451 |
+
for folder in os.listdir(data_root):
|
| 452 |
+
folder_path = os.path.join(data_root, folder)
|
| 453 |
+
if os.path.isdir(folder_path):
|
| 454 |
+
files = [f for f in os.listdir(folder_path) if f.endswith('.json')]
|
| 455 |
+
# try to infer columns from first file
|
| 456 |
+
cols = []
|
| 457 |
+
if files:
|
| 458 |
+
sample_path = os.path.join(folder_path, files[0])
|
| 459 |
+
try:
|
| 460 |
+
with open(sample_path, 'r', encoding='utf-8') as fh:
|
| 461 |
+
data = json.load(fh)
|
| 462 |
+
if isinstance(data, list) and data:
|
| 463 |
+
sample = data[0]
|
| 464 |
+
elif isinstance(data, dict):
|
| 465 |
+
sample = data
|
| 466 |
+
else:
|
| 467 |
+
sample = {}
|
| 468 |
+
cols = [{'name': k, 'type': type(v).__name__} for k, v in (sample or {}).items()]
|
| 469 |
+
except Exception:
|
| 470 |
+
cols = []
|
| 471 |
+
|
| 472 |
+
schema[folder] = {
|
| 473 |
+
'row_count': len(files),
|
| 474 |
+
'columns': cols
|
| 475 |
+
}
|
| 476 |
+
else:
|
| 477 |
+
schema = {}
|
| 478 |
+
|
| 479 |
+
return jsonify({'success': True, 'schema': schema})
|
| 480 |
+
except Exception as e:
|
| 481 |
+
print(f"Schema error: {e}")
|
| 482 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
@app.route('/admin/users', methods=['GET'])
|
| 486 |
+
def admin_list_users():
|
| 487 |
+
"""Return paginated list of users (derived from hf_data files)."""
|
| 488 |
+
try:
|
| 489 |
+
page = int(request.args.get('page', 1))
|
| 490 |
+
per_page = int(request.args.get('per_page', 20))
|
| 491 |
+
|
| 492 |
+
users = get_all_users()
|
| 493 |
+
total = len(users)
|
| 494 |
+
total_pages = max(1, (total + per_page - 1) // per_page)
|
| 495 |
+
|
| 496 |
+
start = (page - 1) * per_page
|
| 497 |
+
end = start + per_page
|
| 498 |
+
users_page = []
|
| 499 |
+
|
| 500 |
+
for uid in users[start:end]:
|
| 501 |
+
# gather basic stats
|
| 502 |
+
flashcards = len(load_user_data(uid, 'flashcards'))
|
| 503 |
+
conversations = len(load_user_data(uid, 'conversations'))
|
| 504 |
+
analytics = len(load_user_data(uid, 'analytics'))
|
| 505 |
+
|
| 506 |
+
# estimate created_at by file mtime if available
|
| 507 |
+
created_at = None
|
| 508 |
+
try:
|
| 509 |
+
path_candidates = [
|
| 510 |
+
os.path.join(DATA_ROOT, 'flashcards', f"{uid}.json"),
|
| 511 |
+
os.path.join(DATA_ROOT, 'conversations', f"{uid}.json"),
|
| 512 |
+
os.path.join(DATA_ROOT, 'analytics', f"{uid}.json")
|
| 513 |
+
]
|
| 514 |
+
mtimes = []
|
| 515 |
+
for p in path_candidates:
|
| 516 |
+
if os.path.exists(p):
|
| 517 |
+
mtimes.append(os.path.getmtime(p))
|
| 518 |
+
if mtimes:
|
| 519 |
+
created_at = datetime.fromtimestamp(min(mtimes)).isoformat()
|
| 520 |
+
except Exception:
|
| 521 |
+
created_at = None
|
| 522 |
+
|
| 523 |
+
users_page.append({
|
| 524 |
+
'id': uid,
|
| 525 |
+
'email': uid,
|
| 526 |
+
'created_at': created_at,
|
| 527 |
+
'flashcard_count': flashcards,
|
| 528 |
+
'conversation_count': conversations,
|
| 529 |
+
'session_count': analytics
|
| 530 |
+
})
|
| 531 |
+
|
| 532 |
+
return jsonify({'success': True, 'data': {'users': users_page, 'page': page, 'per_page': per_page, 'total': total, 'total_pages': total_pages}})
|
| 533 |
+
except Exception as e:
|
| 534 |
+
print(f"admin_list_users error: {e}")
|
| 535 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
@app.route('/admin/users/<path:user_id>', methods=['GET', 'DELETE'])
|
| 539 |
+
def admin_user_detail(user_id):
|
| 540 |
+
"""Get detailed info for a user or delete their data (file-based)."""
|
| 541 |
+
try:
|
| 542 |
+
if request.method == 'DELETE':
|
| 543 |
+
# remove files across hf_data
|
| 544 |
+
removed = []
|
| 545 |
+
for folder in ['flashcards', 'conversations', 'analytics']:
|
| 546 |
+
p = os.path.join(DATA_ROOT, folder, f"{user_id}.json")
|
| 547 |
+
try:
|
| 548 |
+
if os.path.exists(p):
|
| 549 |
+
os.remove(p)
|
| 550 |
+
removed.append(p)
|
| 551 |
+
except Exception as ex:
|
| 552 |
+
print(f"Failed deleting {p}: {ex}")
|
| 553 |
+
return jsonify({'success': True, 'deleted': removed})
|
| 554 |
+
|
| 555 |
+
# GET -> return analytics, flashcards, conversations
|
| 556 |
+
flashcards = load_user_data(user_id, 'flashcards')
|
| 557 |
+
conversations = load_user_data(user_id, 'conversations')
|
| 558 |
+
analytics = load_user_data(user_id, 'analytics')
|
| 559 |
+
|
| 560 |
+
# Build token_usage overview if present in analytics entries
|
| 561 |
+
token_usage = {}
|
| 562 |
+
for entry in analytics:
|
| 563 |
+
if isinstance(entry, dict) and 'token_usage' in entry:
|
| 564 |
+
for prov, usage in entry['token_usage'].items():
|
| 565 |
+
s = token_usage.setdefault(prov, {'input': 0, 'output': 0, 'calls': 0})
|
| 566 |
+
s['input'] += usage.get('input_tokens', 0)
|
| 567 |
+
s['output'] += usage.get('output_tokens', 0)
|
| 568 |
+
s['calls'] += 1
|
| 569 |
+
|
| 570 |
+
user_info = {
|
| 571 |
+
'user': {'id': user_id, 'email': user_id, 'created_at': None},
|
| 572 |
+
'flashcards': flashcards,
|
| 573 |
+
'conversations': conversations,
|
| 574 |
+
'recent_sessions': analytics[-10:] if analytics else [],
|
| 575 |
+
'settings': {},
|
| 576 |
+
'token_usage': [{'provider': k, 'input_tokens': v['input'], 'output_tokens': v['output'], 'calls': v['calls']} for k, v in token_usage.items()]
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
return jsonify({'success': True, 'user': user_info})
|
| 580 |
+
except Exception as e:
|
| 581 |
+
print(f"admin_user_detail error: {e}")
|
| 582 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 583 |
+
|
| 584 |
+
|
| 585 |
+
@app.route('/admin/system/alerts', methods=['GET'])
|
| 586 |
+
def admin_system_alerts():
|
| 587 |
+
"""Return current system alerts (file-based: empty by default)."""
|
| 588 |
+
# In HF file-based mode we have no centralized alerting - return empty list
|
| 589 |
+
return jsonify({'success': True, 'alerts': []})
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
@app.route('/admin/export/users', methods=['GET'])
|
| 593 |
+
def admin_export_users():
|
| 594 |
+
"""Export users list as CSV (file-based)."""
|
| 595 |
+
try:
|
| 596 |
+
import csv
|
| 597 |
+
users = get_all_users()
|
| 598 |
+
output = io.StringIO()
|
| 599 |
+
writer = csv.writer(output)
|
| 600 |
+
writer.writerow(['id', 'email', 'flashcards', 'conversations', 'analytics'])
|
| 601 |
+
for uid in users:
|
| 602 |
+
fc = len(load_user_data(uid, 'flashcards'))
|
| 603 |
+
conv = len(load_user_data(uid, 'conversations'))
|
| 604 |
+
an = len(load_user_data(uid, 'analytics'))
|
| 605 |
+
writer.writerow([uid, uid, fc, conv, an])
|
| 606 |
+
|
| 607 |
+
mem = io.BytesIO(output.getvalue().encode('utf-8'))
|
| 608 |
+
mem.seek(0)
|
| 609 |
+
return send_file(mem, mimetype='text/csv', as_attachment=True, download_name='users_export.csv')
|
| 610 |
+
except Exception as e:
|
| 611 |
+
print(f"export users error: {e}")
|
| 612 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
@app.route('/admin/export/tokens', methods=['GET'])
|
| 616 |
+
def admin_export_tokens():
|
| 617 |
+
"""Export token usage summary as CSV (aggregated from analytics)."""
|
| 618 |
+
try:
|
| 619 |
+
import csv
|
| 620 |
+
users = get_all_users()
|
| 621 |
+
output = io.StringIO()
|
| 622 |
+
writer = csv.writer(output)
|
| 623 |
+
writer.writerow(['user_id', 'provider', 'input_tokens', 'output_tokens', 'calls'])
|
| 624 |
+
|
| 625 |
+
for uid in users:
|
| 626 |
+
analytics = load_user_data(uid, 'analytics')
|
| 627 |
+
agg = {}
|
| 628 |
+
for entry in analytics:
|
| 629 |
+
if isinstance(entry, dict) and 'token_usage' in entry:
|
| 630 |
+
for prov, usage in entry['token_usage'].items():
|
| 631 |
+
a = agg.setdefault(prov, {'input': 0, 'output': 0, 'calls': 0})
|
| 632 |
+
a['input'] += usage.get('input_tokens', 0)
|
| 633 |
+
a['output'] += usage.get('output_tokens', 0)
|
| 634 |
+
a['calls'] += 1
|
| 635 |
+
for prov, vals in agg.items():
|
| 636 |
+
writer.writerow([uid, prov, vals['input'], vals['output'], vals['calls']])
|
| 637 |
+
|
| 638 |
+
mem = io.BytesIO(output.getvalue().encode('utf-8'))
|
| 639 |
+
mem.seek(0)
|
| 640 |
+
return send_file(mem, mimetype='text/csv', as_attachment=True, download_name='tokens_export.csv')
|
| 641 |
+
except Exception as e:
|
| 642 |
+
print(f"export tokens error: {e}")
|
| 643 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
@app.route('/admin/export/all', methods=['GET'])
|
| 647 |
+
def admin_export_all():
|
| 648 |
+
"""Package the entire DATA_ROOT into a zip and send for download."""
|
| 649 |
+
try:
|
| 650 |
+
import zipfile
|
| 651 |
+
import tempfile
|
| 652 |
+
|
| 653 |
+
if not os.path.exists(DATA_ROOT):
|
| 654 |
+
return jsonify({'success': False, 'error': 'No data directory found'}), 404
|
| 655 |
+
|
| 656 |
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
|
| 657 |
+
tmp.close()
|
| 658 |
+
|
| 659 |
+
with zipfile.ZipFile(tmp.name, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
| 660 |
+
for root, dirs, files in os.walk(DATA_ROOT):
|
| 661 |
+
for f in files:
|
| 662 |
+
full = os.path.join(root, f)
|
| 663 |
+
arcname = os.path.relpath(full, DATA_ROOT)
|
| 664 |
+
zf.write(full, arcname)
|
| 665 |
+
|
| 666 |
+
return send_file(tmp.name, mimetype='application/zip', as_attachment=True, download_name='hf_data_export.zip')
|
| 667 |
+
except Exception as e:
|
| 668 |
+
print(f"export all error: {e}")
|
| 669 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
@app.route('/admin/dashboard', methods=['GET'])
|
| 673 |
+
def admin_dashboard_compat():
|
| 674 |
+
"""Compatibility endpoint for older admin UI that expects /admin/dashboard."""
|
| 675 |
+
try:
|
| 676 |
+
stats = admin_stats() # reuse existing
|
| 677 |
+
return jsonify({'success': True, 'stats': stats.get_json() if isinstance(stats, Response) else stats})
|
| 678 |
+
except Exception as e:
|
| 679 |
+
print(f"admin_dashboard error: {e}")
|
| 680 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 681 |
+
|
| 682 |
from study_plan import save_study_plan, load_study_plan, generate_study_plan
|
| 683 |
|
| 684 |
@app.route('/study-plan', methods=['GET', 'POST'])
|
|
|
|
| 687 |
if request.method == 'POST':
|
| 688 |
try:
|
| 689 |
data = request.get_json()
|
| 690 |
+
user_id = data.get('user_id')
|
| 691 |
plan = generate_study_plan(data)
|
| 692 |
+
# save globally
|
| 693 |
save_study_plan(plan)
|
| 694 |
+
# also save per-user if provided
|
| 695 |
+
if user_id:
|
| 696 |
+
save_user_data(user_id, 'study_plans', plan)
|
| 697 |
return jsonify({'success': True, 'message': 'Study plan generated and saved', 'plan': plan})
|
| 698 |
except Exception as e:
|
| 699 |
print(f"Erro ao salvar study plan: {e}")
|
study_plan.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import logging
|
|
|
|
| 2 |
try:
|
| 3 |
from groq import Groq
|
| 4 |
except ImportError:
|
|
@@ -181,23 +182,25 @@ def generate_study_plan(user_data):
|
|
| 181 |
'study_goals': study_goals
|
| 182 |
}
|
| 183 |
return plan
|
| 184 |
-
import os
|
| 185 |
import json
|
| 186 |
from datetime import datetime
|
| 187 |
|
| 188 |
-
|
|
|
|
| 189 |
|
| 190 |
def save_study_plan(plan_data):
|
| 191 |
-
"""
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
def load_study_plan():
|
| 199 |
-
"""
|
| 200 |
-
|
| 201 |
-
with open(STUDY_PLAN_PATH, 'r', encoding='utf-8') as f:
|
| 202 |
-
return json.load(f)
|
| 203 |
-
return None
|
|
|
|
| 1 |
import logging
|
| 2 |
+
import os
|
| 3 |
try:
|
| 4 |
from groq import Groq
|
| 5 |
except ImportError:
|
|
|
|
| 182 |
'study_goals': study_goals
|
| 183 |
}
|
| 184 |
return plan
|
|
|
|
| 185 |
import json
|
| 186 |
from datetime import datetime
|
| 187 |
|
| 188 |
+
# In-memory study plan storage (testing only)
|
| 189 |
+
_MEM_STUDY_PLAN = None
|
| 190 |
|
| 191 |
def save_study_plan(plan_data):
|
| 192 |
+
"""Store the study plan in memory (no disk IO)."""
|
| 193 |
+
global _MEM_STUDY_PLAN
|
| 194 |
+
try:
|
| 195 |
+
if isinstance(plan_data, dict):
|
| 196 |
+
plan_data = dict(plan_data)
|
| 197 |
+
plan_data['saved_at'] = datetime.now().isoformat()
|
| 198 |
+
_MEM_STUDY_PLAN = plan_data
|
| 199 |
+
return True
|
| 200 |
+
except Exception as e:
|
| 201 |
+
logger.warning(f"Failed to save study plan in-memory: {e}")
|
| 202 |
+
return False
|
| 203 |
|
| 204 |
def load_study_plan():
|
| 205 |
+
"""Load the study plan from in-memory storage."""
|
| 206 |
+
return _MEM_STUDY_PLAN
|
|
|
|
|
|
|
|
|