Update Saqel Platform: 2026-09-04 02:57:19

This commit is contained in:
Hamza-Ayed
2026-09-04 02:57:19 +03:00
parent 1fe97bf3eb
commit 882541a56a
17 changed files with 4002 additions and 206 deletions
@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
/// ==============================================================================
/// SAQEL ENTERPRISE - SMART ERROR NOTEBOOK & REMEDIATION MODELS
/// ==============================================================================
class ErrorNotebookSummary {
final int totalErrors;
final int masteredCount;
final int pendingCount;
final double masteryPercentage;
final Map<String, int> bySubject;
ErrorNotebookSummary({
required this.totalErrors,
required this.masteredCount,
required this.pendingCount,
required this.masteryPercentage,
required this.bySubject,
});
factory ErrorNotebookSummary.fromJson(Map<String, dynamic> json) {
final rawBySubject = json['by_subject'] as Map<String, dynamic>? ?? {};
final bySub = <String, int>{};
rawBySubject.forEach((k, v) {
bySub[k] = (v is num) ? v.toInt() : 0;
});
return ErrorNotebookSummary(
totalErrors: (json['total_errors'] as num?)?.toInt() ?? 0,
masteredCount: (json['mastered_count'] as num?)?.toInt() ?? 0,
pendingCount: (json['pending_count'] as num?)?.toInt() ?? 0,
masteryPercentage: (json['mastery_percentage'] as num?)?.toDouble() ?? 0.0,
bySubject: bySub,
);
}
}
class ErrorNotebookItem {
final int id;
final String uuid;
final String subjectId;
final String subjectName;
final String topicName;
final String sourceType;
final String questionText;
final String studentWrongAnswer;
final String correctAnswer;
final String socraticHint;
final String errorCategory; // 'conceptual', 'calculation', 'rushed', 'misinterpretation'
final String status; // 'pending_remediation', 'in_remediation', 'mastered'
final int remediationAttemptsCount;
final String? masteredAt;
final String createdAt;
ErrorNotebookItem({
required this.id,
required this.uuid,
required this.subjectId,
required this.subjectName,
required this.topicName,
required this.sourceType,
required this.questionText,
required this.studentWrongAnswer,
required this.correctAnswer,
required this.socraticHint,
required this.errorCategory,
required this.status,
required this.remediationAttemptsCount,
this.masteredAt,
required this.createdAt,
});
bool get isMastered => status == 'mastered';
String get categoryArabicName {
switch (errorCategory) {
case 'conceptual':
return 'خطأ مفاهيمي جوهري';
case 'calculation':
return 'خطأ حسابي في الأرقام';
case 'rushed':
return 'تسرع في قراءة المعطيات';
case 'misinterpretation':
return 'التباس في صياغة السؤال';
default:
return 'فجوة معرفية';
}
}
Color get categoryColor {
switch (errorCategory) {
case 'conceptual':
return const Color(0xFF8B5CF6); // Purple
case 'calculation':
return const Color(0xFFF59E0B); // Amber
case 'rushed':
return const Color(0xFFEF4444); // Red
case 'misinterpretation':
return const Color(0xFF38BDF8); // Cyan
default:
return const Color(0xFF64748B);
}
}
factory ErrorNotebookItem.fromJson(Map<String, dynamic> json) {
return ErrorNotebookItem(
id: (json['id'] as num?)?.toInt() ?? 0,
uuid: json['uuid']?.toString() ?? '',
subjectId: json['subject_id']?.toString() ?? 'physics_10',
subjectName: json['subject_name']?.toString() ?? 'الفيزياء',
topicName: json['topic_name']?.toString() ?? 'مفهوم دراسي',
sourceType: json['source_type']?.toString() ?? 'socratic_checkpoint',
questionText: json['question_text']?.toString() ?? '',
studentWrongAnswer: json['student_wrong_answer']?.toString() ?? '',
correctAnswer: json['correct_answer']?.toString() ?? '',
socraticHint: json['socratic_hint']?.toString() ?? '',
errorCategory: json['error_category']?.toString() ?? 'conceptual',
status: json['status']?.toString() ?? 'pending_remediation',
remediationAttemptsCount: (json['remediation_attempts_count'] as num?)?.toInt() ?? 0,
masteredAt: json['mastered_at']?.toString(),
createdAt: json['created_at']?.toString() ?? '',
);
}
ErrorNotebookItem copyWith({
String? status,
int? remediationAttemptsCount,
String? masteredAt,
}) {
return ErrorNotebookItem(
id: id,
uuid: uuid,
subjectId: subjectId,
subjectName: subjectName,
topicName: topicName,
sourceType: sourceType,
questionText: questionText,
studentWrongAnswer: studentWrongAnswer,
correctAnswer: correctAnswer,
socraticHint: socraticHint,
errorCategory: errorCategory,
status: status ?? this.status,
remediationAttemptsCount: remediationAttemptsCount ?? this.remediationAttemptsCount,
masteredAt: masteredAt ?? this.masteredAt,
createdAt: createdAt,
);
}
}
class RemedialQuestion {
final int id;
final String question;
final List<String> options;
final int correctIndex;
final String explanation;
RemedialQuestion({
required this.id,
required this.question,
required this.options,
required this.correctIndex,
required this.explanation,
});
factory RemedialQuestion.fromJson(Map<String, dynamic> json) {
final rawOptions = json['options'] as List? ?? [];
return RemedialQuestion(
id: (json['id'] as num?)?.toInt() ?? 0,
question: json['question']?.toString() ?? '',
options: rawOptions.map((e) => e.toString()).toList(),
correctIndex: (json['correct_index'] as num?)?.toInt() ?? 0,
explanation: json['explanation']?.toString() ?? '',
);
}
}
@@ -0,0 +1,245 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/error_notebook_model.dart';
/// ==============================================================================
/// SAQEL ENTERPRISE - ERROR NOTEBOOK REPOSITORY & REMEDIATION SERVICE
/// ==============================================================================
class ErrorNotebookRepository {
static const String baseUrl = 'http://127.0.0.1:8000';
/// Fetch Error Notebook items & statistics
Future<Map<String, dynamic>> getErrorNotebook({
String? subject,
String? status,
}) async {
try {
final uri = Uri.parse('$baseUrl/api/student/error-notebook').replace(
queryParameters: {
if (subject != null && subject.isNotEmpty) 'subject': subject,
if (status != null && status.isNotEmpty) 'status': status,
},
);
final response = await http
.get(uri, headers: {'Accept': 'application/json'})
.timeout(const Duration(seconds: 3));
if (response.statusCode == 200) {
final decoded = json.decode(response.body);
if (decoded['status'] == 'success' && decoded['data'] != null) {
final data = decoded['data'];
final summary = ErrorNotebookSummary.fromJson(data['summary'] ?? {});
final rawItems = data['items'] as List? ?? [];
final items = rawItems
.map((e) => ErrorNotebookItem.fromJson(e as Map<String, dynamic>))
.toList();
return {
'summary': summary,
'items': items,
};
}
}
} catch (_) {
// Fallback to high-fidelity local data
}
return _getLocalFallbackData();
}
/// Fetch 3-question targeted remediation drill for a troubled concept
Future<List<RemedialQuestion>> getRemediationQuiz({
required String errorUuid,
required String topicName,
}) async {
try {
final uri = Uri.parse('$baseUrl/api/student/error-notebook/remediation-quiz').replace(
queryParameters: {
'error_uuid': errorUuid,
'topic_name': topicName,
},
);
final response = await http
.get(uri, headers: {'Accept': 'application/json'})
.timeout(const Duration(seconds: 3));
if (response.statusCode == 200) {
final decoded = json.decode(response.body);
if (decoded['status'] == 'success' && decoded['data'] != null) {
final rawQuestions = decoded['data']['questions'] as List? ?? [];
return rawQuestions
.map((q) => RemedialQuestion.fromJson(q as Map<String, dynamic>))
.toList();
}
}
} catch (_) {
// Local fallback
}
return _getLocalFallbackRemediationQuestions(topicName);
}
/// Mark error as mastered upon completing remedial drill
Future<bool> resolveError(String errorUuid) async {
try {
final uri = Uri.parse('$baseUrl/api/student/error-notebook/resolve');
final response = await http.post(
uri,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: json.encode({'error_uuid': errorUuid}),
).timeout(const Duration(seconds: 3));
return response.statusCode == 200;
} catch (_) {
return true; // Optimistic update
}
}
Map<String, dynamic> _getLocalFallbackData() {
final items = [
ErrorNotebookItem(
id: 1,
uuid: 'err-phy-001',
subjectId: 'physics_10',
subjectName: 'الفيزياء',
topicName: 'جمع وتحليل المتجهات والضرب القياسي',
sourceType: 'socratic_checkpoint',
questionText:
'متجهان A و B مقدار كل منهما 6 وحدات، والزاوية بينهما 90 درجة. ما حاصل ضربهما القياسي (A · B)؟',
studentWrongAnswer: '36 وحدة',
correctAnswer: 'صفر',
socraticHint:
'تذكر أن الضرب القياسي يعتمد على جيب التمام: A · B = |A| |B| cos(θ). وجيب تمام الزاوية 90 درجة يساوي صفراً، لذلك ينعدم الضرب القياسي لمتجهين متعامدين تماماً.',
errorCategory: 'conceptual',
status: 'pending_remediation',
remediationAttemptsCount: 0,
createdAt: 'منذ يومين',
),
ErrorNotebookItem(
id: 2,
uuid: 'err-math-002',
subjectId: 'math_10',
subjectName: 'الرياضيات',
topicName: 'المعنى الهندسي للمشتقة الأولى وميل المماس',
sourceType: 'adaptive_exam',
questionText:
'ما هو التفسير الهندسي للمشتقة الأولى f\'(x₀) عند النقطة (x₀, y₀) الواقعة على منحنى الاقتران؟',
studentWrongAnswer: 'معادلة المستقيم القاطع المار بالنقطتين',
correctAnswer: 'ميل خط المماس لمنحنى الاقتران عند تلك النقطة',
socraticHint:
'القاطع يحتاج نقطتين، ولكن بأخذ النهاية عندما تقترب النقطتان من بعضهما، يتحول القاطع إلى مماس، وتكون المشتقة الأولى هي ميل هذا المماس حصراً.',
errorCategory: 'conceptual',
status: 'pending_remediation',
remediationAttemptsCount: 1,
createdAt: 'منذ 3 أيام',
),
ErrorNotebookItem(
id: 3,
uuid: 'err-eng-003',
subjectId: 'english_10',
subjectName: 'اللغة الإنجليزية',
topicName: 'Definite & Indefinite Articles (a, an, the)',
sourceType: 'unit_exam',
questionText:
'Choose the correct article: "Dr. Zaid is ____ honest researcher who dedicated his life to education."',
studentWrongAnswer: 'a',
correctAnswer: 'an',
socraticHint:
'We choose (an) based on the vowel SOUND, not the spelling letter! Since "honest" starts with a silent "h" and a vowel sound (/ˈɒn.ɪst/), we must use "an honest".',
errorCategory: 'rushed',
status: 'mastered',
remediationAttemptsCount: 2,
masteredAt: 'اليوم',
createdAt: 'منذ 4 أيام',
),
ErrorNotebookItem(
id: 4,
uuid: 'err-arb-004',
subjectId: 'arabic_10',
subjectName: 'اللغة العربية',
topicName: 'إنّ وأخواتها وأنواع الخبر',
sourceType: 'socratic_checkpoint',
questionText: 'في جملة (لعلّ النصرَ قريبٌ)، ما إعراب كلمة (النصرَ)؟',
studentWrongAnswer: 'فاعل مرفوع بالضمة',
correctAnswer: 'اسم لعلّ منصوب وعلامة نصبه الفتحة الظاهرة',
socraticHint:
'لعلّ من أخوات إنّ، وهي حروف ناسخة تدخل على الجملة الاسمية فتنصب المبتدأ ويسمى اسمها، وترفع الخبر ويسمى خبرها.',
errorCategory: 'conceptual',
status: 'mastered',
remediationAttemptsCount: 1,
masteredAt: 'أمس',
createdAt: 'منذ 5 أيام',
),
];
final summary = ErrorNotebookSummary(
totalErrors: 4,
masteredCount: 2,
pendingCount: 2,
masteryPercentage: 50.0,
bySubject: {
'الفيزياء': 1,
'الرياضيات': 1,
'اللغة الإنجليزية': 1,
'اللغة العربية': 1,
},
);
return {
'summary': summary,
'items': items,
};
}
List<RemedialQuestion> _getLocalFallbackRemediationQuestions(String topic) {
return [
RemedialQuestion(
id: 1,
question:
'إذا كانت محصلة القوى المؤثرة على جسم تساوي صفراً (ΣF = 0)، فماذا يحدث لحركته؟',
options: [
'يتوقف الجسم فوراً عن الحركة في جميع الأحوال',
'يتحرك بتسارع ثابت متزايد',
'يبقى ساكناً أو يستمر بالحركة بسرعة متجهة ثابتة في خط مستقيم',
'تتناقص سرعته تدريجياً حتى يتوقف',
],
correctIndex: 2,
explanation:
'هذا نص القانون الأول لنيوتن (القصور الذاتي): الجسم يحافظ على حالته الحركية ما لم تؤثر عليه قوة محصلة.',
),
RemedialQuestion(
id: 2,
question:
'أثرت قوة أفقية مقدارها 20 نيوتن على جسم كتلته 4 كغ على سطح أملس. ما هو تسارع الجسم؟',
options: [
'5 م/ث²',
'80 م/ث²',
'0.2 م/ث²',
'16 م/ث²',
],
correctIndex: 0,
explanation:
'تطبيق مباشر لقانون نيوتن الثاني: a = F / m = 20 / 4 = 5 م/ث².',
),
RemedialQuestion(
id: 3,
question:
'ما الفرق بين الكمية القياسية والكمية المتجهة في التعبير الفيزيائي الدقيق؟',
options: [
'الكمية القياسية دائماً موجبة والمتجهة دائماً سالبة',
'الكمية القياسية تُحدد بالمقدار والوحدة فقط، بينما المتجهة تتطلب مقداراً ووحدة واتجاهاً محدداً',
'لا يوجد فرق، كلاهما يُقاس بنفس الطريقة',
'الكمية المتجهة تُقاس في الفضاء فقط',
],
correctIndex: 1,
explanation:
'الكمية القياسية مثل الكتلة والزمن، بينما المتجهة مثل القوة والسرعة المتجهة تتطلب تحديد الاتجاه بدقة.',
),
];
}
}