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

736 lines
41 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: 14px; }
.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;
}
.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;
}
.tree-unit {
font-size: 12px; font-weight: 600; color: var(--text-secondary); margin: 4px 20px; padding: 3px 6px;
}
.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;
}
.tree-lesson:hover, .tree-lesson.active {
background: rgba(0, 245, 212, 0.15); color: var(--accent-cyan); font-weight: 700;
}
.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: 480px; 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="height: 400px; 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="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h4 style="color: var(--accent-gold); font-size: 16px; margin: 0;">🧠 الاستوديو الذكي للدرس (AI Assets)</h4>
<button class="btn-primary" style="padding: 8px 16px; font-size: 13px;" onclick="generateAiAssets()">✨ توليد الملخص والاختبار آلياً</button>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 13px;">رابط فيديو الشرح الذكي (مثلاً من NotebookLM):</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://...">
</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>
<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>
</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 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 = '';
for (const [gradeKey, grade] of entries) {
html += `
<div class="tree-node">
<div class="tree-header">📁 ${escapeHtml(grade.name)}</div>
`;
for (const [subKey, sub] of Object.entries(grade.subjects || {})) {
html += `<div class="tree-subject">📖 ${escapeHtml(sub.name)}</div>`;
for (const [semKey, sem] of Object.entries(sub.semesters || {})) {
for (const [unitKey, unit] of Object.entries(sem.units || {})) {
html += `<div class="tree-unit">🔹 ${escapeHtml(unit.name)}</div>`;
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>
`;
}
}
// 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;">`;
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)}', ['مرفق إضافي'])">
📎 ${escapeHtml(item.title)}
</div>
`;
}
}
html += `</div>`;
}
}
}
html += `</div>`;
}
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;
}
}
} 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;
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 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%';
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();
} 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();
// 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 fetchUploadLogs() {
if (!currentUploadTaskId) return;
try {
const res = await fetch(`/api/curriculum/upload-log?task_id=${currentUploadTaskId}`);
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;
}
} catch (e) {
console.error('Fetch logs error:', e);
}
}
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() {
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';
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() {
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 = '';
}
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);
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();
}
}