diff --git a/backend/app/Controllers/CurriculumController.php b/backend/app/Controllers/CurriculumController.php index a5ccc3e..398ed4b 100644 --- a/backend/app/Controllers/CurriculumController.php +++ b/backend/app/Controllers/CurriculumController.php @@ -171,24 +171,24 @@ class CurriculumController 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 + 'content' => $content, + 'ai_assets' => $assets ]); } - /** - * Save Lesson Markdown Content - */ 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' => 'بيانات الحفظ غير مكتملة']); @@ -196,6 +196,10 @@ class CurriculumController } CurriculumService::saveLessonMarkdown($file, $content); + if ($aiAssets !== null) { + CurriculumService::saveLessonAiAssets($file, $aiAssets); + } + $serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/'); $response->json([ @@ -204,4 +208,34 @@ class CurriculumController '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'; + + // We will adapt the service call here since AiLessonEnhancerService expects lessonId. + // Actually, let's create a custom static method in AiLessonEnhancerService to accept raw text. + $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' => 'فشل توليد المخرجات الذكية']); + } + } } diff --git a/backend/app/Services/AiLessonEnhancerService.php b/backend/app/Services/AiLessonEnhancerService.php index 48f92da..300ce0e 100644 --- a/backend/app/Services/AiLessonEnhancerService.php +++ b/backend/app/Services/AiLessonEnhancerService.php @@ -10,6 +10,12 @@ class AiLessonEnhancerService /** * Extracts the lively script, cheat sheet, and Socratic quiz using Gemini. */ + public static function generateFromText(string $title, string $content): ?array + { + $prompt = self::buildPrompt($title, $content); + return self::callGemini($prompt); + } + public static function enhanceLesson(int $lessonId): bool { $lesson = Database::selectOne( diff --git a/backend/app/Services/CurriculumService.php b/backend/app/Services/CurriculumService.php index e334aad..4b2ceca 100644 --- a/backend/app/Services/CurriculumService.php +++ b/backend/app/Services/CurriculumService.php @@ -156,6 +156,31 @@ class CurriculumService return "# محتوى المنهاج\nالمحتوى المعتمد للمنهاج الرسمي."; } + public static function saveLessonAiAssets(string $relativePath, array $assets): bool + { + self::ensureStorage(); + $baseName = preg_replace('/\.md$/i', '', ltrim($relativePath, '/')); + $jsonPath = self::$storagePath . '/' . $baseName . '_ai_assets.json'; + + $dir = dirname($jsonPath); + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + return file_put_contents($jsonPath, json_encode($assets, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) !== false; + } + + public static function getLessonAiAssets(string $relativePath): array + { + self::ensureStorage(); + $baseName = preg_replace('/\.md$/i', '', ltrim($relativePath, '/')); + $jsonPath = self::$storagePath . '/' . $baseName . '_ai_assets.json'; + + if (file_exists($jsonPath)) { + return json_decode(file_get_contents($jsonPath), true) ?: []; + } + return []; + } + /** * Fast Keyword Search across All Markdown Files */ diff --git a/backend/app/Views/CurriculumStudio.php b/backend/app/Views/CurriculumStudio.php index fcae40a..34dae67 100644 --- a/backend/app/Views/CurriculumStudio.php +++ b/backend/app/Views/CurriculumStudio.php @@ -197,7 +197,31 @@ class CurriculumStudio - + + + +
+
+

🧠 الاستوديو الذكي للدرس (AI Assets)

+ +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+
@@ -487,6 +511,128 @@ class CurriculumStudio } } + // ========================================================== + // 3. Status Polling & Overlay + // ========================================================== + let pollInterval; + function checkStatus() { + pollInterval = setInterval(async () => { + const res = await fetch('/api/curriculum/status'); + const state = await res.json(); + + if (state.status === 'processing') { + const pct = state.progress || 0; + document.getElementById('upload_progress_fill').style.width = pct + '%'; + document.getElementById('upload_progress_text').textContent = state.message || 'جاري المعالجة...'; + } else if (state.status === 'done') { + clearInterval(pollInterval); + document.getElementById('upload_progress_fill').style.width = '100%'; + document.getElementById('upload_progress_text').textContent = '✅ اكتمل استخراج المنهاج بنجاح!'; + document.getElementById('btn_close_overlay').style.display = 'inline-block'; + resetUploadBtn(); + + setTimeout(() => { + if (state.extracted_data) { + renderTree(state.extracted_data); + if (state.active_file) { + selectLesson(state.active_file, state.extracted_data?.subject_name || 'الدرس المستخرج', 'المنهاج المستخرج', []); + } + } else { + location.reload(); + } + }, 2000); + } else if (state.status === 'error') { + document.getElementById('upload_progress_text').textContent = '⚠️ فشل: ' + state.message; + document.getElementById('btn_close_overlay').style.display = 'inline-block'; + resetUploadBtn(); + } else { + clearInterval(pollInterval); + } + }, 1500); + } + + // ========================================================== + // 4. Save Content + // ========================================================== + async function saveLessonContent() { + if (!currentFilePath) return; + const content = document.getElementById('lesson_markdown_editor').value; + + let socraticJson = null; + const socraticRaw = document.getElementById('ai_socratic_quiz_editor').value.trim(); + if (socraticRaw) { + try { + socraticJson = JSON.parse(socraticRaw); + } catch(e) { + alert('⚠️ خطأ في صيغة JSON الخاصة بالفحص السقراطي'); + return; + } + } + + const aiAssets = { + ai_video_url: document.getElementById('ai_video_url_input').value, + cheat_sheet: document.getElementById('ai_cheat_sheet_editor').value, + socratic_quiz: socraticJson + }; + + try { + const res = await fetch('/api/curriculum/save-lesson', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ file: currentFilePath, content: content, ai_assets: aiAssets }) + }); + const data = await res.json(); + if (res.ok && data.status === 'success') { + alert(`✅ تم حفظ واعتماد التعديلات بنجاح في الملف:\n${data.server_full_path || currentFilePath}`); + } else { + alert('⚠️ فشل حفظ الملف'); + } + } catch (e) { + console.error(e); + alert('⚠️ حدث خطأ في الاتصال'); + } + } + + // ========================================================== + // 5. Generate AI Assets automatically + // ========================================================== + async function generateAiAssets() { + if (!currentFilePath) return; + const content = document.getElementById('lesson_markdown_editor').value; + if (!content) { + alert('يجب أن يحتوي الدرس على نص أولاً'); + return; + } + + const btn = window.event.currentTarget; + const originalText = btn.innerHTML; + btn.innerHTML = '⏳ جاري التوليد الذكي...'; + btn.disabled = true; + + try { + const res = await fetch('/api/curriculum/generate-ai-assets', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ file: currentFilePath }) + }); + const data = await res.json(); + + if (res.ok && data.status === 'success' && data.ai_assets) { + document.getElementById('ai_cheat_sheet_editor').value = data.ai_assets.cheat_sheet || ''; + document.getElementById('ai_socratic_quiz_editor').value = data.ai_assets.socratic_quiz ? JSON.stringify(data.ai_assets.socratic_quiz, null, 2) : ''; + alert('✅ تم توليد الملخص والاختبار السقراطي بنجاح!'); + } else { + alert('⚠️ فشل التوليد: ' + (data.message || 'خطأ غير معروف')); + } + } catch (e) { + console.error(e); + alert('⚠️ حدث خطأ في الاتصال بمحرك الذكاء الاصطناعي'); + } finally { + btn.innerHTML = originalText; + btn.disabled = false; + } + } + function openDirectLogViewer() { const overlay = document.getElementById('upload_progress_overlay'); overlay.style.display = 'flex'; diff --git a/backend/public/index.php b/backend/public/index.php index e168feb..6da8f64 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -62,6 +62,7 @@ $router->get('/api/curriculum/upload-log', [\App\Controllers\CurriculumControlle $router->get('/api/curriculum/tree', [\App\Controllers\CurriculumController::class, 'getTree']); $router->get('/api/curriculum/lesson', [\App\Controllers\CurriculumController::class, 'getLessonContent']); $router->post('/api/curriculum/save-lesson', [\App\Controllers\CurriculumController::class, 'saveLessonContent']); +$router->post('/api/curriculum/generate-ai-assets', [\App\Controllers\CurriculumController::class, 'generateAiAssets']); $router->get('/api/curriculum/search', function ($request, $response) { $q = $request->getQueryParams()['q'] ?? ''; $response->json([