| import gradio as gr |
| import sqlite3 |
| import hashlib |
| import html |
| import re |
| from datetime import datetime, timedelta |
|
|
| DB_NAME = "testimonials.db" |
|
|
| |
| |
| |
|
|
| 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 |
| ) |
| """) |
|
|
| conn.commit() |
| conn.close() |
|
|
|
|
| |
| |
| |
|
|
| 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 = [ |
| "يلعن", |
| "تبن", |
| "سافل", |
| "كلب", |
| "خنزير", |
| "عاهرة" |
| ] |
|
|
| normalized = re.sub(r"\s+", "", text.lower()) |
|
|
| return any(word in normalized for word in bad_words) |
|
|
|
|
| def escape_safe(text): |
| return html.escape(text) |
|
|
|
|
| def get_ip_hash(request: gr.Request): |
|
|
| try: |
| client_ip = request.client.host |
| return hashlib.sha256( |
| client_ip.encode() |
| ).hexdigest()[:16] |
|
|
| except: |
| return "unknown" |
|
|
|
|
| def detect_sentiment(text): |
|
|
| positive_words = [ |
| "رائع", |
| "ممتاز", |
| "جميل", |
| "احترافي", |
| "مفيد", |
| "مذهل" |
| ] |
|
|
| negative_words = [ |
| "سيء", |
| "ضعيف", |
| "فاشل", |
| "سيئة" |
| ] |
|
|
| text = text.lower() |
|
|
| positive_score = sum( |
| word in text for word in positive_words |
| ) |
|
|
| negative_score = sum( |
| word in text for word in negative_words |
| ) |
|
|
| if positive_score > negative_score: |
| return "إيجابي 😊" |
|
|
| elif negative_score > positive_score: |
| return "سلبي 😕" |
|
|
| return "محايد 😐" |
|
|
|
|
| def format_time(timestamp): |
|
|
| try: |
| dt = datetime.fromisoformat(timestamp) |
|
|
| now = datetime.now() |
|
|
| diff = now - dt |
|
|
| if diff.days > 7: |
| return dt.strftime("%Y/%m/%d") |
|
|
| elif diff.days > 0: |
| return f"منذ {diff.days} يوم" |
|
|
| elif diff.seconds >= 3600: |
| hours = diff.seconds // 3600 |
| return f"منذ {hours} ساعة" |
|
|
| elif diff.seconds >= 60: |
| minutes = diff.seconds // 60 |
| return f"منذ {minutes} دقيقة" |
|
|
| return "الآن" |
|
|
| except: |
| return "غير معروف" |
|
|
|
|
| |
| |
| |
|
|
| 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)) |
|
|
| count = c.fetchone()[0] |
|
|
| if count >= 3: |
| conn.close() |
|
|
| return ( |
| "❌ وصلت الحد المسموح حالياً", |
| "", |
| "", |
| update_display() |
| ) |
|
|
| name = clean_text(name) |
| role = clean_text(role) |
|
|
| if not name: |
| name = "مجهول" |
|
|
| 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() |
|
|
| return ( |
| "✅ تم نشر رأيك بنجاح", |
| "", |
| "", |
| update_display() |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def get_testimonials( |
| filter_type="all", |
| sort_by="newest" |
| ): |
|
|
| conn = get_connection() |
| c = conn.cursor() |
|
|
| query = """ |
| SELECT |
| id, |
| name, |
| role, |
| content, |
| sentiment, |
| rating, |
| timestamp, |
| likes, |
| is_highlighted |
| FROM testimonials |
| """ |
|
|
| conditions = [] |
|
|
| 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 |
| """ |
|
|
| c.execute(query) |
|
|
| rows = c.fetchall() |
|
|
| conn.close() |
|
|
| return rows |
|
|
|
|
| |
| |
| |
|
|
| def render_card(row): |
|
|
| ( |
| testimonial_id, |
| name, |
| role, |
| content, |
| sentiment, |
| rating, |
| timestamp, |
| likes, |
| is_highlighted |
| ) = row |
|
|
| name = escape_safe(name) |
| role = escape_safe(role) |
| content = escape_safe(content) |
|
|
| stars = "⭐" * int(rating) |
|
|
| highlight_style = "" |
|
|
| if is_highlighted: |
| highlight_style = """ |
| border-right:5px solid #f59e0b; |
| background:#fffbeb; |
| """ |
|
|
| role_html = "" |
|
|
| if role: |
| role_html = f""" |
| <span style=" |
| background:#eef2ff; |
| color:#4338ca; |
| padding:0.25rem 0.7rem; |
| border-radius:999px; |
| font-size:0.75rem; |
| "> |
| {role} |
| </span> |
| """ |
|
|
| return f""" |
| <div style=" |
| background:white; |
| border-radius:24px; |
| padding:1.5rem; |
| margin-bottom:1rem; |
| border:1px solid #e2e8f0; |
| box-shadow:0 4px 12px rgba(0,0,0,0.05); |
| {highlight_style} |
| "> |
| |
| <div style=" |
| display:flex; |
| justify-content:space-between; |
| align-items:center; |
| flex-wrap:wrap; |
| gap:1rem; |
| "> |
| |
| <div> |
| |
| <div style=" |
| font-size:1rem; |
| font-weight:700; |
| color:#0f172a; |
| "> |
| {name} |
| </div> |
| |
| <div style=" |
| display:flex; |
| gap:0.5rem; |
| margin-top:0.5rem; |
| align-items:center; |
| flex-wrap:wrap; |
| "> |
| |
| {role_html} |
| |
| <span style=" |
| color:#64748b; |
| font-size:0.8rem; |
| "> |
| {sentiment} |
| </span> |
| |
| </div> |
| |
| </div> |
| |
| <div style=" |
| color:#94a3b8; |
| font-size:0.75rem; |
| "> |
| {format_time(timestamp)} |
| </div> |
| |
| </div> |
| |
| <div style=" |
| margin-top:1rem; |
| color:#1e293b; |
| line-height:1.8; |
| "> |
| {content} |
| </div> |
| |
| <div style=" |
| margin-top:1rem; |
| display:flex; |
| justify-content:space-between; |
| align-items:center; |
| "> |
| |
| <div> |
| {stars} |
| </div> |
| |
| <div style=" |
| color:#64748b; |
| font-size:0.9rem; |
| "> |
| 👍 {likes} |
| </div> |
| |
| </div> |
| |
| </div> |
| """ |
|
|
|
|
| |
| |
| |
|
|
| def update_display( |
| filter_type="all", |
| sort_by="newest" |
| ): |
|
|
| rows = get_testimonials( |
| filter_type, |
| sort_by |
| ) |
|
|
| if not rows: |
|
|
| return """ |
| <div style=" |
| background:white; |
| border-radius:24px; |
| padding:2rem; |
| text-align:center; |
| border:1px dashed #cbd5e1; |
| color:#64748b; |
| "> |
| ✨ لا توجد آراء حالياً |
| </div> |
| """ |
|
|
| average_rating = round( |
| sum(row[5] for row in rows) |
| / len(rows), |
| 1 |
| ) |
|
|
| html_output = f""" |
| <div style=" |
| margin-bottom:1rem; |
| background:#e0e7ff; |
| padding:0.8rem 1rem; |
| border-radius:999px; |
| display:inline-block; |
| font-weight:600; |
| color:#3730a3; |
| "> |
| 📊 عدد الآراء: {len(rows)} |
| | |
| ⭐ متوسط التقييم: {average_rating} |
| </div> |
| """ |
|
|
| for row in rows: |
| html_output += render_card(row) |
|
|
| return html_output |
|
|
|
|
| |
| |
| |
|
|
| def create_interface(): |
|
|
| with gr.Blocks( |
| title="جدار الشفافية", |
| theme=gr.themes.Soft() |
| ) as demo: |
|
|
| gr.HTML(""" |
| <div style=" |
| text-align:center; |
| margin-bottom:2rem; |
| "> |
| |
| <h1 style=" |
| font-size:2.7rem; |
| font-weight:800; |
| margin-bottom:0.5rem; |
| background: |
| linear-gradient( |
| 135deg, |
| #1e293b, |
| #4338ca |
| ); |
| -webkit-background-clip:text; |
| color:transparent; |
| "> |
| جدار الشفافية |
| </h1> |
| |
| <p style=" |
| color:#475569; |
| font-size:1rem; |
| "> |
| منصة آراء مباشرة وشفافة |
| </p> |
| |
| </div> |
| """) |
|
|
| with gr.Row(): |
|
|
| |
| with gr.Column(scale=1): |
|
|
| gr.Markdown( |
| "## 📝 أضف رأيك" |
| ) |
|
|
| 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 |
| ) |
|
|
| submit_btn = gr.Button( |
| "📢 نشر الرأي", |
| variant="primary" |
| ) |
|
|
| status_output = gr.Markdown() |
|
|
| |
| with gr.Column(scale=2): |
|
|
| gr.Markdown( |
| "## 💬 آراء المستخدمين" |
| ) |
|
|
| with gr.Row(): |
|
|
| filter_dropdown = gr.Dropdown( |
| choices=[ |
| ("الكل", "all"), |
| ("المؤثرة", "highlight"), |
| ("الأكثر إعجاباً", "liked") |
| ], |
| value="all", |
| label="فلترة" |
| ) |
|
|
| sort_dropdown = gr.Dropdown( |
| choices=[ |
| ("الأحدث", "newest"), |
| ("الأكثر إعجاباً", "likes") |
| ], |
| value="newest", |
| label="ترتيب" |
| ) |
|
|
| refresh_btn = gr.Button( |
| "🔄 تحديث", |
| variant="secondary" |
| ) |
|
|
| testimonials_display = gr.HTML() |
|
|
| |
| submit_btn.click( |
| fn=add_testimonial, |
| inputs=[ |
| name_input, |
| role_input, |
| content_input, |
| rating_input |
| ], |
| outputs=[ |
| status_output, |
| name_input, |
| role_input, |
| testimonials_display |
| ] |
| ) |
|
|
| |
| refresh_btn.click( |
| fn=update_display, |
| inputs=[ |
| filter_dropdown, |
| sort_dropdown |
| ], |
| outputs=testimonials_display |
| ) |
|
|
| |
| filter_dropdown.change( |
| fn=update_display, |
| inputs=[ |
| filter_dropdown, |
| sort_dropdown |
| ], |
| outputs=testimonials_display |
| ) |
|
|
| |
| sort_dropdown.change( |
| fn=update_display, |
| inputs=[ |
| filter_dropdown, |
| sort_dropdown |
| ], |
| outputs=testimonials_display |
| ) |
|
|
| |
| demo.load( |
| fn=update_display, |
| inputs=[ |
| filter_dropdown, |
| sort_dropdown |
| ], |
| outputs=testimonials_display |
| ) |
|
|
| return demo |
|
|
|
|
| |
| |
| |
|
|
| if __name__ == "__main__": |
|
|
| init_db() |
|
|
| demo = create_interface() |
|
|
| demo.launch() |
|
|
|
|