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

2288 lines
125 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Views;
class StudentPortal
{
public static function render(): string
{
return <<<'HTML'
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>بوابة الطالب — منصة صَقِل التعليمية</title>
<link rel="icon" type="image/jpeg" href="/assets/images/saqel_logo.jpg">
<link rel="apple-touch-icon" href="/assets/images/saqel_logo.jpg">
<!-- Apple SF Pro & Alexandria Arabic Web Fonts -->
<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">
<!-- HLS.js for Server-Side Adaptive Video Stream -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.4.12/hls.min.js"></script>
<!-- Apple-Grade Luxury Cupertino CSS -->
<style>
:root {
--bg-dark: #000000;
--bg-card: #121214;
--bg-card-hover: #18181B;
--bg-input: #09090B;
--border: rgba(255, 255, 255, 0.08);
--border-focus: #0071E3;
--text-primary: #F5F5F7;
--text-secondary: #A1A1A6;
--text-muted: #6E6E73;
--accent-blue: #0071E3;
--accent-blue-hover: #0077ED;
--accent-cyan: #00F5D4;
--accent-gold: #F59E0B;
--accent-red: #EF4444;
--accent-green: #10B981;
--accent-purple: #8B5CF6;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro AR", "SF Pro Display", "SF Pro Text", "SF Arabic", "Alexandria", "Segoe UI", Roboto, sans-serif;
letter-spacing: -0.015em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background-color: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
background-image:
radial-gradient(circle at 50% 0%, rgba(0, 113, 227, 0.15) 0%, transparent 60%),
radial-gradient(circle at 90% 40%, rgba(245, 158, 11, 0.05) 0%, transparent 50%),
radial-gradient(circle at 10% 80%, rgba(0, 245, 212, 0.05) 0%, transparent 50%);
background-size: 100% 100%;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
width: 100%;
}
/* Apple Glass Header */
header {
border-bottom: 1px solid var(--border);
background-color: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(24px) saturate(180%);
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
height: 68px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo-area {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
}
.logo-img {
width: 38px;
height: 38px;
border-radius: 10px;
object-fit: cover;
box-shadow: 0 0 16px rgba(0, 113, 227, 0.35);
border: 1px solid rgba(255, 255, 255, 0.15);
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.logo-area:hover .logo-img { transform: scale(1.06); }
.brand-title {
font-size: 19px;
font-weight: 800;
color: #FFFFFF;
letter-spacing: -0.03em;
}
.portal-pill {
font-size: 11px;
font-weight: 700;
color: var(--accent-cyan);
background: rgba(0, 245, 212, 0.1);
border: 1px solid rgba(0, 245, 212, 0.25);
padding: 3px 10px;
border-radius: 980px;
margin-right: 8px;
}
.ws-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 700;
padding: 4px 12px;
border-radius: 980px;
transition: all 0.3s;
}
.ws-online { background: rgba(16, 185, 129, 0.15); color: #34D399; border: 1px solid rgba(16, 185, 129, 0.3); }
.ws-offline { background: rgba(239, 68, 68, 0.15); color: #F87171; border: 1px solid rgba(239, 68, 68, 0.3); }
.ws-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; box-shadow: 0 0 6px currentColor; }
/* Auth Form Box */
.auth-card-wrapper {
max-width: 440px;
margin: 60px auto 40px;
width: 100%;
}
.auth-header { text-align: center; margin-bottom: 28px; }
.auth-title { font-size: 26px; font-weight: 800; color: #FFF; margin-bottom: 8px; }
.auth-subtitle { font-size: 14px; color: var(--text-secondary); }
.auth-box {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 28px;
padding: 36px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
}
/* Form Controls */
.form-group { margin-bottom: 20px; }
.form-label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; }
.input-text {
width: 100%;
padding: 13px 16px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 14px;
color: #FFF;
font-size: 14px;
outline: none;
transition: all 0.2s;
}
.input-text:focus { border-color: var(--accent-blue); box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.25); }
.phone-input-group { display: flex; gap: 8px; }
.country-badge {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
border-radius: 14px;
padding: 0 14px;
display: flex;
align-items: center;
font-weight: 700;
font-size: 13px;
color: var(--text-secondary);
}
.btn-primary {
width: 100%;
padding: 13px;
border-radius: 980px;
border: none;
background: var(--accent-blue);
color: #FFF;
font-size: 14.5px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 4px 20px rgba(0, 113, 227, 0.4);
}
.btn-primary:hover { background: var(--accent-blue-hover); transform: scale(1.01); }
.btn-secondary {
background: none;
border: none;
color: var(--text-muted);
font-size: 13px;
cursor: pointer;
transition: color 0.2s;
}
.btn-secondary:hover { color: #FFF; }
/* Dashboard UI */
.dashboard-hero {
background: linear-gradient(135deg, rgba(18, 18, 20, 0.95), rgba(24, 24, 27, 0.8));
border: 1px solid var(--border);
border-radius: 28px;
padding: 32px 36px;
margin: 32px 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 24px;
}
.readiness-box {
display: flex;
align-items: center;
gap: 16px;
background: rgba(0, 0, 0, 0.6);
border: 1px solid var(--border);
border-radius: 20px;
padding: 16px 24px;
}
.gauge-circle {
width: 58px;
height: 58px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0, 245, 212, 0.2) 0%, transparent 70%);
border: 3px solid var(--accent-cyan);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 900;
color: #FFFFFF;
box-shadow: 0 0 16px rgba(0, 245, 212, 0.3);
}
/* Tabs Nav */
.tabs-nav {
display: flex;
gap: 10px;
margin-bottom: 24px;
border-bottom: 1px solid var(--border);
padding-bottom: 12px;
overflow-x: auto;
}
.tab-btn {
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
color: var(--text-secondary);
font-size: 13.5px;
font-weight: 700;
padding: 10px 22px;
border-radius: 980px;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
white-space: nowrap;
}
.tab-btn:hover { background: rgba(255, 255, 255, 0.08); color: #FFF; }
.tab-btn.active {
background: var(--accent-blue);
color: #FFF;
border-color: var(--accent-blue);
box-shadow: 0 2px 12px rgba(0, 113, 227, 0.4);
}
.studio-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 28px;
padding: 32px;
margin-bottom: 40px;
}
/* Socratic Interactive Video Player */
.video-container {
position: relative;
background: #000000;
border: 1px solid var(--border);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8);
}
.video-element {
width: 100%;
height: 480px;
object-fit: cover;
display: block;
background: #050507;
}
.player-bar {
background: rgba(18, 18, 20, 0.95);
border-top: 1px solid var(--border);
padding: 14px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.time-badge {
font-family: monospace;
font-size: 13px;
font-weight: 700;
color: var(--accent-cyan);
background: rgba(0, 245, 212, 0.1);
padding: 4px 10px;
border-radius: 6px;
}
/* Socratic Checkpoint Modal Overlay */
.socratic-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.88);
backdrop-filter: blur(20px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px;
z-index: 10;
text-align: center;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.quiz-option-btn {
width: 100%;
max-width: 580px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #FFF;
padding: 14px 20px;
border-radius: 14px;
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
cursor: pointer;
text-align: right;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.quiz-option-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: var(--accent-blue);
transform: translateX(-4px);
}
.quiz-option-btn.correct {
background: rgba(16, 185, 129, 0.25) !important;
border-color: #10B981 !important;
color: #34D399 !important;
}
.quiz-option-btn.wrong {
background: rgba(239, 68, 68, 0.25) !important;
border-color: #EF4444 !important;
color: #F87171 !important;
}
/* Real-time Chat Box */
.student-chat-box {
display: flex;
flex-direction: column;
height: 480px;
border: 1px solid var(--border);
border-radius: 20px;
overflow: hidden;
background: #09090B;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.chat-msg {
max-width: 75%;
padding: 12px 18px;
border-radius: 18px;
font-size: 14px;
line-height: 1.6;
}
.chat-msg.student {
align-self: flex-start;
background: var(--accent-blue);
color: #FFF;
border-bottom-right-radius: 4px;
}
.chat-msg.teacher {
align-self: flex-end;
background: #18181B;
border: 1px solid var(--border);
color: #FFF;
border-bottom-left-radius: 4px;
}
.chat-time { display: block; font-size: 11px; opacity: 0.6; margin-top: 4px; }
.chat-input-bar {
padding: 14px;
background: #121214;
border-top: 1px solid var(--border);
display: flex;
gap: 10px;
}
/* Exam Taking Modal & Diagnostic Report */
.exam-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(20px);
z-index: 200;
display: none;
align-items: center;
justify-content: center;
padding: 20px;
}
.exam-card {
background: #121214;
border: 1px solid var(--border);
border-radius: 28px;
max-width: 680px;
width: 100%;
padding: 36px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.8);
position: relative;
}
/* DRM Watermark */
@keyframes bounce-watermark {
0%, 100% { transform: translate(10px, 10px); }
50% { transform: translate(140px, 80px); }
}
.watermark-anim {
position: fixed; bottom: 20px; left: 20px; z-index: 99; pointer-events: none; opacity: 0.35;
animation: bounce-watermark 12s infinite ease-in-out; background: rgba(0, 0, 0, 0.75);
border: 1px solid rgba(255, 255, 255, 0.2); padding: 4px 10px; border-radius: 6px;
font-family: monospace; font-size: 11px; color: var(--accent-cyan);
}
.spinner {
width: 18px; height: 18px; border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: #FFF; border-radius: 50%; animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
footer {
border-top: 1px solid var(--border);
padding: 24px 0;
background: #000000;
color: var(--text-muted);
font-size: 12px;
margin-top: auto;
}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="container">
<div class="header-content">
<a href="/student" class="logo-area">
<img src="/assets/images/saqel_logo.jpg" alt="صَقِل" class="logo-img">
<div>
<span class="brand-title">صَقِل</span>
<span class="portal-pill">بوابة الطالب الذكية</span>
</div>
</a>
<div id="header_user_actions">
<div id="auth_user_badge" style="display: none; align-items: center; gap: 12px;">
<span id="ws_status_badge" class="ws-badge ws-offline">
<span class="ws-dot"></span>
<span id="ws_status_text">Workerman جاري الاتصال...</span>
</span>
<span style="font-size: 13px; color: var(--text-secondary);" id="student_display_name"></span>
<button onclick="handleLogout()" style="background: none; border: 1px solid rgba(239,68,68,0.4); color: #F87171; border-radius: 980px; padding: 6px 14px; font-size: 12px; cursor: pointer;">
تسجيل الخروج
</button>
</div>
<a href="/teacher" class="nav-btn" id="switch_to_teacher_link" style="color: var(--text-secondary); font-size: 13px; text-decoration: none;">
<span>أنت معلم؟</span>
<span style="color: var(--accent-gold); font-weight: 700; margin-right: 4px;">استوديو المعلمين ←</span>
</a>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<div class="container">
<!-- 1. AUTHENTICATION (PHONE & OTP) -->
<div id="auth_container" class="auth-card-wrapper">
<div class="auth-header">
<h1 class="auth-title">ادخل لعالم التمكين والفهم</h1>
<p class="auth-subtitle">تسجيل دخول آمن وسريع عبر رمز الواتساب المعتمد</p>
</div>
<div class="auth-box">
<div id="alert_box_error" class="alert alert-error" style="display: none; color: #F87171; background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); padding: 12px; border-radius: 12px; font-size: 13px; margin-bottom: 16px;">
<span>⚠️ </span><span id="alert_error_msg"></span>
</div>
<div id="alert_box_success" class="alert alert-success" style="display: none; color: #34D399; background: rgba(16,185,129,0.15); border: 1px solid rgba(16,185,129,0.3); padding: 12px; border-radius: 12px; font-size: 13px; margin-bottom: 16px;">
<span>✓ </span><span id="alert_success_msg"></span>
</div>
<!-- Step 1: Phone -->
<div id="step_phone_container">
<div class="form-group">
<label class="form-label">الاسم الكامل للطالب</label>
<input type="text" id="student_fullname" placeholder="مثال: أحمد محمد الغويري" class="input-text">
</div>
<div class="form-group">
<label class="form-label">رقم الهاتف (الواتساب) <span style="color: #EF4444;">*</span></label>
<div class="phone-input-group">
<div class="country-badge">🇯🇴 +962</div>
<input type="tel" id="student_phone" required placeholder="790000000" class="input-text">
</div>
</div>
<button type="button" id="btn_send_otp" onclick="handleSendOtp(event)" class="btn-primary">
<span id="btn_send_text">إرسال رمز التحقق (OTP) ←</span>
<div id="btn_send_spinner" class="spinner" style="display: none;"></div>
</button>
</div>
<!-- Step 2: OTP Code -->
<div id="step_otp_container" style="display: none;">
<div style="text-align: center; margin-bottom: 20px;">
<span style="font-size: 12px; color: var(--text-muted);">تم إرسال رمز التحقق إلى:</span>
<div style="font-family: monospace; font-size: 15px; font-weight: 800; color: var(--accent-cyan); margin-top: 4px;" id="otp_target_display"></div>
</div>
<div class="form-group">
<label class="form-label" style="text-align: center;">أدخل رمز التحقق (6 أرقام)</label>
<input type="text" id="student_otp_code" maxlength="6" autofocus placeholder="• • • • • •" class="input-text" style="text-align: center; font-size: 22px; letter-spacing: 6px;">
</div>
<button type="button" id="btn_verify_otp" onclick="handleVerifyOtp(event)" class="btn-primary">
<span id="btn_verify_text">تأكيد الدخول للمنصة ✨</span>
<div id="btn_verify_spinner" class="spinner" style="display: none;"></div>
</button>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 18px;">
<button type="button" onclick="switchToPhoneStep()" class="btn-secondary">← تعديل الرقم</button>
<span id="resend_timer_label" style="font-size: 12px; color: var(--text-muted); display: none;"></span>
<button type="button" id="btn_resend" onclick="handleSendOtp(event)" class="btn-secondary" style="display: none; color: var(--accent-cyan);">إعادة إرسال الرمز ↺</button>
</div>
</div>
</div>
</div>
<!-- 2. STUDENT DASHBOARD (AUTHENTICATED) -->
<div id="dashboard_container" style="width: 100%; display: none;">
<!-- Hero & Tawjihi Readiness Score -->
<div class="dashboard-hero">
<div>
<span style="display: inline-block; font-size: 11px; font-weight: 800; color: var(--accent-cyan); background: rgba(0,245,212,0.12); border: 1px solid rgba(0,245,212,0.3); padding: 4px 12px; border-radius: 980px; margin-bottom: 10px;">توجيهي 2008 — الفرع العلمي</span>
<h2 style="font-size: 26px; font-weight: 800; color: #FFFFFF;" id="dashboard_welcome_title">أهلاً بك يا بطلنا 🚀</h2>
<p style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;">صقل الفهم، حل الأسئلة الوزارية، والتواصل الحي مع أستاذك.</p>
</div>
<div class="readiness-box">
<div class="gauge-circle" id="readiness_gauge_val">87.5%</div>
<div>
<span style="font-size: 11px; font-weight: 700; color: var(--text-muted); display: block;">مؤشر الجاهزية للوزاري</span>
<span style="font-size: 12px; font-weight: 800; color: #34D399;" id="readiness_status_text">مستواك ممتاز ومستقر (+2.4% هذا الأسبوع)</span>
</div>
</div>
</div>
<!-- Tabs Navigation -->
<div class="tabs-nav">
<button class="tab-btn active" id="tab_btn_lessons" onclick="switchStudentTab('lessons')">🎬 الحصص ومشغل الفيديو السقراطي</button>
<button class="tab-btn" id="tab_btn_steplab" onclick="switchStudentTab('steplab')">🧪 مختبر التفكير السقراطي (حل خطوة بخطوة)</button>
<button class="tab-btn" id="tab_btn_chat" onclick="switchStudentTab('chat')">💬 اسأل الأستاذ حمزة الغويري (Workerman 0ms)</button>
<button class="tab-btn" id="tab_btn_exams" onclick="switchStudentTab('exams')">📝 الامتحانات وهرمية التقييم المعرفي</button>
</div>
<!-- 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;" 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;" id="checkpoint_status_badge">
الذكاء الاصطناعي السقراطي نشط 🧠
</span>
</div>
<div class="video-container">
<video id="lesson_video_player" class="video-element" controls poster="/assets/images/saqel_logo.jpg">
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4">
متصفحك لا يدعم مشغل الفيديو.
</video>
<!-- Socratic Checkpoint Overlay Modal -->
<div id="socratic_quiz_modal" class="socratic-overlay" style="display: none;">
<span style="font-size: 11px; font-weight: 800; color: var(--accent-gold); background: rgba(245, 158, 11, 0.15); border: 1px solid rgba(245, 158, 11, 0.4); padding: 4px 14px; border-radius: 980px; margin-bottom: 14px;">
⚠️ توقف الشرح لفحص الفهم اللحظي (Socratic Active Recall)
</span>
<h3 id="socratic_question_title" style="font-size: 18px; font-weight: 800; color: #FFFFFF; margin-bottom: 18px; max-width: 580px; text-align: center;">
إذا كان f(x) = sin(3x)، فما هي مشتقة الاقتران f'(x)؟
</h3>
<div id="socratic_options_container" style="width: 100%; display: flex; flex-direction: column; align-items: center; gap: 8px;">
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, true, 45)">
أ) 3 cos(3x) (مشتقة الزاوية ضرب مشتقة الاقتران)
</button>
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, false, 45)">
ب) cos(3x)
</button>
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, false, 45)">
ج) -3 cos(3x)
</button>
</div>
<div id="checkpoint_feedback" style="margin-top: 14px; font-size: 13.5px; font-weight: 700; display: none;"></div>
</div>
<div class="player-bar">
<div style="display: flex; align-items: center; gap: 12px;">
<span class="time-badge" id="video_time_display">00:00 / 09:56</span>
<span style="font-size: 12px; color: var(--accent-cyan); font-weight: 700;" id="checkpoint_status_badge">نقاط الفحص السقراطي الذكي نشطة 🧠</span>
</div>
<button type="button" onclick="triggerCheckpointDemo()" style="background: rgba(245,158,11,0.15); border: 1px solid rgba(245,158,11,0.3); color: var(--accent-gold); border-radius: 980px; padding: 5px 14px; font-size: 11.5px; cursor: pointer; font-weight: 700;">
محاكاة نقطة الكويز اللحظي ⏱️
</button>
</div>
</div>
<!-- AI Timeline Chapters Roadmap -->
<div style="margin-top: 20px; background: rgba(15, 23, 42, 0.6); border: 1px solid var(--border); border-radius: 16px; padding: 18px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
<span style="font-size: 13.5px; font-weight: 800; color: var(--accent-cyan);">🗺️ فهرس الأفكار والمحطات الزمنية (التحليل الجنائي للمنهاج)</span>
<span style="font-size: 11px; color: var(--text-muted);">انقر على أي محطة للانتقال المباشر</span>
</div>
<div id="ai_timeline_chapters_list" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 10px;">
<!-- Dynamically loaded via loadStudentLessonPlayback -->
<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>00:00</span>
</div>
<div style="font-size: 12px; color: var(--text-secondary);">المعنى الفيزيائي والهندسي للمشتقة الأولى.</div>
</div>
</div>
</div>
</div>
<!-- TAB 2: Real-time Chat with Teacher (Workerman WebSocket) -->
<div id="tab_chat_content" class="studio-card" style="display: none;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
<div>
<h3 style="font-size: 18px; font-weight: 800;">محادثة معلم المادة المباشرة 💬</h3>
<p style="font-size: 12px; color: var(--text-muted);">اسأل الأستاذ حمزة الغويري عن أي فكرة في الدرس وتلقى الإجابة فوراً.</p>
</div>
<span style="font-size: 11px; font-weight: 800; background: rgba(16,185,129,0.15); border: 1px solid rgba(16,185,129,0.4); color: #34D399; padding: 4px 12px; border-radius: 980px;">
الأستاذ حمزة الغويري (متصل الآن 🟢)
</span>
</div>
<div class="student-chat-box">
<div class="chat-messages" id="student_chat_messages">
<div class="chat-msg teacher">
أهلاً بك يا بطل! إذا واجهت أي خطوة غير واضحة في درس قواعد الاشتقاق، اكتب لي سؤالك هنا وسأجيبك فوراً.
<span class="chat-time">قبل قليل</span>
</div>
</div>
<div class="chat-input-bar">
<input type="text" id="student_chat_input" placeholder="اكتب استفسارك للأستاذ حمزة..." class="input-text" onkeypress="if(event.key==='Enter') sendStudentMessage()">
<button type="button" onclick="sendStudentMessage()" class="btn-primary" style="width: auto; padding: 0 26px; font-size: 13px;">إرسال ↵</button>
</div>
</div>
</div>
<!-- TAB 3: Multi-Level Exams & AI Diagnostics -->
<div id="tab_exams_content" class="studio-card" style="display: none;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px;">
<div>
<h3 style="font-size: 18px; font-weight: 800;">هرمية الامتحانات والتشخيص الذكي 📝</h3>
<p style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">4 مستويات من الامتحانات لقياس الفهم وسد الثغرات قبل الامتحان الوزاري.</p>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px;">
<!-- Level 1 -->
<div style="background: rgba(0,0,0,0.5); border: 1px solid var(--border); border-radius: 20px; padding: 24px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<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;">المستوى 1: اختبار الدرس</span>
<span style="font-size: 11px; color: var(--text-muted);">4 أسئلة • 10 دقائق</span>
</div>
<h4 style="font-size: 16px; font-weight: 800;">امتحان قواعد الاشتقاق</h4>
<p style="font-size: 12px; color: var(--text-muted); margin: 6px 0 16px 0;">فحص المهارات الرياضية المباشرة بعد الدرس.</p>
<button type="button" onclick="startExamModal('امتحان قواعد الاشتقاق', 1)" class="btn-primary" style="padding: 10px; font-size: 13px;">بدء الامتحان الآن ⏱️</button>
</div>
<!-- Level 2 -->
<div style="background: rgba(0,0,0,0.5); border: 1px solid var(--border); border-radius: 20px; padding: 24px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span style="font-size: 11px; font-weight: 800; color: var(--accent-purple); background: rgba(139,92,246,0.1); padding: 2px 8px; border-radius: 6px;">المستوى 2: امتحان الوحدة</span>
<span style="font-size: 11px; color: var(--text-muted);">15 سؤالاً • 30 دقيقة</span>
</div>
<h4 style="font-size: 16px; font-weight: 800;">امتحان وحدة التفاضل الشامل</h4>
<p style="font-size: 12px; color: var(--text-muted); margin: 6px 0 16px 0;">محاكاة معيارية لكافة أنماط الأسئلة الوزارية للوحدة.</p>
<button type="button" onclick="startExamModal('امتحان وحدة التفاضل الشامل', 2)" class="btn-primary" style="padding: 10px; font-size: 13px; background: var(--accent-purple);">بدء امتحان الوحدة 📚</button>
</div>
<!-- Level 3 -->
<div style="background: rgba(0,0,0,0.5); border: 1px solid rgba(245,158,11,0.4); border-radius: 20px; padding: 24px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span style="font-size: 11px; font-weight: 800; color: var(--accent-gold); background: rgba(245,158,11,0.1); padding: 2px 8px; border-radius: 6px;">المستوى 3: ذكاء اصطناعي</span>
<span style="font-size: 11px; color: var(--text-muted);">تكيفي مخصص</span>
</div>
<h4 style="font-size: 16px; font-weight: 800;">امتحان سد الثغرات التلقائي</h4>
<p style="font-size: 12px; color: var(--text-muted); margin: 6px 0 16px 0;">يولد أسئلة تكيفية تركز على نقاط ضعفك بدقة.</p>
<button type="button" onclick="startExamModal('امتحان سد الثغرات التلقائي', 3)" class="btn-primary" style="padding: 10px; font-size: 13px; background: linear-gradient(135deg, #F59E0B, #D97706);">توليد وبدء التقييم 🧠</button>
</div>
</div>
</div>
<!-- TAB 4: Socratic Step-by-Step Problem Lab -->
<div id="tab_steplab_content" class="studio-card" style="display: none;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 12px;">
<div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<span style="font-size: 11px; font-weight: 800; background: rgba(0,245,212,0.12); color: var(--accent-cyan); padding: 3px 10px; border-radius: 6px;">منهجية التفكير الإنتاجي 🧠</span>
<span style="font-size: 11px; font-weight: 800; background: rgba(245,158,11,0.12); color: var(--accent-gold); padding: 3px 10px; border-radius: 6px;">قدرات عليا وزارية 2008</span>
</div>
<h3 style="font-size: 18px; font-weight: 900;">مختبر التفكير السقراطي والحل المتدرج خطوة بخطوة 🧪</h3>
<p style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">واجه المسألة باستقلالية: نافذة تفكير مغلقة 60 ثانية، ثم كشف متدرج للخطوات حسب معايير التصحيح الوزاري.</p>
</div>
<!-- Problem Selector Pills -->
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button type="button" onclick="loadStepLabProblem(0)" id="btn_prob_0" class="btn-primary" style="width: auto; padding: 6px 14px; font-size: 12px; background: linear-gradient(135deg, #0284C7, #0369A1);">مسألة 1: المخروط المائي 💧</button>
<button type="button" onclick="loadStepLabProblem(1)" id="btn_prob_1" class="btn-primary" style="width: auto; padding: 6px 14px; font-size: 12px; background: rgba(255,255,255,0.06); border: 1px solid var(--border);">مسألة 2: انزلاق السلم 🪜</button>
<button type="button" onclick="loadStepLabProblem(2)" id="btn_prob_2" class="btn-primary" style="width: auto; padding: 6px 14px; font-size: 12px; background: rgba(255,255,255,0.06); border: 1px solid var(--border);">مسألة 3: حركة المقذوفات 🎯</button>
</div>
</div>
<!-- Problem Card -->
<div style="background: rgba(11, 19, 43, 0.85); border: 1px solid rgba(56, 189, 248, 0.3); border-radius: 18px; padding: 24px; margin-bottom: 24px; box-shadow: 0 10px 30px rgba(0,0,0,0.4);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; flex-wrap: wrap; gap: 8px;">
<span style="font-size: 13px; font-weight: 800; color: var(--accent-gold);" id="steplab_prob_topic">الرياضيات العلمي — المعدلات المرتبطة بالزمن (توجيهي 2008)</span>
<span style="font-size: 11px; color: var(--text-muted);">المصدر: توماس كالكولوس + بنك التميز الوزاري</span>
</div>
<h4 style="font-size: 16px; font-weight: 700; line-height: 1.8; color: #FFFFFF; margin-bottom: 16px;" id="steplab_prob_text">
خزان ماء على شكل مخروط دائري قائم مقلوب، رأسه إلى أسفل وقاعدته أفقية، نصف قطر قاعدته 2 متر وارتفاعه 6 أمتار. يُصب فيه الماء بمعدل ثابت (0.5 م³/دقيقة). جد معدل ارتفاع منسوب الماء في الخزان عندما يكون عمق الماء 3 أمتار.
</h4>
<!-- INTERACTIVE DML VISUAL SIMULATOR -->
<div style="background: rgba(0,0,0,0.6); border: 1px solid rgba(0,245,212,0.3); border-radius: 16px; padding: 18px; margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; flex-wrap: wrap; gap: 8px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 13.5px; font-weight: 800; color: var(--accent-cyan);">🎮 المحاكاة البصرية الحية وتغيير المعطيات (DML Interactive Canvas)</span>
<span style="font-size: 10.5px; background: rgba(0,245,212,0.15); color: var(--accent-cyan); padding: 2px 8px; border-radius: 4px; font-weight: 700;">تحريك حي بالثواني ⚡</span>
</div>
<div id="sim_live_readout" style="font-family: monospace; font-size: 13px; font-weight: 800; color: var(--accent-gold); background: rgba(245,158,11,0.1); padding: 4px 12px; border-radius: 8px; border: 1px solid rgba(245,158,11,0.3);">
dh/dt = 0.159 m/min
</div>
</div>
<div style="display: flex; flex-direction: column; align-items: center;">
<canvas id="steplab_sim_canvas" width="600" height="260" style="max-width: 100%; height: auto; background: #030712; border-radius: 12px; border: 1px solid rgba(255,255,255,0.08);"></canvas>
</div>
<!-- Interactive Sliders -->
<div id="sim_controls_container" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-top: 14px; background: rgba(255,255,255,0.02); border-radius: 10px; padding: 12px;">
<div>
<div style="display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px;">
<span id="slider_label_1" style="color: var(--text-secondary);">عمق الماء الحالي (h):</span>
<span id="slider_val_1" style="font-family: monospace; color: var(--accent-cyan); font-weight: 700;">3.0 m</span>
</div>
<input type="range" id="sim_slider_1" min="0.5" max="5.8" step="0.1" value="3.0" style="width: 100%; accent-color: var(--accent-cyan);" oninput="onSimSliderChange()">
</div>
<div>
<div style="display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 4px;">
<span id="slider_label_2" style="color: var(--text-secondary);">معدل التدفق (dV/dt):</span>
<span id="slider_val_2" style="font-family: monospace; color: var(--accent-gold); font-weight: 700;">0.5 m³/min</span>
</div>
<input type="range" id="sim_slider_2" min="0.1" max="2.0" step="0.1" value="0.5" style="width: 100%; accent-color: var(--accent-gold);" oninput="onSimSliderChange()">
</div>
</div>
</div>
<!-- 1. LOCKED THINKING / STRUGGLE WINDOW (60s Timer) -->
<div id="steplab_struggle_box" style="background: rgba(0,0,0,0.5); border: 2px dashed rgba(245, 158, 11, 0.5); border-radius: 16px; padding: 20px; text-align: center; margin-top: 16px;">
<div style="font-size: 32px; margin-bottom: 6px;">⏳</div>
<h5 style="font-size: 15px; font-weight: 800; color: var(--accent-gold); margin-bottom: 4px;">نافذة التفكير الحر والتجربة المستقلة (الحل مقفل)</h5>
<p style="font-size: 12.5px; color: var(--text-muted); max-width: 520px; margin: 0 auto 14px auto;">
امسك ورقة وقلم وابدأ برسم المخطط وتفريغ المعطيات. لا يُسمح بكشف خطوات الحل حتى انتهاء المؤقت لتدريب عقلك على التوليد الذاتي للفكرة.
</p>
<div style="display: inline-flex; align-items: center; gap: 10px; background: rgba(245, 158, 11, 0.15); border: 1px solid rgba(245, 158, 11, 0.4); padding: 8px 24px; border-radius: 980px; font-family: monospace; font-size: 20px; font-weight: 900; color: var(--accent-gold);" id="steplab_timer_display">
00:60
</div>
<div style="margin-top: 14px;">
<button type="button" onclick="skipStruggleTimer()" style="background: none; border: none; color: var(--text-muted); font-size: 11.5px; text-decoration: underline; cursor: pointer;">
أنا جاهز وكتبت خطوتي الأولى (بدء الكشف المتدرج ↵)
</button>
</div>
</div>
<!-- 2. PROGRESSIVE STEP-BY-STEP LADDER (Revealed after timer) -->
<div id="steplab_steps_ladder" style="display: none; margin-top: 24px; display: flex; flex-direction: column; gap: 14px;">
<!-- STEP 1 -->
<div class="steplab-card" id="step_card_1" style="background: rgba(15, 23, 42, 0.85); border: 1px solid var(--border); border-radius: 16px; padding: 18px; transition: all 0.3s ease;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 12px; font-weight: 800; background: #0284C7; color: #FFF; width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center;">1</span>
<span style="font-size: 14px; font-weight: 800;">الخطوة الأولى: تفريغ المعطيات والرسم التخطيطي</span>
</div>
<button type="button" id="btn_reveal_step_1" onclick="revealStep(1)" class="btn-primary" style="width: auto; padding: 6px 16px; font-size: 12px;">كشف تفاصيل الخطوة 💡</button>
</div>
<div id="step_body_1" style="display: none; margin-top: 14px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 14px;">
<div style="font-size: 13px; line-height: 1.7; color: #F1F5F9;" id="step_content_1">
• نصف قطر المخروط الكامل: R = 2 m ، الارتفاع الكامل: H = 6 m.<br>
• معدل تدفق الحجم: dV/dt = +0.5 m³/min (إشارة موجبة لأن الحجم يزداد).<br>
• اللحظة الزمنية المطلوبة: عندما يكون عمق الماء h = 3 m.<br>
• المطلوب إيجاده: معدل تغير الارتفاع (dh/dt) = ؟
</div>
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--text-muted);">
<span>هل طابقت خطوتك الأولى؟</span>
<button type="button" onclick="verifyStep(1, true)" style="background: rgba(16,185,129,0.2); border: 1px solid #10B981; color: #34D399; padding: 3px 12px; border-radius: 6px; cursor: pointer; font-weight: 700;">نعم ✓</button>
<button type="button" onclick="verifyStep(1, false)" style="background: rgba(239,68,68,0.2); border: 1px solid #EF4444; color: #F87171; padding: 3px 12px; border-radius: 6px; cursor: pointer; font-weight: 700;">تعثرت هنا ✗</button>
</div>
</div>
</div>
<!-- STEP 2 -->
<div class="steplab-card" id="step_card_2" style="background: rgba(15, 23, 42, 0.85); border: 1px solid var(--border); border-radius: 16px; padding: 18px; opacity: 0.5;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 12px; font-weight: 800; background: #475569; color: #FFF; width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center;">2</span>
<span style="font-size: 14px; font-weight: 800;">الخطوة الثانية: كتابة العلاقة الأساسية والعلاقة المساعدة (تشابه المثلثات)</span>
</div>
<button type="button" id="btn_reveal_step_2" disabled onclick="revealStep(2)" class="btn-primary" style="width: auto; padding: 6px 16px; font-size: 12px;">كشف الخطوة 2 📐</button>
</div>
<div id="step_body_2" style="display: none; margin-top: 14px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 14px;">
<div style="font-size: 13px; line-height: 1.7; color: #F1F5F9;" id="step_content_2">
1. قانون حجم المخروط المائي: V = (1/3) * π * r² * h.<br>
2. العلاقة المساعدة من تشابه مقطع المثلثين: r / h = 2 / 6 = 1 / 3 ⟹ r = (1/3) * h.<br>
3. التخلص من المتغير r لأن المطلوب هو dh/dt: V = (1/3) * π * ((1/3)*h)² * h = (π / 27) * h³.
</div>
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--text-muted);">
<span>هل طابقت خطوتك الثانية؟</span>
<button type="button" onclick="verifyStep(2, true)" style="background: rgba(16,185,129,0.2); border: 1px solid #10B981; color: #34D399; padding: 3px 12px; border-radius: 6px; cursor: pointer; font-weight: 700;">نعم ✓</button>
<button type="button" onclick="verifyStep(2, false)" style="background: rgba(239,68,68,0.2); border: 1px solid #EF4444; color: #F87171; padding: 3px 12px; border-radius: 6px; cursor: pointer; font-weight: 700;">تعثرت هنا ✗</button>
</div>
</div>
</div>
<!-- STEP 3 -->
<div class="steplab-card" id="step_card_3" style="background: rgba(15, 23, 42, 0.85); border: 1px solid var(--border); border-radius: 16px; padding: 18px; opacity: 0.5;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 12px; font-weight: 800; background: #475569; color: #FFF; width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center;">3</span>
<span style="font-size: 14px; font-weight: 800;">الخطوة الثالثة: الاشتقاق الضمني بالنسبة للزمن t</span>
</div>
<button type="button" id="btn_reveal_step_3" disabled onclick="revealStep(3)" class="btn-primary" style="width: auto; padding: 6px 16px; font-size: 12px;">كشف الخطوة 3 ⚡</button>
</div>
<div id="step_body_3" style="display: none; margin-top: 14px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 14px;">
<div style="font-size: 13px; line-height: 1.7; color: #F1F5F9;" id="step_content_3">
باشتقاق طرفي المعادلة [V = (π/27) * h³] بالنسبة للزمن t:<br>
• dV/dt = (π / 27) * 3h² * (dh/dt)<br>
• dV/dt = (π / 9) * h² * (dh/dt)
</div>
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--text-muted);">
<span>هل طابقت خطوتك الثالثة؟</span>
<button type="button" onclick="verifyStep(3, true)" style="background: rgba(16,185,129,0.2); border: 1px solid #10B981; color: #34D399; padding: 3px 12px; border-radius: 6px; cursor: pointer; font-weight: 700;">نعم ✓</button>
<button type="button" onclick="verifyStep(3, false)" style="background: rgba(239,68,68,0.2); border: 1px solid #EF4444; color: #F87171; padding: 3px 12px; border-radius: 6px; cursor: pointer; font-weight: 700;">تعثرت هنا ✗</button>
</div>
</div>
</div>
<!-- STEP 4 -->
<div class="steplab-card" id="step_card_4" style="background: rgba(15, 23, 42, 0.85); border: 1px solid var(--border); border-radius: 16px; padding: 18px; opacity: 0.5;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 12px; font-weight: 800; background: #475569; color: #FFF; width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center;">4</span>
<span style="font-size: 14px; font-weight: 800;">الخطوة الرابعة: التعويض العددي باللحظة الزمنية واستخراج الناتج النهائي</span>
</div>
<button type="button" id="btn_reveal_step_4" disabled onclick="revealStep(4)" class="btn-primary" style="width: auto; padding: 6px 16px; font-size: 12px;">كشف الناتج النهائي 🎯</button>
</div>
<div id="step_body_4" style="display: none; margin-top: 14px; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 14px;">
<div style="font-size: 13px; line-height: 1.7; color: #F1F5F9;" id="step_content_4">
بالتعويض بالقيم [dV/dt = 0.5] و [h = 3 m]:<br>
• 0.5 = (π / 9) * (3)² * (dh/dt)<br>
• 0.5 = (π / 9) * 9 * (dh/dt)<br>
• 0.5 = π * (dh/dt)<br>
• <strong style="color: var(--accent-cyan); font-size: 15px;">dh/dt = 0.5 / π = 1 / (2π) m/min ≈ 0.159 m/min</strong>
</div>
<div style="margin-top: 12px; padding: 10px; background: rgba(0,245,212,0.1); border: 1px solid rgba(0,245,212,0.3); border-radius: 8px; color: var(--accent-cyan); font-size: 12px; font-weight: 700;">
🎉 أحسنت! هكذا تكتمل العلامة الكاملة للمسألة في الامتحان الوزاري خطوة بخطوة.
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Dynamic DRM Watermark -->
<div class="watermark-anim">
<span id="drm_watermark_text">SAQEL-DRM • 079XXXXXXX</span>
</div>
</div>
</div>
</main>
<!-- 3. INTERACTIVE EXAM TAKING MODAL -->
<div id="exam_taking_modal" class="exam-modal">
<div class="exam-card">
<!-- Header -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<div>
<h3 style="font-size: 18px; font-weight: 800;" id="exam_title_display">امتحان قواعد الاشتقاق</h3>
<span style="font-size: 12px; color: var(--text-muted);" id="exam_progress_text">السؤال 1 من 3</span>
</div>
<div style="background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); color: #F87171; padding: 4px 14px; border-radius: 980px; font-family: monospace; font-size: 13px; font-weight: 800;" id="exam_timer_badge">
⏱️ 14:59
</div>
</div>
<!-- Question Screen -->
<div id="exam_question_container">
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 16px; padding: 20px; margin-bottom: 20px;">
<h4 style="font-size: 16px; font-weight: 700; line-height: 1.6;" id="exam_question_text">
إذا كان f(x) = (2x + 1)³، فما هي قيمة المشتقة f'(1)؟
</h4>
</div>
<div id="exam_options_list" style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 24px;">
<!-- Options injected dynamically -->
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<button type="button" onclick="closeExamModal()" class="btn-secondary">إلغاء الخروج</button>
<button type="button" id="btn_next_question" onclick="handleNextQuestion()" class="btn-primary" style="width: auto; padding: 10px 28px;">
السؤال التالي ←
</button>
</div>
</div>
<!-- AI Diagnostic Report (Shown on Submit) -->
<div id="exam_result_container" style="display: none; text-align: center;">
<div style="width: 72px; height: 72px; border-radius: 50%; background: rgba(16,185,129,0.15); border: 2px solid #10B981; color: #34D399; font-size: 32px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px;">
✓
</div>
<h3 style="font-size: 22px; font-weight: 800; margin-bottom: 6px;">تم إنهاء الامتحان وحساب التشخيص الذكي!</h3>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 20px;">تقرير التثبيت المعرفي الصادر من محرك صَقِل للذكاء الاصطناعي</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 20px;">
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 16px; padding: 16px;">
<span style="font-size: 11px; color: var(--text-muted); display: block;">النتيجة النهائية</span>
<strong style="font-size: 24px; color: #34D399;" id="result_score_display">100%</strong>
</div>
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 16px; padding: 16px;">
<span style="font-size: 11px; color: var(--text-muted); display: block;">أثر مؤشر التوجيهي</span>
<strong style="font-size: 24px; color: var(--accent-cyan);" id="result_readiness_display">+2.4%</strong>
</div>
</div>
<div style="background: rgba(245,158,11,0.08); border: 1px solid rgba(245,158,11,0.25); border-radius: 16px; padding: 18px; text-align: right; margin-bottom: 24px;">
<strong style="font-size: 13px; color: var(--accent-gold); display: block; margin-bottom: 4px;">🔍 التوصية السقراطية الذكية:</strong>
<p style="font-size: 12.5px; color: var(--text-secondary); line-height: 1.6;" id="result_recommendation_text">
أظهرت إتقاناً ممتازاً لقاعدة السلسلة. يُوصى بمواصلة الدرس الخامس للحفاظ على نسق الجاهزية الوزارية.
</p>
</div>
<button type="button" onclick="closeExamModal()" class="btn-primary">
العودة لدروس المادة ✓
</button>
</div>
</div>
</div>
<!-- Footer -->
<footer>
<div class="container">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
<span>منصة صَقِل التعليمية — الجيل الثاني للتعليم التفاعلي © 2026</span>
<span style="color: var(--accent-cyan);">الخادم متصل ومشفر بـ Workerman & WSS 🔒</span>
</div>
</div>
</footer>
<!-- Logic Controller with Full Real-time WebSocket Client -->
<script>
let wsSocket = null;
let countdownTimer = null;
let timerSeconds = 0;
let studentData = null;
let checkpointTriggered = false;
// Exam State
let currentExamIndex = 0;
let userSelectedAnswers = [];
const sampleQuestions = [
{
text: "إذا كان f(x) = (2x + 1)³، فما هي قيمة المشتقة f'(1)؟",
options: ["54 (3 × 2 × 3²)", "27", "18", "9"],
correct: 0,
topic: "قاعدة السلسلة"
},
{
text: "ما هي مشتقة الاقتران f(x) = tan(2x)؟",
options: ["2 sec²(2x)", "sec²(2x)", "2 tan(2x)", "-2 sec²(2x)"],
correct: 0,
topic: "المشتقات المثلثية"
},
{
text: "إذا كان ميل المماس لمنحنى عند النقطة (1, 3) يساوي 4، فما هي معادلة المماس؟",
options: ["y - 3 = 4(x - 1)", "y - 1 = 4(x - 3)", "y = 4x + 3", "y = 4x - 3"],
correct: 0,
topic: "التفسير الهندسي للمشتقة"
}
];
let activeLessonCheckpoints = [];
let triggeredCheckpoints = new Set();
let hlsInstance = null;
let activeLessonId = null;
document.addEventListener('DOMContentLoaded', async () => {
const token = localStorage.getItem('saqel_student_jwt');
const cachedUser = localStorage.getItem('saqel_student_user');
if (token) {
if (cachedUser) {
try {
const u = JSON.parse(cachedUser);
renderDashboard(u);
} catch (e) {}
}
initWebSocket(token);
await checkStudentSession(token);
await loadStudentLessonsList();
}
// Video player time listener for Socratic checkpoint
const video = document.getElementById('lesson_video_player');
if (video) {
video.addEventListener('timeupdate', () => {
const cur = Math.floor(video.currentTime);
const dur = Math.floor(video.duration || 596);
const timeEl = document.getElementById('video_time_display');
if (timeEl) timeEl.textContent = `${formatTime(cur)} / ${formatTime(dur)}`;
// Check if current timestamp hits an active Socratic checkpoint
const activeCp = activeLessonCheckpoints.find(cp => cp.timestamp_seconds === cur);
if (activeCp && !triggeredCheckpoints.has(activeCp.exam_id)) {
triggeredCheckpoints.add(activeCp.exam_id);
video.pause();
renderSocraticModal(activeCp);
}
});
}
});
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');
return `${m}:${s}`;
}
function renderSocraticModal(cp) {
const modal = document.getElementById('socratic_quiz_modal');
const titleEl = document.getElementById('socratic_question_title');
const container = document.getElementById('socratic_options_container');
const feedback = document.getElementById('checkpoint_feedback');
titleEl.textContent = cp.question_text || 'سؤال فحص الفهم اللحظي:';
container.innerHTML = '';
feedback.style.display = 'none';
const letters = ['أ', 'ب', 'ج', 'د'];
cp.options.forEach((opt, idx) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'quiz-option-btn';
btn.textContent = `${letters[idx] || (idx+1)}) ${opt.text}`;
btn.onclick = () => handleCheckpointAnswer(btn, opt.is_correct, cp.rewind_on_fail_seconds || 45, cp.exam_id);
container.appendChild(btn);
});
modal.style.display = 'flex';
}
function triggerCheckpointDemo() {
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
});
}
}
function handleCheckpointAnswer(btn, isCorrect, rewindSeconds, examId) {
const feedback = document.getElementById('checkpoint_feedback');
const video = document.getElementById('lesson_video_player');
const btns = document.querySelectorAll('#socratic_options_container .quiz-option-btn');
if (isCorrect) {
btn.style.background = 'rgba(16, 185, 129, 0.25)';
btn.style.borderColor = '#10B981';
feedback.style.color = '#34D399';
feedback.style.display = 'block';
feedback.innerHTML = '✓ إجابة ممتازة وصحيحة 100%! سيتم استئناف الشرح فوراً...';
playChimeNotification();
setTimeout(() => {
document.getElementById('socratic_quiz_modal').style.display = 'none';
feedback.style.display = 'none';
if (video) video.play();
}, 1400);
} else {
btn.style.background = 'rgba(239, 68, 68, 0.25)';
btn.style.borderColor = '#EF4444';
feedback.style.color = '#F87171';
feedback.style.display = 'block';
feedback.innerHTML = `⚠️ إجابة غير دقيقة. سيتم إرجاعك ${rewindSeconds} ثانية لمراجعة الفكرة وتثبيت الفهم!`;
setTimeout(() => {
document.getElementById('socratic_quiz_modal').style.display = 'none';
feedback.style.display = 'none';
if (video) {
video.currentTime = Math.max(0, video.currentTime - rewindSeconds);
triggeredCheckpoints.delete(examId);
video.play();
}
}, 2000);
}
}
function seekToSeconds(sec) {
const video = document.getElementById('lesson_video_player');
if (video) {
video.currentTime = sec;
video.play();
showLuxuryToast('تم الانتقال للمحطة ⏱️', formatTime(sec));
}
}
async function loadStudentLessonPlayback(lessonId = 0) {
const token = localStorage.getItem('saqel_student_jwt');
if (!token) return;
try {
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');
if (badge) {
badge.textContent = `الفحص السقراطي الذكي نشط (${checkpoints.length} نقاط فحص) 🧠`;
}
// Render AI Timeline Chapters
const chaptersList = document.getElementById('ai_timeline_chapters_list');
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>
`).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');
if (video && pb) {
if (pb.hls_url && window.Hls && Hls.isSupported()) {
if (hlsInstance) hlsInstance.destroy();
hlsInstance = new Hls({ enableWorker: true });
hlsInstance.loadSource(pb.hls_url);
hlsInstance.attachMedia(video);
} else if (pb.hls_url && video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = pb.hls_url;
} else if (pb.stream_url) {
video.src = pb.stream_url;
}
}
}
} catch (e) {
console.error('Load lesson playback notice:', e);
}
}
let wsPingInterval = null;
// 1. Initialize Real-time WebSocket (Workerman)
function initWebSocket(token) {
if (!token) return;
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
if (wsSocket && (wsSocket.readyState === WebSocket.OPEN || wsSocket.readyState === WebSocket.CONNECTING)) {
return;
}
try {
wsSocket = new WebSocket(wsUrl);
wsSocket.onopen = () => {
wsSocket.send(JSON.stringify({
event: 'auth',
data: { token: token }
}));
// Heartbeat keep-alive every 20s
clearInterval(wsPingInterval);
wsPingInterval = setInterval(() => {
if (wsSocket && wsSocket.readyState === WebSocket.OPEN) {
wsSocket.send(JSON.stringify({ event: 'ping' }));
}
}, 20000);
};
wsSocket.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
handleIncomingWsEvent(msg);
} catch (e) {
console.error('WS Parse error:', e);
}
};
wsSocket.onclose = () => {
updateWsStatus(false);
clearInterval(wsPingInterval);
setTimeout(() => {
const currentToken = localStorage.getItem('saqel_student_jwt');
if (currentToken) initWebSocket(currentToken);
}, 5000);
};
wsSocket.onerror = (err) => {
updateWsStatus(false);
};
} catch (e) {
updateWsStatus(false);
}
}
function updateWsStatus(isOnline) {
const badge = document.getElementById('ws_status_badge');
const text = document.getElementById('ws_status_text');
if (!badge || !text) return;
if (isOnline) {
badge.className = 'ws-badge ws-online';
text.textContent = 'Workerman متصل 🟢';
} else {
badge.className = 'ws-badge ws-offline';
text.textContent = 'Workerman غير متصل 🔴';
}
}
// ==========================================
// Luxury Audio Chime & Real-time Notifications
// ==========================================
function playChimeNotification() {
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const now = ctx.currentTime;
const osc1 = ctx.createOscillator();
const gain1 = ctx.createGain();
osc1.type = 'sine';
osc1.frequency.setValueAtTime(587.33, now);
gain1.gain.setValueAtTime(0.2, now);
gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
osc1.connect(gain1);
gain1.connect(ctx.destination);
osc1.start(now);
osc1.stop(now + 0.35);
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(880.00, now + 0.09);
gain2.gain.setValueAtTime(0.25, now + 0.09);
gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.55);
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.start(now + 0.09);
osc2.stop(now + 0.55);
} catch (e) {
console.warn('Audio Context note:', e);
}
}
function showLuxuryToast(title, body, icon = '👨‍🏫') {
let container = document.getElementById('saqel_toast_stack');
if (!container) {
container = document.createElement('div');
container.id = 'saqel_toast_stack';
container.style.cssText = 'position: fixed; top: 24px; left: 24px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none; direction: rtl;';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.style.cssText = `
pointer-events: auto;
min-width: 280px;
max-width: 380px;
background: rgba(22, 27, 34, 0.92);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-right: 4px solid var(--accent-color, #38BDF8);
border-radius: 16px;
padding: 14px 18px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 0 0 25px rgba(56, 189, 248, 0.2);
display: flex;
align-items: center;
gap: 12px;
animation: toastSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
cursor: pointer;
`;
toast.innerHTML = `
<div style="font-size: 24px;">${icon}</div>
<div style="flex: 1; min-width: 0;">
<div style="font-size: 13px; font-weight: 700; color: #FFFFFF; margin-bottom: 2px;">${escapeHtml(title)}</div>
<div style="font-size: 12px; color: var(--text-muted, #94A3B8); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(body)}</div>
</div>
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(-30px)';
toast.style.transition = 'all 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 5000);
}
function triggerDesktopNotification(title, body) {
if ('Notification' in window && Notification.permission === 'granted') {
try {
new Notification(title, {
body: body,
icon: '/favicon.ico'
});
} catch (e) {}
}
}
function handleIncomingWsEvent(msg) {
const event = msg.event;
const data = msg.data;
if (event === 'authenticated') {
updateWsStatus(true);
} else if (event === 'chat_message') {
appendChatMessage({ ...data, is_mine: false });
playChimeNotification();
showLuxuryToast('رسالة جديدة من الأستاذ حمزة الغويري 👨‍🏫', data.message || 'رد المعلم على استفسارك');
triggerDesktopNotification('صَقِل — رد الأستاذ حمزة الغويري', data.message || 'رد المعلم على استفسارك');
} else if (event === 'drm_session_terminated') {
alert('⚠️ تم فتح هذا الحساب من متصفح آخر.');
handleLogout();
}
}
let activeTeacherId = null;
// ==========================================
// Socratic Step-by-Step Lab Engine
// ==========================================
let stepLabTimerInterval = null;
let stepLabRemainingSeconds = 60;
let currentStepLabIdx = 0;
const stepLabProblems = [
{
topic: "الرياضيات العلمي — المعدلات المرتبطة بالزمن (توجيهي 2008)",
source: "المصدر: توماس كالكولوس + بنك التميز الوزاري",
text: "خزان ماء على شكل مخروط دائري قائم مقلوب، رأسه إلى أسفل وقاعدته أفقية، نصف قطر قاعدته 2 متر وارتفاعه 6 أمتار. يُصب فيه الماء بمعدل ثابت (0.5 م³/دقيقة). جد معدل ارتفاع منسوب الماء في الخزان عندما يكون عمق الماء 3 أمتار.",
steps: [
"• نصف قطر المخروط الكامل: R = 2 m ، الارتفاع الكامل: H = 6 m.<br>• معدل تدفق الحجم: dV/dt = +0.5 m³/min (إشارة موجبة لأن الحجم يزداد).<br>• اللحظة الزمنية المطلوبة: عندما يكون عمق الماء h = 3 m.<br>• المطلوب إيجاده: معدل تغير الارتفاع (dh/dt) = ؟",
"1. قانون حجم المخروط المائي: V = (1/3) * π * r² * h.<br>2. العلاقة المساعدة من تشابه مقطع المثلثين: r / h = 2 / 6 = 1 / 3 ⟹ r = (1/3) * h.<br>3. التخلص من المتغير r لأن المطلوب هو dh/dt: V = (1/3) * π * ((1/3)*h)² * h = (π / 27) * h³.",
"باشتقاق طرفي المعادلة [V = (π/27) * h³] بالنسبة للزمن t:<br>• dV/dt = (π / 27) * 3h² * (dh/dt)<br>• dV/dt = (π / 9) * h² * (dh/dt)",
"بالتعويض بالقيم [dV/dt = 0.5] و [h = 3 m]:<br>• 0.5 = (π / 9) * (3)² * (dh/dt)<br>• 0.5 = (π / 9) * 9 * (dh/dt)<br>• 0.5 = π * (dh/dt)<br>• <strong style=\"color: var(--accent-cyan); font-size: 15px;\">dh/dt = 0.5 / π = 1 / (2π) m/min ≈ 0.159 m/min</strong>"
]
},
{
topic: "الرياضيات العلمي — تطبيقات التفاضل وهندسة الحركة",
source: "المصدر: ستيوارت كالكولوس + أسئلة قدرات عليا وزارية",
text: "سلّم طوله 5 أمتار يستند بطرفه العلوي على حائط رأسي وبطرفه السفلي على أرض أفقية. إذا بدأ الطرف السفلي ينزلق مبتعداً عن الحائط بمعدل (2 م/ث). جد سرعة انزلاق الطرف العلوي للسلّم عندما يكون الطرف السفلي على بُعد 3 أمتار من الحائط.",
steps: [
"• طول السلّم ثابت (الوتر): L = 5 m.<br>• بعد الطرف السفلي عن الحائط: x = 3 m ، ومعدل انزلاقه: dx/dt = +2 m/s.<br>• بعد الطرف العلوي عن الأرض: y (متغير يتناقص).<br>• المطلوب: معدل انزلاق الطرف العلوي (dy/dt) = ؟",
"1. العلاقة الأساسية من مبرهنة فيثاغورس: x² + y² = 5² = 25.<br>2. إيجاد قيمة y عند اللحظة المطلوبة (x = 3): (3)² + y² = 25 ⟹ y² = 16 ⟹ y = 4 m.",
"باشتقاق العلاقة [x² + y² = 25] ضمنياً بالنسبة للزمن t:<br>• 2x * (dx/dt) + 2y * (dy/dt) = 0<br>• x * (dx/dt) + y * (dy/dt) = 0",
"بالتعويض بالقيم [x = 3, dx/dt = 2, y = 4]:<br>• (3)(2) + 4(dy/dt) = 0<br>• 6 + 4(dy/dt) = 0<br>• <strong style=\"color: var(--accent-cyan); font-size: 15px;\">dy/dt = -6 / 4 = -1.5 m/s</strong> (الإشارة السالبة تعني أن الطرف العلوي ينزلق للأسفل بمعدل 1.5 م/ث)."
]
},
{
topic: "الفيزياء العلمي — المقذوفات والحركة في بعدين",
source: "المصدر: فيزياء سيرويه + معايير التميز الوزاري",
text: "أُطلقت قذيفة من قمة برج ارتفاعه 45 متراً بسرعة ابتدائية 50 م/ث وبزاوية 30° فوق الأفق. بافتراض تسارع الجاذبية g = 10 m/s² وإهمال مقاومة الهواء، جد أقصى ارتفاع تصل إليه القذيفة عن سطح الأرض، والزمن الكلي للوصول إلى الأرض.",
steps: [
"• السرعة الابتدائية: v0 = 50 m/s ، زاوية الإطلاق: θ = 30°.<br>• المركبة الأفقية للسرعة: v0x = 50 * cos(30°) = 25√3 m/s (ثابتة).<br>• المركبة الرأسية الابتدائية: v0y = 50 * sin(30°) = 25 m/s.<br>• الارتفاع الابتدائي: y0 = 45 m.",
"1. عند أقصى ارتفاع تكون السرعة الرأسية vy = 0.<br>2. معادلة الحركة: vy² = v0y² - 2g(h_max - y0)<br>3. 0 = (25)² - 2(10)(Δy) ⟹ 20(Δy) = 625 ⟹ Δy = 31.25 m.",
"أقصى ارتفاع كلي عن سطح الأرض:<br>• H_total = y0 + Δy = 45 + 31.25 = <strong style=\"color: var(--accent-gold);\">76.25 متراً</strong>.",
"لحساب زمن الوصول للأرض نطبق معادلة الإزاحة الرأسية [y - y0 = v0y*t - 0.5g*t²]:<br>• -45 = 25t - 5t² ⟹ 5t² - 25t - 45 = 0 ⟹ t² - 5t - 9 = 0<br>• بالمعادلة التربيعية: <strong style=\"color: var(--accent-cyan); font-size: 15px;\">t ≈ 6.41 ثانية</strong>."
]
}
];
function loadStepLabProblem(idx) {
currentStepLabIdx = idx;
const prob = stepLabProblems[idx];
document.getElementById('steplab_prob_topic').textContent = prob.topic;
document.getElementById('steplab_prob_text').innerHTML = prob.text;
// Reset buttons styling
[0, 1, 2].forEach(i => {
const b = document.getElementById(`btn_prob_${i}`);
if (b) {
if (i === idx) {
b.style.background = 'linear-gradient(135deg, #0284C7, #0369A1)';
b.style.border = 'none';
} else {
b.style.background = 'rgba(255,255,255,0.06)';
b.style.border = '1px solid var(--border)';
}
}
});
// Populate step contents
for (let s = 1; s <= 4; s++) {
const el = document.getElementById(`step_content_${s}`);
if (el) el.innerHTML = prob.steps[s - 1];
const card = document.getElementById(`step_card_${s}`);
const body = document.getElementById(`step_body_${s}`);
const btn = document.getElementById(`btn_reveal_step_${s}`);
if (body) body.style.display = 'none';
if (card) card.style.opacity = (s === 1) ? '1' : '0.5';
if (btn) {
btn.disabled = (s !== 1);
btn.textContent = (s === 1) ? 'كشف تفاصيل الخطوة 💡' : `كشف الخطوة ${s}`;
}
}
// Initialize interactive DML canvas simulation
initSimControls(idx);
// Start struggle timer
startStruggleTimer();
}
function startStruggleTimer() {
clearInterval(stepLabTimerInterval);
stepLabRemainingSeconds = 60;
const box = document.getElementById('steplab_struggle_box');
const ladder = document.getElementById('steplab_steps_ladder');
const display = document.getElementById('steplab_timer_display');
box.style.display = 'block';
ladder.style.display = 'none';
display.textContent = '00:60';
stepLabTimerInterval = setInterval(() => {
stepLabRemainingSeconds--;
const s = stepLabRemainingSeconds.toString().padStart(2, '0');
display.textContent = `00:${s}`;
if (stepLabRemainingSeconds <= 0) {
clearInterval(stepLabTimerInterval);
unlockStepsLadder();
}
}, 1000);
}
function skipStruggleTimer() {
clearInterval(stepLabTimerInterval);
unlockStepsLadder();
}
function unlockStepsLadder() {
document.getElementById('steplab_struggle_box').style.display = 'none';
document.getElementById('steplab_steps_ladder').style.display = 'flex';
playChimeNotification();
showLuxuryToast('انتهت فترة التفكير 💡', 'يمكنك الآن كشف خطوات الحل خطوة بخطوة.');
}
function revealStep(stepNum) {
const body = document.getElementById(`step_body_${stepNum}`);
const btn = document.getElementById(`btn_reveal_step_${stepNum}`);
if (body) body.style.display = 'block';
if (btn) {
btn.style.display = 'none';
}
// Unlock next step card
const nextStep = stepNum + 1;
const nextCard = document.getElementById(`step_card_${nextStep}`);
const nextBtn = document.getElementById(`btn_reveal_step_${nextStep}`);
if (nextCard) nextCard.style.opacity = '1';
if (nextBtn) nextBtn.disabled = false;
}
// ==========================================
// DML Interactive Visual Simulation Engine (HTML5 Canvas)
// ==========================================
function initSimControls(idx) {
const s1 = document.getElementById('sim_slider_1');
const s2 = document.getElementById('sim_slider_2');
const l1 = document.getElementById('slider_label_1');
const l2 = document.getElementById('slider_label_2');
const v1 = document.getElementById('slider_val_1');
const v2 = document.getElementById('slider_val_2');
if (idx === 0) {
l1.textContent = 'عمق الماء الحالي (h):';
s1.min = '0.5'; s1.max = '5.8'; s1.step = '0.1'; s1.value = '3.0';
v1.textContent = '3.0 m';
l2.textContent = 'معدل تدفق الحجم (dV/dt):';
s2.min = '0.1'; s2.max = '2.0'; s2.step = '0.1'; s2.value = '0.5';
v2.textContent = '0.5 m³/min';
} else if (idx === 1) {
l1.textContent = 'بُعد الطرف السفلي عن الحائط (x):';
s1.min = '0.5'; s1.max = '4.8'; s1.step = '0.1'; s1.value = '3.0';
v1.textContent = '3.0 m';
l2.textContent = 'سرعة ابتعاد الطرف السفلي (dx/dt):';
s2.min = '0.5'; s2.max = '4.0'; s2.step = '0.1'; s2.value = '2.0';
v2.textContent = '2.0 m/s';
} else if (idx === 2) {
l1.textContent = 'زاوية إطلاق المقذوف (θ):';
s1.min = '10'; s1.max = '75'; s1.step = '1'; s1.value = '30';
v1.textContent = '30°';
l2.textContent = 'السرعة الابتدائية (v0):';
s2.min = '20'; s2.max = '70'; s2.step = '1'; s2.value = '50';
v2.textContent = '50 m/s';
}
onSimSliderChange();
}
function onSimSliderChange() {
const val1 = parseFloat(document.getElementById('sim_slider_1').value);
const val2 = parseFloat(document.getElementById('sim_slider_2').value);
const v1 = document.getElementById('slider_val_1');
const v2 = document.getElementById('slider_val_2');
if (currentStepLabIdx === 0) {
v1.textContent = `${val1.toFixed(1)} m`;
v2.textContent = `${val2.toFixed(1)} m³/min`;
} else if (currentStepLabIdx === 1) {
v1.textContent = `${val1.toFixed(1)} m`;
v2.textContent = `${val2.toFixed(1)} m/s`;
} else if (currentStepLabIdx === 2) {
v1.textContent = `${Math.round(val1)}°`;
v2.textContent = `${Math.round(val2)} m/s`;
}
renderCanvasSim(currentStepLabIdx, val1, val2);
}
function renderCanvasSim(idx, val1, val2) {
const canvas = document.getElementById('steplab_sim_canvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
ctx.clearRect(0, 0, W, H);
if (idx === 0) {
// Conical Tank Simulation
const h = val1;
const dV = val2;
const r = h / 3;
const dh_dt = dV / (Math.PI * Math.pow(r, 2));
document.getElementById('sim_live_readout').textContent = `dh/dt = ${dh_dt.toFixed(3)} m/min | r = ${r.toFixed(2)} m`;
const cx = W / 2;
const topY = 30;
const coneH = 190;
const coneTopR = 90;
const tipY = topY + coneH;
// Inverted Cone Outline
ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx - coneTopR, topY);
ctx.lineTo(cx, tipY);
ctx.lineTo(cx + coneTopR, topY);
ctx.stroke();
// Cone Top Ellipse
ctx.beginPath();
ctx.ellipse(cx, topY, coneTopR, 18, 0, 0, Math.PI * 2);
ctx.stroke();
// Water inside cone
const waterFraction = h / 6.0;
const waterH = coneH * waterFraction;
const waterY = tipY - waterH;
const waterR = coneTopR * waterFraction;
const grad = ctx.createLinearGradient(0, waterY, 0, tipY);
grad.addColorStop(0, 'rgba(0, 245, 212, 0.7)');
grad.addColorStop(1, 'rgba(2, 132, 199, 0.9)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(cx - waterR, waterY);
ctx.lineTo(cx, tipY);
ctx.lineTo(cx + waterR, waterY);
ctx.closePath();
ctx.fill();
// Water surface ellipse
ctx.fillStyle = 'rgba(0, 245, 212, 0.9)';
ctx.beginPath();
ctx.ellipse(cx, waterY, waterR, 10 * waterFraction, 0, 0, Math.PI * 2);
ctx.fill();
// Text labels on canvas
ctx.fillStyle = '#F59E0B';
ctx.font = 'bold 12px Alexandria, sans-serif';
ctx.fillText(`h = ${h.toFixed(1)} m`, cx + waterR + 12, waterY + 4);
ctx.fillStyle = '#00F5D4';
ctx.fillText(`r = ${(h/3).toFixed(2)} m`, cx - 20, waterY - 8);
} else if (idx === 1) {
// Sliding Ladder Simulation
const x = val1;
const dx_dt = val2;
const y = Math.sqrt(Math.max(0.1, 25 - x * x));
const dy_dt = -(x * dx_dt) / y;
document.getElementById('sim_live_readout').textContent = `dy/dt = ${dy_dt.toFixed(2)} m/s (انزلاق للأسفل) | y = ${y.toFixed(2)} m`;
const originX = 480;
const originY = 210;
const scale = 32;
// Wall and Ground
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(originX, 20);
ctx.lineTo(originX, originY);
ctx.lineTo(80, originY);
ctx.stroke();
// Ladder Endpoints
const ladderTopY = originY - (y * scale);
const ladderBottomX = originX - (x * scale);
// Ladder Bar
ctx.strokeStyle = '#F59E0B';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(originX, ladderTopY);
ctx.lineTo(ladderBottomX, originY);
ctx.stroke();
// Base velocity arrow (leftwards away from wall)
ctx.strokeStyle = '#38BDF8';
ctx.fillStyle = '#38BDF8';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(ladderBottomX, originY + 16);
ctx.lineTo(ladderBottomX - 35, originY + 16);
ctx.stroke();
ctx.fillText(`dx/dt = +${dx_dt.toFixed(1)} m/s`, ladderBottomX - 110, originY + 20);
// Top velocity arrow (downwards)
ctx.strokeStyle = '#EF4444';
ctx.fillStyle = '#EF4444';
ctx.beginPath();
ctx.moveTo(originX + 16, ladderTopY);
ctx.lineTo(originX + 16, ladderTopY + 35);
ctx.stroke();
ctx.fillText(`dy/dt = ${dy_dt.toFixed(2)} m/s`, originX + 26, ladderTopY + 20);
} else if (idx === 2) {
// Projectile Simulation
const rad = (val1 * Math.PI) / 180;
const v0 = val2;
const g = 10;
const y0 = 45;
const v0y = v0 * Math.sin(rad);
const v0x = v0 * Math.cos(rad);
const hMax = y0 + (v0y * v0y) / (2 * g);
// Flight time
const disc = Math.sqrt(v0y * v0y + 2 * g * y0);
const tFlight = (v0y + disc) / g;
const range = v0x * tFlight;
document.getElementById('sim_live_readout').textContent = `أقصى ارتفاع H = ${hMax.toFixed(1)} m | زمن التحليق = ${tFlight.toFixed(2)} s`;
const startX = 60;
const groundY = 220;
const towerH = 45 * 0.9;
const startY = groundY - towerH;
// Draw Tower
ctx.fillStyle = 'rgba(255,255,255,0.1)';
ctx.fillRect(startX - 20, startY, 20, towerH);
ctx.strokeStyle = '#475569';
ctx.strokeRect(startX - 20, startY, 20, towerH);
// Draw Ground
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(30, groundY);
ctx.lineTo(W - 30, groundY);
ctx.stroke();
// Draw Trajectory Parabola
ctx.strokeStyle = '#00F5D4';
ctx.lineWidth = 2.5;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(startX, startY);
const dt = tFlight / 60;
const scaleX = (W - 140) / range;
const scaleY = (groundY - 30) / hMax;
for (let t = 0; t <= tFlight; t += dt) {
const px = startX + (v0x * t) * scaleX;
const py = groundY - (y0 + v0y * t - 0.5 * g * t * t) * scaleY;
ctx.lineTo(px, py);
}
ctx.stroke();
ctx.setLineDash([]);
// Apex Marker
const apexT = v0y / g;
const apexX = startX + (v0x * apexT) * scaleX;
const apexY = groundY - hMax * scaleY;
ctx.fillStyle = '#F59E0B';
ctx.beginPath();
ctx.arc(apexX, apexY, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillText(`H_max = ${hMax.toFixed(1)} m`, apexX - 35, apexY - 10);
}
}
function switchStudentTab(tab) {
document.getElementById('tab_lessons_content').style.display = (tab === 'lessons') ? 'block' : 'none';
document.getElementById('tab_steplab_content').style.display = (tab === 'steplab') ? 'block' : 'none';
document.getElementById('tab_chat_content').style.display = (tab === 'chat') ? 'block' : 'none';
document.getElementById('tab_exams_content').style.display = (tab === 'exams') ? 'block' : 'none';
document.getElementById('tab_btn_lessons').className = (tab === 'lessons') ? 'tab-btn active' : 'tab-btn';
document.getElementById('tab_btn_steplab').className = (tab === 'steplab') ? 'tab-btn active' : 'tab-btn';
document.getElementById('tab_btn_chat').className = (tab === 'chat') ? 'tab-btn active' : 'tab-btn';
document.getElementById('tab_btn_exams').className = (tab === 'exams') ? 'tab-btn active' : 'tab-btn';
if (tab === 'steplab') {
loadStepLabProblem(currentStepLabIdx);
} else if (tab === 'chat') {
loadStudentChat();
}
}
async function loadStudentChat() {
const token = localStorage.getItem('saqel_student_jwt');
if (!token) return;
try {
const res = await fetch('/api/chat/conversations', {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await res.json();
if (res.ok && data.data && data.data.length > 0) {
const teacher = data.data.find(c => c.role === 'teacher') || data.data[0];
if (teacher) {
activeTeacherId = teacher.user_id;
const badge = document.getElementById('chat_teacher_badge_name');
if (badge) {
badge.textContent = `${teacher.full_name} (${teacher.specialization || 'مدرس المادة'}) (متصل الآن 🟢)`;
}
loadStudentMessages(activeTeacherId);
}
}
} catch (e) {
console.error('Load student chat notice:', e);
}
}
async function loadStudentMessages(teacherId) {
const token = localStorage.getItem('saqel_student_jwt');
if (!token || !teacherId) return;
try {
const res = await fetch(`/api/chat/messages?other_user_id=${teacherId}`, {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await res.json();
if (res.ok && data.data?.messages) {
const messagesArea = document.getElementById('student_chat_messages');
messagesArea.innerHTML = '';
if (data.data.messages.length === 0) {
messagesArea.innerHTML = '<div style="text-align: center; color: var(--text-muted); font-size: 13px; margin-top: 40px;">مرحباً بك! اكتب سؤالك هنا وسيجيبك أستاذ المادة فوراً.</div>';
} else {
data.data.messages.forEach(msg => appendChatMessage(msg));
}
}
} catch (e) {
console.error('Fetch student messages notice:', e);
}
}
// Real-time Chat Rendering
function appendChatMessage(msg) {
const messagesArea = document.getElementById('student_chat_messages');
const isMine = msg.is_mine || false;
const div = document.createElement('div');
div.className = `chat-msg ${isMine ? 'student' : 'teacher'}`;
div.innerHTML = `
<div>${msg.message}</div>
<span class="chat-time">${msg.created_at || 'الآن'}</span>
`;
messagesArea.appendChild(div);
messagesArea.scrollTop = messagesArea.scrollHeight;
}
async function sendStudentMessage() {
const input = document.getElementById('student_chat_input');
const message = input.value.trim();
if (!message) return;
input.value = '';
const token = localStorage.getItem('saqel_student_jwt');
// If activeTeacherId is null, try to find teacher
if (!activeTeacherId) {
await loadStudentChat();
}
const targetId = activeTeacherId || 1;
// 1. Send via REST API to guarantee database persistence
try {
const res = await fetch('/api/chat/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
receiver_id: targetId,
message: message,
message_type: 'text'
})
});
const data = await res.json();
if (res.ok && data.data) {
appendChatMessage({ message: message, is_mine: true, created_at: 'الآن' });
}
} catch (err) {
console.error('REST chat send notice:', err);
appendChatMessage({ message: message, is_mine: true, created_at: 'الآن' });
}
// 2. Also broadcast over WebSocket
if (wsSocket && wsSocket.readyState === WebSocket.OPEN) {
wsSocket.send(JSON.stringify({
event: 'chat_send',
data: {
receiver_id: targetId,
message: message,
message_type: 'text'
}
}));
}
}
// Exam Taking & AI Diagnostic Modal
function startExamModal(title, level) {
document.getElementById('exam_title_display').textContent = title;
document.getElementById('exam_question_container').style.display = 'block';
document.getElementById('exam_result_container').style.display = 'none';
document.getElementById('exam_taking_modal').style.display = 'flex';
currentExamIndex = 0;
userSelectedAnswers = [];
renderCurrentExamQuestion();
}
function closeExamModal() {
document.getElementById('exam_taking_modal').style.display = 'none';
}
function renderCurrentExamQuestion() {
const q = sampleQuestions[currentExamIndex];
document.getElementById('exam_progress_text').textContent = `السؤال ${currentExamIndex + 1} من ${sampleQuestions.length} • (${q.topic})`;
document.getElementById('exam_question_text').textContent = q.text;
const list = document.getElementById('exam_options_list');
list.innerHTML = '';
q.options.forEach((opt, idx) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'quiz-option-btn';
btn.textContent = `${['أ', 'ب', 'ج', 'د'][idx]}) ${opt}`;
btn.onclick = () => selectExamOption(idx);
list.appendChild(btn);
});
const nextBtn = document.getElementById('btn_next_question');
nextBtn.textContent = (currentExamIndex === sampleQuestions.length - 1) ? 'إنهاء الامتحان وحساب التقييم ✨' : 'السؤال التالي ←';
}
function selectExamOption(idx) {
userSelectedAnswers[currentExamIndex] = idx;
const btns = document.querySelectorAll('#exam_options_list .quiz-option-btn');
btns.forEach((b, i) => {
if (i === idx) {
b.style.borderColor = 'var(--accent-blue)';
b.style.background = 'rgba(0, 113, 227, 0.2)';
} else {
b.style.borderColor = 'rgba(255,255,255,0.15)';
b.style.background = 'rgba(255,255,255,0.05)';
}
});
}
function handleNextQuestion() {
if (userSelectedAnswers[currentExamIndex] === undefined) {
alert('يرجى اختيار إجابة للمتابعة');
return;
}
if (currentExamIndex < sampleQuestions.length - 1) {
currentExamIndex++;
renderCurrentExamQuestion();
} else {
finishExamEvaluation();
}
}
function finishExamEvaluation() {
document.getElementById('exam_question_container').style.display = 'none';
document.getElementById('exam_result_container').style.display = 'block';
// Calculate score
let correctCount = 0;
userSelectedAnswers.forEach((ans, idx) => {
if (ans === sampleQuestions[idx].correct) correctCount++;
});
const pct = Math.round((correctCount / sampleQuestions.length) * 100);
document.getElementById('result_score_display').textContent = `${pct}%`;
document.getElementById('result_readiness_display').textContent = `+2.4%`;
document.getElementById('readiness_gauge_val').textContent = `89.9%`;
document.getElementById('readiness_status_text').textContent = `مستواك ممتاز ومتقدم (+4.8% إجمالي)`;
}
// Auth Logic
function showError(msg) {
document.getElementById('alert_box_success').style.display = 'none';
const errBox = document.getElementById('alert_box_error');
document.getElementById('alert_error_msg').textContent = msg;
errBox.style.display = 'block';
}
function showSuccess(msg) {
document.getElementById('alert_box_error').style.display = 'none';
const okBox = document.getElementById('alert_box_success');
document.getElementById('alert_success_msg').textContent = msg;
okBox.style.display = 'block';
}
function clearAlerts() {
document.getElementById('alert_box_error').style.display = 'none';
document.getElementById('alert_box_success').style.display = 'none';
}
function switchToPhoneStep() {
document.getElementById('step_otp_container').style.display = 'none';
document.getElementById('step_phone_container').style.display = 'block';
clearAlerts();
clearInterval(countdownTimer);
}
function switchToOtpStep(maskedPhone) {
document.getElementById('step_phone_container').style.display = 'none';
document.getElementById('step_otp_container').style.display = 'block';
document.getElementById('otp_target_display').textContent = maskedPhone;
const otpInput = document.getElementById('student_otp_code');
otpInput.value = '';
otpInput.focus();
startTimer(60);
}
function startTimer(seconds) {
timerSeconds = seconds;
clearInterval(countdownTimer);
const timerLabel = document.getElementById('resend_timer_label');
const resendBtn = document.getElementById('btn_resend');
resendBtn.style.display = 'none';
timerLabel.style.display = 'inline';
timerLabel.textContent = `إعادة الإرسال بعد (${timerSeconds}ث)`;
countdownTimer = setInterval(() => {
timerSeconds--;
if (timerSeconds > 0) {
timerLabel.textContent = `إعادة الإرسال بعد (${timerSeconds}ث)`;
} else {
clearInterval(countdownTimer);
timerLabel.style.display = 'none';
resendBtn.style.display = 'inline';
}
}, 1000);
}
async function handleSendOtp(e) {
if (e) e.preventDefault();
clearAlerts();
const phone = document.getElementById('student_phone').value.trim();
const fullName = document.getElementById('student_fullname').value.trim();
const sendBtn = document.getElementById('btn_send_otp');
const sendText = document.getElementById('btn_send_text');
const sendSpinner = document.getElementById('btn_send_spinner');
if (!phone) {
showError('يرجى إدخال رقم الهاتف');
return;
}
sendBtn.disabled = true;
sendText.textContent = 'جارٍ إرسال البطاقة عبر الواتساب...';
sendSpinner.style.display = 'block';
try {
const res = await fetch('/api/auth/otp/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone_number: phone, role: 'student', full_name: fullName })
});
const data = await res.json();
if (res.ok && data.status === 'success') {
showSuccess(data.message || 'تم إرسال بطاقة التحقق عبر الواتساب!');
switchToOtpStep(data.data?.phone_masked || phone);
} else {
showError(data.message || 'فشل إرسال رمز التحقق.');
}
} catch (err) {
showError('حدث خطأ في الاتصال بالسيرفر.');
} finally {
sendBtn.disabled = false;
sendText.textContent = 'إرسال رمز التحقق (OTP) ←';
sendSpinner.style.display = 'none';
}
}
async function handleVerifyOtp(e) {
if (e) e.preventDefault();
clearAlerts();
const phone = document.getElementById('student_phone').value.trim();
const fullName = document.getElementById('student_fullname').value.trim();
const otpCode = document.getElementById('student_otp_code').value.trim();
const verifyBtn = document.getElementById('btn_verify_otp');
const verifyText = document.getElementById('btn_verify_text');
const verifySpinner = document.getElementById('btn_verify_spinner');
if (!otpCode || otpCode.length < 6) {
showError('يرجى إدخال رمز التحقق المكون من 6 أرقام');
return;
}
verifyBtn.disabled = true;
verifyText.textContent = 'جارٍ التحقق...';
verifySpinner.style.display = 'block';
try {
const res = await fetch('/api/auth/otp/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone_number: phone, otp: otpCode, role: 'student', full_name: fullName })
});
const data = await res.json();
if (res.ok && data.status === 'success' && data.data?.token) {
const token = data.data.token;
const user = data.data.user;
localStorage.setItem('saqel_student_jwt', token);
if (user) {
localStorage.setItem('saqel_student_user', JSON.stringify(user));
}
document.cookie = "saqel_student_jwt=" + token + "; path=/; max-age=2592000; SameSite=Lax";
initWebSocket(token);
renderDashboard(user);
await checkStudentSession(token);
} else {
showError(data.message || 'رمز التحقق غير صحيح');
}
} catch (err) {
showError('حدث خطأ في التحقق من الرمز.');
} finally {
verifyBtn.disabled = false;
verifyText.textContent = 'تأكيد الدخول للمنصة ✨';
verifySpinner.style.display = 'none';
}
}
async function checkStudentSession(token) {
try {
const res = await fetch('/api/auth/me', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (res.status === 401) {
handleLogout();
return;
}
const data = await res.json();
if (res.ok && data.data) {
studentData = data.data;
localStorage.setItem('saqel_student_user', JSON.stringify(data.data));
renderDashboard(data.data);
}
} catch (e) {
console.error('Session check notice:', e);
}
}
function renderDashboard(user) {
document.getElementById('auth_container').style.display = 'none';
document.getElementById('dashboard_container').style.display = 'block';
document.getElementById('switch_to_teacher_link').style.display = 'none';
document.getElementById('auth_user_badge').style.display = 'flex';
const displayName = user?.full_name || 'طالبنا المتميز';
document.getElementById('student_display_name').textContent = `أهلاً، ${displayName}`;
document.getElementById('dashboard_welcome_title').textContent = `أهلاً بك يا ${displayName} 🚀`;
document.getElementById('drm_watermark_text').textContent = `SAQEL-DRM • ${user?.phone_number || '079XXXXXXX'}`;
loadStudentChat();
}
function handleLogout() {
localStorage.removeItem('saqel_student_jwt');
localStorage.removeItem('saqel_student_user');
document.cookie = "saqel_student_jwt=; path=/; max-age=0;";
if (wsSocket) wsSocket.close();
location.reload();
}
</script>
</body>
</html>
HTML;
}
}