886 lines
51 KiB
PHP
886 lines
51 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; flex-wrap: wrap;">
|
|
<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>
|
|
<button type="button" onclick="generateAiQuestionBank()" class="btn-primary" style="background: linear-gradient(135deg, #0071E3, #00F5D4); color: #FFF; font-weight: 800;">
|
|
<span>🧠 توليد وتحديث بنك الأسئلة بالذكاء الاصطناعي (50 سؤالاً)</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>
|
|
<button type="button" class="btn-primary" style="padding: 8px 14px; font-size: 12px; background: linear-gradient(135deg, #0071E3, #00F5D4); color: #FFF; font-weight: 700;" onclick="generateAiQuestionBank()">🧠 توليد بنك أسئلة الوحدة (Gemini)</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, '"')})">
|
|
📄 ${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 raw = await res.text();
|
|
let data;
|
|
try { data = JSON.parse(raw); }
|
|
catch (_) {
|
|
const plain = raw.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
if (res.status === 413) {
|
|
throw new Error('حجم الفيديو أكبر من الحد المسموح في Nginx. ارفع client_max_body_size إلى 512M أو أكثر.');
|
|
}
|
|
throw new Error(`HTTP ${res.status}: ${plain.slice(0, 240) || 'استجابة غير مفهومة من السيرفر'}`);
|
|
}
|
|
if (res.ok && data.status === 'processing') {
|
|
currentUploadTaskId = data.task_id;
|
|
statusSpan.textContent = data.message || '⏳ تم استلام الفيديو، جاري تحويله ورفعه إلى Cloudflare...';
|
|
pollTaskStatus(data.task_id, file.name);
|
|
} else 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('حدث خطأ أثناء رفع الفيديو: ' + (err.message || 'تعذر الاتصال بالخادم'));
|
|
}
|
|
}
|
|
|
|
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 generateAiQuestionBank() {
|
|
let unitPath = 'grade_10/math_10/semester_1/unit_01';
|
|
if (currentFilePath) {
|
|
const parts = currentFilePath.split('/');
|
|
if (parts.length >= 4) {
|
|
parts.pop();
|
|
unitPath = parts.join('/');
|
|
}
|
|
}
|
|
|
|
const btn = window.event ? window.event.currentTarget : null;
|
|
let originalText = '';
|
|
if (btn) {
|
|
originalText = btn.innerHTML;
|
|
btn.innerHTML = '🧠 جاري التوليد الذكي لـ 50 سؤالاً...';
|
|
btn.disabled = true;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('/api/curriculum/generate-questions', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ unit_path: unitPath, course_id: 1, count: 50 })
|
|
});
|
|
const data = await res.json();
|
|
if (res.ok && data.status === 'success') {
|
|
alert(`✅ ${data.message}\n\nتم تحديث بنك الأسئلة بالكامل في قاعدة البيانات، وتفعيل محرك الامتحانات التكيفية وفق نقاط الضعف.`);
|
|
} else {
|
|
alert('⚠️ ' + (data.message || 'فشل توليد بنك الأسئلة'));
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert('⚠️ حدث خطأ في الاتصال بمحرك الذكاء الاصطناعي');
|
|
} finally {
|
|
if (btn) {
|
|
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;
|
|
}
|
|
|
|
// Extract concise essentials from markdown
|
|
const lines = md.split('\n');
|
|
const ideas = [];
|
|
const examples = [];
|
|
let ideaCount = 0;
|
|
|
|
for (const line of lines) {
|
|
const tr = line.trim();
|
|
if ((tr.startsWith('- **فكرة الدرس') || tr.startsWith('### البند') || tr.startsWith('## ') || tr.startsWith('### 1.')) && ideaCount < 4) {
|
|
ideas.push(tr.replace(/^#+\s*/, '').replace(/^\-\s*/, ''));
|
|
ideaCount++;
|
|
} else if (tr.includes('$$') && examples.length < 2) {
|
|
examples.push(tr);
|
|
}
|
|
}
|
|
|
|
const prompt = `اعمل فيديو حصة تعليمية تفاعلية شاملة ومفصلة (من 10 إلى 15 دقيقة) لطلاب الصف العاشر لشرح درس: "${title}" استناداً إلى كتاب المنهاج المرفوع في المصادر.
|
|
|
|
المحاور والخطوات المطلوبة في الشرح:
|
|
1. مقدمة الدرس والربط بالواقع: شرح فكرة الدرس ولماذا نحتاج هذه الطرق، مع ربطها بمسألة اليوم الواقعية.
|
|
2. شرح كافة طرق وقوانين الدرس بالتفصيل خطوة بخطوة:
|
|
- تغطية جميع البنود: ${ideas.map(i => i.replace(/^[\*\-\#\d\.\s]+/, '')).join('، ') || 'كافة المفاهيم والقوانين'}.
|
|
- حل أمثلة تدريبية متعددة ومتدرجة في الصعوبة لكل حالة لتثبيت الفهم التام.
|
|
3. التطبيقات والمسائل العملية: حل مسائل حياتية أو هندسية تطبيقية من الدرس وتوضيح خطوات الوصول للناتج النهائي بدقة.
|
|
4. فخاخ الامتحانات والأخطاء الشائعة: التنبيه والتحذير من الأخطاء التي يقع فيها معظم الطلاب وكيفية تجنبها.
|
|
5. مهارات التفكير العليا والتحدي: حل مسألة تحدٍّ وتفكير ناقد مع خاتمة تشجيعية للطلاب.`;
|
|
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
navigator.clipboard.writeText(prompt).then(() => {
|
|
alert('🎬 تم نسخ برومبت الحصة التفاعلية الشاملة (10-15 دقيقة) لـ 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('🎬 تم نسخ برومبت الحصة التفاعلية الشاملة (10-15 دقيقة) لـ NotebookLM بنجاح!');
|
|
}
|
|
|
|
// 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}] كمنهاج حي بنجاح!`;
|
|
if (state.video_url) {
|
|
document.getElementById('ai_video_url_input').value = state.video_url;
|
|
const player = document.getElementById('lesson_video_player');
|
|
player.src = state.video_url;
|
|
player.style.display = 'block';
|
|
}
|
|
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();
|
|
}
|
|
}
|