feat: Add collapsible curriculum accordion tree, direct video upload to platform, resilient AI asset generation with smart synthesis fallback, and ingest Physics Unit 2
This commit is contained in:
@@ -63,26 +63,44 @@ class CurriculumStudio
|
||||
max-height: calc(100vh - 120px); overflow-y: auto;
|
||||
}
|
||||
|
||||
.tree-node { margin-bottom: 14px; }
|
||||
.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: 8px 12px; background: rgba(255, 209, 102, 0.08); border-radius: 10px; cursor: pointer;
|
||||
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 12px; padding: 4px 8px;
|
||||
cursor: pointer; display: flex; align-items: center; gap: 6px;
|
||||
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 20px; padding: 3px 6px;
|
||||
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 30px; padding: 6px 10px; border-radius: 8px;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
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);
|
||||
@@ -102,7 +120,7 @@ class CurriculumStudio
|
||||
.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);
|
||||
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;
|
||||
}
|
||||
@@ -197,31 +215,48 @@ class CurriculumStudio
|
||||
</div>
|
||||
|
||||
<!-- Markdown Content Area -->
|
||||
<textarea id="lesson_markdown_editor" class="md-editor" style="height: 400px; margin-bottom: 20px;"></textarea>
|
||||
<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: 12px; padding: 20px;">
|
||||
<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;">🧠 الاستوديو الذكي للدرس (AI Assets)</h4>
|
||||
<div style="display: flex; gap: 8px;">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label style="display: block; color: var(--text-secondary); margin-bottom: 8px; font-size: 13px;">رابط فيديو الشرح الذكي (مثلاً من NotebookLM أو YouTube):</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://...">
|
||||
|
||||
<!-- 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;">
|
||||
<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 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;">
|
||||
<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 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>
|
||||
@@ -269,6 +304,20 @@ class CurriculumStudio
|
||||
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 || {});
|
||||
@@ -287,18 +336,50 @@ class CurriculumStudio
|
||||
}
|
||||
|
||||
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">📁 ${escapeHtml(grade.name)}</div>
|
||||
<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 || {})) {
|
||||
html += `<div class="tree-subject">📖 ${escapeHtml(sub.name)}</div>`;
|
||||
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 || {})) {
|
||||
html += `<div class="tree-unit">🔹 ${escapeHtml(unit.name)}</div>`;
|
||||
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' : '';
|
||||
@@ -308,24 +389,19 @@ class CurriculumStudio
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += `</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;">`;
|
||||
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;
|
||||
|
||||
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)}', ['مرفق إضافي'])">
|
||||
<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>
|
||||
`;
|
||||
@@ -334,9 +410,9 @@ class CurriculumStudio
|
||||
html += `</div>`;
|
||||
}
|
||||
}
|
||||
html += `</div>`; // Close Subject Body
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
html += `</div></div>`; // Close Grade Body & Node
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
@@ -382,182 +458,108 @@ class CurriculumStudio
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// Live PDF Upload & Real-Time AI Extraction Tracking
|
||||
// ==========================================================
|
||||
async function handleRealPdfUpload(input) {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
async function handleDirectVideoUpload(input) {
|
||||
if (!input.files || !input.files[0] || !currentFilePath) {
|
||||
alert('يرجى اختيار ملف فيديو صالح.');
|
||||
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 statusSpan = document.getElementById('video_upload_status');
|
||||
statusSpan.textContent = `جاري رفع الفيديو (${(file.size/1024/1024).toFixed(1)} MB)...`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('pdf_file', file);
|
||||
formData.append('video_file', file);
|
||||
formData.append('file', currentFilePath);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/curriculum/upload-pdf', {
|
||||
const res = await fetch('/api/curriculum/upload-video', {
|
||||
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();
|
||||
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 {
|
||||
document.getElementById('upload_progress_text').textContent = '⚠️ ' + (data.message || 'فشلت معالجة الملف');
|
||||
document.getElementById('btn_close_overlay').style.display = 'inline-block';
|
||||
resetUploadBtn();
|
||||
statusSpan.textContent = '⚠️ فشل الرفع';
|
||||
alert('⚠️ ' + (data.message || 'فشل رفع ملف الفيديو'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('PDF Upload error:', err);
|
||||
document.getElementById('upload_progress_text').textContent = 'حدث خطأ في الاتصال بالخادم أثناء رفع الملف.';
|
||||
document.getElementById('btn_close_overlay').style.display = 'inline-block';
|
||||
resetUploadBtn();
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
statusSpan.textContent = '⚠️ خطأ بالاتصال';
|
||||
alert('حدث خطأ أثناء رفع الفيديو.');
|
||||
}
|
||||
}
|
||||
|
||||
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 generateAiAssets() {
|
||||
if (!currentFilePath) return;
|
||||
const content = document.getElementById('lesson_markdown_editor').value;
|
||||
if (!content) {
|
||||
alert('يجب أن يحتوي الدرس على نص أولاً');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUploadLogs() {
|
||||
if (!currentUploadTaskId) return;
|
||||
const btn = window.event.currentTarget;
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = '⏳ جاري التوليد الذكي...';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/curriculum/upload-log?task_id=${currentUploadTaskId}`);
|
||||
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();
|
||||
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;
|
||||
|
||||
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('Fetch logs error:', e);
|
||||
console.error(e);
|
||||
alert('⚠️ حدث خطأ أثناء الاتصال بمحرك الذكاء الاصطناعي');
|
||||
} finally {
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
async function saveActiveMarkdown() {
|
||||
if (!currentFilePath) return;
|
||||
const content = document.getElementById('lesson_markdown_editor').value;
|
||||
|
||||
@@ -586,66 +588,151 @@ class CurriculumStudio
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.status === 'success') {
|
||||
alert(`✅ تم حفظ واعتماد التعديلات بنجاح في الملف:\n${data.server_full_path || currentFilePath}`);
|
||||
alert(`✅ تم حفظ واعتماد التعديلات بنجاح في المنهاج!`);
|
||||
} else {
|
||||
alert('⚠️ فشل حفظ الملف');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('⚠️ حدث خطأ في الاتصال');
|
||||
alert('⚠️ حدث خطأ في الاتصال بالسيرفر');
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// 5. Generate AI Assets automatically
|
||||
// ==========================================================
|
||||
async function generateAiAssets() {
|
||||
if (!currentFilePath) return;
|
||||
const content = document.getElementById('lesson_markdown_editor').value;
|
||||
if (!content) {
|
||||
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;
|
||||
}
|
||||
const prompt = `أنت معلم خبير في المنهاج الأردني. بناءً على محتوى الدرس التالي:
|
||||
عنوان الدرس: ${title}
|
||||
|
||||
const btn = window.event.currentTarget;
|
||||
const originalText = btn.innerHTML;
|
||||
btn.innerHTML = '⏳ جاري التوليد الذكي...';
|
||||
المحتوى التعليمي والتطبيقات:
|
||||
${md}
|
||||
|
||||
المطلوب:
|
||||
1. توليد سيناريو شرح فيديو تفاعلي شيق وممتع للدرس (مدته 3-5 دقائق).
|
||||
2. شرح فكرة الدرس والمفاهيم الأساسية، والخطوات الجبرية للأمثلة بالتفصيل.
|
||||
3. التنبيه على الأخطاء الشائعة وحل مسألة اليوم أو مهارات التفكير العليا.`;
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(prompt).then(() => {
|
||||
alert('🎬 تم نسخ برومبت توليد الفيديو بنجاح! يمكنك الآن لصقه في 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('🎬 تم نسخ برومبت توليد الفيديو بنجاح!');
|
||||
}
|
||||
|
||||
// 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/generate-ai-assets', {
|
||||
const res = await fetch('/api/curriculum/upload-pdf', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file: currentFilePath })
|
||||
body: formData
|
||||
});
|
||||
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('✅ تم توليد الملخص والاختبار السقراطي بنجاح!');
|
||||
|
||||
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 {
|
||||
alert('⚠️ فشل التوليد: ' + (data.message || 'خطأ غير معروف'));
|
||||
document.getElementById('upload_progress_text').textContent = '⚠️ ' + (data.message || 'فشلت معالجة الملف');
|
||||
document.getElementById('btn_close_overlay').style.display = 'inline-block';
|
||||
resetUploadBtn();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('⚠️ حدث خطأ في الاتصال بمحرك الذكاء الاصطناعي');
|
||||
} finally {
|
||||
btn.innerHTML = originalText;
|
||||
btn.disabled = false;
|
||||
} 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}] كمنهاج حي بنجاح!`;
|
||||
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';
|
||||
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() {
|
||||
@@ -660,27 +747,6 @@ class CurriculumStudio
|
||||
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);
|
||||
@@ -723,45 +789,6 @@ class CurriculumStudio
|
||||
renderCurriculumTree(filtered);
|
||||
}
|
||||
|
||||
function copyAiVideoPrompt() {
|
||||
const md = document.getElementById('lesson_markdown_editor').value;
|
||||
const title = document.getElementById('current_lesson_title').textContent;
|
||||
if (!md || md.trim() === '') {
|
||||
alert('يرجى اختيار درس أولاً.');
|
||||
return;
|
||||
}
|
||||
const prompt = `أنت معلم خبير في المنهاج الأردني. بناءً على محتوى الدرس التالي:
|
||||
عنوان الدرس: ${title}
|
||||
|
||||
المحتوى التعليمي والتطبيقات:
|
||||
${md}
|
||||
|
||||
المطلوب:
|
||||
1. توليد سيناريو شرح فيديو تفاعلي شيق وممتع للدرس (مدته 3-5 دقائق).
|
||||
2. شرح فكرة الدرس والمفاهيم الأساسية، والخطوات الجبرية للأمثلة بالتفصيل.
|
||||
3. التنبيه على الأخطاء الشائعة وحل مسألة اليوم أو مهارات التفكير العليا.`;
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(prompt).then(() => {
|
||||
alert('🎬 تم نسخ برومبت توليد الفيديو بنجاح! يمكنك الآن لصقه في 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('🎬 تم نسخ برومبت توليد الفيديو بنجاح!');
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
const d = document.createElement('div');
|
||||
|
||||
Reference in New Issue
Block a user