Spaces:
Build error
Build error
Upload folder using huggingface_hub
Browse files- .env.example +12 -0
- README.md +247 -10
- app.py +140 -0
- assets/css/style.css +295 -0
- config.py +85 -0
- data/prompts.json +31 -0
- data/sample_profiles.json +46 -0
- data/units.json +187 -0
- pages/__init__.py +1 -0
- pages/experiment.py +223 -0
- pages/home.py +160 -0
- pages/lesson.py +233 -0
- pages/plan.py +234 -0
- pages/profile.py +253 -0
- pages/progress.py +307 -0
- pages/projects.py +257 -0
- requirements.txt +10 -0
- tests/test_app.py +99 -0
- utils/__init__.py +1 -0
- utils/api_utils.py +248 -0
- utils/data_utils.py +266 -0
- utils/ui_utils.py +310 -0
.env.example
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face API
|
| 2 |
+
HF_API_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 3 |
+
HF_MODEL_ID=mistralai/Mistral-7B-Instruct-v0.2
|
| 4 |
+
|
| 5 |
+
# Supabase (اختياري - يمكن استخدام ملفات JSON محلية بدلاً منه)
|
| 6 |
+
SUPABASE_URL=https://xxxxxxxxxxxxx.supabase.co
|
| 7 |
+
SUPABASE_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
| 8 |
+
|
| 9 |
+
# Application Settings
|
| 10 |
+
APP_NAME=مدرستك المحورية
|
| 11 |
+
APP_DESCRIPTION=رحلة تعلم مخصصة على مدار 3 أشهر
|
| 12 |
+
DEFAULT_LANGUAGE=ar
|
README.md
CHANGED
|
@@ -1,12 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
title: Edu App Ai
|
| 3 |
-
emoji: 🌍
|
| 4 |
-
colorFrom: purple
|
| 5 |
-
colorTo: gray
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 6.4.0
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎓 مدرستك المحورية - Personalized Learning Hub
|
| 2 |
+
|
| 3 |
+
<div dir="rtl">
|
| 4 |
+
|
| 5 |
+
## 🌟 نظرة عامة
|
| 6 |
+
|
| 7 |
+
**مدرستك المحورية** هي منصة تعليمية ذكية مبنية بتقنية Streamlit ومدعومة بنماذج Hugging Face، توفر لك تجربة تعلم مخصصة بالكامل على مدار 3 أشهر (12 أسبوعاً). المنصة تتكيف مع مستواك، وقتك المتاح، وأسلوب تعلمك المفضل.
|
| 8 |
+
|
| 9 |
+
## ✨ المميزات الرئيسية
|
| 10 |
+
|
| 11 |
+
### 📝 ملف تعريفي مخصص
|
| 12 |
+
- إنشاء ملف تعريفي شامل (مهارات، مستوى، وقت متاح، أهداف)
|
| 13 |
+
- رفع ملف JSON أو ملء نموذج تفاعلي
|
| 14 |
+
- معاينة وتعديل الملف في أي وقت
|
| 15 |
+
|
| 16 |
+
### 🎯 خطط تعليمية ذكية
|
| 17 |
+
- اختيار من 4 محاور تعليمية:
|
| 18 |
+
- 🤖 **الذكاء الاصطناعي**: أساسيات AI والتعلم الآلي
|
| 19 |
+
- 📊 **البيانات والمنطق**: تحليل البيانات والتفكير النقدي
|
| 20 |
+
- 🎨 **التفكير التصميمي**: حل المشكلات بإبداع
|
| 21 |
+
- 💻 **تطوير الويب**: بناء مواقع حديثة
|
| 22 |
+
- توليد منهج مخصص لـ 12 أسبوعاً
|
| 23 |
+
- جدول زمني يتكيف مع ساعاتك المتاحة
|
| 24 |
+
|
| 25 |
+
### 📚 دروس تفاعلية
|
| 26 |
+
- دروس قصيرة ومكثفة (5-10 دقائق)
|
| 27 |
+
- محتوى يتكيف مع أسلوب تعلمك
|
| 28 |
+
- تمارين عملية وأسئلة للتفكير
|
| 29 |
+
- نظام ملاحظات شخصية
|
| 30 |
+
|
| 31 |
+
### 🔬 تجارب عملية
|
| 32 |
+
- تجربة عملية لكل أسبوع
|
| 33 |
+
- رفع المخرجات والملفات
|
| 34 |
+
- توثيق التعلم والتحديات
|
| 35 |
+
- بناء محفظة أعمال
|
| 36 |
+
|
| 37 |
+
### 👥 مشاريع جماعية
|
| 38 |
+
- إنشاء والانضمام لمشاريع
|
| 39 |
+
- إدارة المهام وتعيينها
|
| 40 |
+
- رفع مخرجات المشروع
|
| 41 |
+
- التعاون مع متعلمين آخرين
|
| 42 |
+
|
| 43 |
+
### 📊 متابعة التقدم
|
| 44 |
+
- لوحة تحكم شاملة للتقدم
|
| 45 |
+
- رسوم بيانية تفاعلية (Plotly)
|
| 46 |
+
- نظام شارات وإنجازات
|
| 47 |
+
- تصدير المحفظة بصيغة JSON
|
| 48 |
+
|
| 49 |
+
## 🚀 التثبيت والتشغيل
|
| 50 |
+
|
| 51 |
+
### المتطلبات
|
| 52 |
+
- Python 3.8+
|
| 53 |
+
- pip
|
| 54 |
+
|
| 55 |
+
### خطوات التشغيل محلياً
|
| 56 |
+
|
| 57 |
+
1. **استنساخ المشروع**
|
| 58 |
+
```bash
|
| 59 |
+
git clone <repository-url>
|
| 60 |
+
cd huggingface-space-repo
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
2. **تثبيت المكتبات**
|
| 64 |
+
```bash
|
| 65 |
+
pip install -r requirements.txt
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
3. **إعداد متغيرات البيئة** (اختياري)
|
| 69 |
+
```bash
|
| 70 |
+
cp .env.example .env
|
| 71 |
+
# عدّل الملف وأضف مفاتيح API إن وُجدت
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
4. **تشغيل التطبيق**
|
| 75 |
+
```bash
|
| 76 |
+
streamlit run app.py
|
| 77 |
+
```
|
| 78 |
+
|
| 79 |
+
5. **فتح المتصفح**
|
| 80 |
+
```
|
| 81 |
+
http://localhost:8501
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
## 📁 هيكل المشروع
|
| 85 |
+
|
| 86 |
+
```
|
| 87 |
+
huggingface-space-repo/
|
| 88 |
+
├── app.py # التطبيق الرئيسي
|
| 89 |
+
├── config.py # الإعدادات العامة
|
| 90 |
+
├── requirements.txt # المكتبات المطلوبة
|
| 91 |
+
├── .env.example # مثال لمتغيرات البيئة
|
| 92 |
+
│
|
| 93 |
+
├── utils/ # الوظائف المساعدة
|
| 94 |
+
│ ├── api_utils.py # تكامل HF API و Supabase
|
| 95 |
+
│ ├── data_utils.py # معالجة البيانات
|
| 96 |
+
│ └── ui_utils.py # تخصيص الواجهة
|
| 97 |
+
│
|
| 98 |
+
├── pages/ # صفحات التطبيق
|
| 99 |
+
│ ├── home.py # الصفحة الرئيسية
|
| 100 |
+
│ ├── profile.py # الملف التعريفي
|
| 101 |
+
│ ├── plan.py # الخطة التعليمية
|
| 102 |
+
│ ├── lesson.py # الدروس
|
| 103 |
+
│ ├── experiment.py # التجارب العملية
|
| 104 |
+
│ ├── projects.py # المشاريع الجماعية
|
| 105 |
+
│ └── progress.py # التقدم والمحفظة
|
| 106 |
+
│
|
| 107 |
+
├── assets/ # الأصول البصرية
|
| 108 |
+
│ └── css/
|
| 109 |
+
│ └── style.css # التنسيقات المخصصة
|
| 110 |
+
│
|
| 111 |
+
├── data/ # البيانات الثابتة
|
| 112 |
+
│ ├── sample_profiles.json
|
| 113 |
+
│ ├── units.json
|
| 114 |
+
│ └── prompts.json
|
| 115 |
+
│
|
| 116 |
+
└── tests/ # الاختبارات
|
| 117 |
+
└── test_app.py
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
## 🎨 التخصيص
|
| 121 |
+
|
| 122 |
+
### الألوان والثيم
|
| 123 |
+
عدّل ملف `config.py` لتغيير الألوان:
|
| 124 |
+
```python
|
| 125 |
+
COLORS = {
|
| 126 |
+
"primary": "#667eea",
|
| 127 |
+
"secondary": "#764ba2",
|
| 128 |
+
# ...
|
| 129 |
+
}
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
### إضافة محاور جديدة
|
| 133 |
+
في `config.py`، أضف محور جديد:
|
| 134 |
+
```python
|
| 135 |
+
LEARNING_HUBS = {
|
| 136 |
+
"new_hub": {
|
| 137 |
+
"name": "المحور الجديد",
|
| 138 |
+
"description": "وصف المحور",
|
| 139 |
+
"duration_weeks": 12,
|
| 140 |
+
"estimated_hours": 120,
|
| 141 |
+
"icon": "🎓"
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
## 🔗 التكامل مع Hugging Face
|
| 147 |
+
|
| 148 |
+
### استخدام نموذج مخصص
|
| 149 |
+
1. احصل على API Token من Hugging Face
|
| 150 |
+
2. أضفه في `.env`:
|
| 151 |
+
```
|
| 152 |
+
HF_API_TOKEN=hf_xxxxxxxxxxxxx
|
| 153 |
+
HF_MODEL_ID=mistralai/Mistral-7B-Instruct-v0.2
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
### استخدام Supabase (اختياري)
|
| 157 |
+
للحفظ الدائم للبيانات:
|
| 158 |
+
```
|
| 159 |
+
SUPABASE_URL=https://xxxxx.supabase.co
|
| 160 |
+
SUPABASE_KEY=xxxxxxxxxxxxx
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
## 🚀 النشر على Hugging Face Spaces
|
| 164 |
+
|
| 165 |
+
1. **إنشاء Space جديد**
|
| 166 |
+
- اذهب إلى: https://huggingface.co/spaces
|
| 167 |
+
- انقر "Create new Space"
|
| 168 |
+
- اختر: Streamlit SDK
|
| 169 |
+
|
| 170 |
+
2. **رفع الملفات**
|
| 171 |
+
```bash
|
| 172 |
+
git clone https://huggingface.co/spaces/<username>/<space-name>
|
| 173 |
+
cd <space-name>
|
| 174 |
+
|
| 175 |
+
# انسخ جميع الملفات
|
| 176 |
+
cp -r /path/to/huggingface-space-repo/* .
|
| 177 |
+
|
| 178 |
+
git add .
|
| 179 |
+
git commit -m "Initial commit"
|
| 180 |
+
git push
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
3. **إعداد Secrets**
|
| 184 |
+
- في إعدادات Space، أضف:
|
| 185 |
+
- `HF_API_TOKEN`
|
| 186 |
+
- `SUPABASE_URL` (إن استخدمت)
|
| 187 |
+
- `SUPABASE_KEY` (إن استخدمت)
|
| 188 |
+
|
| 189 |
+
4. **التشغيل**
|
| 190 |
+
- Space سيعمل تلقائياً!
|
| 191 |
+
|
| 192 |
+
## 📝 الاستخدام
|
| 193 |
+
|
| 194 |
+
### 1. إنشاء ملف تعريفي
|
| 195 |
+
- انتقل لصفحة "الملف التعريفي"
|
| 196 |
+
- املأ معلوماتك (الاسم، المستوى، الوقت المتاح...)
|
| 197 |
+
- أو ارفع ملف JSON
|
| 198 |
+
|
| 199 |
+
### 2. اختيار محور
|
| 200 |
+
- اذهب لصفحة "الخطة التعليمية"
|
| 201 |
+
- اختر المحور المناسب لك
|
| 202 |
+
- انقر "توليد الخطة المخصصة"
|
| 203 |
+
|
| 204 |
+
### 3. البدء بالتعلم
|
| 205 |
+
- افتح "الدروس"
|
| 206 |
+
- اختر أسبوعاً
|
| 207 |
+
- ابدأ بقراءة الدروس
|
| 208 |
+
- ضع علامة "مكتمل" عند الانتهاء
|
| 209 |
+
|
| 210 |
+
### 4. التجارب العملية
|
| 211 |
+
- افتح "التجارب"
|
| 212 |
+
- اختر تجربة من أسبوع معين
|
| 213 |
+
- قم برفع مخرجاتك
|
| 214 |
+
|
| 215 |
+
### 5. المشاريع الجماعية
|
| 216 |
+
- أنشئ مشروعاً أو انضم لواحد
|
| 217 |
+
- أضف مهام وأعضاء
|
| 218 |
+
- ارفع مخرجات المشروع
|
| 219 |
+
|
| 220 |
+
## 🤝 المساهمة
|
| 221 |
+
|
| 222 |
+
نرحب بالمساهمات! لإضافة ميزات جديدة:
|
| 223 |
+
|
| 224 |
+
1. Fork المشروع
|
| 225 |
+
2. أنشئ فرعاً جديداً (`git checkout -b feature/AmazingFeature`)
|
| 226 |
+
3. Commit تغييراتك (`git commit -m 'Add some AmazingFeature'`)
|
| 227 |
+
4. Push للفرع (`git push origin feature/AmazingFeature`)
|
| 228 |
+
5. افتح Pull Request
|
| 229 |
+
|
| 230 |
+
## 📄 الترخيص
|
| 231 |
+
|
| 232 |
+
هذا المشروع مفتوح المصدر تحت ترخيص MIT.
|
| 233 |
+
|
| 234 |
+
## 📧 التواصل
|
| 235 |
+
|
| 236 |
+
لأي استفسارات أو مشاكل، يرجى فتح Issue على GitHub.
|
| 237 |
+
|
| 238 |
+
## 🙏 شكر وتقدير
|
| 239 |
+
|
| 240 |
+
- **Streamlit** - إطار العمل الرائع
|
| 241 |
+
- **Hugging Face** - نماذج الذكاء الاصطناعي
|
| 242 |
+
- **Plotly** - الرسوم البيانية التفاعلية
|
| 243 |
+
- **المجتمع العربي** - الإلهام والدعم
|
| 244 |
+
|
| 245 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
+
**بُني بـ ❤️ لتمكين المتعلمين العرب في كل مكان** 🎓
|
| 248 |
+
|
| 249 |
+
</div>
|
app.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
التطبيق الرئيسي - مدرستك المحورية
|
| 3 |
+
نسخة بدون تسجيل دخول (MVP)
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import streamlit as st
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
# إضافة المسار الحالي
|
| 11 |
+
sys.path.append(str(Path(__file__).parent))
|
| 12 |
+
|
| 13 |
+
import config
|
| 14 |
+
from utils.ui_utils import load_custom_css, init_session_state
|
| 15 |
+
|
| 16 |
+
# استيراد الصفحات
|
| 17 |
+
from pages import home, profile, plan, lesson, experiment, projects, progress
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def main():
|
| 21 |
+
"""الدالة الرئيسية للتطبيق"""
|
| 22 |
+
|
| 23 |
+
# إعدادات الصفحة
|
| 24 |
+
st.set_page_config(
|
| 25 |
+
page_title=config.APP_NAME,
|
| 26 |
+
page_icon="🎓",
|
| 27 |
+
layout="wide",
|
| 28 |
+
initial_sidebar_state="expanded"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# تحميل CSS مخصص
|
| 32 |
+
load_custom_css()
|
| 33 |
+
|
| 34 |
+
# تهيئة session state
|
| 35 |
+
init_session_state({
|
| 36 |
+
"page": "home",
|
| 37 |
+
"profile": None,
|
| 38 |
+
"selected_hub": None,
|
| 39 |
+
"curriculum": None,
|
| 40 |
+
"schedule": None,
|
| 41 |
+
"completed_lessons": [],
|
| 42 |
+
"completed_experiments": [],
|
| 43 |
+
"completed_units": [],
|
| 44 |
+
"projects": [],
|
| 45 |
+
"user_projects": [],
|
| 46 |
+
"experiment_submissions": {},
|
| 47 |
+
"selected_week_idx": 0
|
| 48 |
+
})
|
| 49 |
+
|
| 50 |
+
# Sidebar للتنقل
|
| 51 |
+
with st.sidebar:
|
| 52 |
+
st.markdown(f"# {config.APP_NAME}")
|
| 53 |
+
st.markdown("---")
|
| 54 |
+
|
| 55 |
+
# قائمة التنقل
|
| 56 |
+
st.markdown("### 📍 التنقل")
|
| 57 |
+
|
| 58 |
+
pages = {
|
| 59 |
+
"home": {"name": "🏠 الصفحة الرئيسية", "icon": "🏠"},
|
| 60 |
+
"profile": {"name": "👤 الملف التعريفي", "icon": "👤"},
|
| 61 |
+
"plan": {"name": "📅 الخطة التعليمية", "icon": "📅"},
|
| 62 |
+
"lesson": {"name": "📚 الدروس", "icon": "📚"},
|
| 63 |
+
"experiment": {"name": "🔬 التجارب", "icon": "🔬"},
|
| 64 |
+
"projects": {"name": "👥 المشاريع", "icon": "👥"},
|
| 65 |
+
"progress": {"name": "📊 التقدم", "icon": "📊"}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
for page_key, page_data in pages.items():
|
| 69 |
+
if st.button(
|
| 70 |
+
page_data['name'],
|
| 71 |
+
key=f"nav_{page_key}",
|
| 72 |
+
use_container_width=True,
|
| 73 |
+
type="primary" if st.session_state.page == page_key else "secondary"
|
| 74 |
+
):
|
| 75 |
+
st.session_state.page = page_key
|
| 76 |
+
st.rerun()
|
| 77 |
+
|
| 78 |
+
st.markdown("---")
|
| 79 |
+
|
| 80 |
+
# معلومات سريعة
|
| 81 |
+
if st.session_state.profile:
|
| 82 |
+
st.markdown("### 👤 معلومات سريعة")
|
| 83 |
+
st.markdown(f"**الاسم:** {st.session_state.profile.get('name', 'غير محدد')}")
|
| 84 |
+
st.markdown(f"**المستوى:** {st.session_state.profile.get('level', 'مبتدئ')}")
|
| 85 |
+
|
| 86 |
+
if st.session_state.curriculum:
|
| 87 |
+
total_lessons = sum(len(unit.get("lessons", [])) for unit in st.session_state.curriculum)
|
| 88 |
+
completed = len(st.session_state.completed_lessons)
|
| 89 |
+
percentage = (completed / total_lessons * 100) if total_lessons > 0 else 0
|
| 90 |
+
|
| 91 |
+
st.markdown(f"**التقدم:** {percentage:.1f}%")
|
| 92 |
+
st.progress(percentage / 100)
|
| 93 |
+
|
| 94 |
+
st.markdown("---")
|
| 95 |
+
|
| 96 |
+
# معلومات التطبيق
|
| 97 |
+
st.markdown("### ℹ️ معلومات")
|
| 98 |
+
st.markdown(f"""
|
| 99 |
+
**الإصدار:** 1.0.0 (MVP)
|
| 100 |
+
**النوع:** بدون تسجيل دخول
|
| 101 |
+
**الحالة:** تجريبي
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
🤗 **مدعوم بـ Hugging Face**
|
| 106 |
+
💻 **مبني بـ Streamlit**
|
| 107 |
+
""")
|
| 108 |
+
|
| 109 |
+
# زر إعادة تعيين (للتجربة)
|
| 110 |
+
st.markdown("---")
|
| 111 |
+
if st.button("🔄 إعادة تعيين التطبيق", use_container_width=True):
|
| 112 |
+
for key in list(st.session_state.keys()):
|
| 113 |
+
del st.session_state[key]
|
| 114 |
+
st.rerun()
|
| 115 |
+
|
| 116 |
+
# عرض الصفحة المحددة
|
| 117 |
+
try:
|
| 118 |
+
if st.session_state.page == "home":
|
| 119 |
+
home.show()
|
| 120 |
+
elif st.session_state.page == "profile":
|
| 121 |
+
profile.show()
|
| 122 |
+
elif st.session_state.page == "plan":
|
| 123 |
+
plan.show()
|
| 124 |
+
elif st.session_state.page == "lesson":
|
| 125 |
+
lesson.show()
|
| 126 |
+
elif st.session_state.page == "experiment":
|
| 127 |
+
experiment.show()
|
| 128 |
+
elif st.session_state.page == "projects":
|
| 129 |
+
projects.show()
|
| 130 |
+
elif st.session_state.page == "progress":
|
| 131 |
+
progress.show()
|
| 132 |
+
else:
|
| 133 |
+
home.show()
|
| 134 |
+
except Exception as e:
|
| 135 |
+
st.error(f"حدث خطأ: {str(e)}")
|
| 136 |
+
st.info("يرجى المحاولة مرة أخرى أو إعادة تعيين التطبيق من الشريط الجانبي")
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
if __name__ == "__main__":
|
| 140 |
+
main()
|
assets/css/style.css
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ملف CSS مخصص للتطبيق */
|
| 2 |
+
|
| 3 |
+
/* استيراد خط عربي جميل */
|
| 4 |
+
@import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@300;400;500;700;800&display=swap');
|
| 5 |
+
|
| 6 |
+
/* تطبيق الخط على كل العناصر */
|
| 7 |
+
* {
|
| 8 |
+
font-family: 'Tajawal', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
| 9 |
+
direction: rtl;
|
| 10 |
+
text-align: right;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
/* خلفية التطبيق */
|
| 14 |
+
.main {
|
| 15 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 16 |
+
background-attachment: fixed;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* حاوية المحتوى */
|
| 20 |
+
.block-container {
|
| 21 |
+
background: rgba(255, 255, 255, 0.97);
|
| 22 |
+
border-radius: 25px;
|
| 23 |
+
padding: 2.5rem;
|
| 24 |
+
margin-top: 2rem;
|
| 25 |
+
margin-bottom: 2rem;
|
| 26 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
| 27 |
+
backdrop-filter: blur(10px);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/* العناوين */
|
| 31 |
+
h1, h2, h3, h4, h5, h6 {
|
| 32 |
+
color: #667eea;
|
| 33 |
+
font-weight: 700;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
h1 {
|
| 37 |
+
font-size: 2.5rem !important;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* الأزرار */
|
| 41 |
+
.stButton > button {
|
| 42 |
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
| 43 |
+
color: white;
|
| 44 |
+
border: none;
|
| 45 |
+
border-radius: 12px;
|
| 46 |
+
padding: 0.75rem 2rem;
|
| 47 |
+
font-size: 1.05rem;
|
| 48 |
+
font-weight: 600;
|
| 49 |
+
transition: all 0.3s ease;
|
| 50 |
+
width: 100%;
|
| 51 |
+
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.stButton > button:hover {
|
| 55 |
+
transform: translateY(-3px);
|
| 56 |
+
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.stButton > button:active {
|
| 60 |
+
transform: translateY(-1px);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* الأزرار الثانوية */
|
| 64 |
+
.stButton > button[kind="secondary"] {
|
| 65 |
+
background: white;
|
| 66 |
+
color: #667eea;
|
| 67 |
+
border: 2px solid #667eea;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.stButton > button[kind="secondary"]:hover {
|
| 71 |
+
background: #f7fafc;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* صناديق المعلومات */
|
| 75 |
+
.success-box {
|
| 76 |
+
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
| 77 |
+
color: white;
|
| 78 |
+
padding: 1.25rem;
|
| 79 |
+
border-radius: 12px;
|
| 80 |
+
margin: 1.5rem 0;
|
| 81 |
+
box-shadow: 0 4px 15px rgba(72, 187, 120, 0.3);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.info-box {
|
| 85 |
+
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
|
| 86 |
+
color: white;
|
| 87 |
+
padding: 1.25rem;
|
| 88 |
+
border-radius: 12px;
|
| 89 |
+
margin: 1.5rem 0;
|
| 90 |
+
box-shadow: 0 4px 15px rgba(66, 153, 225, 0.3);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.warning-box {
|
| 94 |
+
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
|
| 95 |
+
color: white;
|
| 96 |
+
padding: 1.25rem;
|
| 97 |
+
border-radius: 12px;
|
| 98 |
+
margin: 1.5rem 0;
|
| 99 |
+
box-shadow: 0 4px 15px rgba(237, 137, 54, 0.3);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/* البطاقات */
|
| 103 |
+
.card {
|
| 104 |
+
background: white;
|
| 105 |
+
border-radius: 15px;
|
| 106 |
+
padding: 1.75rem;
|
| 107 |
+
margin: 1.25rem 0;
|
| 108 |
+
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
|
| 109 |
+
transition: all 0.3s ease;
|
| 110 |
+
border: 1px solid #e2e8f0;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.card:hover {
|
| 114 |
+
transform: translateY(-5px);
|
| 115 |
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* شريط التقدم */
|
| 119 |
+
.progress-bar {
|
| 120 |
+
background: #e2e8f0;
|
| 121 |
+
border-radius: 12px;
|
| 122 |
+
height: 30px;
|
| 123 |
+
overflow: hidden;
|
| 124 |
+
margin: 1.5rem 0;
|
| 125 |
+
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
.progress-fill {
|
| 129 |
+
background: linear-gradient(90deg, #48bb78 0%, #38a169 100%);
|
| 130 |
+
height: 100%;
|
| 131 |
+
display: flex;
|
| 132 |
+
align-items: center;
|
| 133 |
+
justify-content: center;
|
| 134 |
+
color: white;
|
| 135 |
+
font-weight: 700;
|
| 136 |
+
font-size: 0.95rem;
|
| 137 |
+
transition: width 0.6s ease;
|
| 138 |
+
box-shadow: 0 2px 8px rgba(72, 187, 120, 0.4);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
/* الشارات */
|
| 142 |
+
.badge {
|
| 143 |
+
display: inline-block;
|
| 144 |
+
padding: 0.4rem 0.9rem;
|
| 145 |
+
border-radius: 25px;
|
| 146 |
+
font-size: 0.85rem;
|
| 147 |
+
font-weight: 600;
|
| 148 |
+
margin: 0.25rem;
|
| 149 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.badge-primary {
|
| 153 |
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
| 154 |
+
color: white;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
.badge-success {
|
| 158 |
+
background: #48bb78;
|
| 159 |
+
color: white;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.badge-warning {
|
| 163 |
+
background: #ed8936;
|
| 164 |
+
color: white;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.badge-info {
|
| 168 |
+
background: #4299e1;
|
| 169 |
+
color: white;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.badge-danger {
|
| 173 |
+
background: #f56565;
|
| 174 |
+
color: white;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/* حقول الإدخال */
|
| 178 |
+
.stTextInput > div > div > input,
|
| 179 |
+
.stTextArea > div > div > textarea,
|
| 180 |
+
.stSelectbox > div > div > select {
|
| 181 |
+
border-radius: 10px;
|
| 182 |
+
border: 2px solid #e2e8f0;
|
| 183 |
+
padding: 0.75rem;
|
| 184 |
+
font-size: 1rem;
|
| 185 |
+
transition: all 0.3s ease;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.stTextInput > div > div > input:focus,
|
| 189 |
+
.stTextArea > div > div > textarea:focus,
|
| 190 |
+
.stSelectbox > div > div > select:focus {
|
| 191 |
+
border-color: #667eea;
|
| 192 |
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/* Expander */
|
| 196 |
+
.streamlit-expanderHeader {
|
| 197 |
+
background: #f7fafc;
|
| 198 |
+
border-radius: 10px;
|
| 199 |
+
font-weight: 600;
|
| 200 |
+
padding: 1rem;
|
| 201 |
+
border: 1px solid #e2e8f0;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.streamlit-expanderHeader:hover {
|
| 205 |
+
background: #edf2f7;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/* Tabs */
|
| 209 |
+
.stTabs [data-baseweb="tab-list"] {
|
| 210 |
+
gap: 0.5rem;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.stTabs [data-baseweb="tab"] {
|
| 214 |
+
border-radius: 10px 10px 0 0;
|
| 215 |
+
padding: 0.75rem 1.5rem;
|
| 216 |
+
font-weight: 600;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.stTabs [aria-selected="true"] {
|
| 220 |
+
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
|
| 221 |
+
color: white;
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
/* Metrics */
|
| 225 |
+
.stMetric {
|
| 226 |
+
background: white;
|
| 227 |
+
padding: 1rem;
|
| 228 |
+
border-radius: 10px;
|
| 229 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/* Sidebar */
|
| 233 |
+
.css-1d391kg, [data-testid="stSidebar"] {
|
| 234 |
+
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
|
| 235 |
+
color: white;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.css-1d391kg .stButton > button, [data-testid="stSidebar"] .stButton > button {
|
| 239 |
+
background: rgba(255, 255, 255, 0.2);
|
| 240 |
+
color: white;
|
| 241 |
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.css-1d391kg .stButton > button:hover, [data-testid="stSidebar"] .stButton > button:hover {
|
| 245 |
+
background: rgba(255, 255, 255, 0.3);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
/* تنسيقات إضافية */
|
| 249 |
+
hr {
|
| 250 |
+
border: none;
|
| 251 |
+
border-top: 2px solid #e2e8f0;
|
| 252 |
+
margin: 2rem 0;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
/* تحسين المسافات */
|
| 256 |
+
.element-container {
|
| 257 |
+
margin-bottom: 1rem;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
/* رسائل Streamlit */
|
| 261 |
+
.stAlert {
|
| 262 |
+
border-radius: 10px;
|
| 263 |
+
border: none;
|
| 264 |
+
padding: 1rem;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
/* File Uploader */
|
| 268 |
+
.stFileUploader {
|
| 269 |
+
border: 2px dashed #cbd5e0;
|
| 270 |
+
border-radius: 10px;
|
| 271 |
+
padding: 2rem;
|
| 272 |
+
transition: all 0.3s ease;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.stFileUploader:hover {
|
| 276 |
+
border-color: #667eea;
|
| 277 |
+
background: #f7fafc;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
/* تحسينات للجوال */
|
| 281 |
+
@media (max-width: 768px) {
|
| 282 |
+
.block-container {
|
| 283 |
+
padding: 1.5rem;
|
| 284 |
+
margin-top: 1rem;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
h1 {
|
| 288 |
+
font-size: 2rem !important;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.stButton > button {
|
| 292 |
+
padding: 0.6rem 1rem;
|
| 293 |
+
font-size: 0.95rem;
|
| 294 |
+
}
|
| 295 |
+
}
|
config.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ملف الإعدادات العامة للتطبيق
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
|
| 7 |
+
# تحميل متغيرات البيئة
|
| 8 |
+
load_dotenv()
|
| 9 |
+
|
| 10 |
+
# إعدادات Hugging Face
|
| 11 |
+
HF_API_TOKEN = os.getenv("HF_API_TOKEN", "")
|
| 12 |
+
HF_MODEL_ID = os.getenv("HF_MODEL_ID", "mistralai/Mistral-7B-Instruct-v0.2")
|
| 13 |
+
HF_API_URL = f"https://api-inference.huggingface.co/models/{HF_MODEL_ID}"
|
| 14 |
+
|
| 15 |
+
# إعدادات Supabase (اختياري)
|
| 16 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
|
| 17 |
+
SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
|
| 18 |
+
USE_SUPABASE = bool(SUPABASE_URL and SUPABASE_KEY)
|
| 19 |
+
|
| 20 |
+
# إعدادات التطبيق
|
| 21 |
+
APP_NAME = "🎓 مدرستك المحورية"
|
| 22 |
+
APP_DESCRIPTION = "رحلة تعلم مخصصة على مدار 3 أشهر"
|
| 23 |
+
DEFAULT_LANGUAGE = "ar"
|
| 24 |
+
|
| 25 |
+
# إعدادات الخطط
|
| 26 |
+
PLAN_DURATION_WEEKS = 12
|
| 27 |
+
DEFAULT_HOURS_PER_WEEK = 10
|
| 28 |
+
|
| 29 |
+
# ألوان التطبيق (Theme)
|
| 30 |
+
COLORS = {
|
| 31 |
+
"primary": "#667eea",
|
| 32 |
+
"secondary": "#764ba2",
|
| 33 |
+
"success": "#48bb78",
|
| 34 |
+
"warning": "#ed8936",
|
| 35 |
+
"danger": "#f56565",
|
| 36 |
+
"info": "#4299e1",
|
| 37 |
+
"light": "#f7fafc",
|
| 38 |
+
"dark": "#2d3748",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
# أيقونات
|
| 42 |
+
ICONS = {
|
| 43 |
+
"home": "🏠",
|
| 44 |
+
"profile": "👤",
|
| 45 |
+
"plan": "📅",
|
| 46 |
+
"lesson": "📚",
|
| 47 |
+
"experiment": "🔬",
|
| 48 |
+
"projects": "👥",
|
| 49 |
+
"progress": "📊",
|
| 50 |
+
"completed": "✅",
|
| 51 |
+
"pending": "⏳",
|
| 52 |
+
"locked": "🔒",
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
# محاور التعلم (Hubs) المتاحة
|
| 56 |
+
LEARNING_HUBS = {
|
| 57 |
+
"ai_intro": {
|
| 58 |
+
"name": "مقدمة في الذكاء الاصطناعي",
|
| 59 |
+
"description": "تعلم أساسيات الذكاء الاصطناعي والتعلم الآلي",
|
| 60 |
+
"duration_weeks": 12,
|
| 61 |
+
"estimated_hours": 120,
|
| 62 |
+
"icon": "🤖"
|
| 63 |
+
},
|
| 64 |
+
"data_logic": {
|
| 65 |
+
"name": "البيانات والمنطق",
|
| 66 |
+
"description": "تحليل البيانات والتفكير المنطقي",
|
| 67 |
+
"duration_weeks": 12,
|
| 68 |
+
"estimated_hours": 100,
|
| 69 |
+
"icon": "📊"
|
| 70 |
+
},
|
| 71 |
+
"design_thinking": {
|
| 72 |
+
"name": "التفكير التصميمي",
|
| 73 |
+
"description": "حل المشكلات بطريقة إبداعية",
|
| 74 |
+
"duration_weeks": 12,
|
| 75 |
+
"estimated_hours": 90,
|
| 76 |
+
"icon": "🎨"
|
| 77 |
+
},
|
| 78 |
+
"web_development": {
|
| 79 |
+
"name": "تطوير الويب",
|
| 80 |
+
"description": "بناء مواقع وتطبيقات ويب حديثة",
|
| 81 |
+
"duration_weeks": 12,
|
| 82 |
+
"estimated_hours": 150,
|
| 83 |
+
"icon": "💻"
|
| 84 |
+
},
|
| 85 |
+
}
|
data/prompts.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"curriculum_generation": {
|
| 3 |
+
"system": "أنت مصمم مناهج تعليمية خبير. مهمتك توليد خطة تعلم لمدة {weeks} أسبوعاً لمستخدم بناءً على ملفه التعريفي.",
|
| 4 |
+
"user_template": "معلومات المستخدم:\n- الاسم: {name}\n- المستوى: {level}\n- الوقت المتاح أسبوعياً: {available_hours} ساعات\n- أسلوب التعلم المفضل: {learning_style}\n- المحور المطلوب: {hub_name}\n\nولّد خطة تعلم بصيغة JSON تحتوي على {weeks} وحدة أسبوعية. كل وحدة يجب أن تحتوي على:\n- week: رقم الأسبوع\n- unit_title: عنوان الوحدة\n- estimated_time_mins: الوقت المقدر بالدقائق\n- lessons: قائمة بالدروس (كل درس له: title, duration_mins, type, description)\n- experiment: تجربة عملية (title, description, deliverable)\n\nردّ فقط بصيغة JSON بدون أي نص إضافي."
|
| 5 |
+
},
|
| 6 |
+
|
| 7 |
+
"lesson_generation": {
|
| 8 |
+
"system": "أنت مدرس متخصص يكتب دروساً تعليمية قصيرة (5-10 دقائق قراءة) لفهم مفهوم واحد عميق.",
|
| 9 |
+
"user_template": "موضوع الدرس: {lesson_title}\n\nالدرس يجب أن يتضمن:\n1. الفكرة المركزية (ملخص في سطرين)\n2. شرح مبسط مع أمثلة واقعية\n3. نقاط رئيسية (3-5 نقاط)\n4. سؤال للتفكير\n5. مقترحات للمتابعة\n\nاكتب بأسلوب واضح ومشجع ومناسب لمستوى: {level}"
|
| 10 |
+
},
|
| 11 |
+
|
| 12 |
+
"lesson_adaptation": {
|
| 13 |
+
"system": "أنت مدرس يتكيف مع أسلوب المتعلم.",
|
| 14 |
+
"user_template": "المستخدم يفضل الأسلوب: {preferred_style}\n\nأعد كتابة الدرس التالي بطريقة تناسب هذا الأسلوب:\n\n{lesson_content}\n\nتأكد من:\n- إذا كان بصرياً: استخدم أوصافاً مرئية وأمثلة مصورة\n- إذا كان نصياً: ركز على التفاصيل المكتوبة والشروحات الدقيقة\n- إذا كان عملياً: أكثر من الأمثلة التطبيقية والتمارين\n- إذا كان مختلطاً: امزج بين كل الأساليب"
|
| 15 |
+
},
|
| 16 |
+
|
| 17 |
+
"exercise_generation": {
|
| 18 |
+
"system": "أنت مصمم تمارين تعليمية مبتكرة.",
|
| 19 |
+
"user_template": "اصنع تمريناً يختبر الفهم الحقيقي للمفهوم التالي: {concept}\n\nالتمرين يجب أن:\n- لا يعتمد فقط على الحفظ\n- يتطلب تطبيق عملي أو تفكير نقدي\n- يكون مناسباً لمستوى: {level}\n- يستغرق حوالي {duration} دقيقة\n\nأرفق معيار تصحيح مختصر."
|
| 20 |
+
},
|
| 21 |
+
|
| 22 |
+
"project_suggestion": {
|
| 23 |
+
"system": "أنت مستشار مشاريع تعليمية.",
|
| 24 |
+
"user_template": "اقترح {count} أفكار مشاريع جماعية مناسبة للمحور: {hub_name}\n\nكل مشروع يجب أن يتضمن:\n- العنوان\n- الوصف (2-3 جمل)\n- المهارات المطلوبة\n- المدة المقترحة (بالأسابيع)\n- المخرجات المتوقعة\n- عدد الأعضاء المناسب\n\nالمشاريع يجب أن تكون:\n- عملية وقابلة للتنفيذ\n- مناسبة لمستوى: {level}\n- تعزز التعلم التعاوني"
|
| 25 |
+
},
|
| 26 |
+
|
| 27 |
+
"feedback_generation": {
|
| 28 |
+
"system": "أنت معلم يقدم تغذية راجعة بناءة ومشجعة.",
|
| 29 |
+
"user_template": "الطالب أكمل التمرين/المشروع التالي:\n\n{submission}\n\nقدم تغذية راجعة تتضمن:\n1. نقاط القوة (2-3 نقاط)\n2. مجالات التحسين (2-3 نقاط)\n3. اقتراحات محددة للتطوير\n4. كلمة تشجيعية\n\nكن إيجابياً وداعماً مع الحفاظ على الموضوعية."
|
| 30 |
+
}
|
| 31 |
+
}
|
data/sample_profiles.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"sample_profiles": [
|
| 3 |
+
{
|
| 4 |
+
"name": "أحمد محمد",
|
| 5 |
+
"age": "25",
|
| 6 |
+
"level": "متوسط",
|
| 7 |
+
"skills": ["Python", "تحليل البيانات", "Excel"],
|
| 8 |
+
"available_hours": 15,
|
| 9 |
+
"learning_style": "مختلط (نظري وعملي)",
|
| 10 |
+
"goals": "أريد تعلم الذكاء الاصطناعي والتعلم الآلي لبناء تطبيقات ذكية وتحسين مهاراتي في مجال البيانات",
|
| 11 |
+
"preferences": {
|
| 12 |
+
"language": "ar",
|
| 13 |
+
"notifications": true,
|
| 14 |
+
"theme": "light"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"name": "سارة علي",
|
| 19 |
+
"age": "22",
|
| 20 |
+
"level": "مبتدئ",
|
| 21 |
+
"skills": ["تصميم جرافيك", "Photoshop"],
|
| 22 |
+
"available_hours": 10,
|
| 23 |
+
"learning_style": "بصري (فيديوهات، صور)",
|
| 24 |
+
"goals": "أرغب في تعلم التفكير التصميمي وتطوير مهاراتي الإبداعية لحل المشكلات بطرق مبتكرة",
|
| 25 |
+
"preferences": {
|
| 26 |
+
"language": "ar",
|
| 27 |
+
"notifications": true,
|
| 28 |
+
"theme": "light"
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"name": "محمد حسن",
|
| 33 |
+
"age": "30",
|
| 34 |
+
"level": "متقدم",
|
| 35 |
+
"skills": ["JavaScript", "React", "Node.js", "HTML", "CSS"],
|
| 36 |
+
"available_hours": 20,
|
| 37 |
+
"learning_style": "عملي (تطبيقات، مشاريع)",
|
| 38 |
+
"goals": "أسعى لتطوير مهاراتي في تطوير الويب الحديث وبناء تطبيقات متقدمة",
|
| 39 |
+
"preferences": {
|
| 40 |
+
"language": "ar",
|
| 41 |
+
"notifications": true,
|
| 42 |
+
"theme": "dark"
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
]
|
| 46 |
+
}
|
data/units.json
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"ai_intro": {
|
| 3 |
+
"hub_id": "ai_intro",
|
| 4 |
+
"name": "مقدمة في الذكاء الاصطناعي",
|
| 5 |
+
"description": "تعلم أساسيات الذكاء الاصطناعي والتعلم الآلي",
|
| 6 |
+
"icon": "🤖",
|
| 7 |
+
"sample_units": [
|
| 8 |
+
{
|
| 9 |
+
"week": 1,
|
| 10 |
+
"unit_title": "ما هو الذكاء الاصطناعي؟",
|
| 11 |
+
"estimated_time_mins": 480,
|
| 12 |
+
"lessons": [
|
| 13 |
+
{
|
| 14 |
+
"title": "تعريف الذكاء الاصطناعي",
|
| 15 |
+
"duration_mins": 60,
|
| 16 |
+
"type": "concept",
|
| 17 |
+
"description": "فهم المفاهيم الأساسية للذكاء الاصطناعي وتطبيقاته"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"title": "تاريخ الذكاء الاصطناعي",
|
| 21 |
+
"duration_mins": 90,
|
| 22 |
+
"type": "theory",
|
| 23 |
+
"description": "رحلة عبر تطور AI من الخمسينات حتى اليوم"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"title": "أنواع الذكاء الاصطناعي",
|
| 27 |
+
"duration_mins": 120,
|
| 28 |
+
"type": "concept",
|
| 29 |
+
"description": "التعرف على AI الضيق، العام، والفائق"
|
| 30 |
+
}
|
| 31 |
+
],
|
| 32 |
+
"experiment": {
|
| 33 |
+
"title": "استكشاف تطبيقات AI في حياتك",
|
| 34 |
+
"description": "قم بتوثيق 10 تطبيقات ذكاء اصطناعي تستخدمها يومياً وكيف تعمل",
|
| 35 |
+
"deliverable": "تقرير مكتوب أو عرض تقديمي"
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"week": 2,
|
| 40 |
+
"unit_title": "أساسيات التعلم الآلي",
|
| 41 |
+
"estimated_time_mins": 540,
|
| 42 |
+
"lessons": [
|
| 43 |
+
{
|
| 44 |
+
"title": "مقدمة في Machine Learning",
|
| 45 |
+
"duration_mins": 90,
|
| 46 |
+
"type": "concept",
|
| 47 |
+
"description": "فهم الفرق بين البرمجة التقليدية والتعلم الآلي"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"title": "أنواع التعلم الآلي",
|
| 51 |
+
"duration_mins": 120,
|
| 52 |
+
"type": "theory",
|
| 53 |
+
"description": "التعلم الموجه، غير الموجه، والتعزيزي"
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"title": "أول نموذج تعلم آلي",
|
| 57 |
+
"duration_mins": 180,
|
| 58 |
+
"type": "practical",
|
| 59 |
+
"description": "بناء نموذج بسيط للتنبؤ باستخدام Python"
|
| 60 |
+
}
|
| 61 |
+
],
|
| 62 |
+
"experiment": {
|
| 63 |
+
"title": "بناء نموذج تصنيف بسيط",
|
| 64 |
+
"description": "استخدم مكتبة scikit-learn لبناء نموذج يصنف البيانات",
|
| 65 |
+
"deliverable": "كود Python + شرح للنتائج"
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
]
|
| 69 |
+
},
|
| 70 |
+
|
| 71 |
+
"data_logic": {
|
| 72 |
+
"hub_id": "data_logic",
|
| 73 |
+
"name": "البيانات والمنطق",
|
| 74 |
+
"description": "تحليل البيانات والتفكير المنطقي",
|
| 75 |
+
"icon": "📊",
|
| 76 |
+
"sample_units": [
|
| 77 |
+
{
|
| 78 |
+
"week": 1,
|
| 79 |
+
"unit_title": "مقدمة في تحليل البيانات",
|
| 80 |
+
"estimated_time_mins": 450,
|
| 81 |
+
"lessons": [
|
| 82 |
+
{
|
| 83 |
+
"title": "ما هي البيانات؟",
|
| 84 |
+
"duration_mins": 60,
|
| 85 |
+
"type": "concept",
|
| 86 |
+
"description": "فهم أنواع البيانات ومصادرها"
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"title": "أدوات تحليل البيانات",
|
| 90 |
+
"duration_mins": 120,
|
| 91 |
+
"type": "practical",
|
| 92 |
+
"description": "التعرف على Excel, Python, وأدوات أخرى"
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"title": "التفكير النقدي والبيانات",
|
| 96 |
+
"duration_mins": 90,
|
| 97 |
+
"type": "theory",
|
| 98 |
+
"description": "كيف نقرأ البيانات بشكل نقدي"
|
| 99 |
+
}
|
| 100 |
+
],
|
| 101 |
+
"experiment": {
|
| 102 |
+
"title": "تحليل مجموعة بيانات حقيقية",
|
| 103 |
+
"description": "اختر مجموعة بيانات من Kaggle وقم بتحليلها",
|
| 104 |
+
"deliverable": "تقرير تحليلي + رسوم بيانية"
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
]
|
| 108 |
+
},
|
| 109 |
+
|
| 110 |
+
"design_thinking": {
|
| 111 |
+
"hub_id": "design_thinking",
|
| 112 |
+
"name": "التفكير التصميمي",
|
| 113 |
+
"description": "حل المشكلات بطريقة إبداعية",
|
| 114 |
+
"icon": "🎨",
|
| 115 |
+
"sample_units": [
|
| 116 |
+
{
|
| 117 |
+
"week": 1,
|
| 118 |
+
"unit_title": "مبادئ التفكير التصميمي",
|
| 119 |
+
"estimated_time_mins": 420,
|
| 120 |
+
"lessons": [
|
| 121 |
+
{
|
| 122 |
+
"title": "ما هو التفكير التصميمي؟",
|
| 123 |
+
"duration_mins": 60,
|
| 124 |
+
"type": "concept",
|
| 125 |
+
"description": "فهم العملية الإبداعية لحل المشكلات"
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"title": "المراحل الخمس",
|
| 129 |
+
"duration_mins": 120,
|
| 130 |
+
"type": "theory",
|
| 131 |
+
"description": "التعاطف، التعريف، التخيل، النمذجة، الاختبار"
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"title": "التعاطف مع المستخدم",
|
| 135 |
+
"duration_mins": 90,
|
| 136 |
+
"type": "practical",
|
| 137 |
+
"description": "تقنيات لفهم احتياجات المستخدمين"
|
| 138 |
+
}
|
| 139 |
+
],
|
| 140 |
+
"experiment": {
|
| 141 |
+
"title": "مشروع تصميم حل لمشكلة محلية",
|
| 142 |
+
"description": "حدد مشكلة في محيطك وطبق عملية التفكير التصميمي",
|
| 143 |
+
"deliverable": "عرض تقديمي للحل المقترح"
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
]
|
| 147 |
+
},
|
| 148 |
+
|
| 149 |
+
"web_development": {
|
| 150 |
+
"hub_id": "web_development",
|
| 151 |
+
"name": "تطوير الويب",
|
| 152 |
+
"description": "بناء مواقع وتطبيقات ويب حديثة",
|
| 153 |
+
"icon": "💻",
|
| 154 |
+
"sample_units": [
|
| 155 |
+
{
|
| 156 |
+
"week": 1,
|
| 157 |
+
"unit_title": "أساسيات HTML و CSS",
|
| 158 |
+
"estimated_time_mins": 600,
|
| 159 |
+
"lessons": [
|
| 160 |
+
{
|
| 161 |
+
"title": "مقدمة في HTML",
|
| 162 |
+
"duration_mins": 120,
|
| 163 |
+
"type": "concept",
|
| 164 |
+
"description": "بنية صفحة الويب والعناصر الأساسية"
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"title": "تنسيق الصفحات بـ CSS",
|
| 168 |
+
"duration_mins": 180,
|
| 169 |
+
"type": "practical",
|
| 170 |
+
"description": "الألوان، الخطوط، والتخطيط"
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"title": "التصميم المتجاوب",
|
| 174 |
+
"duration_mins": 120,
|
| 175 |
+
"type": "practical",
|
| 176 |
+
"description": "بناء صفحات تعمل على جميع الأجهزة"
|
| 177 |
+
}
|
| 178 |
+
],
|
| 179 |
+
"experiment": {
|
| 180 |
+
"title": "بناء صفحة ويب شخصية",
|
| 181 |
+
"description": "صمم وطور صفحة ويب تعريفية عنك",
|
| 182 |
+
"deliverable": "موقع ويب منشور على GitHub Pages"
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
]
|
| 186 |
+
}
|
| 187 |
+
}
|
pages/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# ملف لجعل pages مجلداً قابلاً للاستيراد
|
pages/experiment.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
صفحة التجارب العملية - رفع المخرجات وتوثيق التجارب
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import sys
|
| 6 |
+
sys.path.append('..')
|
| 7 |
+
from utils.ui_utils import load_custom_css, show_success_message, show_warning_message
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def show():
|
| 12 |
+
"""عرض صفحة التجارب العملية"""
|
| 13 |
+
|
| 14 |
+
load_custom_css()
|
| 15 |
+
|
| 16 |
+
st.markdown("## 🔬 التجارب العملية")
|
| 17 |
+
st.markdown("---")
|
| 18 |
+
|
| 19 |
+
# تهيئة completed_experiments
|
| 20 |
+
if "completed_experiments" not in st.session_state:
|
| 21 |
+
st.session_state.completed_experiments = []
|
| 22 |
+
|
| 23 |
+
if "experiment_submissions" not in st.session_state:
|
| 24 |
+
st.session_state.experiment_submissions = {}
|
| 25 |
+
|
| 26 |
+
# إذا كان هناك تجربة محددة
|
| 27 |
+
if "current_experiment" in st.session_state and st.session_state.current_experiment:
|
| 28 |
+
experiment = st.session_state.current_experiment
|
| 29 |
+
week = st.session_state.get("current_week", 0)
|
| 30 |
+
|
| 31 |
+
st.markdown(f"""
|
| 32 |
+
<div class='card' style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;'>
|
| 33 |
+
<h2 style='color: white; margin-top: 0;'>🔬 {experiment['title']}</h2>
|
| 34 |
+
<p><strong>الأسبوع:</strong> {week}</p>
|
| 35 |
+
<p style='font-size: 1.1rem;'>{experiment['description']}</p>
|
| 36 |
+
<p><strong>📦 المخرج المطلوب:</strong> {experiment['deliverable']}</p>
|
| 37 |
+
</div>
|
| 38 |
+
""", unsafe_allow_html=True)
|
| 39 |
+
|
| 40 |
+
st.markdown("---")
|
| 41 |
+
|
| 42 |
+
experiment_id = f"week_{week}_{experiment['title']}"
|
| 43 |
+
is_completed = experiment_id in st.session_state.completed_experiments
|
| 44 |
+
|
| 45 |
+
if is_completed:
|
| 46 |
+
show_success_message("✅ لقد أكملت هذه التجربة!")
|
| 47 |
+
|
| 48 |
+
# عرض التسليم السابق
|
| 49 |
+
if experiment_id in st.session_state.experiment_submissions:
|
| 50 |
+
submission = st.session_state.experiment_submissions[experiment_id]
|
| 51 |
+
|
| 52 |
+
st.markdown("### 📄 تسليمك السابق:")
|
| 53 |
+
st.markdown(f"""
|
| 54 |
+
<div class='card'>
|
| 55 |
+
<p><strong>تاريخ التسليم:</strong> {submission.get('date', 'غير محدد')}</p>
|
| 56 |
+
<p><strong>الملاحظات:</strong></p>
|
| 57 |
+
<p>{submission.get('notes', 'لا توجد ملاحظات')}</p>
|
| 58 |
+
</div>
|
| 59 |
+
""", unsafe_allow_html=True)
|
| 60 |
+
|
| 61 |
+
if submission.get('files'):
|
| 62 |
+
st.markdown("**📎 الملفات المرفقة:**")
|
| 63 |
+
for file_name in submission['files']:
|
| 64 |
+
st.markdown(f"- 📄 {file_name}")
|
| 65 |
+
|
| 66 |
+
else:
|
| 67 |
+
# نموذج التسليم
|
| 68 |
+
st.markdown("### 📤 تسليم التجربة")
|
| 69 |
+
|
| 70 |
+
st.markdown("""
|
| 71 |
+
<div class='info-box'>
|
| 72 |
+
<strong>💡 نصائح للتسليم الناجح:</strong>
|
| 73 |
+
<ul style='margin: 0.5rem 0 0 0;'>
|
| 74 |
+
<li>وثّق خطوات عملك بوضوح</li>
|
| 75 |
+
<li>اشرح التحديات التي واجهتك وكيف حللتها</li>
|
| 76 |
+
<li>ضع تأملاتك وما تعلمته</li>
|
| 77 |
+
<li>أرفق ملفات أو روابط ذات صلة</li>
|
| 78 |
+
</ul>
|
| 79 |
+
</div>
|
| 80 |
+
""", unsafe_allow_html=True)
|
| 81 |
+
|
| 82 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 83 |
+
|
| 84 |
+
# حقول التسليم
|
| 85 |
+
notes = st.text_area(
|
| 86 |
+
"📝 وصف عملك والملاحظات:",
|
| 87 |
+
placeholder="اشرح ما قمت به، الخطوات، التحديات، وما تعلمته...",
|
| 88 |
+
height=200
|
| 89 |
+
)
|
| 90 |
+
|
| 91 |
+
st.markdown("---")
|
| 92 |
+
|
| 93 |
+
# رفع ملفات
|
| 94 |
+
st.markdown("### 📎 رفع الملفات (اختياري)")
|
| 95 |
+
uploaded_files = st.file_uploader(
|
| 96 |
+
"ارفع ملفات المشروع (صور، مستندات، كود...)",
|
| 97 |
+
accept_multiple_files=True,
|
| 98 |
+
help="يمكنك رفع عدة ملفات"
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# أو إضافة روابط
|
| 102 |
+
st.markdown("### 🔗 أو أضف روابط")
|
| 103 |
+
links = st.text_area(
|
| 104 |
+
"روابط (GitHub, Google Drive, إلخ)",
|
| 105 |
+
placeholder="https://github.com/username/project\nhttps://drive.google.com/...",
|
| 106 |
+
height=100
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 110 |
+
|
| 111 |
+
# زر التسليم
|
| 112 |
+
col1, col2 = st.columns([2, 1])
|
| 113 |
+
with col1:
|
| 114 |
+
if st.button("✅ تسليم التجربة", use_container_width=True, type="primary"):
|
| 115 |
+
if notes.strip():
|
| 116 |
+
# حفظ التسليم
|
| 117 |
+
submission = {
|
| 118 |
+
"date": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
| 119 |
+
"notes": notes,
|
| 120 |
+
"files": [f.name for f in uploaded_files] if uploaded_files else [],
|
| 121 |
+
"links": links.strip().split("\n") if links.strip() else []
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
st.session_state.experiment_submissions[experiment_id] = submission
|
| 125 |
+
st.session_state.completed_experiments.append(experiment_id)
|
| 126 |
+
|
| 127 |
+
show_success_message("🎉 تم تسليم التجربة بنجاح!")
|
| 128 |
+
st.balloons()
|
| 129 |
+
st.rerun()
|
| 130 |
+
else:
|
| 131 |
+
show_warning_message("⚠️ يجب كتابة وصف للعمل على الأقل")
|
| 132 |
+
|
| 133 |
+
with col2:
|
| 134 |
+
if st.button("💾 حفظ مسودة"):
|
| 135 |
+
st.info("تم حفظ عملك كمسودة")
|
| 136 |
+
|
| 137 |
+
# زر العودة
|
| 138 |
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
| 139 |
+
if st.button("⬅️ العودة للدروس"):
|
| 140 |
+
st.session_state.page = "lesson"
|
| 141 |
+
st.rerun()
|
| 142 |
+
|
| 143 |
+
else:
|
| 144 |
+
# عرض جميع التجارب
|
| 145 |
+
st.markdown("### 🔬 جميع التجارب العملية")
|
| 146 |
+
|
| 147 |
+
if "curriculum" not in st.session_state or not st.session_state.curriculum:
|
| 148 |
+
st.warning("⚠️ لم يتم إنشاء خطة تعليمية بعد!")
|
| 149 |
+
if st.button("📅 إنشاء خطة"):
|
| 150 |
+
st.session_state.page = "plan"
|
| 151 |
+
st.rerun()
|
| 152 |
+
return
|
| 153 |
+
|
| 154 |
+
curriculum = st.session_state.curriculum
|
| 155 |
+
experiments = [(unit['week'], unit.get('experiment')) for unit in curriculum if unit.get('experiment')]
|
| 156 |
+
|
| 157 |
+
# حساب التقدم
|
| 158 |
+
total_experiments = len(experiments)
|
| 159 |
+
completed_count = len(st.session_state.completed_experiments)
|
| 160 |
+
progress_percentage = (completed_count / total_experiments * 100) if total_experiments > 0 else 0
|
| 161 |
+
|
| 162 |
+
col1, col2, col3 = st.columns(3)
|
| 163 |
+
with col1:
|
| 164 |
+
st.metric("إجمالي التجارب", total_experiments)
|
| 165 |
+
with col2:
|
| 166 |
+
st.metric("مكتملة", completed_count)
|
| 167 |
+
with col3:
|
| 168 |
+
st.metric("النسبة", f"{progress_percentage:.1f}%")
|
| 169 |
+
|
| 170 |
+
st.markdown("---")
|
| 171 |
+
|
| 172 |
+
# عرض التجارب
|
| 173 |
+
for week, experiment in experiments:
|
| 174 |
+
experiment_id = f"week_{week}_{experiment['title']}"
|
| 175 |
+
is_completed = experiment_id in st.session_state.completed_experiments
|
| 176 |
+
|
| 177 |
+
status_icon = "✅" if is_completed else "⏳"
|
| 178 |
+
status_color = "#48bb78" if is_completed else "#ed8936"
|
| 179 |
+
|
| 180 |
+
with st.expander(f"{status_icon} الأسبوع {week}: {experiment['title']}"):
|
| 181 |
+
st.markdown(f"""
|
| 182 |
+
<div class='card' style='border-right: 4px solid {status_color};'>
|
| 183 |
+
<p><strong>الوصف:</strong> {experiment['description']}</p>
|
| 184 |
+
<p><strong>📦 المخرج:</strong> {experiment['deliverable']}</p>
|
| 185 |
+
</div>
|
| 186 |
+
""", unsafe_allow_html=True)
|
| 187 |
+
|
| 188 |
+
if is_completed:
|
| 189 |
+
st.success("مكتمل ✅")
|
| 190 |
+
|
| 191 |
+
# عرض التسليم
|
| 192 |
+
if experiment_id in st.session_state.experiment_submissions:
|
| 193 |
+
submission = st.session_state.experiment_submissions[experiment_id]
|
| 194 |
+
with st.expander("📄 عرض التسليم"):
|
| 195 |
+
st.markdown(f"**تاريخ التسليم:** {submission.get('date', 'غير محدد')}")
|
| 196 |
+
st.markdown("**الملاحظات:**")
|
| 197 |
+
st.markdown(submission.get('notes', 'لا توجد'))
|
| 198 |
+
|
| 199 |
+
if submission.get('files'):
|
| 200 |
+
st.markdown("**الملفات:**")
|
| 201 |
+
for file_name in submission['files']:
|
| 202 |
+
st.markdown(f"- {file_name}")
|
| 203 |
+
|
| 204 |
+
if submission.get('links'):
|
| 205 |
+
st.markdown("**الروابط:**")
|
| 206 |
+
for link in submission['links']:
|
| 207 |
+
if link.strip():
|
| 208 |
+
st.markdown(f"- [{link}]({link})")
|
| 209 |
+
else:
|
| 210 |
+
if st.button(f"🚀 ابدأ التجربة", key=f"start_exp_{week}", use_container_width=True):
|
| 211 |
+
st.session_state.current_experiment = experiment
|
| 212 |
+
st.session_state.current_week = week
|
| 213 |
+
st.rerun()
|
| 214 |
+
|
| 215 |
+
# زر العودة
|
| 216 |
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
| 217 |
+
if st.button("⬅️ العودة للدروس"):
|
| 218 |
+
st.session_state.page = "lesson"
|
| 219 |
+
st.rerun()
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
if __name__ == "__main__":
|
| 223 |
+
show()
|
pages/home.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
الصفحة الرئيسية - شرح التطبيق وزر البدء
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import sys
|
| 6 |
+
sys.path.append('..')
|
| 7 |
+
import config
|
| 8 |
+
from utils.ui_utils import show_hero_section, show_feature_cards, load_custom_css
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def show():
|
| 12 |
+
"""عرض صفحة البداية"""
|
| 13 |
+
|
| 14 |
+
# تحميل CSS مخصص
|
| 15 |
+
load_custom_css()
|
| 16 |
+
|
| 17 |
+
# قسم Hero
|
| 18 |
+
show_hero_section(
|
| 19 |
+
config.APP_NAME,
|
| 20 |
+
config.APP_DESCRIPTION
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# وصف مختصر
|
| 24 |
+
st.markdown("""
|
| 25 |
+
<div style='text-align: center; max-width: 800px; margin: 2rem auto;'>
|
| 26 |
+
<p style='font-size: 1.1rem; color: #4a5568; line-height: 1.8;'>
|
| 27 |
+
مرحباً بك في منصة تعليمية ذكية تُصمم لك خطة تعلم مخصصة بناءً على مهاراتك،
|
| 28 |
+
وقتك المتاح، وأهدافك. ستحصل على منهج شامل لمدة 12 أسبوعاً مع دروس تفاعلية،
|
| 29 |
+
تجارب عملية، ومشاريع جماعية لتعزيز تعلمك.
|
| 30 |
+
</p>
|
| 31 |
+
</div>
|
| 32 |
+
""", unsafe_allow_html=True)
|
| 33 |
+
|
| 34 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 35 |
+
|
| 36 |
+
# بطاقات الميزات
|
| 37 |
+
features = [
|
| 38 |
+
{
|
| 39 |
+
"icon": "🎯",
|
| 40 |
+
"title": "خطة مخصصة",
|
| 41 |
+
"description": "منهج يتكيف مع مستواك ووقتك المتاح"
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"icon": "📚",
|
| 45 |
+
"title": "دروس تفاعلية",
|
| 46 |
+
"description": "محتوى تعليمي غني بالأمثلة والتطبيقات"
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"icon": "🔬",
|
| 50 |
+
"title": "تجارب عملية",
|
| 51 |
+
"description": "مشاريع تطبيقية لتعزيز الفهم"
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"icon": "📊",
|
| 55 |
+
"title": "متابعة التقدم",
|
| 56 |
+
"description": "تتبع إنجازاتك ومحفظة أعمالك"
|
| 57 |
+
}
|
| 58 |
+
]
|
| 59 |
+
|
| 60 |
+
show_feature_cards(features)
|
| 61 |
+
|
| 62 |
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
| 63 |
+
|
| 64 |
+
# كيف يعمل؟
|
| 65 |
+
st.markdown("""
|
| 66 |
+
<div style='max-width: 900px; margin: 3rem auto;'>
|
| 67 |
+
<h2 style='text-align: center; color: #667eea; margin-bottom: 2rem;'>
|
| 68 |
+
🚀 كيف تبدأ رحلتك؟
|
| 69 |
+
</h2>
|
| 70 |
+
</div>
|
| 71 |
+
""", unsafe_allow_html=True)
|
| 72 |
+
|
| 73 |
+
# خطوات البدء
|
| 74 |
+
steps = [
|
| 75 |
+
{
|
| 76 |
+
"number": "1",
|
| 77 |
+
"title": "أنشئ ملفك التعريفي",
|
| 78 |
+
"description": "أخبرنا عن مهاراتك، مستواك، ووقتك المتاح",
|
| 79 |
+
"icon": "👤"
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"number": "2",
|
| 83 |
+
"title": "اختر محورك التعليمي",
|
| 84 |
+
"description": "حدد المجال الذي تريد التعلم فيه (AI، البيانات، التصميم...)",
|
| 85 |
+
"icon": "🎓"
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"number": "3",
|
| 89 |
+
"title": "احصل على خطتك المخصصة",
|
| 90 |
+
"description": "سنولّد لك منهجاً كاملاً لـ 12 أسبوعاً",
|
| 91 |
+
"icon": "📅"
|
| 92 |
+
},
|
| 93 |
+
{
|
| 94 |
+
"number": "4",
|
| 95 |
+
"title": "ابدأ التعلم والممارسة",
|
| 96 |
+
"description": "دروس يومية، تجارب عملية، ومشاريع مثيرة",
|
| 97 |
+
"icon": "🚀"
|
| 98 |
+
}
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
for step in steps:
|
| 102 |
+
st.markdown(f"""
|
| 103 |
+
<div class='card' style='display: flex; align-items: center; gap: 1.5rem; margin: 1rem auto; max-width: 800px;'>
|
| 104 |
+
<div style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 105 |
+
color: white; width: 60px; height: 60px; border-radius: 50%;
|
| 106 |
+
display: flex; align-items: center; justify-content: center;
|
| 107 |
+
font-size: 1.5rem; font-weight: bold; flex-shrink: 0;'>
|
| 108 |
+
{step['number']}
|
| 109 |
+
</div>
|
| 110 |
+
<div style='flex: 1;'>
|
| 111 |
+
<h3 style='margin: 0 0 0.5rem 0; color: #2d3748;'>
|
| 112 |
+
{step['icon']} {step['title']}
|
| 113 |
+
</h3>
|
| 114 |
+
<p style='margin: 0; color: #718096;'>{step['description']}</p>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
""", unsafe_allow_html=True)
|
| 118 |
+
|
| 119 |
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
| 120 |
+
|
| 121 |
+
# زر البدء
|
| 122 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
| 123 |
+
with col2:
|
| 124 |
+
if st.button("🎯 ابدأ رحلتك التعليمية الآن", use_container_width=True, type="primary"):
|
| 125 |
+
st.session_state.page = "profile"
|
| 126 |
+
st.rerun()
|
| 127 |
+
|
| 128 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 129 |
+
|
| 130 |
+
# معلومات إضافية
|
| 131 |
+
with st.expander("❓ أسئلة شائعة"):
|
| 132 |
+
st.markdown("""
|
| 133 |
+
### هل التطبيق مجاني؟
|
| 134 |
+
نعم! هذه نسخة تجريبية مجانية بالكامل.
|
| 135 |
+
|
| 136 |
+
### كم من الوقت أحتاج أسبوعياً؟
|
| 137 |
+
يمكنك تحديد الوقت المتاح لديك (من ساعة إلى 40 ساعة أسبوعياً) وسيتكيف المنهج معك.
|
| 138 |
+
|
| 139 |
+
### هل أحتاج خبرة سابقة؟
|
| 140 |
+
لا! نوفر محاور للمبتدئين وللمتقدمين على حد سواء.
|
| 141 |
+
|
| 142 |
+
### هل يمكنني تغيير الخطة لاحقاً؟
|
| 143 |
+
بالتأكيد! يمكنك تعديل خطتك في أي وقت.
|
| 144 |
+
|
| 145 |
+
### هل هناك شهادات؟
|
| 146 |
+
حالياً لا نقدم شهادات رسمية، لكننا نوفر محفظة أعمال توثّق إنجازاتك.
|
| 147 |
+
""")
|
| 148 |
+
|
| 149 |
+
# Footer
|
| 150 |
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
| 151 |
+
st.markdown("""
|
| 152 |
+
<div style='text-align: center; color: #a0aec0; padding: 2rem; border-top: 1px solid #e2e8f0; margin-top: 3rem;'>
|
| 153 |
+
<p>🎓 مدرستك المحورية | مدعوم بـ Hugging Face 🤗</p>
|
| 154 |
+
<p style='font-size: 0.9rem;'>بُني بـ ❤️ لتمكين المتعلمين في كل مكان</p>
|
| 155 |
+
</div>
|
| 156 |
+
""", unsafe_allow_html=True)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
if __name__ == "__main__":
|
| 160 |
+
show()
|
pages/lesson.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
صفحة الدرس اليومي - عرض الدروس والشرح
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import sys
|
| 6 |
+
sys.path.append('..')
|
| 7 |
+
from utils.ui_utils import load_custom_css, show_success_message, show_lesson_card, show_progress_bar
|
| 8 |
+
from utils.api_utils import generate_lesson_content
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def show():
|
| 12 |
+
"""عرض صفحة الدروس"""
|
| 13 |
+
|
| 14 |
+
load_custom_css()
|
| 15 |
+
|
| 16 |
+
st.markdown("## 📚 الدروس التعليمية")
|
| 17 |
+
st.markdown("---")
|
| 18 |
+
|
| 19 |
+
# التحقق من وجود منهج
|
| 20 |
+
if "curriculum" not in st.session_state or not st.session_state.curriculum:
|
| 21 |
+
st.warning("⚠️ لم يتم إنشاء خطة تعليمية بعد!")
|
| 22 |
+
if st.button("📅 إنشاء خطة"):
|
| 23 |
+
st.session_state.page = "plan"
|
| 24 |
+
st.rerun()
|
| 25 |
+
return
|
| 26 |
+
|
| 27 |
+
curriculum = st.session_state.curriculum
|
| 28 |
+
|
| 29 |
+
# تهيئة الدروس المكتملة
|
| 30 |
+
if "completed_lessons" not in st.session_state:
|
| 31 |
+
st.session_state.completed_lessons = []
|
| 32 |
+
|
| 33 |
+
# حساب التقدم
|
| 34 |
+
total_lessons = sum(len(unit.get("lessons", [])) for unit in curriculum)
|
| 35 |
+
completed_count = len(st.session_state.completed_lessons)
|
| 36 |
+
progress_percentage = (completed_count / total_lessons * 100) if total_lessons > 0 else 0
|
| 37 |
+
|
| 38 |
+
# عرض التقدم
|
| 39 |
+
st.markdown("### 📊 تقدمك في الدروس")
|
| 40 |
+
col1, col2, col3 = st.columns(3)
|
| 41 |
+
with col1:
|
| 42 |
+
st.metric("الدروس المكتملة", f"{completed_count}/{total_lessons}")
|
| 43 |
+
with col2:
|
| 44 |
+
st.metric("نسبة الإنجاز", f"{progress_percentage:.1f}%")
|
| 45 |
+
with col3:
|
| 46 |
+
remaining = total_lessons - completed_count
|
| 47 |
+
st.metric("الدروس المتبقية", remaining)
|
| 48 |
+
|
| 49 |
+
show_progress_bar(progress_percentage, "")
|
| 50 |
+
|
| 51 |
+
st.markdown("---")
|
| 52 |
+
|
| 53 |
+
# اختيار الأسبوع
|
| 54 |
+
st.markdown("### 📅 اختر الأسبوع")
|
| 55 |
+
|
| 56 |
+
week_options = [f"الأسبوع {unit['week']}: {unit['unit_title']}" for unit in curriculum]
|
| 57 |
+
|
| 58 |
+
if "selected_week_idx" not in st.session_state:
|
| 59 |
+
st.session_state.selected_week_idx = 0
|
| 60 |
+
|
| 61 |
+
selected_week_display = st.selectbox(
|
| 62 |
+
"اختر أسبوعاً:",
|
| 63 |
+
week_options,
|
| 64 |
+
index=st.session_state.selected_week_idx
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
selected_week_idx = week_options.index(selected_week_display)
|
| 68 |
+
st.session_state.selected_week_idx = selected_week_idx
|
| 69 |
+
|
| 70 |
+
current_unit = curriculum[selected_week_idx]
|
| 71 |
+
|
| 72 |
+
st.markdown("---")
|
| 73 |
+
|
| 74 |
+
# عرض معلومات الوحدة
|
| 75 |
+
st.markdown(f"### 📖 {current_unit['unit_title']}")
|
| 76 |
+
|
| 77 |
+
col1, col2 = st.columns(2)
|
| 78 |
+
with col1:
|
| 79 |
+
st.markdown(f"""
|
| 80 |
+
<div class='card'>
|
| 81 |
+
<h4>⏱️ معلومات الوحدة</h4>
|
| 82 |
+
<p><strong>الأسبوع:</strong> {current_unit['week']}</p>
|
| 83 |
+
<p><strong>الوقت المقدر:</strong> {current_unit['estimated_time_mins']} دقيقة (~{round(current_unit['estimated_time_mins']/60, 1)} ساعة)</p>
|
| 84 |
+
<p><strong>عدد الدروس:</strong> {len(current_unit.get('lessons', []))}</p>
|
| 85 |
+
</div>
|
| 86 |
+
""", unsafe_allow_html=True)
|
| 87 |
+
|
| 88 |
+
with col2:
|
| 89 |
+
unit_lessons = current_unit.get("lessons", [])
|
| 90 |
+
unit_completed = sum(1 for lesson in unit_lessons if f"{current_unit['week']}_{lesson['title']}" in st.session_state.completed_lessons)
|
| 91 |
+
unit_progress = (unit_completed / len(unit_lessons) * 100) if len(unit_lessons) > 0 else 0
|
| 92 |
+
|
| 93 |
+
st.markdown(f"""
|
| 94 |
+
<div class='card'>
|
| 95 |
+
<h4>📊 تقدم هذه الوحدة</h4>
|
| 96 |
+
<p><strong>مكتمل:</strong> {unit_completed}/{len(unit_lessons)} دروس</p>
|
| 97 |
+
</div>
|
| 98 |
+
""", unsafe_allow_html=True)
|
| 99 |
+
show_progress_bar(unit_progress, "")
|
| 100 |
+
|
| 101 |
+
st.markdown("---")
|
| 102 |
+
|
| 103 |
+
# عرض الدروس
|
| 104 |
+
st.markdown("### 📚 دروس هذا الأسبوع")
|
| 105 |
+
|
| 106 |
+
lessons = current_unit.get("lessons", [])
|
| 107 |
+
|
| 108 |
+
if not lessons:
|
| 109 |
+
st.info("لا توجد دروس في هذه الوحدة")
|
| 110 |
+
else:
|
| 111 |
+
for idx, lesson in enumerate(lessons):
|
| 112 |
+
lesson_id = f"{current_unit['week']}_{lesson['title']}"
|
| 113 |
+
is_completed = lesson_id in st.session_state.completed_lessons
|
| 114 |
+
|
| 115 |
+
with st.expander(f"{'✅' if is_completed else '📖'} درس {idx+1}: {lesson['title']}", expanded=(idx==0 and not is_completed)):
|
| 116 |
+
|
| 117 |
+
# معلومات الدرس
|
| 118 |
+
col1, col2 = st.columns([3, 1])
|
| 119 |
+
with col1:
|
| 120 |
+
st.markdown(f"**الوصف:** {lesson.get('description', 'لا يوجد وصف')}")
|
| 121 |
+
with col2:
|
| 122 |
+
st.markdown(f"**⏱️ المدة:** {lesson.get('duration_mins', 0)} دقيقة")
|
| 123 |
+
st.markdown(f"**📌 النوع:** {lesson.get('type', 'درس')}")
|
| 124 |
+
|
| 125 |
+
st.markdown("---")
|
| 126 |
+
|
| 127 |
+
# محتوى الدرس
|
| 128 |
+
if f"lesson_content_{lesson_id}" not in st.session_state:
|
| 129 |
+
if st.button(f"📖 عرض محتوى الدرس", key=f"load_{lesson_id}"):
|
| 130 |
+
with st.spinner("🔄 جاري تحميل المحتوى..."):
|
| 131 |
+
content = generate_lesson_content(
|
| 132 |
+
lesson['title'],
|
| 133 |
+
st.session_state.get("profile", {})
|
| 134 |
+
)
|
| 135 |
+
st.session_state[f"lesson_content_{lesson_id}"] = content
|
| 136 |
+
st.rerun()
|
| 137 |
+
else:
|
| 138 |
+
# عرض المحتوى
|
| 139 |
+
content = st.session_state[f"lesson_content_{lesson_id}"]
|
| 140 |
+
|
| 141 |
+
st.markdown(f"""
|
| 142 |
+
<div class='card' style='background: #f7fafc;'>
|
| 143 |
+
{content}
|
| 144 |
+
</div>
|
| 145 |
+
""", unsafe_allow_html=True)
|
| 146 |
+
|
| 147 |
+
st.markdown("---")
|
| 148 |
+
|
| 149 |
+
# أزرار التفاعل
|
| 150 |
+
col1, col2, col3 = st.columns(3)
|
| 151 |
+
|
| 152 |
+
with col1:
|
| 153 |
+
if not is_completed:
|
| 154 |
+
if st.button("✅ وضع علامة مكتمل", key=f"complete_{lesson_id}", use_container_width=True):
|
| 155 |
+
st.session_state.completed_lessons.append(lesson_id)
|
| 156 |
+
show_success_message("تم تسجيل إتمام الدرس! 🎉")
|
| 157 |
+
st.balloons()
|
| 158 |
+
st.rerun()
|
| 159 |
+
else:
|
| 160 |
+
st.success("مكتمل ✅")
|
| 161 |
+
|
| 162 |
+
with col2:
|
| 163 |
+
if st.button("🔄 إعادة الشرح", key=f"reload_{lesson_id}", use_container_width=True):
|
| 164 |
+
del st.session_state[f"lesson_content_{lesson_id}"]
|
| 165 |
+
st.info("قم بالضغط على 'عرض محتوى الدرس' مرة أخرى")
|
| 166 |
+
|
| 167 |
+
with col3:
|
| 168 |
+
if st.button("📝 ملاحظات", key=f"notes_{lesson_id}", use_container_width=True):
|
| 169 |
+
st.session_state[f"show_notes_{lesson_id}"] = True
|
| 170 |
+
|
| 171 |
+
# منطقة الملاحظات
|
| 172 |
+
if st.session_state.get(f"show_notes_{lesson_id}", False):
|
| 173 |
+
st.markdown("**✍️ ملاحظاتك الشخصية:**")
|
| 174 |
+
note_key = f"note_{lesson_id}"
|
| 175 |
+
if note_key not in st.session_state:
|
| 176 |
+
st.session_state[note_key] = ""
|
| 177 |
+
|
| 178 |
+
note = st.text_area(
|
| 179 |
+
"اكتب ملاحظاتك هنا:",
|
| 180 |
+
value=st.session_state[note_key],
|
| 181 |
+
key=f"note_input_{lesson_id}",
|
| 182 |
+
height=100
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
if st.button("💾 حفظ الملاحظات", key=f"save_note_{lesson_id}"):
|
| 186 |
+
st.session_state[note_key] = note
|
| 187 |
+
show_success_message("تم حفظ ملاحظاتك!")
|
| 188 |
+
|
| 189 |
+
# التجربة العملية
|
| 190 |
+
if current_unit.get("experiment"):
|
| 191 |
+
st.markdown("---")
|
| 192 |
+
st.markdown("### 🔬 التجربة العملية للأسبوع")
|
| 193 |
+
|
| 194 |
+
experiment = current_unit["experiment"]
|
| 195 |
+
|
| 196 |
+
st.markdown(f"""
|
| 197 |
+
<div class='card' style='background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;'>
|
| 198 |
+
<h3 style='color: white; margin-top: 0;'>🔬 {experiment['title']}</h3>
|
| 199 |
+
<p>{experiment['description']}</p>
|
| 200 |
+
<p><strong>📦 المخرج المطلوب:</strong> {experiment['deliverable']}</p>
|
| 201 |
+
</div>
|
| 202 |
+
""", unsafe_allow_html=True)
|
| 203 |
+
|
| 204 |
+
if st.button("🚀 ابدأ التجربة", use_container_width=True, type="primary"):
|
| 205 |
+
st.session_state.current_experiment = experiment
|
| 206 |
+
st.session_state.current_week = current_unit['week']
|
| 207 |
+
st.session_state.page = "experiment"
|
| 208 |
+
st.rerun()
|
| 209 |
+
|
| 210 |
+
# أزرار التنقل
|
| 211 |
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
| 212 |
+
st.markdown("---")
|
| 213 |
+
|
| 214 |
+
col1, col2, col3 = st.columns(3)
|
| 215 |
+
|
| 216 |
+
with col1:
|
| 217 |
+
if st.button("⬅️ العودة للخطة"):
|
| 218 |
+
st.session_state.page = "plan"
|
| 219 |
+
st.rerun()
|
| 220 |
+
|
| 221 |
+
with col2:
|
| 222 |
+
if st.button("📊 التقدم والإنجازات", use_container_width=True):
|
| 223 |
+
st.session_state.page = "progress"
|
| 224 |
+
st.rerun()
|
| 225 |
+
|
| 226 |
+
with col3:
|
| 227 |
+
if st.button("👥 المشاريع الجماعية", use_container_width=True):
|
| 228 |
+
st.session_state.page = "projects"
|
| 229 |
+
st.rerun()
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
if __name__ == "__main__":
|
| 233 |
+
show()
|
pages/plan.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
صفحة اختيار/توليد الخطة التعليمية
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import sys
|
| 6 |
+
sys.path.append('..')
|
| 7 |
+
import config
|
| 8 |
+
from utils.ui_utils import load_custom_css, show_hub_selector, show_success_message, show_warning_message, show_progress_bar
|
| 9 |
+
from utils.api_utils import generate_curriculum, generate_default_curriculum
|
| 10 |
+
from utils.data_utils import generate_schedule
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def show():
|
| 14 |
+
"""عرض صفحة اختيار الخطة"""
|
| 15 |
+
|
| 16 |
+
load_custom_css()
|
| 17 |
+
|
| 18 |
+
st.markdown("## 📅 خطة التعلم المخصصة")
|
| 19 |
+
st.markdown("---")
|
| 20 |
+
|
| 21 |
+
# التحقق من وجود ملف تعريفي
|
| 22 |
+
if "profile" not in st.session_state or not st.session_state.profile:
|
| 23 |
+
show_warning_message("⚠️ يجب إنشاء ملف تعريفي أولاً!")
|
| 24 |
+
if st.button("👤 إنشاء ملف تعريفي"):
|
| 25 |
+
st.session_state.page = "profile"
|
| 26 |
+
st.rerun()
|
| 27 |
+
return
|
| 28 |
+
|
| 29 |
+
profile = st.session_state.profile
|
| 30 |
+
|
| 31 |
+
# عرض ملخص الملف التعريفي
|
| 32 |
+
st.markdown("### 👤 ملفك التعريفي")
|
| 33 |
+
col1, col2, col3 = st.columns(3)
|
| 34 |
+
with col1:
|
| 35 |
+
st.metric("الاسم", profile.get("name", "غير محدد"))
|
| 36 |
+
with col2:
|
| 37 |
+
st.metric("المستوى", profile.get("level", "مبتدئ"))
|
| 38 |
+
with col3:
|
| 39 |
+
st.metric("الوقت المتاح", f"{profile.get('available_hours', 10)} ساعة/أسبوع")
|
| 40 |
+
|
| 41 |
+
st.markdown("---")
|
| 42 |
+
|
| 43 |
+
# اختيار المحور
|
| 44 |
+
if "selected_hub" not in st.session_state:
|
| 45 |
+
st.session_state.selected_hub = None
|
| 46 |
+
|
| 47 |
+
if not st.session_state.selected_hub:
|
| 48 |
+
st.markdown("### 🎯 الخطوة 1: اختر محورك التعليمي")
|
| 49 |
+
st.markdown("اختر المجال الذي تريد التخصص فيه على مدار 3 أشهر:")
|
| 50 |
+
|
| 51 |
+
# عرض المحاور
|
| 52 |
+
cols = st.columns(2)
|
| 53 |
+
hub_items = list(config.LEARNING_HUBS.items())
|
| 54 |
+
|
| 55 |
+
for idx, (hub_id, hub_data) in enumerate(hub_items):
|
| 56 |
+
col = cols[idx % 2]
|
| 57 |
+
with col:
|
| 58 |
+
st.markdown(f"""
|
| 59 |
+
<div class='card' style='min-height: 220px;'>
|
| 60 |
+
<div style='text-align: center; font-size: 3rem; margin-bottom: 1rem;'>
|
| 61 |
+
{hub_data['icon']}
|
| 62 |
+
</div>
|
| 63 |
+
<h3 style='text-align: center; margin: 0.5rem 0;'>{hub_data['name']}</h3>
|
| 64 |
+
<p style='text-align: center; color: #718096;'>{hub_data['description']}</p>
|
| 65 |
+
<div style='text-align: center; margin-top: 1rem;'>
|
| 66 |
+
<span class='badge badge-info'>⏱️ {hub_data['duration_weeks']} أسبوع</span>
|
| 67 |
+
<span class='badge badge-primary'>📊 ~{hub_data['estimated_hours']} ساعة</span>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
""", unsafe_allow_html=True)
|
| 71 |
+
|
| 72 |
+
if st.button(f"اختيار {hub_data['name']}", key=f"select_{hub_id}", use_container_width=True):
|
| 73 |
+
st.session_state.selected_hub = hub_id
|
| 74 |
+
st.rerun()
|
| 75 |
+
|
| 76 |
+
else:
|
| 77 |
+
# عرض المحور المختار
|
| 78 |
+
hub_data = config.LEARNING_HUBS[st.session_state.selected_hub]
|
| 79 |
+
|
| 80 |
+
st.markdown(f"""
|
| 81 |
+
<div class='success-box' style='text-align: center;'>
|
| 82 |
+
<h3 style='margin: 0;'>{hub_data['icon']} المحور المختار: {hub_data['name']}</h3>
|
| 83 |
+
<p style='margin: 0.5rem 0 0 0;'>{hub_data['description']}</p>
|
| 84 |
+
</div>
|
| 85 |
+
""", unsafe_allow_html=True)
|
| 86 |
+
|
| 87 |
+
if st.button("🔄 تغيير المحور"):
|
| 88 |
+
st.session_state.selected_hub = None
|
| 89 |
+
st.rerun()
|
| 90 |
+
|
| 91 |
+
st.markdown("---")
|
| 92 |
+
|
| 93 |
+
# الخطوة 2: توليد الخطة
|
| 94 |
+
st.markdown("### 📋 الخطوة 2: توليد خطتك المخصصة")
|
| 95 |
+
|
| 96 |
+
if "curriculum" not in st.session_state:
|
| 97 |
+
st.session_state.curriculum = None
|
| 98 |
+
|
| 99 |
+
if not st.session_state.curriculum:
|
| 100 |
+
st.markdown("""
|
| 101 |
+
<div class='info-box'>
|
| 102 |
+
<p style='margin: 0;'>
|
| 103 |
+
سنقوم الآن بتوليد منهج مخصص لك بناءً على:
|
| 104 |
+
</p>
|
| 105 |
+
<ul style='margin: 0.5rem 0 0 0;'>
|
| 106 |
+
<li>مستواك الحالي: <strong>{}</strong></li>
|
| 107 |
+
<li>الوقت المتاح: <strong>{} ساعة/أسبوع</strong></li>
|
| 108 |
+
<li>أسلوب التعلم: <strong>{}</strong></li>
|
| 109 |
+
<li>المحور: <strong>{}</strong></li>
|
| 110 |
+
</ul>
|
| 111 |
+
</div>
|
| 112 |
+
""".format(
|
| 113 |
+
profile.get("level", "مبتدئ"),
|
| 114 |
+
profile.get("available_hours", 10),
|
| 115 |
+
profile.get("learning_style", "مختلط"),
|
| 116 |
+
hub_data["name"]
|
| 117 |
+
), unsafe_allow_html=True)
|
| 118 |
+
|
| 119 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 120 |
+
|
| 121 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
| 122 |
+
with col2:
|
| 123 |
+
if st.button("✨ توليد الخطة المخصصة", use_container_width=True, type="primary"):
|
| 124 |
+
with st.spinner("🔄 جاري توليد خطتك المخصصة... قد يستغرق هذا دقيقة..."):
|
| 125 |
+
# محاولة توليد المنهج عبر API
|
| 126 |
+
curriculum_result = generate_curriculum(profile, st.session_state.selected_hub)
|
| 127 |
+
|
| 128 |
+
if curriculum_result and "curriculum" in curriculum_result:
|
| 129 |
+
st.session_state.curriculum = curriculum_result["curriculum"]
|
| 130 |
+
else:
|
| 131 |
+
# استخدام منهج افتراضي
|
| 132 |
+
default_result = generate_default_curriculum(st.session_state.selected_hub)
|
| 133 |
+
st.session_state.curriculum = default_result["curriculum"]
|
| 134 |
+
|
| 135 |
+
# توليد الجدول الزمني
|
| 136 |
+
st.session_state.schedule = generate_schedule(
|
| 137 |
+
st.session_state.curriculum,
|
| 138 |
+
profile.get("available_hours", 10)
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
st.session_state.completed_units = []
|
| 142 |
+
st.session_state.completed_lessons = []
|
| 143 |
+
|
| 144 |
+
st.rerun()
|
| 145 |
+
|
| 146 |
+
else:
|
| 147 |
+
# عرض الخطة المُولدة
|
| 148 |
+
show_success_message("✅ تم توليد خطتك المخصصة بنجاح!")
|
| 149 |
+
|
| 150 |
+
st.markdown("### 📊 نظرة عامة على خطتك")
|
| 151 |
+
|
| 152 |
+
curriculum = st.session_state.curriculum
|
| 153 |
+
|
| 154 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 155 |
+
with col1:
|
| 156 |
+
st.metric("عدد الأسابيع", f"{len(curriculum)} أسبوع")
|
| 157 |
+
with col2:
|
| 158 |
+
total_lessons = sum(len(unit.get("lessons", [])) for unit in curriculum)
|
| 159 |
+
st.metric("إجمالي الدروس", total_lessons)
|
| 160 |
+
with col3:
|
| 161 |
+
total_hours = sum(unit.get("estimated_time_mins", 0) for unit in curriculum) / 60
|
| 162 |
+
st.metric("إجمالي الساعات", f"~{int(total_hours)} ساعة")
|
| 163 |
+
with col4:
|
| 164 |
+
st.metric("التجارب العملية", len(curriculum))
|
| 165 |
+
|
| 166 |
+
st.markdown("---")
|
| 167 |
+
|
| 168 |
+
# عرض الوحدات
|
| 169 |
+
st.markdown("### 📚 الوحدات التعليمية (12 أسبوع)")
|
| 170 |
+
|
| 171 |
+
for unit in curriculum[:6]: # عرض أول 6 وحدات فقط
|
| 172 |
+
week = unit.get("week", 0)
|
| 173 |
+
title = unit.get("unit_title", f"الوحدة {week}")
|
| 174 |
+
time_mins = unit.get("estimated_time_mins", 0)
|
| 175 |
+
time_hours = round(time_mins / 60, 1)
|
| 176 |
+
|
| 177 |
+
with st.expander(f"📖 الأسبوع {week}: {title}"):
|
| 178 |
+
st.markdown(f"**⏱️ الوقت المقدر:** {time_hours} ساعة ({time_mins} دقيقة)")
|
| 179 |
+
|
| 180 |
+
# الدروس
|
| 181 |
+
st.markdown("**📚 الدروس:**")
|
| 182 |
+
for lesson in unit.get("lessons", []):
|
| 183 |
+
st.markdown(f"- {lesson.get('title', '')} ({lesson.get('duration_mins', 0)} دقيقة) - {lesson.get('type', 'درس')}")
|
| 184 |
+
|
| 185 |
+
# التجربة
|
| 186 |
+
if unit.get("experiment"):
|
| 187 |
+
exp = unit["experiment"]
|
| 188 |
+
st.markdown("**🔬 التجربة العملية:**")
|
| 189 |
+
st.markdown(f"- **{exp.get('title', '')}**")
|
| 190 |
+
st.markdown(f" {exp.get('description', '')}")
|
| 191 |
+
st.markdown(f" *المخرج المطلوب:* {exp.get('deliverable', 'غير محدد')}")
|
| 192 |
+
|
| 193 |
+
if len(curriculum) > 6:
|
| 194 |
+
st.info(f"💡 هناك {len(curriculum) - 6} وحدات إضافية. ستظهر جميعها في صفحة الدروس.")
|
| 195 |
+
|
| 196 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 197 |
+
|
| 198 |
+
# أزرار الإجراءات
|
| 199 |
+
col1, col2, col3 = st.columns(3)
|
| 200 |
+
|
| 201 |
+
with col1:
|
| 202 |
+
if st.button("🔄 إعادة التوليد", use_container_width=True):
|
| 203 |
+
st.session_state.curriculum = None
|
| 204 |
+
st.rerun()
|
| 205 |
+
|
| 206 |
+
with col2:
|
| 207 |
+
if st.button("📥 تنزيل الخطة (JSON)", use_container_width=True):
|
| 208 |
+
import json
|
| 209 |
+
curriculum_json = json.dumps({
|
| 210 |
+
"hub": st.session_state.selected_hub,
|
| 211 |
+
"profile": profile,
|
| 212 |
+
"curriculum": curriculum
|
| 213 |
+
}, ensure_ascii=False, indent=2)
|
| 214 |
+
st.download_button(
|
| 215 |
+
"تنزيل",
|
| 216 |
+
data=curriculum_json,
|
| 217 |
+
file_name="my_learning_plan.json",
|
| 218 |
+
mime="application/json"
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
with col3:
|
| 222 |
+
if st.button("📚 ابدأ التعلم!", use_container_width=True, type="primary"):
|
| 223 |
+
st.session_state.page = "lesson"
|
| 224 |
+
st.rerun()
|
| 225 |
+
|
| 226 |
+
# زر العودة
|
| 227 |
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
| 228 |
+
if st.button("⬅️ العودة للملف التعريفي"):
|
| 229 |
+
st.session_state.page = "profile"
|
| 230 |
+
st.rerun()
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
if __name__ == "__main__":
|
| 234 |
+
show()
|
pages/profile.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
صفحة الملف التعريفي - رفع أو إنشاء ملف تعريفي
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import json
|
| 6 |
+
import sys
|
| 7 |
+
sys.path.append('..')
|
| 8 |
+
from utils.ui_utils import show_hero_section, show_success_message, show_warning_message, load_custom_css
|
| 9 |
+
from utils.data_utils import parse_profile_file, create_default_profile, validate_profile, export_profile_to_json
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def show():
|
| 13 |
+
"""عرض صفحة الملف التعريفي"""
|
| 14 |
+
|
| 15 |
+
load_custom_css()
|
| 16 |
+
|
| 17 |
+
st.markdown("## 👤 الملف التعريفي")
|
| 18 |
+
st.markdown("---")
|
| 19 |
+
|
| 20 |
+
# خيارات إنشاء الملف
|
| 21 |
+
st.markdown("### 🎯 كيف تريد إنشاء ملفك؟")
|
| 22 |
+
|
| 23 |
+
tab1, tab2, tab3 = st.tabs(["📝 تعبئة نموذج", "📤 رفع ملف", "👁️ معاينة الملف"])
|
| 24 |
+
|
| 25 |
+
# Tab 1: تعبئة نموذج
|
| 26 |
+
with tab1:
|
| 27 |
+
st.markdown("#### املأ المعلومات التالية:")
|
| 28 |
+
|
| 29 |
+
col1, col2 = st.columns(2)
|
| 30 |
+
|
| 31 |
+
with col1:
|
| 32 |
+
name = st.text_input("الاسم الكامل *", value=st.session_state.get("profile", {}).get("name", ""))
|
| 33 |
+
age = st.text_input("العمر", value=st.session_state.get("profile", {}).get("age", ""))
|
| 34 |
+
level = st.selectbox(
|
| 35 |
+
"المستوى الحالي *",
|
| 36 |
+
["مبتدئ", "متوسط", "متقدم", "خبير"],
|
| 37 |
+
index=0
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
with col2:
|
| 41 |
+
available_hours = st.number_input(
|
| 42 |
+
"الوقت المتاح أسبوعياً (ساعات) *",
|
| 43 |
+
min_value=1,
|
| 44 |
+
max_value=40,
|
| 45 |
+
value=st.session_state.get("profile", {}).get("available_hours", 10)
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
learning_style = st.selectbox(
|
| 49 |
+
"أسلوب التعلم المفضل",
|
| 50 |
+
["بصري (فيديوهات، صور)", "نصي (قراءة، مقالات)", "عملي (تطبيقات، مشاريع)", "مختلط (كل ما سبق)"],
|
| 51 |
+
index=3
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
st.markdown("---")
|
| 55 |
+
|
| 56 |
+
skills = st.text_area(
|
| 57 |
+
"المهارات الحالية (اختياري)",
|
| 58 |
+
placeholder="مثال: برمجة Python، تصميم جرافيك، تحليل بيانات...",
|
| 59 |
+
value=st.session_state.get("profile", {}).get("skills", "")
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
goals = st.text_area(
|
| 63 |
+
"أهدافك من التعلم *",
|
| 64 |
+
placeholder="مثال: أريد تعلم الذكاء الاصطناعي لبناء تطبيقات ذكية...",
|
| 65 |
+
value=st.session_state.get("profile", {}).get("goals", "")
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 69 |
+
|
| 70 |
+
if st.button("💾 حفظ الملف التعريفي", use_container_width=True, type="primary"):
|
| 71 |
+
# إنشاء الملف
|
| 72 |
+
profile = {
|
| 73 |
+
"name": name,
|
| 74 |
+
"age": age,
|
| 75 |
+
"level": level,
|
| 76 |
+
"skills": [s.strip() for s in skills.split(",") if s.strip()] if skills else [],
|
| 77 |
+
"available_hours": available_hours,
|
| 78 |
+
"learning_style": learning_style,
|
| 79 |
+
"goals": goals,
|
| 80 |
+
"preferences": {
|
| 81 |
+
"language": "ar",
|
| 82 |
+
"notifications": True,
|
| 83 |
+
"theme": "light"
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
# التحقق من الصحة
|
| 88 |
+
is_valid, errors = validate_profile(profile)
|
| 89 |
+
|
| 90 |
+
if is_valid:
|
| 91 |
+
st.session_state.profile = profile
|
| 92 |
+
show_success_message("✅ تم حفظ ملفك التعريفي بنجاح!")
|
| 93 |
+
st.balloons()
|
| 94 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 95 |
+
|
| 96 |
+
col1, col2 = st.columns(2)
|
| 97 |
+
with col1:
|
| 98 |
+
if st.button("📅 التالي: اختيار الخطة", use_container_width=True):
|
| 99 |
+
st.session_state.page = "plan"
|
| 100 |
+
st.rerun()
|
| 101 |
+
|
| 102 |
+
with col2:
|
| 103 |
+
# تنزيل الملف
|
| 104 |
+
json_str = export_profile_to_json(profile)
|
| 105 |
+
st.download_button(
|
| 106 |
+
"📥 تنزيل الملف (JSON)",
|
| 107 |
+
data=json_str,
|
| 108 |
+
file_name="my_profile.json",
|
| 109 |
+
mime="application/json",
|
| 110 |
+
use_container_width=True
|
| 111 |
+
)
|
| 112 |
+
else:
|
| 113 |
+
for error in errors:
|
| 114 |
+
show_warning_message(f"⚠️ {error}")
|
| 115 |
+
|
| 116 |
+
# Tab 2: رفع ملف
|
| 117 |
+
with tab2:
|
| 118 |
+
st.markdown("#### 📤 ارفع ملف تعريفي موجود")
|
| 119 |
+
st.markdown("يمكنك رفع ملف JSON أو TXT يحتوي على معلوماتك")
|
| 120 |
+
|
| 121 |
+
uploaded_file = st.file_uploader(
|
| 122 |
+
"اختر ملفاً",
|
| 123 |
+
type=["json", "txt", "md"],
|
| 124 |
+
help="ملف JSON، TXT، أو Markdown"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
if uploaded_file:
|
| 128 |
+
try:
|
| 129 |
+
content = uploaded_file.read().decode("utf-8")
|
| 130 |
+
file_type = uploaded_file.name.split(".")[-1]
|
| 131 |
+
|
| 132 |
+
parsed_profile = parse_profile_file(content, file_type)
|
| 133 |
+
|
| 134 |
+
if parsed_profile:
|
| 135 |
+
show_success_message("تم تحليل الملف بنجاح!")
|
| 136 |
+
|
| 137 |
+
# عرض محتوى الملف
|
| 138 |
+
st.json(parsed_profile)
|
| 139 |
+
|
| 140 |
+
if st.button("✅ استخدام هذا الملف", use_container_width=True):
|
| 141 |
+
st.session_state.profile = parsed_profile
|
| 142 |
+
show_success_message("تم حفظ الملف التعريفي!")
|
| 143 |
+
st.balloons()
|
| 144 |
+
else:
|
| 145 |
+
show_warning_message("لم نتمكن من تحليل الملف. حاول مرة أخرى.")
|
| 146 |
+
|
| 147 |
+
except Exception as e:
|
| 148 |
+
show_warning_message(f"خطأ في قراءة الملف: {str(e)}")
|
| 149 |
+
|
| 150 |
+
# مثال على تنسيق الملف
|
| 151 |
+
with st.expander("💡 مثال على تنسيق الملف"):
|
| 152 |
+
st.markdown("**JSON Format:**")
|
| 153 |
+
st.code('''
|
| 154 |
+
{
|
| 155 |
+
"name": "أحمد محمد",
|
| 156 |
+
"age": "25",
|
| 157 |
+
"level": "متوسط",
|
| 158 |
+
"skills": ["Python", "تصميم"],
|
| 159 |
+
"available_hours": 15,
|
| 160 |
+
"learning_style": "مختلط",
|
| 161 |
+
"goals": "تعلم الذكاء الاصطناعي"
|
| 162 |
+
}
|
| 163 |
+
''', language="json")
|
| 164 |
+
|
| 165 |
+
st.markdown("**Text Format:**")
|
| 166 |
+
st.code('''
|
| 167 |
+
الاسم: أحمد محمد
|
| 168 |
+
العمر: 25
|
| 169 |
+
المستوى: متوسط
|
| 170 |
+
المهارات: Python, تصميم
|
| 171 |
+
ساعات متاحة: 15
|
| 172 |
+
أسلوب التعلم: مختلط
|
| 173 |
+
الأهداف: تعلم الذكاء الاصطناعي
|
| 174 |
+
''', language="text")
|
| 175 |
+
|
| 176 |
+
# Tab 3: معاينة الملف الحالي
|
| 177 |
+
with tab3:
|
| 178 |
+
if "profile" in st.session_state and st.session_state.profile:
|
| 179 |
+
st.markdown("#### 👁️ ملفك الحالي:")
|
| 180 |
+
|
| 181 |
+
profile = st.session_state.profile
|
| 182 |
+
|
| 183 |
+
# عرض جميل للملف
|
| 184 |
+
col1, col2 = st.columns(2)
|
| 185 |
+
|
| 186 |
+
with col1:
|
| 187 |
+
st.markdown(f"""
|
| 188 |
+
<div class='card'>
|
| 189 |
+
<h4>👤 المعلومات الشخصية</h4>
|
| 190 |
+
<p><strong>الاسم:</strong> {profile.get('name', 'غير محدد')}</p>
|
| 191 |
+
<p><strong>العمر:</strong> {profile.get('age', 'غير محدد')}</p>
|
| 192 |
+
<p><strong>المستوى:</strong> {profile.get('level', 'غير محدد')}</p>
|
| 193 |
+
</div>
|
| 194 |
+
""", unsafe_allow_html=True)
|
| 195 |
+
|
| 196 |
+
with col2:
|
| 197 |
+
st.markdown(f"""
|
| 198 |
+
<div class='card'>
|
| 199 |
+
<h4>⏰ الوقت والتفضيلات</h4>
|
| 200 |
+
<p><strong>الوقت المتاح:</strong> {profile.get('available_hours', 0)} ساعة/أسبوع</p>
|
| 201 |
+
<p><strong>أسلوب التعلم:</strong> {profile.get('learning_style', 'غير محدد')}</p>
|
| 202 |
+
</div>
|
| 203 |
+
""", unsafe_allow_html=True)
|
| 204 |
+
|
| 205 |
+
st.markdown(f"""
|
| 206 |
+
<div class='card'>
|
| 207 |
+
<h4>🎯 الأهداف</h4>
|
| 208 |
+
<p>{profile.get('goals', 'لم يتم تحديد أهداف')}</p>
|
| 209 |
+
</div>
|
| 210 |
+
""", unsafe_allow_html=True)
|
| 211 |
+
|
| 212 |
+
if profile.get('skills'):
|
| 213 |
+
st.markdown(f"""
|
| 214 |
+
<div class='card'>
|
| 215 |
+
<h4>💪 المهارات الحالية</h4>
|
| 216 |
+
<p>{', '.join(profile.get('skills', [])) if isinstance(profile.get('skills'), list) else profile.get('skills', 'لا توجد')}</p>
|
| 217 |
+
</div>
|
| 218 |
+
""", unsafe_allow_html=True)
|
| 219 |
+
|
| 220 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 221 |
+
|
| 222 |
+
col1, col2, col3 = st.columns(3)
|
| 223 |
+
|
| 224 |
+
with col1:
|
| 225 |
+
if st.button("✏️ تعديل الملف", use_container_width=True):
|
| 226 |
+
st.info("عد إلى تبويب 'تعبئة نموذج' للتعديل")
|
| 227 |
+
|
| 228 |
+
with col2:
|
| 229 |
+
json_str = export_profile_to_json(profile)
|
| 230 |
+
st.download_button(
|
| 231 |
+
"📥 تنزيل",
|
| 232 |
+
data=json_str,
|
| 233 |
+
file_name="my_profile.json",
|
| 234 |
+
mime="application/json",
|
| 235 |
+
use_container_width=True
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
with col3:
|
| 239 |
+
if st.button("📅 التالي: الخطة", use_container_width=True, type="primary"):
|
| 240 |
+
st.session_state.page = "plan"
|
| 241 |
+
st.rerun()
|
| 242 |
+
else:
|
| 243 |
+
st.info("لم يتم إنشاء ملف تعريفي بعد. عد إلى تبويب 'تعبئة نموذج' أو 'رفع ملف'")
|
| 244 |
+
|
| 245 |
+
# زر العودة
|
| 246 |
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
| 247 |
+
if st.button("⬅️ العودة للصفحة الرئيسية"):
|
| 248 |
+
st.session_state.page = "home"
|
| 249 |
+
st.rerun()
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
if __name__ == "__main__":
|
| 253 |
+
show()
|
pages/progress.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
صفحة التقدم والمحفظة - عرض الإنجازات والإحصائيات
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
import plotly.express as px
|
| 7 |
+
import sys
|
| 8 |
+
sys.path.append('..')
|
| 9 |
+
from utils.ui_utils import load_custom_css, show_progress_bar, create_stat_card
|
| 10 |
+
from utils.data_utils import calculate_progress, get_completion_stats
|
| 11 |
+
import pandas as pd
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def show():
|
| 15 |
+
"""عرض صفحة التقدم والإنجازات"""
|
| 16 |
+
|
| 17 |
+
load_custom_css()
|
| 18 |
+
|
| 19 |
+
st.markdown("## 📊 تقدمك وإنجازاتك")
|
| 20 |
+
st.markdown("---")
|
| 21 |
+
|
| 22 |
+
# التحقق من وجود منهج
|
| 23 |
+
if "curriculum" not in st.session_state or not st.session_state.curriculum:
|
| 24 |
+
st.warning("⚠️ لم يتم إنشاء خطة تعليمية بعد!")
|
| 25 |
+
if st.button("📅 إنشاء خطة"):
|
| 26 |
+
st.session_state.page = "plan"
|
| 27 |
+
st.rerun()
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
curriculum = st.session_state.curriculum
|
| 31 |
+
|
| 32 |
+
# تهيئة البيانات
|
| 33 |
+
if "completed_lessons" not in st.session_state:
|
| 34 |
+
st.session_state.completed_lessons = []
|
| 35 |
+
|
| 36 |
+
if "completed_experiments" not in st.session_state:
|
| 37 |
+
st.session_state.completed_experiments = []
|
| 38 |
+
|
| 39 |
+
# حساب الإحصائيات
|
| 40 |
+
stats = get_completion_stats(st.session_state.completed_lessons, curriculum)
|
| 41 |
+
|
| 42 |
+
# نظرة عامة
|
| 43 |
+
st.markdown("### 🎯 نظرة عامة")
|
| 44 |
+
|
| 45 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 46 |
+
|
| 47 |
+
with col1:
|
| 48 |
+
st.markdown(create_stat_card(
|
| 49 |
+
"الدروس المكتملة",
|
| 50 |
+
f"{stats['lessons']['completed']}/{stats['lessons']['total']}",
|
| 51 |
+
"📚"
|
| 52 |
+
), unsafe_allow_html=True)
|
| 53 |
+
|
| 54 |
+
with col2:
|
| 55 |
+
st.markdown(create_stat_card(
|
| 56 |
+
"التجارب المكتملة",
|
| 57 |
+
f"{stats['experiments']['completed']}/{stats['experiments']['total']}",
|
| 58 |
+
"🔬"
|
| 59 |
+
), unsafe_allow_html=True)
|
| 60 |
+
|
| 61 |
+
with col3:
|
| 62 |
+
st.markdown(create_stat_card(
|
| 63 |
+
"نسبة الإنجاز",
|
| 64 |
+
f"{stats['overall_percentage']:.1f}%",
|
| 65 |
+
"🎯"
|
| 66 |
+
), unsafe_allow_html=True)
|
| 67 |
+
|
| 68 |
+
with col4:
|
| 69 |
+
total_weeks = len(curriculum)
|
| 70 |
+
completed_weeks = len([u for u in curriculum if all(
|
| 71 |
+
f"{u['week']}_{lesson['title']}" in st.session_state.completed_lessons
|
| 72 |
+
for lesson in u.get('lessons', [])
|
| 73 |
+
)])
|
| 74 |
+
st.markdown(create_stat_card(
|
| 75 |
+
"الأسابيع المكتملة",
|
| 76 |
+
f"{completed_weeks}/{total_weeks}",
|
| 77 |
+
"📅"
|
| 78 |
+
), unsafe_allow_html=True)
|
| 79 |
+
|
| 80 |
+
st.markdown("---")
|
| 81 |
+
|
| 82 |
+
# شريط التقدم الإجمالي
|
| 83 |
+
st.markdown("### 📈 التقدم الإجمالي")
|
| 84 |
+
show_progress_bar(stats['overall_percentage'], get_progress_status(stats['overall_percentage']))
|
| 85 |
+
|
| 86 |
+
st.markdown("---")
|
| 87 |
+
|
| 88 |
+
# رسم بياني دائري
|
| 89 |
+
col1, col2 = st.columns(2)
|
| 90 |
+
|
| 91 |
+
with col1:
|
| 92 |
+
st.markdown("### 📚 توزيع الدروس")
|
| 93 |
+
|
| 94 |
+
fig = go.Figure(data=[go.Pie(
|
| 95 |
+
labels=['مكتملة', 'متبقية'],
|
| 96 |
+
values=[stats['lessons']['completed'], stats['lessons']['total'] - stats['lessons']['completed']],
|
| 97 |
+
hole=0.6,
|
| 98 |
+
marker_colors=['#48bb78', '#e2e8f0']
|
| 99 |
+
)])
|
| 100 |
+
|
| 101 |
+
fig.update_layout(
|
| 102 |
+
showlegend=True,
|
| 103 |
+
height=300,
|
| 104 |
+
margin=dict(l=20, r=20, t=20, b=20),
|
| 105 |
+
annotations=[dict(
|
| 106 |
+
text=f"{stats['lessons']['percentage']:.1f}%",
|
| 107 |
+
x=0.5, y=0.5,
|
| 108 |
+
font_size=28,
|
| 109 |
+
font_color='#667eea',
|
| 110 |
+
showarrow=False
|
| 111 |
+
)]
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 115 |
+
|
| 116 |
+
with col2:
|
| 117 |
+
st.markdown("### 🔬 توزيع التجارب")
|
| 118 |
+
|
| 119 |
+
fig2 = go.Figure(data=[go.Pie(
|
| 120 |
+
labels=['مكتملة', 'متبقية'],
|
| 121 |
+
values=[stats['experiments']['completed'], stats['experiments']['total'] - stats['experiments']['completed']],
|
| 122 |
+
hole=0.6,
|
| 123 |
+
marker_colors=['#764ba2', '#e2e8f0']
|
| 124 |
+
)])
|
| 125 |
+
|
| 126 |
+
fig2.update_layout(
|
| 127 |
+
showlegend=True,
|
| 128 |
+
height=300,
|
| 129 |
+
margin=dict(l=20, r=20, t=20, b=20),
|
| 130 |
+
annotations=[dict(
|
| 131 |
+
text=f"{stats['experiments']['percentage']:.1f}%",
|
| 132 |
+
x=0.5, y=0.5,
|
| 133 |
+
font_size=28,
|
| 134 |
+
font_color='#764ba2',
|
| 135 |
+
showarrow=False
|
| 136 |
+
)]
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
st.plotly_chart(fig2, use_container_width=True)
|
| 140 |
+
|
| 141 |
+
st.markdown("---")
|
| 142 |
+
|
| 143 |
+
# التقدم الأسبوعي
|
| 144 |
+
st.markdown("### 📅 التقدم الأسبوعي")
|
| 145 |
+
|
| 146 |
+
weekly_progress = []
|
| 147 |
+
for unit in curriculum:
|
| 148 |
+
week = unit['week']
|
| 149 |
+
unit_lessons = unit.get('lessons', [])
|
| 150 |
+
completed_in_week = sum(1 for lesson in unit_lessons if f"{week}_{lesson['title']}" in st.session_state.completed_lessons)
|
| 151 |
+
total_in_week = len(unit_lessons)
|
| 152 |
+
percentage = (completed_in_week / total_in_week * 100) if total_in_week > 0 else 0
|
| 153 |
+
|
| 154 |
+
weekly_progress.append({
|
| 155 |
+
'الأسبوع': f"أسبوع {week}",
|
| 156 |
+
'النسبة': percentage,
|
| 157 |
+
'مكتمل': completed_in_week,
|
| 158 |
+
'الإجمالي': total_in_week
|
| 159 |
+
})
|
| 160 |
+
|
| 161 |
+
df = pd.DataFrame(weekly_progress)
|
| 162 |
+
|
| 163 |
+
fig3 = px.bar(
|
| 164 |
+
df,
|
| 165 |
+
x='الأسبوع',
|
| 166 |
+
y='النسبة',
|
| 167 |
+
text='النسبة',
|
| 168 |
+
color='النسبة',
|
| 169 |
+
color_continuous_scale=['#e2e8f0', '#667eea', '#764ba2'],
|
| 170 |
+
range_color=[0, 100]
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
fig3.update_traces(texttemplate='%{text:.1f}%', textposition='outside')
|
| 174 |
+
fig3.update_layout(
|
| 175 |
+
height=400,
|
| 176 |
+
showlegend=False,
|
| 177 |
+
xaxis_title="",
|
| 178 |
+
yaxis_title="نسبة الإنجاز (%)",
|
| 179 |
+
yaxis_range=[0, 110]
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
st.plotly_chart(fig3, use_container_width=True)
|
| 183 |
+
|
| 184 |
+
st.markdown("---")
|
| 185 |
+
|
| 186 |
+
# الشارات والإنجازات
|
| 187 |
+
st.markdown("### 🏆 الشارات والإنجازات")
|
| 188 |
+
|
| 189 |
+
badges = []
|
| 190 |
+
|
| 191 |
+
# شارة البداية
|
| 192 |
+
if stats['lessons']['completed'] > 0:
|
| 193 |
+
badges.append({"name": "🌱 الخطوة الأولى", "desc": "أكملت أول درس"})
|
| 194 |
+
|
| 195 |
+
# شارة الأسبوع الأول
|
| 196 |
+
if completed_weeks >= 1:
|
| 197 |
+
badges.append({"name": "🎯 أسبوع كامل", "desc": "أكملت أسبوعاً كاملاً"})
|
| 198 |
+
|
| 199 |
+
# شارة نصف الطريق
|
| 200 |
+
if stats['overall_percentage'] >= 50:
|
| 201 |
+
badges.append({"name": "⭐ منتصف الرحلة", "desc": "أكملت نصف المنهج"})
|
| 202 |
+
|
| 203 |
+
# شارة التجارب
|
| 204 |
+
if stats['experiments']['completed'] >= 3:
|
| 205 |
+
badges.append({"name": "🔬 خبير تجارب", "desc": "أكملت 3 تجارب عملية"})
|
| 206 |
+
|
| 207 |
+
# شارة المثابرة
|
| 208 |
+
if stats['lessons']['completed'] >= 20:
|
| 209 |
+
badges.append({"name": "💪 مثابر", "desc": "أكملت 20 درساً"})
|
| 210 |
+
|
| 211 |
+
# شارة الإنجاز الكامل
|
| 212 |
+
if stats['overall_percentage'] >= 100:
|
| 213 |
+
badges.append({"name": "🎉 متخرج!", "desc": "أكملت المنهج بالكامل"})
|
| 214 |
+
|
| 215 |
+
if badges:
|
| 216 |
+
cols = st.columns(min(len(badges), 4))
|
| 217 |
+
for idx, badge in enumerate(badges):
|
| 218 |
+
col = cols[idx % 4]
|
| 219 |
+
with col:
|
| 220 |
+
st.markdown(f"""
|
| 221 |
+
<div class='card' style='text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; min-height: 150px; display: flex; flex-direction: column; justify-content: center;'>
|
| 222 |
+
<h2 style='margin: 0.5rem 0; color: white;'>{badge['name'].split()[0]}</h2>
|
| 223 |
+
<h4 style='margin: 0.25rem 0; color: white;'>{' '.join(badge['name'].split()[1:])}</h4>
|
| 224 |
+
<p style='margin: 0.5rem 0 0 0; font-size: 0.85rem; color: rgba(255,255,255,0.9);'>{badge['desc']}</p>
|
| 225 |
+
</div>
|
| 226 |
+
""", unsafe_allow_html=True)
|
| 227 |
+
else:
|
| 228 |
+
st.info("ابدأ التعلم لتحصل على شاراتك الأولى! 🎯")
|
| 229 |
+
|
| 230 |
+
st.markdown("---")
|
| 231 |
+
|
| 232 |
+
# المحفظة
|
| 233 |
+
st.markdown("### 📁 محفظتك")
|
| 234 |
+
|
| 235 |
+
st.markdown("""
|
| 236 |
+
<div class='info-box'>
|
| 237 |
+
<strong>💡 محفظتك تحتوي على:</strong>
|
| 238 |
+
<ul style='margin: 0.5rem 0 0 0;'>
|
| 239 |
+
<li>جميع الدروس المكتملة مع ملاحظاتك</li>
|
| 240 |
+
<li>التجارب العملية التي أنجزتها</li>
|
| 241 |
+
<li>المشاريع الجماعية التي شاركت فيها</li>
|
| 242 |
+
<li>الشارات والإنجازات</li>
|
| 243 |
+
</ul>
|
| 244 |
+
</div>
|
| 245 |
+
""", unsafe_allow_html=True)
|
| 246 |
+
|
| 247 |
+
# زر تصدير المحفظة
|
| 248 |
+
if st.button("📥 تصدير المحفظة (JSON)", use_container_width=True):
|
| 249 |
+
import json
|
| 250 |
+
from datetime import datetime
|
| 251 |
+
|
| 252 |
+
portfolio = {
|
| 253 |
+
"profile": st.session_state.get('profile', {}),
|
| 254 |
+
"curriculum": curriculum,
|
| 255 |
+
"stats": stats,
|
| 256 |
+
"completed_lessons": st.session_state.completed_lessons,
|
| 257 |
+
"completed_experiments": st.session_state.completed_experiments,
|
| 258 |
+
"badges": badges,
|
| 259 |
+
"exported_at": datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
portfolio_json = json.dumps(portfolio, ensure_ascii=False, indent=2)
|
| 263 |
+
st.download_button(
|
| 264 |
+
"💾 تنزيل المحفظة",
|
| 265 |
+
data=portfolio_json,
|
| 266 |
+
file_name="my_portfolio.json",
|
| 267 |
+
mime="application/json"
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
# أزرار التنقل
|
| 271 |
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
| 272 |
+
st.markdown("---")
|
| 273 |
+
|
| 274 |
+
col1, col2, col3 = st.columns(3)
|
| 275 |
+
|
| 276 |
+
with col1:
|
| 277 |
+
if st.button("📚 العودة للدروس", use_container_width=True):
|
| 278 |
+
st.session_state.page = "lesson"
|
| 279 |
+
st.rerun()
|
| 280 |
+
|
| 281 |
+
with col2:
|
| 282 |
+
if st.button("🔬 التجارب", use_container_width=True):
|
| 283 |
+
st.session_state.page = "experiment"
|
| 284 |
+
st.rerun()
|
| 285 |
+
|
| 286 |
+
with col3:
|
| 287 |
+
if st.button("👥 المشاريع", use_container_width=True):
|
| 288 |
+
st.session_state.page = "projects"
|
| 289 |
+
st.rerun()
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def get_progress_status(percentage: float) -> str:
|
| 293 |
+
"""تحديد حالة التقدم"""
|
| 294 |
+
if percentage < 25:
|
| 295 |
+
return "بداية الرحلة 🌱"
|
| 296 |
+
elif percentage < 50:
|
| 297 |
+
return "في الطريق 🚀"
|
| 298 |
+
elif percentage < 75:
|
| 299 |
+
return "تقدم ممتاز 🌟"
|
| 300 |
+
elif percentage < 100:
|
| 301 |
+
return "قرب الإنجاز 🎯"
|
| 302 |
+
else:
|
| 303 |
+
return "مكتمل! 🎉"
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
if __name__ == "__main__":
|
| 307 |
+
show()
|
pages/projects.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
صفحة المشاريع الجماعية - إنشاء وإدارة المشاريع
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import sys
|
| 6 |
+
sys.path.append('..')
|
| 7 |
+
from utils.ui_utils import load_custom_css, show_success_message, show_warning_message
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def show():
|
| 12 |
+
"""عرض صفحة المشاريع الجماعية"""
|
| 13 |
+
|
| 14 |
+
load_custom_css()
|
| 15 |
+
|
| 16 |
+
st.markdown("## 👥 المشاريع الجماعية")
|
| 17 |
+
st.markdown("---")
|
| 18 |
+
|
| 19 |
+
# تهيئة المشاريع
|
| 20 |
+
if "projects" not in st.session_state:
|
| 21 |
+
st.session_state.projects = []
|
| 22 |
+
|
| 23 |
+
if "user_projects" not in st.session_state:
|
| 24 |
+
st.session_state.user_projects = [] # المشاريع التي انضم لها المستخدم
|
| 25 |
+
|
| 26 |
+
# علامات تبويب
|
| 27 |
+
tab1, tab2, tab3 = st.tabs(["📋 المشاريع المتاحة", "➕ إنشاء مشروع", "📁 مشاريعي"])
|
| 28 |
+
|
| 29 |
+
# Tab 1: المشاريع المتاحة
|
| 30 |
+
with tab1:
|
| 31 |
+
st.markdown("### 🌟 المشاريع المتاحة للانضمام")
|
| 32 |
+
|
| 33 |
+
if not st.session_state.projects:
|
| 34 |
+
st.info("""
|
| 35 |
+
🎯 **لا توجد مشاريع متاحة حالياً**
|
| 36 |
+
|
| 37 |
+
يمكنك إنشاء مشروع جديد من تبويب "إنشاء مشروع" وسيظهر هنا للآخرين للانضمام إليه!
|
| 38 |
+
""")
|
| 39 |
+
else:
|
| 40 |
+
for project in st.session_state.projects:
|
| 41 |
+
if project['id'] not in st.session_state.user_projects:
|
| 42 |
+
with st.expander(f"🚀 {project['title']}"):
|
| 43 |
+
col1, col2 = st.columns([3, 1])
|
| 44 |
+
|
| 45 |
+
with col1:
|
| 46 |
+
st.markdown(f"""
|
| 47 |
+
<div class='card'>
|
| 48 |
+
<p><strong>الوصف:</strong> {project['description']}</p>
|
| 49 |
+
<p><strong>👤 منشئ المشروع:</strong> {project['owner']}</p>
|
| 50 |
+
<p><strong>👥 الأعضاء:</strong> {len(project['members'])}/{project['max_members']}</p>
|
| 51 |
+
<p><strong>📅 تاريخ الإنشاء:</strong> {project['created_at']}</p>
|
| 52 |
+
<p><strong>🏷️ المهارات المطلوبة:</strong> {', '.join(project['skills'])}</p>
|
| 53 |
+
</div>
|
| 54 |
+
""", unsafe_allow_html=True)
|
| 55 |
+
|
| 56 |
+
with col2:
|
| 57 |
+
if len(project['members']) < project['max_members']:
|
| 58 |
+
if st.button(f"✅ انضم", key=f"join_{project['id']}", use_container_width=True):
|
| 59 |
+
# إضافة المستخدم للمشروع
|
| 60 |
+
user_name = st.session_state.get('profile', {}).get('name', 'مستخدم')
|
| 61 |
+
project['members'].append(user_name)
|
| 62 |
+
st.session_state.user_projects.append(project['id'])
|
| 63 |
+
show_success_message(f"تم انضمامك لمشروع '{project['title']}'!")
|
| 64 |
+
st.balloons()
|
| 65 |
+
st.rerun()
|
| 66 |
+
else:
|
| 67 |
+
st.warning("مكتمل")
|
| 68 |
+
|
| 69 |
+
# Tab 2: إنشاء مشروع
|
| 70 |
+
with tab2:
|
| 71 |
+
st.markdown("### ➕ إنشاء مشروع جديد")
|
| 72 |
+
|
| 73 |
+
st.markdown("""
|
| 74 |
+
<div class='info-box'>
|
| 75 |
+
<strong>💡 نصائح لمشروع ناجح:</strong>
|
| 76 |
+
<ul style='margin: 0.5rem 0 0 0;'>
|
| 77 |
+
<li>اختر عنواناً واضحاً ووصفاً مفصلاً</li>
|
| 78 |
+
<li>حدد المهارات المطلوبة بدقة</li>
|
| 79 |
+
<li>ضع خطة زمنية واقعية</li>
|
| 80 |
+
<li>كن نشطاً في التواصل مع الفريق</li>
|
| 81 |
+
</ul>
|
| 82 |
+
</div>
|
| 83 |
+
""", unsafe_allow_html=True)
|
| 84 |
+
|
| 85 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 86 |
+
|
| 87 |
+
with st.form("create_project_form"):
|
| 88 |
+
project_title = st.text_input("عنوان المشروع *", placeholder="مثال: تطبيق ذكاء اصطناعي للتعرف على الصور")
|
| 89 |
+
|
| 90 |
+
project_description = st.text_area(
|
| 91 |
+
"وصف المشروع *",
|
| 92 |
+
placeholder="اشرح فكرة المشروع، الأهداف، والمخرجات المتوقعة...",
|
| 93 |
+
height=150
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
col1, col2 = st.columns(2)
|
| 97 |
+
with col1:
|
| 98 |
+
max_members = st.number_input("الحد الأقصى للأعضاء", min_value=2, max_value=10, value=4)
|
| 99 |
+
|
| 100 |
+
with col2:
|
| 101 |
+
duration_weeks = st.number_input("المدة (بالأسابيع)", min_value=1, max_value=12, value=4)
|
| 102 |
+
|
| 103 |
+
skills_input = st.text_input(
|
| 104 |
+
"المهارات المطلوبة (افصل بفواصل)",
|
| 105 |
+
placeholder="Python, تصميم, تحليل بيانات"
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
submitted = st.form_submit_button("🚀 إنشاء المشروع", use_container_width=True, type="primary")
|
| 109 |
+
|
| 110 |
+
if submitted:
|
| 111 |
+
if project_title.strip() and project_description.strip():
|
| 112 |
+
# إنشاء المشروع
|
| 113 |
+
new_project = {
|
| 114 |
+
"id": f"proj_{len(st.session_state.projects) + 1}_{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
| 115 |
+
"title": project_title,
|
| 116 |
+
"description": project_description,
|
| 117 |
+
"owner": st.session_state.get('profile', {}).get('name', 'مستخدم'),
|
| 118 |
+
"members": [st.session_state.get('profile', {}).get('name', 'مستخدم')],
|
| 119 |
+
"max_members": max_members,
|
| 120 |
+
"skills": [s.strip() for s in skills_input.split(",") if s.strip()],
|
| 121 |
+
"duration_weeks": duration_weeks,
|
| 122 |
+
"created_at": datetime.now().strftime("%Y-%m-%d"),
|
| 123 |
+
"status": "active",
|
| 124 |
+
"tasks": [],
|
| 125 |
+
"deliverables": []
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
st.session_state.projects.append(new_project)
|
| 129 |
+
st.session_state.user_projects.append(new_project['id'])
|
| 130 |
+
|
| 131 |
+
show_success_message(f"✅ تم إنشاء مشروع '{project_title}' بنجاح!")
|
| 132 |
+
st.balloons()
|
| 133 |
+
st.rerun()
|
| 134 |
+
else:
|
| 135 |
+
show_warning_message("⚠️ يجب ملء جميع الحقول المطلوبة")
|
| 136 |
+
|
| 137 |
+
# Tab 3: مشاريعي
|
| 138 |
+
with tab3:
|
| 139 |
+
st.markdown("### 📁 المشاريع التي أشارك فيها")
|
| 140 |
+
|
| 141 |
+
my_projects = [p for p in st.session_state.projects if p['id'] in st.session_state.user_projects]
|
| 142 |
+
|
| 143 |
+
if not my_projects:
|
| 144 |
+
st.info("لم تنضم لأي مشروع بعد! تصفح المشاريع المتاحة أو أنشئ مشروعاً جديداً.")
|
| 145 |
+
else:
|
| 146 |
+
for project in my_projects:
|
| 147 |
+
user_name = st.session_state.get('profile', {}).get('name', 'مستخدم')
|
| 148 |
+
is_owner = project['owner'] == user_name
|
| 149 |
+
|
| 150 |
+
with st.expander(f"{'👑' if is_owner else '👤'} {project['title']}", expanded=True):
|
| 151 |
+
|
| 152 |
+
# معلومات المشروع
|
| 153 |
+
st.markdown(f"""
|
| 154 |
+
<div class='card'>
|
| 155 |
+
<p><strong>الوصف:</strong> {project['description']}</p>
|
| 156 |
+
<p><strong>👑 منشئ المشروع:</strong> {project['owner']}</p>
|
| 157 |
+
<p><strong>⏱️ المدة:</strong> {project['duration_weeks']} أسابيع</p>
|
| 158 |
+
<p><strong>📅 تاريخ البدء:</strong> {project['created_at']}</p>
|
| 159 |
+
</div>
|
| 160 |
+
""", unsafe_allow_html=True)
|
| 161 |
+
|
| 162 |
+
# الأعضاء
|
| 163 |
+
st.markdown("**👥 أعضاء الفريق:**")
|
| 164 |
+
members_display = ", ".join([f"{'👑 ' if m == project['owner'] else ''}{m}" for m in project['members']])
|
| 165 |
+
st.markdown(f"<div class='badge badge-info'>{members_display}</div>", unsafe_allow_html=True)
|
| 166 |
+
|
| 167 |
+
st.markdown("---")
|
| 168 |
+
|
| 169 |
+
# المهام
|
| 170 |
+
st.markdown("**📋 المهام:**")
|
| 171 |
+
|
| 172 |
+
project_tasks_key = f"tasks_{project['id']}"
|
| 173 |
+
if project_tasks_key not in st.session_state:
|
| 174 |
+
st.session_state[project_tasks_key] = project.get('tasks', [])
|
| 175 |
+
|
| 176 |
+
tasks = st.session_state[project_tasks_key]
|
| 177 |
+
|
| 178 |
+
if tasks:
|
| 179 |
+
for idx, task in enumerate(tasks):
|
| 180 |
+
col1, col2 = st.columns([4, 1])
|
| 181 |
+
with col1:
|
| 182 |
+
st.markdown(f"- {task['title']} (المسؤول: {task['assigned_to']})")
|
| 183 |
+
with col2:
|
| 184 |
+
status_badge = "success" if task['status'] == 'completed' else "warning"
|
| 185 |
+
st.markdown(f"<span class='badge badge-{status_badge}'>{task['status']}</span>", unsafe_allow_html=True)
|
| 186 |
+
else:
|
| 187 |
+
st.info("لا توجد مهام بعد")
|
| 188 |
+
|
| 189 |
+
# إضافة مهمة جديدة (للمنشئ فقط)
|
| 190 |
+
if is_owner:
|
| 191 |
+
with st.form(f"add_task_{project['id']}"):
|
| 192 |
+
st.markdown("**➕ إضافة مهمة جديدة:**")
|
| 193 |
+
col1, col2 = st.columns(2)
|
| 194 |
+
with col1:
|
| 195 |
+
task_title = st.text_input("عنوان المهمة", key=f"task_title_{project['id']}")
|
| 196 |
+
with col2:
|
| 197 |
+
assigned_to = st.selectbox("تعيين إلى", project['members'], key=f"assign_{project['id']}")
|
| 198 |
+
|
| 199 |
+
if st.form_submit_button("➕ إضافة"):
|
| 200 |
+
if task_title.strip():
|
| 201 |
+
new_task = {
|
| 202 |
+
"title": task_title,
|
| 203 |
+
"assigned_to": assigned_to,
|
| 204 |
+
"status": "pending",
|
| 205 |
+
"created_at": datetime.now().strftime("%Y-%m-%d")
|
| 206 |
+
}
|
| 207 |
+
tasks.append(new_task)
|
| 208 |
+
project['tasks'] = tasks
|
| 209 |
+
show_success_message("تم إضافة المهمة!")
|
| 210 |
+
st.rerun()
|
| 211 |
+
|
| 212 |
+
st.markdown("---")
|
| 213 |
+
|
| 214 |
+
# منطقة التواصل
|
| 215 |
+
st.markdown("**💬 التواصل:**")
|
| 216 |
+
st.info("""
|
| 217 |
+
💡 **نصيحة:** استخدم أدوات التواصل الخارجية للتنسيق:
|
| 218 |
+
- Slack / Discord للمحادثات
|
| 219 |
+
- GitHub للكود
|
| 220 |
+
- Google Drive / Notion للوثائق
|
| 221 |
+
""")
|
| 222 |
+
|
| 223 |
+
# رفع مخرجات
|
| 224 |
+
st.markdown("**📤 رفع مخرجات:**")
|
| 225 |
+
deliverable_file = st.file_uploader(
|
| 226 |
+
"ارفع ملفات المشروع",
|
| 227 |
+
key=f"upload_{project['id']}",
|
| 228 |
+
accept_multiple_files=True
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
if deliverable_file:
|
| 232 |
+
if st.button(f"💾 حفظ المخرجات", key=f"save_del_{project['id']}"):
|
| 233 |
+
for file in deliverable_file:
|
| 234 |
+
project['deliverables'].append({
|
| 235 |
+
"file_name": file.name,
|
| 236 |
+
"uploaded_by": user_name,
|
| 237 |
+
"uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 238 |
+
})
|
| 239 |
+
show_success_message("تم رفع المخرجات!")
|
| 240 |
+
st.rerun()
|
| 241 |
+
|
| 242 |
+
# عرض المخرجات
|
| 243 |
+
if project.get('deliverables'):
|
| 244 |
+
st.markdown("**📦 المخرجات المرفوعة:**")
|
| 245 |
+
for del_item in project['deliverables']:
|
| 246 |
+
st.markdown(f"- 📄 {del_item['file_name']} (بواسطة {del_item['uploaded_by']} - {del_item['uploaded_at']})")
|
| 247 |
+
|
| 248 |
+
# زر العودة
|
| 249 |
+
st.markdown("<br><br>", unsafe_allow_html=True)
|
| 250 |
+
st.markdown("---")
|
| 251 |
+
if st.button("⬅️ العودة للدروس"):
|
| 252 |
+
st.session_state.page = "lesson"
|
| 253 |
+
st.rerun()
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
if __name__ == "__main__":
|
| 257 |
+
show()
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.32.0
|
| 2 |
+
streamlit-option-menu==0.3.12
|
| 3 |
+
streamlit-lottie==0.0.5
|
| 4 |
+
supabase==2.4.0
|
| 5 |
+
python-dotenv==1.0.1
|
| 6 |
+
requests==2.31.0
|
| 7 |
+
plotly==5.19.0
|
| 8 |
+
pandas==2.2.1
|
| 9 |
+
Pillow==10.2.0
|
| 10 |
+
python-multipart==0.0.9
|
tests/test_app.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
اختبارات بسيطة للتطبيق
|
| 3 |
+
"""
|
| 4 |
+
import pytest
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
# إضافة المسار
|
| 9 |
+
sys.path.append(str(Path(__file__).parent.parent))
|
| 10 |
+
|
| 11 |
+
from utils.data_utils import validate_profile, create_default_profile, parse_profile_file
|
| 12 |
+
from utils.api_utils import generate_default_curriculum
|
| 13 |
+
import config
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_create_default_profile():
|
| 17 |
+
"""اختبار إنشاء ملف تعريفي افتراضي"""
|
| 18 |
+
profile = create_default_profile()
|
| 19 |
+
|
| 20 |
+
assert profile is not None
|
| 21 |
+
assert "name" in profile
|
| 22 |
+
assert "level" in profile
|
| 23 |
+
assert "available_hours" in profile
|
| 24 |
+
assert profile["available_hours"] == 10
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def test_validate_profile_success():
|
| 28 |
+
"""اختبار التحقق من ملف تعريفي صحيح"""
|
| 29 |
+
profile = {
|
| 30 |
+
"name": "أحمد",
|
| 31 |
+
"level": "متوسط",
|
| 32 |
+
"available_hours": 15
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
is_valid, errors = validate_profile(profile)
|
| 36 |
+
|
| 37 |
+
assert is_valid == True
|
| 38 |
+
assert len(errors) == 0
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_validate_profile_missing_fields():
|
| 42 |
+
"""اختبار التحقق من ملف تعريفي ناقص"""
|
| 43 |
+
profile = {
|
| 44 |
+
"name": "أحمد"
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
is_valid, errors = validate_profile(profile)
|
| 48 |
+
|
| 49 |
+
assert is_valid == False
|
| 50 |
+
assert len(errors) > 0
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_validate_profile_invalid_hours():
|
| 54 |
+
"""اختبار التحقق من ساعات غير صحيحة"""
|
| 55 |
+
profile = {
|
| 56 |
+
"name": "أحمد",
|
| 57 |
+
"level": "متوسط",
|
| 58 |
+
"available_hours": 50 # أكثر من الحد الأقصى
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
is_valid, errors = validate_profile(profile)
|
| 62 |
+
|
| 63 |
+
assert is_valid == False
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def test_parse_json_profile():
|
| 67 |
+
"""اختبار تحليل ملف JSON"""
|
| 68 |
+
json_content = '{"name": "سارة", "level": "مبتدئ", "available_hours": 10}'
|
| 69 |
+
|
| 70 |
+
profile = parse_profile_file(json_content, "json")
|
| 71 |
+
|
| 72 |
+
assert profile is not None
|
| 73 |
+
assert profile["name"] == "سارة"
|
| 74 |
+
assert profile["level"] == "مبتدئ"
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def test_generate_default_curriculum():
|
| 78 |
+
"""اختبار توليد منهج افتراضي"""
|
| 79 |
+
curriculum_result = generate_default_curriculum("ai_intro")
|
| 80 |
+
|
| 81 |
+
assert curriculum_result is not None
|
| 82 |
+
assert "curriculum" in curriculum_result
|
| 83 |
+
assert len(curriculum_result["curriculum"]) == 12 # 12 أسبوع
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def test_learning_hubs_config():
|
| 87 |
+
"""اختبار إعدادات المحاور"""
|
| 88 |
+
assert len(config.LEARNING_HUBS) > 0
|
| 89 |
+
|
| 90 |
+
for hub_id, hub_data in config.LEARNING_HUBS.items():
|
| 91 |
+
assert "name" in hub_data
|
| 92 |
+
assert "description" in hub_data
|
| 93 |
+
assert "icon" in hub_data
|
| 94 |
+
assert "duration_weeks" in hub_data
|
| 95 |
+
assert hub_data["duration_weeks"] == 12
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
if __name__ == "__main__":
|
| 99 |
+
pytest.main([__file__, "-v"])
|
utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# ملف لجعل utils مجلداً قابلاً للاستيراد
|
utils/api_utils.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
وظائف مساعدة للتعامل مع APIs (Hugging Face و Supabase)
|
| 3 |
+
"""
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
from typing import Dict, List, Optional
|
| 7 |
+
import config
|
| 8 |
+
|
| 9 |
+
def call_hf_api(prompt: str, max_tokens: int = 500, temperature: float = 0.7) -> Optional[str]:
|
| 10 |
+
"""
|
| 11 |
+
استدعاء Hugging Face Inference API
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
prompt: النص المُدخل للنموذج
|
| 15 |
+
max_tokens: الحد الأقصى للـ tokens في الرد
|
| 16 |
+
temperature: درجة العشوائية (0-1)
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
النص المُولّد أو None في حالة الخطأ
|
| 20 |
+
"""
|
| 21 |
+
if not config.HF_API_TOKEN:
|
| 22 |
+
return None
|
| 23 |
+
|
| 24 |
+
headers = {
|
| 25 |
+
"Authorization": f"Bearer {config.HF_API_TOKEN}",
|
| 26 |
+
"Content-Type": "application/json"
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
payload = {
|
| 30 |
+
"inputs": prompt,
|
| 31 |
+
"parameters": {
|
| 32 |
+
"max_new_tokens": max_tokens,
|
| 33 |
+
"temperature": temperature,
|
| 34 |
+
"top_p": 0.9,
|
| 35 |
+
"return_full_text": False
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
response = requests.post(
|
| 41 |
+
config.HF_API_URL,
|
| 42 |
+
headers=headers,
|
| 43 |
+
json=payload,
|
| 44 |
+
timeout=30
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
if response.status_code == 200:
|
| 48 |
+
result = response.json()
|
| 49 |
+
if isinstance(result, list) and len(result) > 0:
|
| 50 |
+
return result[0].get("generated_text", "")
|
| 51 |
+
return result.get("generated_text", "")
|
| 52 |
+
else:
|
| 53 |
+
print(f"خطأ في API: {response.status_code} - {response.text}")
|
| 54 |
+
return None
|
| 55 |
+
|
| 56 |
+
except Exception as e:
|
| 57 |
+
print(f"خطأ في استدعاء HF API: {str(e)}")
|
| 58 |
+
return None
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def generate_curriculum(user_profile: Dict, hub_id: str) -> Optional[Dict]:
|
| 62 |
+
"""
|
| 63 |
+
توليد منهج مخصص بناءً على ملف المستخدم
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
user_profile: بيانات المستخدم
|
| 67 |
+
hub_id: معرف المحور المختار
|
| 68 |
+
|
| 69 |
+
Returns:
|
| 70 |
+
المنهج المُولّد أو None
|
| 71 |
+
"""
|
| 72 |
+
hub = config.LEARNING_HUBS.get(hub_id, {})
|
| 73 |
+
|
| 74 |
+
prompt = f"""أنت مصمم مناهج تعليمية خبير. مهمتك توليد خطة تعلم لمدة {config.PLAN_DURATION_WEEKS} أسبوعاً.
|
| 75 |
+
|
| 76 |
+
معلومات المستخدم:
|
| 77 |
+
- الاسم: {user_profile.get('name', 'غير محدد')}
|
| 78 |
+
- المستوى: {user_profile.get('level', 'مبتدئ')}
|
| 79 |
+
- الوقت المتاح أسبوعياً: {user_profile.get('available_hours', 10)} ساعات
|
| 80 |
+
- أسلوب التعلم المفضل: {user_profile.get('learning_style', 'مختلط')}
|
| 81 |
+
|
| 82 |
+
المحور المطلوب: {hub.get('name', 'غير محدد')}
|
| 83 |
+
|
| 84 |
+
ولّد خطة تعلم بصيغة JSON تحتوي على 12 وحدة أسبوعية. كل وحدة يجب أن تحتوي على:
|
| 85 |
+
- week: رقم الأسبوع
|
| 86 |
+
- unit_title: عنوان الوحدة
|
| 87 |
+
- estimated_time_mins: الوقت المقدر بالدقائق
|
| 88 |
+
- lessons: قائمة بالدروس (كل درس له: title, duration_mins, type, description)
|
| 89 |
+
- experiment: تجربة عملية (title, description, deliverable)
|
| 90 |
+
|
| 91 |
+
مثال على البنية:
|
| 92 |
+
{{
|
| 93 |
+
"curriculum": [
|
| 94 |
+
{{
|
| 95 |
+
"week": 1,
|
| 96 |
+
"unit_title": "...",
|
| 97 |
+
"estimated_time_mins": 600,
|
| 98 |
+
"lessons": [...],
|
| 99 |
+
"experiment": {{...}}
|
| 100 |
+
}}
|
| 101 |
+
]
|
| 102 |
+
}}
|
| 103 |
+
|
| 104 |
+
ردّ فقط بصيغة JSON بدون أي نص إضافي."""
|
| 105 |
+
|
| 106 |
+
# محاولة استدعاء API
|
| 107 |
+
response = call_hf_api(prompt, max_tokens=2000, temperature=0.7)
|
| 108 |
+
|
| 109 |
+
if response:
|
| 110 |
+
try:
|
| 111 |
+
# محاولة استخراج JSON من الرد
|
| 112 |
+
json_start = response.find("{")
|
| 113 |
+
json_end = response.rfind("}") + 1
|
| 114 |
+
if json_start != -1 and json_end > json_start:
|
| 115 |
+
json_str = response[json_start:json_end]
|
| 116 |
+
return json.loads(json_str)
|
| 117 |
+
except:
|
| 118 |
+
pass
|
| 119 |
+
|
| 120 |
+
# في حالة الفشل، إرجاع منهج افتراضي
|
| 121 |
+
return generate_default_curriculum(hub_id)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def generate_default_curriculum(hub_id: str) -> Dict:
|
| 125 |
+
"""
|
| 126 |
+
توليد منهج افتراضي في حالة عدم توفر API
|
| 127 |
+
"""
|
| 128 |
+
hub = config.LEARNING_HUBS.get(hub_id, config.LEARNING_HUBS["ai_intro"])
|
| 129 |
+
|
| 130 |
+
curriculum = []
|
| 131 |
+
for week in range(1, 13):
|
| 132 |
+
unit = {
|
| 133 |
+
"week": week,
|
| 134 |
+
"unit_title": f"الوحدة {week}: {hub['name']} - جزء {week}",
|
| 135 |
+
"estimated_time_mins": 480,
|
| 136 |
+
"lessons": [
|
| 137 |
+
{
|
| 138 |
+
"title": f"درس {week}.1: المفاهيم الأساسية",
|
| 139 |
+
"duration_mins": 120,
|
| 140 |
+
"type": "concept",
|
| 141 |
+
"description": f"شرح المفاهيم الأساسية في الأسبوع {week}"
|
| 142 |
+
},
|
| 143 |
+
{
|
| 144 |
+
"title": f"درس {week}.2: التطبيق العملي",
|
| 145 |
+
"duration_mins": 180,
|
| 146 |
+
"type": "practical",
|
| 147 |
+
"description": f"تطبيق عملي على ما تم تعلمه"
|
| 148 |
+
}
|
| 149 |
+
],
|
| 150 |
+
"experiment": {
|
| 151 |
+
"title": f"تجربة الأسبوع {week}",
|
| 152 |
+
"description": f"مشروع تطبيقي لتعزيز الفهم",
|
| 153 |
+
"deliverable": "ملف أو تقرير"
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
curriculum.append(unit)
|
| 157 |
+
|
| 158 |
+
return {"curriculum": curriculum}
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def generate_lesson_content(lesson_title: str, user_context: Dict) -> str:
|
| 162 |
+
"""
|
| 163 |
+
توليد محتوى درس مفصل
|
| 164 |
+
"""
|
| 165 |
+
prompt = f"""أنت مدرس متخصص. اكتب درساً تعليمياً قصيراً (5-10 دقائق قراءة) عن:
|
| 166 |
+
|
| 167 |
+
{lesson_title}
|
| 168 |
+
|
| 169 |
+
الدرس يجب أن يتضمن:
|
| 170 |
+
1. الفكرة المركزية (ملخص في سطرين)
|
| 171 |
+
2. شرح مبسط مع أمثلة واقعية
|
| 172 |
+
3. نقاط رئيسية (3-5 نقاط)
|
| 173 |
+
4. سؤال للتفكير
|
| 174 |
+
5. مصادر للمتابعة (اختياري)
|
| 175 |
+
|
| 176 |
+
اكتب بأسلوب واضح ومشجع."""
|
| 177 |
+
|
| 178 |
+
response = call_hf_api(prompt, max_tokens=800)
|
| 179 |
+
|
| 180 |
+
if response:
|
| 181 |
+
return response
|
| 182 |
+
|
| 183 |
+
# محتوى افتراضي
|
| 184 |
+
return f"""# {lesson_title}
|
| 185 |
+
|
| 186 |
+
## الفكرة المركزية 💡
|
| 187 |
+
هذا درس تعليمي يهدف إلى تعريفك بالمفهوم الأساسي وتطبيقاته العملية.
|
| 188 |
+
|
| 189 |
+
## الشرح 📖
|
| 190 |
+
سنستكشف معاً هذا الموضوع المهم من خلال أمثلة واقعية وتطبيقات عملية تساعدك على الفهم العميق.
|
| 191 |
+
|
| 192 |
+
## النقاط الرئيسية ⭐
|
| 193 |
+
1. المفهوم الأساسي وأهميته
|
| 194 |
+
2. التطبيقات العملية في الحياة اليومية
|
| 195 |
+
3. خطوات التنفيذ والممارسة
|
| 196 |
+
4. الأخطاء الشائعة وكيفية تجنبها
|
| 197 |
+
5. الخطوات التالية للتعمق
|
| 198 |
+
|
| 199 |
+
## سؤال للتفكير 🤔
|
| 200 |
+
كيف يمكنك تطبيق ما تعلمته اليوم في مشروعك الشخصي أو عملك؟
|
| 201 |
+
|
| 202 |
+
## للمتابعة 📚
|
| 203 |
+
- ابحث عن أمثلة إضافية في مجالك
|
| 204 |
+
- مارس التطبيق العملي بشكل يومي
|
| 205 |
+
- شارك ما تعلمته مع الآخرين
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
# دوال Supabase (اختيارية - يمكن استبدالها بملفات JSON محلية)
|
| 210 |
+
def init_supabase():
|
| 211 |
+
"""تهيئة اتصال Supabase"""
|
| 212 |
+
if not config.USE_SUPABASE:
|
| 213 |
+
return None
|
| 214 |
+
|
| 215 |
+
try:
|
| 216 |
+
from supabase import create_client
|
| 217 |
+
return create_client(config.SUPABASE_URL, config.SUPABASE_KEY)
|
| 218 |
+
except:
|
| 219 |
+
return None
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def save_profile_to_supabase(profile: Dict) -> bool:
|
| 223 |
+
"""حفظ ملف تعريفي في Supabase"""
|
| 224 |
+
client = init_supabase()
|
| 225 |
+
if not client:
|
| 226 |
+
return False
|
| 227 |
+
|
| 228 |
+
try:
|
| 229 |
+
client.table("profiles").insert(profile).execute()
|
| 230 |
+
return True
|
| 231 |
+
except:
|
| 232 |
+
return False
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def get_profile_from_supabase(user_id: str) -> Optional[Dict]:
|
| 236 |
+
"""جلب ملف تعريفي من Supabase"""
|
| 237 |
+
client = init_supabase()
|
| 238 |
+
if not client:
|
| 239 |
+
return None
|
| 240 |
+
|
| 241 |
+
try:
|
| 242 |
+
response = client.table("profiles").select("*").eq("user_id", user_id).execute()
|
| 243 |
+
if response.data:
|
| 244 |
+
return response.data[0]
|
| 245 |
+
except:
|
| 246 |
+
pass
|
| 247 |
+
|
| 248 |
+
return None
|
utils/data_utils.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
وظائف معالجة البيانات (تحليل الملفات التعريفية، إدارة التقدم، إلخ)
|
| 3 |
+
"""
|
| 4 |
+
import json
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
from datetime import datetime, timedelta
|
| 7 |
+
import io
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def parse_profile_file(file_content: str, file_type: str = "json") -> Optional[Dict]:
|
| 11 |
+
"""
|
| 12 |
+
تحليل ملف تعريفي مرفوع
|
| 13 |
+
|
| 14 |
+
Args:
|
| 15 |
+
file_content: محتوى الملف
|
| 16 |
+
file_type: نوع الملف (json, txt, markdown)
|
| 17 |
+
|
| 18 |
+
Returns:
|
| 19 |
+
القاموس المُحلل أو None
|
| 20 |
+
"""
|
| 21 |
+
try:
|
| 22 |
+
if file_type == "json":
|
| 23 |
+
return json.loads(file_content)
|
| 24 |
+
|
| 25 |
+
elif file_type in ["txt", "markdown", "md"]:
|
| 26 |
+
# تحليل بسيط من نص حر
|
| 27 |
+
profile = {
|
| 28 |
+
"name": extract_field(file_content, ["name", "الاسم", "اسم"]),
|
| 29 |
+
"age": extract_field(file_content, ["age", "العمر", "عمر"]),
|
| 30 |
+
"level": extract_field(file_content, ["level", "المستوى", "مستوى"]),
|
| 31 |
+
"skills": extract_field(file_content, ["skills", "المهارات", "مهارات"]),
|
| 32 |
+
"available_hours": extract_field(file_content, ["hours", "ساعات", "وقت متاح"]),
|
| 33 |
+
"learning_style": extract_field(file_content, ["style", "أسلوب", "تفضيلات"]),
|
| 34 |
+
"goals": extract_field(file_content, ["goals", "أهداف", "هدف"]),
|
| 35 |
+
}
|
| 36 |
+
return profile
|
| 37 |
+
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"خطأ في تحليل الملف: {str(e)}")
|
| 40 |
+
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def extract_field(text: str, keywords: List[str]) -> str:
|
| 45 |
+
"""استخراج حقل من نص حر بناءً على كلمات مفتاحية"""
|
| 46 |
+
text_lower = text.lower()
|
| 47 |
+
|
| 48 |
+
for keyword in keywords:
|
| 49 |
+
if keyword.lower() in text_lower:
|
| 50 |
+
# البحث عن السطر الذي يحتوي على الكلمة المفتاحية
|
| 51 |
+
for line in text.split('\n'):
|
| 52 |
+
if keyword.lower() in line.lower():
|
| 53 |
+
# استخراج القيمة بعد النقطتين أو المساواة
|
| 54 |
+
if ':' in line:
|
| 55 |
+
return line.split(':', 1)[1].strip()
|
| 56 |
+
elif '=' in line:
|
| 57 |
+
return line.split('=', 1)[1].strip()
|
| 58 |
+
|
| 59 |
+
return ""
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def create_default_profile() -> Dict:
|
| 63 |
+
"""إنشاء ملف تعريفي افتراضي"""
|
| 64 |
+
return {
|
| 65 |
+
"name": "مستخدم جديد",
|
| 66 |
+
"age": "",
|
| 67 |
+
"level": "مبتدئ",
|
| 68 |
+
"skills": [],
|
| 69 |
+
"available_hours": 10,
|
| 70 |
+
"learning_style": "مختلط (نظري وعملي)",
|
| 71 |
+
"goals": "تعلم مهارات جديدة",
|
| 72 |
+
"preferences": {
|
| 73 |
+
"language": "ar",
|
| 74 |
+
"notifications": True,
|
| 75 |
+
"theme": "light"
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def validate_profile(profile: Dict) -> tuple[bool, List[str]]:
|
| 81 |
+
"""
|
| 82 |
+
التحقق من صحة الملف التعريفي
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
(هل صالح, قائمة بالأخطاء)
|
| 86 |
+
"""
|
| 87 |
+
errors = []
|
| 88 |
+
|
| 89 |
+
required_fields = ["name", "level", "available_hours"]
|
| 90 |
+
for field in required_fields:
|
| 91 |
+
if field not in profile or not profile[field]:
|
| 92 |
+
errors.append(f"الحقل '{field}' مطلوب")
|
| 93 |
+
|
| 94 |
+
if profile.get("available_hours"):
|
| 95 |
+
try:
|
| 96 |
+
hours = int(profile["available_hours"])
|
| 97 |
+
if hours < 1 or hours > 40:
|
| 98 |
+
errors.append("الوقت المتاح يجب أن يكون بين 1 و 40 ساعة أسبوعياً")
|
| 99 |
+
except:
|
| 100 |
+
errors.append("الوقت المتاح يجب أن يكون رقماً")
|
| 101 |
+
|
| 102 |
+
return len(errors) == 0, errors
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def generate_schedule(curriculum: List[Dict], available_hours: int) -> List[Dict]:
|
| 106 |
+
"""
|
| 107 |
+
توليد جدول زمني بناءً على المنهج والوقت المتاح
|
| 108 |
+
|
| 109 |
+
Args:
|
| 110 |
+
curriculum: المنهج الدراسي
|
| 111 |
+
available_hours: الساعات المتاحة أسبوعياً
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
جدول زمني مفصل
|
| 115 |
+
"""
|
| 116 |
+
schedule = []
|
| 117 |
+
start_date = datetime.now()
|
| 118 |
+
|
| 119 |
+
for week_num, unit in enumerate(curriculum, start=1):
|
| 120 |
+
week_start = start_date + timedelta(weeks=week_num - 1)
|
| 121 |
+
|
| 122 |
+
# توزيع الدروس على أيام الأسبوع
|
| 123 |
+
lessons = unit.get("lessons", [])
|
| 124 |
+
total_mins = sum(lesson.get("duration_mins", 60) for lesson in lessons)
|
| 125 |
+
|
| 126 |
+
# إضافة وقت التجربة
|
| 127 |
+
if unit.get("experiment"):
|
| 128 |
+
total_mins += 120 # افتراض ساعتين للتجربة
|
| 129 |
+
|
| 130 |
+
schedule_entry = {
|
| 131 |
+
"week": week_num,
|
| 132 |
+
"start_date": week_start.strftime("%Y-%m-%d"),
|
| 133 |
+
"unit_title": unit.get("unit_title", f"الوحدة {week_num}"),
|
| 134 |
+
"total_hours": round(total_mins / 60, 1),
|
| 135 |
+
"daily_breakdown": distribute_to_days(lessons, available_hours),
|
| 136 |
+
"experiment": unit.get("experiment"),
|
| 137 |
+
"status": "pending"
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
schedule.append(schedule_entry)
|
| 141 |
+
|
| 142 |
+
return schedule
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def distribute_to_days(lessons: List[Dict], hours_per_week: int) -> List[Dict]:
|
| 146 |
+
"""توزيع الدروس على أيام الأسبوع"""
|
| 147 |
+
days = ["الأحد", "الاثنين", "الثلاثاء", "الأربعاء", "الخميس"]
|
| 148 |
+
distribution = []
|
| 149 |
+
|
| 150 |
+
mins_per_day = (hours_per_week * 60) // len(days)
|
| 151 |
+
|
| 152 |
+
current_day = 0
|
| 153 |
+
current_day_mins = 0
|
| 154 |
+
current_day_lessons = []
|
| 155 |
+
|
| 156 |
+
for lesson in lessons:
|
| 157 |
+
lesson_mins = lesson.get("duration_mins", 60)
|
| 158 |
+
|
| 159 |
+
if current_day_mins + lesson_mins > mins_per_day and current_day_lessons:
|
| 160 |
+
# حفظ اليوم الحالي والانتقال للتالي
|
| 161 |
+
distribution.append({
|
| 162 |
+
"day": days[current_day % len(days)],
|
| 163 |
+
"lessons": current_day_lessons.copy(),
|
| 164 |
+
"total_mins": current_day_mins
|
| 165 |
+
})
|
| 166 |
+
current_day += 1
|
| 167 |
+
current_day_mins = 0
|
| 168 |
+
current_day_lessons = []
|
| 169 |
+
|
| 170 |
+
current_day_lessons.append(lesson)
|
| 171 |
+
current_day_mins += lesson_mins
|
| 172 |
+
|
| 173 |
+
# إضافة آخر يوم
|
| 174 |
+
if current_day_lessons:
|
| 175 |
+
distribution.append({
|
| 176 |
+
"day": days[current_day % len(days)],
|
| 177 |
+
"lessons": current_day_lessons,
|
| 178 |
+
"total_mins": current_day_mins
|
| 179 |
+
})
|
| 180 |
+
|
| 181 |
+
return distribution
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def calculate_progress(completed_units: List[int], total_units: int) -> Dict:
|
| 185 |
+
"""
|
| 186 |
+
حساب التقدم الإجمالي
|
| 187 |
+
|
| 188 |
+
Returns:
|
| 189 |
+
معلومات التقدم
|
| 190 |
+
"""
|
| 191 |
+
completed = len(completed_units)
|
| 192 |
+
percentage = (completed / total_units * 100) if total_units > 0 else 0
|
| 193 |
+
|
| 194 |
+
return {
|
| 195 |
+
"completed_units": completed,
|
| 196 |
+
"total_units": total_units,
|
| 197 |
+
"percentage": round(percentage, 1),
|
| 198 |
+
"remaining_units": total_units - completed,
|
| 199 |
+
"status": get_progress_status(percentage)
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def get_progress_status(percentage: float) -> str:
|
| 204 |
+
"""تحديد حالة التقدم"""
|
| 205 |
+
if percentage < 25:
|
| 206 |
+
return "بداية الرحلة 🌱"
|
| 207 |
+
elif percentage < 50:
|
| 208 |
+
return "في الطريق 🚀"
|
| 209 |
+
elif percentage < 75:
|
| 210 |
+
return "تقدم ممتاز 🌟"
|
| 211 |
+
elif percentage < 100:
|
| 212 |
+
return "قرب الإنجاز 🎯"
|
| 213 |
+
else:
|
| 214 |
+
return "مكتمل! 🎉"
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def export_profile_to_json(profile: Dict) -> str:
|
| 218 |
+
"""تصدير الملف التعريفي إلى JSON"""
|
| 219 |
+
return json.dumps(profile, ensure_ascii=False, indent=2)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def export_progress_to_json(progress_data: Dict) -> str:
|
| 223 |
+
"""تصدير بيانات التقدم إلى JSON"""
|
| 224 |
+
return json.dumps(progress_data, ensure_ascii=False, indent=2)
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def load_local_data(filename: str) -> Optional[Dict]:
|
| 228 |
+
"""تحميل بيانات من ملف JSON محلي"""
|
| 229 |
+
try:
|
| 230 |
+
with open(f"data/{filename}", "r", encoding="utf-8") as f:
|
| 231 |
+
return json.load(f)
|
| 232 |
+
except:
|
| 233 |
+
return None
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def save_local_data(filename: str, data: Dict) -> bool:
|
| 237 |
+
"""حفظ بيانات في ملف JSON محلي"""
|
| 238 |
+
try:
|
| 239 |
+
with open(f"data/{filename}", "w", encoding="utf-8") as f:
|
| 240 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
| 241 |
+
return True
|
| 242 |
+
except:
|
| 243 |
+
return False
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def get_completion_stats(completed_lessons: List[str], curriculum: List[Dict]) -> Dict:
|
| 247 |
+
"""حساب إحصائيات الإنجاز المفصلة"""
|
| 248 |
+
total_lessons = sum(len(unit.get("lessons", [])) for unit in curriculum)
|
| 249 |
+
completed = len(completed_lessons)
|
| 250 |
+
|
| 251 |
+
total_experiments = len([u for u in curriculum if u.get("experiment")])
|
| 252 |
+
completed_experiments = 0 # يمكن تتبعها بشكل منفصل
|
| 253 |
+
|
| 254 |
+
return {
|
| 255 |
+
"lessons": {
|
| 256 |
+
"completed": completed,
|
| 257 |
+
"total": total_lessons,
|
| 258 |
+
"percentage": round(completed / total_lessons * 100, 1) if total_lessons > 0 else 0
|
| 259 |
+
},
|
| 260 |
+
"experiments": {
|
| 261 |
+
"completed": completed_experiments,
|
| 262 |
+
"total": total_experiments,
|
| 263 |
+
"percentage": round(completed_experiments / total_experiments * 100, 1) if total_experiments > 0 else 0
|
| 264 |
+
},
|
| 265 |
+
"overall_percentage": round((completed + completed_experiments) / (total_lessons + total_experiments) * 100, 1)
|
| 266 |
+
}
|
utils/ui_utils.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
وظائف تخصيص واجهة المستخدم (UI utilities)
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
import config
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def load_custom_css():
|
| 10 |
+
"""تحميل CSS مخصص"""
|
| 11 |
+
try:
|
| 12 |
+
with open("assets/css/style.css", "r", encoding="utf-8") as f:
|
| 13 |
+
st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
|
| 14 |
+
except:
|
| 15 |
+
# CSS افتراضي في حالة عدم وجود الملف
|
| 16 |
+
st.markdown("""
|
| 17 |
+
<style>
|
| 18 |
+
@import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@400;500;700&display=swap');
|
| 19 |
+
|
| 20 |
+
* {
|
| 21 |
+
font-family: 'Tajawal', sans-serif !important;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.main {
|
| 25 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 26 |
+
background-attachment: fixed;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.block-container {
|
| 30 |
+
background: rgba(255, 255, 255, 0.95);
|
| 31 |
+
border-radius: 20px;
|
| 32 |
+
padding: 2rem;
|
| 33 |
+
margin-top: 2rem;
|
| 34 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
h1, h2, h3 {
|
| 38 |
+
color: #667eea;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.stButton > button {
|
| 42 |
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
| 43 |
+
color: white;
|
| 44 |
+
border: none;
|
| 45 |
+
border-radius: 10px;
|
| 46 |
+
padding: 0.75rem 2rem;
|
| 47 |
+
font-size: 1.1rem;
|
| 48 |
+
font-weight: 600;
|
| 49 |
+
transition: all 0.3s ease;
|
| 50 |
+
width: 100%;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.stButton > button:hover {
|
| 54 |
+
transform: translateY(-2px);
|
| 55 |
+
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.success-box {
|
| 59 |
+
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
|
| 60 |
+
color: white;
|
| 61 |
+
padding: 1rem;
|
| 62 |
+
border-radius: 10px;
|
| 63 |
+
margin: 1rem 0;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.info-box {
|
| 67 |
+
background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
|
| 68 |
+
color: white;
|
| 69 |
+
padding: 1rem;
|
| 70 |
+
border-radius: 10px;
|
| 71 |
+
margin: 1rem 0;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.warning-box {
|
| 75 |
+
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
|
| 76 |
+
color: white;
|
| 77 |
+
padding: 1rem;
|
| 78 |
+
border-radius: 10px;
|
| 79 |
+
margin: 1rem 0;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.card {
|
| 83 |
+
background: white;
|
| 84 |
+
border-radius: 15px;
|
| 85 |
+
padding: 1.5rem;
|
| 86 |
+
margin: 1rem 0;
|
| 87 |
+
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
| 88 |
+
transition: all 0.3s ease;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.card:hover {
|
| 92 |
+
transform: translateY(-5px);
|
| 93 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.progress-bar {
|
| 97 |
+
background: #e2e8f0;
|
| 98 |
+
border-radius: 10px;
|
| 99 |
+
height: 25px;
|
| 100 |
+
overflow: hidden;
|
| 101 |
+
margin: 1rem 0;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.progress-fill {
|
| 105 |
+
background: linear-gradient(90deg, #48bb78 0%, #38a169 100%);
|
| 106 |
+
height: 100%;
|
| 107 |
+
display: flex;
|
| 108 |
+
align-items: center;
|
| 109 |
+
justify-content: center;
|
| 110 |
+
color: white;
|
| 111 |
+
font-weight: 600;
|
| 112 |
+
transition: width 0.5s ease;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.badge {
|
| 116 |
+
display: inline-block;
|
| 117 |
+
padding: 0.35rem 0.75rem;
|
| 118 |
+
border-radius: 20px;
|
| 119 |
+
font-size: 0.875rem;
|
| 120 |
+
font-weight: 600;
|
| 121 |
+
margin: 0.25rem;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.badge-primary {
|
| 125 |
+
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
| 126 |
+
color: white;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.badge-success {
|
| 130 |
+
background: #48bb78;
|
| 131 |
+
color: white;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.badge-warning {
|
| 135 |
+
background: #ed8936;
|
| 136 |
+
color: white;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.badge-info {
|
| 140 |
+
background: #4299e1;
|
| 141 |
+
color: white;
|
| 142 |
+
}
|
| 143 |
+
</style>
|
| 144 |
+
""", unsafe_allow_html=True)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def show_hero_section(title: str, description: str):
|
| 148 |
+
"""عرض قسم Hero جميل"""
|
| 149 |
+
st.markdown(f"""
|
| 150 |
+
<div style='text-align: center; padding: 3rem 1rem;'>
|
| 151 |
+
<h1 style='font-size: 3em; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
| 152 |
+
-webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 0.5rem;'>
|
| 153 |
+
{title}
|
| 154 |
+
</h1>
|
| 155 |
+
<p style='font-size: 1.3rem; color: #4a5568; margin-top: 0;'>
|
| 156 |
+
{description}
|
| 157 |
+
</p>
|
| 158 |
+
</div>
|
| 159 |
+
""", unsafe_allow_html=True)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def show_card(title: str, content: str, icon: str = "📚"):
|
| 163 |
+
"""عرض بطاقة جميلة"""
|
| 164 |
+
st.markdown(f"""
|
| 165 |
+
<div class='card'>
|
| 166 |
+
<h3 style='margin-top: 0;'>{icon} {title}</h3>
|
| 167 |
+
<p style='color: #4a5568;'>{content}</p>
|
| 168 |
+
</div>
|
| 169 |
+
""", unsafe_allow_html=True)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def show_feature_cards(features: List[Dict]):
|
| 173 |
+
"""عرض بطاقات الميزات في أعمدة"""
|
| 174 |
+
cols = st.columns(len(features))
|
| 175 |
+
|
| 176 |
+
for col, feature in zip(cols, features):
|
| 177 |
+
with col:
|
| 178 |
+
st.markdown(f"""
|
| 179 |
+
<div class='card' style='text-align: center;'>
|
| 180 |
+
<div style='font-size: 3rem; margin-bottom: 0.5rem;'>{feature.get('icon', '✨')}</div>
|
| 181 |
+
<h3 style='margin: 0.5rem 0;'>{feature.get('title', '')}</h3>
|
| 182 |
+
<p style='color: #718096; font-size: 0.95rem;'>{feature.get('description', '')}</p>
|
| 183 |
+
</div>
|
| 184 |
+
""", unsafe_allow_html=True)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def show_progress_bar(percentage: float, label: str = ""):
|
| 188 |
+
"""عرض شريط تقدم جميل"""
|
| 189 |
+
st.markdown(f"""
|
| 190 |
+
<div class='progress-bar'>
|
| 191 |
+
<div class='progress-fill' style='width: {percentage}%;'>
|
| 192 |
+
{percentage:.1f}%
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
{f'<p style="text-align: center; color: #4a5568; margin-top: 0.5rem;">{label}</p>' if label else ''}
|
| 196 |
+
""", unsafe_allow_html=True)
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def show_badge(text: str, badge_type: str = "primary"):
|
| 200 |
+
"""عرض شارة ملونة"""
|
| 201 |
+
return f"<span class='badge badge-{badge_type}'>{text}</span>"
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def show_success_message(message: str):
|
| 205 |
+
"""رسالة نجاح جميلة"""
|
| 206 |
+
st.markdown(f"""
|
| 207 |
+
<div class='success-box'>
|
| 208 |
+
<strong>✅ {message}</strong>
|
| 209 |
+
</div>
|
| 210 |
+
""", unsafe_allow_html=True)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def show_info_message(message: str):
|
| 214 |
+
"""رسالة معلومات جميلة"""
|
| 215 |
+
st.markdown(f"""
|
| 216 |
+
<div class='info-box'>
|
| 217 |
+
<strong>ℹ️ {message}</strong>
|
| 218 |
+
</div>
|
| 219 |
+
""", unsafe_allow_html=True)
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def show_warning_message(message: str):
|
| 223 |
+
"""رسالة تحذير جميلة"""
|
| 224 |
+
st.markdown(f"""
|
| 225 |
+
<div class='warning-box'>
|
| 226 |
+
<strong>⚠️ {message}</strong>
|
| 227 |
+
</div>
|
| 228 |
+
""", unsafe_allow_html=True)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def create_stat_card(label: str, value: str, icon: str = "📊"):
|
| 232 |
+
"""إنشاء بطاقة إحصائية"""
|
| 233 |
+
return f"""
|
| 234 |
+
<div class='card' style='text-align: center;'>
|
| 235 |
+
<div style='font-size: 2.5rem;'>{icon}</div>
|
| 236 |
+
<h2 style='margin: 0.5rem 0; color: #667eea;'>{value}</h2>
|
| 237 |
+
<p style='color: #718096; margin: 0;'>{label}</p>
|
| 238 |
+
</div>
|
| 239 |
+
"""
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def show_timeline(events: List[Dict]):
|
| 243 |
+
"""عرض خط زمني للأحداث"""
|
| 244 |
+
for event in events:
|
| 245 |
+
status_icon = "✅" if event.get("completed") else "⏳"
|
| 246 |
+
st.markdown(f"""
|
| 247 |
+
<div class='card' style='border-right: 4px solid {"#48bb78" if event.get("completed") else "#cbd5e0"};'>
|
| 248 |
+
<div style='display: flex; align-items: center; gap: 1rem;'>
|
| 249 |
+
<div style='font-size: 2rem;'>{status_icon}</div>
|
| 250 |
+
<div style='flex: 1;'>
|
| 251 |
+
<h4 style='margin: 0; color: #2d3748;'>{event.get('title', '')}</h4>
|
| 252 |
+
<p style='margin: 0.25rem 0 0 0; color: #718096; font-size: 0.9rem;'>
|
| 253 |
+
{event.get('date', '')}
|
| 254 |
+
</p>
|
| 255 |
+
</div>
|
| 256 |
+
</div>
|
| 257 |
+
</div>
|
| 258 |
+
""", unsafe_allow_html=True)
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def show_hub_selector(hubs: Dict) -> Optional[str]:
|
| 262 |
+
"""عرض محدد المحاور التعليمية"""
|
| 263 |
+
st.markdown("### 🎯 اختر محورك التعليمي")
|
| 264 |
+
|
| 265 |
+
selected_hub = None
|
| 266 |
+
cols = st.columns(2)
|
| 267 |
+
|
| 268 |
+
hub_items = list(hubs.items())
|
| 269 |
+
for idx, (hub_id, hub_data) in enumerate(hub_items):
|
| 270 |
+
col = cols[idx % 2]
|
| 271 |
+
with col:
|
| 272 |
+
if st.button(
|
| 273 |
+
f"{hub_data['icon']} {hub_data['name']}\n\n{hub_data['description']}",
|
| 274 |
+
key=f"hub_{hub_id}",
|
| 275 |
+
use_container_width=True
|
| 276 |
+
):
|
| 277 |
+
selected_hub = hub_id
|
| 278 |
+
|
| 279 |
+
return selected_hub
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def init_session_state(defaults: Dict):
|
| 283 |
+
"""تهيئة session state بقيم افتراضية"""
|
| 284 |
+
for key, value in defaults.items():
|
| 285 |
+
if key not in st.session_state:
|
| 286 |
+
st.session_state[key] = value
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def show_lesson_card(lesson: Dict, lesson_id: str, is_completed: bool = False):
|
| 290 |
+
"""عرض بطاقة درس"""
|
| 291 |
+
status_color = "#48bb78" if is_completed else "#cbd5e0"
|
| 292 |
+
status_text = "مكتمل ✅" if is_completed else "قيد الانتظار ⏳"
|
| 293 |
+
|
| 294 |
+
st.markdown(f"""
|
| 295 |
+
<div class='card' style='border-right: 4px solid {status_color};'>
|
| 296 |
+
<div style='display: flex; justify-content: space-between; align-items: start;'>
|
| 297 |
+
<div style='flex: 1;'>
|
| 298 |
+
<h4 style='margin: 0 0 0.5rem 0; color: #2d3748;'>{lesson.get('title', '')}</h4>
|
| 299 |
+
<p style='color: #718096; margin: 0.25rem 0;'>{lesson.get('description', '')}</p>
|
| 300 |
+
<div style='margin-top: 0.75rem;'>
|
| 301 |
+
<span class='badge badge-info'>⏱️ {lesson.get('duration_mins', 0)} دقيقة</span>
|
| 302 |
+
<span class='badge badge-primary'>{lesson.get('type', 'درس')}</span>
|
| 303 |
+
</div>
|
| 304 |
+
</div>
|
| 305 |
+
<div style='text-align: left; min-width: 100px;'>
|
| 306 |
+
<span class='badge badge-{"success" if is_completed else "warning"}'>{status_text}</span>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
</div>
|
| 310 |
+
""", unsafe_allow_html=True)
|