import html import sys import gradio as gr import numpy as np from search_service import search_df # =============================== # Helper functions # =============================== def safe_get(row, col): try: v = row.get(col, "") if v is None or (isinstance(v, float) and np.isnan(v)): return "" return str(v).strip() except Exception: return "" def first_nonempty(row, cols): for c in cols: val = safe_get(row, c) if val: return val return "" def short_preview(text, length=300): text = (text or "").strip() if len(text) <= length: return text return text[:length].rsplit(" ", 1)[0] + "..." def search_hadith_tool(query: str, top_k: int = 5) -> list[dict]: """ Search Sahih al-Bukhari for hadiths relevant to a query. Uses ColBERT late-interaction semantic search combined with BM25. The query can be a topic, question or description of a meaning, preferably in Arabic. Args: query: Non-empty search text. top_k: Number of hadiths to return, 1 to 20 (default 5). Values above 20 are limited to 20. Returns: A list of hadiths, best match first, each with "hadith", "isnad_text" and "url". """ try: results_df, _ = search_df(query, top_k) except ValueError as e: raise gr.Error(str(e)) return [ { "hadith": str(row["matn_text"]), "isnad_text": str(row["isnad_text"]), "url": str(row["url"]), } for _, row in results_df.iterrows() ] # =============================== # Search function (UI layer only) # =============================== def search_hadith(query, top_k): try: results_df, _ = search_df(query, int(top_k)) except ValueError: # empty query or invalid top_k: friendly message for humans return "

الرجاء إدخال استعلام بحث

" except Exception as e: print(f"[search_hadith] internal error: {e!r}", file=sys.stderr) return "

حدث خطأ أثناء البحث، حاول مرة أخرى

" html_parts = ["
"] for rank, (hadith_idx, row) in enumerate(results_df.iterrows(), start=1): title_val = ( first_nonempty(row, ["isnad_text", "hadith_title", "title", "main_subj"]) or short_preview(safe_get(row, "matn_text"), 60) or f"حديث #{hadith_idx}" ) clean_text = safe_get(row, "matn_text") clean_text_escaped = html.escape(clean_text).replace("\n", "
") topic = html.escape(safe_get(row, "main_subj")) source_url = safe_get(row, "url") score_display = f"{float(row['hybrid_score']):.3f}" preview_escaped = html.escape(short_preview(clean_text, 360)).replace("\n", "
") link_html = ( f"📖 عرض المصدر" if source_url.startswith("http") else "" ) card_html = f"""
#{rank}
{html.escape(title_val)}
الموضوع: {topic}
الرقم: {hadith_idx}
الدرجة: {score_display}
{preview_escaped}
{clean_text_escaped}
""" html_parts.append(card_html) html_parts.append("
") return "\n".join(html_parts) # =============================== # Styling # =============================== CUSTOM_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700&family=Noto+Sans+Arabic:wght@400;500;700&display=swap'); :root { --body-background-fill: #020617 !important; --background-fill-primary: #020617 !important; --background-fill-secondary: #0f172a !important; --border-color-primary: #334155 !important; --color-text-primary: #f1f5f9 !important; --color-text-secondary: #cbd5e1 !important; --button-primary-background-fill: #3b82f6 !important; --button-primary-background-fill-hover: #2563eb !important; } body, .gradio-container, .gr-panel, .gr-box, .gr-form, .wrap, .panel, .block, footer { background-color: #020617 !important; color: #f1f5f9 !important; font-family: 'Cairo', 'Noto Sans Arabic', system-ui, sans-serif !important; } .gradio-container { max-width: 960px !important; margin: 0 auto !important; } .text-rtl, .full-text, .summary, .title, .topic { font-family: 'Cairo', 'Noto Sans Arabic', sans-serif !important; line-height: 2.05 !important; } label span, input, textarea, .gr-input, .gr-textarea { color: #f1f5f9 !important; background: #1e2937 !important; border-color: #475569 !important; } button.primary { background: linear-gradient(90deg, #3b82f6, #60a5fa) !important; font-weight: 600 !important; } .card { background: #1e2937 !important; border-radius: 18px !important; padding: 26px !important; margin-bottom: 24px !important; box-shadow: 0 10px 30px rgba(0,0,0,0.4) !important; border: 1px solid #334155 !important; transition: all 0.35s ease !important; } .card:hover { transform: translateY(-8px); box-shadow: 0 25px 50px rgba(59, 130, 246, 0.2) !important; } .card-header { display: flex; justify-content: space-between; gap: 16px; margin-bottom: 14px; } .index { display: inline-block; background: linear-gradient(90deg, #3b82f6, #60a5fa) !important; color: #fff !important; padding: 7px 16px !important; border-radius: 9999px !important; font-weight: 700 !important; } .title { font-size: 19px !important; font-weight: 700 !important; } .topic { font-size: 14px !important; color: #94a3b8 !important; } .meta { font-size: 13px; color: #94a3b8; } .text-rtl { direction: rtl; text-align: right; background: #0f172a !important; border-radius: 12px !important; padding: 18px !important; border-right: 6px solid #3b82f6 !important; font-size: 17px !important; color: #e2e8f0 !important; } .results { margin-top: 24px; } .footer a { color: #60a5fa !important; font-weight: 600 !important; } .empty { color: #94a3b8 !important; text-align: center; padding: 50px 20px; font-size: 17px; } @media (max-width: 768px) { .card-header { flex-direction: column !important; } } """ # =============================== # Interface # =============================== ui = gr.Interface( fn=search_hadith, inputs=[ gr.Textbox( label="🔍 استعلام البحث", placeholder="مثال: أهمية النية في الإسلام", lines=2, max_lines=3, ), gr.Slider(minimum=1, maximum=20, value=5, step=1, label="📌 عدد النتائج"), ], outputs=gr.HTML(), title="📖 Intelligent Hadith Search Engine", description=( "Semantic search over Sahih al-Bukhari.
" "Combines ColBERT late-interaction retrieval with BM25 lexical search." ), examples=[ ["أهمية النية وأثرها في قبول الأعمال", 5], ["حقوق الوالدين في الإسلام", 5], ["فضل طلب العلم", 5], ], cache_examples=False, # don't run searches at startup flagging_mode="never", api_name=False, ) with gr.Blocks() as demo: ui.render() gr.api(search_hadith_tool, api_name="search_hadith") # one search at a time: the model runs on CPU, parallel calls would just slow each other demo.queue(default_concurrency_limit=1, max_size=10) if __name__ == "__main__": # Gradio 6: theme and css are passed to launch(), not the constructor demo.launch(mcp_server=True, theme=gr.themes.Soft(), css=CUSTOM_CSS) # one search at a time: the model runs on CPU, parallel calls would just slow each other