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
)