feat: Dynamic student lesson discovery, lesson selector carousel, and live Socratic checkpoint streaming
This commit is contained in:
@@ -600,13 +600,24 @@ class StudentPortal
|
||||
|
||||
<!-- TAB 1: Socratic Interactive Lesson Player -->
|
||||
<div id="tab_lessons_content" class="studio-card">
|
||||
<!-- Lesson Selector Carousel -->
|
||||
<div style="margin-bottom: 20px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<span style="font-size: 13px; font-weight: 800; color: var(--accent-cyan);">📚 فهرس الحصص والدروس المتاحة:</span>
|
||||
<span style="font-size: 11px; color: var(--text-muted);" id="lessons_count_label">جارٍ التحميل...</span>
|
||||
</div>
|
||||
<div id="student_lessons_carousel" style="display: flex; gap: 10px; overflow-x: auto; padding-bottom: 8px;">
|
||||
<!-- Dynamically loaded via loadStudentLessonsList() -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; flex-wrap: wrap; gap: 10px;">
|
||||
<div>
|
||||
<h3 style="font-size: 18px; font-weight: 800;">الدرس 4: قواعد الاشتقاق الأساسية — رياضيات علمي</h3>
|
||||
<span style="font-size: 12px; color: var(--text-muted);">الأستاذ حمزة الغويري • Bunny Stream DRM Protection</span>
|
||||
<h3 style="font-size: 18px; font-weight: 800;" id="current_lesson_title">الدرس: قواعد الاشتقاق والتحليل الجنائي</h3>
|
||||
<span style="font-size: 12px; color: var(--text-muted);" id="current_lesson_subtitle">الأستاذ حمزة الغويري • Cloudflare R2 + HLS Adaptive Stream</span>
|
||||
</div>
|
||||
<span style="font-size: 11px; font-weight: 800; color: var(--accent-cyan); background: rgba(0,245,212,0.1); border: 1px solid rgba(0,245,212,0.3); padding: 5px 14px; border-radius: 980px;">
|
||||
Checkpoint Active (00:15)
|
||||
<span style="font-size: 11px; font-weight: 800; color: var(--accent-cyan); background: rgba(0,245,212,0.1); border: 1px solid rgba(0,245,212,0.3); padding: 5px 14px; border-radius: 980px;" id="checkpoint_status_badge">
|
||||
الذكاء الاصطناعي السقراطي نشط 🧠
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1048,6 +1059,7 @@ class StudentPortal
|
||||
let activeLessonCheckpoints = [];
|
||||
let triggeredCheckpoints = new Set();
|
||||
let hlsInstance = null;
|
||||
let activeLessonId = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const token = localStorage.getItem('saqel_student_jwt');
|
||||
@@ -1061,7 +1073,7 @@ class StudentPortal
|
||||
}
|
||||
initWebSocket(token);
|
||||
await checkStudentSession(token);
|
||||
await loadStudentLessonPlayback(1);
|
||||
await loadStudentLessonsList();
|
||||
}
|
||||
|
||||
// Video player time listener for Socratic checkpoint
|
||||
@@ -1084,6 +1096,55 @@ class StudentPortal
|
||||
}
|
||||
});
|
||||
|
||||
async function loadStudentLessonsList() {
|
||||
const token = localStorage.getItem('saqel_student_jwt');
|
||||
if (!token) return;
|
||||
try {
|
||||
const res = await fetch('/api/student/lessons', {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.data && data.data.length > 0) {
|
||||
const lessons = data.data;
|
||||
const countLabel = document.getElementById('lessons_count_label');
|
||||
if (countLabel) countLabel.textContent = `${lessons.length} حصص متاحة`;
|
||||
|
||||
const carousel = document.getElementById('student_lessons_carousel');
|
||||
if (carousel) {
|
||||
carousel.innerHTML = lessons.map((les, idx) => `
|
||||
<button type="button" onclick="selectStudentLesson(${les.id})" id="btn_lesson_${les.id}" style="padding: 8px 16px; font-size: 12px; border-radius: 12px; cursor: pointer; white-space: nowrap; transition: all 0.2s ease; display: flex; align-items: center; gap: 8px; font-family: inherit; font-weight: 700; ${idx === 0 ? 'background: linear-gradient(135deg, #0284C7, #0369A1); color: #FFF; border: 1px solid #38BDF8;' : 'background: rgba(255,255,255,0.06); color: var(--text-secondary); border: 1px solid var(--border);'}">
|
||||
<span>🎬 ${escapeHtml(les.title)}</span>
|
||||
<span style="font-size: 10px; background: rgba(0,0,0,0.35); padding: 2px 8px; border-radius: 4px; color: var(--accent-cyan);">🧠 ${les.checkpoints_count || 0} فحص</span>
|
||||
</button>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
if (!activeLessonId && lessons[0]) {
|
||||
selectStudentLesson(lessons[0].id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Load lessons list notice:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function selectStudentLesson(lessonId) {
|
||||
activeLessonId = lessonId;
|
||||
const btns = document.querySelectorAll('#student_lessons_carousel button');
|
||||
btns.forEach(b => {
|
||||
b.style.background = 'rgba(255,255,255,0.06)';
|
||||
b.style.color = 'var(--text-secondary)';
|
||||
b.style.borderColor = 'var(--border)';
|
||||
});
|
||||
const activeBtn = document.getElementById(`btn_lesson_${lessonId}`);
|
||||
if (activeBtn) {
|
||||
activeBtn.style.background = 'linear-gradient(135deg, #0284C7, #0369A1)';
|
||||
activeBtn.style.color = '#FFF';
|
||||
activeBtn.style.borderColor = '#38BDF8';
|
||||
}
|
||||
loadStudentLessonPlayback(lessonId);
|
||||
}
|
||||
|
||||
function formatTime(secs) {
|
||||
const m = Math.floor(secs / 60).toString().padStart(2, '0');
|
||||
const s = (secs % 60).toString().padStart(2, '0');
|
||||
@@ -1114,11 +1175,19 @@ class StudentPortal
|
||||
}
|
||||
|
||||
function triggerCheckpointDemo() {
|
||||
const video = document.getElementById('lesson_video_player');
|
||||
if (video) {
|
||||
video.currentTime = 14;
|
||||
triggeredCheckpoints.clear();
|
||||
video.play();
|
||||
if (activeLessonCheckpoints.length > 0) {
|
||||
renderSocraticModal(activeLessonCheckpoints[0]);
|
||||
} else {
|
||||
renderSocraticModal({
|
||||
question_text: "إذا كان f(x) = sin(3x)، فما هي مشتقة الاقتران f'(x)؟",
|
||||
options: [
|
||||
{ text: "3 cos(3x) (مشتقة الزاوية ضرب مشتقة الاقتران)", is_correct: true },
|
||||
{ text: "cos(3x)", is_correct: false },
|
||||
{ text: "-3 cos(3x)", is_correct: false },
|
||||
{ text: "-cos(3x)", is_correct: false }
|
||||
],
|
||||
rewind_on_fail_seconds: 45
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1168,19 +1237,30 @@ class StudentPortal
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStudentLessonPlayback(lessonId = 1) {
|
||||
async function loadStudentLessonPlayback(lessonId = 0) {
|
||||
const token = localStorage.getItem('saqel_student_jwt');
|
||||
if (!token) return;
|
||||
try {
|
||||
const res = await fetch(`/api/lessons/${lessonId}/playback`, {
|
||||
const targetUrl = lessonId > 0 ? `/api/lessons/${lessonId}/playback` : '/api/lessons/0/playback';
|
||||
const res = await fetch(targetUrl, {
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.status === 'success') {
|
||||
const les = data.data.lesson;
|
||||
const pb = data.data.playback;
|
||||
const checkpoints = data.data.checkpoints || [];
|
||||
const chapters = data.data.chapters || [];
|
||||
activeLessonCheckpoints = checkpoints;
|
||||
triggeredCheckpoints.clear();
|
||||
|
||||
// Update Titles
|
||||
if (les) {
|
||||
const titleEl = document.getElementById('current_lesson_title');
|
||||
const subtitleEl = document.getElementById('current_lesson_subtitle');
|
||||
if (titleEl) titleEl.textContent = `الدرس: ${les.title}`;
|
||||
if (subtitleEl) subtitleEl.textContent = `الأستاذ حمزة الغويري • Cloudflare R2 + HLS Adaptive Stream`;
|
||||
}
|
||||
|
||||
// Update Checkpoint Status badge
|
||||
const badge = document.getElementById('checkpoint_status_badge');
|
||||
@@ -1190,16 +1270,28 @@ class StudentPortal
|
||||
|
||||
// Render AI Timeline Chapters
|
||||
const chaptersList = document.getElementById('ai_timeline_chapters_list');
|
||||
if (chaptersList && chapters.length > 0) {
|
||||
chaptersList.innerHTML = chapters.map((ch, idx) => `
|
||||
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 12px; padding: 12px; cursor: pointer; transition: all 0.2s ease;" onclick="seekToSeconds(${ch.start_seconds})" onmouseover="this.style.borderColor='var(--accent-cyan)'" onmouseout="this.style.borderColor='var(--border)'">
|
||||
<div style="display: flex; justify-content: space-between; font-size: 11.5px; color: var(--accent-gold); font-weight: 700; margin-bottom: 4px;">
|
||||
<span>المحطة ${idx + 1}: ${escapeHtml(ch.title)}</span>
|
||||
<span style="font-family: monospace; color: var(--accent-cyan);">${formatTime(ch.start_seconds)}</span>
|
||||
if (chaptersList) {
|
||||
if (chapters.length > 0) {
|
||||
chaptersList.innerHTML = chapters.map((ch, idx) => `
|
||||
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 12px; padding: 12px; cursor: pointer; transition: all 0.2s ease;" onclick="seekToSeconds(${ch.start_seconds})" onmouseover="this.style.borderColor='var(--accent-cyan)'" onmouseout="this.style.borderColor='var(--border)'">
|
||||
<div style="display: flex; justify-content: space-between; font-size: 11.5px; color: var(--accent-gold); font-weight: 700; margin-bottom: 4px;">
|
||||
<span>المحطة ${idx + 1}: ${escapeHtml(ch.title)}</span>
|
||||
<span style="font-family: monospace; color: var(--accent-cyan);">${formatTime(ch.start_seconds)}</span>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--text-secondary); line-height: 1.4;">${escapeHtml(ch.summary || '')}</div>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--text-secondary); line-height: 1.4;">${escapeHtml(ch.summary || '')}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`).join('');
|
||||
} else {
|
||||
chaptersList.innerHTML = `
|
||||
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 12px; padding: 12px; cursor: pointer;" onclick="seekToSeconds(0)">
|
||||
<div style="display: flex; justify-content: space-between; font-size: 11.5px; color: var(--accent-gold); font-weight: 700; margin-bottom: 4px;">
|
||||
<span>المحطة 1: الشرح الكامل والتطبيقات</span>
|
||||
<span style="font-family: monospace; color: var(--accent-cyan);">00:00</span>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: var(--text-secondary);">استعراض المفاهيم وتطبيقات المنهاج الوزاري.</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const video = document.getElementById('lesson_video_player');
|
||||
|
||||
Reference in New Issue
Block a user