File size: 7,992 Bytes
1c48a5d
 
2c80af5
1c48a5d
2c80af5
 
 
1c48a5d
2c80af5
 
 
1c48a5d
 
 
 
 
 
 
 
2c80af5
1c48a5d
 
 
2c80af5
 
1c48a5d
 
 
 
 
f8aae10
1c48a5d
 
 
 
 
 
 
 
 
 
2c80af5
1c48a5d
49f9fdf
2c80af5
 
49f9fdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c80af5
 
 
1c48a5d
 
 
2c80af5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1c48a5d
 
 
2c80af5
 
1c48a5d
 
 
 
 
 
2c80af5
 
 
 
 
 
 
1c48a5d
 
 
 
 
 
f8aae10
 
1c48a5d
f8aae10
2c80af5
 
 
1c48a5d
 
2c80af5
1c48a5d
 
f8aae10
1c48a5d
 
 
 
 
 
 
2c80af5
1c48a5d
2c80af5
 
1c48a5d
 
 
2c80af5
1c48a5d
 
2c80af5
 
 
 
 
1c48a5d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c80af5
 
 
 
 
 
1c48a5d
 
 
 
2c80af5
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# ============================================================
# 📄 الملف: app/database.py
# 🎯 الغرض: طبقة قاعدة البيانات الوحيدة — MongoDB.
#
# يُقرأ رابط الاتصال MONGODB_URI من البيئة/الأسرار (لا يُكتب في الكود):
#   • محليًا   : من ملف .env  (مثال: mongodb://localhost:27017)
#   • على HF   : من أسرار الـ Space (رابط Atlas: mongodb+srv://...)
#
# مجموعتان (collections):
#   evaluators : المقيّمون (email فريد).
#   ratings    : وثيقة لكل كلمة قُيّمت (صح/خطأ) + اسم وإيميل المقيّم.
#
# أمان: عمليات الحذف في الواجهة مقصورة على إيميل المقيّم الحالي،
#       فلا يقدر مقيّم على حذف تقييمات غيره.
# ============================================================

import os
from datetime import datetime

import certifi
import pandas as pd
import streamlit as st

# اسم قاعدة البيانات الافتراضي إن لم يُحدَّد داخل الرابط
DEFAULT_DB_NAME = "reverse_dictionary"

# خرائط الأعمدة → أسماء عربية للعرض في الواجهة
_AR_COLUMNS = {
    "created_at": "التاريخ والوقت",
    "query": "الاستعلام",
    "word_type": "التصنيف",
    "model": "النموذج",
    "word": "الكلمة",
    "similarity": "نسبة التشابه",
    "verdict": "التقييم",
    "evaluator_name": "المقيّم",
    "evaluator_email": "الإيميل",
}


# ------------------------------------------------------------
# 🔌 قراءة الإعدادات والاتصال
# ------------------------------------------------------------
def _setting(key: str) -> str | None:
    """يقرأ إعدادًا من متغيّرات البيئة أولًا (HF Spaces / .env)، ثم من
    st.secrets فقط إن وُجد ملف secrets.toml — لتفادي تحذير «No secrets found»."""
    val = os.getenv(key)
    if val:
        return val
    secret_paths = (
        "/root/.streamlit/secrets.toml",
        "/app/.streamlit/secrets.toml",
        os.path.expanduser("~/.streamlit/secrets.toml"),
        ".streamlit/secrets.toml",
    )
    if any(os.path.exists(p) for p in secret_paths):
        try:
            return st.secrets[key]
        except Exception:
            return None
    return None


def is_configured() -> bool:
    """هل MONGODB_URI مضبوط؟ (التطبيق يتطلّبه للتخزين)."""
    return _setting("MONGODB_URI") is not None


@st.cache_resource(show_spinner=False)
def _get_db():
    """يفتح الاتصال مرة واحدة، وينشئ فهرس الإيميل الفريد.
    tlsCAFile يُستخدم تلقائيًا لروابط Atlas (mongodb+srv) ويُتجاهل محليًا."""
    from pymongo import MongoClient

    uri = _setting("MONGODB_URI") or ""
    # شهادات TLS لازمة لروابط Atlas (mongodb+srv/ssl)، ومضرّة محليًا (localhost)
    low = uri.lower()
    use_tls = ("mongodb+srv" in low) or ("tls=true" in low) or ("ssl=true" in low)
    kwargs = {"tlsCAFile": certifi.where()} if use_tls else {}
    client = MongoClient(uri, **kwargs)
    try:
        db = client.get_default_database()   # اسم القاعدة من الرابط إن وُجد
    except Exception:
        db = None
    if db is None:                           # الرابط بلا اسم قاعدة → الافتراضي
        db = client[DEFAULT_DB_NAME]
    db["evaluators"].create_index("email", unique=True)
    return db


