Spaces:
Running
Running
| # app.py | |
| # ================== الاستيراد ================== | |
| import gradio as gr | |
| import pandas as pd | |
| import numpy as np | |
| import tempfile | |
| import os, pickle | |
| from datetime import datetime | |
| import io, base64 | |
| import arabic_reshaper | |
| from bidi.algorithm import get_display | |
| import matplotlib.pyplot as plt | |
| from sentence_transformers import SentenceTransformer, util | |
| from Bio import Entrez | |
| import gdown | |
| # --- إضافات خاصة بالـ Hugging Face dataset sync --- | |
| from huggingface_hub import HfApi, hf_hub_download, upload_file | |
| # ---------------------------------------------------------------- | |
| # ------------------ Data store & Google Drive sync (background sync) ------------------ | |
| import shutil, time, threading | |
| from datetime import datetime | |
| import os | |
| DATA_FOLDER = "data_store" | |
| DRIVE_FOLDER_ID = "1znvCrr63iSN2MWveAZvcXuGT3o5TfZc2" # Library_Backups folder ID on your Drive | |
| def ensure_data_folder(): | |
| os.makedirs(DATA_FOLDER, exist_ok=True) | |
| return DATA_FOLDER | |
| def get_data_path(filename): | |
| ensure_data_folder() | |
| return os.path.join(DATA_FOLDER, filename) | |
| # Attempt to import PyDrive2 and set up authentication functions. | |
| try: | |
| from pydrive2.auth import GoogleAuth | |
| from pydrive2.drive import GoogleDrive | |
| from oauth2client.service_account import ServiceAccountCredentials | |
| _GD_AVAILABLE = True | |
| except Exception: | |
| _GD_AVAILABLE = False | |
| def ensure_drive_auth(): | |
| """ | |
| Authenticate to Google Drive using a Service Account JSON file (client_secrets.json) | |
| placed inside data_store/. Works automatically on Spaces without manual login. | |
| """ | |
| if not _GD_AVAILABLE: | |
| print("❌ PyDrive2 not available.") | |
| return None | |
| try: | |
| service_account_path = os.path.join(DATA_FOLDER, "client_secrets.json") | |
| if not os.path.exists(service_account_path): | |
| print("⚠️ لم يتم العثور على ملف client_secrets.json في data_store/") | |
| return None | |
| # Create credentials from service account JSON | |
| scope = ['https://www.googleapis.com/auth/drive'] | |
| credentials = ServiceAccountCredentials.from_json_keyfile_name(service_account_path, scope) | |
| gauth = GoogleAuth() | |
| gauth.credentials = credentials | |
| drive = GoogleDrive(gauth) | |
| print("✅ تم الاتصال بـ Google Drive باستخدام Service Account بنجاح.") | |
| return drive | |
| except Exception as e: | |
| print("❌ فشل الاتصال بـ Google Drive:", e) | |
| return None | |
| def upload_to_drive(local_path, remote_name=None): | |
| """ | |
| Uploads a local file to Google Drive inside the folder defined by DRIVE_FOLDER_ID. | |
| """ | |
| try: | |
| drive = ensure_drive_auth() | |
| if drive is None: | |
| print("⚠️ لم يتم الاتصال بـ Google Drive، لم يتم رفع الملف:", local_path) | |
| return False | |
| if remote_name is None: | |
| remote_name = os.path.basename(local_path) | |
| q = f"'{DRIVE_FOLDER_ID}' in parents and title = '{remote_name}' and trashed=false" | |
| file_list = drive.ListFile({'q': q}).GetList() | |
| if file_list: | |
| f = file_list[0] | |
| f.SetContentFile(local_path) | |
| f.Upload() | |
| print(f"🔁 تم تحديث الملف الموجود في Google Drive: {remote_name}") | |
| else: | |
| file_metadata = {'title': remote_name, 'parents': [{'id': DRIVE_FOLDER_ID}]} | |
| f = drive.CreateFile(file_metadata) | |
| f.SetContentFile(local_path) | |
| f.Upload() | |
| print(f"✅ تم رفع ملف جديد إلى Google Drive: {remote_name}") | |
| return True | |
| except Exception as e: | |
| print("❌ خطأ أثناء رفع الملف إلى Google Drive:", e) | |
| return False | |
| def _background_sync(interval_seconds=30): | |
| """ | |
| Background thread to watch for new or modified CSV/XLSX files and upload them to Google Drive automatically. | |
| """ | |
| def _worker(): | |
| seen = {} | |
| while True: | |
| try: | |
| # scan for csv and xlsx files in project root | |
| for fname in os.listdir(): | |
| if fname.lower().endswith(('.csv', '.xlsx')) and fname not in (os.path.basename(__file__),): | |
| try: | |
| src_path = os.path.join(os.getcwd(), fname) | |
| dst_path = get_data_path(fname) | |
| # check modification time | |
| mtime = os.path.getmtime(src_path) | |
| if fname not in seen or seen[fname] < mtime: | |
| shutil.copy2(src_path, dst_path) | |
| seen[fname] = mtime | |
| success = upload_to_drive(dst_path, os.path.basename(dst_path)) | |
| if success: | |
| print(f"📤 تمت مزامنة الملف مع Google Drive: {fname} ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})") | |
| else: | |
| print(f"⚠️ فشل رفع الملف: {fname}") | |
| except Exception as e: | |
| print("❌ خطأ أثناء المزامنة:", e) | |
| except Exception: | |
| pass | |
| time.sleep(interval_seconds) | |
| t = threading.Thread(target=_worker, daemon=True) | |
| t.start() | |
| # تشغيل المزامنة التلقائية كل 20 ثانية | |
| try: | |
| _background_sync(interval_seconds=20) | |
| except Exception as e: | |
| print("⚠️ لم يتم تشغيل المزامنة التلقائية:", e) | |
| # ------------------ end background sync ------------------ | |
| # ================== HF dataset sync configuration ================== | |
| DATASET_REPO = "pharma-library/AI_LibExplorer" | |
| HF_TOKEN = os.getenv("HF_TOKEN") # يجب وضع هذا التوكن في Secrets باسم HF_TOKEN | |
| ADMIN_PASS = os.getenv("ADMIN_PASS", "pharmacy1") # ضع Secret باسم ADMIN_PASS بقيمة pharmacy1 | |
| # قائمة الملفات التي نريد التأكد من توافرها ومزامنتها | |
| LOCAL_FILES = [ | |
| "usage_stats.xlsx", | |
| "questions.csv", | |
| "suggestions.csv", | |
| "user_logs.csv", | |
| "faculty_borrow.csv", | |
| "bank_requests.csv" | |
| ] | |
| def download_data_files(): | |
| """Download listed files from dataset repo to local project root if present.""" | |
| for file_name in LOCAL_FILES: | |
| try: | |
| # hf_hub_download سيحفظ الملف في local_dir كما طلبنا | |
| hf_hub_download( | |
| repo_id=DATASET_REPO, | |
| filename=file_name, | |
| repo_type="dataset", | |
| local_dir=".", | |
| token=HF_TOKEN | |
| ) | |
| print(f"✅ تم تنزيل {file_name} من الـ Dataset.") | |
| except Exception as e: | |
| # الملف غير موجود بعد في الـ dataset — ليس خطأ، سيتم إنشاؤه عند الحاجة | |
| print(f"ℹ️ {file_name} غير موجود على الـ Dataset حالياً ({e}). سيُنشأ لاحقًا عند كتابة البيانات.") | |
| def upload_data_file(file_path): | |
| """Upload given file_path (relative path) to the dataset repo (overwrites/creates).""" | |
| try: | |
| # upload_file من huggingface_hub سيرفع الملف إلى المسار path_in_repo المحدد | |
| upload_file( | |
| path_or_fileobj=file_path, | |
| path_in_repo=os.path.basename(file_path), | |
| repo_id=DATASET_REPO, | |
| repo_type="dataset", | |
| token=HF_TOKEN | |
| ) | |
| print(f"✅ Uploaded {file_path} to {DATASET_REPO}") | |
| except Exception as e: | |
| print(f"⚠️ خطأ في رفع {file_path} إلى HF dataset: {e}") | |
| # تحميل الملفات عند بداية تشغيل التطبيق | |
| download_data_files() | |
| # ================== end HF dataset sync ================== | |
| # ================== دوال مساعدة ================== | |
| def download_from_drive(file_id, output): | |
| url = f"https://drive.google.com/uc?id={file_id}" | |
| try: | |
| gdown.download(url, output, quiet=True) | |
| except Exception: | |
| # إذا فشل التحميل من Drive نتابع لوجود ملفات محلية بالفعل | |
| pass | |
| # ================== إعدادات ومسارات ================== | |
| Entrez.email = "emananter0123@gmail.com" | |
| EMB_DIR = "embeddings_cache" | |
| os.makedirs(EMB_DIR, exist_ok=True) | |
| def embeddings_path(name): | |
| return os.path.join(EMB_DIR, f"{name}_embeddings.pkl") | |
| BOOKS_FILE, THESES_FILE, FAQ_FILE = "book.xlsx", "theses.xlsx", "faq.xlsx" | |
| QUESTIONS_FILE = "questions.csv" # ملف حفظ الأسئلة | |
| FACULTY_BORROW_FILE = "faculty_borrow.csv" # طلبات أعضاء هيئة التدريس للاستعارة | |
| # محاولة تنزيل الملفات من Google Drive (إن أردتِ يمكنك حذف السطور التالية إذا الملفات محلية) | |
| download_from_drive("1FElHiASfiVLeuHWYaqd2Q5foxWRlJT-O", BOOKS_FILE) # book.xlsx | |
| download_from_drive("1K2Mtze6ZdvfKUsFMCOWlRBjDq-ZnJNrv", THESES_FILE) # theses.xlsx | |
| download_from_drive("1ONjXtfv709BOxiIR0Qzmz6lQngnWEEso", FAQ_FILE) # faq.xlsx | |
| # ================== تحميل البيانات المحلية ================== | |
| def load_local_data(): | |
| if not os.path.exists(BOOKS_FILE) or not os.path.exists(THESES_FILE): | |
| raise FileNotFoundError("تأكدي من رفع الملفات book.xlsx و theses.xlsx في مجلد المشروع.") | |
| books = pd.read_excel(BOOKS_FILE).fillna("غير متوافر") | |
| theses = pd.read_excel(THESES_FILE).fillna("غير متوافر") | |
| # توحيد اسم عمود العنوان بين ملفات مختلفة | |
| if "Title" not in books.columns and "العنوان" in books.columns: | |
| books["Title"] = books["العنوان"].astype(str) | |
| elif "Title" not in books.columns: | |
| books["Title"] = books.iloc[:,0].astype(str) | |
| if "Title" not in theses.columns and "العنوان" in theses.columns: | |
| theses["Title"] = theses["العنوان"].astype(str) | |
| elif "Title" not in theses.columns: | |
| theses["Title"] = theses.iloc[:,0].astype(str) | |
| return books, theses | |
| books_df, theses_df = load_local_data() | |
| # ================== نموذج Semantic ================== | |
| MODEL_NAME = "all-MiniLM-L6-v2" | |
| model = SentenceTransformer(MODEL_NAME) | |
| def build_or_load_embeddings(df, name): | |
| path = embeddings_path(name) | |
| if os.path.exists(path): | |
| try: | |
| with open(path,"rb") as f: | |
| emb = pickle.load(f) | |
| if len(emb) == len(df): | |
| return emb | |
| except Exception: | |
| pass | |
| texts = df["Title"].astype(str).tolist() | |
| emb = model.encode(texts, convert_to_numpy=True, show_progress_bar=True) | |
| with open(path,"wb") as f: | |
| pickle.dump(emb,f) | |
| return emb | |
| books_embeddings = build_or_load_embeddings(books_df,"books") | |
| theses_embeddings = build_or_load_embeddings(theses_df,"theses") | |
| # ================== FAQ & مساعد ذكي ================== | |
| if os.path.exists(FAQ_FILE): | |
| faq_df = pd.read_excel(FAQ_FILE).fillna("") | |
| else: | |
| faq_df = pd.DataFrame({ | |
| "السؤال":["مواعيد المكتبة","خدمات المكتبة","كيفية الاستعارة","التسجيل في بنك المعرفة","التصوير","أقسام المكتبة","التواصل"], | |
| "الإجابة":[ | |
| "🕘 المكتبة تعمل من السبت إلى الخميس من 9 صباحًا حتى 2 ظهرًا", | |
| "📌 خدمات المكتبة: الإحاطة الجارية، التسجيل على بنك المعرفة، التصوير، البحث الإلكتروني، الاستعارة الخارجية", | |
| "✅ يتم استعارة الكتب لمدة أسبوعين (أو حسب القاعدة المحلية)", | |
| "🔗 التسجيل في بنك المعرفة عبر موقع بنك المعرفة المصري", | |
| "🖨️ التصوير متاح وفق القواعد", | |
| "📚 المكتبة بها: قاعة المراجع، قاعة الكتب الدراسية، قاعة الدوريات، قاعة الرسائل الجامعية", | |
| "☎️ للتواصل: <a href='https://www.facebook.com/share/1AuUSQUn4n/' target='_blank'>صفحة الفيسبوك</a>" | |
| ] | |
| }) | |
| faq_questions = faq_df["السؤال"].astype(str).tolist() | |
| faq_answers = faq_df["الإجابة"].astype(str).tolist() | |
| FAQ_EMB_PATH = embeddings_path("faq") | |
| def build_or_load_faq_embeddings(questions): | |
| if os.path.exists(FAQ_EMB_PATH): | |
| try: | |
| with open(FAQ_EMB_PATH,"rb") as f: | |
| emb = pickle.load(f) | |
| if len(emb) == len(questions): | |
| return emb | |
| except Exception: | |
| pass | |
| emb = model.encode(questions, convert_to_numpy=True, show_progress_bar=False) | |
| with open(FAQ_EMB_PATH,"wb") as f: | |
| pickle.dump(emb,f) | |
| return emb | |
| faq_embeddings = build_or_load_faq_embeddings(faq_questions) | |
| def library_assistant_smart(question): | |
| if not str(question).strip(): | |
| return "⚠️ من فضلك اكتب سؤالك" | |
| q_emb = model.encode([str(question)], convert_to_numpy=True) | |
| sims = util.cos_sim(q_emb, faq_embeddings)[0].cpu().numpy() | |
| idx_best = int(np.argmax(sims)) | |
| best_score = sims[idx_best] | |
| log_usage("المساعد الذكي", question, "FAQ", 1) | |
| if best_score < 0.75: | |
| return "❗ لم أجد إجابة مناسبة، حاول صياغة سؤالك بشكل مختلف." | |
| return f"<b>الإجابة الأقرب:</b><br>{faq_answers[idx_best]}<br><i>درجة التشابه: {best_score:.2f}</i>" | |
| # ================== CSS ================== | |
| CUSTOM_CSS = """ | |
| <style> | |
| .styled-table{border-collapse:collapse;margin:15px 0;font-size:14px;width:100%;text-align:right;direction:rtl;} | |
| .styled-table th,.styled-table td{border:1px solid #ddd;padding:8px;} | |
| .styled-table tr:nth-child(even){background-color:#f9f9f9;} | |
| .styled-table tr:nth-child(odd){background-color:#fff;} | |
| .styled-table th{background-color:#4da6ff;color:white;} | |
| a{color:#0066cc;text-decoration:none;} | |
| a:hover{text-decoration:underline;} | |
| .question-card{border:1px solid #ddd;padding:10px;margin-bottom:12px;border-radius:8px;background:#fff;direction:rtl;} | |
| .question-meta{font-size:13px;color:#666;margin-bottom:6px;} | |
| .answer{margin-top:8px;padding:8px;border-radius:6px;background:#f7f7f7;} | |
| .anon{color:#999;font-style:italic;} | |
| .tabs.svelte-1ipelgc{display:flex!important;flex-direction:row!important;flex-wrap:nowrap;overflow-x:auto;gap:6px;scrollbar-width:thin;scrollbar-color:#4da6ff #eee;background:#f5f8ff;padding:8px;border-radius:10px;} | |
| .tabs.svelte-1ipelgc::-webkit-scrollbar{height:6px;} | |
| .tabs.svelte-1ipelgc::-webkit-scrollbar-thumb{background:#4da6ff;border-radius:4px;} | |
| .tabitem.svelte-1ipelgc{background:white;color:#333;border:1px solid #ccc;border-radius:8px;padding:8px 14px;cursor:pointer;transition:all .2s ease;font-size:15px;white-space:nowrap;flex-shrink:0;} | |
| .tabitem.svelte-1ipelgc:hover{background:#e7f0ff;border-color:#4da6ff;} | |
| .tabitem.svelte-1ipelgc[aria-selected='true']{background-color:#4da6ff!important;color:white!important;font-weight:bold;box-shadow:0 2px 5px rgba(0,0,0,.1);} | |
| @media(max-width:768px){ | |
| .tabs.svelte-1ipelgc{flex-wrap:wrap;justify-content:center;} | |
| .tabitem.svelte-1ipelgc{flex:1 1 45%;text-align:center;margin-bottom:4px;font-size:15px;} | |
| } | |
| </style> | |
| """ | |
| # ================== تتبع الاستخدام ================== | |
| def log_usage(tab, query="", mode="", results_count=0): | |
| file_path = "usage_stats.xlsx" | |
| entry = pd.DataFrame([{ | |
| "التاريخ": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| "التب": tab, | |
| "الكلمة المفتاحية": query, | |
| "نوع البحث": mode, | |
| "عدد النتائج": results_count | |
| }]) | |
| if os.path.exists(file_path): | |
| try: | |
| old = pd.read_excel(file_path) | |
| entry = pd.concat([old, entry], ignore_index=True) | |
| except Exception: | |
| entry = entry | |
| entry.to_excel(file_path, index=False) | |
| # upload to HF dataset (auto backup) | |
| try: | |
| upload_data_file(file_path) | |
| except Exception as e: | |
| print("⚠️ خطأ أثناء رفع usage_stats:", e) | |
| # ================== عرض إحصاءات الاستخدام ================== | |
| def show_usage_stats(): | |
| file_path = "usage_stats.xlsx" | |
| if not os.path.exists(file_path): | |
| return "<p>📊 لا توجد بيانات بعد</p>" | |
| df = pd.read_excel(file_path) | |
| total = len(df) | |
| by_tab = df["التب"].value_counts() | |
| fig, ax = plt.subplots() | |
| labels = [get_display(arabic_reshaper.reshape(str(t))) for t in by_tab.index] | |
| ax.bar(labels, by_tab.values) | |
| for i, v in enumerate(by_tab.values): | |
| ax.text(i, v + 0.1, str(v), ha='center', va='bottom', fontsize=10) | |
| ax.set_title(get_display(arabic_reshaper.reshape("عدد مرات استخدام كل تب"))) | |
| ax.set_xlabel(get_display(arabic_reshaper.reshape("التب"))) | |
| ax.set_ylabel(get_display(arabic_reshaper.reshape("عدد العمليات"))) | |
| plt.xticks(rotation=30, ha='right') | |
| buf = io.BytesIO() | |
| plt.tight_layout() | |
| plt.savefig(buf, format="png") | |
| plt.close(fig) | |
| buf.seek(0) | |
| img = base64.b64encode(buf.read()).decode("utf-8") | |
| summary = f"<h3>📈 إجمالي الاستخدام: {total} عملية</h3>" | |
| html = summary + "<img src='data:image/png;base64," + img + "'/>" | |
| return CUSTOM_CSS + html + df.to_html(escape=False, index=False, classes="styled-table") | |
| # ================== تحويل نتائج إلى HTML أنيق ================== | |
| def results_to_html(df): | |
| if df.empty: | |
| return "<p>❌ لا توجد نتائج</p>" | |
| html = CUSTOM_CSS + "<div style='direction:rtl;text-align:right;'>" | |
| for i, row in df.iterrows(): | |
| html += "<table class='styled-table' style='margin-bottom:15px;'>" | |
| html += f"<caption style='caption-side:top;text-align:right;font-weight:bold;margin-bottom:5px;'>📘 النتيجة {i+1}</caption>" | |
| for col, val in row.items(): | |
| html += f"<tr><th>{col}</th><td>{val}</td></tr>" | |
| html += "</table>" | |
| html += "</div>" | |
| return html | |
| def df_to_html(df): | |
| if isinstance(df, str): | |
| return df | |
| if df.empty: | |
| return "<p>❌ لا توجد نتائج</p>" | |
| return CUSTOM_CSS + df.to_html(escape=False, index=False, classes="styled-table") | |
| # ================== حفظ النتائج إلى ملف Excel ================== | |
| def save_to_excel(df): | |
| if df is None or (isinstance(df, pd.DataFrame) and df.empty): | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") | |
| pd.DataFrame().to_excel(tmp.name, index=False) | |
| return tmp.name | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") | |
| if isinstance(df, pd.DataFrame): | |
| df.to_excel(tmp.name, index=False) | |
| else: | |
| pd.DataFrame({"result_html":[str(df)]}).to_excel(tmp.name, index=False) | |
| return tmp.name | |
| # ================== البحث المحلي ================== | |
| def local_search_df(query, category, mode): | |
| if not query or not str(query).strip(): | |
| return "<p>⚠️ اكتب كلمة أو جملة للبحث</p>", pd.DataFrame() | |
| if mode == "نصي": | |
| if category == "Books": | |
| if "العنوان" in books_df.columns: | |
| df = books_df[books_df["العنوان"].astype(str).str.contains(query, case=False, na=False)] | |
| else: | |
| df = books_df[books_df["Title"].astype(str).str.contains(query, case=False, na=False)] | |
| else: | |
| if "العنوان" in theses_df.columns: | |
| df = theses_df[theses_df["العنوان"].astype(str).str.contains(query, case=False, na=False)] | |
| else: | |
| df = theses_df[theses_df["Title"].astype(str).str.contains(query, case=False, na=False)] | |
| else: | |
| q_emb = model.encode([query], convert_to_numpy=True) | |
| if category == "Books": | |
| scores = util.cos_sim(q_emb, books_embeddings)[0].cpu().numpy() | |
| idx = np.argsort(-scores) | |
| df = books_df.iloc[idx] | |
| else: | |
| scores = util.cos_sim(q_emb, theses_embeddings)[0].cpu().numpy() | |
| idx = np.argsort(-scores) | |
| df = theses_df.iloc[idx] | |
| if df is None or df.empty: | |
| df = pd.DataFrame([{"نتيجة":"❌ لم يتم العثور على نتائج"}]) | |
| else: | |
| if "Title" in df.columns: | |
| df = df.drop(columns=["Title"]) | |
| log_usage("البحث المحلي", query, mode, len(df) if isinstance(df, pd.DataFrame) else 0) | |
| html_results = results_to_html(df) | |
| return html_results, df | |
| # ================== البحث في PubMed ================== | |
| def search_pubmed_html(query, max_results=5): | |
| if not query or not str(query).strip(): | |
| return "<p>⚠️ اكتب كلمة للبحث</p>" | |
| try: | |
| handle = Entrez.esearch(db="pubmed", term=query, retmax=max_results) | |
| record = Entrez.read(handle) | |
| handle.close() | |
| ids = record.get("IdList", []) | |
| if not ids: | |
| return "<p>❌ لا توجد نتائج</p>" | |
| handle = Entrez.efetch(db="pubmed", id=",".join(ids), rettype="xml", retmode="xml") | |
| records = Entrez.read(handle) | |
| handle.close() | |
| rows = [] | |
| for rec in records.get('PubmedArticle', []): | |
| try: | |
| title = rec['MedlineCitation']['Article']['ArticleTitle'] | |
| pmid = rec['MedlineCitation']['PMID'] | |
| link = f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/" | |
| rows.append({ | |
| "الموقع": "PubMed", | |
| "الوصف": "قاعدة بيانات PubMed", | |
| "العنوان": title, | |
| "الرابط": f"<a href='{link}' target='_blank'>فتح</a>" | |
| }) | |
| except Exception: | |
| continue | |
| if not rows: | |
| return "<p>❌ لم يتم العثور على نتائج صالحة.</p>" | |
| df = pd.DataFrame(rows) | |
| log_usage("المصادر الخارجية", query, "PubMed", len(df)) | |
| return CUSTOM_CSS + df.to_html(escape=False, index=False, classes="styled-table") | |
| except Exception as e: | |
| return f"<p>❌ حدث خطأ أثناء جلب البيانات من PubMed:<br>{str(e)}</p>" | |
| # ================== البحث في المواقع الخارجية ================== | |
| EXTERNAL_LINKS = { | |
| "Google Scholar": {"url": "https://scholar.google.com/scholar?q=", "desc": "محرك بحث للأبحاث"}, | |
| "PubMed": {"url": "https://pubmed.ncbi.nlm.nih.gov/?term=", "desc": "قاعدة بيانات PubMed"}, | |
| "Europe PMC": {"url": "https://europepmc.org/search?query=", "desc": "أبحاث Europe PMC"}, | |
| "ClinicalTrials.gov": {"url": "https://clinicaltrials.gov/search?term=", "desc": "تجارب سريرية"}, | |
| "OpenFDA": {"url": "https://open.fda.gov/", "desc": "بيانات الأدوية FDA"}, | |
| "MedlinePlus": {"url": "https://medlineplus.gov/search/?query=", "desc": "معلومات طبية"}, | |
| "DrugBank": {"url": "https://go.drugbank.com/", "desc": "قاعدة بيانات الأدوية"}, | |
| "WHO GHO": {"url": "https://www.who.int/data/gho/", "desc": "بيانات الصحة العالمية"}, | |
| "ICD-11": {"url": "https://icd.who.int/browse11/l-m/en", "desc": "التصنيف الدولي للأمراض"}, | |
| "ClinicalKey": {"url": "https://www.clinicalkey.com/search?q=", "desc": "مصادر طبية شاملة"}, | |
| "Scimago": {"url": "https://www.scimagojr.com/journalsearch.php?q=", "desc": "تصنيف الدوريات"}, | |
| "EKB": {"url": "https://www.ekb.eg/", "desc": "بنك المعرفة المصري"} | |
| } | |
| def external_search_html(query, site): | |
| try: | |
| if not query or not str(query).strip(): | |
| return "<p>⚠️ من فضلك اكتب كلمة للبحث</p>" | |
| if not site or site not in EXTERNAL_LINKS: | |
| return f"<p>⚠️ الموقع المحدد غير معروف أو غير موجود في القائمة.</p>" | |
| if site == "PubMed": | |
| return search_pubmed_html(query) | |
| base = EXTERNAL_LINKS[site]["url"] | |
| desc = EXTERNAL_LINKS[site]["desc"] | |
| if "?" in base or "=" in base: | |
| link = f"{base}{query}" | |
| else: | |
| link = base | |
| df = pd.DataFrame([ | |
| {"اسم الموقع": site, "الوصف": desc, "رابط البحث": f"<a href='{link}' target='_blank'>للوصول إلى نتيجة البحث</a>"} | |
| ]) | |
| log_usage("المصادر الخارجية", query, site, 1) | |
| html_table = df.to_html(escape=False, index=False, classes="styled-table") | |
| return CUSTOM_CSS + f"<h3>🔗 نتائج البحث في {site}</h3>" + html_table | |
| except Exception as e: | |
| return f"<p>❌ حدث خطأ أثناء تنفيذ البحث:<br>{str(e)}</p>" | |
| # ================== البحث في الدوريات ================== | |
| JOURNALS = { | |
| "Elsevier Journal Finder": {"url": "https://journalfinder.elsevier.com/", "desc": "اقتراح مجلات النشر"}, | |
| "Springer Journal Suggester": {"url": "https://journalsuggester.springer.com/", "desc": "اقتراح مجلات النشر"}, | |
| "DOAJ": {"url": "https://doaj.org/search/journals?ref=homepage-box&source=", "desc": "مجلات مفتوحة الوصول"}, | |
| "Journal Citation Reports": {"url": "https://jcr.clarivate.com/jcr/browse-journals?query=", "desc": "تقييم الدوريات العلمية"}, | |
| "Scimago": {"url": "https://www.scimagojr.com/journalsearch.php?q=", "desc": "تصنيف الدوريات"}, | |
| "Google Scholar": {"url": "https://scholar.google.com/scholar?q=", "desc": "بحث عام"} | |
| } | |
| def journal_search_html(query, site): | |
| try: | |
| if not query or not str(query).strip(): | |
| return "<p>⚠️ من فضلك اكتب كلمة للبحث</p>" | |
| if not site or site not in JOURNALS: | |
| return f"<p>⚠️ الموقع المحدد غير معروف أو غير موجود في قائمة الدوريات.</p>" | |
| base = JOURNALS[site]["url"] | |
| desc = JOURNALS[site]["desc"] | |
| if "?" in base or "=" in base: | |
| link = f"{base}{query}" | |
| else: | |
| link = base | |
| df = pd.DataFrame([{"اسم الموقع": site, "الوصف": desc, "رابط البحث": f"<a href='{link}' target='_blank'>للوصول إلى نتيجة البحث</a>"}]) | |
| log_usage("الدوريات", query, site, 1) | |
| html_table = df.to_html(escape=False, index=False, classes="styled-table") | |
| return CUSTOM_CSS + f"<h3>📄 نتائج البحث في {site}</h3>" + html_table | |
| except Exception as e: | |
| return f"<p>❌ حدث خطأ أثناء تنفيذ البحث:<br>{str(e)}</p>" | |
| # ================== أدوات الذكاء الاصطناعي ================== | |
| AI_TOOLS = { | |
| "Semantic Scholar": {"url": "https://www.semanticscholar.org/search?q=", "desc": "بحث أكاديمي يعتمد على الذكاء الاصطناعي"}, | |
| "Connected Papers": {"url": "https://www.connectedpapers.com/search?q=", "desc": "خرائط علاقات بين الأبحاث"}, | |
| "Research Rabbit": {"url": "https://www.researchrabbitapp.com/", "desc": "تتبع شبكات الأبحاث"}, | |
| "Elicit": {"url": "https://elicit.org/", "desc": "مساعد بحث علمي يستخدم AI"}, | |
| "Explainpaper": {"url": "https://www.explainpaper.com/", "desc": "تبسيط الأبحاث العلمية"}, | |
| "Consensus": {"url": "https://consensus.app/search/?q=", "desc": "إجابات سريعة مدعومة بالأدلة"} | |
| } | |
| def ai_search_html(query, site): | |
| try: | |
| if not query or not str(query).strip(): | |
| return "<p>⚠️ من فضلك اكتب كلمة للبحث</p>" | |
| if not site or site not in AI_TOOLS: | |
| return f"<p>⚠️ الأداة المحددة غير معروفة أو غير موجودة في القائمة.</p>" | |
| base = AI_TOOLS[site]["url"] | |
| desc = AI_TOOLS[site]["desc"] | |
| if "?" in base or "=" in base: | |
| link = f"{base}{query}" | |
| else: | |
| link = base | |
| df = pd.DataFrame([{"اسم الأداة": site, "الوصف": desc, "رابط البحث": f"<a href='{link}' target='_blank'>للوصول إلى نتيجة البحث</a>"}]) | |
| log_usage("أدوات الذكاء الاصطناعي", query, site, 1) | |
| html_table = df.to_html(escape=False, index=False, classes="styled-table") | |
| return CUSTOM_CSS + f"<h3>🤖 نتائج البحث في {site}</h3>" + html_table | |
| except Exception as e: | |
| return f"<p>❌ حدث خطأ أثناء تنفيذ البحث:<br>{str(e)}</p>" | |
| # ================== وظائف إدارة الأسئلة (CSV) ================== | |
| def ensure_questions_file(): | |
| if not os.path.exists(QUESTIONS_FILE): | |
| df = pd.DataFrame(columns=["id","name","question","answer","status","datetime"]) | |
| df.to_csv(QUESTIONS_FILE, index=False, encoding="utf-8-sig") | |
| def read_questions_df(): | |
| ensure_questions_file() | |
| try: | |
| df = pd.read_csv(QUESTIONS_FILE, encoding="utf-8-sig") | |
| except Exception: | |
| df = pd.DataFrame(columns=["id","name","question","answer","status","datetime"]) | |
| return df | |
| def save_question_to_csv(name, question): | |
| ensure_questions_file() | |
| df = read_questions_df() | |
| if df.empty: | |
| next_id = 1 | |
| else: | |
| try: | |
| next_id = int(df["id"].max()) + 1 | |
| except Exception: | |
| next_id = len(df) + 1 | |
| row = { | |
| "id": next_id, | |
| "name": name if name and str(name).strip() else "👤 طالب مجهول", | |
| "question": question, | |
| "answer": "", | |
| "status": "لم يتم الرد", | |
| "datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| df = pd.concat([df, pd.DataFrame([row])], ignore_index=True) | |
| df.to_csv(QUESTIONS_FILE, index=False, encoding="utf-8-sig") | |
| # رفع تلقائي للملف بعد الحفظ | |
| try: | |
| upload_data_file(QUESTIONS_FILE) | |
| except Exception as e: | |
| print("⚠️ خطأ أثناء رفع questions.csv:", e) | |
| return next_id | |
| def save_answer_to_csv(qid, answer_text): | |
| ensure_questions_file() | |
| df = read_questions_df() | |
| if df.empty: | |
| return False, "لا توجد أسئلة." | |
| try: | |
| qid = int(qid) | |
| except Exception: | |
| return False, "معرّف السؤال غير صالح." | |
| if qid not in df["id"].values: | |
| return False, "لم يتم إيجاد السؤال بالمعرّف المحدد." | |
| df.loc[df["id"] == qid, "answer"] = answer_text | |
| df.loc[df["id"] == qid, "status"] = "تم الرد" | |
| df.to_csv(QUESTIONS_FILE, index=False, encoding="utf-8-sig") | |
| # رفع تلقائي بعد الحفظ | |
| try: | |
| upload_data_file(QUESTIONS_FILE) | |
| except Exception as e: | |
| print("⚠️ خطأ أثناء رفع questions.csv بعد الرد:", e) | |
| return True, "تم حفظ الرد بنجاح." | |
| def questions_to_html(df): | |
| if df is None or df.empty: | |
| return "<p>❌ لا توجد أسئلة حتى الآن.</p>" | |
| html = CUSTOM_CSS + "<div style='direction:rtl;text-align:right;'>" | |
| df_sorted = df.sort_values(by="id", ascending=False).reset_index(drop=True) | |
| for _, row in df_sorted.iterrows(): | |
| name = row.get("name","👤 طالب مجهول") | |
| qtext = row.get("question","") | |
| answer = row.get("answer","") | |
| status = row.get("status","لم يتم الرد") | |
| qid = row.get("id","") | |
| dt = row.get("datetime","") | |
| html += f"<div class='question-card'><div class='question-meta'># {qid} — {dt} — <b>{name}</b></div>" | |
| html += f"<div><b>السؤال:</b> {qtext}</div>" | |
| if status == "تم الرد" and str(answer).strip(): | |
| html += f"<div class='answer'><b>الرد:</b> {answer} <div class='question-meta'>حالة: {status}</div></div>" | |
| else: | |
| html += f"<div class='answer'><i>لم يتم الرد بعد</i> <div class='question-meta'>حالة: {status}</div></div>" | |
| html += "</div>" | |
| html += "</div>" | |
| return html | |
| # ================== وظائف إدارة طلبات الاستعارة (أعضاء هيئة التدريس) ================== | |
| def ensure_faculty_file(): | |
| if not os.path.exists(FACULTY_BORROW_FILE): | |
| df = pd.DataFrame(columns=["id","name","phone","nid","email","position","birthdate","status","datetime"]) | |
| df.to_csv(FACULTY_BORROW_FILE, index=False, encoding="utf-8-sig") | |
| def read_faculty_df(): | |
| ensure_faculty_file() | |
| try: | |
| df = pd.read_csv(FACULTY_BORROW_FILE, encoding="utf-8-sig") | |
| except Exception: | |
| df = pd.DataFrame(columns=["id","name","phone","nid","email","position","birthdate","status","datetime"]) | |
| return df | |
| def save_faculty_request(name, phone, nid, email, position, birthdate): | |
| ensure_faculty_file() | |
| df = read_faculty_df() | |
| if df.empty: | |
| next_id = 1 | |
| else: | |
| try: | |
| next_id = int(df["id"].max()) + 1 | |
| except Exception: | |
| next_id = len(df) + 1 | |
| row = { | |
| "id": next_id, | |
| "name": name, | |
| "phone": phone, | |
| "nid": nid, | |
| "email": email, | |
| "position": position, | |
| "birthdate": birthdate, | |
| "status": "قيد المراجعة", | |
| "datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| df = pd.concat([df, pd.DataFrame([row])], ignore_index=True) | |
| df.to_csv(FACULTY_BORROW_FILE, index=False, encoding="utf-8-sig") | |
| # رفع تلقائي بعد الحفظ | |
| try: | |
| upload_data_file(FACULTY_BORROW_FILE) | |
| except Exception as e: | |
| print("⚠️ خطأ أثناء رفع faculty_borrow.csv:", e) | |
| return next_id | |
| # ================== واجهة Gradio ================== | |
| with gr.Blocks() as demo: | |
| with gr.Tab("📚 عن المكتبة"): | |
| with gr.Tabs(): | |
| with gr.Tab("🌟 رؤية المكتبة"): | |
| gr.Markdown("تسعى المكتبة إلى أن تكون مركزًا معرفيًا متميزًا يدعم البحث والتعليم ويُسهم في بناء مجتمع المعرفة.") | |
| with gr.Tab("🎯 رسالة المكتبة"): | |
| gr.Markdown("تقديم خدمات معلوماتية متكاملة تُمكّن الباحثين والطلاب من الوصول إلى مصادر المعرفة بسهولة وكفاءة.") | |
| with gr.Tab("🎯 أهداف المكتبة"): | |
| gr.Markdown("- دعم العملية التعليمية والبحثية.\n- تيسير الوصول إلى المعلومات والمصادر العلمية.\n- تشجيع استخدام التقنيات الحديثة في البحث.\n- تعزيز ثقافة القراءة والتعلم الذاتي.") | |
| with gr.Tab("🧩 الخدمات"): | |
| with gr.Tabs(): | |
| with gr.Tab("📘 الإحاطة الجارية"): | |
| gr.Markdown("خدمة تتيح للباحثين متابعة أحدث الإصدارات والموضوعات الجديدة في مجالات اهتمامهم.") | |
| with gr.Tab("🔗 التسجيل في بنك المعرفة"): | |
| gr.Markdown("يرجى ملء البيانات التالية للتسجيل في بنك المعرفة المصري:") | |
| name = gr.Textbox(label="الاسم بالكامل", placeholder="اكتب اسمك الثلاثي هنا") | |
| nid = gr.Textbox(label="رقم البطاقة", placeholder="14 رقمًا") | |
| birth = gr.Textbox(label="تاريخ الميلاد", placeholder="مثال: 1999-05-12") | |
| email = gr.Textbox(label="الإيميل الجامعي", placeholder="example@university.edu.eg") | |
| phone = gr.Textbox(label="رقم التليفون", placeholder="010XXXXXXXXX") | |
| submit_btn = gr.Button("إرسال الطلب") | |
| output_msg = gr.HTML() | |
| def save_registration(name, nid, birth, email, phone): | |
| import csv, os | |
| from datetime import datetime | |
| file_path = "bank_requests.csv" | |
| file_exists = os.path.exists(file_path) | |
| with open(file_path, mode="a", newline="", encoding="utf-8") as f: | |
| writer = csv.writer(f) | |
| if not file_exists: | |
| writer.writerow(["الاسم", "رقم البطاقة", "تاريخ الميلاد", "الإيميل الجامعي", "رقم التليفون", "تاريخ التسجيل"]) | |
| writer.writerow([name, nid, birth, email, phone, datetime.now().strftime("%Y-%m-%d %H:%M:%S")]) | |
| # رفع تلقائي للملف | |
| try: | |
| upload_data_file(file_path) | |
| except Exception as e: | |
| print("⚠️ خطأ أثناء رفع bank_requests.csv:", e) | |
| return "<b style='color:green;'>✅ تم استلام بياناتك بنجاح، يرجى مراجعة بريدك الجامعي خلال 24 ساعة.</b>" | |
| submit_btn.click(save_registration, inputs=[name, nid, birth, email, phone], outputs=output_msg) | |
| with gr.Tab("🖨️ التصوير"): | |
| gr.Markdown("توفر المكتبة خدمة تصوير المواد العلمية وفق القواعد المعمول بها داخل المكتبة.") | |
| with gr.Tab("🔍 البحث الإلكتروني"): | |
| gr.Markdown("يمكن للباحثين استخدام أجهزة المكتبة للبحث في قواعد البيانات الإلكترونية والمصادر العلمية.") | |
| with gr.Tab("📖 الاطلاع الداخلي"): | |
| gr.Markdown("تتيح المكتبة للرواد قراءة الكتب والمراجع داخل القاعات المخصصة دون الحاجة إلى استعارتها خارجًا.") | |
| # ------------------ تبويب الاستعارة الخارجية (معدّل) ------------------ | |
| with gr.Tab("📚 الاستعارة الخارجية"): | |
| gr.Markdown(""" | |
| تقدّم المكتبة خدمة الاستعارة الخارجية للكتب تبعًا لعدد النسخ، حيث أنه غير مسموح بالاستعارة الخارجية للكتب والمراجع ذات النسخة الواحدة أو ذات الطابع الخاص أو الدوريات أو الرسائل العلمية. | |
| """) | |
| with gr.Tabs(): | |
| with gr.Tab("📌 الاستعارة للطلاب"): | |
| gr.Markdown(""" | |
| 📌 يستخرج الطالب استمارة الاستعارة عن طريق ملء استمارة ضمان يقوم بختمها من شؤون الطلاب، | |
| مع ختمها بختم النسر الخاص بالجهة الحكومية التابع لها الضامن. | |
| """) | |
| gr.Markdown("### 📄 تحميل الاستمارات المطلوبة:") | |
| # روابط ملفات Google Drive | |
| student_form_url = "https://drive.google.com/uc?export=download&id=18BBlUYnKu37TEPqup8CeJGPv5UEOy1tY" | |
| guarantor_form_url = "https://drive.google.com/uc?export=download&id=1th9cJm0xxI16_YfLQ148l94zvkSqJmLD" | |
| gr.HTML(f""" | |
| <div style='display:flex; gap:15px; flex-wrap:wrap; margin-top:10px;'> | |
| <a href='{student_form_url}' target='_blank'> | |
| <button style='background-color:#4CAF50; color:white; padding:10px 18px; border:none; border-radius:8px; font-size:16px; cursor:pointer;'> | |
| 📄 تحميل بيانات الطالب | |
| </button> | |
| </a> | |
| <a href='{guarantor_form_url}' target='_blank'> | |
| <button style='background-color:#2196F3; color:white; padding:10px 18px; border:none; border-radius:8px; font-size:16px; cursor:pointer;'> | |
| 📄 تحميل بيانات الضامن | |
| </button> | |
| </a> | |
| </div> | |
| """) | |
| gr.Markdown("برجاء طباعة الاستمارتين وتوقيعهما قبل التوجه إلى المكتبة") | |
| with gr.Tab("📌 الاستعارة لأعضاء هيئة التدريس"): | |
| gr.Markdown("أعضاء هيئة التدريس يرجى تعبئة البيانات التالية لطلب الاستعارة:") | |
| fac_name = gr.Textbox(label="الاسم", placeholder="الاسم الكامل") | |
| fac_phone = gr.Textbox(label="رقم التليفون", placeholder="010XXXXXXXX") | |
| fac_nid = gr.Textbox(label="رقم البطاقة", placeholder="14 رقمًا") | |
| fac_email = gr.Textbox(label="الإيميل الجامعي", placeholder="example@university.edu.eg") | |
| fac_position = gr.Textbox(label="التوصيف الوظيفي", placeholder="مثال: أستاذ مساعد – قسم الصيدلة") | |
| fac_birth = gr.Textbox(label="تاريخ الميلاد", placeholder="مثال: 1980-05-12") | |
| fac_submit = gr.Button("إرسال الطلب") | |
| fac_msg = gr.Markdown("") | |
| def submit_faculty_request(name, phone, nid, email, position, birthdate): | |
| if not name or not str(name).strip(): | |
| return "<b style='color:red;'>الاسم مطلوب.</b>" | |
| if not phone or not str(phone).strip(): | |
| return "<b style='color:red;'>رقم التليفون مطلوب.</b>" | |
| if not nid or not str(nid).strip(): | |
| return "<b style='color:red;'>رقم البطاقة مطلوب.</b>" | |
| if not email or not str(email).strip(): | |
| return "<b style='color:red;'>الإيميل الجامعي مطلوب.</b>" | |
| # حفظ البيانات | |
| qid = save_faculty_request(name, phone, nid, email, position, birthdate) | |
| return f"✅ تم إرسال طلب الاستعارة (#{qid}) وسيتم مراجعته." | |
| fac_submit.click(submit_faculty_request, inputs=[fac_name, fac_phone, fac_nid, fac_email, fac_position, fac_birth], outputs=fac_msg) | |
| with gr.Tab("🔎 البحث المحلي"): | |
| query_local = gr.Textbox(label="اكتب كلمة البحث") | |
| category = gr.Radio(["Books", "Theses"], label="اختر الفئة") | |
| mode = gr.Radio(["نصي", "دلالي (Semantic)"], label="نوع البحث") | |
| btn_local = gr.Button("بحث") | |
| df_local_state = gr.State() | |
| output_local_html = gr.HTML() | |
| file_local = gr.File(label="⬇️ تحميل النتائج", visible=False) | |
| btn_local.click( | |
| local_search_df, | |
| inputs=[query_local, category, mode], | |
| outputs=[output_local_html, df_local_state] | |
| ) | |
| btn_save_local = gr.Button("📥 حفظ النتائج") | |
| btn_save_local.click( | |
| save_to_excel, | |
| inputs=df_local_state, | |
| outputs=file_local | |
| ) | |
| file_local.change(lambda x: gr.update(visible=True), inputs=file_local, outputs=file_local) | |
| with gr.Tab("🌐 المصادر الخارجية"): | |
| query_ext = gr.Textbox(label="كلمة البحث") | |
| site_ext = gr.Dropdown(list(EXTERNAL_LINKS.keys()), label="اختر الموقع") | |
| btn_ext = gr.Button("بحث") | |
| output_ext = gr.HTML() | |
| btn_ext.click(external_search_html, inputs=[query_ext, site_ext], outputs=output_ext) | |
| with gr.Tab("📄 الدوريات"): | |
| query_jour = gr.Textbox(label="كلمة البحث") | |
| site_jour = gr.Dropdown(list(JOURNALS.keys()), label="اختر الموقع") | |
| btn_jour = gr.Button("بحث") | |
| output_jour = gr.HTML() | |
| btn_jour.click(journal_search_html, inputs=[query_jour, site_jour], outputs=output_jour) | |
| with gr.Tab("🤖 أدوات الذكاء الاصطناعي"): | |
| query_ai = gr.Textbox(label="كلمة البحث") | |
| site_ai = gr.Dropdown(list(AI_TOOLS.keys()), label="اختر الأداة") | |
| btn_ai = gr.Button("بحث") | |
| output_ai = gr.HTML() | |
| btn_ai.click(ai_search_html, inputs=[query_ai, site_ai], outputs=output_ai) | |
| with gr.Tab("📑 برامج إدارة المراجع"): | |
| gr.Markdown("ℹ️ **تنويه:** هذه البرامج تحتاج تنزيل أو تثبيت الإضافات الخاصة بها.") | |
| gr.Markdown("### • **Mendeley** \n🔗 [زيارة الموقع](https://www.mendeley.com/)") | |
| gr.Markdown("### • **Zotero** \n🔗 [زيارة الموقع](https://www.zotero.org/)") | |
| gr.Markdown("### • **EndNote** \n🔗 [زيارة الموقع](https://endnote.com/)") | |
| with gr.Tab("💬 المساعد الذكي"): | |
| q_faq = gr.Textbox(label="اسأل مساعد المكتبة") | |
| ans_faq = gr.HTML() | |
| btn_faq = gr.Button("إجابة") | |
| btn_faq.click(library_assistant_smart, inputs=q_faq, outputs=ans_faq) | |
| with gr.Tab("📊 الإحصاءات"): | |
| stats_html = gr.HTML() | |
| btn_stats = gr.Button("عرض الإحصاءات") | |
| btn_stats.click(lambda: show_usage_stats(), outputs=stats_html) | |
| # -------------------- تبويب "📞 تواصل معنا" المضاف سابقًا -------------------- | |
| with gr.Tab("📞 تواصل معنا"): | |
| gr.Markdown(""" | |
| ## 📞 تواصل معنا | |
| يسعدنا تواصلك مع **مكتبة كلية الصيدلة – جامعة المنصورة** عبر القنوات التالية: | |
| 📍 **العنوان:** الدور الرابع – المبنى الإداري – كلية الصيدلة – جامعة المنصورة | |
| 📧 **البريد الإلكتروني:** [phrlib1@mans.edu.eg](mailto:phrlib1@mans.edu.eg) | |
| """) | |
| fb_link = "https://www.facebook.com/people/%D9%85%D9%83%D8%AA%D8%A8%D8%A9-%D9%83%D9%84%D9%8A%D8%A9-%D8%A7%D9%84%D8%B5%D9%8A%D8%AF%D9%84%D8%A9-%D8%AC%D8%A7%D9%85%D8%B9%D8%A9-%D8%A7%D9%84%D9%85%D9%86%D8%B5%D9%88%D8%B1%D8%A9/61554318849433/" | |
| youtube_link = "https://www.youtube.com/@%D9%85%D9%83%D8%AA%D8%A8%D8%A9%D9%83%D9%84%D9%8A%D8%A9%D8%A7%D9%84%D8%B5%D9%8A%D8%AF%D9%84%D8%A9%D8%AC%D8%A7%D9%85%D8%B9%D8%A9%D8%A7%D9%84%D9%85%D9%86%D8%B5%D9%88%D8%B1%D8%A9" | |
| email_link = "https://mail.google.com/mail/?view=cm&fs=1&to=phrlib1@mans.edu.eg" | |
| gr.Markdown("### 🌐 قنوات التواصل السريعة:") | |
| gr.HTML(f""" | |
| <div style='display:flex; gap:10px; flex-wrap:wrap;'> | |
| <!-- Facebook --> | |
| <a href='{fb_link}' target='_blank' style='text-decoration:none;'> | |
| <button style='background-color:#1877f2;color:white;padding:10px 18px; | |
| border:none;border-radius:8px;font-size:16px;cursor:pointer;display:flex; | |
| align-items:center;gap:8px;'> | |
| <svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' | |
| fill='white' viewBox='0 0 24 24'> | |
| <path d='M22.675 0h-21.35C.597 0 0 .597 0 1.333v21.334C0 23.403.597 24 | |
| 1.325 24h11.495v-9.294H9.691v-3.622h3.129V8.413c0-3.1 1.894-4.788 | |
| 4.659-4.788 1.325 0 2.464.099 2.797.143v3.243h-1.921c-1.506 0-1.798.716- | |
| 1.798 1.767v2.317h3.595l-.468 3.622h-3.127V24h6.127C23.403 24 24 | |
| 23.403 24 22.667V1.333C24 .597 23.403 0 22.675 0z'/> | |
| </svg> | |
| </button> | |
| </a> | |
| <!-- YouTube --> | |
| <a href='{youtube_link}' target='_blank' style='text-decoration:none;'> | |
| <button style='background-color:#FF0000;color:white;padding:10px 18px; | |
| border:none;border-radius:8px;font-size:16px;cursor:pointer;display:flex; | |
| align-items:center;gap:8px;'> | |
| <svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' | |
| fill='white' viewBox='0 0 24 24'> | |
| <path d='M23.498 6.186a2.993 2.993 0 0 0-2.107-2.12C19.4 3.5 12 | |
| 3.5 12 3.5s-7.4 0-9.391.566A2.993 2.993 0 0 0 .502 6.186 | |
| 31.04 31.04 0 0 0 0 12a31.04 31.04 0 0 0 .502 5.814 | |
| 2.993 2.993 0 0 0 2.107 2.12C4.6 20.5 12 20.5 12 | |
| 20.5s7.4 0 9.391-.566a2.993 2.993 0 0 0 2.107-2.12A31.04 | |
| 31.04 0 0 0 24 12a31.04 31.04 0 0 0-.502-5.814zM9.75 | |
| 15.568V8.432L15.818 12 9.75 15.568z'/> | |
| </svg> | |
| YouTube | |
| </button> | |
| </a> | |
| <!-- Email --> | |
| <a href='{email_link}' target='_blank' style='text-decoration:none;'> | |
| <button style='background-color:#444;color:white;padding:10px 18px; | |
| border:none;border-radius:8px;font-size:16px;cursor:pointer;display:flex; | |
| align-items:center;gap:8px;'> | |
| <svg xmlns='http://www.w3.org/2000/svg' width='20' height='20' | |
| fill='white' viewBox='0 0 24 24'> | |
| <path d='M12 13.065L.015 6h23.97L12 13.065zm0 2.248L24 8v13H0V8l12 | |
| 7.313z'/> | |
| </svg> | |
| </button> | |
| </a> | |
| </div> | |
| """) | |
| # -------------------- تبويب "❓ اسأل أخصائي المكتبة" (التبويب الجديد للطلاب) -------------------- | |
| with gr.Tab("❓ اسأل أخصائي المكتبة"): | |
| gr.Markdown("### اسأل أخصائي المكتبة\nاكتب سؤالك وسيظهر في نظام المكتبة ليرد عليه الأخصائي لاحقًا.") | |
| student_name = gr.Textbox(label="الاسم (اختياري)", placeholder="يمكن تركه فارغًا") | |
| student_question = gr.Textbox(label="السؤال", lines=4, placeholder="اكتب سؤالك هنا...", interactive=True) | |
| student_send = gr.Button("إرسال السؤال") | |
| student_result = gr.Markdown("") | |
| student_questions_html = gr.HTML() | |
| def load_questions_html(): | |
| df = read_questions_df() | |
| return questions_to_html(df) | |
| def student_submit_question(name, question): | |
| if not question or not str(question).strip(): | |
| return "⚠️ اكتب سؤالك قبل الإرسال.", questions_to_html(read_questions_df()) | |
| qid = save_question_to_csv(name, question) | |
| df = read_questions_df() | |
| return f"✅ تم إرسال سؤالك (#{qid})، سنقوم بالرد في أقرب وقت ممكن.", questions_to_html(df) | |
| student_send.click(student_submit_question, inputs=[student_name, student_question], outputs=[student_result, student_questions_html]) | |
| btn_load_questions = gr.Button("تحديث عرض الأسئلة") | |
| btn_load_questions.click(lambda: load_questions_html(), outputs=student_questions_html) | |
| # -------------------- 📂 تبويب إدارة خدمات المكتبة -------------------- | |
| with gr.Tab("📂 إدارة خدمات المكتبة"): | |
| gr.Markdown("### 🔐 تسجيل الدخول لإدارة خدمات المكتبة") | |
| password = gr.Textbox(label="كلمة المرور", type="password", placeholder="أدخل كلمة المرور هنا") | |
| login_btn = gr.Button("دخول") | |
| login_msg = gr.Markdown("") | |
| admin_area = gr.Group(visible=False) | |
| with admin_area: | |
| gr.Markdown("### عرض طلبات التسجيل في بنك المعرفة") | |
| refresh_btn = gr.Button("🔄 تحديث البيانات") | |
| data_table = gr.Dataframe(headers=["الاسم", "رقم البطاقة", "تاريخ الميلاد", "الإيميل الجامعي", "رقم التليفون", "تاريخ التسجيل"], interactive=False) | |
| download_btn = gr.Button("⬇️ تنزيل الطلبات كملف Excel") | |
| download_file = gr.File(label="ملف Excel", visible=False) | |
| import pandas as pd, os, tempfile | |
| def check_password(pw): | |
| # اقرأ كلمة مرور الأدمن من Secret (ADMIN_PASS) — لو مش موجود يستخدم 'pharmacy1' كقيمة افتراضية قصيرة الأمد | |
| correct_pw = os.getenv("ADMIN_PASS", "pharmacy1") | |
| if pw == correct_pw: | |
| return gr.update(visible=True), "✅ تم تسجيل الدخول بنجاح." | |
| else: | |
| return gr.update(visible=False), "❌ كلمة المرور غير صحيحة." | |
| login_btn.click(check_password, inputs=password, outputs=[admin_area, login_msg]) | |
| def load_requests(): | |
| file_path = "bank_requests.csv" | |
| if not os.path.exists(file_path): | |
| return pd.DataFrame(columns=["الاسم", "رقم البطاقة", "تاريخ الميلاد", "الإيميل الجامعي", "رقم التليفون", "تاريخ التسجيل"]) | |
| df = pd.read_csv(file_path) | |
| return df | |
| def export_excel(df): | |
| if df is None or df.empty: | |
| return gr.update(visible=False) | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") | |
| df.to_excel(tmp.name, index=False) | |
| return gr.update(value=tmp.name, visible=True) | |
| refresh_btn.click(load_requests, outputs=data_table) | |
| download_btn.click(export_excel, inputs=data_table, outputs=download_file) | |
| # -------------------- قسم إدارة الأسئلة داخل منطقة الإدارة -------------------- | |
| with admin_area: | |
| gr.Markdown("### 🛠️ إدارة الأسئلة") | |
| admin_refresh_q = gr.Button("🔄 تحديث الأسئلة") | |
| admin_questions_df = gr.Dataframe(headers=["id","name","question","answer","status","datetime"], interactive=False) | |
| admin_select_q = gr.Dropdown(choices=[], label="اختر رقم السؤال للرد عليه (ID)", value=None) | |
| admin_answer_box = gr.Textbox(label="اكتب الرد هنا", lines=3, placeholder="أدخل رد الأخصائي...") | |
| admin_save_answer_btn = gr.Button("حفظ الرد") | |
| admin_msg = gr.Markdown("") | |
| admin_export_btn = gr.Button("⬇️ تنزيل الأسئلة كملف Excel") | |
| admin_export_file = gr.File(label="ملف الأسئلة", visible=False) | |
| def admin_load_questions(): | |
| df = read_questions_df() | |
| if df is None or df.empty: | |
| df_show = pd.DataFrame(columns=["id","name","question","answer","status","datetime"]) | |
| else: | |
| df_show = df.sort_values(by="id", ascending=False) | |
| choices = df_show["id"].astype(str).tolist() if not df_show.empty else [] | |
| return df_show, gr.update(choices=choices, value=choices[0] if choices else None) | |
| def admin_save_answer(qid, answer_text): | |
| success, msg = save_answer_to_csv(qid, answer_text) | |
| if not success: | |
| return "❌ " + msg, admin_load_questions()[0] | |
| df_show, dd_update = admin_load_questions() | |
| return "✅ تم حفظ الرد.", df_show | |
| def admin_export_questions(): | |
| df = read_questions_df() | |
| if df is None or df.empty: | |
| return gr.update(visible=False) | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") | |
| df.to_excel(tmp.name, index=False) | |
| return gr.update(value=tmp.name, visible=True) | |
| admin_refresh_q.click(lambda: admin_load_questions()[0], outputs=admin_questions_df) | |
| admin_refresh_q.click(lambda: admin_load_questions()[1], outputs=admin_select_q) | |
| admin_save_answer_btn.click(admin_save_answer, inputs=[admin_select_q, admin_answer_box], outputs=[admin_msg, admin_questions_df]) | |
| admin_export_btn.click(admin_export_questions, outputs=admin_export_file) | |
| # -------------------- قسم إدارة طلبات الاستعارة لأعضاء هيئة التدريس -------------------- | |
| with admin_area: | |
| gr.Markdown("### 🗂️ إدارة طلبات الاستعارة (أعضاء هيئة التدريس)") | |
| admin_refresh_fac = gr.Button("🔄 تحديث طلبات الاستعارة") | |
| admin_fac_df = gr.Dataframe(headers=["id","name","phone","nid","email","position","birthdate","status","datetime"], interactive=False) | |
| admin_export_fac_btn = gr.Button("⬇️ تنزيل طلبات الاستعارة كملف Excel") | |
| admin_export_fac_file = gr.File(label="ملف طلبات الاستعارة", visible=False) | |
| def admin_load_faculty(): | |
| df = read_faculty_df() | |
| if df is None or df.empty: | |
| df_show = pd.DataFrame(columns=["id","name","phone","nid","email","position","birthdate","status","datetime"]) | |
| else: | |
| df_show = df.sort_values(by="id", ascending=False) | |
| return df_show | |
| def admin_export_faculty(): | |
| df = read_faculty_df() | |
| if df is None or df.empty: | |
| return gr.update(visible=False) | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") | |
| df.to_excel(tmp.name, index=False) | |
| return gr.update(value=tmp.name, visible=True) | |
| admin_refresh_fac.click(lambda: admin_load_faculty(), outputs=admin_fac_df) | |
| admin_export_fac_btn.click(admin_export_faculty, outputs=admin_export_fac_file) | |
| gr.Markdown("---\n📌 *المشروع أعدته eman anter*") | |
| # تشغيل التطبيق | |
| if __name__ == "__main__": | |
| demo.launch(share=True) | |