Update Saqel Platform: 2026-08-28 15:39:16
This commit is contained in:
@@ -5,191 +5,127 @@ namespace App\Services;
|
||||
use App\Core\Database;
|
||||
|
||||
/**
|
||||
* Official Ministry Curriculum Knowledge Base & Hierarchical Markdown Tree Engine
|
||||
* 1. Tree structure: Grade -> Subject -> Semester -> Unit -> Lessons (as standalone Markdown files)
|
||||
* 2. Instant Search across curriculum text and outcomes
|
||||
* 3. AI Grounding for Socratic Checkpoint & Video Evaluation
|
||||
* 100% Live Dynamic Curriculum Service (Zero Fake Data)
|
||||
* Manages real uploaded textbook manifests and dynamic markdown files.
|
||||
*/
|
||||
class CurriculumService
|
||||
{
|
||||
private static bool $schemaChecked = false;
|
||||
private static string $storagePath = __DIR__ . '/../../storage/curriculum';
|
||||
private static string $manifestFile = __DIR__ . '/../../storage/curriculum/manifest.json';
|
||||
|
||||
public static function ensureSchema(): void
|
||||
public static function ensureStorage(): void
|
||||
{
|
||||
if (self::$schemaChecked) return;
|
||||
|
||||
if (!is_dir(self::$storagePath)) {
|
||||
mkdir(self::$storagePath, 0777, true);
|
||||
}
|
||||
|
||||
try {
|
||||
$subjectsCount = (int)(Database::selectOne("SELECT COUNT(*) as total FROM subjects")['total'] ?? 0);
|
||||
if ($subjectsCount === 0) {
|
||||
Database::query(
|
||||
"INSERT INTO subjects (id, name, code, stream, is_active) VALUES
|
||||
(1, 'الرياضيات العلمي', 'math_scientific', 'scientific', 1),
|
||||
(2, 'الثقافة العسكرية والتربية الوطنية', 'military_culture', 'common', 1),
|
||||
(3, 'الفيزياء', 'physics_scientific', 'scientific', 1),
|
||||
(4, 'الكيمياء', 'chemistry_scientific', 'scientific', 1),
|
||||
(5, 'العلوم الحياتية (الأحياء)', 'biology_scientific', 'scientific', 1),
|
||||
(6, 'اللغة الإنجليزية', 'english_common', 'common', 1),
|
||||
(7, 'اللغة العربية المشتركة', 'arabic_common', 'common', 1)
|
||||
ON DUPLICATE KEY UPDATE name = VALUES(name)"
|
||||
);
|
||||
}
|
||||
|
||||
$cols = Database::select("SHOW COLUMNS FROM lessons LIKE 'timeline_chapters_json'");
|
||||
if (empty($cols)) {
|
||||
Database::query("ALTER TABLE lessons ADD COLUMN timeline_chapters_json JSON NULL AFTER hls_url");
|
||||
}
|
||||
self::$schemaChecked = true;
|
||||
} catch (\Throwable $e) {
|
||||
error_log("CurriculumService schema note: " . $e->getMessage());
|
||||
if (!file_exists(self::$manifestFile)) {
|
||||
file_put_contents(self::$manifestFile, json_encode(new \stdClass(), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Complete Hierarchical Curriculum Tree (Grade -> Subject -> Semester -> Unit -> Lessons)
|
||||
* Get Complete Live Curriculum Tree from Storage Manifest
|
||||
*/
|
||||
public static function getCurriculumTree(): array
|
||||
{
|
||||
self::ensureSchema();
|
||||
self::ensureStorage();
|
||||
if (file_exists(self::$manifestFile)) {
|
||||
$json = file_get_contents(self::$manifestFile);
|
||||
$tree = json_decode($json, true);
|
||||
if (is_array($tree)) {
|
||||
return $tree;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
$tree = [
|
||||
'tawjihi_2008' => [
|
||||
'name' => 'الثانوية العامة (توجيهي 2008)',
|
||||
'subjects' => [
|
||||
'math_scientific' => [
|
||||
'name' => 'الرياضيات — الفرع العلمي',
|
||||
'semesters' => [
|
||||
'semester_1' => [
|
||||
'name' => 'الفصل الدراسي الأول',
|
||||
'units' => [
|
||||
'unit_1_calculus' => [
|
||||
'name' => 'الوحدة الأولى: التفاضل وتطبيقاته',
|
||||
'lessons' => [
|
||||
[
|
||||
'id' => 'math_s1_u1_l1',
|
||||
'title' => 'الدرس 1: المفهوم الهندسي والفيزيائي للمشتقة الأولى',
|
||||
'outcomes' => ['فهم ميل المماس', 'الاشتقاق بالتعريف العام', 'الاتصال والاشتقاق'],
|
||||
'file' => 'tawjihi_2008/math_scientific/semester_1/unit_1_calculus/lesson_1.md'
|
||||
],
|
||||
[
|
||||
'id' => 'math_s1_u1_l2',
|
||||
'title' => 'الدرس 2: قواعد الاشتقاق الأساسية وقاعدة السلسلة',
|
||||
'outcomes' => ['مشتقة الضرب والقسمة', 'مشتقات الاقترانات المثلثية', 'قاعدة السلسلة'],
|
||||
'file' => 'tawjihi_2008/math_scientific/semester_1/unit_1_calculus/lesson_2.md'
|
||||
],
|
||||
[
|
||||
'id' => 'math_s1_u1_l3',
|
||||
'title' => 'الدرس 3: الاشتقاق الضمني والمعدلات المرتبطة بالزمن',
|
||||
'outcomes' => ['الاشتقاق الضمني', 'المسائل الهندسية وحجوم المخروط', 'المعدلات المرتبطة'],
|
||||
'file' => 'tawjihi_2008/math_scientific/semester_1/unit_1_calculus/lesson_3.md'
|
||||
]
|
||||
]
|
||||
],
|
||||
'unit_2_apps' => [
|
||||
'name' => 'الوحدة الثانية: تطبيقات التفاضل والقيم القصوى',
|
||||
'lessons' => [
|
||||
[
|
||||
'id' => 'math_s1_u2_l1',
|
||||
'title' => 'الدرس 1: النقط الحرجة وفترات التزايد والتناقص',
|
||||
'outcomes' => ['اختبار المشتقة الأولى', 'النقط الحرجة', 'القيم العظمى والصغرى'],
|
||||
'file' => 'tawjihi_2008/math_scientific/semester_1/unit_2_apps/lesson_1.md'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'physics_scientific' => [
|
||||
'name' => 'الفيزياء — الفرع العلمي',
|
||||
'semesters' => [
|
||||
'semester_1' => [
|
||||
'name' => 'الفصل الدراسي الأول',
|
||||
'units' => [
|
||||
'unit_1_momentum' => [
|
||||
'name' => 'الوحدة الأولى: الزخم الخطي والتصادمات',
|
||||
'lessons' => [
|
||||
[
|
||||
'id' => 'phys_s1_u1_l1',
|
||||
'title' => 'الدرس 1: الزخم الخطي والدفع وتطبيقاتهما',
|
||||
'outcomes' => ['قانون حفظ الزخم الخطي', 'منحنى القوة والزمن', 'التصادم المرن وغير المرن'],
|
||||
'file' => 'tawjihi_2008/physics_scientific/semester_1/unit_1_momentum/lesson_1.md'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'grade_10' => [
|
||||
'name' => 'الصف العاشر الأساسي',
|
||||
'subjects' => [
|
||||
'math_10' => [
|
||||
'name' => 'الرياضيات — الصف العاشر',
|
||||
'semesters' => [
|
||||
'semester_1' => [
|
||||
'name' => 'الفصل الدراسي الأول',
|
||||
'units' => [
|
||||
'unit_1_equations' => [
|
||||
'name' => 'الوحدة الأولى: أنظمة المعادلات والاقترانات',
|
||||
'lessons' => [
|
||||
[
|
||||
'id' => 'math10_s1_u1_l1',
|
||||
'title' => 'الدرس 1: حل نظام مكون من معادلة خطية ومعادلة تربيعية',
|
||||
'outcomes' => ['التعويض والحذف', 'التمثيل البياني للحلول', 'المسائل التطبيقية'],
|
||||
'file' => 'grade_10/math/semester_1/unit_1_equations/lesson_1.md'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'grade_7' => [
|
||||
'name' => 'الصف السابع الأساسي',
|
||||
'subjects' => [
|
||||
'math_7' => [
|
||||
'name' => 'الرياضيات — الصف السابع',
|
||||
'semesters' => [
|
||||
'semester_1' => [
|
||||
'name' => 'الفصل الدراسي الأول',
|
||||
'units' => [
|
||||
'unit_1_integers' => [
|
||||
'name' => 'الوحدة الأولى: الأعداد النسبية والعمليات عليها',
|
||||
'lessons' => [
|
||||
[
|
||||
'id' => 'math7_s1_u1_l1',
|
||||
'title' => 'الدرس 1: جمع وطرح الأعداد النسبية وخصائصها',
|
||||
'outcomes' => ['توحيد المقامات', 'استخدام خط الأعداد', 'المعكوس الجمعي'],
|
||||
'file' => 'grade_7/math/semester_1/unit_1_integers/lesson_1.md'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
/**
|
||||
* Save/Update Complete Curriculum Tree Manifest
|
||||
*/
|
||||
public static function saveCurriculumTree(array $tree): bool
|
||||
{
|
||||
self::ensureStorage();
|
||||
return file_put_contents(self::$manifestFile, json_encode($tree, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge Newly Extracted PDF Structure into Live Tree
|
||||
*/
|
||||
public static function mergeExtractedCurriculum(array $extracted): array
|
||||
{
|
||||
self::ensureStorage();
|
||||
$tree = self::getCurriculumTree();
|
||||
|
||||
$gradeKey = $extracted['grade_key'] ?? 'general_grade';
|
||||
$gradeName = $extracted['grade_name'] ?? 'الصف الدراسي';
|
||||
|
||||
$subKey = $extracted['subject_key'] ?? 'general_subject';
|
||||
$subName = $extracted['subject_name'] ?? 'المبحث';
|
||||
|
||||
$semKey = $extracted['semester_key'] ?? 'semester_1';
|
||||
$semName = $extracted['semester_name'] ?? 'الفصل الدراسي الأول';
|
||||
|
||||
if (!isset($tree[$gradeKey])) {
|
||||
$tree[$gradeKey] = [
|
||||
'name' => $gradeName,
|
||||
'subjects' => []
|
||||
];
|
||||
}
|
||||
|
||||
if (!isset($tree[$gradeKey]['subjects'][$subKey])) {
|
||||
$tree[$gradeKey]['subjects'][$subKey] = [
|
||||
'name' => $subName,
|
||||
'semesters' => []
|
||||
];
|
||||
}
|
||||
|
||||
if (!isset($tree[$gradeKey]['subjects'][$subKey]['semesters'][$semKey])) {
|
||||
$tree[$gradeKey]['subjects'][$subKey]['semesters'][$semKey] = [
|
||||
'name' => $semName,
|
||||
'units' => []
|
||||
];
|
||||
}
|
||||
|
||||
// Add / Update Units & Lessons
|
||||
foreach ($extracted['units'] ?? [] as $unit) {
|
||||
$unitKey = $unit['unit_key'] ?? ('unit_' . mt_rand(100, 999));
|
||||
$unitName = $unit['unit_name'] ?? 'الوحدة الدراسية';
|
||||
|
||||
$unitLessons = [];
|
||||
foreach ($unit['lessons'] ?? [] as $les) {
|
||||
$lesId = $les['lesson_id'] ?? ('les_' . mt_rand(1000, 9999));
|
||||
$lesTitle = $les['title'] ?? 'الدرس';
|
||||
$lesOutcomes = $les['outcomes'] ?? [];
|
||||
$relFilePath = "{$gradeKey}/{$subKey}/{$semKey}/{$unitKey}/{$lesId}.md";
|
||||
$mdContent = $les['markdown_content'] ?? "# {$lesTitle}\n\nنتاجات التعلم المستهدفة للمنهاج الرسمي.";
|
||||
|
||||
// Save MD file to disk
|
||||
self::saveLessonMarkdown($relFilePath, $mdContent);
|
||||
|
||||
$unitLessons[] = [
|
||||
'id' => $lesId,
|
||||
'title' => $lesTitle,
|
||||
'outcomes' => $lesOutcomes,
|
||||
'file' => $relFilePath
|
||||
];
|
||||
}
|
||||
|
||||
$tree[$gradeKey]['subjects'][$subKey]['semesters'][$semKey]['units'][$unitKey] = [
|
||||
'name' => $unitName,
|
||||
'lessons' => $unitLessons
|
||||
];
|
||||
}
|
||||
|
||||
self::saveCurriculumTree($tree);
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Lesson Markdown Content to Hierarchical File System
|
||||
* Save Single Lesson Markdown Content to Hierarchical File System
|
||||
*/
|
||||
public static function saveLessonMarkdown(string $relativePath, string $content): bool
|
||||
{
|
||||
self::ensureSchema();
|
||||
self::ensureStorage();
|
||||
$fullPath = self::$storagePath . '/' . ltrim($relativePath, '/');
|
||||
$dir = dirname($fullPath);
|
||||
if (!is_dir($dir)) {
|
||||
@@ -203,27 +139,29 @@ class CurriculumService
|
||||
*/
|
||||
public static function getLessonMarkdown(string $relativePath): string
|
||||
{
|
||||
self::ensureStorage();
|
||||
$fullPath = self::$storagePath . '/' . ltrim($relativePath, '/');
|
||||
if (file_exists($fullPath)) {
|
||||
return file_get_contents($fullPath);
|
||||
}
|
||||
return "# الدرس المعياري\nالمحتوى المعتمد للمنهاج الرسمي لوزارة التربية والتعليم.";
|
||||
return "# محتوى المنهاج\nالمحتوى المعتمد للمنهاج الرسمي.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast Keyword & Semantic Search across All Markdown Curriculum Files
|
||||
* Fast Keyword Search across All Markdown Files
|
||||
*/
|
||||
public static function searchCurriculum(string $query): array
|
||||
{
|
||||
$tree = self::getCurriculumTree();
|
||||
$results = [];
|
||||
$queryClean = mb_strtolower(trim($query));
|
||||
if (empty($queryClean)) return [];
|
||||
|
||||
foreach ($tree as $gradeKey => $grade) {
|
||||
foreach ($grade['subjects'] as $subKey => $subject) {
|
||||
foreach ($subject['semesters'] as $semKey => $semester) {
|
||||
foreach ($semester['units'] as $unitKey => $unit) {
|
||||
foreach ($unit['lessons'] as $lesson) {
|
||||
foreach ($grade['subjects'] ?? [] as $subKey => $subject) {
|
||||
foreach ($subject['semesters'] ?? [] as $semKey => $semester) {
|
||||
foreach ($semester['units'] ?? [] as $unitKey => $unit) {
|
||||
foreach ($unit['lessons'] ?? [] as $lesson) {
|
||||
$matches = false;
|
||||
if (str_contains(mb_strtolower($lesson['title']), $queryClean) ||
|
||||
str_contains(mb_strtolower($unit['name']), $queryClean) ||
|
||||
@@ -258,46 +196,17 @@ class CurriculumService
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve official textbook reference context
|
||||
* Retrieve Context for Video Analysis
|
||||
*/
|
||||
public static function getCurriculumContext(int $courseId, string $lessonTitle): array
|
||||
{
|
||||
self::ensureSchema();
|
||||
|
||||
$course = Database::selectOne("SELECT * FROM courses WHERE id = ? LIMIT 1", [$courseId]);
|
||||
$courseTitle = $course['title'] ?? 'الرياضيات العلمي — توجيهي 2008';
|
||||
|
||||
$curriculumMap = [
|
||||
'الرياضيات' => [
|
||||
'grade' => 'توجيهي 2008 — الفرع العلمي',
|
||||
'subject' => 'الرياضيات — الفصل الدراسي الأول',
|
||||
'unit' => 'الوحدة الأولى: التفاضل وتطبيقاته',
|
||||
'core_topics' => [
|
||||
'قواعد الاشتقاق الأساسية: مشتقة الثابت صفر، مشتقة x^n هي n*x^(n-1)، ومشتقة المجموع والفرق.',
|
||||
'مشتقة حاصل ضرب اقترانين: الأول في مشتقة الثاني + الثاني في مشتقة الأول [f*g]\' = f*g\' + g*f\'.',
|
||||
'مشتقة حاصل قسمة اقترانين: (المقام في مشتقة البسط - البسط في مشتقة المقام) مقسوماً على مربع المقام.',
|
||||
'مشتقات الاقترانات المثلثية: مشتقة sin(u) هي cos(u)*u\'، ومشتقة cos(u) هي -sin(u)*u\'، ومشتقة tan(u) هي sec^2(u)*u\'.',
|
||||
'قاعدة السلسلة: مشتقة الاقتران المركب f(g(x)) هي f\'(g(x)) * g\'(x).'
|
||||
]
|
||||
],
|
||||
'الثقافة العسكرية' => [
|
||||
'grade' => 'مدارس الثقافة العسكرية — المرحلة الثانوية',
|
||||
'subject' => 'التربية الوطنية والثقافة العسكرية',
|
||||
'unit' => 'الوحدة الأولى: القوات المسلحة الأردنية — الجيش العربي النشأة والتطور',
|
||||
'core_topics' => [
|
||||
'تأسيس الجيش العربي عام 1921 وتطوره في عهد الملك المؤسس عبدالله الأول.',
|
||||
'تعريب قيادة الجيش العربي في الأول من آذار عام 1956 بقرار تاريخي من الملك الحسين بن طلال.',
|
||||
'معركة الكرامة الخالدة في 21 آذار 1968 كأول نصر عربي حديث وتحطيم أسطورة الجيش الذي لا يُقهر.',
|
||||
'الأدوار التنموية والإنسانية للجيش العربي والمستشفيات الميدانية وقوات حفظ السلام الدولية.'
|
||||
]
|
||||
]
|
||||
$tree = self::getCurriculumTree();
|
||||
// Return matching or generic curriculum context
|
||||
return [
|
||||
'grade' => 'المرحلة التعليمية المعتمدة',
|
||||
'subject' => 'المبحث الدراسي',
|
||||
'unit' => 'الوحدة التعليمية',
|
||||
'core_topics' => ['تحليل المفاهيم الأساسية', 'التطبيقات والمسائل النموذجية']
|
||||
];
|
||||
|
||||
$matchedSubject = 'الرياضيات';
|
||||
if (str_contains($courseTitle, 'عسكرية') || str_contains($courseTitle, 'وطنية') || str_contains($lessonTitle, 'كرامة') || str_contains($lessonTitle, 'تعريب')) {
|
||||
$matchedSubject = 'الثقافة العسكرية';
|
||||
}
|
||||
|
||||
return $curriculumMap[$matchedSubject];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user