Update Saqel Platform: 2026-08-28 15:39:16

This commit is contained in:
Hamza-Ayed
2026-08-28 15:39:16 +03:00
parent 2bcf2d2ba2
commit 974de0a641
5 changed files with 533 additions and 268 deletions
+154 -58
View File
@@ -18,7 +18,7 @@ class CurriculumStudio
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>استوديو إدارة وفهرسة المناهج الوزارية — صَقِل Enterprise</title>
<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>
@@ -59,10 +59,10 @@ class CurriculumStudio
max-height: calc(100vh - 120px); overflow-y: auto;
}
.tree-node { margin-bottom: 12px; }
.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: 6px 10px; background: rgba(255, 209, 102, 0.08); border-radius: 8px; cursor: pointer;
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;
@@ -72,7 +72,7 @@ class CurriculumStudio
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;
font-size: 11.5px; color: var(--text-muted); margin: 3px 30px; padding: 5px 10px; border-radius: 8px;
cursor: pointer; transition: all 0.2s;
}
.tree-lesson:hover, .tree-lesson.active {
@@ -87,6 +87,7 @@ class CurriculumStudio
.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); }
@@ -97,10 +98,14 @@ class CurriculumStudio
.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);
width: 100%; height: 420px; 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;
}
.empty-tree-state {
padding: 40px 20px; text-align: center; color: var(--text-muted); font-size: 13px; line-height: 1.6;
}
</style>
</head>
<body>
@@ -110,7 +115,7 @@ class CurriculumStudio
<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>
<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>
@@ -120,17 +125,17 @@ class CurriculumStudio
</header>
<div class="container">
<!-- PDF Ingestion & Extraction Top Banner -->
<!-- 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 فائقة السرعة 📑</h2>
<p style="font-size: 12.5px; color: var(--text-secondary);">ارفع كتاب المنهاج، واستخرج الوحدات والدروس ونتاجات التعلم، وقارن دقة النص قبل اعتماده في قاعدة المعرفة.</p>
<h2 style="font-size: 20px; font-weight: 900; color: #FFF; margin-bottom: 4px;">رفع وتحليل كتاب المنهاج الوزاري PDF (معالجة حية 100%) 📑</h2>
<p style="font-size: 12.5px; color: var(--text-secondary);" id="upload_status_desc">اختر ملف كتاب المنهاج (PDF) ليقوم الخادم والذكاء الاصطناعي بفك تشفيره واستخراج الفصول والوحدات والدروس الحقيقية فوراً.</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
<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>
@@ -142,49 +147,67 @@ class CurriculumStudio
<input type="text" class="search-box" id="curriculum_search_input" placeholder="🔍 ابحث في المناهج والوحدات ونتاجات التعلم..." oninput="handleCurriculumSearch(this.value)">
<div id="curriculum_tree_container">
<!-- Rendered by JS -->
<!-- Rendered Dynamically from Real Storage -->
</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 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 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 id="active_lesson_view" style="display: none;">
<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"></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>
</div>
<!-- Markdown Content Area -->
<textarea id="lesson_markdown_editor" class="md-editor"></textarea>
<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"></textarea>
</div>
</div>
</div>
</div>
<script>
let currentLessonId = 'math_s1_u1_l1';
let currentFilePath = 'tawjihi_2008/math_scientific/semester_1/unit_1_calculus/lesson_1.md';
let currentFilePath = '';
document.addEventListener('DOMContentLoaded', () => {
renderCurriculumTree(window.CURRICULUM_TREE);
loadSampleLessonContent();
});
function renderCurriculumTree(tree) {
const container = document.getElementById('curriculum_tree_container');
let html = '';
const entries = Object.entries(tree || {});
for (const [gradeKey, grade] of Object.entries(tree)) {
if (entries.length === 0) {
container.innerHTML = `
<div class="empty-tree-state">
<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>
@@ -198,9 +221,9 @@ class CurriculumStudio
html += `<div class="tree-unit">🔹 ${escapeHtml(unit.name)}</div>`;
for (const les of (unit.lessons || [])) {
const activeClass = (les.id === currentLessonId) ? ' active' : '';
const activeClass = (les.file === currentFilePath) ? ' 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;')})">
<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>
`;
@@ -213,17 +236,31 @@ class CurriculumStudio
}
container.innerHTML = html;
// Auto-select first lesson if none selected
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 || []);
}
}
}
function selectLesson(id, title, file, breadcrumb, outcomes) {
currentLessonId = id;
currentFilePath = file;
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;
// Render Outcomes
const tags = document.getElementById('current_outcomes_tags');
tags.innerHTML = outcomes.map(o => `
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>
@@ -231,27 +268,86 @@ class CurriculumStudio
// Highlight Active in Sidebar
document.querySelectorAll('.tree-lesson').forEach(el => el.classList.remove('active'));
event.currentTarget?.classList.add('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- جاهز للاعتماد والمطابقة في شجرة المناهج.`;
// Fetch Real Markdown from Server
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 || '';
}
} catch (e) {
console.error('Fetch markdown error:', e);
}
}
function saveCurrentMarkdown() {
alert('✅ تم حفظ وتحديث ملف المنهاج بنجاح في المسار:\n' + currentFilePath);
async function handleRealPdfUpload(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
const btn = document.getElementById('btn_upload_pdf');
const desc = document.getElementById('upload_status_desc');
btn.disabled = true;
btn.innerHTML = '<span>⏳ جارٍ فك تشفير الكتاب بالذكاء الاصطناعي...</span>';
desc.textContent = `جارٍ معالجة وتفصيص ملف [${file.name}] وبناء شجرة المناهج الحقيقية...`;
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 === 'success') {
window.CURRICULUM_TREE = data.tree;
currentFilePath = data.active_file || '';
renderCurriculumTree(data.tree);
if (data.active_file) {
document.getElementById('empty_selection_view').style.display = 'none';
document.getElementById('active_lesson_view').style.display = 'block';
document.getElementById('lesson_markdown_editor').value = data.active_md || '';
document.getElementById('current_lesson_title_display').textContent = data.extracted_data?.units?.[0]?.lessons?.[0]?.title || 'الدرس المستخرج';
document.getElementById('current_breadcrumb').textContent = `${data.extracted_data?.grade_name} ⟵ ${data.extracted_data?.subject_name}`;
}
alert(`✅ ${data.message}`);
desc.textContent = `تم اعتماد المنهاج [${file.name}] بنجاح في قاعدة المعرفة الشجرية!`;
} else {
alert('⚠️ ' + (data.message || 'فشلت معالجة الملف'));
}
} catch (err) {
console.error('PDF Upload error:', err);
alert('حدث خطأ في الاتصال بالخادم أثناء رفع الملف.');
} finally {
btn.disabled = false;
btn.innerHTML = '<span>📤 رفع ملف المنهاج PDF الحقيقي</span>';
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('✅ تم حفظ واعتماد التعديلات بنجاح في المنهاج الرسمي!');
} else {
alert('⚠️ فشل حفظ الملف');
}
} catch (e) {
alert('تعذر الاتصال بالسيرفر');
}
}
function handleCurriculumSearch(q) {
@@ -262,7 +358,7 @@ class CurriculumStudio
const qClean = q.toLowerCase().trim();
const filtered = {};
for (const [gKey, grade] of Object.entries(window.CURRICULUM_TREE)) {
for (const [gKey, grade] of Object.entries(window.CURRICULUM_TREE || {})) {
let hasMatch = false;
const newSubs = {};