Feedbacks / app.py
EngReem85's picture
Update app.py
64e43b9 verified
Raw
History Blame Contribute Delete
25.9 kB
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", "your-username/your-space-name")
TOKEN = os.getenv("HF_TOKEN")
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ==================================================
# إدارة قاعدة البيانات
# ==================================================
def download_db_from_hub():
"""تحميل قاعدة البيانات من Hugging Face Hub"""
try:
if not TOKEN:
return False
local_path = hf_hub_download(
repo_id=REPO_ID,
filename=DB_NAME,
token=TOKEN,
local_dir="./",
local_dir_use_symlinks=False
)
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,
sentiment TEXT,
rating INTEGER DEFAULT 5,
timestamp TEXT NOT NULL,
likes INTEGER DEFAULT 0,
is_highlighted INTEGER DEFAULT 0,
ip_hash TEXT
)
""")
c.execute("""
CREATE TABLE IF NOT EXISTS likes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
testimonial_id INTEGER,
ip_hash TEXT,
created_at TEXT,
FOREIGN KEY (testimonial_id) REFERENCES testimonials(id) ON DELETE CASCADE
)
""")
# الفهارس
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 detect_sentiment(text):
positive = ["رائع", "ممتاز", "جميل", "احترافي", "مفيد", "مذهل", "أفضل", "متميز"]
negative = ["سيء", "ضعيف", "فاشل", "مخيب", "فظيع", "تافه"]
text_lower = text.lower()
pos_score = sum(1 for w in positive if w in text_lower)
neg_score = sum(1 for w in negative if w in text_lower)
if pos_score > neg_score:
return "إيجابي 😊"
elif neg_score > pos_score:
return "سلبي 😕"
return "محايد 😐"
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
# توزيع المشاعر
c.execute("""
SELECT
COUNT(CASE WHEN sentiment LIKE '%إيجابي%' THEN 1 END) as positive,
COUNT(CASE WHEN sentiment LIKE '%سلبي%' THEN 1 END) as negative,
COUNT(CASE WHEN sentiment LIKE '%محايد%' THEN 1 END) as neutral
FROM testimonials
""")
sentiments = c.fetchone()
# الأكثر إعجاباً
c.execute("""
SELECT name, content, likes
FROM testimonials
ORDER BY likes DESC
LIMIT 1
""")
most_liked = c.fetchone()
except Exception as e:
logger.error(f"خطأ في الإحصائيات: {e}")
return {"total": 0, "avg_rating": 0, "positive": 0, "negative": 0, "neutral": 0, "most_liked": None}
finally:
conn.close()
return {
"total": total,
"avg_rating": avg_rating,
"positive": sentiments[0] if sentiments else 0,
"negative": sentiments[1] if sentiments else 0,
"neutral": sentiments[2] if sentiments else 0,
"most_liked": most_liked
}
def render_stats(stats):
"""عرض الإحصائيات بشكل جميل"""
most_liked_html = ""
if stats['most_liked'] and stats['most_liked'][2] > 0:
most_liked_html = f"""
<div style="
background:#fef3c7;
padding:0.3rem 0.8rem;
border-radius:999px;
font-size:0.85rem;
color:#92400e;
">
🏆 الأكثر إعجاباً: {escape_safe(stats['most_liked'][0])} 👍 {stats['most_liked'][2]}
</div>
"""
return f"""
<div style="
display:grid;
grid-template-columns:repeat(auto-fit, minmax(140px, 1fr));
gap:0.8rem;
background:white;
padding:1rem 1.5rem;
border-radius:16px;
border:1px solid #e2e8f0;
margin-bottom:1.5rem;
box-shadow:0 2px 8px rgba(0,0,0,0.04);
">
<div style="text-align:center;">
<div style="font-size:1.8rem;font-weight:700;color:#1e293b;">{stats['total']}</div>
<div style="font-size:0.8rem;color:#64748b;">📝 آراء</div>
</div>
<div style="text-align:center;">
<div style="font-size:1.8rem;font-weight:700;color:#f59e0b;">{stats['avg_rating']}</div>
<div style="font-size:0.8rem;color:#64748b;">⭐ متوسط التقييم</div>
</div>
<div style="text-align:center;">
<div style="font-size:1.2rem;font-weight:600;color:#22c55e;">{stats['positive']}</div>
<div style="font-size:0.8rem;color:#64748b;">😊 إيجابي</div>
</div>
<div style="text-align:center;">
<div style="font-size:1.2rem;font-weight:600;color:#ef4444;">{stats['negative']}</div>
<div style="font-size:0.8rem;color:#64748b;">😕 سلبي</div>
</div>
<div style="text-align:center;">
<div style="font-size:1.2rem;font-weight:600;color:#94a3b8;">{stats['neutral']}</div>
<div style="font-size:0.8rem;color:#64748b;">😐 محايد</div>
</div>
{most_liked_html}
</div>
"""
# ==================================================
# إضافة رأي
# ==================================================
def add_testimonial(name, role, content, rating, request: gr.Request):
content = clean_text(content)
if not content:
return "❌ الرجاء كتابة رأيك", "", "", update_display()
if len(content) > 800:
return "❌ الحد الأقصى 800 حرف", "", "", update_display()
if contains_bad_words(content):
return "❌ يوجد كلمات غير مناسبة", "", "", update_display()
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()
name = clean_text(name) or "مجهول"
role = clean_text(role) or ""
sentiment = detect_sentiment(content)
c.execute("""
INSERT INTO testimonials (name, role, content, sentiment, rating, timestamp, likes, is_highlighted, ip_hash)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (name, role, content, sentiment, int(rating), datetime.now().isoformat(), 0, 0, ip_hash))
conn.commit()
conn.close()
upload_db_to_hub()
return "✅ تم نشر رأيك بنجاح!", "", "", update_display()
# ==================================================
# جلب الآراء
# ==================================================
def get_testimonials(filter_type="all", sort_by="newest", page=1, per_page=5):
conn = get_connection()
c = conn.cursor()
query = "SELECT id, name, role, content, sentiment, rating, timestamp, likes, is_highlighted FROM testimonials"
conditions = []
params = []
if filter_type == "highlight":
conditions.append("is_highlighted = 1")
elif filter_type == "liked":
conditions.append("likes > 0")
if conditions:
query += " WHERE " + " AND ".join(conditions)
if sort_by == "likes":
query += " ORDER BY likes DESC, timestamp DESC"
else:
query += " ORDER BY timestamp DESC"
offset = (page - 1) * per_page
query += " LIMIT ? OFFSET ?"
params.extend([per_page, offset])
c.execute(query, params)
rows = c.fetchall()
# العدد الإجمالي
count_query = "SELECT COUNT(*) FROM testimonials"
if conditions:
count_query += " WHERE " + " AND ".join(conditions)
c.execute(count_query)
total = c.fetchone()[0]
conn.close()
return rows, total, (total + per_page - 1) // per_page if total > 0 else 1
# ==================================================
# عرض البطاقات
# ==================================================
def render_card(row):
(id, name, role, content, sentiment, rating, timestamp, likes, is_highlighted) = row
highlight_style = ""
if is_highlighted:
highlight_style = "border-right:4px solid #f59e0b; background:#fffbeb;"
stars = "⭐" * int(rating)
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 ""
return f"""
<div style="
background:white;
border-radius:16px;
padding:1rem 1.2rem;
margin-bottom:0.8rem;
border:1px solid #e2e8f0;
box-shadow:0 2px 8px rgba(0,0,0,0.04);
{highlight_style}
transition: all 0.2s;
">
<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.5rem;flex-wrap:wrap;">
<span style="font-weight:700;color:#0f172a;">{escape_safe(name)}</span>
{role_html}
<span style="color:#64748b;font-size:0.75rem;">{sentiment}</span>
</div>
<span style="color:#94a3b8;font-size:0.7rem;">{format_time(timestamp)}</span>
</div>
<div style="margin-top:0.5rem;color:#1e293b;line-height:1.7;font-size:0.95rem;">
{escape_safe(content)}
</div>
<div style="margin-top:0.5rem;display:flex;justify-content:space-between;align-items:center;">
<span>{stars}</span>
<span style="color:#64748b;font-size:0.8rem;">👍 {likes}</span>
</div>
</div>
"""
# ==================================================
# تحديث العرض
# ==================================================
def update_display(filter_type="all", sort_by="newest", page=1):
rows, total, total_pages = get_testimonials(filter_type, sort_by, page)
if not rows:
return """
<div style="
background:white;
border-radius:16px;
padding:2rem;
text-align:center;
border:1px dashed #cbd5e1;
color:#64748b;
">
✨ لا توجد آراء حالياً
</div>
"""
html = ""
for row in rows:
html += render_card(row)
# أزرار التنقل
if total_pages > 1:
html += f"""
<div style="display:flex;justify-content:center;gap:0.5rem;margin-top:0.8rem;">
"""
if page > 1:
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>'
html += f'<span style="padding:0.4rem 1rem;color:#64748b;">{page} / {total_pages}</span>'
if page < total_pages:
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>'
html += "</div>"
return html
# ==================================================
# تصدير البيانات
# ==================================================
def export_data():
conn = get_connection()
c = conn.cursor()
c.execute("SELECT name, role, content, sentiment, rating, timestamp, likes FROM testimonials ORDER BY timestamp DESC")
rows = c.fetchall()
conn.close()
if not rows:
return "لا توجد بيانات للتصدير"
# تصدير كـ JSON
data = []
for row in rows:
data.append({
"الاسم": row[0],
"الدور": row[1],
"الرأي": row[2],
"المشاعر": row[3],
"التقييم": row[4],
"التاريخ": row[5],
"الإعجابات": row[6]
})
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():
custom_css = """
.gradio-container {
max-width: 1200px !important;
margin: 0 auto !important;
padding: 0.5rem !important;
}
@media (max-width: 768px) {
.grid-container {
grid-template-columns: 1fr !important;
}
}
"""
with gr.Blocks(
title="منصة الآراء",
theme=gr.themes.Soft(),
css=custom_css,
fill_width=True
) as demo:
# ===== الهيدر =====
gr.HTML("""
<div style="
text-align:center;
padding:1rem 0 0.5rem 0;
">
<h1 style="
font-size:2.2rem;
font-weight:800;
margin:0;
background:linear-gradient(135deg, #1e293b, #4338ca);
-webkit-background-clip:text;
-webkit-text-fill-color:transparent;
">
💬 منصة الآراء
</h1>
<p style="color:#64748b;margin:0.2rem 0 0 0;font-size:0.9rem;">
شارك رأيك بكل شفافية • جميع الآراء محفوظة بشكل دائم
</p>
</div>
""")
# ===== الإحصائيات =====
stats_display = gr.HTML()
# ===== المحتوى الرئيسي =====
with gr.Row(equal_height=False):
# ===== العمود الأيسر: إضافة رأي =====
with gr.Column(scale=1, min_width=280):
gr.Markdown("### ✍️ أضف رأيك")
name_input = gr.Textbox(
label="الاسم",
placeholder="اختياري",
container=False
)
role_input = gr.Textbox(
label="الدور",
placeholder="مثال: مهندس",
container=False
)
rating_input = gr.Slider(
minimum=1,
maximum=5,
value=5,
step=1,
label="⭐ التقييم"
)
content_input = gr.Textbox(
label="الرأي",
placeholder="اكتب رأيك هنا...",
lines=4,
container=False
)
with gr.Row():
submit_btn = gr.Button("📢 نشر", variant="primary", scale=2)
clear_btn = gr.Button("🗑️ مسح", scale=1)
status_output = gr.Markdown()
# ===== العمود الأيمن: عرض الآراء =====
with gr.Column(scale=2, min_width=400):
gr.Markdown("### 💬 آراء المستخدمين")
with gr.Row():
filter_dropdown = gr.Dropdown(
choices=[
("الكل", "all"),
("المميزة", "highlight"),
("الأكثر إعجاباً", "liked")
],
value="all",
label="🔍 فلترة",
scale=1
)
sort_dropdown = gr.Dropdown(
choices=[
("الأحدث", "newest"),
("الأكثر إعجاباً", "likes")
],
value="newest",
label="📊 ترتيب",
scale=1
)
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(filter_type="all", sort_by="newest", page=1):
return update_stats(), update_display(filter_type, sort_by, page)
# زر النشر
submit_btn.click(
fn=add_testimonial,
inputs=[name_input, role_input, content_input, rating_input],
outputs=[status_output, name_input, role_input, testimonials_display]
).then(
fn=update_stats,
outputs=stats_display
)
# زر التحديث
refresh_btn.click(
fn=full_update,
inputs=[filter_dropdown, sort_dropdown],
outputs=[stats_display, testimonials_display]
)
# تغيير الفلترة/الترتيب
filter_dropdown.change(
fn=full_update,
inputs=[filter_dropdown, sort_dropdown],
outputs=[stats_display, testimonials_display]
)
sort_dropdown.change(
fn=full_update,
inputs=[filter_dropdown, sort_dropdown],
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: ("", "", "", ""),
outputs=[name_input, role_input, content_input, status_output]
)
# تحميل أولي
demo.load(
fn=full_update,
inputs=[filter_dropdown, sort_dropdown],
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)