2196 lines
119 KiB
PHP
2196 lines
119 KiB
PHP
<?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">
|
||
<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>
|
||
</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>
|
||
</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;
|
||
|
||
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 loadStudentLessonPlayback(1);
|
||
}
|
||
|
||
// 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);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
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() {
|
||
const video = document.getElementById('lesson_video_player');
|
||
if (video) {
|
||
video.currentTime = 14;
|
||
triggeredCheckpoints.clear();
|
||
video.play();
|
||
}
|
||
}
|
||
|
||
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 = 1) {
|
||
const token = localStorage.getItem('saqel_student_jwt');
|
||
if (!token) return;
|
||
try {
|
||
const res = await fetch(`/api/lessons/${lessonId}/playback`, {
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
});
|
||
const data = await res.json();
|
||
if (res.ok && data.status === 'success') {
|
||
const pb = data.data.playback;
|
||
const checkpoints = data.data.checkpoints || [];
|
||
const chapters = data.data.chapters || [];
|
||
activeLessonCheckpoints = checkpoints;
|
||
|
||
// 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 && 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('');
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|