|
|
| from flask import Flask, render_template, request, jsonify, send_from_directory
|
| from datetime import datetime, timedelta
|
| import json
|
| import os
|
| import numpy as np
|
| from pathlib import Path
|
|
|
| app = Flask(__name__)
|
|
|
|
|
|
|
|
|
| BASE_DIR = Path(__file__).parent
|
| DATA_DIR = BASE_DIR / "data"
|
| DATA_DIR.mkdir(exist_ok=True)
|
|
|
|
|
| COMPETENCES_FILE = BASE_DIR / "competences.json"
|
| JOURNAL_FILE = DATA_DIR / "journal.json"
|
| PLANNER_FILE = DATA_DIR / "planner.json"
|
| LEARNER_STATE_FILE = DATA_DIR / "learner_state.json"
|
|
|
|
|
|
|
|
|
|
|
| def load_json_file(file_path, default_data):
|
| """Charge un fichier JSON ou retourne les données par défaut"""
|
| try:
|
| if file_path.exists():
|
| with open(file_path, 'r', encoding='utf-8') as f:
|
| return json.load(f)
|
| except:
|
| pass
|
| return default_data
|
|
|
| def save_json_file(file_path, data):
|
| """Sauvegarde des données dans un fichier JSON"""
|
| try:
|
| with open(file_path, 'w', encoding='utf-8') as f:
|
| json.dump(data, f, ensure_ascii=False, indent=2)
|
| return True
|
| except:
|
| return False
|
|
|
|
|
| COMPETENCES = load_json_file(COMPETENCES_FILE, [
|
| {"id": "D-MCONJ", "label": "Marques de la conjugaison", "category": "Discours"},
|
| {"id": "M-COMP", "label": "Construction des phrases", "category": "Maîtrise"},
|
| {"id": "P-GRAM", "label": "Orthographe grammaticale", "category": "Phrase"},
|
| {"id": "T-ORG", "label": "Organisation textuelle", "category": "Texte"}
|
| ])
|
|
|
| JOURNAL = load_json_file(JOURNAL_FILE, [])
|
| PLANNER = load_json_file(PLANNER_FILE, [])
|
| LEARNER_STATE = load_json_file(LEARNER_STATE_FILE, {
|
| "retention": {},
|
| "engagement": "stable",
|
| "risk": "low",
|
| "days_inactive": 0,
|
| "total_sessions": 0,
|
| "success_rate": 0.0
|
| })
|
|
|
|
|
| SM2_INTERVALS = [1, 3, 7, 14, 30, 60]
|
|
|
|
|
|
|
|
|
|
|
| def get_reminder_for_quality(quality_level):
|
| """Retourne un rappel adapté au niveau de qualité"""
|
| reminders = [
|
| "🧠 Révision fréquente conseillée pour renforcer la mémoire",
|
| "📚 Répétition espacée standard recommandée",
|
| "🚀 Excellente progression, continuez ainsi !",
|
| "🏆 Niveau expert, rétention à long terme optimale"
|
| ]
|
| return reminders[min(quality_level, 3)]
|
|
|
| def get_intervals_for_quality(quality_level):
|
| """Retourne les intervalles adaptés au niveau de qualité"""
|
| quality_multipliers = [0.7, 1.0, 1.5, 2.0]
|
| multiplier = quality_multipliers[min(quality_level, 3)]
|
| return [int(interval * multiplier) for interval in SM2_INTERVALS]
|
|
|
|
|
|
|
|
|
|
|
| @app.route('/')
|
| def home():
|
| """Page d'accueil"""
|
| return render_template('index.html', competences=COMPETENCES)
|
|
|
| @app.route('/api/competences')
|
| def get_competences():
|
| return jsonify(COMPETENCES)
|
|
|
| @app.route('/api/planner/visualize')
|
| def visualize_planner():
|
| """Génère les données de visualisation pour la courbe SM-2"""
|
| try:
|
| competence = request.args.get('competence', 'Compétence')
|
| quality = int(request.args.get('quality', 1))
|
|
|
| intervals = get_intervals_for_quality(quality)
|
|
|
| return jsonify({
|
| 'success': True,
|
| 'intervals': intervals,
|
| 'competence': competence,
|
| 'quality_level': quality,
|
| 'description': f'Courbe SM-2 adaptée (Niveau {quality})'
|
| })
|
| except Exception as e:
|
| return jsonify({'success': False, 'error': str(e)})
|
|
|
| @app.route('/api/planner/generate', methods=['POST'])
|
| def generate_planner():
|
| try:
|
| data = request.json
|
| competence = data.get('competence', 'Compétence')
|
| start_date = datetime.strptime(data.get('start_date', '2026-01-01'), '%Y-%m-%d')
|
| duration = int(data.get('session_duration', 20))
|
| repetitions = int(data.get('repetitions', 6))
|
| quality_level = int(data.get('quality_level', 1))
|
|
|
|
|
| adjusted_intervals = get_intervals_for_quality(quality_level)
|
|
|
| plan = []
|
| current_date = start_date
|
|
|
| for i in range(min(repetitions, len(adjusted_intervals))):
|
| plan.append({
|
| 'date': current_date.strftime('%Y-%m-%d'),
|
| 'jour': current_date.strftime('%A'),
|
| 'competence': competence,
|
| 'repetition': i + 1,
|
| 'duree': f'{duration} minutes',
|
| 'intervalle': f'+{adjusted_intervals[i]} jours',
|
| 'type': 'Apprentissage' if i == 0 else 'Révision espacée',
|
| 'strategie': 'Active Recall + Élaboration',
|
| 'reminder': get_reminder_for_quality(quality_level)
|
| })
|
| current_date += timedelta(days=adjusted_intervals[i])
|
|
|
|
|
| PLANNER.extend(plan)
|
| save_json_file(PLANNER_FILE, PLANNER)
|
|
|
| return jsonify({
|
| 'success': True,
|
| 'plan': plan,
|
| 'intervals': adjusted_intervals,
|
| 'quality_level': quality_level
|
| })
|
| except Exception as e:
|
| return jsonify({'success': False, 'error': str(e)})
|
|
|
| @app.route('/api/journal', methods=['POST'])
|
| def save_journal():
|
| try:
|
| entry = request.json
|
| entry['timestamp'] = datetime.now().isoformat()
|
| entry['date'] = datetime.now().strftime('%Y-%m-%d %H:%M')
|
|
|
| JOURNAL.append(entry)
|
| save_json_file(JOURNAL_FILE, JOURNAL)
|
|
|
|
|
| if 'auto_eval' in entry:
|
| try:
|
| eval_score = float(entry['auto_eval'])
|
| LEARNER_STATE['total_sessions'] += 1
|
| LEARNER_STATE['success_rate'] = (
|
| (LEARNER_STATE['success_rate'] * (LEARNER_STATE['total_sessions'] - 1) + eval_score)
|
| / LEARNER_STATE['total_sessions']
|
| )
|
| save_json_file(LEARNER_STATE_FILE, LEARNER_STATE)
|
| except:
|
| pass
|
|
|
| return jsonify({'success': True, 'message': 'Journal sauvegardé'})
|
| except Exception as e:
|
| return jsonify({'success': False, 'error': str(e)})
|
|
|
| @app.route('/api/journal/entries')
|
| def get_journal_entries():
|
| """Récupère les dernières entrées du journal"""
|
| limit = int(request.args.get('limit', 5))
|
| entries = JOURNAL[-limit:] if len(JOURNAL) > limit else JOURNAL
|
| return jsonify(entries)
|
|
|
| @app.route('/api/notifications')
|
| def get_notification():
|
| messages = [
|
| "🌱 5 minutes aujourd'hui renforcent durablement votre mémoire.",
|
| "⏰ Une courte révision maintenant évite l'oubli.",
|
| "💪 Votre régularité montre une vraie progression.",
|
| "🤔 Quelle stratégie vous aide le plus aujourd'hui ?"
|
| ]
|
|
|
| message = np.random.choice(messages)
|
|
|
| return jsonify({
|
| 'message': message,
|
| 'timestamp': datetime.now().isoformat(),
|
| 'type': 'reminder'
|
| })
|
|
|
| @app.route('/api/notifications/intelligent')
|
| def get_intelligent_notification():
|
| """Notifications plus élaborées"""
|
| notifications = [
|
| {
|
| 'message': "🌱 5 minutes aujourd'hui renforcent durablement votre mémoire.",
|
| 'type': 'motivation',
|
| 'effectiveness_score': 0.85
|
| },
|
| {
|
| 'message': "⏰ Une courte révision maintenant évite l'oubli.",
|
| 'type': 'rappel_court',
|
| 'effectiveness_score': 0.92
|
| },
|
| {
|
| 'message': "💪 Votre régularité montre une vraie progression.",
|
| 'type': 'encouragement',
|
| 'effectiveness_score': 0.78
|
| },
|
| {
|
| 'message': "🤔 Quelle stratégie vous aide le plus aujourd'hui ?",
|
| 'type': 'metacognition',
|
| 'effectiveness_score': 0.88
|
| }
|
| ]
|
|
|
| notification = np.random.choice(notifications)
|
| notification['timestamp'] = datetime.now().isoformat()
|
|
|
| return jsonify(notification)
|
|
|
| @app.route('/api/stats')
|
| def get_stats():
|
| return jsonify({
|
| 'total_sessions': LEARNER_STATE.get('total_sessions', 0),
|
| 'success_rate': round(LEARNER_STATE.get('success_rate', 0), 2),
|
| 'journal_entries': len(JOURNAL),
|
| 'planned_sessions': len(PLANNER),
|
| 'engagement': LEARNER_STATE.get('engagement', 'stable')
|
| })
|
|
|
| @app.route('/api/learner/stats')
|
| def get_learner_stats():
|
| """Statistiques détaillées de l'apprenant"""
|
| retention_data = LEARNER_STATE.get('retention', {})
|
|
|
|
|
| total_sessions = LEARNER_STATE.get('total_sessions', 0)
|
| if total_sessions > 20:
|
| engagement = 'high'
|
| elif total_sessions > 10:
|
| engagement = 'medium'
|
| else:
|
| engagement = 'low'
|
|
|
|
|
| days_inactive = LEARNER_STATE.get('days_inactive', 0)
|
| if days_inactive > 7:
|
| risk = 'high'
|
| elif days_inactive > 3:
|
| risk = 'medium'
|
| else:
|
| risk = 'low'
|
|
|
| return jsonify({
|
| 'total_sessions': total_sessions,
|
| 'success_rate': round(LEARNER_STATE.get('success_rate', 0), 2),
|
| 'journal_entries': len(JOURNAL),
|
| 'planned_sessions': len(PLANNER),
|
| 'engagement_level': engagement,
|
| 'risk_level': risk,
|
| 'days_inactive': days_inactive,
|
| 'retention_rates': retention_data,
|
| 'notification_effectiveness': {
|
| 'motivation': 0.85,
|
| 'rappel_court': 0.92,
|
| 'encouragement': 0.78,
|
| 'metacognition': 0.88
|
| }
|
| })
|
|
|
| @app.route('/api/sm2/explain')
|
| def explain_sm2():
|
| return jsonify({
|
| 'title': 'Algorithme SM-2',
|
| 'description': 'Répétition espacée optimisée pour la rétention mémoire',
|
| 'quality_levels': {
|
| '0': 'Difficulté - Révisions fréquentes nécessaires',
|
| '1': 'Moyen - Intervalles standard',
|
| '2': 'Bon - Consolidation progressive',
|
| '3': 'Excellent - Rétention long terme'
|
| },
|
| 'intervals': SM2_INTERVALS,
|
| 'principles': [
|
| 'Réviser juste avant d\'oublier',
|
| 'Augmenter l\'intervalle progressivement',
|
| 'Adapter selon la difficulté',
|
| 'Être régulier dans la pratique'
|
| ],
|
| 'recommendations': [
|
| 'Commencez avec des sessions courtes (10-20 minutes)',
|
| 'Augmentez la durée progressivement',
|
| 'Notez votre niveau de confiance après chaque session',
|
| 'Ajustez les intervalles selon vos résultats'
|
| ]
|
| })
|
|
|
|
|
|
|
|
|
|
|
| @app.route('/static/<path:filename>')
|
| def serve_static(filename):
|
| return send_from_directory('static', filename)
|
|
|
| @app.route('/favicon.ico')
|
| def favicon():
|
| return '', 204
|
|
|
|
|
|
|
|
|
|
|
| if __name__ == '__main__':
|
|
|
| port = int(os.environ.get('PORT', 7860))
|
| debug = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
|
|
|
| print(f"🚀 Démarrage de l'application SRL sur le port {port}")
|
| print(f"📚 Compétences chargées: {len(COMPETENCES)}")
|
| print(f"📔 Entrées journal: {len(JOURNAL)}")
|
| print(f"📅 Sessions planifiées: {len(PLANNER)}")
|
|
|
| app.run(
|
| host='0.0.0.0',
|
| port=port,
|
| debug=debug,
|
| use_reloader=False
|
| ) |