EngReem85 commited on
Commit
64e43b9
·
verified ·
1 Parent(s): 5ef0bff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +360 -696
app.py CHANGED
@@ -7,36 +7,29 @@ import os
7
  import json
8
  import logging
9
  from datetime import datetime, timedelta
10
- from functools import lru_cache
11
  from huggingface_hub import HfApi, hf_hub_download
12
- import tempfile
13
  import shutil
14
- import time
15
 
16
  # ==================================================
17
  # الإعدادات الأساسية
18
  # ==================================================
19
 
20
  DB_NAME = "testimonials.db"
21
- REPO_ID = os.getenv("SPACE_ID", "your-username/your-space-name") # غيّر هذا
22
  TOKEN = os.getenv("HF_TOKEN")
23
 
24
- # إعداد التسجيل (Logging)
25
  logging.basicConfig(level=logging.INFO)
26
  logger = logging.getLogger(__name__)
27
 
28
  # ==================================================
29
- # إدارة قاعدة البيانات مع حفظ على Hugging Face
30
  # ==================================================
31
 
32
  def download_db_from_hub():
33
  """تحميل قاعدة البيانات من Hugging Face Hub"""
34
  try:
35
  if not TOKEN:
36
- logger.warning("HF_TOKEN غير موجود، سيتم إنشاء قاعدة بيانات جديدة")
37
  return False
38
-
39
- # محاولة تحميل الملف
40
  local_path = hf_hub_download(
41
  repo_id=REPO_ID,
42
  filename=DB_NAME,
@@ -44,32 +37,20 @@ def download_db_from_hub():
44
  local_dir="./",
45
  local_dir_use_symlinks=False
46
  )
47
-
48
- # نسخ الملف إلى المكان الصحيح
49
  if local_path != DB_NAME:
50
  shutil.copy2(local_path, DB_NAME)
51
-
52
  logger.info("تم تحميل قاعدة البيانات من Hub")
53
  return True
54
  except Exception as e:
55
- logger.warning(f"لم يتم العثور على قاعدة بيانات في Hub: {e}")
56
  return False
57
 
58
  def upload_db_to_hub():
59
  """رفع قاعدة البيانات إلى Hugging Face Hub"""
60
  try:
61
- if not TOKEN:
62
- logger.warning("HF_TOKEN غير موجود، لن يتم رفع قاعدة البيانات")
63
  return False
64
-
65
- # التحقق من وجود الملف
66
- if not os.path.exists(DB_NAME):
67
- logger.warning("قاعدة البيانات غير موجودة")
68
- return False
69
-
70
  api = HfApi()
71
-
72
- # رفع الملف
73
  api.upload_file(
74
  path_or_fileobj=DB_NAME,
75
  path_in_repo=DB_NAME,
@@ -77,7 +58,6 @@ def upload_db_to_hub():
77
  token=TOKEN,
78
  repo_type="space"
79
  )
80
-
81
  logger.info("تم رفع قاعدة البيانات إلى Hub")
82
  return True
83
  except Exception as e:
@@ -89,15 +69,12 @@ def upload_db_to_hub():
89
  # ==================================================
90
 
91
  def get_connection():
92
- """إنشاء اتصال بقاعدة البيانات"""
93
  return sqlite3.connect(DB_NAME, check_same_thread=False)
94
 
95
  def init_db():
96
- """تهيئة قاعدة البيانات مع الفهارس"""
97
  conn = get_connection()
98
  c = conn.cursor()
99
 
100
- # إنشاء جدول الآراء
101
  c.execute("""
102
  CREATE TABLE IF NOT EXISTS testimonials (
103
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -113,7 +90,6 @@ def init_db():
113
  )
114
  """)
115
 
116
- # إنشاء جدول الإعجابات
117
  c.execute("""
118
  CREATE TABLE IF NOT EXISTS likes (
119
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -124,25 +100,21 @@ def init_db():
124
  )
125
  """)
126
 
127
- # إضافة الفهارس لتحسين الأداء
128
  c.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON testimonials(timestamp)")
129
  c.execute("CREATE INDEX IF NOT EXISTS idx_ip_hash ON testimonials(ip_hash)")
130
  c.execute("CREATE INDEX IF NOT EXISTS idx_highlighted ON testimonials(is_highlighted)")
131
  c.execute("CREATE INDEX IF NOT EXISTS idx_rating ON testimonials(rating)")
132
- c.execute("CREATE INDEX IF NOT EXISTS idx_likes ON testimonials(likes)")
133
- c.execute("CREATE INDEX IF NOT EXISTS idx_likes_testimonial ON likes(testimonial_id)")
134
 
135
  conn.commit()
136
  conn.close()
137
-
138
- logger.info("تم تهيئة قاعدة البيانات بنجاح")
139
 
140
  # ==================================================
141
  # الأدوات المساعدة
142
  # ==================================================
143
 
144
  def clean_text(text):
145
- """تنظيف النص من المسافات الزائدة"""
146
  if not text:
147
  return ""
148
  text = text.strip()
@@ -150,141 +122,164 @@ def clean_text(text):
150
  return text
151
 
152
  def contains_bad_words(text):
153
- """التحقق من وجود كلمات ممنوعة"""
154
- bad_words = [
155
- "يلعن", "تبن", "سافل", "كلب", "خنزير", "عاهرة",
156
- "قحبة", "منيك", "كس", "زبر", "شرموطة", "متناكة"
157
- ]
158
- # تحسين الفلترة باستخدام تعبيرات منتظمة
159
  pattern = r'\b(' + '|'.join(bad_words) + r')\b'
160
  return bool(re.search(pattern, text.lower()))
161
 
162
  def escape_safe(text):
163
- """تنقية النص من HTML"""
164
- if not text:
165
- return ""
166
- return html.escape(text)
167
 
168
  def get_ip_hash(request: gr.Request):
169
- """الحصول على هاش عنوان IP"""
170
  try:
171
  if request and hasattr(request, 'client') and request.client:
172
- client_ip = request.client.host
173
- return hashlib.sha256(client_ip.encode()).hexdigest()[:16]
174
  except:
175
  pass
176
  return "unknown"
177
 
178
  def detect_sentiment(text):
179
- """تحليل المشاعر (نسخة محسنة)"""
180
- positive_words = [
181
- "رائع", "ممتاز", "جميل", "احترافي", "مفيد", "مذهل",
182
- "أفضل", "رائعة", "ممتازة", "جميلة", "مفيدة", "مذهلة",
183
- "خيال", "ممتاز", "متميز", "مبدع", "رائع", "لطيف"
184
- ]
185
- negative_words = [
186
- "سيء", "ضعيف", "فاشل", "سيئة", "ضعيفة", "فاشلة",
187
- "مخيب", "مخيبة", "فظيع", "فظيعة", "سخيف", "تافه"
188
- ]
189
 
190
  text_lower = text.lower()
191
- positive_score = sum(1 for word in positive_words if word in text_lower)
192
- negative_score = sum(1 for word in negative_words if word in text_lower)
193
 
194
- if positive_score > negative_score:
195
  return "إيجابي 😊"
196
- elif negative_score > positive_score:
197
  return "سلبي 😕"
198
  return "محايد 😐"
199
 
200
  def format_time(timestamp):
201
- """تنسيق الوقت بطريقة مفهومة"""
202
  try:
203
  dt = datetime.fromisoformat(timestamp)
204
- now = datetime.now()
205
- diff = now - dt
206
 
207
  if diff.days > 7:
208
  return dt.strftime("%Y/%m/%d")
209
  elif diff.days > 0:
210
  return f"منذ {diff.days} يوم"
211
  elif diff.seconds >= 3600:
212
- hours = diff.seconds // 3600
213
- return f"منذ {hours} ساعة"
214
  elif diff.seconds >= 60:
