| import gradio as gr |
| import sqlite3 |
| import hashlib |
| import html |
| import re |
| import os |
| import json |
| import logging |
| from datetime import datetime, timedelta |
| from huggingface_hub import HfApi, hf_hub_download |
| import shutil |
|
|
| |
| |
| |
|
|
| DB_NAME = "testimonials.db" |
| REPO_ID = os.getenv("SPACE_ID", "EngReem85/Feedbacks") |
| TOKEN = os.getenv("HF_TOKEN") |
|
|
| logging.basicConfig(level=logging.INFO) |
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| |
|
|
| CUSTOM_CSS = """ |
| /* CSS للتصميم الدارك */ |
| @import url('https://fonts.googleapis.com/css2?family=Tajawal:wght@400;600;700;800&display=swap'); |
| |
| * { |
| font-family: 'Tajawal', sans-serif !important; |
| } |
| |
| .gradio-container { |
| background: #0f0f1a !important; |
| min-height: 100vh; |
| } |
| |
| /* تأثيرات الحركة */ |
| @keyframes slideIn { |
| from { |
| opacity: 0; |
| transform: translateY(20px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| @keyframes slideDown { |
| from { |
| opacity: 0; |
| transform: translateY(-10px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| @keyframes shimmer { |
| 0% { transform: translateX(-100%) rotate(45deg); } |
| 100% { transform: translateX(100%) rotate(45deg); } |
| } |
| |
| /* تحسين البطاقات في الوضع الدارك */ |
| .testimonial-card { |
| animation: slideIn 0.5s ease-out; |
| transition: all 0.3s ease; |
| background: #1a1a2e !important; |
| border: 1px solid #2d2d44 !important; |
| } |
| |
| .testimonial-card:hover { |
| transform: translateY(-4px); |
| box-shadow: 0 12px 40px rgba(99, 102, 241, 0.2) !important; |
| border-color: #6366f1 !important; |
| } |
| |
| /* أزرار متألقة */ |
| .glow-button { |
| background: linear-gradient(135deg, #667eea, #764ba2) !important; |
| color: white !important; |
| border: none !important; |
| padding: 12px 32px !important; |
| border-radius: 50px !important; |
| font-weight: 700 !important; |
| transition: all 0.3s ease !important; |
| box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4) !important; |
| } |
| |
| .glow-button:hover { |
| transform: scale(1.05) !important; |
| box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6) !important; |
| } |
| |
| /* تحسين حقول الإدخال في الوضع الدارك */ |
| .gr-textbox input, .gr-textbox textarea { |
| border-radius: 12px !important; |
| border: 2px solid #2d2d44 !important; |
| transition: all 0.3s ease !important; |
| font-size: 1rem !important; |
| background: #1a1a2e !important; |
| color: #e2e8f0 !important; |
| } |
| |
| .gr-textbox input:focus, .gr-textbox textarea:focus { |
| border-color: #6366f1 !important; |
| box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2) !important; |
| } |
| |
| .gr-textbox input::placeholder, .gr-textbox textarea::placeholder { |
| color: #64748b !important; |
| } |
| |
| /* شريط التمرير */ |
| ::-webkit-scrollbar { |
| width: 8px; |
| height: 8px; |
| } |
| |
| ::-webkit-scrollbar-track { |
| background: #1a1a2e; |
| border-radius: 10px; |
| } |
| |
| ::-webkit-scrollbar-thumb { |
| background: linear-gradient(135deg, #667eea, #764ba2); |
| border-radius: 10px; |
| } |
| |
| ::-webkit-scrollbar-thumb:hover { |
| background: #4f46e5; |
| } |
| |
| /* تحسين العناوين في الوضع الدارك */ |
| h1, h2, h3, h4, h5, h6, label { |
| color: #e2e8f0 !important; |
| } |
| |
| /* تحسين الـ Dropdown في الوضع الدارك */ |
| select, .gr-dropdown { |
| background: #1a1a2e !important; |
| color: #e2e8f0 !important; |
| border-color: #2d2d44 !important; |
| } |
| |
| /* تحسين الـ Slider */ |
| input[type="range"] { |
| accent-color: #6366f1; |
| } |
| |
| /* تحسين النصوص */ |
| p, span, div { |
| color: #cbd5e1; |
| } |
| |
| /* تحسين الأزرار الثانوية */ |
| .gr-button-secondary { |
| background: #2d2d44 !important; |
| color: #e2e8f0 !important; |
| border: 1px solid #3d3d5c !important; |
| } |
| |
| .gr-button-secondary:hover { |
| background: #3d3d5c !important; |
| border-color: #6366f1 !important; |
| } |
| |
| /* تحسين الإحصائيات */ |
| .stat-card { |
| background: #1a1a2e !important; |
| border: 1px solid #2d2d44 !important; |
| transition: all 0.3s ease; |
| } |
| |
| .stat-card:hover { |
| transform: translateY(-2px); |
| border-color: #6366f1 !important; |
| box-shadow: 0 4px 20px rgba(99, 102, 241, 0.1) !important; |
| } |
| """ |
|
|
| |
| |
| |
|
|
| def download_db_from_hub(): |
| """تحميل قاعدة البيانات من Hugging Face Hub""" |
| try: |
| if not TOKEN: |
| logger.warning("HF_TOKEN غير موجود، سيتم إنشاء قاعدة بيانات جديدة") |
| return False |
| local_path = hf_hub_download( |
| repo_id=REPO_ID, |
| filename=DB_NAME, |
| token=TOKEN, |
| local_dir="./" |
| ) |
| if local_path != DB_NAME: |
| shutil.copy2(local_path, DB_NAME) |
| logger.info("تم تحميل قاعدة البيانات من Hub") |
| return True |
| except Exception as e: |
| logger.warning(f"لم يتم العثور على قاعدة بيانات: {e}") |
| return False |
|
|
| def upload_db_to_hub(): |
| """رفع قاعدة البيانات إلى Hugging Face Hub""" |
| try: |
| if not TOKEN or not os.path.exists(DB_NAME): |
| return False |
| api = HfApi() |
| api.upload_file( |
| path_or_fileobj=DB_NAME, |
| path_in_repo=DB_NAME, |
| repo_id=REPO_ID, |
| token=TOKEN, |
| repo_type="space" |
| ) |
| logger.info("تم رفع قاعدة البيانات إلى Hub") |
| return True |
| except Exception as e: |
| logger.error(f"فشل رفع قاعدة البيانات: {e}") |
| return False |
|
|
| |
| |
| |
|
|
| def get_connection(): |
| return sqlite3.connect(DB_NAME, check_same_thread=False) |
|
|
| def init_db(): |
| conn = get_connection() |
| c = conn.cursor() |
| |
| c.execute(""" |
| CREATE TABLE IF NOT EXISTS testimonials ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| name TEXT, |
| role TEXT, |
| content TEXT NOT NULL, |
| rating INTEGER DEFAULT 5, |
| timestamp TEXT NOT NULL, |
| is_highlighted INTEGER DEFAULT 0, |
| ip_hash TEXT |
| ) |
| """) |
| |
| |
| c.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON testimonials(timestamp)") |
| c.execute("CREATE INDEX IF NOT EXISTS idx_ip_hash ON testimonials(ip_hash)") |
| c.execute("CREATE INDEX IF NOT EXISTS idx_highlighted ON testimonials(is_highlighted)") |
| c.execute("CREATE INDEX IF NOT EXISTS idx_rating ON testimonials(rating)") |
| |
| conn.commit() |
| conn.close() |
| logger.info("تم تهيئة قاعدة البيانات") |
|
|
| |
| |
| |
|
|
| def clean_text(text): |
| if not text: |
| return "" |
| text = text.strip() |
| text = re.sub(r"\s+", " ", text) |
| return text |
|
|
| def contains_bad_words(text): |
| bad_words = ["يلعن", "تبن", "سافل", "كلب", "خنزير", "عاهرة", "قحبة"] |
| pattern = r'\b(' + '|'.join(bad_words) + r')\b' |
| return bool(re.search(pattern, text.lower())) |
|
|
| def escape_safe(text): |
| return html.escape(text) if text else "" |
|
|
| def get_ip_hash(request: gr.Request): |
| try: |
| if request and hasattr(request, 'client') and request.client: |
| return hashlib.sha256(request.client.host.encode()).hexdigest()[:16] |
| except: |
| pass |
| return "unknown" |
|
|
| def format_time(timestamp): |
| try: |
| dt = datetime.fromisoformat(timestamp) |
| diff = datetime.now() - dt |
| |
| if diff.days > 7: |
| return dt.strftime("%Y/%m/%d") |
| elif diff.days > 0: |
| return f"منذ {diff.days} يوم" |
| elif diff.seconds >= 3600: |
| return f"منذ {diff.seconds // 3600} ساعة" |
| elif diff.seconds >= 60: |
| return f"منذ {diff.seconds // 60} دقيقة" |
| return "الآن" |
| except: |
| return "غير معروف" |
|
|
| |
| |
| |
|
|
| def get_stats(): |
| """جلب الإحصائيات""" |
| conn = get_connection() |
| c = conn.cursor() |
| |
| try: |
| c.execute("SELECT COUNT(*) FROM testimonials") |
| total = c.fetchone()[0] |
| |
| c.execute("SELECT AVG(rating) FROM testimonials") |
| avg = c.fetchone()[0] |
| avg_rating = round(avg, 1) if avg else 0 |
| |
| except Exception as e: |
| logger.error(f"خطأ في الإحصائيات: {e}") |
| return {"total": 0, "avg_rating": 0} |
| finally: |
| conn.close() |
| |
| return { |
| "total": total, |
| "avg_rating": avg_rating |
| } |
|
|
| def render_stats(stats): |
| """عرض الإحصائيات""" |
| return f""" |
| <div style=" |
| display:grid; |
| grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); |
| gap:1rem; |
| margin-bottom:1.5rem; |
| "> |
| <div class="stat-card" style=" |
| background:#1a1a2e; |
| padding:1.5rem; |
| border-radius:16px; |
| text-align:center; |
| border:1px solid #2d2d44; |
| transition: all 0.3s ease; |
| "> |
| <div style="font-size:3rem;font-weight:700;color:#818cf8;">{stats['total']}</div> |
| <div style="color:#94a3b8;font-size:0.95rem;">📝 عدد الآراء</div> |
| </div> |
| |
| <div class="stat-card" style=" |
| background:#1a1a2e; |
| padding:1.5rem; |
| border-radius:16px; |
| text-align:center; |
| border:1px solid #2d2d44; |
| transition: all 0.3s ease; |
| "> |
| <div style="font-size:3rem;font-weight:700;color:#fbbf24;">{stats['avg_rating']}</div> |
| <div style="color:#94a3b8;font-size:0.95rem;">⭐ متوسط التقييم</div> |
| </div> |
| </div> |
| """ |
|
|
| |
| |
| |
|
|
| def add_testimonial(name, role, content, rating, request: gr.Request): |
| content = clean_text(content) |
| |
| if not content: |
| return """ |
| <div style=" |
| background: #450a0a; |
| color: #fca5a5; |
| padding: 12px 20px; |
| border-radius: 12px; |
| animation: slideDown 0.3s ease-out; |
| margin-bottom: 1rem; |
| border-right: 4px solid #ef4444; |
| "> |
| ❌ الرجاء كتابة رأيك |
| </div> |
| """, "", "", update_display(), "**0** / 800 حرف" |
| |
| if len(content) > 800: |
| return """ |
| <div style=" |
| background: #450a0a; |
| color: #fca5a5; |
| padding: 12px 20px; |
| border-radius: 12px; |
| animation: slideDown 0.3s ease-out; |
| margin-bottom: 1rem; |
| border-right: 4px solid #ef4444; |
| "> |
| ❌ الحد الأقصى 800 حرف |
| </div> |
| """, "", "", update_display(), f"**{len(content)}** / 800 حرف" |
| |
| if contains_bad_words(content): |
| return """ |
| <div style=" |
| background: #450a0a; |
| color: #fca5a5; |
| padding: 12px 20px; |
| border-radius: 12px; |
| animation: slideDown 0.3s ease-out; |
| margin-bottom: 1rem; |
| border-right: 4px solid #ef4444; |
| "> |
| ❌ يوجد كلمات غير مناسبة |
| </div> |
| """, "", "", update_display(), f"**{len(content)}** / 800 حرف" |
| |
| ip_hash = get_ip_hash(request) |
| conn = get_connection() |
| c = conn.cursor() |
| |
| |
| one_hour_ago = (datetime.now() - timedelta(hours=1)).isoformat() |
| c.execute("SELECT COUNT(*) FROM testimonials WHERE ip_hash = ? AND timestamp >= ?", |
| (ip_hash, one_hour_ago)) |
| |
| if c.fetchone()[0] >= 3: |
| conn.close() |
| return """ |
| <div style=" |
| background: #450a0a; |
| color: #fca5a5; |
| padding: 12px 20px; |
| border-radius: 12px; |
| animation: slideDown 0.3s ease-out; |
| margin-bottom: 1rem; |
| border-right: 4px solid #ef4444; |
| "> |
| ❌ وصلت الحد المسموح (3 آراء في الساعة) |
| </div> |
| """, "", "", update_display(), "**0** / 800 حرف" |
| |
| name = clean_text(name) or "مجهول" |
| role = clean_text(role) or "" |
| |
| c.execute(""" |
| INSERT INTO testimonials (name, role, content, rating, timestamp, is_highlighted, ip_hash) |
| VALUES (?, ?, ?, ?, ?, ?, ?) |
| """, (name, role, content, int(rating), datetime.now().isoformat(), 0, ip_hash)) |
| |
| conn.commit() |
| conn.close() |
| |
| upload_db_to_hub() |
| |
| return """ |
| <div style=" |
| background: #064e3b; |
| color: #6ee7b7; |
| padding: 12px 20px; |
| border-radius: 12px; |
| animation: slideDown 0.3s ease-out; |
| margin-bottom: 1rem; |
| box-shadow: 0 4px 15px rgba(16, 185, 129, 0.2); |
| border-right: 4px solid #10b981; |
| "> |
| ✅ تم نشر رأيك بنجاح! 🎉 |
| </div> |
| """, "", "", update_display(), "**0** / 800 حرف" |
|
|
| |
| |
| |
|
|
| def get_testimonials(page=1, per_page=5): |
| conn = get_connection() |
| c = conn.cursor() |
| |
| query = "SELECT id, name, role, content, rating, timestamp, is_highlighted FROM testimonials" |
| query += " ORDER BY timestamp DESC" |
| |
| offset = (page - 1) * per_page |
| query += " LIMIT ? OFFSET ?" |
| |
| c.execute(query, (per_page, offset)) |
| rows = c.fetchall() |
| |
| |
| c.execute("SELECT COUNT(*) FROM testimonials") |
| total = c.fetchone()[0] |
| |
| conn.close() |
| return rows, total, (total + per_page - 1) // per_page if total > 0 else 1 |
|
|
| |
| |
| |
|
|
| def render_stars(rating): |
| """عرض التقييم بشكل جميل""" |
| full_stars = '⭐' * int(rating) |
| empty_stars = '☆' * (5 - int(rating)) |
| return f'<span style="font-size:1.1rem;color:#fbbf24;">{full_stars}{empty_stars}</span>' |
|
|
| def render_card(row): |
| """عرض بطاقة الرأي""" |
| (id, name, role, content, rating, timestamp, is_highlighted) = row |
| |
| highlight_style = "" |
| highlight_badge = "" |
| if is_highlighted: |
| highlight_style = "border-right:4px solid #fbbf24; background:linear-gradient(135deg, #1a1a2e, #2d1f0a);" |
| highlight_badge = """ |
| <span style=" |
| background: #fbbf24; |
| color: #1a1a2e; |
| padding: 2px 10px; |
| border-radius: 999px; |
| font-size: 0.7rem; |
| font-weight: 600; |
| "> |
| ⭐ مميز |
| </span> |
| """ |
| |
| role_html = f'<span style="background:#312e81;color:#818cf8;padding:0.2rem 0.6rem;border-radius:999px;font-size:0.75rem;">{escape_safe(role)}</span>' if role else "" |
| |
| return f""" |
| <div class="testimonial-card" id="card-{id}" style=" |
| background:#1a1a2e; |
| border-radius:20px; |
| padding:1.2rem 1.5rem; |
| margin-bottom:1rem; |
| border:1px solid #2d2d44; |
| {highlight_style} |
| animation: slideIn 0.5s ease-out; |
| transition: all 0.3s ease; |
| "> |
| <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:0.5rem;"> |
| <div style="display:flex;align-items:center;gap:0.8rem;"> |
| <div style=" |
| width:42px; |
| height:42px; |
| border-radius:50%; |
| background:linear-gradient(135deg, #667eea, #764ba2); |
| display:flex; |
| align-items:center; |
| justify-content:center; |
| color:white; |
| font-weight:700; |
| font-size:1.2rem; |
| flex-shrink:0; |
| "> |
| {escape_safe(name[0].upper())} |
| </div> |
| <div> |
| <div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;"> |
| <span style="font-weight:700;color:#e2e8f0;font-size:1.05rem;">{escape_safe(name)}</span> |
| {role_html} |
| {highlight_badge} |
| </div> |
| <div style="display:flex;align-items:center;gap:0.5rem;margin-top:0.2rem;"> |
| <span style="color:#64748b;font-size:0.75rem;">{format_time(timestamp)}</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div style=" |
| margin-top:0.8rem; |
| color:#cbd5e1; |
| line-height:1.8; |
| font-size:0.98rem; |
| padding:0.5rem 0; |
| "> |
| {escape_safe(content)} |
| </div> |
| |
| <div style=" |
| margin-top:0.8rem; |
| padding-top:0.8rem; |
| border-top:1px solid #2d2d44; |
| display:flex; |
| justify-content:flex-end; |
| align-items:center; |
| flex-wrap:wrap; |
| gap:0.5rem; |
| "> |
| {render_stars(rating)} |
| </div> |
| </div> |
| """ |
|
|
| |
| |
| |
|
|
| def update_display(page=1): |
| rows, total, total_pages = get_testimonials(page) |
| |
| if not rows: |
| return """ |
| <div style=" |
| background:#1a1a2e; |
| border-radius:16px; |
| padding:3rem 2rem; |
| text-align:center; |
| border:2px dashed #2d2d44; |
| color:#64748b; |
| "> |
| <div style="font-size:3rem;margin-bottom:1rem;">✨</div> |
| <div style="font-size:1.2rem;font-weight:600;color:#94a3b8;">لا توجد آراء حالياً</div> |
| <div style="font-size:0.9rem;margin-top:0.5rem;color:#64748b;">كن أول من يشارك رأيه!</div> |
| </div> |
| """ |
| |
| html = "" |
| for row in rows: |
| html += render_card(row) |
| |
| |
| if total_pages > 1: |
| html += f""" |
| <div style=" |
| display:flex; |
| justify-content:center; |
| align-items:center; |
| gap:0.8rem; |
| margin-top:1.2rem; |
| padding:0.5rem; |
| "> |
| """ |
| if page > 1: |
| html += f''' |
| <button onclick="changePage({page-1})" style=" |
| padding:0.5rem 1.5rem; |
| background:linear-gradient(135deg, #667eea, #764ba2); |
| color:white; |
| border:none; |
| border-radius:50px; |
| cursor:pointer; |
| font-size:0.9rem; |
| font-weight:600; |
| transition:all 0.3s ease; |
| box-shadow:0 4px 15px rgba(102,126,234,0.3); |
| "> |
| ⬅ السابق |
| </button> |
| ''' |
| |
| html += f''' |
| <span style=" |
| padding:0.4rem 1.2rem; |
| background:#2d2d44; |
| border-radius:50px; |
| color:#94a3b8; |
| font-weight:600; |
| font-size:0.9rem; |
| "> |
| {page} / {total_pages} |
| </span> |
| ''' |
| |
| if page < total_pages: |
| html += f''' |
| <button onclick="changePage({page+1})" style=" |
| padding:0.5rem 1.5rem; |
| background:linear-gradient(135deg, #667eea, #764ba2); |
| color:white; |
| border:none; |
| border-radius:50px; |
| cursor:pointer; |
| font-size:0.9rem; |
| font-weight:600; |
| transition:all 0.3s ease; |
| box-shadow:0 4px 15px rgba(102,126,234,0.3); |
| "> |
| التالي ➡ |
| </button> |
| ''' |
| html += "</div>" |
| |
| return html |
|
|
| |
| |
| |
|
|
| def export_data(): |
| conn = get_connection() |
| c = conn.cursor() |
| c.execute("SELECT name, role, content, rating, timestamp FROM testimonials ORDER BY timestamp DESC") |
| rows = c.fetchall() |
| conn.close() |
| |
| if not rows: |
| return "لا توجد بيانات للتصدير" |
| |
| data = [] |
| for row in rows: |
| data.append({ |
| "الاسم": row[0], |
| "الدور": row[1], |
| "الرأي": row[2], |
| "التقييم": row[3], |
| "التاريخ": row[4] |
| }) |
| |
| return json.dumps(data, ensure_ascii=False, indent=2) |
|
|
| |
| |
| |
|
|
| def admin_panel(): |
| with gr.Blocks() as admin: |
| gr.Markdown("### 🔐 لوحة تحكم المشرف") |
| |
| with gr.Row(): |
| admin_key = gr.Textbox(label="مفتاح المشرف", type="password", scale=1) |
| action = gr.Radio(choices=[("حذف", "delete"), ("تمييز", "highlight")], label="الإجراء", scale=1) |
| testimonial_id = gr.Number(label="رقم الرأي", precision=0, minimum=1, scale=1) |
| |
| execute_btn = gr.Button("تنفيذ", variant="primary") |
| result = gr.Markdown() |
| |
| def admin_action(key, action, id): |
| expected = os.getenv("ADMIN_KEY", "admin123") |
| if key != expected: |
| return "❌ مفتاح المشرف غير صحيح" |
| |
| conn = get_connection() |
| c = conn.cursor() |
| |
| if action == "delete": |
| c.execute("DELETE FROM testimonials WHERE id = ?", (int(id),)) |
| result_text = "✅ تم حذف الرأي" |
| else: |
| c.execute(""" |
| UPDATE testimonials |
| SET is_highlighted = CASE WHEN is_highlighted = 1 THEN 0 ELSE 1 END |
| WHERE id = ? |
| """, (int(id),)) |
| result_text = "✅ تم تحديث التمييز" |
| |
| conn.commit() |
| conn.close() |
| upload_db_to_hub() |
| return result_text + " (تم التحديث)" |
| |
| execute_btn.click( |
| fn=admin_action, |
| inputs=[admin_key, action, testimonial_id], |
| outputs=result |
| ) |
| |
| return admin |
|
|
| |
| |
| |
|
|
| def create_interface(): |
| with gr.Blocks() as demo: |
| |
| |
| gr.HTML(""" |
| <div style=" |
| text-align:center; |
| padding: 2.5rem 2rem; |
| background: linear-gradient(135deg, #1a1a2e 0%, #2d1f4a 50%, #1a1a2e 100%); |
| border-radius: 24px; |
| margin-bottom: 2rem; |
| position: relative; |
| overflow: hidden; |
| border: 1px solid #2d2d44; |
| box-shadow: 0 10px 40px rgba(0,0,0,0.5); |
| "> |
| <!-- خلفية متحركة --> |
| <div style=" |
| position: absolute; |
| top: -50%; |
| left: -50%; |
| width: 200%; |
| height: 200%; |
| background: radial-gradient(circle at 30% 40%, rgba(99,102,241,0.1) 0%, transparent 70%); |
| animation: shimmer 8s infinite; |
| "></div> |
| |
| <div style="position:relative;z-index:1;"> |
| <h1 style=" |
| font-size:3rem; |
| font-weight:800; |
| margin:0; |
| background: linear-gradient(135deg, #818cf8, #a78bfa, #c084fc); |
| -webkit-background-clip:text; |
| -webkit-text-fill-color:transparent; |
| text-shadow: none; |
| "> |
| من منظورهم |
| </h1> |
| <p style=" |
| color:#94a3b8; |
| margin:0.5rem 0 0 0; |
| font-size:1.1rem; |
| font-weight:300; |
| "> |
| .شارك رأيك بكل شفافية، فكل كلمة تُحدث فرقًا |
| </p> |
| </div> |
| </div> |
| """) |
| |
| |
| stats_display = gr.HTML() |
| |
| |
| with gr.Row(equal_height=False): |
| |
| |
| with gr.Column(scale=1, min_width=300): |
| name_input = gr.Textbox( |
| label="الاسم", |
| placeholder="اختياري" |
| ) |
| role_input = gr.Textbox( |
| label="الدور", |
| placeholder="مثال: مهندس برمجيات" |
| ) |
| rating_input = gr.Slider( |
| minimum=1, |
| maximum=5, |
| value=5, |
| step=1, |
| label="⭐ التقييم" |
| ) |
| content_input = gr.Textbox( |
| label="الرأي", |
| placeholder="اكتب رأيك هنا...", |
| lines=5, |
| max_lines=8 |
| ) |
| |
| |
| char_count = gr.Markdown("**0** / 800 حرف") |
| |
| def update_char_count(content): |
| return f"**{len(content)}** / 800 حرف" |
| |
| content_input.change( |
| fn=update_char_count, |
| inputs=content_input, |
| outputs=char_count |
| ) |
| |
| with gr.Row(): |
| submit_btn = gr.Button( |
| "📢 نشر الرأي", |
| variant="primary", |
| size="lg", |
| elem_classes="glow-button", |
| scale=2 |
| ) |
| clear_btn = gr.Button("🗑️ مسح", variant="secondary", size="lg", scale=1) |
| |
| status_output = gr.Markdown() |
| |
| |
| with gr.Column(scale=2, min_width=400): |
| gr.Markdown("### 💬 آراء المستخدمين") |
| |
| with gr.Row(): |
| refresh_btn = gr.Button("🔄 تحديث", variant="secondary", size="sm") |
| |
| testimonials_display = gr.HTML() |
| |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| with gr.Accordion("⚙️ أدوات إضافية", open=False): |
| with gr.Row(): |
| export_btn = gr.Button("📥 تصدير البيانات (JSON)", size="sm") |
| export_output = gr.Textbox(label="", lines=3, visible=False) |
| |
| admin_btn = gr.Button("🔐 لوحة المشرف", size="sm", variant="stop") |
| |
| admin_interface = admin_panel() |
| admin_interface.visible = False |
| |
| def toggle_admin(): |
| return gr.update(visible=not admin_interface.visible) |
| |
| admin_btn.click( |
| fn=toggle_admin, |
| outputs=admin_interface |
| ) |
| |
| |
| |
| def update_stats(): |
| return render_stats(get_stats()) |
| |
| def full_update(): |
| stats_html = render_stats(get_stats()) |
| display = update_display() |
| return stats_html, display |
| |
| |
| submit_btn.click( |
| fn=add_testimonial, |
| inputs=[name_input, role_input, content_input, rating_input], |
| outputs=[status_output, name_input, role_input, testimonials_display, char_count] |
| ).then( |
| fn=update_stats, |
| outputs=[stats_display] |
| ) |
| |
| |
| refresh_btn.click( |
| fn=full_update, |
| outputs=[stats_display, testimonials_display] |
| ) |
| |
| |
| def show_export(): |
| data = export_data() |
| return gr.update(visible=True, value=data) |
| |
| export_btn.click( |
| fn=show_export, |
| outputs=export_output |
| ) |
| |
| |
| clear_btn.click( |
| fn=lambda: ("", "", "", "", "**0** / 800 حرف"), |
| outputs=[name_input, role_input, content_input, status_output, char_count] |
| ) |
| |
| |
| demo.load( |
| fn=full_update, |
| outputs=[stats_display, testimonials_display] |
| ) |
| |
| return demo |
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
| logger.info("جاري تحميل قاعدة البيانات...") |
| |
| if not download_db_from_hub(): |
| logger.info("إنشاء قاعدة بيانات جديدة...") |
| init_db() |
| upload_db_to_hub() |
| else: |
| init_db() |
| |
| demo = create_interface() |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=7860, |
| theme=gr.themes.Soft( |
| primary_hue="indigo", |
| secondary_hue="purple", |
| neutral_hue="slate", |
| ), |
| css=CUSTOM_CSS |
| ) |