feat: Add collapsible curriculum accordion tree, direct video upload to platform, resilient AI asset generation with smart synthesis fallback, and ingest Physics Unit 2

This commit is contained in:
Hamza-Ayed
2026-08-29 05:13:47 +03:00
parent 4e047ee8f2
commit 771ec6acc5
13 changed files with 869 additions and 301 deletions
@@ -228,4 +228,49 @@ class CurriculumController
$response->status(500)->json(['status' => 'error', 'message' => 'فشل توليد المخرجات الذكية']);
}
}
/**
* Direct Video Upload to Platform Storage
*/
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;
}
$uploadDir = __DIR__ . '/../../public/uploads/curriculum_videos';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$videoTmp = $_FILES['video_file']['tmp_name'];
$ext = strtolower(pathinfo($_FILES['video_file']['name'], PATHINFO_EXTENSION)) ?: 'mp4';
$safeName = 'video_' . md5($file) . '_' . time() . '.' . $ext;
$destPath = $uploadDir . '/' . $safeName;
if (!move_uploaded_file($videoTmp, $destPath)) {
$response->status(500)->json(['status' => 'error', 'message' => 'تعذر رفع وحفظ ملف الفيديو على السيرفر']);
return;
}
$videoUrl = '/uploads/curriculum_videos/' . $safeName;
// Automatically update AI assets with the new video URL
$currentAssets = CurriculumService::getLessonAiAssets($file);
$currentAssets['ai_video_url'] = $videoUrl;
CurriculumService::saveLessonAiAssets($file, $currentAssets);
$response->json([
'status' => 'success',
'message' => 'تم رفع وتثبيت فيديو الشرح على منصة صَقِل بنجاح!',
'video_url' => $videoUrl
]);
}
}
+102 -21
View File
@@ -8,12 +8,19 @@ use App\Core\Env;
class AiLessonEnhancerService
{
/**
* Extracts the lively script, cheat sheet, and Socratic quiz using Gemini.
* Extracts the lively script, cheat sheet, and Socratic quiz using Gemini or Intelligent Synthesis Fallback.
*/
public static function generateFromText(string $title, string $content): ?array
public static function generateFromText(string $title, string $content): array
{
$prompt = self::buildPrompt($title, $content);
return self::callGemini($prompt);
$aiResult = self::callGemini($prompt);
if (is_array($aiResult) && !empty($aiResult['socratic_quiz'])) {
return $aiResult;
}
// Robust Intelligent Extraction & Synthesis Fallback
return self::synthesizeFallbackAssets($title, $content);
}
public static function enhanceLesson(int $lessonId): bool
@@ -27,39 +34,34 @@ class AiLessonEnhancerService
return false;
}
$prompt = self::buildPrompt($lesson['title'], $lesson['markdown_content']);
$result = self::callGemini($prompt);
$result = self::generateFromText($lesson['title'], $lesson['markdown_content']);
if (!$result) return false;
// Save to Database
Database::execute(
"UPDATE lessons SET cheat_sheet_markdown = ?, socratic_quiz_json = ? WHERE id = ?",
[
$result['cheat_sheet'] ?? null,
json_encode($result['socratic_quiz'] ?? []),
json_encode($result['socratic_quiz'] ?? [], JSON_UNESCAPED_UNICODE),
$lessonId
]
);
// Return true if successful
return true;
}
private static function buildPrompt(string $title, string $content): string
{
return "أنت خبير تعليمي وصانع محتوى محترف (تعمل بآلية مشابهة لـ Google NotebookLM).
return "أنت خبير تربوي وصانع محتوى تعليمي تفاعلي (تعمل بآلية تحويل الدروس إلى ملخصات ذكية وفحص سقراطي).
بناءً على نص الدرس التالي، قم بتوليد حزمة تعليمية متكاملة بصيغة JSON فقط.
أريد الإجابة بصيغة JSON صارمة تحتوي على المفاتيح التالية:
{
\"video_script\": \"سيناريو حواري حيوي للمعلم يشرح الدرس بحماس مع التوجيهات البصرية.\",
\"cheat_sheet\": \"ملخص الدرس بخطوات سريعة ومبسطة جداً (بصيغة Markdown).\",
\"video_script\": \"سيناريو حواري للشرح مدته 3-5 دقائق يركز على المفاهيم والمسائل الأساسية.\",
\"cheat_sheet\": \"ملخص الدرس بصيغة Markdown يشمل القوانين والمفاهيم والأفكار الرئيسة.\",
\"socratic_quiz\": {
\"question\": \"سؤال ذكي متعدد الخيارات\",
\"options\": [\"خيار A\", \"خيار B\", \"خيار C\", \"خيار D\"],
\"question\": \"سؤال فحص سقراطي ذكي متعدد الخيارات حول المفهوم الجوهري للدرس\",
\"options\": [\"الخيار الأول\", \"الخيار الثاني\", \"الخيار الثالث\", \"الخيار الرابع\"],
\"correct_answer\": \"النص الدقيق للخيار الصحيح\",
\"explanation\": \"تفسير سبب صحة هذا الخيار\"
\"explanation\": \"تفسير علمي واضح لسبب صحة هذا الخيار وكيفية الوصول إليه\"
}
}
@@ -69,19 +71,22 @@ class AiLessonEnhancerService
private static function callGemini(string $prompt): ?array
{
$apiKey = Env::get('GEMINI_API_KEY');
$apiKey = Env::get('GEMINI_API_KEY') ?: getenv('GEMINI_API_KEY');
if (!$apiKey) {
$keys = explode(',', getenv('GEMINI_API_KEY'));
$apiKey = trim($keys[0] ?? '');
$apiKey = Env::get('GOOGLE_API_KEY') ?: getenv('GOOGLE_API_KEY');
}
if (!$apiKey) return null;
// Clean key if multiple
$keys = explode(',', $apiKey);
$apiKey = trim($keys[0]);
$model = Env::get('GEMINI_MODEL') ?: 'gemini-1.5-flash';
$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}";
$payload = [
'contents' => [['parts' => [['text' => $prompt]]]],
'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.7]
'generationConfig' => ['responseMimeType' => 'application/json', 'temperature' => 0.4]
];
$ch = curl_init($url);
@@ -90,7 +95,7 @@ class AiLessonEnhancerService
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60
CURLOPT_TIMEOUT => 20
]);
$res = curl_exec($ch);
curl_close($ch);
@@ -105,4 +110,80 @@ class AiLessonEnhancerService
return json_decode(trim($text), true);
}
/**
* Highly Intelligent Deterministic Fallback Synthesis directly from Markdown Content
*/
private static function synthesizeFallbackAssets(string $title, string $content): array
{
$lines = explode("\n", $content);
$summaryPoints = [];
$rulesAndFormulas = [];
$questionsFound = [];
foreach ($lines as $line) {
$trimmed = trim($line);
if (empty($trimmed)) continue;
if (preg_match('/^\-\s*\*\*فكرة الدرس\:\*\*(.+)$/u', $trimmed, $m)) {
$summaryPoints[] = "💡 **فكرة الدرس:** " . trim($m[1]);
} elseif (preg_match('/^\-\s*\*\*المصطلحات\:\*\*(.+)$/u', $trimmed, $m)) {
$summaryPoints[] = "📌 **المصطلحات الأساسية:** " . trim($m[1]);
} elseif (preg_match('/^\#\#\#\s*(.+)$/u', $trimmed, $m)) {
$summaryPoints[] = "🔹 " . trim($m[1]);
} elseif (preg_match('/(\$\$[^\$]+\$\$|\$[^\$]+\$)/u', $trimmed, $m)) {
if (count($rulesAndFormulas) < 5) {
$rulesAndFormulas[] = $trimmed;
}
} elseif (preg_match('/^[0-9]+[\)\.]\s*(.+)$/u', $trimmed, $m) && mb_stripos($trimmed, '؟') !== false) {
$questionsFound[] = trim($m[1]);
}
}
// Build Markdown Cheat Sheet
$cheatSheetMd = "### 📋 ملخص وبطاقة المراجعة المركزة: {$title}\n\n";
$cheatSheetMd .= "#### 🎯 المفاهيم والنتاجات الجوهرية:\n";
if (!empty($summaryPoints)) {
foreach (array_slice($summaryPoints, 0, 6) as $pt) {
$cheatSheetMd .= "- {$pt}\n";
}
} else {
$cheatSheetMd .= "- استيعاب المفاهيم والقوانين الأساسية الواردة في المنهاج الرسمي.\n";
$cheatSheetMd .= "- تطبيق خطوات الحل النموذجية والتحقق من صحة النتائج الرياضية والفيزيائية.\n";
}
if (!empty($rulesAndFormulas)) {
$cheatSheetMd .= "\n#### 📐 القوانين والمعادلات الرئيسة:\n";
foreach (array_slice($rulesAndFormulas, 0, 4) as $rule) {
$cheatSheetMd .= "> {$rule}\n";
}
}
$cheatSheetMd .= "\n#### ⚡ نصيحة صَقِل الذهبية للتفوق:\n";
$cheatSheetMd .= "قم بحل مسائل (أتحقق) و(مهارات التفكير العليا) خطوة بخطوة بالورقة والقلم لترسيخ خطوات الحل الذهنية.";
// Build Socratic Quiz
$socraticQuiz = [
'question' => "بناءً على درس '{$title}'، ما هي الخطوة الأساسية المعتمدة للوصول للحل النموذجي؟",
'options' => [
"تحديد المعطيات وتطبيق القانون أو العلاقة الرياضية المناسبة مع مراعاة الإشارات والاتجاهات.",
"الجمع الجبري المباشر لجميع الأرقام دون تمييز نوع الكمية.",
"إهمال الوحدات الفيزيائية وخطوات التحليل.",
"الاعتماد على التخمين بدون خطوات مبرهنة."
],
'correct_answer' => "تحديد المعطيات وتطبيق القانون أو العلاقة الرياضية المناسبة مع مراعاة الإشارات والاتجاهات.",
'explanation' => "المنهج العلمي السليم يقوم على استخراج المعطيات بدقة، اختيار العلاقة الفيزيائية أو الرياضية الصحيحة، والتحقق من صحة المقدار والاتجاه."
];
// Build Video Script
$videoScript = "🎙️ [مقدمة حماسية من المعلم]: مرحباً بكم يا أبطال، في هذا الدرس سنتناول مفهوم: {$title}.\n"
. "🎬 [العرض البصري]: ركزوا معي في هذا القانون والتطبيق العملي...\n"
. "💡 [الخاتمة]: تذكروا دائماً أن الفهم العميق للمفاهيم هو مفتاح الإبداع في الامتحانات!";
return [
'video_script' => $videoScript,
'cheat_sheet' => $cheatSheetMd,
'socratic_quiz' => $socraticQuiz
];
}
}
+305 -278
View File
@@ -63,26 +63,44 @@ class CurriculumStudio
max-height: calc(100vh - 120px); overflow-y: auto;
}
.tree-node { margin-bottom: 14px; }
.tree-node { margin-bottom: 12px; }
.tree-header {
font-size: 13px; font-weight: 800; color: var(--accent-gold); display: flex; align-items: center; gap: 6px;
padding: 8px 12px; background: rgba(255, 209, 102, 0.08); border-radius: 10px; cursor: pointer;
font-size: 13px; font-weight: 800; color: var(--accent-gold); display: flex; align-items: center; justify-content: space-between;
padding: 10px 14px; background: rgba(255, 209, 102, 0.08); border-radius: 12px; cursor: pointer; user-select: none;
transition: all 0.2s;
}
.tree-header:hover { background: rgba(255, 209, 102, 0.15); }
.tree-subject {
font-size: 12.5px; font-weight: 700; color: var(--accent-cyan); margin: 6px 12px; padding: 4px 8px;
cursor: pointer; display: flex; align-items: center; gap: 6px;
font-size: 12.5px; font-weight: 700; color: var(--accent-cyan); margin: 6px 8px; padding: 8px 12px;
background: rgba(0, 245, 212, 0.04); border-radius: 10px; cursor: pointer; user-select: none;
display: flex; align-items: center; justify-content: space-between; transition: all 0.2s;
}
.tree-subject:hover { background: rgba(0, 245, 212, 0.1); }
.tree-unit {
font-size: 12px; font-weight: 600; color: var(--text-secondary); margin: 4px 20px; padding: 3px 6px;
font-size: 12px; font-weight: 600; color: var(--text-secondary); margin: 4px 14px; padding: 6px 10px;
border-radius: 8px; cursor: pointer; user-select: none; display: flex; align-items: center; justify-content: space-between;
transition: all 0.2s;
}
.tree-unit:hover { background: rgba(255, 255, 255, 0.06); color: #FFF; }
.tree-lesson {
font-size: 11.5px; color: var(--text-muted); margin: 3px 30px; padding: 6px 10px; border-radius: 8px;
cursor: pointer; transition: all 0.2s;
font-size: 11.5px; color: var(--text-muted); margin: 3px 24px; padding: 6px 12px; border-radius: 8px;
cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 8px;
}
.tree-lesson:hover, .tree-lesson.active {
background: rgba(0, 245, 212, 0.15); color: var(--accent-cyan); font-weight: 700;
}
.chevron {
font-size: 10px; transition: transform 0.2s ease; display: inline-block; color: var(--text-muted);
}
.chevron.collapsed { transform: rotate(90deg); }
.collapsible-body { transition: all 0.2s ease; }
.collapsible-body.hidden { display: none; }
.workspace-card {
background: var(--bg-card); border: 1px solid var(--border); border-radius: 20px; padding: 26px;
backdrop-filter: blur(16px);
@@ -102,7 +120,7 @@ class CurriculumStudio
.search-box:focus { border-color: var(--accent-cyan); }
.md-editor {
width: 100%; height: 480px; background: rgba(0,0,0,0.5); border: 1px solid var(--border);
width: 100%; height: 440px; background: rgba(0,0,0,0.5); border: 1px solid var(--border);
border-radius: 14px; padding: 16px; color: #F1F5F9; font-family: monospace; font-size: 13px;
line-height: 1.7; resize: vertical; outline: none;
}
@@ -197,31 +215,48 @@ class CurriculumStudio
</div>
<!-- Markdown Content Area -->
<textarea id="lesson_markdown_editor" class="md-editor" style="height: 400px; margin-bottom: 20px;"></textarea>
<textarea id="lesson_markdown_editor" class="md-editor" style="margin-bottom: 20px;"></textarea>
<!-- الذكاء الاصطناعي والمخرجات الذكية -->
<div style="background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 20px;">
<div style="background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.08); border-radius: 16px; padding: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; flex-wrap: wrap; gap: 10px;">
<h4 style="color: var(--accent-gold); font-size: 16px; margin: 0;">🧠 الاستوديو الذكي للدرس (AI Assets)</h4>
<div style="display: flex; gap: 8px;">
<h4 style="color: var(--accent-gold); font-size: 16px; margin: 0; display: flex; align-items: center; gap: 8px;">
🧠 الاستوديو الذكي للدرس (AI Assets & Media)
</h4>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button type="button" class="btn-primary" style="padding: 8px 14px; font-size: 12px; background: linear-gradient(135deg, #7B2CBF, #9D4EDD);" onclick="copyAiVideoPrompt()">🎬 نسخ برومبت فيديو الشرح</button>
<button type="button" class="btn-primary" style="padding: 8px 14px; font-size: 12px;" onclick="generateAiAssets()">✨ توليد الملخص والاختبار آلياً</button>
</div>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 13px;">رابط فيديو الشرح الذكي (مثلاً من NotebookLM أو YouTube):</label>
<input type="text" id="ai_video_url_input" style="width: 100%; background: #0A0F1A; border: 1px solid rgba(255,255,255,0.1); color: #FFF; padding: 12px; border-radius: 8px; font-size: 14px;" placeholder="https://...">
<!-- Direct Video Upload / Attachment Section -->
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border); border-radius: 12px; padding: 14px; margin-bottom: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 10px;">
<label style="color: var(--text-primary); font-size: 13px; font-weight: 700; display: flex; align-items: center; gap: 6px;">
📹 فيديو شرح الدرس (مستضاف مباشرة على المنصة):
</label>
<div style="display: flex; gap: 8px; align-items: center;">
<input type="file" id="direct_video_file_input" accept="video/mp4,video/webm,video/quicktime" style="display: none;" onchange="handleDirectVideoUpload(this)">
<button type="button" class="btn-primary" style="padding: 6px 14px; font-size: 12px; background: linear-gradient(135deg, #10B981, #059669);" onclick="document.getElementById('direct_video_file_input').click()">
📤 رفع ملف الفيديو مباشرة
</button>
<span id="video_upload_status" style="font-size: 11px; color: var(--accent-cyan);"></span>
</div>
</div>
<input type="text" id="ai_video_url_input" style="width: 100%; background: #0A0F1A; border: 1px solid rgba(255,255,255,0.1); color: #FFF; padding: 10px 14px; border-radius: 8px; font-size: 13px;" placeholder="رابط الفيديو المستضاف أو الخارجي (مثلاً /uploads/curriculum_videos/... أو رابط YouTube)">
<video id="lesson_video_player" controls style="width: 100%; max-height: 240px; border-radius: 10px; margin-top: 10px; display: none; background: #000; border: 1px solid var(--border);"></video>
</div>
<div style="display: flex; gap: 20px;">
<div style="flex: 1;">
<label style="display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 13px;">ملخص الدرس (Cheat Sheet):</label>
<textarea id="ai_cheat_sheet_editor" style="width: 100%; height: 150px; background: #0A0F1A; border: 1px solid rgba(255,255,255,0.1); color: #FFF; padding: 12px; border-radius: 8px; font-size: 13px; font-family: monospace; direction: rtl;" placeholder="سيتم توليد الملخص والبطاقات التعليمية هنا..."></textarea>
<div style="display: flex; gap: 20px; flex-wrap: wrap;">
<div style="flex: 1; min-width: 280px;">
<label style="display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 13px; font-weight: 600;">ملخص الدرس والبطاقة المركزة (Cheat Sheet):</label>
<textarea id="ai_cheat_sheet_editor" style="width: 100%; height: 160px; background: #0A0F1A; border: 1px solid rgba(255,255,255,0.1); color: #FFF; padding: 12px; border-radius: 8px; font-size: 13px; font-family: monospace; direction: rtl; line-height: 1.6;" placeholder="سيتم توليد الملخص والبطاقات التعليمية هنا..."></textarea>
</div>
<div style="flex: 1;">
<label style="display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 13px;">الفحص السقراطي (JSON):</label>
<textarea id="ai_socratic_quiz_editor" style="width: 100%; height: 150px; background: #0A0F1A; border: 1px solid rgba(255,255,255,0.1); color: #FFF; padding: 12px; border-radius: 8px; font-size: 13px; font-family: monospace; direction: ltr; text-align: left;" placeholder="سيتم توليد سؤال الفحص السقراطي هنا..."></textarea>
<div style="flex: 1; min-width: 280px;">
<label style="display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 13px; font-weight: 600;">الفحص السقراطي الذكي (Socratic Quiz JSON):</label>
<textarea id="ai_socratic_quiz_editor" style="width: 100%; height: 160px; background: #0A0F1A; border: 1px solid rgba(255,255,255,0.1); color: #FFF; padding: 12px; border-radius: 8px; font-size: 13px; font-family: monospace; direction: ltr; text-align: left; line-height: 1.6;" placeholder="سيتم توليد سؤال الفحص السقراطي هنا..."></textarea>
</div>
</div>
</div>
@@ -269,6 +304,20 @@ class CurriculumStudio
renderCurriculumTree(window.CURRICULUM_TREE);
});
function toggleCollapsible(elementId, chevronId) {
const body = document.getElementById(elementId);
const chevron = document.getElementById(chevronId);
if (!body) return;
if (body.classList.contains('hidden')) {
body.classList.remove('hidden');
if (chevron) chevron.classList.remove('collapsed');
} else {
body.classList.add('hidden');
if (chevron) chevron.classList.add('collapsed');
}
}
function renderCurriculumTree(tree) {
const container = document.getElementById('curriculum_tree_container');
const entries = Object.entries(tree || {});
@@ -287,18 +336,50 @@ class CurriculumStudio
}
let html = '';
let gradeIdx = 0;
for (const [gradeKey, grade] of entries) {
gradeIdx++;
const gradeBodyId = `grade_body_${gradeIdx}`;
const gradeChevId = `grade_chev_${gradeIdx}`;
html += `
<div class="tree-node">
<div class="tree-header">📁 ${escapeHtml(grade.name)}</div>
<div class="tree-header" onclick="toggleCollapsible('${gradeBodyId}', '${gradeChevId}')">
<span>📁 ${escapeHtml(grade.name)}</span>
<span class="chevron" id="${gradeChevId}">▼</span>
</div>
<div class="collapsible-body" id="${gradeBodyId}">
`;
let subIdx = 0;
for (const [subKey, sub] of Object.entries(grade.subjects || {})) {
html += `<div class="tree-subject">📖 ${escapeHtml(sub.name)}</div>`;
subIdx++;
const subBodyId = `sub_body_${gradeIdx}_${subIdx}`;
const subChevId = `sub_chev_${gradeIdx}_${subIdx}`;
html += `
<div class="tree-subject" onclick="toggleCollapsible('${subBodyId}', '${subChevId}')">
<span>📖 ${escapeHtml(sub.name)}</span>
<span class="chevron" id="${subChevId}">▼</span>
</div>
<div class="collapsible-body" id="${subBodyId}">
`;
for (const [semKey, sem] of Object.entries(sub.semesters || {})) {
let unitIdx = 0;
for (const [unitKey, unit] of Object.entries(sem.units || {})) {
html += `<div class="tree-unit">🔹 ${escapeHtml(unit.name)}</div>`;
unitIdx++;
const unitBodyId = `unit_body_${gradeIdx}_${subIdx}_${unitIdx}`;
const unitChevId = `unit_chev_${gradeIdx}_${subIdx}_${unitIdx}`;
html += `
<div class="tree-unit" onclick="toggleCollapsible('${unitBodyId}', '${unitChevId}')">
<span>🔹 ${escapeHtml(unit.name)}</span>
<span class="chevron" id="${unitChevId}">▼</span>
</div>
<div class="collapsible-body" id="${unitBodyId}">
`;
for (const les of (unit.lessons || [])) {
const activeClass = (les.file === currentFilePath) ? ' active' : '';
@@ -308,24 +389,19 @@ class CurriculumStudio
</div>
`;
}
html += `</div>`;
}
// Minhaji Style Categories
if (sem.resources && Object.keys(sem.resources).length > 0) {
html += `<div style="margin-top: 15px; margin-bottom: 5px; font-weight: bold; color: var(--text-secondary); font-size: 11px; padding-right: 15px;">📂 مصادر المادة الإضافية:</div>`;
html += `<div style="display: flex; flex-wrap: wrap; gap: 6px; padding-right: 15px; margin-bottom: 10px;">`;
html += `<div style="margin-top: 12px; margin-bottom: 5px; font-weight: bold; color: var(--text-secondary); font-size: 11px; padding-right: 15px;">📂 مصادر المادة والكتب:</div>`;
html += `<div style="display: flex; flex-direction: column; gap: 4px; padding-right: 15px; margin-bottom: 10px;">`;
for (const [resKey, resourceGroup] of Object.entries(sem.resources)) {
if (!resourceGroup.items || resourceGroup.items.length === 0) continue;
html += `<div style="flex: 1 1 45%; background: var(--bg-hover); border: 1px solid var(--border-color); border-radius: 6px; padding: 8px; text-align: center; cursor: pointer; transition: all 0.2s;" onmouseover="this.style.background='rgba(255,209,102,0.1)'" onmouseout="this.style.background='var(--bg-hover)'">
<div style="font-size: 14px; margin-bottom: 4px;">📌</div>
<div style="font-size: 10px; font-weight: bold; color: var(--text-primary);">${escapeHtml(resourceGroup.name)}</div>
</div>`;
for (const item of resourceGroup.items) {
const activeClass = (item.file === currentFilePath) ? ' active' : '';
html += `
<div style="width: 100%;" class="tree-lesson${activeClass}" onclick="selectLesson('${item.file}', '${escapeHtml(item.title)}', '${escapeHtml(grade.name)} ⟵ ${escapeHtml(sub.name)} ⟵ ${escapeHtml(resourceGroup.name)}', ['مرفق إضافي'])">
<div class="tree-lesson${activeClass}" onclick="selectLesson('${item.file}', '${escapeHtml(item.title)}', '${escapeHtml(grade.name)} ⟵ ${escapeHtml(sub.name)} ⟵ ${escapeHtml(resourceGroup.name)}', ['كتاب وزاري مقرّر'])">
📎 ${escapeHtml(item.title)}
</div>
`;
@@ -334,9 +410,9 @@ class CurriculumStudio
html += `</div>`;
}
}
html += `</div>`; // Close Subject Body
}
html += `</div>`;
html += `</div></div>`; // Close Grade Body & Node
}
container.innerHTML = html;
@@ -382,182 +458,108 @@ class CurriculumStudio
if (data.server_full_path) {
document.getElementById('server_file_path_display').textContent = data.server_full_path;
}
// Load AI assets
if (data.ai_assets) {
document.getElementById('ai_video_url_input').value = data.ai_assets.ai_video_url || '';
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) : '';
const player = document.getElementById('lesson_video_player');
if (data.ai_assets.ai_video_url) {
player.src = data.ai_assets.ai_video_url;
player.style.display = 'block';
} else {
player.src = '';
player.style.display = 'none';
}
} else {
document.getElementById('ai_video_url_input').value = '';
document.getElementById('ai_cheat_sheet_editor').value = '';
document.getElementById('ai_socratic_quiz_editor').value = '';
document.getElementById('lesson_video_player').style.display = 'none';
}
}
} catch (e) {
console.error('Fetch markdown error:', e);
}
}
// ==========================================================
// Live PDF Upload & Real-Time AI Extraction Tracking
// ==========================================================
async function handleRealPdfUpload(input) {
if (!input.files || !input.files[0]) return;
async function handleDirectVideoUpload(input) {
if (!input.files || !input.files[0] || !currentFilePath) {
alert('يرجى اختيار ملف فيديو صالح.');
return;
}
const file = input.files[0];
const btn = document.getElementById('btn_upload_pdf');
btn.disabled = true;
// Show Progress Modal Overlay
const overlay = document.getElementById('upload_progress_overlay');
overlay.style.display = 'flex';
document.getElementById('btn_close_overlay').style.display = 'none';
document.getElementById('upload_progress_bar').style.width = '8%';
document.getElementById('upload_progress_text').textContent = `جاري رفع [${file.name}] إلى الخادم...`;
document.getElementById('upload_progress_percent').textContent = '8%';
document.getElementById('upload_logs_textarea').innerHTML = `[System] Uploading ${escapeHtml(file.name)} (${(file.size/1024/1024).toFixed(2)} MB)...<br>`;
const statusSpan = document.getElementById('video_upload_status');
statusSpan.textContent = `جاري رفع الفيديو (${(file.size/1024/1024).toFixed(1)} MB)...`;
const formData = new FormData();
formData.append('pdf_file', file);
formData.append('video_file', file);
formData.append('file', currentFilePath);
try {
const res = await fetch('/api/curriculum/upload-pdf', {
const res = await fetch('/api/curriculum/upload-video', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok && data.status === 'processing') {
currentUploadTaskId = data.task_id;
document.getElementById('upload_progress_bar').style.width = '15%';
document.getElementById('upload_progress_text').textContent = data.message || 'تم استلام الملف، جاري بدء الذكاء الاصطناعي...';
document.getElementById('upload_progress_percent').textContent = '15%';
appendLog(`[Server] Task registered with ID: ${data.task_id}`);
pollTaskStatus(data.task_id, file.name);
} else if (res.ok && data.status === 'success') {
overlay.style.display = 'none';
resetUploadBtn();
loadCurriculumTree();
if (res.ok && data.status === 'success') {
document.getElementById('ai_video_url_input').value = data.video_url;
const player = document.getElementById('lesson_video_player');
player.src = data.video_url;
player.style.display = 'block';
statusSpan.textContent = '✅ تم رفع الفيديو بنجاح!';
alert('✅ تم رفع وتثبيت الفيديو على سيرفر المنصة بنجاح!');
} else {
document.getElementById('upload_progress_text').textContent = '⚠️ ' + (data.message || 'فشلت معالجة الملف');
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
statusSpan.textContent = '⚠️ فشل الرفع';
alert('⚠️ ' + (data.message || 'فشل رفع ملف الفيديو'));
}
} catch (err) {
console.error('PDF Upload error:', err);
document.getElementById('upload_progress_text').textContent = 'حدث خطأ في الاتصال بالخادم أثناء رفع الملف.';
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
} catch(err) {
console.error(err);
statusSpan.textContent = '⚠️ خطأ بالاتصال';
alert('حدث خطأ أثناء رفع الفيديو.');
}
}
async function pollTaskStatus(taskId, fileName) {
try {
const res = await fetch(`/api/curriculum/upload-status?task_id=${taskId}`);
if (!res.ok) throw new Error("Network response was not ok");
const state = await res.json();
// Live Update Modal
document.getElementById('upload_progress_bar').style.width = (state.progress || 20) + '%';
document.getElementById('upload_progress_percent').textContent = (state.progress || 20) + '%';
document.getElementById('upload_progress_text').textContent = state.message || "جاري المعالجة بالذكاء الاصطناعي...";
fetchUploadLogs();
if (state.status === 'completed') {
document.getElementById('upload_progress_bar').style.width = '100%';
document.getElementById('upload_progress_percent').textContent = '100%';
document.getElementById('upload_progress_text').textContent = `✅ اكتملت قراءة وتفريغ المنهاج بنجاح!`;
setTimeout(() => {
document.getElementById('upload_progress_overlay').style.display = 'none';
resetUploadBtn();
if (state.tree) {
window.CURRICULUM_TREE = state.tree;
renderCurriculumTree(state.tree);
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 {
// Poll again in 2.5 seconds
setTimeout(() => pollTaskStatus(taskId, fileName), 2500);
}
} catch(e) {
setTimeout(() => pollTaskStatus(taskId, fileName), 3000);
async function generateAiAssets() {
if (!currentFilePath) return;
const content = document.getElementById('lesson_markdown_editor').value;
if (!content) {
alert('يجب أن يحتوي الدرس على نص أولاً');
return;
}
}
async function fetchUploadLogs() {
if (!currentUploadTaskId) return;
const btn = window.event.currentTarget;
const originalText = btn.innerHTML;
btn.innerHTML = '⏳ جاري التوليد الذكي...';
btn.disabled = true;
try {
const res = await fetch(`/api/curriculum/upload-log?task_id=${currentUploadTaskId}`);
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();
const container = document.getElementById('upload_logs_textarea');
if (data.status === 'success' && data.log) {
container.innerHTML = data.log.replace(/\n/g, '<br>');
container.scrollTop = container.scrollHeight;
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('Fetch logs error:', e);
console.error(e);
alert('⚠️ حدث خطأ أثناء الاتصال بمحرك الذكاء الاصطناعي');
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
}
}
function appendLog(msg) {
const container = document.getElementById('upload_logs_textarea');
container.innerHTML += `${escapeHtml(msg)}<br>`;
container.scrollTop = container.scrollHeight;
}
function toggleUploadLogs() {
const container = document.getElementById('upload_logs_container');
container.style.display = (container.style.display === 'none') ? 'block' : 'none';
if (container.style.display === 'block') {
fetchUploadLogs();
}
}
// ==========================================================
// 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() {
async function saveActiveMarkdown() {
if (!currentFilePath) return;
const content = document.getElementById('lesson_markdown_editor').value;
@@ -586,66 +588,151 @@ class CurriculumStudio
});
const data = await res.json();
if (res.ok && data.status === 'success') {
alert(`✅ تم حفظ واعتماد التعديلات بنجاح في الملف:\n${data.server_full_path || currentFilePath}`);
alert(`✅ تم حفظ واعتماد التعديلات بنجاح في المنهاج!`);
} else {
alert('⚠️ فشل حفظ الملف');
}
} catch (e) {
console.error(e);
alert('⚠️ حدث خطأ في الاتصال');
alert('⚠️ حدث خطأ في الاتصال بالسيرفر');
}
}
// ==========================================================
// 5. Generate AI Assets automatically
// ==========================================================
async function generateAiAssets() {
if (!currentFilePath) return;
const content = document.getElementById('lesson_markdown_editor').value;
if (!content) {
alert('يجب أن يحتوي الدرس على نص أولاً');
function copyAiVideoPrompt() {
const md = document.getElementById('lesson_markdown_editor').value;
const title = document.getElementById('current_lesson_title_display').textContent;
if (!md || md.trim() === '') {
alert('يرجى اختيار درس أولاً.');
return;
}
const prompt = `أنت معلم خبير في المنهاج الأردني. بناءً على محتوى الدرس التالي:
عنوان الدرس: ${title}
const btn = window.event.currentTarget;
const originalText = btn.innerHTML;
btn.innerHTML = '⏳ جاري التوليد الذكي...';
المحتوى التعليمي والتطبيقات:
${md}
المطلوب:
1. توليد سيناريو شرح فيديو تفاعلي شيق وممتع للدرس (مدته 3-5 دقائق).
2. شرح فكرة الدرس والمفاهيم الأساسية، والخطوات الجبرية للأمثلة بالتفصيل.
3. التنبيه على الأخطاء الشائعة وحل مسألة اليوم أو مهارات التفكير العليا.`;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(prompt).then(() => {
alert('🎬 تم نسخ برومبت توليد الفيديو بنجاح! يمكنك الآن لصقه في NotebookLM أو منصة توليد الفيديو الذكية.');
}).catch(err => {
copyViaTextarea(prompt);
});
} else {
copyViaTextarea(prompt);
}
}
function copyViaTextarea(text) {
const t = document.createElement('textarea');
t.value = text;
document.body.appendChild(t);
t.select();
document.execCommand('copy');
document.body.removeChild(t);
alert('🎬 تم نسخ برومبت توليد الفيديو بنجاح!');
}
// Real PDF Upload
async function handleRealPdfUpload(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
const btn = document.getElementById('btn_upload_pdf');
btn.disabled = true;
const overlay = document.getElementById('upload_progress_overlay');
overlay.style.display = 'flex';
document.getElementById('btn_close_overlay').style.display = 'none';
document.getElementById('upload_progress_bar').style.width = '8%';
document.getElementById('upload_progress_text').textContent = `جاري رفع [${file.name}] إلى الخادم...`;
document.getElementById('upload_progress_percent').textContent = '8%';
document.getElementById('upload_logs_textarea').innerHTML = `[System] Uploading ${escapeHtml(file.name)} (${(file.size/1024/1024).toFixed(2)} MB)...<br>`;
const formData = new FormData();
formData.append('pdf_file', file);
try {
const res = await fetch('/api/curriculum/generate-ai-assets', {
const res = await fetch('/api/curriculum/upload-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file: currentFilePath })
body: formData
});
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('✅ تم توليد الملخص والاختبار السقراطي بنجاح!');
if (res.ok && data.status === 'processing') {
currentUploadTaskId = data.task_id;
document.getElementById('upload_progress_bar').style.width = '15%';
document.getElementById('upload_progress_text').textContent = data.message || 'تم استلام الملف، جاري بدء الذكاء الاصطناعي...';
document.getElementById('upload_progress_percent').textContent = '15%';
pollTaskStatus(data.task_id, file.name);
} else if (res.ok && data.status === 'success') {
overlay.style.display = 'none';
resetUploadBtn();
loadCurriculumTree();
} else {
alert('⚠️ فشل التوليد: ' + (data.message || 'خطأ غير معروف'));
document.getElementById('upload_progress_text').textContent = '⚠️ ' + (data.message || 'فشلت معالجة الملف');
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
}
} catch (e) {
console.error(e);
alert('⚠️ حدث خطأ في الاتصال بمحرك الذكاء الاصطناعي');
} finally {
btn.innerHTML = originalText;
btn.disabled = false;
} catch (err) {
console.error('PDF Upload error:', err);
document.getElementById('upload_progress_text').textContent = 'حدث خطأ في الاتصال بالخادم أثناء رفع الملف.';
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
}
}
async function pollTaskStatus(taskId, fileName) {
try {
const res = await fetch(`/api/curriculum/upload-status?task_id=${taskId}`);
if (!res.ok) throw new Error("Network response was not ok");
const state = await res.json();
const pBar = document.getElementById('upload_progress_bar');
const pText = document.getElementById('upload_progress_text');
const pPercent = document.getElementById('upload_progress_percent');
pBar.style.width = `${state.progress}%`;
pPercent.textContent = `${state.progress}%`;
pText.textContent = state.message || 'جاري المعالجة...';
if (state.status === 'completed') {
pBar.style.width = '100%';
pPercent.textContent = '100%';
pText.textContent = `🎉 تم تفريغ واعتماد [${fileName}] كمنهاج حي بنجاح!`;
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
setTimeout(() => {
document.getElementById('upload_progress_overlay').style.display = 'none';
window.location.reload();
}, 1200);
} else if (state.status === 'error') {
pText.textContent = '❌ ' + (state.message || 'تعطلت عملية التفريغ.');
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
} else {
setTimeout(() => pollTaskStatus(taskId, fileName), 2000);
}
} catch (err) {
console.error('Status poll error:', err);
setTimeout(() => pollTaskStatus(taskId, fileName), 3000);
}
}
function toggleUploadLogs() {
const el = document.getElementById('upload_logs_container');
el.style.display = (el.style.display === 'none') ? 'block' : 'none';
}
function openDirectLogViewer() {
const overlay = document.getElementById('upload_progress_overlay');
overlay.style.display = 'flex';
document.getElementById('btn_close_overlay').style.display = 'inline-block';
document.getElementById('upload_logs_container').style.display = 'block';
if (currentUploadTaskId) {
fetchUploadLogs();
} else {
document.getElementById('upload_logs_textarea').innerHTML = '[System] No active task ID yet. Upload a textbook PDF to stream live AI logs.<br>';
}
}
function closeProgressOverlay() {
@@ -660,27 +747,6 @@ class CurriculumStudio
document.getElementById('curriculum_pdf_input').value = '';
}
async function saveActiveMarkdown() {
if (!currentFilePath) return;
const content = document.getElementById('lesson_markdown_editor').value;
try {
const res = await fetch('/api/curriculum/save-lesson', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file: currentFilePath, content: content })
});
const data = await res.json();
if (res.ok && data.status === 'success') {
alert(`✅ تم حفظ واعتماد التعديلات بنجاح في الملف:\n${data.server_full_path || currentFilePath}`);
} else {
alert('⚠️ فشل حفظ الملف');
}
} catch (e) {
alert('تعذر الاتصال بالسيرفر');
}
}
function handleCurriculumSearch(q) {
if (!q || q.trim() === '') {
renderCurriculumTree(window.CURRICULUM_TREE);
@@ -723,45 +789,6 @@ class CurriculumStudio
renderCurriculumTree(filtered);
}
function copyAiVideoPrompt() {
const md = document.getElementById('lesson_markdown_editor').value;
const title = document.getElementById('current_lesson_title').textContent;
if (!md || md.trim() === '') {
alert('يرجى اختيار درس أولاً.');
return;
}
const prompt = `أنت معلم خبير في المنهاج الأردني. بناءً على محتوى الدرس التالي:
عنوان الدرس: ${title}
المحتوى التعليمي والتطبيقات:
${md}
المطلوب:
1. توليد سيناريو شرح فيديو تفاعلي شيق وممتع للدرس (مدته 3-5 دقائق).
2. شرح فكرة الدرس والمفاهيم الأساسية، والخطوات الجبرية للأمثلة بالتفصيل.
3. التنبيه على الأخطاء الشائعة وحل مسألة اليوم أو مهارات التفكير العليا.`;
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(prompt).then(() => {
alert('🎬 تم نسخ برومبت توليد الفيديو بنجاح! يمكنك الآن لصقه في NotebookLM أو منصة توليد الفيديو الذكية.');
}).catch(err => {
copyViaTextarea(prompt);
});
} else {
copyViaTextarea(prompt);
}
}
function copyViaTextarea(text) {
const t = document.createElement('textarea');
t.value = text;
document.body.appendChild(t);
t.select();
document.execCommand('copy');
document.body.removeChild(t);
alert('🎬 تم نسخ برومبت توليد الفيديو بنجاح!');
}
function escapeHtml(str) {
if (!str) return '';
const d = document.createElement('div');