Files
saqel/backend/app/Views/CurriculumStudio.php
T

805 lines
46 KiB
PHP

<?php
namespace App\Views;
use App\Services\CurriculumService;
class CurriculumStudio
{
public static function render(): string
{
$tree = CurriculumService::getCurriculumTree();
$treeJson = json_encode($tree, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
$serverStorageDir = realpath(__DIR__ . '/../../storage/curriculum') ?: (dirname(__DIR__, 2) . '/storage/curriculum');
ob_start();
?>
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>استوديو إدارة وفهرسة المناهج الحية — صَقِل Enterprise</title>
<link rel="icon" type="image/jpeg" href="/assets/images/saqel_logo.jpg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Alexandria:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<script>
window.CURRICULUM_TREE = <?= $treeJson ?>;
window.SERVER_STORAGE_DIR = "<?= addslashes($serverStorageDir) ?>";
</script>
<style>
:root {
--bg-base: #0B0F19;
--bg-card: rgba(22, 27, 34, 0.85);
--bg-hover: rgba(255, 255, 255, 0.05);
--border: rgba(255, 255, 255, 0.08);
--border-color: rgba(255, 255, 255, 0.12);
--accent-cyan: #00F5D4;
--accent-gold: #FFD166;
--accent-purple: #7B2CBF;
--text-primary: #F8FAFC;
--text-secondary: #94A3B8;
--text-muted: #64748B;
}
* { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Alexandria', sans-serif; }
body { background-color: var(--bg-base); color: var(--text-primary); min-height: 100vh; }
header {
position: sticky; top: 0; z-index: 100;
background: rgba(11, 15, 25, 0.85); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border); padding: 12px 24px;
}
.header-content { max-width: 1400px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; }
.container { max-width: 1400px; margin: 24px auto; padding: 0 20px; }
.main-layout { display: grid; grid-template-columns: 380px 1fr; gap: 24px; }
@media (max-width: 900px) { .main-layout { grid-template-columns: 1fr; } }
.tree-sidebar {
background: var(--bg-card); border: 1px solid var(--border); border-radius: 20px; padding: 20px;
max-height: calc(100vh - 120px); overflow-y: auto;
}
.tree-node { margin-bottom: 12px; }
.tree-header {
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 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 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 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);
}
.btn-primary {
background: linear-gradient(135deg, #0284C7, #0369A1); color: #FFF; border: none; padding: 10px 20px;
border-radius: 12px; font-size: 13px; font-weight: 700; cursor: pointer; transition: all 0.2s;
display: inline-flex; align-items: center; gap: 8px;
}
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 16px rgba(2, 132, 199, 0.4); }
.search-box {
width: 100%; background: rgba(0,0,0,0.4); border: 1px solid var(--border); border-radius: 12px;
padding: 10px 16px; color: #FFF; font-size: 13px; margin-bottom: 16px; outline: none;
}
.search-box:focus { border-color: var(--accent-cyan); }
.md-editor {
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;
}
.server-path-box {
background: rgba(0,0,0,0.4); border: 1px solid var(--border); border-radius: 8px; padding: 6px 12px;
font-size: 11.5px; font-family: monospace; color: var(--accent-cyan); margin-bottom: 14px;
display: flex; align-items: center; justify-content: space-between;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
</style>
</head>
<body>
<header>
<div class="header-content">
<div style="display: flex; align-items: center; gap: 10px;">
<img src="/assets/images/saqel_logo.jpg" alt="صَقِل" style="width: 36px; height: 36px; border-radius: 10px;">
<span style="font-weight: 800; font-size: 18px; color: #FFF;">صَقِل Enterprise</span>
<span style="font-size: 11px; font-weight: 700; color: var(--accent-gold); background: rgba(255,209,102,0.12); padding: 2px 10px; border-radius: 980px;">استوديو تفريغ وفهرسة المناهج الحية 📚</span>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<button type="button" onclick="openDirectLogViewer()" style="font-size: 12px; color: var(--accent-gold); background: rgba(255,209,102,0.1); border: 1px solid rgba(255,209,102,0.3); padding: 5px 14px; border-radius: 980px; cursor: pointer; font-weight: 700;">
🔍 سجل العمليات والسيرفر (Logs)
</button>
<a href="/teacher" style="font-size: 12px; color: var(--accent-cyan); text-decoration: none; border: 1px solid rgba(0,245,212,0.3); padding: 5px 14px; border-radius: 980px;">استوديو المعلم 👨‍🏫</a>
<a href="/student" style="font-size: 12px; color: var(--text-muted); text-decoration: none; border: 1px solid var(--border); padding: 5px 14px; border-radius: 980px;">بوابة الطالب 🎓</a>
</div>
</div>
</header>
<div class="container">
<!-- Live PDF Ingestion Bar -->
<div class="workspace-card" style="margin-bottom: 24px; background: linear-gradient(135deg, rgba(30, 41, 59, 0.7), rgba(15, 23, 42, 0.9));">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
<div>
<h2 style="font-size: 20px; font-weight: 900; color: #FFF; margin-bottom: 4px;">رفع وتفريغ كتب المناهج الوزارية (PDF ⟵ Markdown Tree) 📑</h2>
<p style="font-size: 12.5px; color: var(--text-secondary);" id="upload_status_desc">اختر ملف كتاب المنهاج (PDF) ليقوم الخادم والذكاء الاصطناعي بتفريغ الوحدات والدروس والمصادر كملفات Markdown حية.</p>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="file" id="curriculum_pdf_input" accept=".pdf" style="display: none;" onchange="handleRealPdfUpload(this)">
<button type="button" id="btn_upload_pdf" onclick="document.getElementById('curriculum_pdf_input').click()" class="btn-primary">
<span>📤 رفع كتاب المنهاج PDF الحقيقي</span>
</button>
</div>
</div>
</div>
<div class="main-layout">
<!-- Sidebar: Hierarchical Curriculum Tree Explorer -->
<div class="tree-sidebar">
<input type="text" class="search-box" id="curriculum_search_input" placeholder="🔍 ابحث في المناهج والوحدات ونتاجات التعلم..." oninput="handleCurriculumSearch(this.value)">
<div id="curriculum_tree_container">
<!-- Rendered Dynamically from Real Storage -->
</div>
</div>
<!-- Workspace: Markdown Editor & Extraction Inspector -->
<div class="workspace-card" id="workspace_content_area">
<div id="empty_selection_view" style="display: none; text-align: center; padding: 60px 20px;">
<div style="font-size: 48px; margin-bottom: 12px;">📚</div>
<h3 style="font-size: 18px; font-weight: 800; color: #FFF; margin-bottom: 8px;">لا توجد مناهج مضافة حالياً</h3>
<p style="font-size: 13px; color: var(--text-muted); max-width: 480px; margin: 0 auto;">ارفع كتاب المنهاج PDF لبناء الشجرة وتفريغ الدروس بالمارك داون على السيرفر.</p>
</div>
<div id="active_lesson_view" style="display: none;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 10px;">
<div>
<span style="font-size: 11px; font-weight: 800; color: var(--accent-cyan); background: rgba(0,245,212,0.1); padding: 2px 8px; border-radius: 6px;" id="current_breadcrumb"></span>
<h3 style="font-size: 18px; font-weight: 900; color: #FFF; margin-top: 6px;" id="current_lesson_title_display"></h3>
</div>
<button type="button" onclick="saveActiveMarkdown()" class="btn-primary" style="background: linear-gradient(135deg, #10B981, #059669);">
💾 حفظ التعديلات على السيرفر
</button>
</div>
<!-- Exact Server Storage Path Readout -->
<div class="server-path-box">
<span>📁 مسار الملف على السيرفر: <strong id="server_file_path_display" style="color: #FFF;"></strong></span>
<span style="color: var(--accent-gold); font-size: 11px;">(ملف Markdown حي على القرص)</span>
</div>
<div style="margin-bottom: 14px;">
<span style="font-size: 12px; font-weight: 700; color: var(--text-muted); display: block; margin-bottom: 6px;">نتاجات التعلم والمفاهيم المستهدفة:</span>
<div id="current_outcomes_tags" style="display: flex; gap: 8px; flex-wrap: wrap;"></div>
</div>
<!-- Markdown Content Area -->
<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: 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; 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>
<!-- 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; 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; 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>
</div>
</div>
</div>
</div>
<!-- Upload Progress & Live Terminal Modal -->
<div id="upload_progress_overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(10, 15, 26, 0.9); backdrop-filter: blur(10px); z-index: 9999; flex-direction: column; align-items: center; justify-content: center;">
<div style="background: #16203D; border: 1px solid rgba(255,255,255,0.15); border-radius: 20px; width: 550px; max-width: 92%; padding: 28px; text-align: center; box-shadow: 0 20px 50px rgba(0,0,0,0.7);">
<div id="ai_spinner" style="font-size: 44px; margin-bottom: 12px; animation: pulse 1.5s infinite;">🧠</div>
<h3 style="color: #FFF; font-size: 18px; font-weight: 800; margin-bottom: 6px;">معالجة واستخراج الذكاء الاصطناعي</h3>
<p id="upload_progress_text" style="color: var(--accent-cyan); font-size: 13px; font-weight: 700; margin-bottom: 14px;">جاري التهيئة...</p>
<div style="width: 100%; background: rgba(0,0,0,0.5); border-radius: 980px; height: 12px; margin-bottom: 8px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1);">
<div id="upload_progress_bar" style="height: 100%; width: 5%; background: linear-gradient(90deg, #00F5D4, #FFD166); transition: width 0.4s ease;"></div>
</div>
<span id="upload_progress_percent" style="color: var(--accent-gold); font-size: 12px; font-weight: 800; font-family: monospace;">5%</span>
<div style="margin-top: 20px; display: flex; justify-content: center; gap: 10px;">
<button type="button" id="btn_toggle_logs" onclick="toggleUploadLogs()" style="background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15); color: #FFF; padding: 6px 16px; border-radius: 980px; font-size: 12px; cursor: pointer; font-weight: 700;">
🔍 تفاصيل السجل الحي (Terminal Logs)
</button>
<button type="button" id="btn_close_overlay" onclick="closeProgressOverlay()" style="display: none; background: rgba(239,68,68,0.15); border: 1px solid #EF4444; color: #F87171; padding: 6px 16px; border-radius: 980px; font-size: 12px; cursor: pointer; font-weight: 700;">
إغلاق النافذة ✕
</button>
</div>
<!-- Terminal Output Box -->
<div id="upload_logs_container" style="display: none; margin-top: 18px; width: 100%; text-align: left;">
<div style="background: #0D1117; border: 1px solid #30363D; border-radius: 10px; padding: 12px; font-family: monospace; font-size: 11.5px; color: #39D353; height: 160px; overflow-y: auto; direction: ltr;" id="upload_logs_textarea">
[System] Initializing background task worker...
</div>
</div>
</div>
</div>
<script>
let currentFilePath = '';
let currentUploadTaskId = null;
let logsPollingInterval = null;
document.addEventListener('DOMContentLoaded', () => {
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 || {});
if (entries.length === 0) {
container.innerHTML = `
<div style="padding: 40px 20px; text-align: center; color: var(--text-muted); font-size: 13px;">
<div style="font-size: 32px; margin-bottom: 8px;">📂</div>
<strong>لا توجد مناهج مفهرسة بعد</strong>
<p style="margin-top: 4px;">ارفع كتاب المنهاج الوزاري (PDF) عبر الزر بالأعلى ليتم بناء الشجرة وفهرستها تلقائياً.</p>
</div>
`;
document.getElementById('empty_selection_view').style.display = 'block';
document.getElementById('active_lesson_view').style.display = 'none';
return;
}
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" 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 || {})) {
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 || {})) {
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' : '';
html += `
<div class="tree-lesson${activeClass}" onclick="selectLesson('${les.file}', '${escapeHtml(les.title)}', '${escapeHtml(grade.name)} ⟵ ${escapeHtml(sub.name)} ⟵ ${escapeHtml(unit.name)}', ${JSON.stringify(les.outcomes || []).replace(/"/g, '&quot;')})">
📄 ${escapeHtml(les.title)}
</div>
`;
}
html += `</div>`;
}
// Minhaji Style Categories
if (sem.resources && Object.keys(sem.resources).length > 0) {
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;
for (const item of resourceGroup.items) {
const activeClass = (item.file === currentFilePath) ? ' active' : '';
html += `
<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>
`;
}
}
html += `</div>`;
}
}
html += `</div>`; // Close Subject Body
}
html += `</div></div>`; // Close Grade Body & Node
}
container.innerHTML = html;
if (!currentFilePath) {
const firstGrade = Object.values(tree)[0];
const firstSub = firstGrade?.subjects ? Object.values(firstGrade.subjects)[0] : null;
const firstSem = firstSub?.semesters ? Object.values(firstSub.semesters)[0] : null;
const firstUnit = firstSem?.units ? Object.values(firstSem.units)[0] : null;
const firstLes = firstUnit?.lessons ? firstUnit.lessons[0] : null;
if (firstLes) {
selectLesson(firstLes.file, firstLes.title, `${firstGrade.name} ⟵ ${firstSub.name} ⟵ ${firstUnit.name}`, firstLes.outcomes || []);
}
}
}
async function selectLesson(filePath, title, breadcrumb, outcomes) {
currentFilePath = filePath;
document.getElementById('empty_selection_view').style.display = 'none';
document.getElementById('active_lesson_view').style.display = 'block';
document.getElementById('current_breadcrumb').textContent = breadcrumb;
document.getElementById('current_lesson_title_display').textContent = title;
document.getElementById('server_file_path_display').textContent = `backend/storage/curriculum/${filePath}`;
const tags = document.getElementById('current_outcomes_tags');
tags.innerHTML = (outcomes || []).map(o => `
<span style="font-size: 11px; font-weight: 700; background: rgba(255,209,102,0.12); color: var(--accent-gold); border: 1px solid rgba(255,209,102,0.3); padding: 3px 10px; border-radius: 980px;">
🎯 ${escapeHtml(o)}
</span>
`).join('');
document.querySelectorAll('.tree-lesson').forEach(el => el.classList.remove('active'));
if (window.event?.currentTarget) {
window.event.currentTarget.classList.add('active');
}
try {
const res = await fetch(`/api/curriculum/lesson?file=${encodeURIComponent(filePath)}`);
const data = await res.json();
if (res.ok && data.status === 'success') {
document.getElementById('lesson_markdown_editor').value = data.content || '';
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);
}
}
async function handleDirectVideoUpload(input) {
if (!input.files || !input.files[0] || !currentFilePath) {
alert('يرجى اختيار ملف فيديو صالح.');
return;
}
const file = input.files[0];
const statusSpan = document.getElementById('video_upload_status');
statusSpan.textContent = `جاري رفع الفيديو (${(file.size/1024/1024).toFixed(1)} MB)...`;
const formData = new FormData();
formData.append('video_file', file);
formData.append('file', currentFilePath);
try {
const res = await fetch('/api/curriculum/upload-video', {
method: 'POST',
body: formData
});
const data = await res.json();
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 {
statusSpan.textContent = '⚠️ فشل الرفع';
alert('⚠️ ' + (data.message || 'فشل رفع ملف الفيديو'));
}
} catch(err) {
console.error(err);
statusSpan.textContent = '⚠️ خطأ بالاتصال';
alert('حدث خطأ أثناء رفع الفيديو.');
}
}
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;
}
}
async function saveActiveMarkdown() {
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(`✅ تم حفظ واعتماد التعديلات بنجاح في المنهاج!`);
} else {
alert('⚠️ فشل حفظ الملف');
}
} catch (e) {
console.error(e);
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}
المحتوى التعليمي والتطبيقات:
${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/upload-pdf', {
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%';
pollTaskStatus(data.task_id, file.name);
} else if (res.ok && data.status === 'success') {
overlay.style.display = 'none';
resetUploadBtn();
loadCurriculumTree();
} else {
document.getElementById('upload_progress_text').textContent = '⚠️ ' + (data.message || 'فشلت معالجة الملف');
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
}
} 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';
}
function closeProgressOverlay() {
document.getElementById('upload_progress_overlay').style.display = 'none';
resetUploadBtn();
}
function resetUploadBtn() {
const btn = document.getElementById('btn_upload_pdf');
btn.disabled = false;
btn.innerHTML = '<span>📤 رفع كتاب المنهاج PDF الحقيقي</span>';
document.getElementById('curriculum_pdf_input').value = '';
}
function handleCurriculumSearch(q) {
if (!q || q.trim() === '') {
renderCurriculumTree(window.CURRICULUM_TREE);
return;
}
const qClean = q.toLowerCase().trim();
const filtered = {};
for (const [gKey, grade] of Object.entries(window.CURRICULUM_TREE || {})) {
let hasMatch = false;
const newSubs = {};
for (const [sKey, sub] of Object.entries(grade.subjects || {})) {
const newSems = {};
for (const [semKey, sem] of Object.entries(sub.semesters || {})) {
const newUnits = {};
for (const [uKey, unit] of Object.entries(sem.units || {})) {
const matchedLessons = (unit.lessons || []).filter(l =>
l.title.toLowerCase().includes(qClean) ||
(l.outcomes && l.outcomes.some(o => o.toLowerCase().includes(qClean)))
);
if (matchedLessons.length > 0) {
newUnits[uKey] = { ...unit, lessons: matchedLessons };
hasMatch = true;
}
}
if (Object.keys(newUnits).length > 0) {
newSems[semKey] = { ...sem, units: newUnits };
}
}
if (Object.keys(newSems).length > 0) {
newSubs[sKey] = { ...sub, semesters: newSems };
}
}
if (hasMatch) {
filtered[gKey] = { ...grade, subjects: newSubs };
}
}
renderCurriculumTree(filtered);
}
function escapeHtml(str) {
if (!str) return '';
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
</script>
</body>
</html>
<?php
return ob_get_clean();
}
}