Update Saqel Platform: 2026-08-28 18:21:58

This commit is contained in:
Hamza-Ayed
2026-08-28 18:21:58 +03:00
parent 2c529c579c
commit abe862f04d
6 changed files with 486 additions and 164 deletions
+177 -91
View File
@@ -32,7 +32,9 @@ class CurriculumStudio
: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;
@@ -110,6 +112,12 @@ class CurriculumStudio
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>
@@ -121,7 +129,10 @@ class CurriculumStudio
<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;">
<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>
@@ -134,7 +145,7 @@ class CurriculumStudio
<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) ليقوم الخادم بتفريغه وفهرسة وحداته ودروسه الحقيقية على القرص وتوليد ملفات المارك داون.</p>
<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)">
@@ -192,14 +203,46 @@ class CurriculumStudio
</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) {
function renderCurriculumTree(tree) {
const container = document.getElementById('curriculum_tree_container');
const entries = Object.entries(tree || {});
@@ -240,20 +283,18 @@ class CurriculumStudio
}
}
// NEW: Render Resources (Minhaji Style Categories)
// 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;
// Hexagon / Badge Style
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>`;
// Render actual files below the badge (hidden or just list them)
for (const item of resourceGroup.items) {
const activeClass = (item.file === currentFilePath) ? ' active' : '';
html += `
@@ -273,7 +314,6 @@ 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;
@@ -285,6 +325,7 @@ class CurriculumStudio
}
}
}
async function selectLesson(filePath, title, breadcrumb, outcomes) {
currentFilePath = filePath;
document.getElementById('empty_selection_view').style.display = 'none';
@@ -294,7 +335,6 @@ class CurriculumStudio
document.getElementById('current_lesson_title_display').textContent = title;
document.getElementById('server_file_path_display').textContent = `backend/storage/curriculum/${filePath}`;
// 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;">
@@ -302,11 +342,11 @@ class CurriculumStudio
</span>
`).join('');
// Highlight Active in Sidebar
document.querySelectorAll('.tree-lesson').forEach(el => el.classList.remove('active'));
event?.currentTarget?.classList.add('active');
if (window.event?.currentTarget) {
window.event.currentTarget.classList.add('active');
}
// Fetch Real Markdown from Server
try {
const res = await fetch(`/api/curriculum/lesson?file=${encodeURIComponent(filePath)}`);
const data = await res.json();
@@ -321,15 +361,24 @@ class CurriculumStudio
}
}
// ==========================================================
// 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');
const desc = document.getElementById('upload_status_desc');
btn.disabled = true;
btn.innerHTML = '<span>⏳ جارٍ فك تشفير وتفريغ الكتاب...</span>';
desc.textContent = `جارٍ تفريغ ملف [${file.name}] إلى شجرة ملفات Markdown حقيقية على السيرفر...`;
// 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);
@@ -340,35 +389,128 @@ class CurriculumStudio
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.active_title || 'الدرس المستخرج';
document.getElementById('current_breadcrumb').textContent = data.active_breadcrumb || 'المسار التعليمي';
document.getElementById('server_file_path_display').textContent = `${data.server_storage_dir}/${data.active_file}`;
}
alert(`✅ ${data.message}\n\nتم حفظ ملفات المنهاج على السيرفر في:\n${data.server_storage_dir}`);
desc.textContent = `تم اعتماد المنهاج [${file.name}] بنجاح في قاعدة المعرفة الشجرية!`;
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 {
alert('⚠️ ' + (data.message || 'فشلت معالجة الملف'));
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);
alert('حدث خطأ في الاتصال بالخادم أثناء رفع الملف.');
} finally {
btn.disabled = false;
btn.innerHTML = '<span>📤 رفع كتاب المنهاج PDF الحقيقي</span>';
input.value = '';
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();
}
}
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;
@@ -439,62 +581,6 @@ class CurriculumStudio
return d.innerHTML;
}
</script>
<!-- Upload Progress 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.85); backdrop-filter: blur(8px); z-index: 9999; flex-direction: column; align-items: center; justify-content: center;">
<div style="background: var(--bg-card); padding: 30px; border-radius: 16px; border: 1px solid var(--border-color); width: 400px; max-width: 90%; text-align: center; box-shadow: 0 10px 40px rgba(0,0,0,0.5);">
<div id="ai_spinner" style="font-size: 40px; margin-bottom: 15px; animation: pulse 1.5s infinite;">🧠</div>
<h3 style="color: var(--text-primary); margin-bottom: 10px; font-family: 'SF Arabic', sans-serif;">معالجة الذكاء الاصطناعي</h3>
<div style="width: 100%; background: var(--bg-hover); border-radius: 10px; height: 10px; margin-bottom: 15px; overflow: hidden; position: relative;">
<div id="upload_progress_bar" style="height: 100%; width: 0%; background: linear-gradient(90deg, var(--accent-gold), #ffed4a); transition: width 0.4s ease;"></div>
</div>
<p id="upload_progress_text" style="color: var(--text-secondary); font-size: 13px; font-weight: bold;">جاري التهيئة...</p>
<p id="upload_progress_percent" style="color: var(--accent-gold); font-size: 12px; margin-top: 5px;">0%</p>
<button id="btn_view_logs" type="button" style="margin-top: 15px; background: transparent; border: 1px solid var(--border-color); color: var(--text-muted); padding: 5px 12px; border-radius: 6px; font-size: 11px; cursor: pointer; transition: all 0.2s;" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='var(--text-muted)'" onclick="toggleUploadLogs()">🔍 عرض سجل السيرفر (Logs)</button>
<div id="upload_logs_container" style="display: none; margin-top: 15px; width: 100%; text-align: left;">
<textarea id="upload_logs_textarea" readonly style="width: 100%; height: 120px; background: #000; color: #0f0; font-family: monospace; font-size: 11px; border: 1px solid #333; border-radius: 4px; padding: 8px; resize: none; direction: ltr;"></textarea>
</div>
</div>
</div>
<script>
let currentUploadTaskId = null;
function toggleUploadLogs() {
const container = document.getElementById('upload_logs_container');
if (container.style.display === 'none') {
container.style.display = 'block';
fetchUploadLogs();
} else {
container.style.display = 'none';
}
}
async function fetchUploadLogs() {
if (!currentUploadTaskId) return;
try {
const res = await fetch(`/api/curriculum/upload-log?task_id=${currentUploadTaskId}`);
const data = await res.json();
const textarea = document.getElementById('upload_logs_textarea');
if (data.status === 'success') {
textarea.value = data.log || 'No logs generated yet...';
textarea.scrollTop = textarea.scrollHeight;
}
} catch (e) {
console.error(e);
}
}
</script>
<style>
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
</style>
</body>
</html>
<?php