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

312 lines
16 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);
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 ?>;
</script>
<style>
:root {
--bg-base: #0B0F19;
--bg-card: rgba(22, 27, 34, 0.85);
--border: rgba(255, 255, 255, 0.08);
--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: 360px 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; gap: 6px;
padding: 6px 10px; background: rgba(255, 209, 102, 0.08); border-radius: 8px; 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: 4px 8px; border-radius: 6px;
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;
}
.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: 380px; 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.6; resize: vertical; outline: none;
}
</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;">
<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">
<!-- PDF Ingestion & Extraction Top Banner -->
<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 فائقة السرعة 📑</h2>
<p style="font-size: 12.5px; color: var(--text-secondary);">ارفع كتاب المنهاج، واستخرج الوحدات والدروس ونتاجات التعلم، وقارن دقة النص قبل اعتماده في قاعدة المعرفة.</p>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="file" id="curriculum_pdf_input" accept=".pdf,.doc,.docx" style="display: none;" onchange="handlePdfSelected(this)">
<button type="button" onclick="document.getElementById('curriculum_pdf_input').click()" class="btn-primary">
📤 رفع ملف المنهاج PDF
</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 by JS -->
</div>
</div>
<!-- Workspace: Markdown Editor & Extraction Inspector -->
<div class="workspace-card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; 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">توجيهي 2008 ⟵ الرياضيات العلمي ⟵ الوحدة 1</span>
<h3 style="font-size: 18px; font-weight: 900; color: #FFF; margin-top: 6px;" id="current_lesson_title_display">الدرس 1: المفهوم الهندسي والفيزيائي للمشتقة الأولى</h3>
</div>
<button type="button" onclick="saveCurrentMarkdown()" class="btn-primary" style="background: linear-gradient(135deg, #10B981, #059669);">
💾 حفظ واعتماد الدرس في الشجرة
</button>
</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;">
<!-- Tags rendered by JS -->
</div>
</div>
<!-- Markdown Content Area -->
<textarea id="lesson_markdown_editor" class="md-editor"></textarea>
</div>
</div>
</div>
<script>
let currentLessonId = 'math_s1_u1_l1';
let currentFilePath = 'tawjihi_2008/math_scientific/semester_1/unit_1_calculus/lesson_1.md';
document.addEventListener('DOMContentLoaded', () => {
renderCurriculumTree(window.CURRICULUM_TREE);
loadSampleLessonContent();
});
function renderCurriculumTree(tree) {
const container = document.getElementById('curriculum_tree_container');
let html = '';
for (const [gradeKey, grade] of Object.entries(tree)) {
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.id === currentLessonId) ? ' active' : '';
html += `
<div class="tree-lesson${activeClass}" onclick="selectLesson('${les.id}', '${escapeHtml(les.title)}', '${les.file}', '${escapeHtml(grade.name)} ⟵ ${escapeHtml(sub.name)} ⟵ ${escapeHtml(unit.name)}', ${JSON.stringify(les.outcomes || []).replace(/"/g, '&quot;')})">
📄 ${escapeHtml(les.title)}
</div>
`;
}
}
}
}
html += `</div>`;
}
container.innerHTML = html;
}
function selectLesson(id, title, file, breadcrumb, outcomes) {
currentLessonId = id;
currentFilePath = file;
document.getElementById('current_breadcrumb').textContent = breadcrumb;
document.getElementById('current_lesson_title_display').textContent = title;
// Render Outcomes
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('');
// Highlight Active in Sidebar
document.querySelectorAll('.tree-lesson').forEach(el => el.classList.remove('active'));
event.currentTarget?.classList.add('active');
loadSampleLessonContent();
}
function loadSampleLessonContent() {
const editor = document.getElementById('lesson_markdown_editor');
editor.value = `# مبحث الرياضيات — الفرع العلمي (توجيهي 2008)\n## الوحدة الأولى: التفاضل وتطبيقاته\n\n### الدرس 1: المفهوم الهندسي والفيزيائي للمشتقة الأولى\n\n- **نتاجات التعلم المستهدفة:**\n 1. فهم المعنى الهندسي للمشتقة الأولى كميل للمماس عند نقطة التماس (x1, y1).\n 2. التمييز بين الاقتران المتصل والاقتران القابل لاشتقاق.\n 3. حساب المشتقة باستخدام التعريف العام للمشتقة: f'(x) = lim(h->0) [f(x+h) - f(x)] / h.\n\n- **القواعد الأساسية:**\n • مشتقة الثابت = 0\n • مشتقة x^n = n * x^(n-1)\n • ميل المماس m = f'(x1)`;
}
function handlePdfSelected(input) {
if (input.files && input.files[0]) {
const file = input.files[0];
alert(`✅ تم استلام ملف المنهاج: ${file.name}\n\nجارٍ فك التشفير واستخراج الوحدات والدروس تلقائياً بالذكاء الاصطناعي...`);
// Simulate AI extraction and auto-population
document.getElementById('lesson_markdown_editor').value = `# مستخرج تلقائي من المنهاج الوزاري: ${file.name}\n\n## الوحدة المستخرجة: التفاضل والتكامل المتقدم\n\n- تم التعرف على 4 وحدات و 14 درساً معيارياً.\n- جاهز للاعتماد والمطابقة في شجرة المناهج.`;
}
}
function saveCurrentMarkdown() {
alert('✅ تم حفظ وتحديث ملف المنهاج بنجاح في المسار:\n' + currentFilePath);
}
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();
}
}