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__) # ================================================== # CSS - تصميم دارك (بدون تحكم في القياسات) # ================================================== 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"""
{stats['total']}
📝 عدد الآراء
{stats['avg_rating']}
⭐ متوسط التقييم
""" # ================================================== # إضافة رأي # ================================================== def add_testimonial(name, role, content, rating, request: gr.Request): content = clean_text(content) if not content: return """
❌ الرجاء كتابة رأيك
""", "", "", update_display(), "**0** / 800 حرف" if len(content) > 800: return """
❌ الحد الأقصى 800 حرف
""", "", "", update_display(), f"**{len(content)}** / 800 حرف" if contains_bad_words(content): return """
❌ يوجد كلمات غير مناسبة
""", "", "", 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 """
❌ وصلت الحد المسموح (3 آراء في الساعة)
""", "", "", 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 """
✅ تم نشر رأيك بنجاح! 🎉
""", "", "", 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'{full_stars}{empty_stars}' 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 = """ ⭐ مميز """ role_html = f'{escape_safe(role)}' if role else "" return f"""
{escape_safe(name[0].upper())}
{escape_safe(name)} {role_html} {highlight_badge}
{format_time(timestamp)}
{escape_safe(content)}
{render_stars(rating)}
""" # ================================================== # تحديث العرض # ================================================== def update_display(page=1): rows, total, total_pages = get_testimonials(page) if not rows: return """
لا توجد آراء حالياً
كن أول من يشارك رأيه!
""" html = "" for row in rows: html += render_card(row) # أزرار التنقل if total_pages > 1: html += f"""
""" if page > 1: html += f''' ''' html += f''' {page} / {total_pages} ''' if page < total_pages: html += f''' ''' html += "
" 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("""

من منظورهم

.شارك رأيك بكل شفافية، فكل كلمة تُحدث فرقًا

""") # ===== الإحصائيات ===== 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 )