def backend_label() -> str:
    """وصف مكان التخزين (يُعرض في الشريط الجانبي)."""
    return "MongoDB"


# ------------------------------------------------------------
# 👤 المقيّمون
# ------------------------------------------------------------
def upsert_evaluator(name: str, email: str) -> None:
    """يُدرج المقيّم أو يحدّث اسمه (المفتاح: الإيميل)."""
    _get_db()["evaluators"].update_one(
        {"email": email},
        {"$set": {"name": name},
         "$setOnInsert": {"email": email, "created_at": datetime.now()}},
        upsert=True,
    )


# ------------------------------------------------------------
# 💾 حفظ تقييم استعلام كامل (كل النماذج وكلماتها دفعة واحدة)
# ------------------------------------------------------------
def save_evaluation(query: str, per_model: list[dict],
                    evaluator_name: str, evaluator_email: str,
                    word_type: str = "") -> int:
    """per_model = [{"model": str, "items": [{"lemma","score","chosen"}, ...]}, ...].
    word_type = تصنيف الكلمة المختار في الواجهة (فعل / معنى معجمي متقدم / معنى عام).
    يرجّع عدد الوثائق المحفوظة."""
    ts = datetime.now()   # وقت واحد لكل وثائق هذا الحفظ (لتماسك التجميع)
    docs = []
    for block in per_model:
        for item in block["items"]:
            docs.append({
                "created_at": ts,
                "query": query,
                "word_type": word_type,
                "model": block["model"],
                "word": item["lemma"],
                "similarity": round(float(item["score"]), 3),
                "verdict": "صح" if item["chosen"] else "خطأ",
                "evaluator_name": evaluator_name,
                "evaluator_email": evaluator_email,
            })
    if not docs:
        return 0
    _get_db()["ratings"].insert_many(docs)
    return len(docs)


# ------------------------------------------------------------
# 📖 قراءة التقييمات (يمكن قصرها على مقيّم) — ترجع DataFrame بأعمدة عربية
# ------------------------------------------------------------
def load_ratings(evaluator_email: str | None = None) -> pd.DataFrame:
    flt = {"evaluator_email": evaluator_email} if evaluator_email else {}
    rows = list(_get_db()["ratings"].find(flt).sort("created_at", 1))
    if not rows:
        return pd.DataFrame()
    df = pd.DataFrame(rows).drop(columns=["_id"], errors="ignore")
    return df.rename(columns=_AR_COLUMNS)


# ------------------------------------------------------------
# 📊 ملخّص أداء النماذج (نسبة الصح) — يمكن قصره على مقيّم
# ------------------------------------------------------------
def get_summary(evaluator_email: str | None = None) -> pd.DataFrame:
    df = load_ratings(evaluator_email)
    if df.empty:
        return pd.DataFrame()
    summary = df.groupby("النموذج")["التقييم"].value_counts().unstack(fill_value=0)
    for col in ["صح", "خطأ"]:
        if col not in summary.columns:
            summary[col] = 0
    summary["الإجمالي"] = summary["صح"] + summary["خطأ"]
    summary["نسبة الصح"] = (summary["صح"] / summary["الإجمالي"] * 100).round(1).astype(str) + "%"
    return summary[["صح", "خطأ", "الإجمالي", "نسبة الصح"]]


# ------------------------------------------------------------
# 🗑️ حذف (مقصور على إيميل المقيّم الحالي للأمان)
# ------------------------------------------------------------
def delete_query(created_at, query: str, evaluator_email: str) -> int:
    """يحذف تقييم استعلام واحد لهذا المقيّم."""
    res = _get_db()["ratings"].delete_many({
        "created_at": created_at,
        "query": query,
        "evaluator_email": evaluator_email,
    })
    return res.deleted_count


def delete_all_for(evaluator_email: str) -> int:
    """يحذف كل تقييمات المقيّم الحالي فقط."""
    res = _get_db()["ratings"].delete_many({"evaluator_email": evaluator_email})
    return res.deleted_count