215
- minutes = diff.seconds // 60
216
- return f"منذ {minutes} دقيقة"
217
  return "الآن"
218
  except:
219
  return "غير معروف"
220
 
221
  # ==================================================
222
- # إدارة الإعجابات
223
  # ==================================================
224
 
225
- def toggle_like(testimonial_id, request: gr.Request):
226
- """تبديل حالة الإعجاب"""
 
 
 
227
  try:
228
- ip_hash = get_ip_hash(request)
229
- conn = get_connection()
230
- c = conn.cursor()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
- try:
233
- # التحقق إذا كان المستخدم قد أعجب مسبقاً
234
- c.execute("""
235
- SELECT id FROM likes
236
- WHERE testimonial_id = ? AND ip_hash = ?
237
- """, (testimonial_id, ip_hash))
238
-
239
- if c.fetchone():
240
- # إلغاء الإعجاب
241
- c.execute("DELETE FROM likes WHERE testimonial_id = ? AND ip_hash = ?",
242
- (testimonial_id, ip_hash))
243
- c.execute("UPDATE testimonials SET likes = likes - 1 WHERE id = ?",
244
- (testimonial_id,))
245
- message = "👎 تم إلغاء الإعجاب"
246
- else:
247
- # إضافة إعجاب
248
- c.execute("INSERT INTO likes (testimonial_id, ip_hash, created_at) VALUES (?, ?, ?)",
249
- (testimonial_id, ip_hash, datetime.now().isoformat()))
250
- c.execute("UPDATE testimonials SET likes = likes + 1 WHERE id = ?",
251
- (testimonial_id,))
252
- message = "👍 تم الإعجاب"
253
-
254
- conn.commit()
255
-
256
- # رفع قاعدة البيانات بعد التغيير
257
- upload_db_to_hub()
258
-
259
- # إعادة تحميل العرض
260
- return message, update_display()
261
-
262
- except Exception as e:
263
- logger.error(f"خطأ في معالجة الإعجاب: {e}")
264
- return "⚠️ حدث خطأ", update_display()
265
- finally:
266
- conn.close()
267
-
268
  except Exception as e:
269
- logger.error(f"خطأ في معالجة الإعجاب: {e}")
270
- return "⚠️ حدث خطأ", update_display()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
  # ==================================================
273
- # إضافة رأي جديد
274
  # ==================================================
275
 
276
- def add_testimonial(
277
- name,
278
- role,
279
- content,
280
- rating,
281
- request: gr.Request
282
- ):
283
- """إضافة رأي جديد مع التحقق من البريد المزعج"""
284
-
285
  content = clean_text(content)
286
 
287
- # التحقق من صحة المحتوى
288
  if not content:
289
  return "❌ الرجاء كتابة رأيك", "", "", update_display()
290
 
