amauricunha commited on
Commit
be3ddee
·
verified ·
1 Parent(s): 879d451

Upload 5 files

Browse files
Files changed (3) hide show
  1. README.md +51 -52
  2. flask_app_hf.py +393 -44
  3. 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
- sdk_version: 5.49.1
11
- ---
12
-
13
- # English Helper - HF Spaces
14
-
15
- Sistema simplificado de aprendizado de inglês para Hugging Face Spaces.
16
-
17
- ## 🚀 Funcionalidades
18
-
19
- - **Seleção de Usuário**: Escolha ou crie usuário (sem autenticação)
20
- - **Text-to-Speech**: Áudio dos textos com gTTS
21
- - **Flashcards**: Criação automática com IA
22
- - **Chat**: Conversação básica
23
- - **Admin Panel**: Gestão de usuários em `/admin`
24
- - **Status Page**: Monitoramento em `/status`
25
-
26
- ## 🏗️ Arquitetura Simplificada
27
-
28
- - **Storage**: Arquivos JSON em `hf_data/`
29
- - **No Database**: Sistema baseado em arquivos
30
- - **No Authentication**: Seleção de usuário apenas
31
- - **Minimal Dependencies**: Flask + requests + gTTS
32
-
33
- ## 📁 Estrutura
34
-
35
- ```
36
- app.py # Entry point HF
37
- flask_app_hf.py # Flask simplificado
38
- requirements.txt # Dependências mínimas
39
- templates/ # Interface HTML
40
- ├── index.html # Interface principal
41
- ── admin.html # Painel admin
42
- └── status.html # Status do sistema
43
- ```
44
-
45
- ## 🎯 Deploy
46
-
47
- 1. Faça upload de todos os arquivos desta pasta para seu HF Space
48
- 2. O app roda automaticamente na porta 7860
49
- 3. Acesse a interface principal, admin (/admin) e status (/status)
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
- from gtts import gTTS
9
- from groq import Groq
10
- import google.generativeai as genai
11
- from google.generativeai.types import GenerationConfig
 
 
 
 
 
 
 
 
 
 
 
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 arquivos JSON"""
27
  try:
28
- user_dir = f"hf_data/{data_type}"
29
- os.makedirs(user_dir, exist_ok=True)
30
-
31
- file_path = f"{user_dir}/{user_id}.json"
32
-
33
- # Carregar dados existentes
34
- existing_data = []
35
- if os.path.exists(file_path):
36
- with open(file_path, 'r', encoding='utf-8') as f:
37
- existing_data = json.load(f)
38
-
39
- # Adicionar novos dados
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
- """Carregar dados do usuário"""
58
  try:
59
- file_path = f"hf_data/{data_type}/{user_id}.json"
60
- if os.path.exists(file_path):
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
- """Obter lista de todos os usuários"""
70
  users = set()
71
- for data_type in ['flashcards', 'conversations', 'analytics']:
72
- data_dir = f"hf_data/{data_type}"
73
- if os.path.exists(data_dir):
74
- for filename in os.listdir(data_dir):
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
- STUDY_PLAN_PATH = 'hf_data/study_plan.json'
 
189
 
190
  def save_study_plan(plan_data):
191
- """Salva o plano de estudos em JSON."""
192
- os.makedirs(os.path.dirname(STUDY_PLAN_PATH), exist_ok=True)
193
- plan_data['saved_at'] = datetime.now().isoformat()
194
- with open(STUDY_PLAN_PATH, 'w', encoding='utf-8') as f:
195
- json.dump(plan_data, f, ensure_ascii=False, indent=2)
196
- return True
 
 
 
 
 
197
 
198
  def load_study_plan():
199
- """Carrega o plano de estudos do JSON."""
200
- if os.path.exists(STUDY_PLAN_PATH):
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