548 lines
25 KiB
PHP
548 lines
25 KiB
PHP
<?php
|
|
/**
|
|
* ==============================================================================
|
|
* SAQEL ENTERPRISE (EDTECH 2.0) - CURRICULUM & STUDIO CONTROLLER
|
|
* ==============================================================================
|
|
*
|
|
* ملف: CurriculumController.php
|
|
* الهدف المعماري:
|
|
* إدارة منظومة تفريغ وفهرسة كتب المناهج الوزارية واستوديو المناهج (Curriculum Studio).
|
|
* يتولى هذا الملف المهام التالية:
|
|
* 1. استلام كتب الـ PDF الوزارية المرفوعة وتمريرها لعمال الخلفية لاستخراج شجرة الماركداون.
|
|
* 2. متابعة حالة مهام المعالجة والتفريغ التلقائي وإرجاع سجلات التشغيل الحية (Live Logs).
|
|
* 3. استرجاع الشجرة الهرمية للمنهاج (المرحلة، المادة، الفصل، الوحدة، الدرس).
|
|
* 4. حفظ وتحديث محتوى ملفات الماركداون للدروس وربطها مع قاعدة البيانات.
|
|
* 5. توليد الملخصات الذكية (Cheat Sheets) والفحوصات السقراطية عبر Gemini.
|
|
* 6. توليد وتحديث بنوك الأسئلة (50 سؤالاً للوحدة) بالذكاء الاصطناعي بنقرة واحدة من الاستوديو.
|
|
* 7. استضافة ورفع ملفات الفيديو مباشرة وربطها بالدروس.
|
|
*/
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Core\Request;
|
|
use App\Core\Response;
|
|
use App\Services\CurriculumService;
|
|
use App\Services\CurriculumExtractorService;
|
|
|
|
class CurriculumController
|
|
{
|
|
/**
|
|
* استلام ورفع كتاب المنهاج الوزاري بصيغة PDF وجدولته في طابور المعالجة المعمقة
|
|
* POST /api/curriculum/upload-pdf
|
|
*
|
|
* @param Request $request طلب الـ HTTP المحتوي على ملف الـ PDF المرفوع
|
|
* @param Response $response كائن الاستجابة بمعرف المهمة ومسار المتابعة
|
|
*/
|
|
public function uploadPdf(Request $request, Response $response): void
|
|
{
|
|
if (empty($_FILES['pdf_file']) || $_FILES['pdf_file']['error'] !== UPLOAD_ERR_OK) {
|
|
$response->status(400)->json([
|
|
'status' => 'error',
|
|
'message' => 'يرجى اختيار ملف PDF صالح للمنهاج الوزاري.'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$file = $_FILES['pdf_file'];
|
|
$origName = $file['name'];
|
|
$tmpPath = $file['tmp_name'];
|
|
$taskId = uniqid('task_');
|
|
|
|
// Setup processing directory
|
|
$processingDir = __DIR__ . '/../../storage/curriculum/processing';
|
|
if (!is_dir($processingDir)) {
|
|
mkdir($processingDir, 0777, true);
|
|
}
|
|
|
|
$savedPdfPath = $processingDir . '/' . $taskId . '.pdf';
|
|
move_uploaded_file($tmpPath, $savedPdfPath);
|
|
|
|
// Initialize Task State
|
|
$taskState = [
|
|
'task_id' => $taskId,
|
|
'status' => 'queued',
|
|
'progress' => 0,
|
|
'message' => 'تم استلام الملف وجاري تحويله للمعالجة المعمقة...',
|
|
'orig_name' => $origName
|
|
];
|
|
file_put_contents($processingDir . '/' . $taskId . '.json', json_encode($taskState, JSON_UNESCAPED_UNICODE));
|
|
|
|
// Prepare task paths
|
|
$scriptPath = realpath(__DIR__ . '/../../scripts/curriculum_worker.php');
|
|
$logPath = $processingDir . '/' . $taskId . '.log';
|
|
|
|
// Send processing response immediately
|
|
$response->json([
|
|
'status' => 'processing',
|
|
'task_id' => $taskId,
|
|
'message' => 'تم استلام الكتاب، وجاري المعالجة المعمقة بواسطة الذكاء الاصطناعي. يرجى الانتظار...'
|
|
]);
|
|
|
|
// Terminate request and flush buffers so the client receives the JSON immediately
|
|
if (function_exists('fastcgi_finish_request')) {
|
|
fastcgi_finish_request();
|
|
} else {
|
|
// Fallback for non-FPM
|
|
ob_end_flush();
|
|
flush();
|
|
}
|
|
|
|
// Run worker completely in the background via CLI
|
|
try {
|
|
$cmd = "php " . escapeshellarg($scriptPath) . " " . escapeshellarg($taskId) . " > /dev/null 2>&1 &";
|
|
exec($cmd);
|
|
|
|
// Log that we spawned it
|
|
file_put_contents($logPath, "[System] Spawning background worker: $cmd\n", FILE_APPEND);
|
|
} catch (\Exception $e) {
|
|
$err = "[Exception] " . $e->getMessage();
|
|
file_put_contents($logPath, $err . "\n", FILE_APPEND);
|
|
|
|
// Update state file to error
|
|
$stateFile = $processingDir . '/' . $taskId . '.json';
|
|
if (file_exists($stateFile)) {
|
|
$st = json_decode(file_get_contents($stateFile), true);
|
|
$st['status'] = 'error';
|
|
$st['message'] = 'تعطل النظام أثناء التحليل (Crash). يرجى مراجعة سجل الأخطاء.';
|
|
file_put_contents($stateFile, json_encode($st, JSON_UNESCAPED_UNICODE));
|
|
}
|
|
}
|
|
|
|
// Ensure script stops here so it doesn't return anything else
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Poll Upload Status
|
|
*/
|
|
public function getUploadStatus(Request $request, Response $response): void
|
|
{
|
|
$taskId = $request->getQueryParams()['task_id'] ?? '';
|
|
if (empty($taskId)) {
|
|
$response->status(400)->json(['status' => 'error', 'message' => 'رقم المهمة مفقود']);
|
|
return;
|
|
}
|
|
|
|
$processingDir = __DIR__ . '/../../storage/curriculum/processing';
|
|
$taskId = basename($taskId);
|
|
if (!preg_match('/^task_[A-Za-z0-9_.-]+$|^video_[A-Za-z0-9_.-]+$/', $taskId)) {
|
|
$response->status(400)->json(['status' => 'error', 'message' => 'رقم المهمة غير صالح']);
|
|
return;
|
|
}
|
|
$stateFile = $processingDir . '/' . $taskId . '.json';
|
|
|
|
if (!file_exists($stateFile)) {
|
|
$response->status(404)->json(['status' => 'error', 'message' => 'المهمة غير موجودة']);
|
|
return;
|
|
}
|
|
|
|
$state = json_decode(file_get_contents($stateFile), true);
|
|
$response->json($state);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get Upload Log
|
|
*/
|
|
public function getUploadLog(Request $request, Response $response): void
|
|
{
|
|
$taskId = $request->getQueryParams()['task_id'] ?? '';
|
|
if (empty($taskId)) {
|
|
$response->status(400)->json(['status' => 'error', 'message' => 'رقم المهمة مفقود']);
|
|
return;
|
|
}
|
|
|
|
$processingDir = __DIR__ . '/../../storage/curriculum/processing';
|
|
$logFile = $processingDir . '/' . $taskId . '.log';
|
|
|
|
if (!file_exists($logFile)) {
|
|
$response->status(404)->json(['status' => 'error', 'message' => 'سجل الأخطاء غير موجود']);
|
|
return;
|
|
}
|
|
|
|
$logContent = file_get_contents($logFile);
|
|
$response->json(['status' => 'success', 'log' => $logContent]);
|
|
}
|
|
|
|
/**
|
|
* Get Complete Live Tree
|
|
*/
|
|
|
|
public function getTree(Request $request, Response $response): void
|
|
{
|
|
$response->json([
|
|
'status' => 'success',
|
|
'data' => CurriculumService::getCurriculumTree()
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get Single Lesson Markdown Content
|
|
*/
|
|
public function getLessonContent(Request $request, Response $response): void
|
|
{
|
|
$file = $request->getQueryParams()['file'] ?? '';
|
|
if (empty($file)) {
|
|
$response->status(400)->json(['status' => 'error', 'message' => 'مسار الملف مطلوب']);
|
|
return;
|
|
}
|
|
$content = CurriculumService::getLessonMarkdown($file);
|
|
$assets = CurriculumService::getLessonAiAssets($file);
|
|
$serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/');
|
|
|
|
$response->json([
|
|
'status' => 'success',
|
|
'file' => $file,
|
|
'server_full_path' => $serverFullPath,
|
|
'content' => $content,
|
|
'ai_assets' => $assets
|
|
]);
|
|
}
|
|
|
|
public function saveLessonContent(Request $request, Response $response): void
|
|
{
|
|
$body = $request->getBody();
|
|
$file = $body['file'] ?? '';
|
|
$content = $body['content'] ?? '';
|
|
$aiAssets = $body['ai_assets'] ?? null;
|
|
|
|
if (empty($file) || empty($content)) {
|
|
$response->status(400)->json(['status' => 'error', 'message' => 'بيانات الحفظ غير مكتملة']);
|
|
return;
|
|
}
|
|
|
|
CurriculumService::saveLessonMarkdown($file, $content);
|
|
if ($aiAssets !== null) {
|
|
CurriculumService::saveLessonAiAssets($file, $aiAssets);
|
|
}
|
|
|
|
// Sync to MySQL Database Table `lessons` if connected
|
|
try {
|
|
$aiVideoUrl = $aiAssets['ai_video_url'] ?? null;
|
|
$cheatSheet = $aiAssets['cheat_sheet'] ?? null;
|
|
$socraticJson = isset($aiAssets['socratic_quiz']) ? json_encode($aiAssets['socratic_quiz'], JSON_UNESCAPED_UNICODE) : null;
|
|
|
|
// Search lesson by matching filename or title
|
|
$filename = basename($file, '.md');
|
|
\App\Core\Database::query(
|
|
"UPDATE lessons SET markdown_content = ?, ai_video_url = COALESCE(?, ai_video_url), cheat_sheet_markdown = COALESCE(?, cheat_sheet_markdown), socratic_quiz_json = COALESCE(?, socratic_quiz_json)
|
|
WHERE title LIKE ? OR markdown_content LIKE ?",
|
|
[$content, $aiVideoUrl, $cheatSheet, $socraticJson, "%{$filename}%", "%{$file}%"]
|
|
);
|
|
} catch (\Throwable $dbEx) {
|
|
error_log("Curriculum DB save sync note: " . $dbEx->getMessage());
|
|
}
|
|
|
|
$serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/');
|
|
|
|
$response->json([
|
|
'status' => 'success',
|
|
'message' => 'تم حفظ واعتماد محتوى الدرس في المنهاج وقاعدة البيانات بنجاح!',
|
|
'server_full_path' => $serverFullPath
|
|
]);
|
|
}
|
|
|
|
public function generateAiAssets(Request $request, Response $response): void
|
|
{
|
|
$body = $request->getBody();
|
|
$file = $body['file'] ?? '';
|
|
if (empty($file)) {
|
|
$response->status(400)->json(['status' => 'error', 'message' => 'مسار الملف مطلوب']);
|
|
return;
|
|
}
|
|
|
|
$content = CurriculumService::getLessonMarkdown($file);
|
|
if (empty($content)) {
|
|
$response->status(400)->json(['status' => 'error', 'message' => 'محتوى الدرس فارغ']);
|
|
return;
|
|
}
|
|
|
|
// Generate Assets
|
|
require_once __DIR__ . '/../Services/AiLessonEnhancerService.php';
|
|
|
|
$assets = \App\Services\AiLessonEnhancerService::generateFromText('الدرس', $content);
|
|
|
|
if ($assets) {
|
|
CurriculumService::saveLessonAiAssets($file, $assets);
|
|
$response->json(['status' => 'success', 'ai_assets' => $assets]);
|
|
} else {
|
|
$response->status(500)->json(['status' => 'error', 'message' => 'فشل توليد المخرجات الذكية']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Direct Video Upload to Platform Storage with Semantic Lesson Binding
|
|
*/
|
|
public function uploadLessonVideo(Request $request, Response $response): void
|
|
{
|
|
$file = $_POST['file'] ?? '';
|
|
if (empty($file)) {
|
|
$response->status(400)->json(['status' => 'error', 'message' => 'مسار الدرس مطلوب']);
|
|
return;
|
|
}
|
|
|
|
if (empty($_FILES['video_file']) || $_FILES['video_file']['error'] !== UPLOAD_ERR_OK) {
|
|
$response->status(400)->json(['status' => 'error', 'message' => 'يرجى اختيار ملف فيديو صالح (MP4, WebM, MOV)']);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Video transcoding and R2 upload can take longer than a web request.
|
|
// Persist the upload and hand the heavy work to a CLI worker.
|
|
$processingDir = __DIR__ . '/../../storage/curriculum/processing';
|
|
if (!is_dir($processingDir)) mkdir($processingDir, 0755, true);
|
|
$taskId = uniqid('video_', true);
|
|
$extension = strtolower(pathinfo($_FILES['video_file']['name'], PATHINFO_EXTENSION)) ?: 'mp4';
|
|
$savedVideoPath = $processingDir . '/' . $taskId . '.' . $extension;
|
|
if (!move_uploaded_file($_FILES['video_file']['tmp_name'], $savedVideoPath)) {
|
|
throw new \RuntimeException('تعذر حفظ ملف الفيديو المؤقت على السيرفر.');
|
|
}
|
|
file_put_contents($processingDir . '/' . $taskId . '.json', json_encode([
|
|
'task_id' => $taskId, 'status' => 'queued', 'progress' => 5,
|
|
'message' => 'تم استلام الفيديو، وسيبدأ التحويل والرفع السحابي الآن.',
|
|
'file' => $file, 'video_path' => $savedVideoPath,
|
|
'original_name' => $_FILES['video_file']['name']
|
|
], JSON_UNESCAPED_UNICODE));
|
|
|
|
$response->status(202)->json(['status' => 'processing', 'task_id' => $taskId, 'message' => 'تم استلام الفيديو وجاري تحويله ورفعه إلى Cloudflare R2...']);
|
|
if (function_exists('fastcgi_finish_request')) fastcgi_finish_request();
|
|
elseif (ob_get_level()) { ob_end_flush(); flush(); }
|
|
$scriptPath = realpath(__DIR__ . '/../../scripts/video_upload_worker.php');
|
|
$logPath = $processingDir . '/' . $taskId . '.log';
|
|
exec('php ' . escapeshellarg($scriptPath) . ' ' . escapeshellarg($taskId) . ' >> ' . escapeshellarg($logPath) . ' 2>&1 &');
|
|
return;
|
|
|
|
} catch (\Throwable $e) {
|
|
$response->status(500)->json([
|
|
'status' => 'error',
|
|
'message' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate & Refresh AI Question Bank for any Unit or Lesson (Triggered from Curriculum Studio)
|
|
* POST /api/curriculum/generate-questions
|
|
*/
|
|
public function generateQuestionBank(Request $request, Response $response): void
|
|
{
|
|
$body = $request->getBody();
|
|
$unitPath = $body['unit_path'] ?? 'grade_10/math_10/semester_1/unit_01';
|
|
$courseId = (int)($body['course_id'] ?? 1);
|
|
$count = (int)($body['count'] ?? 50);
|
|
|
|
try {
|
|
require_once __DIR__ . '/../Services/AiQuestionBankGeneratorService.php';
|
|
$result = \App\Services\AiQuestionBankGeneratorService::generateUnitQuestionBank($unitPath, $courseId, $count);
|
|
|
|
$response->json([
|
|
'status' => 'success',
|
|
'message' => "تم توليد وتحديث بنك الأسئلة بالذكاء الاصطناعي بنجاح ({$result['total_in_bank']} سؤالاً معيارياً في قاعدة البيانات)!",
|
|
'data' => $result
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
$response->status(500)->json([
|
|
'status' => 'error',
|
|
'message' => 'تعذر توليد بنك الأسئلة: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all available simulations for a subject or overall
|
|
* GET /api/curriculum/simulations
|
|
*/
|
|
public function listSimulations(Request $request, Response $response): void
|
|
{
|
|
$subject = $request->getQueryParams()['subject'] ?? 'physics_10';
|
|
$simDir = realpath(__DIR__ . '/../../storage/curriculum/simulations') . "/{$subject}";
|
|
|
|
$simulations = [];
|
|
if (is_dir($simDir)) {
|
|
$files = glob("{$simDir}/*.html");
|
|
foreach ($files as $f) {
|
|
$slug = basename($f, '.html');
|
|
$title = ($slug === 'vectors_lab') ? 'مختبر جمع وتحليل المتجهات التفاعلي' :
|
|
(($slug === 'motion_1d_lab') ? 'مختبر الحركة في بُعد واحد والتسارع' : $slug);
|
|
$unit = ($slug === 'vectors_lab') ? 'الوحدة 1: المتجهات والكميات' :
|
|
(($slug === 'motion_1d_lab') ? 'الوحدة 2: الحركة والقوى' : 'تجارب تفاعلية');
|
|
|
|
$simulations[] = [
|
|
'slug' => $slug,
|
|
'title' => $title,
|
|
'unit_title' => $unit,
|
|
'url' => "/api/curriculum/simulations/{$subject}/{$slug}",
|
|
'has_canvas' => true,
|
|
'watermark' => 'منصة صَقِل التعليمية الذكية © Saqel Lab',
|
|
];
|
|
}
|
|
}
|
|
|
|
$response->json([
|
|
'status' => 'success',
|
|
'data' => $simulations
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get / Serve Interactive HTML5 Canvas Simulation
|
|
* GET /api/curriculum/simulations/{subject}/{simName}
|
|
*/
|
|
public function getSimulation(Request $request, Response $response): void
|
|
{
|
|
$subject = preg_replace('/[^a-zA-Z0-9_-]/', '', $request->getParam('subject') ?? '');
|
|
$simName = preg_replace('/[^a-zA-Z0-9_-]/', '', $request->getParam('simName') ?? '');
|
|
|
|
$simPath = realpath(__DIR__ . '/../../storage/curriculum/simulations') . "/{$subject}/{$simName}.html";
|
|
|
|
if (!file_exists($simPath)) {
|
|
// Fallback check in root simulations
|
|
$simPath = realpath(__DIR__ . '/../../storage/curriculum/simulations') . "/{$simName}.html";
|
|
}
|
|
|
|
if (!file_exists($simPath)) {
|
|
$response->status(404)->json(['status' => 'error', 'message' => 'المحاكي التفاعلي غير موجود']);
|
|
return;
|
|
}
|
|
|
|
header('Content-Type: text/html; charset=UTF-8');
|
|
header('X-Frame-Options: SAMEORIGIN');
|
|
readfile($simPath);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Get real document or textbook markdown content dynamically per subject
|
|
* GET /api/curriculum/document
|
|
*/
|
|
public function getDocumentContent(Request $request, Response $response): void
|
|
{
|
|
$params = $request->getQueryParams();
|
|
$file = $params['file'] ?? '';
|
|
$subject = strtolower($params['subject'] ?? '');
|
|
$type = $params['type'] ?? 'textbook';
|
|
|
|
$storage = realpath(__DIR__ . '/../../storage/curriculum');
|
|
$resolvedFile = null;
|
|
|
|
if (!empty($file) && !str_ends_with($file, '.pdf')) {
|
|
$cleanFile = ltrim($file, '/');
|
|
if (file_exists("{$storage}/{$cleanFile}")) {
|
|
$resolvedFile = "{$storage}/{$cleanFile}";
|
|
}
|
|
}
|
|
|
|
// If not resolved by exact path, resolve by subject
|
|
if (!$resolvedFile && !empty($subject)) {
|
|
if (str_contains($subject, 'english') || str_contains($subject, 'إنجليز')) {
|
|
$mds = glob("{$storage}/grade_10/english_10/semester_1/*/*.md");
|
|
if (!empty($mds)) $resolvedFile = $mds[0];
|
|
} elseif (str_contains($subject, 'physic') || str_contains($subject, 'فيزياء')) {
|
|
$mds = glob("{$storage}/grade_10/physics_10/semester_1/*/*.md");
|
|
if (!empty($mds)) $resolvedFile = $mds[1] ?? $mds[0];
|
|
} elseif (str_contains($subject, 'math') || str_contains($subject, 'رياضيات')) {
|
|
$mds = glob("{$storage}/grade_10/math_10/semester_1/*/*.md");
|
|
if (!empty($mds)) $resolvedFile = $mds[1] ?? $mds[0];
|
|
}
|
|
}
|
|
|
|
if ($resolvedFile && file_exists($resolvedFile)) {
|
|
$content = file_get_contents($resolvedFile);
|
|
$response->json([
|
|
'status' => 'success',
|
|
'file' => str_replace("{$storage}/", '', $resolvedFile),
|
|
'subject' => $subject,
|
|
'type' => $type,
|
|
'content' => $content
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// Tailored subject fallback
|
|
$content = self::getDefaultSubjectDocument($subject, $type);
|
|
$response->json([
|
|
'status' => 'success',
|
|
'subject' => $subject,
|
|
'type' => $type,
|
|
'content' => $content
|
|
]);
|
|
}
|
|
|
|
private static function getDefaultSubjectDocument(string $subject, string $type): string
|
|
{
|
|
$s = strtolower($subject);
|
|
if (str_contains($s, 'english') || str_contains($s, 'إنجليز')) {
|
|
return "## Unit 01: Looking Good — Vocabulary & Reading\n\n### 1. Key Vocabulary & Word Formation\n- **Casual clothing:** Everyday informal garments (jeans, sneakers, hoodie).\n- **Traditional attire:** Cultural heritage wear (Jordanian Thobe and Keffiyeh).\n- **Subconscious influence:** The way external appearance affects cognitive confidence.\n\n### 2. Reading Text: The Power of First Impressions\nResearch shows that within the first seven seconds of meeting someone, people make subconscious judgments about character, competence, and reliability. In a famous experiment, doctors wearing clean white coats demonstrated higher diagnostic focus.\n\n### 3. Grammar Workshop: Articles (a, an, the, zero article)\n- Use **a / an** for non-specific singular countable nouns.\n- Use **the** when the listener knows which specific thing is meant.\n- Use **zero article** with plural or uncountable nouns spoken in general terms.";
|
|
}
|
|
if (str_contains($s, 'physic') || str_contains($s, 'فيزياء')) {
|
|
return "## الوحدة الأولى: المتجهات والكميات الفيزيائية\n\n### 1. التمييز بين الكميات القياسية والمتجهة\n- **الكميات القياسية:** تُحدد بالمقدار ووحدة القياس فقط (الكتلة، الزمن، الطاقة، درجة الحرارة).\n- **الكميات المتجهة:** تُحدد بالمقدار والاتجاه ونقطة التأثير (القوة، السرعة المتجهة، التسارع، الإزاحة).\n\n### 2. جمع وتحليل المتجهات بيانياً وتحليلياً\n- **المركبة الأفقية:** A_x = A cos θ\n- **المركبة الرأسية:** A_y = A sin θ\n- **المحصلة:** R = √(R_x² + R_y²)\n- **زاوية الاتجاه:** tan θ = |R_y / R_x|";
|
|
}
|
|
return "## ملخص الوحدة والنتاجات التعليمية المقررة\n\n### 1. الأهداف العامة للمادة\n- استيعاب المفاهيم والمصطلحات الأساسية وفق المنهاج الوزاري المعتمد.\n- التدرب على حل النماذج والتطبيقات العملية والأسئلة الوزارية.\n- التحقق من اكتساب المهارات من خلال بنك الأسئلة التكيفي والمختبر.";
|
|
}
|
|
|
|
/**
|
|
* استرجاع محتوى المختبر التفاعلي الذكي (أمثلة الكتاب الوزاري + 10 تدريبات ذكاء اصطناعي)
|
|
* يقرأ مباشرة من الملف المخبوز المسبق أو قاعدة البيانات بأقصى سرعة
|
|
* GET /api/curriculum/interactive-lab
|
|
*/
|
|
public function getInteractiveLab(Request $request, Response $response): void
|
|
{
|
|
$file = (string) $request->getQuery('file', '');
|
|
$subject = (string) $request->getQuery('subject', 'arabic_10');
|
|
$topic = (string) $request->getQuery('topic', 'inna_and_sisters');
|
|
|
|
// 1. Check if companion file exists
|
|
if (!empty($file)) {
|
|
$cachedLab = CurriculumService::getLessonLabData($file);
|
|
if (!empty($cachedLab)) {
|
|
$response->json([
|
|
'status' => 'success',
|
|
'source' => 'prebaked_companion_file',
|
|
'data' => $cachedLab
|
|
]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 2. Otherwise serve and bake on-the-fly
|
|
$data = \App\Services\AiHumanitiesLabGeneratorService::getLabContent($subject, $topic);
|
|
if (!empty($file)) {
|
|
CurriculumService::saveLessonLabData($file, $data);
|
|
}
|
|
|
|
$response->json([
|
|
'status' => 'success',
|
|
'source' => 'generated_and_cached',
|
|
'data' => $data
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* بناء وتجهيز أصول المختبر التفاعلي مسبقاً وحفظها في المنهاج وقاعدة البيانات بنقرة واحدة من الاستوديو
|
|
* POST /api/curriculum/bake-lab
|
|
*/
|
|
public function bakeInteractiveLab(Request $request, Response $response): void
|
|
{
|
|
$body = $request->getBody();
|
|
$file = $body['file'] ?? '';
|
|
$subject = $body['subject'] ?? 'arabic_10';
|
|
$topic = $body['topic'] ?? basename($file, '.md');
|
|
|
|
if (empty($file)) {
|
|
$response->status(400)->json(['status' => 'error', 'message' => 'مسار ملف الدرس مطلوب']);
|
|
return;
|
|
}
|
|
|
|
$content = CurriculumService::getLessonMarkdown($file);
|
|
$labData = \App\Services\AiHumanitiesLabGeneratorService::bakeLessonLab($file, $subject, $topic, $content);
|
|
|
|
$response->json([
|
|
'status' => 'success',
|
|
'message' => 'تم بناء وتثبيت المختبر التفاعلي كأصل ثابت في ملفات المنهاج وقاعدة البيانات بنجاح',
|
|
'file' => $file,
|
|
'lab_data' => $labData
|
|
]);
|
|
}
|
|
}
|
|
|