@@ -300,58 +295,37 @@ def add_testimonial(
300
 
301
  # مكافحة البريد المزعج
302
  one_hour_ago = (datetime.now() - timedelta(hours=1)).isoformat()
303
- c.execute("""
304
- SELECT COUNT(*) FROM testimonials
305
- WHERE ip_hash = ? AND timestamp >= ?
306
- """, (ip_hash, one_hour_ago))
307
 
308
  if c.fetchone()[0] >= 3:
309
  conn.close()
310
- return "❌ وصلت الحد المسموح حالياً", "", "", update_display()
311
 
312
- # تنظيف البيانات
313
  name = clean_text(name) or "مجهول"
314
  role = clean_text(role) or ""
315
-
316
- # تحليل المشاعر
317
  sentiment = detect_sentiment(content)
318
 
319
- # إدخال البيانات
320
  c.execute("""
321
- INSERT INTO testimonials (
322
- name, role, content, sentiment, rating,
323
- timestamp, likes, is_highlighted, ip_hash
324
- )
325
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
326
- """, (
327
- name, role, content, sentiment, int(rating),
328
- datetime.now().isoformat(), 0, 0, ip_hash
329
- ))
330
 
331
  conn.commit()
332
  conn.close()
333
 
334
- # حفظ التغييرات في Hugging Face Hub
335
  upload_db_to_hub()
336
-
337
- return "✅ تم نشر رأيك بنجاح", "", "", update_display()
338
 
339
  # ==================================================
340
- # جلب الآراء مع ترقيم الصفحات
341
  # ==================================================
342
 
343
- def get_testimonials(filter_type="all", sort_by="newest", page=1, per_page=10):
344
- """جلب الآراء مع دعم التصفية والترتيب وترقيم الصفحات"""
345
  conn = get_connection()
346
  c = conn.cursor()
347
 
348
- query = """
349
- SELECT
350
- id, name, role, content, sentiment,
351
- rating, timestamp, likes, is_highlighted
352
- FROM testimonials
353
- """
354
-
355
  conditions = []
356
  params = []
357
 
@@ -368,7 +342,6 @@ def get_testimonials(filter_type="all", sort_by="newest", page=1, per_page=10):
368
  else:
369
  query += " ORDER BY timestamp DESC"
370
 
371
- # إضافة ترقيم الصفحات
372
  offset = (page - 1) * per_page
373
  query += " LIMIT ? OFFSET ?"
374
  params.extend([per_page, offset])
@@ -376,18 +349,14 @@ def get_testimonials(filter_type="all", sort_by="newest", page=1, per_page=10):
376
  c.execute(query, params)
377
  rows = c.fetchall()
378
 
379
- # جلب العدد الإجمالي
380
- count_query = """
381
- SELECT COUNT(*) FROM testimonials
382
- """
383
  if conditions:
384
  count_query += " WHERE " + " AND ".join(conditions)
385
-
386
  c.execute(count_query)
387
  total = c.fetchone()[0]
388
 
389
  conn.close()
390
-
391
  return rows, total, (total + per_page - 1) // per_page if total > 0 else 1
392
 
393
  # ==================================================
@@ -395,114 +364,40 @@ def get_testimonials(filter_type="all", sort_by="newest", page=1, per_page=10):
395
  # ==================================================
396
 
397
  def render_card(row):
398
- """إنشاء بطاقة عرض الرأي"""
399
- (
400
- testimonial_id, name, role, content, sentiment,
401
- rating, timestamp, likes, is_highlighted
402
- ) = row
403
-
404
- name = escape_safe(name)
405
- role = escape_safe(role)
406
- content = escape_safe(content)
407
-
408
- stars = "⭐" * int(rating)
409
 
410
  highlight_style = ""
411
  if is_highlighted:
412
- highlight_style = """
413
- border-right:5px solid #f59e0b;
414
- background:#fffbeb;
415
- """
416
 
417
- role_html = ""
418
- if role:
419
- role_html = f"""
420
- <span style="
421
- background:#eef2ff;
422
- color:#4338ca;
423
- padding:0.25rem 0.7rem;
424
- border-radius:999px;
425
- font-size:0.75rem;
426
- ">
427
- {role}
428
- </span>
429
- """
430
 
431
- # استخدام onclick مع JavaScript
432
  return f"""
433
- <div class="testimonial-card" style="
434
  background:white;
435
- border-radius:24px;
436
- padding:1.5rem;
437
- margin-bottom:1rem;
438
  border:1px solid #e2e8f0;
439
- box-shadow:0 4px 12px rgba(0,0,0,0.05);
440
- transition: all 0.3s ease;
441
  {highlight_style}
 
442
  ">
443
- <div style="
444
- display:flex;
445
- justify-content:space-between;
446
- align-items:center;
447
- flex-wrap:wrap;
448
- gap:1rem;
449
- ">
450
- <div>
451
- <div style="
452
- font-size:1rem;
453
- font-weight:700;
454
- color:#0f172a;
455
- ">
456
- {name}
457
- </div>
458
- <div style="
459
- display:flex;
460
- gap:0.5rem;
461
- margin-top:0.5rem;
462
- align-items:center;
463
- flex-wrap:wrap;
464
- ">
465
- {role_html}
466
- <span style="
467
- color:#64748b;
468
- font-size:0.8rem;
469
- ">
470
- {sentiment}
471
- </span>
472
- </div>
473
- </div>
474
- <div style="
475
- color:#94a3b8;
476
- font-size:0.75rem;
477
- ">
478
- {format_time(timestamp)}
479
  </div>
 
480
  </div>
481
- <div style="
482
- margin-top:1rem;
483
- color:#1e293b;
484
- line-height:1.8;
485
- ">
486
- {content}
487
  </div>
488
- <div style="
489
- margin-top:1rem;
490
- display:flex;
491
- justify-content:space-between;
492
- align-items:center;
493
- ">
494
- <div>
495
- {stars}
496
- </div>
497
- <div style="
498
- display:flex;
499
- align-items:center;
500
- gap:0.5rem;
501
- color:#64748b;
502
- font-size:0.9rem;
503
- ">
504
- <span>👍 {likes}</span>
505
- </div>
506
  </div>
507
  </div>
508
  """
@@ -512,15 +407,13 @@ def render_card(row):
512
  # ==================================================
513
 
514
  def update_display(filter_type="all", sort_by="newest", page=1):
515
- """تحديث عرض الآراء"""
516
-
517
  rows, total, total_pages = get_testimonials(filter_type, sort_by, page)
518
 
519
  if not rows:
520
- return f"""
521
  <div style="
522
  background:white;
523
- border-radius:24px;
524
  padding:2rem;
525
  text-align:center;
526
  border:1px dashed #cbd5e1;
@@ -530,546 +423,317 @@ def update_display(filter_type="all", sort_by="newest", page=1):
530
  </div>
531
  """
532
 
533
- average_rating = round(sum(row[5] for row in rows) / len(rows), 1)
534
-
535
- html_output = f"""
536
- <div style="
537
- margin-bottom:1rem;
538
- display:flex;
539
- justify-content:space-between;
540
- align-items:center;
541
- flex-wrap:wrap;
542
- gap:0.5rem;
543
- ">
544
- <div style="
545
- background:#e0e7ff;
546
- padding:0.8rem 1rem;
547
- border-radius:999px;
548
- font-weight:600;
549
- color:#3730a3;
550
- ">
551
- 📊 عدد الآراء: {total}
552
- |
553
- ⭐ متوسط التقييم: {average_rating}
554
- </div>
555
- <div style="
556
- color:#64748b;
557
- font-size:0.9rem;
558
- ">
559
- الصفحة {page} من {total_pages}
560
- </div>
561
- </div>
562
- """
563
-
564
  for row in rows:
565
- html_output += render_card(row)
566
 
567
- # إضافة أزرار التنقل بين الصفحات
568
  if total_pages > 1:
569
- html_output += f"""
570
- <div style="
571
- display:flex;
572
- justify-content:center;
573
- gap:0.5rem;
574
- margin-top:1rem;
575
- ">
576
  """
577
-
578
  if page > 1:
579
- html_output += f"""
580
- <button onclick="changePage({page-1})" style="
581
- padding:0.5rem 1rem;
582
- background:#4338ca;
583
- color:white;
584
- border:none;
585
- border-radius:8px;
586
- cursor:pointer;
587
- font-size:1rem;
588
- ">
589
- ⬅ السابق
590
- </button>
591
- """
592
-
593
  if page < total_pages:
594
- html_output += f"""
595
- <button onclick="changePage({page+1})" style="
596
- padding:0.5rem 1rem;
597
- background:#4338ca;
598
- color:white;
599
- border:none;
600
- border-radius:8px;
601
- cursor:pointer;
602
- font-size:1rem;
603
- ">
604
- التالي ➡
605
- </button>
606
- """
607
-
608
- html_output += "</div>"
609
-
610
- return html_output
611
-
612
- # ==================================================
613
- # لوحة تحكم المشرف
614
- # ==================================================
615
-
616
- def admin_login(admin_key):
617
- """التحقق من مفتاح المشرف"""
618
- expected_key = os.getenv("ADMIN_KEY", "admin123")
619
- return admin_key == expected_key
620
-
621
- def delete_testimonial(testimonial_id, admin_key):
622
- """حذف رأي"""
623
- if not admin_login(admin_key):
624
- return "❌ مفتاح المشرف غير صحيح", update_display()
625
 
626
- try:
627
- conn = get_connection()
628
- c = conn.cursor()
629
- c.execute("DELETE FROM testimonials WHERE id = ?", (int(testimonial_id),))
630
- conn.commit()
631
- conn.close()
632
-
633
- upload_db_to_hub()
634
- return "✅ تم حذف الرأي بنجاح", update_display()
635
- except Exception as e:
636
- return f"❌ خطأ: {e}", update_display()
637
-
638
- def highlight_testimonial(testimonial_id, admin_key):
639
- """تمييز رأي"""
640
- if not admin_login(admin_key):
641
- return "❌ مفتاح المشرف غير صحيح", update_display()
642
-
643
- try:
644
- conn = get_connection()
645
- c = conn.cursor()
646
- c.execute("""
647
- UPDATE testimonials
648
- SET is_highlighted = CASE
649
- WHEN is_highlighted = 1 THEN 0
650
- ELSE 1
651
- END
652
- WHERE id = ?
653
- """, (int(testimonial_id),))
654
- conn.commit()
655
- conn.close()
656
-
657
- upload_db_to_hub()
658
- return "✅ تم تحديث التمييز", update_display()
659
- except Exception as e:
660
- return f"❌ خطأ: {e}", update_display()
661
 
662
  # ==================================================
663
  # تصدير البيانات
664
  # ==================================================
665
 
666
- def export_testimonials(format_type="json"):
667
- """تصدير الآراء بصيغة JSON أو CSV"""
668
- rows, total, _ = get_testimonials(page=1, per_page=9999)
 
 
 
 
 
 
669
 
 
670
  data = []
671
  for row in rows:
672
  data.append({
673
- "id": row[0],
674
- "name": row[1],
675
- "role": row[2],
676
- "content": row[3],
677
- "sentiment": row[4],
678
- "rating": row[5],
679
- "timestamp": row[6],
680
- "likes": row[7],
681
- "is_highlighted": bool(row[8])
682
  })
683
 
684
- if format_type == "json":
685
- return json.dumps(data, ensure_ascii=False, indent=2)
686
- else:
687
- # CSV
688
- import csv
689
- from io import StringIO
690
- output = StringIO()
691
- if data:
692
- writer = csv.DictWriter(output, fieldnames=data[0].keys())
693
- writer.writeheader()
694
- writer.writerows(data)
695
- return output.getvalue()
696
 
697
  # ==================================================
698
- # إحصائيات وتحليلات
699
  # ==================================================
700
 
701
- def get_analytics():
702
- """جلب الإحصائيات"""
703
- conn = get_connection()
704
- c = conn.cursor()
705
-
706
- analytics = {}
707
-
708
- try:
709
- # العدد الإجمالي
710
- c.execute("SELECT COUNT(*) FROM testimonials")
711
- analytics['total'] = c.fetchone()[0]
712
 
713
- # متوسط التقييم
714
- c.execute("SELECT AVG(rating) FROM testimonials")
715
- avg = c.fetchone()[0]
716
- analytics['avg_rating'] = round(avg, 1) if avg else 0
717
 
718
- # توزيع المشاعر
719
- c.execute("""
720
- SELECT sentiment, COUNT(*)
721
- FROM testimonials
722
- GROUP BY sentiment
723
- """)
724
- analytics['sentiment'] = c.fetchall()
725
 
726
- # توزيع التقييمات
727
- c.execute("""
728
- SELECT rating, COUNT(*)
729
- FROM testimonials
730
- GROUP BY rating
731
- ORDER BY rating
732
- """)
733
- analytics['ratings'] = c.fetchall()
734
-
735
- # الآراء الأكثر إعجاباً
736
- c.execute("""
737
- SELECT name, content, likes
738
- FROM testimonials
739
- ORDER BY likes DESC
740
- LIMIT 5
741
- """)
742
- analytics['most_liked'] = c.fetchall()
 
 
 
 
 
 
743
 
744
- # الآراء اليومية (آخر 7 أيام)
745
- c.execute("""
746
- SELECT DATE(timestamp), COUNT(*)
747
- FROM testimonials
748
- WHERE timestamp >= DATE('now', '-7 days')
749
- GROUP BY DATE(timestamp)
750
- ORDER BY DATE(timestamp) DESC
751
- """)
752
- analytics['daily'] = c.fetchall()
753
 
754
- except Exception as e:
755
- logger.error(f"خطأ في جلب الإحصائيات: {e}")
756
- analytics['error'] = str(e)
757
-
758
- conn.close()
759
- return analytics
760
-
761
- # ==================================================
762
- # JavaScript للتفاعل
763
- # ==================================================
764
-
765
- def get_js():
766
- """إرجاع كود JavaScript للتفاعل"""
767
- return """
768
- <script>
769
- function changePage(page) {
770
- // تحديث الصفحة
771
- const filter = document.querySelector('#filter_dropdown');
772
- const sort = document.querySelector('#sort_dropdown');
773
- // إعادة تحميل الصفحة مع المعاملات الجديدة
774
- window.location.href = window.location.pathname + '?page=' + page;
775
- }
776
- </script>
777
- """
778
 
779
  # ==================================================
780
- # إنشاء الواجهة الرئيسية
781
  # ==================================================
782
 
783
  def create_interface():
784
- """إنشاء واجهة Gradio الرئيسية"""
785
-
786
- # إضافة CSS مخصص
787
  custom_css = """
788
- @media (max-width: 768px) {
789
- .testimonial-card {
790
- padding: 0.8rem !important;
791
- }
792
- .testimonial-card h1 {
793
- font-size: 1.8rem !important;
794
- }
795
- .flex-container {
796
- flex-direction: column !important;
797
- }
798
- .buttons-row {
799
- flex-wrap: wrap !important;
800
- }
801
- .admin-panel {
802
- padding: 0.5rem !important;
803
- }
804
- }
805
- @media (max-width: 480px) {
806
- .testimonial-card {
807
- padding: 0.5rem !important;
808
- }
809
- .rating-stars {
810
- font-size: 0.8rem !important;
811
- }
812
- }
813
  .gradio-container {
814
  max-width: 1200px !important;
815
  margin: 0 auto !important;
 
 
 
 
 
 
816
  }
817
  """
818
 
819
  with gr.Blocks(
820
- title="جدار الشفافية",
821
  theme=gr.themes.Soft(),
822
- css=custom_css
 
823
  ) as demo:
824
 
825
- # ==================================================
826
- # العناوين الرئيسية
827
- # ==================================================
828
  gr.HTML("""
829
  <div style="
830
  text-align:center;
831
- margin-bottom:2rem;
832
- padding:1rem;
833
  ">
834
  <h1 style="
835
- font-size:2.7rem;
836
  font-weight:800;
837
- margin-bottom:0.5rem;
838
  background:linear-gradient(135deg, #1e293b, #4338ca);
839
  -webkit-background-clip:text;
840
  -webkit-text-fill-color:transparent;
841
  ">
842
- جدار الشفافية
843
  </h1>
844
- <p style="
845
- color:#475569;
846
- font-size:1.1rem;
847
- margin-bottom:0.5rem;
848
- ">
849
- منصة آراء مباشرة وشفافة
850
- </p>
851
- <p style="
852
- color:#94a3b8;
853
- font-size:0.9rem;
854
- ">
855
- 💡 جميع الآراء محفوظة بشكل دائم حتى عند إعادة تشغيل التطبيق
856
  </p>
857
  </div>
858
  """)
859
 
860
- # ==================================================
861
- # تبويبات (Tabs)
862
- # ==================================================
863
- with gr.Tabs():
 
864
 
865
- # ==================================================
866
- # التبويب 1: عرض الآراء
867
- # ==================================================
868
- with gr.TabItem("💬 الآراء"):
869
- with gr.Row(equal_height=False):
870
- # العمود الأيسر - إضافة رأي
871
- with gr.Column(scale=1, min_width=300):
872
- gr.Markdown("### 📝 أضف رأيك")
873
-
874
- name_input = gr.Textbox(
875
- label="الاسم",
876
- placeholder="اختياري"
877
- )
878
- role_input = gr.Textbox(
879
- label="الدور",
880
- placeholder="مثال: مهندس برمجيات"
881
- )
882
- rating_input = gr.Slider(
883
- minimum=1,
884
- maximum=5,
885
- value=5,
886
- step=1,
887
- label="التقييم"
888
- )
889
- content_input = gr.Textbox(
890
- label="الرأي",
891
- placeholder="شارك رأيك هنا...",
892
- lines=5
893
- )
894
-
895
- submit_btn = gr.Button(
896
- "📢 نشر الرأي",
897
- variant="primary",
898
- size="lg"
899
- )
900
- status_output = gr.Markdown()
901
-
902
- # العمود الأيمن - عرض الآراء
903
- with gr.Column(scale=2, min_width=500):
904
- gr.Markdown("### 💬 آراء المستخدمين")
905
-
906
- with gr.Row():
907
- filter_dropdown = gr.Dropdown(
908
- choices=[
909
- ("الكل", "all"),
910
- ("المميزة", "highlight"),
911
- ("الأكثر إعجاباً", "liked")
912
- ],
913
- value="all",
914
- label="🔍 فلترة"
915
- )
916
- sort_dropdown = gr.Dropdown(
917
- choices=[
918
- ("الأحدث", "newest"),
919
- ("الأكثر إعجاباً", "likes")
920
- ],
921
- value="newest",
922
- label="📊 ترتيب"
923
- )
924
-
925
- refresh_btn = gr.Button(
926
- "🔄 تحديث",
927
- variant="secondary"
928
- )
929
-
930
- testimonials_display = gr.HTML()
931
 
932
- # ==================================================
933
- # التبويب 2: لوحة التحكم
934
- # ==================================================
935
- with gr.TabItem("⚙️ لوحة التحكم"):
936
  with gr.Row():
937
- with gr.Column(scale=1):
938
- gr.Markdown("### 🔐 دخول المشرف")
939
- admin_key_input = gr.Textbox(
940
- label="مفتاح المشرف",
941
- type="password",
942
- placeholder="أدخل مفتاح المشرف"
943
- )
944
- login_status = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
945
 
946
- with gr.Column(scale=2):
947
- gr.Markdown("### 📋 إدارة الآراء")
948
- admin_actions = gr.Radio(
949
- choices=[
950
- ("حذف رأي", "delete"),
951
- ("تمييز/إلغاء تمييز", "highlight")
952
- ],
953
- label="اختر الإجراء"
954
- )
955
- testimonial_id_input = gr.Number(
956
- label="رقم الرأي",
957
- precision=0,
958
- minimum=1
959
- )
960
- execute_btn = gr.Button("تنفيذ", variant="primary")
961
- admin_result = gr.Markdown()
962
-
963
- # ==================================================
964
- # ال��بويب 3: الإحصائيات
965
- # ==================================================
966
- with gr.TabItem("📊 الإحصائيات"):
967
- gr.Markdown("### 📈 تحليلات وإحصائيات")
968
- analytics_btn = gr.Button("عرض الإحصائيات", variant="primary")
969
- analytics_output = gr.JSON()
970
-
971
- # ==================================================
972
- # التبويب 4: تصدير البيانات
973
- # ==================================================
974
- with gr.TabItem("📥 تصدير البيانات"):
975
- gr.Markdown("### 📤 تصدير الآراء")
976
- format_choice = gr.Radio(
977
- choices=["JSON", "CSV"],
978
- value="JSON",
979
- label="اختر الصيغة"
980
- )
981
- export_btn = gr.Button("📥 تحميل", variant="primary")
982
- export_output = gr.Textbox(
983
- label="البيانات",
984
- lines=10
985
- )
986
 
987
- # ==================================================
988
- # الأحداث (Events)
989
- # ==================================================
990
 
991
- # 1. نشر الرأي
 
 
 
 
992
  submit_btn.click(
993
  fn=add_testimonial,
994
  inputs=[name_input, role_input, content_input, rating_input],
995
  outputs=[status_output, name_input, role_input, testimonials_display]
 
 
 
996
  )
997
 
998
- # 2. تحديث العرض
999
  refresh_btn.click(
1000
- fn=update_display,
1001
  inputs=[filter_dropdown, sort_dropdown],
1002
- outputs=testimonials_display
1003
  )
1004
 
1005
- # 3. تغيير الفلترة
1006
  filter_dropdown.change(
1007
- fn=update_display,
1008
  inputs=[filter_dropdown, sort_dropdown],
1009
- outputs=testimonials_display
1010
  )
1011
 
1012
- # 4. تغيير الترتيب
1013
  sort_dropdown.change(
1014
- fn=update_display,
1015
  inputs=[filter_dropdown, sort_dropdown],
1016
- outputs=testimonials_display
1017
  )
1018
 
1019
- # 5. إجراءات المشرف
1020
- execute_btn.click(
1021
- fn=lambda action, id, key: (
1022
- delete_testimonial(int(id), key) if action == "delete"
1023
- else highlight_testimonial(int(id), key)
1024
- ),
1025
- inputs=[admin_actions, testimonial_id_input, admin_key_input],
1026
- outputs=[admin_result, testimonials_display]
1027
- )
1028
-
1029
- # 6. عرض الإحصائيات
1030
- analytics_btn.click(
1031
- fn=get_analytics,
1032
- inputs=[],
1033
- outputs=analytics_output
1034
- )
1035
 
1036
- # 7. تصدير البيانات
1037
  export_btn.click(
1038
- fn=export_testimonials,
1039
- inputs=[format_choice],
1040
  outputs=export_output
1041
  )
1042
 
1043
- # 8. تحميل أولي
 
 
 
 
 
 
1044
  demo.load(
1045
- fn=update_display,
1046
  inputs=[filter_dropdown, sort_dropdown],
1047
- outputs=testimonials_display
1048
  )
1049
-
1050
  return demo
1051
 
1052
-
1053
  # ==================================================
1054
- # تشغيل التطبيق
1055
  # ==================================================
1056
 
1057
  if __name__ == "__main__":
1058
-
1059
- # تحميل قاعدة البيانات من Hugging Face Hub
1060
  logger.info("جاري تحميل قاعدة البيانات...")
1061
- db_exists = download_db_from_hub()
1062
 
1063
- if not db_exists:
1064
  logger.info("إنشاء قاعدة بيانات جديدة...")
1065
  init_db()
1066
  upload_db_to_hub()
1067
  else:
1068
- # التحقق من وجود الجداول والفهارس
1069
  init_db()
1070
 
1071
- # إنشاء الواجهة
1072
  demo = create_interface()
1073
-
1074
- # تشغيل التطبيق
1075
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
7
  import json
8
  import logging
9
  from datetime import datetime, timedelta
 
10
  from huggingface_hub import HfApi, hf_hub_download
 
11
  import shutil
 
12
 
13
  # ==================================================
14
  # الإعدادات الأساسية
15
  # ==================================================
16
 
17
  DB_NAME = "testimonials.db"
18
+ REPO_ID = os.getenv("SPACE_ID", "your-username/your-space-name")
19
  TOKEN = os.getenv("HF_TOKEN")
20
 
 
21
  logging.basicConfig(level=logging.INFO)
22
  logger = logging.getLogger(__name__)
23
 
24
  # ==================================================
25
+ # إدارة قاعدة البيانات
26
  # ==================================================
27
 
28
  def download_db_from_hub():
29
  """تحميل قاعدة البيانات من Hugging Face Hub"""
30
  try:
31
  if not TOKEN:
 
32
  return False
 
 
33
  local_path = hf_hub_download(
34
  repo_id=REPO_ID,
35
  filename=DB_NAME,
 
37
  local_dir="./",
38
  local_dir_use_symlinks=False
39
  )
 
 
40
  if local_path != DB_NAME:
41
  shutil.copy2(local_path, DB_NAME)
 
42
  logger.info("تم تحميل قاعدة البيانات من Hub")
43
  return True
44
  except Exception as e:
45
+ logger.warning(f"لم يتم العثور على قاعدة بيانات: {e}")
46
  return False
47
 
48
  def upload_db_to_hub():
49
  """رفع قاعدة البيانات إلى Hugging Face Hub"""
50
  try:
51
+ if not TOKEN or not os.path.exists(DB_NAME):
 
52
  return False
 
 
 
 
 
 
53
  api = HfApi()
 
 
54
  api.upload_file(
55
  path_or_fileobj=DB_NAME,
56
  path_in_repo=DB_NAME,
 
58
  token=TOKEN,
59
  repo_type="space"
60
  )
 
61
  logger.info("تم رفع قاعدة البيانات إلى Hub")
62
  return True
63
  except Exception as e:
 
69
  # ==================================================
70
 
71
  def get_connection():
 
72
  return sqlite3.connect(DB_NAME, check_same_thread=False)
73
 
74
  def init_db():
 
75
  conn = get_connection()
76
  c = conn.cursor()
77
 
 
78
  c.execute("""
79
  CREATE TABLE IF NOT EXISTS testimonials (
80
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
90
  )
91
  """)
92
 
 
93
  c.execute("""
94
  CREATE TABLE IF NOT EXISTS likes (
95
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
100
  )
101
  """)
102
 
103
+ # الفهارس
104
  c.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON testimonials(timestamp)")
105
  c.execute("CREATE INDEX IF NOT EXISTS idx_ip_hash ON testimonials(ip_hash)")
106
  c.execute("CREATE INDEX IF NOT EXISTS idx_highlighted ON testimonials(is_highlighted)")
107
  c.execute("CREATE INDEX IF NOT EXISTS idx_rating ON testimonials(rating)")
 
 
108
 
109
  conn.commit()
110
  conn.close()
111
+ logger.info("تم تهيئة قاعدة البيانات")
 
112
 
113
  # ==================================================
114
  # الأدوات المساعدة
115
  # ==================================================
116
 
117
  def clean_text(text):
 
118
  if not text:
119
  return ""
120
  text = text.strip()
 
122
  return text
123
 
124
  def contains_bad_words(text):
125
+ bad_words = ["يلعن", بن", "سافل", "كلب", نزير", "عاهرة", "قحبة"]
 
 
 
 
 
126
  pattern = r'\b(' + '|'.join(bad_words) + r')\b'
127
  return bool(re.search(pattern, text.lower()))
128
 
129
  def escape_safe(text):
130
+ return html.escape(text) if text else ""
 
 
 
131
 
132
  def get_ip_hash(request: gr.Request):
 
133
  try:
134
  if request and hasattr(request, 'client') and request.client:
135
+ return hashlib.sha256(request.client.host.encode()).hexdigest()[:16]
 
136
  except:
137
  pass
138
  return "unknown"
139
 
140
  def detect_sentiment(text):
141
+ positive = ["رائع", "ممتاز", "جميل", "احترافي", "مفيد", "مذهل", "أفضل", "متميز"]
142
+ negative = ["سيء", "ضعيف", "فاشل", "مخيب", "فظيع", "تافه"]
 
 
 
 
 
 
 
 
143
 
144
  text_lower = text.lower()
145
+ pos_score = sum(1 for w in positive if w in text_lower)
146
+ neg_score = sum(1 for w in negative if w in text_lower)
147
 
148
+ if pos_score > neg_score:
149
  return "إيجابي 😊"
150
+ elif neg_score > pos_score:
151
  return "سلبي 😕"
152
  return "محايد 😐"
153
 
154
  def format_time(timestamp):
 
155
  try:
156
  dt = datetime.fromisoformat(timestamp)
157
+ diff = datetime.now() - dt
 
158
 
159
  if diff.days > 7:
160
  return dt.strftime("%Y/%m/%d")
161
  elif diff.days > 0:
162
  return f"منذ {diff.days} يوم"
163
  elif diff.seconds >= 3600:
164
+ return f"منذ {diff.seconds // 3600} ساعة"
 
165
  elif diff.seconds >= 60:
166
+ return f"منذ {diff.seconds // 60} دقيقة"
 
167
  return "الآن"
168
  except:
169
  return "غير معروف"
170
 
171
  # ==================================================
172
+ # الإحصائيات
173
  # ==================================================
174
 
175
+ def get_stats():
176
+ """جلب الإحصائيات لعرضها في الأعلى"""
177
+ conn = get_connection()
178
+ c = conn.cursor()
179
+
180
  try:
181
+ # العدد الإجمالي
182
+ c.execute("SELECT COUNT(*) FROM testimonials")
183
+ total = c.fetchone()[0]
184
+
185
+ # متوسط التقييم
186
+ c.execute("SELECT AVG(rating) FROM testimonials")
187
+ avg = c.fetchone()[0]
188
+ avg_rating = round(avg, 1) if avg else 0
189
+
190
+ # توزيع المشاعر
191
+ c.execute("""
192
+ SELECT
193
+ COUNT(CASE WHEN sentiment LIKE '%إيجابي%' THEN 1 END) as positive,
194
+ COUNT(CASE WHEN sentiment LIKE '%سلبي%' THEN 1 END) as negative,
195
+ COUNT(CASE WHEN sentiment LIKE '%محايد%' THEN 1 END) as neutral
196
+ FROM testimonials
197
+ """)
198
+ sentiments = c.fetchone()
199
+
200
+ # الأكثر إعجاباً
201
+ c.execute("""
202
+ SELECT name, content, likes
203
+ FROM testimonials
204
+ ORDER BY likes DESC
205
+ LIMIT 1
206
+ """)
207
+ most_liked = c.fetchone()
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  except Exception as e:
210
+ logger.error(f"خطأ في الإحصائيات: {e}")
211
+ return {"total": 0, "avg_rating": 0, "positive": 0, "negative": 0, "neutral": 0, "most_liked": None}
212
+ finally:
213
+ conn.close()
214
+
215
+ return {
216
+ "total": total,
217
+ "avg_rating": avg_rating,
218
+ "positive": sentiments[0] if sentiments else 0,
219
+ "negative": sentiments[1] if sentiments else 0,
220
+ "neutral": sentiments[2] if sentiments else 0,
221
+ "most_liked": most_liked
222
+ }
223
+
224
+ def render_stats(stats):
225
+ """عرض الإحصائيات بشكل جميل"""
226
+ most_liked_html = ""
227
+ if stats['most_liked'] and stats['most_liked'][2] > 0:
228
+ most_liked_html = f"""
229
+ <div style="
230
+ background:#fef3c7;
231
+ padding:0.3rem 0.8rem;
232
+ border-radius:999px;
233
+ font-size:0.85rem;
234
+ color:#92400e;
235
+ ">
236
+ 🏆 الأكثر إعجاباً: {escape_safe(stats['most_liked'][0])} 👍 {stats['most_liked'][2]}
237
+ </div>
238
+ """
239
+
240
+ return f"""
241
+ <div style="
242
+ display:grid;
243
+ grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));
244
+ gap:0.8rem;
245
+ background:white;
246
+ padding:1rem 1.5rem;
247
+ border-radius:16px;
248
+ border:1px solid #e2e8f0;
249
+ margin-bottom:1.5rem;
250
+ box-shadow:0 2px 8px rgba(0,0,0,0.04);
251
+ ">
252
+ <div style="text-align:center;">
253
+ <div style="font-size:1.8rem;font-weight:700;color:#1e293b;">{stats['total']}</div>
254
+ <div style="font-size:0.8rem;color:#64748b;">📝 آراء</div>
255
+ </div>
256
+ <div style="text-align:center;">
257
+ <div style="font-size:1.8rem;font-weight:700;color:#f59e0b;">{stats['avg_rating']}</div>
258
+ <div style="font-size:0.8rem;color:#64748b;">⭐ متوسط التقييم</div>
259
+ </div>
260
+ <div style="text-align:center;">
261
+ <div style="font-size:1.2rem;font-weight:600;color:#22c55e;">{stats['positive']}</div>
262
+ <div style="font-size:0.8rem;color:#64748b;">😊 إيجابي</div>
263
+ </div>
264
+ <div style="text-align:center;">
265
+ <div style="font-size:1.2rem;font-weight:600;color:#ef4444;">{stats['negative']}</div>
266
+ <div style="font-size:0.8rem;color:#64748b;">😕 سلبي</div>
267
+ </div>
268
+ <div style="text-align:center;">
269
+ <div style="font-size:1.2rem;font-weight:600;color:#94a3b8;">{stats['neutral']}</div>
270
+ <div style="font-size:0.8rem;color:#64748b;">😐 محايد</div>
271
+ </div>
272
+ {most_liked_html}
273
+ </div>
274
+ """
275
 
276
  # ==================================================
277
+ # إضافة رأي
278
  # ==================================================
279
 
280
+ def add_testimonial(name, role, content, rating, request: gr.Request):
 
 
 
 
 
 
 
 
281
  content = clean_text(content)
282
 
 
283
  if not content:
284
  return "❌ الرجاء كتابة رأيك", "", "", update_display()
285
 
 
295
 
296
  # مكافحة البريد المزعج
297
  one_hour_ago = (datetime.now() - timedelta(hours=1)).isoformat()
298
+ c.execute("SELECT COUNT(*) FROM testimonials WHERE ip_hash = ? AND timestamp >= ?",
299
+ (ip_hash, one_hour_ago))
 
 
300
 
301
  if c.fetchone()[0] >= 3:
302
  conn.close()
303
+ return "❌ وصلت الحد المسموح (3 آراء في الساعة)", "", "", update_display()
304
 
 
305
  name = clean_text(name) or "مجهول"
306
  role = clean_text(role) or ""
 
 
307
  sentiment = detect_sentiment(content)
308
 
 
309
  c.execute("""
310
+ INSERT INTO testimonials (name, role, content, sentiment, rating, timestamp, likes, is_highlighted, ip_hash)
 
 
 
311
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
312
+ """, (name, role, content, sentiment, int(rating), datetime.now().isoformat(), 0, 0, ip_hash))
 
 
 
313
 
314
  conn.commit()
315
  conn.close()
316
 
 
317
  upload_db_to_hub()
318
+ return "✅ تم نشر رأيك بنجاح!", "", "", update_display()
 
319
 
320
  # ==================================================
321
+ # جلب الآراء
322
  # ==================================================
323
 
324
+ def get_testimonials(filter_type="all", sort_by="newest", page=1, per_page=5):
 
325
  conn = get_connection()
326
  c = conn.cursor()
327
 
328
+ query = "SELECT id, name, role, content, sentiment, rating, timestamp, likes, is_highlighted FROM testimonials"
 
 
 
 
 
 
329
  conditions = []
330
  params = []
331
 
 
342
  else:
343
  query += " ORDER BY timestamp DESC"
344
 
 
345
  offset = (page - 1) * per_page
346
  query += " LIMIT ? OFFSET ?"
347
  params.extend([per_page, offset])
 
349
  c.execute(query, params)
350
  rows = c.fetchall()
351
 
352
+ # العدد الإجمالي
353
+ count_query = "SELECT COUNT(*) FROM testimonials"
 
 
354
  if conditions:
355
  count_query += " WHERE " + " AND ".join(conditions)
 
356
  c.execute(count_query)
357
  total = c.fetchone()[0]
358
 
359
  conn.close()
 
360
  return rows, total, (total + per_page - 1) // per_page if total > 0 else 1
361
 
362
  # ==================================================
 
364
  # ==================================================
365
 
366
  def render_card(row):
367
+ (id, name, role, content, sentiment, rating, timestamp, likes, is_highlighted) = row
 
 
 
 
 
 
 
 
 
 
368
 
369
  highlight_style = ""
370
  if is_highlighted:
371
+ highlight_style = "border-right:4px solid #f59e0b; background:#fffbeb;"
 
 
 
372
 
373
+ stars = "" * int(rating)
374
+ role_html = f'<span style="background:#eef2ff;color:#4338ca;padding:0.2rem 0.6rem;border-radius:999px;font-size:0.75rem;">{escape_safe(role)}</span>' if role else ""
 
 
 
 
 
 
 
 
 
 
 
375
 
 
376
  return f"""
377
+ <div style="
378
  background:white;
379
+ border-radius:16px;
380
+ padding:1rem 1.2rem;
381
+ margin-bottom:0.8rem;
382
  border:1px solid #e2e8f0;
383
+ box-shadow:0 2px 8px rgba(0,0,0,0.04);
 
384
  {highlight_style}
385
+ transition: all 0.2s;
386
  ">
387
+ <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;">
388
+ <div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
389
+ <span style="font-weight:700;color:#0f172a;">{escape_safe(name)}</span>
390
+ {role_html}
391
+ <span style="color:#64748b;font-size:0.75rem;">{sentiment}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  </div>
393
+ <span style="color:#94a3b8;font-size:0.7rem;">{format_time(timestamp)}</span>
394
  </div>
395
+ <div style="margin-top:0.5rem;color:#1e293b;line-height:1.7;font-size:0.95rem;">
396
+ {escape_safe(content)}
 
 
 
 
397
  </div>
398
+ <div style="margin-top:0.5rem;display:flex;justify-content:space-between;align-items:center;">
399
+ <span>{stars}</span>
400
+ <span style="color:#64748b;font-size:0.8rem;">👍 {likes}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  </div>
402
  </div>
403
  """
 
407
  # ==================================================
408
 
409
  def update_display(filter_type="all", sort_by="newest", page=1):
 
 
410
  rows, total, total_pages = get_testimonials(filter_type, sort_by, page)
411
 
412
  if not rows:
413
+ return """
414
  <div style="
415
  background:white;
416
+ border-radius:16px;
417
  padding:2rem;
418
  text-align:center;
419
  border:1px dashed #cbd5e1;
 
423
  </div>
424
  """
425
 
426
+ html = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  for row in rows:
428
+ html += render_card(row)
429
 
430
+ # أزرار التنقل
431
  if total_pages > 1:
432
+ html += f"""
433
+ <div style="display:flex;justify-content:center;gap:0.5rem;margin-top:0.8rem;">
 
 
 
 
 
434
  """
 
435
  if page > 1:
436
+ html += f'<button onclick="changePage({page-1})" style="padding:0.4rem 1.2rem;background:#4338ca;color:white;border:none;border-radius:8px;cursor:pointer;font-size:0.9rem;">⬅ السابق</button>'
437
+ html += f'<span style="padding:0.4rem 1rem;color:#64748b;">{page} / {total_pages}</span>'
 
 
 
 
 
 
 
 
 
 
 
 
438
  if page < total_pages:
439
+ html += f'<button onclick="changePage({page+1})" style="padding:0.4rem 1.2rem;background:#4338ca;color:white;border:none;border-radius:8px;cursor:pointer;font-size:0.9rem;">التالي ➡</button>'
440
+ html += "</div>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
+ return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
 
444
  # ==================================================
445
  # تصدير البيانات
446
  # ==================================================
447
 
448
+ def export_data():
449
+ conn = get_connection()
450
+ c = conn.cursor()
451
+ c.execute("SELECT name, role, content, sentiment, rating, timestamp, likes FROM testimonials ORDER BY timestamp DESC")
452
+ rows = c.fetchall()
453
+ conn.close()
454
+
455
+ if not rows:
456
+ return "لا توجد بيانات للتصدير"
457
 
458
+ # تصدير كـ JSON
459
  data = []
460
  for row in rows:
461
  data.append({
462
+ "الاسم": row[0],
463
+ "الدور": row[1],
464
+ "الرأي": row[2],
465
+ "المشاعر": row[3],
466
+ "التقييم": row[4],
467
+ "التاريخ": row[5],
468
+ "الإعجابات": row[6]
 
 
469
  })
470
 
471
+ return json.dumps(data, ensure_ascii=False, indent=2)
 
 
 
 
 
 
 
 
 
 
 
472
 
473
  # ==================================================
474
+ # لوحة تحكم المشرف (منبثقة)
475
  # ==================================================
476
 
477
+ def admin_panel():
478
+ with gr.Blocks() as admin:
479
+ gr.Markdown("### 🔐 لوحة تحكم المشرف")
 
 
 
 
 
 
 
 
480
 
481
+ with gr.Row():
482
+ admin_key = gr.Textbox(label="مفتاح المشرف", type="password", scale=1)
483
+ action = gr.Radio(choices=[("حذف", "delete"), ("تمييز", "highlight")], label="الإجراء", scale=1)
484
+ testimonial_id = gr.Number(label="رقم الرأي", precision=0, minimum=1, scale=1)
485
 
486
+ execute_btn = gr.Button("تنفيذ", variant="primary")
487
+ result = gr.Markdown()
 
 
 
 
 
488
 
489
+ def admin_action(key, action, id):
490
+ expected = os.getenv("ADMIN_KEY", "admin123")
491
+ if key != expected:
492
+ return "❌ مفتاح المشرف غير صحيح"
493
+
494
+ conn = get_connection()
495
+ c = conn.cursor()
496
+
497
+ if action == "delete":
498
+ c.execute("DELETE FROM testimonials WHERE id = ?", (int(id),))
499
+ result_text = "✅ تم حذف الرأي"
500
+ else:
501
+ c.execute("""
502
+ UPDATE testimonials
503
+ SET is_highlighted = CASE WHEN is_highlighted = 1 THEN 0 ELSE 1 END
504
+ WHERE id = ?
505
+ """, (int(id),))
506
+ result_text = "✅ تم تحديث التمييز"
507
+
508
+ conn.commit()
509
+ conn.close()
510
+ upload_db_to_hub()
511
+ return result_text + " (تم التحديث)"
512
 
513
+ execute_btn.click(
514
+ fn=admin_action,
515
+ inputs=[admin_key, action, testimonial_id],
516
+ outputs=result
517
+ )
 
 
 
 
518
 
519
+ return admin
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
 
521
  # ==================================================
522
+ # الواجهة الرئيسية
523
  # ==================================================
524
 
525
  def create_interface():
 
 
 
526
  custom_css = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  .gradio-container {
528
  max-width: 1200px !important;
529
  margin: 0 auto !important;
530
+ padding: 0.5rem !important;
531
+ }
532
+ @media (max-width: 768px) {
533
+ .grid-container {
534
+ grid-template-columns: 1fr !important;
535
+ }
536
  }
537
  """
538
 
539
  with gr.Blocks(
540
+ title="منصة الآراء",
541
  theme=gr.themes.Soft(),
542
+ css=custom_css,
543
+ fill_width=True
544
  ) as demo:
545
 
546
+ # ===== الهيدر =====
 
 
547
  gr.HTML("""
548
  <div style="
549
  text-align:center;
550
+ padding:1rem 0 0.5rem 0;
 
551
  ">
552
  <h1 style="
553
+ font-size:2.2rem;
554
  font-weight:800;
555
+ margin:0;
556
  background:linear-gradient(135deg, #1e293b, #4338ca);
557
  -webkit-background-clip:text;
558
  -webkit-text-fill-color:transparent;
559
  ">
560
+ 💬 منصة الآراء
561
  </h1>
562
+ <p style="color:#64748b;margin:0.2rem 0 0 0;font-size:0.9rem;">
563
+ شارك رأيك بكل شفافية • جميع الآراء محفوظة بشكل دائم
 
 
 
 
 
 
 
 
 
 
564
  </p>
565
  </div>
566
  """)
567
 
568
+ # ===== الإحصائيات =====
569
+ stats_display = gr.HTML()
570
+
571
+ # ===== المحتوى الرئيسي =====
572
+ with gr.Row(equal_height=False):
573
 
574
+ # ===== العمود الأيسر: إضافة رأي =====
575
+ with gr.Column(scale=1, min_width=280):
576
+ gr.Markdown("### ✍️ أضف رأيك")
577
+
578
+ name_input = gr.Textbox(
579
+ label="الاسم",
580
+ placeholder="اختياري",
581
+ container=False
582
+ )
583
+ role_input = gr.Textbox(
584
+ label="الدور",
585
+ placeholder="مثال: مهندس",
586
+ container=False
587
+ )
588
+ rating_input = gr.Slider(
589
+ minimum=1,
590
+ maximum=5,
591
+ value=5,
592
+ step=1,
593
+ label="⭐ التقييم"
594
+ )
595
+ content_input = gr.Textbox(
596
+ label="الرأي",
597
+ placeholder="اكتب رأيك هنا...",
598
+ lines=4,
599
+ container=False
600
+ )
601
+
602
+ with gr.Row():
603
+ submit_btn = gr.Button("📢 نشر", variant="primary", scale=2)
604
+ clear_btn = gr.Button("🗑️ مسح", scale=1)
605
+
606
+ status_output = gr.Markdown()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
 
608
+ # ===== العمود الأيمن: عرض الآراء =====
609
+ with gr.Column(scale=2, min_width=400):
610
+ gr.Markdown("### 💬 آراء المستخدمين")
611
+
612
  with gr.Row():
613
+ filter_dropdown = gr.Dropdown(
614
+ choices=[
615
+ ("الكل", "all"),
616
+ ("المميزة", "highlight"),
617
+ ("الأكثر إعجاباً", "liked")
618
+ ],
619
+ value="all",
620
+ label="🔍 فلترة",
621
+ scale=1
622
+ )
623
+ sort_dropdown = gr.Dropdown(
624
+ choices=[
625
+ ("الأحدث", "newest"),
626
+ ("الأكثر إعجاباً", "likes")
627
+ ],
628
+ value="newest",
629
+ label="📊 ترتيب",
630
+ scale=1
631
+ )
632
+
633
+ refresh_btn = gr.Button("🔄 تحديث", variant="secondary", size="sm")
634
+ testimonials_display = gr.HTML()
635
+
636
+ # ===== أدوات إضافية في الأسفل =====
637
+ with gr.Row():
638
+ with gr.Column(scale=1):
639
+ with gr.Accordion("⚙️ أدوات إضافية", open=False):
640
+ with gr.Row():
641
+ export_btn = gr.Button("📥 تصدير البيانات (JSON)", size="sm")
642
+ export_output = gr.Textbox(label="", lines=3, visible=False)
643
+
644
+ # زر فتح لوحة المشرف
645
+ admin_btn = gr.Button("🔐 لوحة المشرف", size="sm", variant="stop")
646
 
647
+ # لوحة المشرف (مخفية حتى الضغط)
648
+ admin_interface = admin_panel()
649
+ admin_interface.visible = False
650
+
651
+ def toggle_admin():
652
+ return gr.update(visible=not admin_interface.visible)
653
+
654
+ admin_btn.click(
655
+ fn=toggle_admin,
656
+ outputs=admin_interface
657
+ )
658
+
659
+ # ===== الأحداث =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
 
661
+ # تحديث الإحصائيات
662
+ def update_stats():
663
+ return render_stats(get_stats())
664
 
665
+ # دمج التحديثات
666
+ def full_update(filter_type="all", sort_by="newest", page=1):
667
+ return update_stats(), update_display(filter_type, sort_by, page)
668
+
669
+ # زر النشر
670
  submit_btn.click(
671
  fn=add_testimonial,
672
  inputs=[name_input, role_input, content_input, rating_input],
673
  outputs=[status_output, name_input, role_input, testimonials_display]
674
+ ).then(
675
+ fn=update_stats,
676
+ outputs=stats_display
677
  )
678
 
679
+ # زر التحديث
680
  refresh_btn.click(
681
+ fn=full_update,
682
  inputs=[filter_dropdown, sort_dropdown],
683
+ outputs=[stats_display, testimonials_display]
684
  )
685
 
686
+ # تغيير الفلترة/الترتيب
687
  filter_dropdown.change(
688
+ fn=full_update,
689
  inputs=[filter_dropdown, sort_dropdown],
690
+ outputs=[stats_display, testimonials_display]
691
  )
692
 
 
693
  sort_dropdown.change(
694
+ fn=full_update,
695
  inputs=[filter_dropdown, sort_dropdown],
696
+ outputs=[stats_display, testimonials_display]
697
  )
698
 
699
+ # زر التصدير
700
+ def show_export():
701
+ data = export_data()
702
+ return gr.update(visible=True, value=data)
 
 
 
 
 
 
 
 
 
 
 
 
703
 
 
704
  export_btn.click(
705
+ fn=show_export,
 
706
  outputs=export_output
707
  )
708
 
709
+ # زر المسح
710
+ clear_btn.click(
711
+ fn=lambda: ("", "", "", ""),
712
+ outputs=[name_input, role_input, content_input, status_output]
713
+ )
714
+
715
+ # تحميل أولي
716
  demo.load(
717
+ fn=full_update,
718
  inputs=[filter_dropdown, sort_dropdown],
719
+ outputs=[stats_display, testimonials_display]
720
  )
721
+
722
  return demo
723
 
 
724
  # ==================================================
725
+ # التشغيل
726
  # ==================================================
727
 
728
  if __name__ == "__main__":
 
 
729
  logger.info("جاري تحميل قاعدة البيانات...")
 
730
 
731
+ if not download_db_from_hub():
732
  logger.info("إنشاء قاعدة بيانات جديدة...")
733
  init_db()
734
  upload_db_to_hub()
735
  else:
 
736
  init_db()
737
 
 
738
  demo = create_interface()
 
 
739
  demo.launch(server_name="0.0.0.0", server_port=7860)