Update Saqel Platform: 2026-09-04 02:57:19
This commit is contained in:
@@ -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:
|
||||
'الكمية القياسية مثل الكتلة والزمن، بينما المتجهة مثل القوة والسرعة المتجهة تتطلب تحديد الاتجاه بدقة.',
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user