import os import gradio as gr import pandas as pd import numpy as np import datetime import urllib.parse import urllib.request import urllib.error import json import time import re import html as html_lib from supabase import create_client, Client # ── Helper: build query from terms and operators ────────────── def build_boolean_query(term1, op1, term2, op2, term3): """ Build a boolean query from up to three terms with operators. """ terms = [] if term1 and term1.strip(): terms.append(term1.strip()) if term2 and term2.strip(): terms.append(term2.strip()) if term3 and term3.strip(): terms.append(term3.strip()) if len(terms) == 0: return "" if len(terms) == 1: return terms[0] # Build query with operators query_parts = [] for i, t in enumerate(terms): # Put quotes around phrases with spaces if ' ' in t and not (t.startswith('"') and t.endswith('"')): t = f'"{t}"' query_parts.append(t) # Insert operators between terms if len(query_parts) == 2: return f"{query_parts[0]} {op1} {query_parts[1]}" else: return f"{query_parts[0]} {op1} {query_parts[1]} {op2} {query_parts[2]}" def normalize_boolean_query(query: str) -> str: """ Replace lowercase 'and' and 'or' with uppercase operators. """ if not query: return query pattern_and = re.compile(r'(? bool: """Return True if query contains AND or OR operators.""" q = normalize_boolean_query(query) return ' AND ' in q or ' OR ' in q # ── Role-based admin authentication using session tracking ───────── # Roles: junior = DOI review only; senior = full administration. _admin_sessions = {} # session_id -> role def set_admin_active(session_id, role="senior"): if session_id: _admin_sessions[session_id] = role return True def get_admin_role(session_id): return _admin_sessions.get(session_id) def is_admin_active(session_id): return bool(session_id and session_id in _admin_sessions) def has_admin_role(session_id, *roles): role = get_admin_role(session_id) return bool(role and role in roles) def request_has_role(request, *roles): session_hash = getattr(request, "session_hash", None) return has_admin_role(session_hash, *roles) # ── Supabase credentials ────────────────────────────────────────── url: str = os.environ.get("SUPABASE_URL") key: str = os.environ.get("SUPABASE_KEY") # Existing credentials become the junior-admin credentials. AUTH_USER = os.environ.get("UPLOAD_USER") AUTH_PASS = os.environ.get("UPLOAD_PASS") # New senior-admin credentials. Set these as Hugging Face Space Secrets. SENIOR_ADMIN_USER = os.environ.get("s_admin_user") SENIOR_ADMIN_PASS = os.environ.get("s_admin_pass") if not url or not key: print("Warning: Missing Supabase credentials in Space Secrets!") supabase = None else: supabase: Client = create_client(url, key) HOMEPAGE_FILE = "homepage_content.html" HOME_CONTACT_HTML = """
Our Team & Contact

Our Team

Dr. Bharati Pandey
Scientist
Animal Biotechnology Division, NDRI, Karnal
Mr. Udiksh Malik
Intern
B.Sc. Biotechnology (Hons), Amity University, Noida
Dr. Manoj Kumar Singh
Principal Scientist
Animal Biotechnology Division, NDRI, Karnal
Dr. Naresh Selokar
Senior Scientist
Animal Biotechnology Division, NDRI, Karnal

Contact Us

Dr. Bharati Pandey
Scientist · NDRI
Email
bharati.pandey@icar.org.in
Phone
+91 9560421766
Address
Animal Biotechnology Division, NDRI, Karnal
Udiksh Malik
Intern · Amity University
Email
malikudiksh@gmail.com
Address
B.Sc. Biotechnology (Hons) with Research, Amity University, Noida
""" # ── Paper Alert Configuration ───────────────────────────────────────────────── PAPER_ALERT_TOPIC = "bovine oocyte transcriptomics" PAPER_ALERT_DAYS = 30 PAPER_ALERT_MAX = 5 # ───────────────────────────────────────────────────────────────────────────── EXACT_MATCH_COLUMNS = ["Accession", "Organism", "Type", "Platform", "Samples", "Study Title"] ALL_UNIQUE_COLUMNS = EXACT_MATCH_COLUMNS + ["tissue", "dataset"] BLANK_TEMPLATE_DF = pd.DataFrame(columns=ALL_UNIQUE_COLUMNS) GEO_TOOLS_DATA = [ {"Name": "GEO2R", "URL": "https://www.ncbi.nlm.nih.gov/geo/geo2r/", "Type": "Web-based (Free)", "Feature": "Official NCBI tool; differential gene expression between sample groups; volcano plots, Venn diagrams."}, {"Name": "GEOexplorer", "URL": "https://geoexplorer.rosalind.kcl.ac.uk/", "Type": "Web-based (Free)", "Feature": "End-to-end gene expression analysis; EDA, DGEA, gene enrichment analysis (GEA); interactive plots."}, {"Name": "shinyGEO", "URL": "http://gdancik.github.io/shinyGEO/", "Type": "Web-based / Docker (Free)", "Feature": "Download expression and sample data from GEO; differential expression and survival analysis."}, {"Name": "iDEP", "URL": "http://bioinformatics.sdstate.edu/idep/", "Type": "Web-based (Free)", "Feature": "Integrated analysis platform; DEG analysis (DESeq2, edgeR, limma); PCA, heatmaps, pathway enrichment."}, {"Name": "TidyGEO", "URL": "https://tidygeo.uams.edu/", "Type": "Web-based (Free)", "Feature": "Downloads and reformats GEO series; tidies and standardises sample-level annotations."}, {"Name": "GeneCloudOmics", "URL": "https://abiotrans.ait.ie/", "Type": "Web-based (Free)", "Feature": "23 data analytics and bioinformatics tasks; PCA, clustering, t-SNE, DEG analysis, pathway enrichment."}, {"Name": "BART (Bioinformatics Array Research Tool)", "URL": "http://bart.salk.edu/", "Type": "Web-based (Free)", "Feature": "Automates microarray download and analysis from GEO; batch effect correction; differential expression; functional enrichment."}, {"Name": "GEOquery (R/Bioconductor)", "URL": "https://bioconductor.org/packages/GEOquery/", "Type": "Standalone - R package (Free)", "Feature": "Downloads GEO datasets directly into R; parses GEO SOFT/MINiML formats into ExpressionSet objects."}, {"Name": "GEOfetch", "URL": "https://github.com/pepkit/geofetch", "Type": "Standalone - CLI (Free)", "Feature": "Command-line tool for downloading GEO data and metadata; outputs standardised PEP format."}, {"Name": "GEOmetadb (R/Bioconductor)", "URL": "https://bioconductor.org/packages/GEOmetadb/", "Type": "Standalone - R package (Free)", "Feature": "SQLite database of all GEO metadata; enables advanced SQL queries; faster than NCBI API."}, {"Name": "NCBI Datasets (GEO)", "URL": "https://www.ncbi.nlm.nih.gov/datasets/", "Type": "Web-based + CLI (Free)", "Feature": "Official NCBI data retrieval portal; download GEO datasets, metadata, and associated sequences."}, {"Name": "Correlation Engine (formerly NextBio)", "URL": "https://www.illumina.com/products/by-type/informatics-products/basespace-correlation-engine.html", "Type": "Web-based (Freemium)", "Feature": "Cross-study meta-analysis; correlation of gene expression signatures across GEO studies; disease/drug association analysis; biomarker discovery."}, {"Name": "Phantasus", "URL": "https://genome.ifmo.ru/phantasus/", "Type": "Web-based (Free)", "Feature": "Interactive visual analysis of GEO datasets; heatmaps, PCA, k-means clustering, DESeq2/limma."}, {"Name": "ExpressionAtlas", "URL": "https://www.ebi.ac.uk/gxa/", "Type": "Web-based (Free)", "Feature": "Curated gene expression across species and conditions; differential and baseline expression data."}, {"Name": "Enrichr", "URL": "https://maayanlab.cloud/Enrichr/", "Type": "Web-based (Free)", "Feature": "Gene set enrichment analysis; 200+ gene set libraries; interactive visualisations; accepts gene lists."} ] SRA_TOOLS_DATA = [ {"Name": "SRA Toolkit (NCBI)", "URL": "https://github.com/ncbi/sra-tools", "Type": "Standalone – CLI (Free)", "Feature": "Official NCBI toolkit; prefetch (downloads SRA files), fasterq-dump (converts to FASTQ, multi-threaded); fastq-dump, sam-dump, vdb-config; supports SRA/dbGaP/ADSP data; handles paired-end and single-end reads."}, {"Name": "SRA Explorer", "URL": "https://sra-explorer.info/", "Type": "Web-based (Free)", "Feature": "Browser-based search of SRA by accession/keyword; generates batch download scripts (curl/wget/Aspera); finds ENA FTP direct download URLs; exports metadata; no installation required."}, {"Name": "pysradb", "URL": "https://github.com/saketkc/pysradb", "Type": "Standalone – Python (Free)", "Feature": "Python package for SRA and GEO metadata retrieval; converts between GSE/RSP/SRX/SRR accessions; downloads FASTQ files; LLM-based metadata extraction; batch operations; integrates with Entrez API."}, {"Name": "Kingfisher", "URL": "https://github.com/wwood/kingfisher-download", "Type": "Standalone – CLI (Free)", "Feature": "Fast flexible SRA/ENA/AWS/GCP download; tries multiple download methods in sequence (ENA FTP, Aspera, AWS, GCP, prefetch); FASTQ/FASTA/SRA/GZIP output; usually faster than SRA Toolkit."}, {"Name": "ffq (Find FASTQ)", "URL": "https://github.com/pachterlab/ffq", "Type": "Standalone – CLI (Free)", "Feature": "Retrieves metadata and download links for SRA, GEO, ENA, DDBJ, EMBL datasets by accession; outputs JSON with full metadata; converts between SRA/study/sample/run accessions; no data download (links only)."}, {"Name": "SRAdownloader", "URL": "https://github.com/s-andrews/sradownloader", "Type": "Standalone – CLI (Free)", "Feature": "Takes SRA Run Selector annotation table as input; retrieves FASTQ files from ENA or NCBI; assigns meaningful filenames based on metadata; batch processing; simpler interface than SRA Toolkit."}, {"Name": "nf-core/fetchngs", "URL": "https://nf-co.re/fetchngs/", "Type": "Standalone – Nextflow (Free)", "Feature": "Nextflow pipeline for downloading raw sequencing data from SRA/ENA/DDBJ/GEO; automatically downloads FASTQ and metadata; outputs standardised samplesheets for other nf-core pipelines; supports HPC/cloud."}, {"Name": "parallel-fastq-dump", "URL": "https://github.com/rvalieris/parallel-fastq-dump", "Type": "Standalone – CLI (Free)", "Feature": "Parallelised wrapper for fastq-dump; splits SRA file and downloads in chunks simultaneously; faster than standard fastq-dump; supports gzip compression; retains all fastq-dump options."}, {"Name": "ENA Browser / ENA FTP", "URL": "https://www.ebi.ac.uk/ena/browser/", "Type": "Web-based + FTP (Free)", "Feature": "European mirror of SRA data; often faster downloads than NCBI for non-US users; direct FTP/Aspera access to FASTQ files; metadata search; API access; direct wget/curl download of FASTQ without conversion."}, {"Name": "Entrez Direct (EDirect)", "URL": "https://www.ncbi.nlm.nih.gov/books/NBK179288/", "Type": "Standalone – CLI (Free)", "Feature": "NCBI command-line utilities for querying all NCBI databases including SRA; esearch, efetch, elink, efilter commands; powerful text-based queries; metadata retrieval; scripting-friendly."}, {"Name": "Trim Galore", "URL": "https://github.com/FelixKrueger/TrimGalore", "Type": "Standalone – CLI (Free)", "Feature": "Quality trimming of FASTQ reads from SRA downloads; adapter auto-detection; integrates FastQC reports; supports paired-end; RRBS and small RNA modes; simple one-command usage."}, {"Name": "Trimmomatic", "URL": "http://www.usadellab.org/cms/?page=trimmomatic", "Type": "Standalone – Java (Free)", "Feature": "Flexible adapter trimming and quality filtering; LEADING, TRAILING, SLIDINGWINDOW, MINLEN, AVGQUAL, HEADCROP parameters; paired-end and single-end support; widely used in standard pipelines."}, {"Name": "fastp", "URL": "https://github.com/OpenGene/fastp", "Type": "Standalone – CLI (Free)", "Feature": "Ultra-fast FASTQ QC and trimming; automatic adapter detection; duplication analysis; per-base quality correction; HTML/JSON reports; supports paired-end; very fast due to multithreading."}, {"Name": "FastQC", "URL": "https://www.bioinformatics.babraham.ac.uk/projects/fastqc/", "Type": "Standalone + Web (Free)", "Feature": "Per-base quality scores; sequence content; GC content; adapter contamination; overrepresented sequences; duplication levels; HTML QC report; GUI and command-line modes; supports many formats."}, {"Name": "MultiQC", "URL": "https://multiqc.info/", "Type": "Standalone – CLI (Free)", "Feature": "Aggregates QC reports from FastQC, Trimmomatic, fastp, STAR, BWA, Bowtie2, featureCounts, and 100+ other tools into a single interactive HTML report; essential for multi-sample SRA studies."}, {"Name": "Galaxy (UseGalaxy.org)", "URL": "https://usegalaxy.org/", "Type": "Web-based (Free)", "Feature": "Browser-based bioinformatics platform; SRA import via accession; quality control; alignment; variant calling; assembly; annotation; 1,000+ integrated tools; no programming required; workflow sharing."}, {"Name": "NCBI Run Selector", "URL": "https://www.ncbi.nlm.nih.gov/Traces/study/", "Type": "Web-based (Free)", "Feature": "Web interface to browse and filter SRA experiments; filter by organism, platform, library strategy, read length, metadata; download accession lists and metadata tables for batch downloading."}, ] # ── Helper functions ────────────────────────────────────────────────────────── def sanitize_dataframe(df): df = df.replace({np.nan: None, 'nan': None, 'NaN': None}) return df.astype(object).where(pd.notnull(df), None) def normalize_val(v): if v is None or pd.isna(v) or str(v).lower().strip() in ['nan', 'none', '', 'n/a']: return "" return str(v).lower().strip() BOS_TAURUS_ALIASES = {"bos taurus", "cattle"} def normalize_organism(v): val = normalize_val(v) return "bos taurus" if val in BOS_TAURUS_ALIASES else val def read_saved_homepage(): if os.path.exists(HOMEPAGE_FILE): try: with open(HOMEPAGE_FILE, "r", encoding="utf-8") as f: return f.read() except Exception: return "" return "" def save_homepage_content(html_content): try: with open(HOMEPAGE_FILE, "w", encoding="utf-8") as f: f.write(html_content) return " Homepage saved successfully!" except Exception as e: return f" Error saving: {str(e)}" def generate_view_html(): saved_content = read_saved_homepage() if not saved_content.strip(): return '
' + HOMEPAGE_DEFAULT + '
' return f"""
{saved_content}
""" def generate_editor_html(): saved_content = read_saved_homepage() return f"""
{saved_content}
""" # ── Paper Alert HTML generator ──────────────────────────────────────────────── def generate_paper_alert_html( topic=PAPER_ALERT_TOPIC, days=PAPER_ALERT_DAYS, max_results=PAPER_ALERT_MAX ): topic_js = topic.replace("'", "\\'") return f""" """ # ══════════════════════════════════════════════════════════════════════════════ # ── Multi-Database Literature Search Engine ─────────────────────────────────── # Searches: PubMed · Europe PMC · CrossRef · Semantic Scholar · bioRxiv/medRxiv # ══════════════════════════════════════════════════════════════════════════════ def _safe_request(url, headers=None, timeout=12): """Make an HTTP GET request and return parsed JSON, or None on failure.""" try: default_headers = {'User-Agent': 'Mozilla/5.0 (BioinformaticsHub/1.0)'} if headers: default_headers.update(headers) req = urllib.request.Request(url, headers=default_headers) with urllib.request.urlopen(req, timeout=timeout) as resp: return json.loads(resp.read().decode('utf-8')) except Exception: return None def _fetch_pubmed(query, since_date_str, max_results=200): """Fetch from NCBI PubMed using boolean query (AND/OR) with separate date range.""" results = [] try: query = normalize_boolean_query(query) query_enc = urllib.parse.quote(query) search_url = ( f"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi" f"?db=pubmed&retmax={max_results}&retmode=json&sort=date" f"&term={query_enc}&mindate={since_date_str}&maxdate=3000" ) search_data = _safe_request(search_url) if not search_data: return results, "PubMed: request failed" ids = search_data.get('esearchresult', {}).get('idlist', []) if not ids: return results, "PubMed: 0 results" summary_url = ( f"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi" f"?db=pubmed&retmode=json&id={','.join(ids)}" ) summary_data = _safe_request(summary_url) if not summary_data: return results, f"PubMed: retrieved {len(ids)} IDs but summary failed" result_dict = summary_data.get('result', {}) uids = result_dict.get('uids', []) for uid in uids: a = result_dict.get(uid, {}) raw_auth = a.get('authors', []) names = [x.get('name', '') for x in raw_auth[:3]] if len(raw_auth) > 3: names.append('et al.') results.append({ 'title': a.get('title', 'Untitled'), 'authors': ', '.join(names) if names else 'Unknown Authors', 'journal': a.get('fulljournalname', a.get('source', '')), 'date': a.get('pubdate', 'N/A'), 'url': f"https://pubmed.ncbi.nlm.nih.gov/{uid}/", 'doi': a.get('elocationid', '').replace('doi: ', '').strip(), 'source': 'PubMed', 'source_color': '#dc2626', 'source_bg': '#fef2f2', }) return results, f"PubMed: {len(results)} results" except Exception as e: return results, f"PubMed: error – {str(e)}" def _fetch_europe_pmc(query, since_date_str, max_results=200): """Fetch from Europe PMC using boolean query with dateFrom parameter.""" results = [] try: query = normalize_boolean_query(query) since_epmc = since_date_str.replace('/', '-') query_enc = urllib.parse.quote(query) url = ( f"https://www.ebi.ac.uk/europepmc/webservices/rest/search" f"?query={query_enc}&resultType=core&pageSize={max_results}&format=json&sort=P_PDATE_D%20desc" f"&dateFrom={since_epmc}" ) data = _safe_request(url) if not data: return results, "Europe PMC: request failed" articles = data.get('resultList', {}).get('result', []) for a in articles: authors_list = a.get('authorList', {}).get('author', []) names = [au.get('fullName', '') for au in authors_list[:3]] if len(authors_list) > 3: names.append('et al.') doi = a.get('doi', '') pmid = a.get('pmid', '') article_url = ( f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/" if pmid else (f"https://doi.org/{doi}" if doi else f"https://europepmc.org/article/{a.get('source','')}/{a.get('id','')}") ) results.append({ 'title': a.get('title', 'Untitled').rstrip('.'), 'authors': ', '.join(names) if names else 'Unknown Authors', 'journal': a.get('journalTitle', a.get('bookOrReportDetails', {}).get('publisher', '')), 'date': a.get('firstPublicationDate', a.get('pubYear', 'N/A')), 'url': article_url, 'doi': doi, 'source': 'Europe PMC', 'source_color': '#7c3aed', 'source_bg': '#f5f3ff', }) return results, f"Europe PMC: {len(results)} results" except Exception as e: return results, f"Europe PMC: error – {str(e)}" def _fetch_crossref(query, since_date_str, max_results=100): """Fetch from CrossRef Works API – supports simple phrase queries.""" results = [] try: since_cr = since_date_str.replace('/', '-') query_enc = urllib.parse.quote(query) url = ( f"https://api.crossref.org/works" f"?query={query_enc}&filter=from-pub-date:{since_cr}" f"&rows={max_results}&sort=published&order=desc" f"&select=DOI,title,author,container-title,published,type" f"&mailto=biohub@example.com" ) data = _safe_request(url) if not data: return results, "CrossRef: request failed" items = data.get('message', {}).get('items', []) for item in items: item_type = item.get('type', '') if item_type not in ('journal-article', 'proceedings-article', 'posted-content'): continue titles = item.get('title', ['Untitled']) title = titles[0] if titles else 'Untitled' authors_raw = item.get('author', []) names = [] for au in authors_raw[:3]: fn = au.get('given', '') ln = au.get('family', '') names.append(f"{fn} {ln}".strip() if fn or ln else au.get('name', '')) if len(authors_raw) > 3: names.append('et al.') journals = item.get('container-title', []) journal = journals[0] if journals else '' doi = item.get('DOI', '') pub_date_parts = item.get('published', {}).get('date-parts', [[]]) parts = pub_date_parts[0] if pub_date_parts else [] pub_date = '-'.join(str(p) for p in parts) if parts else 'N/A' results.append({ 'title': title, 'authors': ', '.join(n for n in names if n) or 'Unknown Authors', 'journal': journal, 'date': pub_date, 'url': f"https://doi.org/{doi}" if doi else '', 'doi': doi, 'source': 'CrossRef', 'source_color': '#059669', 'source_bg': '#ecfdf5', }) return results, f"CrossRef: {len(results)} results" except Exception as e: return results, f"CrossRef: error – {str(e)}" def _fetch_semantic_scholar(query, since_date_str, max_results=100): """Fetch from Semantic Scholar – simple query string.""" results = [] try: since_ss = since_date_str.replace('/', '-') query_enc = urllib.parse.quote(query) url = ( f"https://api.semanticscholar.org/graph/v1/paper/search" f"?query={query_enc}" f"&publicationDateOrYear={since_ss}:" f"&fields=title,authors,venue,year,publicationDate,externalIds,openAccessPdf" f"&limit={min(max_results, 100)}" f"&sort=publicationDate:desc" ) data = _safe_request(url, headers={'x-api-key': ''}) if not data: return results, "Semantic Scholar: request failed" papers = data.get('data', []) for p in papers: authors_raw = p.get('authors', []) names = [a.get('name', '') for a in authors_raw[:3]] if len(authors_raw) > 3: names.append('et al.') ext_ids = p.get('externalIds', {}) doi = ext_ids.get('DOI', '') paper_id = p.get('paperId', '') pdf_info = p.get('openAccessPdf') or {} article_url = ( pdf_info.get('url') or (f"https://doi.org/{doi}" if doi else '') or (f"https://www.semanticscholar.org/paper/{paper_id}" if paper_id else '') ) pub_date = p.get('publicationDate') or str(p.get('year', 'N/A')) results.append({ 'title': p.get('title', 'Untitled'), 'authors': ', '.join(n for n in names if n) or 'Unknown Authors', 'journal': p.get('venue', ''), 'date': pub_date, 'url': article_url, 'doi': doi, 'source': 'Semantic Scholar', 'source_color': '#d97706', 'source_bg': '#fffbeb', }) return results, f"Semantic Scholar: {len(results)} results" except Exception as e: return results, f"Semantic Scholar: error – {str(e)}" def _normalize_doi(doi): """Normalize common DOI forms to a bare DOI string.""" doi = (doi or "").strip() doi = re.sub(r'^(?:https?://)?(?:dx\.)?doi\.org/', '', doi, flags=re.IGNORECASE) doi = doi.strip().rstrip('.,;') return doi.lower() def _extract_doi(doi_text): """Extract a DOI from provider strings such as 'pii: ..., doi: 10.xxxx/yyy'.""" raw = str(doi_text or '').strip() if not raw: return '' # Prefer an actual DOI-looking substring over provider prefixes such as pii:/doi:. match = re.search(r'(10\.\d{4,9}/[^\s,;]+)', raw, flags=re.IGNORECASE) if match: return _normalize_doi(match.group(1)) return _normalize_doi(raw) def _doi_html_result(doi, title, authors='', journal='', date='', url='', source='DOI lookup'): doi = _extract_doi(doi) doi_display = doi url = url or f"https://doi.org/{doi}" is_reviewed = doi in load_reviewed_dois() if doi else False reviewed_badge = ( ' Read' if is_reviewed else '' ) return f"""
{source} DOI {reviewed_badge}
{title}
{f'
Authors: {authors}
' if authors else ''} {f'
Journal: {journal}
' if journal else ''} {f'
Date: {date}
' if date else ''}
DOI: {doi_display}
""" def search_by_doi(doi): """Look up a DOI across CrossRef, Europe PMC and PubMed.""" doi = _extract_doi(doi) if not doi or not re.match(r'^10\.\d{4,9}/\S+$', doi): return "
Please enter a valid DOI, for example 10.1093/nar/gkac123.
" # CrossRef — primary DOI metadata source cr_url = "https://api.crossref.org/works/" + urllib.parse.quote(doi, safe='') cr = _safe_request(cr_url) if cr and cr.get('message'): item = cr['message'] titles = item.get('title') or ['Untitled'] title = titles[0] if titles else 'Untitled' auth = item.get('author') or [] names = [] for a in auth[:5]: name = ' '.join(x for x in [a.get('given',''), a.get('family','')] if x).strip() or a.get('name','') if name: names.append(name) if len(auth) > 5: names.append('et al.') journals = item.get('container-title') or [] journal = journals[0] if journals else '' date_parts = (item.get('published-print') or item.get('published-online') or item.get('published') or {}).get('date-parts', [[]]) parts = date_parts[0] if date_parts else [] date = '-'.join(str(x) for x in parts) if parts else '' url = item.get('URL') or f"https://doi.org/{doi}" return _doi_html_result(doi, title, ', '.join(names), journal, date, url, 'CrossRef') # Europe PMC fallback epmc = _safe_request( "https://www.ebi.ac.uk/europepmc/webservices/rest/search?query=" + urllib.parse.quote(f'DOI:"{doi}"') + "&resultType=core&pageSize=5&format=json" ) if epmc: items = epmc.get('resultList', {}).get('result', []) if items: item = items[0] authors_list = item.get('authorList', {}).get('author', []) names = [a.get('fullName','') for a in authors_list[:5] if a.get('fullName')] if len(authors_list) > 5: names.append('et al.') pmid = item.get('pmid','') url = f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/" if pmid else f"https://doi.org/{doi}" return _doi_html_result( doi, item.get('title','Untitled'), ', '.join(names), item.get('journalTitle',''), item.get('firstPublicationDate', str(item.get('pubYear',''))), url, 'Europe PMC' ) # PubMed final fallback pm_url = ( "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&retmode=json&retmax=5&term=" + urllib.parse.quote(f'"{doi}"[AID] OR "{doi}"[DOI]') ) pm = _safe_request(pm_url) ids = (pm or {}).get('esearchresult', {}).get('idlist', []) if ids: sm = _safe_request( "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&id=" + ','.join(ids) ) if sm: result = sm.get('result', {}) uid = result.get('uids', [ids[0]])[0] item = result.get(uid, {}) raw = item.get('authors', []) names = [a.get('name','') for a in raw[:5] if a.get('name')] if len(raw) > 5: names.append('et al.') return _doi_html_result( doi, item.get('title','Untitled'), ', '.join(names), item.get('fulljournalname', item.get('source','')), item.get('pubdate',''), f"https://pubmed.ncbi.nlm.nih.gov/{uid}/", 'PubMed' ) return "
No publication metadata found for " + doi + ".
" def get_reviewed_table_df(): """Return all reviewed DOIs as a DataFrame for the admin console.""" columns = ['DOI', 'Reviewed At', 'Link'] rows = [] if _check_reviewed_table(): try: res = supabase.table('reviewed_articles').select('doi,reviewed_at').order('reviewed_at', desc=True).execute() for r in (res.data or []): doi = (r.get('doi') or '').strip() if doi: rows.append({'DOI': doi, 'Reviewed At': r.get('reviewed_at') or '', 'Link': f'https://doi.org/{doi}'}) except Exception: pass db_ids = {r['DOI'].lower() for r in rows} for doi in sorted(_reviewed_cache): if doi not in db_ids: rows.append({'DOI': doi, 'Reviewed At': 'Session only', 'Link': f'https://doi.org/{doi}'}) return pd.DataFrame(rows, columns=columns) def reviewed_row_selection(evt: gr.SelectData, current_df): try: row_idx = evt.index[0] doi = str(current_df.iloc[row_idx].get('DOI', '')).strip() return doi, f'Selected reviewed DOI: {doi}' if doi else 'No DOI selected.' except Exception as e: return '', f'Selection error: {e}' def remove_selected_reviewed_doi(selected_doi, request: gr.Request): if not request_has_role(request, "senior"): return 'Senior admin access required.', get_reviewed_table_df(), '' if not selected_doi or not selected_doi.strip(): return ' Select a marked article first.', get_reviewed_table_df(), '' msg = remove_reviewed_article(selected_doi) return msg, get_reviewed_table_df(), '' def _fetch_biorxiv(query, since_date_str, max_results=100): """Fetch from bioRxiv/medRxiv – basic keyword filtering (simple AND on split terms).""" results = [] try: today_str = datetime.date.today().strftime("%Y-%m-%d") since_bxr = since_date_str.replace('/', '-') # Extract keywords (remove quotes and split) keywords = [w.strip('"').lower() for w in query.split() if len(w) > 2 and w not in ('AND', 'OR', 'NOT')] # If query has quotes, keep them as phrase? # We'll keep it simple: use the raw query as a phrase for title/abstract checking. for server in ['biorxiv', 'medrxiv']: url = f"https://api.biorxiv.org/details/{server}/{since_bxr}/{today_str}/0/json" data = _safe_request(url) if not data: continue collection = data.get('collection', []) for item in collection: title = item.get('title', 'Untitled') abstract = item.get('abstract', '').lower() # Check if all keywords appear (simple AND) match = True for kw in keywords: if kw not in title.lower() and kw not in abstract: match = False break if not match: continue doi = item.get('doi', '') authors_str = item.get('authors', 'Unknown Authors') author_list = [a.strip() for a in authors_str.split(';')] names = author_list[:3] if len(author_list) > 3: names.append('et al.') results.append({ 'title': title, 'authors': '; '.join(names), 'journal': f"{server.capitalize()} (Preprint)", 'date': item.get('date', 'N/A'), 'url': f"https://doi.org/{doi}" if doi else f"https://www.{server}.org/", 'doi': doi, 'source': server.capitalize(), 'source_color': '#0891b2', 'source_bg': '#ecfeff', }) if len(results) >= max_results: break if len(results) >= max_results: break return results, f"bioRxiv/medRxiv: {len(results)} results" except Exception as e: return results, f"bioRxiv/medRxiv: error – {str(e)}" def _deduplicate(all_results): """Remove duplicates by DOI (if present) or by normalised title.""" seen_dois = set() seen_titles = set() unique = [] for r in all_results: doi = r.get('doi', '').strip().lower() title_key = r.get('title', '').strip().lower()[:80] if doi and doi in seen_dois: continue if title_key and title_key in seen_titles: continue if doi: seen_dois.add(doi) if title_key: seen_titles.add(title_key) unique.append(r) return unique def _sort_results(results): """Sort results by date descending, with unparseable dates last.""" def date_key(r): d = r.get('date', '') or '' for fmt in ('%Y-%m-%d', '%Y/%m/%d', '%Y %b %d', '%Y %b', '%Y'): try: return datetime.datetime.strptime(d.strip()[:len(fmt)+2], fmt) except Exception: pass parts = d.strip().split() if parts: try: return datetime.datetime(int(parts[0]), 1, 1) except Exception: pass return datetime.datetime.min return sorted(results, key=date_key, reverse=True) def _render_source_badge(source, color, bg): return ( f'{source}' ) def _run_multi_db_search(query, selected_sources, since_date, since_date_dash, window_label): """ Shared engine for running a multi-database search given an explicit 'since' date window, and rendering the resulting HTML. """ if not query or not query.strip(): return "
Please enter a valid search query.
" # If query contains boolean operators, force only PubMed if _is_boolean_query(query): selected_sources = ['PubMed'] # override all_results = [] source_logs = [] source_map = { 'PubMed': lambda: _fetch_pubmed(query, since_date), 'Europe PMC': lambda: _fetch_europe_pmc(query, since_date_dash), 'CrossRef': lambda: _fetch_crossref(query, since_date_dash), 'Semantic Scholar': lambda: _fetch_semantic_scholar(query, since_date_dash), 'bioRxiv / medRxiv': lambda: _fetch_biorxiv(query, since_date_dash), } for src_name, fetch_fn in source_map.items(): if src_name not in selected_sources: continue try: res, log = fetch_fn() all_results.extend(res) source_logs.append(log) except Exception as e: source_logs.append(f"{src_name}: unexpected error – {str(e)}") if not all_results: log_html = '  | '.join(source_logs) return f"""
No papers found for "{query}" {window_label}.
{log_html}
""" unique_results = _deduplicate(all_results) sorted_results = _sort_results(unique_results) source_counts = {} for r in sorted_results: src = r.get('source', 'Unknown') source_counts[src] = source_counts.get(src, 0) + 1 badge_row = ' '.join( f'' f'{src}: {cnt}' for src, cnt in source_counts.items() ) log_html = '  | '.join(source_logs) reviewed_dois = load_reviewed_dois() html = f"""

{len(sorted_results)} unique publications retrieved ({window_label} · deduplicated)

{badge_row}

{log_html}

""" # No reviewed DOIs or mark buttons - removed entirely for r in sorted_results: src_badge = _render_source_badge(r['source'], r['source_color'], r['source_bg']) title = r.get('title', 'Untitled') url = r.get('url', '') authors = r.get('authors', 'Unknown Authors') journal = r.get('journal', '') date = r.get('date', 'N/A') doi = _extract_doi(r.get('doi', '')) reviewed_badge = ( ' Read' if doi and doi in reviewed_dois else '' ) doi_link = ( f'DOI: {doi}' if doi else '' ) title_html = ( f'{title}' if url else f'{title}' ) html += f"""
{title_html}
{src_badge} Authors: {authors} {'•' + journal + '' if journal else ''} {date} {reviewed_badge} {doi_link}
""" html += "
" return html def fetch_multi_db_papers(query, selected_sources): """ Entry-point for the recent (past 6 months) multi-database literature search. """ six_months_ago = datetime.date.today() - datetime.timedelta(days=180) since_date = six_months_ago.strftime("%Y/%m/%d") since_date_dash = six_months_ago.strftime("%Y-%m-%d") return _run_multi_db_search(query, selected_sources, since_date, since_date_dash, "past 6 months") # ── Paged All-Time Literature Search ───────────────────────────────────────── LS_FETCH_SIZE = 50 LS_PAGE_SIZE = 20 LS_YEAR_MIN = 1900 LS_YEAR_MAX = datetime.date.today().year def _fetch_pubmed_paged(query, start_year, end_year, offset): results, exhausted = [], False try: query = normalize_boolean_query(query) query_enc = urllib.parse.quote(query) search_url = ( f"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi" f"?db=pubmed&retmax={LS_FETCH_SIZE}&retstart={offset}" f"&retmode=json&sort=date&term={query_enc}" f"&mindate={start_year}/01/01&maxdate={end_year}/12/31" ) search_data = _safe_request(search_url) if not search_data: return results, True esresult = search_data.get('esearchresult', {}) total = int(esresult.get('count', 0)) ids = esresult.get('idlist', []) if not ids: return results, True exhausted = (offset + len(ids)) >= total summary_url = ( f"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi" f"?db=pubmed&retmode=json&id={','.join(ids)}" ) summary_data = _safe_request(summary_url) if not summary_data: return results, exhausted result_dict = summary_data.get('result', {}) for uid in result_dict.get('uids', []): a = result_dict.get(uid, {}) raw_auth = a.get('authors', []) names = [x.get('name', '') for x in raw_auth[:3]] if len(raw_auth) > 3: names.append('et al.') results.append({ 'title': a.get('title', 'Untitled'), 'authors': ', '.join(names) if names else 'Unknown Authors', 'journal': a.get('fulljournalname', a.get('source', '')), 'date': a.get('pubdate', 'N/A'), 'url': f"https://pubmed.ncbi.nlm.nih.gov/{uid}/", 'doi': a.get('elocationid', '').replace('doi: ', '').strip(), 'source': 'PubMed', 'source_color': '#dc2626', 'source_bg': '#fef2f2', }) return results, exhausted except Exception: return results, True def _fetch_europe_pmc_paged(query, start_year, end_year, offset): results, exhausted = [], False try: query = normalize_boolean_query(query) page_num = offset // LS_FETCH_SIZE query_enc = urllib.parse.quote(query) url = ( f"https://www.ebi.ac.uk/europepmc/webservices/rest/search" f"?query={query_enc}&resultType=core&pageSize={LS_FETCH_SIZE}" f"&page={page_num}&format=json&sort=P_PDATE_D%20desc" f"&dateFrom={start_year}-01-01&dateTo={end_year}-12-31" ) data = _safe_request(url) if not data: return results, True hit_count = int(data.get('hitCount', 0)) articles = data.get('resultList', {}).get('result', []) if not articles: return results, True exhausted = (offset + len(articles)) >= hit_count for a in articles: auth_list = a.get('authorList', {}).get('author', []) names = [au.get('fullName', '') for au in auth_list[:3]] if len(auth_list) > 3: names.append('et al.') doi = a.get('doi', '') pmid = a.get('pmid', '') article_url = ( f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/" if pmid else (f"https://doi.org/{doi}" if doi else f"https://europepmc.org/article/{a.get('source','')}/{a.get('id','')}") ) results.append({ 'title': a.get('title', 'Untitled').rstrip('.'), 'authors': ', '.join(names) if names else 'Unknown Authors', 'journal': a.get('journalTitle', ''), 'date': a.get('firstPublicationDate', a.get('pubYear', 'N/A')), 'url': article_url, 'doi': doi, 'source': 'Europe PMC', 'source_color': '#7c3aed', 'source_bg': '#f5f3ff', }) return results, exhausted except Exception: return results, True def _fetch_crossref_paged(query, start_year, end_year, offset): results, exhausted = [], False try: query_enc = urllib.parse.quote(query) url = ( f"https://api.crossref.org/works" f"?query={query_enc}" f"&filter=from-pub-date:{start_year}-01-01,until-pub-date:{end_year}-12-31" f"&rows={LS_FETCH_SIZE}&offset={offset}&sort=published&order=desc" f"&select=DOI,title,author,container-title,published,type" f"&mailto=biohub@example.com" ) data = _safe_request(url) if not data: return results, True msg = data.get('message', {}) total = msg.get('total-results', 0) items = msg.get('items', []) if not items: return results, True exhausted = (offset + len(items)) >= total for item in items: if item.get('type', '') not in ('journal-article', 'proceedings-article', 'posted-content'): continue titles = item.get('title', ['Untitled']) title = titles[0] if titles else 'Untitled' authors_raw = item.get('author', []) names = [] for au in authors_raw[:3]: fn = au.get('given', ''); ln = au.get('family', '') names.append(f"{fn} {ln}".strip() if fn or ln else au.get('name', '')) if len(authors_raw) > 3: names.append('et al.') journals = item.get('container-title', []) journal = journals[0] if journals else '' doi = item.get('DOI', '') parts = (item.get('published', {}).get('date-parts', [[]])[0] or []) pub_date = '-'.join(str(p) for p in parts) if parts else 'N/A' results.append({ 'title': title, 'authors': ', '.join(n for n in names if n) or 'Unknown Authors', 'journal': journal, 'date': pub_date, 'url': f"https://doi.org/{doi}" if doi else '', 'doi': doi, 'source': 'CrossRef', 'source_color': '#059669', 'source_bg': '#ecfdf5', }) return results, exhausted except Exception: return results, True def _fetch_semantic_scholar_paged(query, start_year, end_year, offset): results, exhausted = [], False try: query_enc = urllib.parse.quote(query) url = ( f"https://api.semanticscholar.org/graph/v1/paper/search" f"?query={query_enc}" f"&publicationDateOrYear={start_year}:{end_year}" f"&fields=title,authors,venue,year,publicationDate,externalIds,openAccessPdf" f"&limit={min(LS_FETCH_SIZE, 100)}&offset={offset}" ) data = _safe_request(url, headers={'x-api-key': ''}) if not data: return results, True total = data.get('total', 0) papers = data.get('data', []) if not papers: return results, True exhausted = (offset + len(papers)) >= total for p in papers: authors_raw = p.get('authors', []) names = [a.get('name', '') for a in authors_raw[:3]] if len(authors_raw) > 3: names.append('et al.') ext_ids = p.get('externalIds', {}) doi = ext_ids.get('DOI', '') paper_id = p.get('paperId', '') pdf_info = p.get('openAccessPdf') or {} article_url = ( pdf_info.get('url') or (f"https://doi.org/{doi}" if doi else '') or (f"https://www.semanticscholar.org/paper/{paper_id}" if paper_id else '') ) pub_date = p.get('publicationDate') or str(p.get('year', 'N/A')) results.append({ 'title': p.get('title', 'Untitled'), 'authors': ', '.join(n for n in names if n) or 'Unknown Authors', 'journal': p.get('venue', ''), 'date': pub_date, 'url': article_url, 'doi': doi, 'source': 'Semantic Scholar', 'source_color': '#d97706', 'source_bg': '#fffbeb', }) return results, exhausted except Exception: return results, True def _fetch_biorxiv_paged(query, start_year, end_year, cursor): results, exhausted = [], False try: start_dt = f"{start_year}-01-01" end_dt = f"{end_year}-12-31" # Use normalized query for keyword extraction norm_query = normalize_boolean_query(query) keywords = [w.strip('"').lower() for w in norm_query.split() if len(w) > 2 and w not in ('AND', 'OR', 'NOT')] for server in ['biorxiv', 'medrxiv']: url = f"https://api.biorxiv.org/details/{server}/{start_dt}/{end_dt}/{cursor}/json" data = _safe_request(url) if not data: continue collection = data.get('collection', []) if not collection: exhausted = True continue if len(collection) < 100: exhausted = True for item in collection: title = item.get('title', 'Untitled') abstract = item.get('abstract', '').lower() match = True for kw in keywords: if kw not in title.lower() and kw not in abstract: match = False break if not match: continue doi = item.get('doi', '') authors_str = item.get('authors', 'Unknown Authors') author_list = [a.strip() for a in authors_str.split(';')] names = author_list[:3] if len(author_list) > 3: names.append('et al.') results.append({ 'title': title, 'authors': '; '.join(names), 'journal': f"{server.capitalize()} (Preprint)", 'date': item.get('date', 'N/A'), 'url': f"https://doi.org/{doi}" if doi else f"https://www.{server}.org/", 'doi': doi, 'source': server.capitalize(), 'source_color': '#0891b2', 'source_bg': '#ecfeff', }) return results, exhausted except Exception: return results, True def _ls_fetch_batch(state): """Fetch one batch from all non-exhausted sources and merge into pool.""" topic = state['topic'] start_year = state['start_year'] end_year = state['end_year'] sources = state['sources'] exhausted = set(state['exhausted']) seen_dois = set(state['seen_dois']) seen_titles = set(state['seen_titles']) fetcher_map = { 'PubMed': lambda off: _fetch_pubmed_paged(topic, start_year, end_year, off), 'Europe PMC': lambda off: _fetch_europe_pmc_paged(topic, start_year, end_year, off), 'CrossRef': lambda off: _fetch_crossref_paged(topic, start_year, end_year, off), 'Semantic Scholar': lambda off: _fetch_semantic_scholar_paged(topic, start_year, end_year, off), 'bioRxiv / medRxiv': lambda off: _fetch_biorxiv_paged(topic, start_year, end_year, off), } new_results = [] for src_name, fetcher in fetcher_map.items(): if src_name not in sources or src_name in exhausted: continue offset = state['offsets'].get(src_name, 0) try: res, is_exhausted = fetcher(offset) if is_exhausted: exhausted.add(src_name) state['offsets'][src_name] = offset + LS_FETCH_SIZE new_results.extend(res) except Exception: exhausted.add(src_name) for r in new_results: doi = r.get('doi', '').strip().lower() title_key = r.get('title', '').strip().lower()[:80] if doi and doi in seen_dois: continue if title_key and title_key in seen_titles: continue if doi: seen_dois.add(doi) if title_key: seen_titles.add(title_key) state['pool'].append(r) state['pool'] = _sort_results(state['pool']) state['exhausted'] = list(exhausted) state['seen_dois'] = list(seen_dois) state['seen_titles'] = list(seen_titles) def _ls_render_page(state): # No reviewed DOIs or mark buttons - removed entirely page = state['page'] pool = state['pool'] start = (page - 1) * LS_PAGE_SIZE end = start + LS_PAGE_SIZE items = pool[start:end] if not items: return ("
No results on this page.
") reviewed_dois = load_reviewed_dois() html = "" for r in items: src_badge = _render_source_badge(r['source'], r['source_color'], r['source_bg']) title = r.get('title', 'Untitled') url = r.get('url', '') authors = r.get('authors', 'Unknown Authors') journal = r.get('journal', '') date = r.get('date', 'N/A') doi = _extract_doi(r.get('doi', '')) reviewed_badge = ( ' Read' if doi and doi in reviewed_dois else '' ) doi_link = ( f'DOI: {doi}' if doi else '' ) title_html = ( f'{title}' if url else f'{title}' ) html += f"""
{title_html}
{src_badge} Authors: {authors} {'•' + journal + '' if journal else ''} {date} {reviewed_badge} {doi_link}
""" return html def _ls_render_nav(state): page = state['page'] pool = state['pool'] exhausted = state['exhausted'] sources = state['sources'] total_pool = len(pool) max_known_page = max(1, (total_pool + LS_PAGE_SIZE - 1) // LS_PAGE_SIZE) all_exhausted = all(s in exhausted for s in sources) if sources else True has_more = not all_exhausted pages_to_show = list(range(1, min(max_known_page, 10) + 1)) show_ellipsis = (max_known_page > 10) or has_more btns = "" for p in pages_to_show: if p == page: btns += (f'{p}') else: btns += (f'{p}') if show_ellipsis: btns += ('…') src_counts = {} for r in pool: s = r.get('source', '') src_counts[s] = src_counts.get(s, 0) + 1 badges = ' '.join( f'{s}: {c}' for s, c in src_counts.items() ) status_line = ( f'
' f'Page {page} · {total_pool} results loaded' + (' · fetching more…' if has_more else ' · all sources loaded') + f'
' ) return f"""
{status_line}
{btns}
{badges}
""" def _ls_empty_state(): return { 'topic': '', 'sources': [], 'start_year': LS_YEAR_MIN, 'end_year': LS_YEAR_MAX, 'page': 1, 'pool': [], 'offsets': {}, 'exhausted': [], 'seen_dois': [], 'seen_titles': [], } def ls_init_search(query, sources, start_year, end_year): start_year = int(start_year) if start_year else LS_YEAR_MIN end_year = int(end_year) if end_year else LS_YEAR_MAX if not query or not query.strip(): err = ("
" " Please enter a valid search query.
") return err, "", _ls_empty_state() # If boolean query, restrict to PubMed if _is_boolean_query(query): sources = ['PubMed'] state = { 'topic': query.strip(), 'sources': list(sources), 'start_year': start_year, 'end_year': end_year, 'page': 1, 'pool': [], 'offsets': {}, 'exhausted': [], 'seen_dois': [], 'seen_titles': [], } _ls_fetch_batch(state) if len(state['pool']) < LS_PAGE_SIZE: _ls_fetch_batch(state) if not state['pool']: empty_html = ( "
" " No papers found for “" + query + "” in the selected year range across chosen databases.
" ) return empty_html, "", state return _ls_render_page(state), _ls_render_nav(state), state def ls_prev_page(state): if not state or state['page'] <= 1: return _ls_render_page(state), _ls_render_nav(state), state state = {**state, 'page': state['page'] - 1} return _ls_render_page(state), _ls_render_nav(state), state def ls_next_page(state): if not state: return "", "", state state = dict(state) next_page = state['page'] + 1 needed = next_page * LS_PAGE_SIZE max_batches = 6 batches_done = 0 all_exhausted = lambda: all(s in state['exhausted'] for s in state['sources']) while len(state['pool']) < needed and not all_exhausted() and batches_done < max_batches: _ls_fetch_batch(state) batches_done += 1 if len(state['pool']) >= (next_page - 1) * LS_PAGE_SIZE + 1: state['page'] = next_page return _ls_render_page(state), _ls_render_nav(state), state # ── Database functions ──────────────────────────────────────────────────────── def search_supabase(species, tissue, tool, accession=""): if not supabase: return pd.DataFrame({"Error": ["Supabase keys are missing. Connection failed."]}) try: table_name = "sra_database" if tool == "SRA" else "geo_database" response = supabase.table(table_name).select("*").execute() raw_data = response.data if not raw_data: return pd.DataFrame({"Status": [f"No records found in {table_name}."]}) df = pd.DataFrame(raw_data) if 'created_at' in df.columns: df = df.drop(columns=['created_at']) if accession and accession.strip(): if "Accession" in df.columns: df = df[df["Accession"].astype(str).str.lower().str.strip() == accession.lower().strip()] if df.empty: return pd.DataFrame({"Status": [f"No matching record for Accession '{accession}'."]}) else: if "Organism" in df.columns: df = df[df["Organism"].apply(normalize_organism) == normalize_organism(species)] if "tissue" in df.columns: df = df[df["tissue"].astype(str).str.lower().str.strip() == tissue.lower().strip()] if df.empty: return pd.DataFrame({"Status": [f"No records for '{species}' in {tool}."]}) display_cols = [c for c in ALL_UNIQUE_COLUMNS if c in df.columns] return df[display_cols].fillna("N/A") except Exception as e: return pd.DataFrame({"Database Error": [f"Failed to fetch data: {str(e)}"]}) def load_entire_table(): if not supabase: return pd.DataFrame({"Error": ["Supabase connection is inactive."]}) try: sra_res = supabase.table("sra_database").select("*").execute() df_sra = pd.DataFrame(sra_res.data) if sra_res.data else pd.DataFrame() if 'created_at' in df_sra.columns: df_sra = df_sra.drop(columns=['created_at']) geo_res = supabase.table("geo_database").select("*").execute() df_geo = pd.DataFrame(geo_res.data) if geo_res.data else pd.DataFrame() if 'created_at' in df_geo.columns: df_geo = df_geo.drop(columns=['created_at']) if df_sra.empty and df_geo.empty: return pd.DataFrame(columns=ALL_UNIQUE_COLUMNS) combined_df = pd.concat([df_sra, df_geo], ignore_index=True) ordered_cols = [c for c in ALL_UNIQUE_COLUMNS if c in combined_df.columns] return combined_df[ordered_cols].fillna("N/A") except Exception as e: return pd.DataFrame({"Error": [str(e)]}) def generate_tools_launchpad_html(dataset_type="GEO"): tools = SRA_TOOLS_DATA if dataset_type == "SRA" else GEO_TOOLS_DATA icon = "" if dataset_type == "SRA" else "" label = "SRA Download & QC" if dataset_type == "SRA" else "GEO Analytical" accent = "#0891b2" if dataset_type == "SRA" else "#2563eb" html = f"""

{icon} Biological Data Located. Launch a {label} Suite below:

""" for tool in tools: html += f"""
{tool['Name']} {tool['Type']}

{tool['Feature']}

Go to Tool
""" html += "
" return html def handle_search_and_box(species, tissue, tool, accession): df_result = search_supabase(species, tissue, tool, accession) tools_launchpad = generate_tools_launchpad_html(dataset_type=tool) return df_result, gr.Column(visible=True), gr.HTML(value=f"
{tools_launchpad}
") def check_upload_credentials_generator(username, password, request: gr.Request): """Authenticate either the junior or senior admin and expose only their allowed UI.""" global _admin_active session_hash = getattr(request, "session_hash", None) junior_configured = bool(AUTH_USER and AUTH_PASS) senior_configured = bool(SENIOR_ADMIN_USER and SENIOR_ADMIN_PASS) if not junior_configured and not senior_configured: _admin_active = False if session_hash: _admin_sessions.pop(session_hash, None) yield ( gr.Column(visible=True), gr.Column(visible=False), "Configuration Error: No admin credentials are configured.", BLANK_TEMPLATE_DF, gr.Column(visible=True), gr.Column(visible=False), gr.HTML(value=generate_editor_html()), False, gr.Column(visible=False), gr.Column(visible=False), get_reviewed_table_df(), get_pubcrawler_terms_df(), get_pubcrawler_articles_html(request), gr.TabItem(visible=False), gr.Column(visible=False) ) return role = None if senior_configured and username == SENIOR_ADMIN_USER and password == SENIOR_ADMIN_PASS: role = "senior" elif junior_configured and username == AUTH_USER and password == AUTH_PASS: role = "junior" if role: _admin_active = True if session_hash: _admin_sessions[session_hash] = role if role == "senior": # Senior: full console, homepage editor, review history and PubCrawler. yield ( gr.Column(visible=False), gr.Column(visible=True), "Senior admin authenticated. Loading administration workspace...", BLANK_TEMPLATE_DF, gr.Column(visible=False), gr.Column(visible=True), gr.HTML(value=generate_editor_html()), True, gr.Column(visible=True), gr.Column(visible=True), get_reviewed_table_df(), get_pubcrawler_terms_df(), get_pubcrawler_articles_html(request), gr.TabItem(visible=True), gr.Column(visible=False) ) entire_table_df = load_entire_table() yield ( gr.Column(visible=False), gr.Column(visible=True), "Senior admin workspace ready.", entire_table_df, gr.Column(visible=False), gr.Column(visible=True), gr.HTML(value=generate_editor_html()), True, gr.Column(visible=True), gr.Column(visible=True), get_reviewed_table_df(), get_pubcrawler_terms_df(), get_pubcrawler_articles_html(request), gr.TabItem(visible=True), gr.Column(visible=False) ) else: # Junior: DOI mark-as-read controls only. No data upload/delete, homepage, # reviewed-history management, or PubCrawler administration. yield ( gr.Column(visible=False), gr.Column(visible=False), "Junior admin authenticated. DOI review tools are enabled in the Literature tabs.", BLANK_TEMPLATE_DF, gr.Column(visible=True), gr.Column(visible=False), gr.HTML(value=generate_view_html()), False, gr.Column(visible=True), gr.Column(visible=True), get_reviewed_table_df(), get_pubcrawler_terms_df(), get_pubcrawler_articles_html(request), gr.TabItem(visible=False), gr.Column(visible=True) ) else: _admin_active = False if session_hash: _admin_sessions.pop(session_hash, None) yield ( gr.Column(visible=True), gr.Column(visible=False), "Invalid credentials. Access denied.", BLANK_TEMPLATE_DF, gr.Column(visible=True), gr.Column(visible=False), gr.HTML(value=generate_view_html()), False, gr.Column(visible=False), gr.Column(visible=False), get_reviewed_table_df(), get_pubcrawler_terms_df(), get_pubcrawler_articles_html(request), gr.TabItem(visible=False), gr.Column(visible=False) ) def upload_data_to_supabase(file_obj, species, tissue, dataset_type, request: gr.Request): if not request_has_role(request, "senior"): return "Senior admin access required." if not supabase: return "Configuration Error: Supabase connection is inactive." if file_obj is None: return "Please upload a valid CSV or Excel file first." try: if file_obj.name.endswith('.csv'): uploaded_df = pd.read_csv(file_obj.name) elif file_obj.name.endswith(('.xls', '.xlsx')): uploaded_df = pd.read_excel(file_obj.name) else: return "Unsupported format. Please upload a .csv or .xlsx file." uploaded_df.columns = [c.strip() for c in uploaded_df.columns] table_name = "sra_database" if dataset_type == "SRA" else "geo_database" for target_col in EXACT_MATCH_COLUMNS: if target_col not in uploaded_df.columns: uploaded_df[target_col] = None db_ready_df = uploaded_df[EXACT_MATCH_COLUMNS].copy().dropna(subset=["Accession"]) canonical_organism = "Bos taurus" if normalize_organism(species) == "bos taurus" else species.strip() db_ready_df["Organism"] = canonical_organism db_ready_df["tissue"] = tissue.strip() db_ready_df["dataset"] = dataset_type.strip() db_ready_df = sanitize_dataframe(db_ready_df) new_records = db_ready_df.to_dict(orient="records") if not new_records: return "No rows with valid Accession numbers found." db_response = supabase.table(table_name).select("Accession,Organism,tissue").execute() existing = db_response.data if db_response.data else [] exact_keys = set() accession_only = {} for r in existing: acc = normalize_val(r.get("Accession")) org = normalize_organism(r.get("Organism", "")) tis = normalize_val(r.get("tissue", "")) exact_keys.add((acc, org, tis)) if acc not in accession_only: accession_only[acc] = [] accession_only[acc].append((r.get("Organism", ""), r.get("tissue", ""))) incoming_org_norm = normalize_organism(canonical_organism) incoming_tis_norm = normalize_val(tissue) true_dupes = [] cross_category_warnings = [] final_records = [] for r in new_records: acc = normalize_val(r.get("Accession")) key = (acc, incoming_org_norm, incoming_tis_norm) if key in exact_keys: true_dupes.append(acc) else: if acc in accession_only: for (ex_org, ex_tis) in accession_only[acc]: cross_category_warnings.append( f" • {acc} already exists as Organism='{ex_org}', Tissue='{ex_tis}'" ) final_records.append(r) msg_parts = [] if cross_category_warnings: warn_lines = "\n".join(cross_category_warnings) msg_parts.append( f" Warning — are you sure you inputted the right parameters?\n" f"The following accessions already exist in a different category:\n{warn_lines}" ) if true_dupes: msg_parts.append(f"Skipped {len(true_dupes)} exact duplicate(s) (same accession + organism + tissue).") if not final_records: msg_parts.append("No new records to insert after duplicate check.") return "\n".join(msg_parts) supabase.table(table_name).insert(final_records).execute() msg_parts.append(f" Inserted {len(final_records)} record(s) into {table_name}.") return "\n".join(msg_parts) except Exception as e: return f"Upload failed: {str(e)}" def handle_row_selection(evt: gr.SelectData, current_df): try: row_idx = evt.index[0] if "Accession" in current_df.columns: accession_id = str(current_df.iloc[row_idx].get("Accession", "")).strip() if accession_id and accession_id != "N/A": return accession_id, f"Selected: {accession_id}" return "", "Selected row has no valid Accession ID." except Exception as e: return "", f"Selection error: {str(e)}" def delete_record_from_supabase(accession_id, current_df, request: gr.Request): if not request_has_role(request, "senior"): return "Senior admin access required.", current_df, accession_id if not supabase or not accession_id.strip() or accession_id == "N/A": return "No valid record selected.", current_df, accession_id try: response = supabase.table("sra_database").delete().eq("Accession", accession_id.strip()).execute() if not response.data: response = supabase.table("geo_database").delete().eq("Accession", accession_id.strip()).execute() if response.data: return f"Deleted: {accession_id}", load_entire_table(), "" return f"Record {accession_id} not found.", current_df, accession_id except Exception as e: return f"Deletion error: {str(e)}", current_df, accession_id def _df_to_html_table(df): if df is None or df.empty: return "

No records found.

" cols = list(df.columns) header = "".join(f'{c}' for c in cols) rows = "" for i, row in df.iterrows(): bg = "#ffffff" if i % 2 == 0 else "#f8fafc" cells = "".join(f'{str(v)}' for v in row) rows += f'{cells}' return f'''
{header}{rows}
''' def update_homepage_string(html_content, request: gr.Request): if not request_has_role(request, "senior"): return "Senior admin access required.", generate_view_html() log_msg = save_homepage_content(html_content) return log_msg, generate_view_html() # ── Homepage Default Content ────────────────────────────────────────────────── HOMEPAGE_DEFAULT = """

Welcome to ReproOmics Hub

Integrated Bioinformatics Platform for Animal Reproduction Research

ReproOmics Hub is an integrated bioinformatics platform developed to support research in Animal Reproductive Technologies (ART) with a primary focus on cattle and buffalo. The portal provides seamless access to public omics datasets, literature resources, AI-assisted analytical tools, and bioinformatics workflows to accelerate research in reproductive biology, embryo development, fertility, and livestock improvement.

About ReproOmics Hub

The portal integrates biological databases, literature resources, and computational tools into a single platform for researchers working in:

Animal Reproduction Technologies (ART) Embryo Development Oocyte Biology Sperm Biology Fertility Research Reproductive Genomics Transcriptomics Epigenomics Single-cell RNA sequencing Functional Genomics

Researchers can search public repositories such as GEO and SRA, explore reproductive omics datasets, perform downstream analyses, and access AI-assisted resources through a unified interface.

Query Database Hub Literature Finder Literature Search (All Time) AI / ML Tools
""" # ── Reviewed-Article helpers (kept for admin DOI box) ────────────────────────────────── _admin_active = False _reviewed_cache: set = set() _supabase_reviewed_ok: bool | None = None def _check_reviewed_table(): global _supabase_reviewed_ok if _supabase_reviewed_ok is not None: return _supabase_reviewed_ok if not supabase: _supabase_reviewed_ok = False return False try: supabase.table("reviewed_articles").select("doi").limit(1).execute() _supabase_reviewed_ok = True except Exception: _supabase_reviewed_ok = False return _supabase_reviewed_ok def load_reviewed_dois(): if _check_reviewed_table(): try: res = supabase.table("reviewed_articles").select("doi").execute() return {r['doi'].strip().lower() for r in (res.data or []) if r.get('doi')} except Exception: pass return set(_reviewed_cache) def save_reviewed_article(article_id): article_id = (article_id or "").strip().lower() if not article_id: return " No identifier provided" if _check_reviewed_table(): try: supabase.table("reviewed_articles").upsert({"doi": article_id}).execute() _reviewed_cache.add(article_id) return f" Marked as reviewed" except Exception: pass _reviewed_cache.add(article_id) return " Marked as reviewed (session only — create reviewed_articles table in Supabase for persistence)" def remove_reviewed_article(article_id): article_id = (article_id or "").strip().lower() if not article_id: return " No identifier provided" if _check_reviewed_table(): try: supabase.table("reviewed_articles").delete().eq("doi", article_id).execute() _reviewed_cache.discard(article_id) return "Removed from reviewed" except Exception: pass _reviewed_cache.discard(article_id) return "Removed from reviewed (session only)" def mark_article_api(doi, action, session_hash=None): role = get_admin_role(session_hash) if role is None: return "Admin login required. Please log in first." if action == "unmark" and role != "senior": return "Senior admin access required to remove reviewed status." article_id = (doi or "").strip().lower() if not article_id: return " No identifier provided" if action == "mark": return save_reviewed_article(article_id) elif action == "unmark": return remove_reviewed_article(article_id) else: return " Unknown action" def list_reviewed_html(): rows = [] if _check_reviewed_table(): try: res = supabase.table("reviewed_articles").select("doi,reviewed_at").order("reviewed_at", desc=True).execute() rows = res.data or [] except Exception: pass db_ids = {r.get('doi', '').lower() for r in rows} cache_only = [c for c in _reviewed_cache if c not in db_ids] if not rows and not cache_only: note = "" if _check_reviewed_table() else ( "

" " Create a reviewed_articles (doi TEXT PRIMARY KEY, reviewed_at TIMESTAMPTZ DEFAULT NOW())" " table in Supabase for persistent storage. Currently using session memory.

" ) return "

No articles marked yet.

" + note html = "" for r in rows: article_id = r.get('doi', '') at = (r.get('reviewed_at') or '')[:10] display = article_id[:80] + ('...' if len(article_id) > 80 else '') link = f"https://doi.org/{article_id}" if not article_id.startswith('http') else article_id html += ( f"
" f"" f"{display}" + (f"{at}" if at else "") + f"
" ) for c in cache_only: display = c[:80] + ('...' if len(c) > 80 else '') html += ( f"
" f"*" f"{display}" f"session" f"
" ) return html def manual_mark_doi(doi, request: gr.Request): """Manually mark a DOI as read for the current authenticated Gradio session.""" if not doi or not doi.strip(): return " Please enter a valid DOI." session_hash = getattr(request, "session_hash", None) if not session_hash: return " Could not determine your session. Please reload the page and log in again." return mark_article_api(doi.strip(), "mark", session_hash) def manual_mark_doi_and_refresh(doi, request: gr.Request): if not request_has_role(request, "senior"): return "Senior admin access required.", get_reviewed_table_df() msg = manual_mark_doi(doi, request) return msg, get_reviewed_table_df() # ── Theme ───────────────────────────────────────────────────────────────────── light_theme = gr.themes.Base( primary_hue=gr.themes.colors.blue, neutral_hue=gr.themes.colors.slate, ).set( body_background_fill="#f3f6fa", body_background_fill_dark="#f3f6fa", body_text_color="#132238", body_text_color_dark="#132238", block_background_fill="#ffffff", block_background_fill_dark="#ffffff", block_border_color="#dbe3ed", block_border_color_dark="#dbe3ed", block_border_width="1px", block_label_text_color="#475569", block_label_text_color_dark="#475569", block_label_text_weight="600", block_label_text_size="13px", block_title_text_color="#132238", block_title_text_color_dark="#132238", block_title_text_weight="700", block_shadow="0 1px 2px rgba(15,23,42,.04)", input_background_fill="#ffffff", input_background_fill_dark="#ffffff", input_border_color="#cfd9e5", input_border_color_dark="#cfd9e5", input_border_width="1px", input_placeholder_color="#94a3b8", input_placeholder_color_dark="#94a3b8", input_shadow="none", input_shadow_focus="0 0 0 3px rgba(37,99,235,.10)", checkbox_background_color="#ffffff", checkbox_background_color_dark="#ffffff", checkbox_border_color="#cfd9e5", checkbox_border_color_hover="#94a3b8", checkbox_border_color_selected="#2563eb", checkbox_background_color_selected="#2563eb", button_primary_background_fill="#2563eb", button_primary_background_fill_dark="#2563eb", button_primary_background_fill_hover="#1d4ed8", button_primary_text_color="#ffffff", button_primary_text_color_dark="#ffffff", button_primary_border_color="transparent", button_secondary_background_fill="#ffffff", button_secondary_background_fill_dark="#ffffff", button_secondary_background_fill_hover="#f8fafc", button_secondary_text_color="#334155", button_secondary_text_color_dark="#334155", button_secondary_border_color="#cfd9e5", button_secondary_border_color_dark="#cfd9e5", button_secondary_border_color_hover="#b8c6d8", button_large_padding="10px 18px", button_small_padding="6px 12px", button_large_text_size="13px", button_small_text_size="12px", table_even_background_fill="#ffffff", table_even_background_fill_dark="#ffffff", table_odd_background_fill="#f8fafc", table_odd_background_fill_dark="#f8fafc", table_border_color="#dbe3ed", table_border_color_dark="#dbe3ed", border_color_primary="#dbe3ed", color_accent="#2563eb", color_accent_soft="#eff6ff", ) css = r""" :root { --bg: #eef2f7; --bg-deep: #e9eef5; --surface: rgba(255,255,255,.96); --surface-solid: #ffffff; --surface-soft: #f7f9fc; --surface-tint: #f3f7ff; --line: #dde4ee; --line-strong: #cbd6e4; --text: #122033; --text-2: #334155; --muted: #66758a; --muted-2: #94a3b8; --accent: #2563eb; --accent-2: #4f46e5; --accent-soft: #eef4ff; --success: #16803a; --success-soft: #edf9f1; --danger: #b42318; --danger-soft: #fff1f1; --warning-soft: #fff8e8; --radius-sm: 10px; --radius-md: 14px; --radius-lg: 18px; --shadow-xs: 0 1px 2px rgba(15,23,42,.04); --shadow-sm: 0 2px 8px rgba(15,23,42,.05), 0 1px 2px rgba(15,23,42,.04); --shadow-md: 0 12px 30px rgba(15,23,42,.07), 0 2px 8px rgba(15,23,42,.04); --shadow-lg: 0 22px 50px rgba(15,23,42,.10), 0 4px 14px rgba(15,23,42,.05); } *, *::before, *::after { box-sizing: border-box; } html, body { margin: 0 !important; background: var(--bg) !important; } body { color: var(--text) !important; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important; background: radial-gradient(circle at 10% 0%, rgba(79,70,229,.05), transparent 32%), radial-gradient(circle at 92% 5%, rgba(37,99,235,.05), transparent 28%), var(--bg) !important; } .gradio-container { max-width: 1540px !important; margin: 0 auto !important; padding: 20px 30px 48px !important; background: transparent !important; } .gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container h4, .gradio-container strong, .gradio-container b { color: var(--text) !important; } .gradio-container p, .gradio-container label, .gradio-container .block label span { color: var(--muted) !important; } /* App header */ .app-header { position: relative; overflow: hidden; margin: 0 0 18px; padding: 20px 22px; border: 1px solid rgba(203,214,228,.9); border-radius: 20px; background: linear-gradient(135deg, rgba(255,255,255,.99) 0%, rgba(246,249,255,.98) 62%, rgba(243,247,255,.98) 100%); box-shadow: var(--shadow-md); } .app-header::after { content: ""; position: absolute; left: 22px; right: 22px; bottom: 0; height: 2px; background: linear-gradient(90deg, transparent, #7aa2ff, #8b7cf6, transparent); opacity: .6; } .app-header-row { display:flex; align-items:center; justify-content:space-between; gap:22px; } .app-brand { display:flex; align-items:center; gap:14px; min-width:0; } .app-mark { width: 48px; height: 48px; flex: 0 0 48px; display:flex; align-items:center; justify-content:center; border-radius:14px; color:#fff; font-size:16px; font-weight:800; letter-spacing:.02em; background: linear-gradient(135deg, #2563eb 0%, #4f46e5 100%); border: 1px solid rgba(37,99,235,.25); box-shadow: 0 8px 18px rgba(37,99,235,.20); } .app-title { margin:0; font-size:21px; font-weight:850; letter-spacing:-.035em; } .app-subtitle { margin:4px 0 0; font-size:12px; color:var(--muted); } .app-meta { display:flex; align-items:center; gap:7px; flex-wrap:wrap; justify-content:flex-end; } .app-meta-chip { border:1px solid #d9e1ec; background:rgba(255,255,255,.82); color:#475569; padding:6px 10px; border-radius:999px; font-size:10.5px; font-weight:700; letter-spacing:.01em; box-shadow: var(--shadow-xs); } /* Navigation */ .tabs { gap: 10px !important; } .tab-nav { position: sticky; top: 10px; z-index: 20; display:flex !important; gap:6px !important; width:fit-content; max-width:100%; margin:0 auto 22px !important; padding:6px !important; border:1px solid rgba(203,214,228,.92) !important; border-radius:14px !important; background:rgba(255,255,255,.90) !important; box-shadow:0 10px 28px rgba(15,23,42,.08) !important; backdrop-filter:blur(14px); } .tab-nav button { border:1px solid transparent !important; background:transparent !important; color:#64748b !important; border-radius:9px !important; padding:9px 13px !important; font-size:11.5px !important; font-weight:750 !important; transition:background .18s ease, color .18s ease, box-shadow .18s ease, transform .18s ease !important; } .tab-nav button:hover { background:#f4f7fb !important; color:#1e40af !important; transform:translateY(-1px); } .tab-nav button.selected { color:#173b9e !important; background:linear-gradient(180deg,#eef4ff,#e7efff) !important; border-color:#c9d8fb !important; box-shadow:0 3px 10px rgba(37,99,235,.10) !important; } /* Base component surfaces */ .gradio-container .block, .gradio-container .form, .gradio-container .panel, .gradio-container [data-testid="block-wrap"] { border-color: transparent !important; box-shadow: none !important; background: transparent !important; } .gradio-container .block:hover, .gradio-container .form:hover, .gradio-container .panel:hover, .gradio-container [data-testid="block-wrap"]:hover { box-shadow:none !important; background:transparent !important; } .clean-card, .admin-section { background:var(--surface-solid); border:1px solid var(--line); border-radius:var(--radius-md); box-shadow:var(--shadow-sm); } .clean-card { padding:18px; } .clean-section-title { margin:0 0 6px; font-size:15px; font-weight:800; letter-spacing:-.015em; } .clean-section-note { margin:0; font-size:12px; color:var(--muted); line-height:1.65; } .clean-kicker { margin-bottom:7px; color:#64748b; font-size:9.5px; font-weight:850; text-transform:uppercase; letter-spacing:.13em; } .home-hero { position:relative; overflow:hidden; padding:28px 28px; border:1px solid #d7e2f0; border-radius:20px; background: radial-gradient(circle at 100% 0%, rgba(79,70,229,.07), transparent 30%), linear-gradient(140deg,#ffffff,#f7faff); box-shadow:var(--shadow-md); } .home-hero::before { content:""; position:absolute; width:180px; height:180px; right:-70px; top:-75px; border-radius:50%; background:rgba(37,99,235,.05); } /* Form controls */ .gradio-container input, .gradio-container textarea, .gradio-container select, .gradio-container .wrap input, .gradio-container .wrap textarea { background:#fff !important; border:1px solid var(--line-strong) !important; border-radius:10px !important; box-shadow:none !important; color:var(--text) !important; font-size:13px !important; transition:border-color .16s ease, box-shadow .16s ease, background .16s ease !important; } .gradio-container input:hover, .gradio-container textarea:hover, .gradio-container select:hover { border-color:#bac7d7 !important; } .gradio-container input:focus, .gradio-container textarea:focus, .gradio-container select:focus { border-color:#78a0ee !important; box-shadow:0 0 0 3px rgba(37,99,235,.09) !important; outline:none !important; } /* The component wrapper stays visually neutral; only the actual control responds. */ .gradio-container .block:has(input):hover, .gradio-container .block:has(textarea):hover, .gradio-container .block:has(select):hover { background:transparent !important; box-shadow:none !important; border-color:transparent !important; } /* Checkbox / radio groups */ .gradio-container .checkbox-group .wrap, .gradio-container .radio-group .wrap { gap:8px !important; } .gradio-container .checkbox-group label, .gradio-container .radio-group label { min-height:36px; border:1px solid var(--line-strong) !important; background:#fff !important; border-radius:10px !important; padding:8px 11px !important; color:#475569 !important; transition:border-color .15s ease, background .15s ease, transform .15s ease !important; } .gradio-container .checkbox-group label:hover, .gradio-container .radio-group label:hover { background:#f8fbff !important; border-color:#b8c6d9 !important; transform:translateY(-1px); } .gradio-container .checkbox-group label.selected, .gradio-container .radio-group label.selected { background:linear-gradient(180deg,#f0f5ff,#eaf1ff) !important; border-color:#94b1eb !important; color:#17408f !important; } /* Buttons */ .gradio-container button { min-height:40px !important; border-radius:10px !important; box-shadow:none !important; font-size:12px !important; font-weight:760 !important; letter-spacing:.005em !important; transition:transform .16s ease, box-shadow .16s ease, background .16s ease, border-color .16s ease !important; } .gradio-container button.primary { color:#fff !important; background:linear-gradient(135deg,#2563eb,#315bdc) !important; border:1px solid #2563eb !important; box-shadow:0 7px 16px rgba(37,99,235,.18) !important; } .gradio-container button.primary:hover { background:linear-gradient(135deg,#1d4ed8,#4338ca) !important; transform:translateY(-1px); box-shadow:0 10px 20px rgba(37,99,235,.22) !important; } .gradio-container button.secondary { color:#334155 !important; background:#fff !important; border:1px solid var(--line-strong) !important; } .gradio-container button.secondary:hover { background:#f8fafc !important; border-color:#b8c6d7 !important; transform:translateY(-1px); box-shadow:0 5px 12px rgba(15,23,42,.06) !important; } .gradio-container button.stop { color:#b42318 !important; background:#fff !important; border:1px solid #f0c9c6 !important; } .gradio-container button.stop:hover { background:#fff7f6 !important; border-color:#e7aaa5 !important; } /* Dataframes */ .gradio-container .table-wrap, .gradio-container .dataframe { overflow:hidden !important; border:1px solid var(--line) !important; border-radius:12px !important; background:#fff !important; box-shadow:var(--shadow-xs) !important; } .gradio-container .dataframe table { font-size:12px !important; } .gradio-container .dataframe th { padding:10px 12px !important; background:#f6f8fb !important; color:#475569 !important; font-weight:750 !important; border-bottom:1px solid var(--line) !important; } .gradio-container .dataframe td { padding:9px 12px !important; color:#334155 !important; border-color:#edf1f5 !important; } .gradio-container .dataframe tr:hover td { background:#f8fbff !important; } /* Result area */ .results-shell { display:flex; flex-direction:column; gap:12px; } .results-summary { padding:14px 16px; border:1px solid #d9e3ef; border-radius:12px; background:linear-gradient(135deg,#ffffff,#f8fbff); box-shadow:var(--shadow-xs); } .results-summary-title { font-size:13px; font-weight:800; color:var(--text); } .results-summary-note { font-size:11px; color:var(--muted); margin-top:3px; } .source-pills { display:flex; gap:6px; flex-wrap:wrap; } .source-pill { padding:4px 8px; border-radius:999px; background:#f4f7fb; border:1px solid #dfe6ee; font-size:10px; color:#475569; font-weight:700; } /* AI tools */ .ai-tools-section-title { margin:28px 0 12px; padding:0 0 9px; border-bottom:1px solid var(--line); color:#334155 !important; font-size:11px !important; font-weight:850 !important; text-transform:uppercase; letter-spacing:.12em; } .ai-tools-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:15px; } .ai-tool-card { display:flex; flex-direction:column; gap:9px; padding:17px 18px; background:#fff !important; border:1px solid var(--line) !important; border-radius:14px !important; text-decoration:none !important; color:inherit !important; box-shadow:var(--shadow-sm) !important; transition:transform .18s ease, border-color .18s ease, box-shadow .18s ease !important; } .ai-tool-card:hover { transform:translateY(-3px) !important; border-color:#b9c9e0 !important; box-shadow:var(--shadow-lg) !important; } .ai-tool-card .card-name { color:#1d4ed8 !important; font-size:13px; font-weight:780; line-height:1.4; } .ai-tool-card .card-desc { color:#64748b !important; font-size:11.5px; line-height:1.62; flex:1; } .ai-tool-card .card-footer { display:flex; flex-wrap:wrap; gap:6px; margin-top:3px; } .card-badge { font-size:9.5px; font-weight:700; padding:3px 7px; border-radius:999px; } .badge-type { background:#eef4ff !important; color:#1d4ed8 !important; border:1px solid #c5d7ff !important; } .badge-spp { background:#edf9f1 !important; color:#166534 !important; border:1px solid #c9ebd2 !important; } .badge-link { background:#f7f9fc !important; color:#64748b !important; border:1px solid #e2e7ef !important; margin-left:auto; } /* PubCrawler */ .crawler-banner { position:relative; overflow:hidden; border:1px solid #d2def1; background:linear-gradient(135deg,#ffffff,#f3f7ff 72%,#f2efff 100%); border-radius:18px; padding:22px 22px; margin-bottom:16px; box-shadow:var(--shadow-md); } .crawler-banner::after { content:""; position:absolute; width:160px; height:160px; right:-55px; top:-70px; border-radius:50%; background:rgba(79,70,229,.05); } .crawler-banner h2 { margin:0 0 6px; font-size:20px; letter-spacing:-.025em; } .crawler-banner p { margin:0; font-size:12px; max-width:820px; line-height:1.65; } .crawler-stat { color:#1d4ed8; font-weight:800; } .crawler-grid { align-items:stretch !important; } /* Admin */ .admin-login { max-width:570px; margin:34px auto; } .admin-login > div { background:linear-gradient(145deg,#ffffff,#f7faff) !important; border:1px solid #d7e2ef !important; border-radius:18px !important; box-shadow:var(--shadow-lg) !important; padding:20px !important; } .admin-dashboard-title { margin:0 0 4px; font-size:18px; font-weight:800; } .admin-section { padding:17px; } .admin-section + .admin-section { margin-top:13px; } /* Homepage editor */ #homepage-canvas-editor { background:#fbfcfe !important; border:1px solid #d6dee9 !important; border-radius:12px !important; } /* Reduce excessive default Gradio whitespace */ .gradio-container .gap { gap:12px !important; } .gradio-container .form { gap:10px !important; } /* Mobile */ @media (max-width: 1100px) { .gradio-container { padding:14px 16px 34px !important; } .app-header-row { align-items:flex-start; flex-direction:column; } .app-meta { justify-content:flex-start; } .tab-nav { width:100%; overflow-x:auto; justify-content:flex-start; margin-left:0 !important; margin-right:0 !important; } .tab-nav button { flex:0 0 auto; } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition:none !important; animation:none !important; } } /* ── Visibility and contrast refinement ─────────────────────────────────── */ :root { --bg: #f4f7fb; --surface: #ffffff; --surface-solid: #ffffff; --surface-soft: #f8fafc; --surface-tint: #eef4ff; --line: #cfd8e3; --line-strong: #aebccc; --text: #0f172a; --text-2: #334155; --muted: #475569; --muted-2: #64748b; } /* Stronger baseline typography and clearer section hierarchy. */ .gradio-container, .gradio-container p, .gradio-container label, .gradio-container .block label span, .gradio-container .prose, .gradio-container .markdown-body { opacity: 1 !important; } .gradio-container p, .gradio-container .clean-section-note, .gradio-container .clean-kicker { color: #475569 !important; } .gradio-container .clean-section-title, .gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container h4, .gradio-container strong, .gradio-container b { color: #0f172a !important; } .gradio-container .clean-kicker { font-size: 10px !important; font-weight: 850 !important; letter-spacing: .14em !important; } /* Make form labels unmistakable. */ .gradio-container label, .gradio-container .block label, .gradio-container .block label span, .gradio-container [data-testid="block-label"], .gradio-container [data-testid="block-label"] span { color: #1e293b !important; font-weight: 700 !important; font-size: 12px !important; } /* Clearer inputs and placeholders. */ .gradio-container input, .gradio-container textarea, .gradio-container select, .gradio-container .wrap input, .gradio-container .wrap textarea { border-color: #aebccc !important; color: #0f172a !important; background: #ffffff !important; } .gradio-container input::placeholder, .gradio-container textarea::placeholder { color: #64748b !important; opacity: 1 !important; } .gradio-container input:hover, .gradio-container textarea:hover, .gradio-container select:hover { border-color: #7f91a6 !important; } .gradio-container input:focus, .gradio-container textarea:focus, .gradio-container select:focus { border-color: #2563eb !important; box-shadow: 0 0 0 3px rgba(37,99,235,.12) !important; } /* Stronger radio/checkbox readability without adding visual clutter. */ .gradio-container .checkbox-group label, .gradio-container .radio-group label { border-color: #c1ccd8 !important; color: #334155 !important; background: #ffffff !important; } .gradio-container .checkbox-group label:hover, .gradio-container .radio-group label:hover { background: #f3f7ff !important; border-color: #8ea4c0 !important; } .gradio-container .checkbox-group label.selected, .gradio-container .radio-group label.selected { color: #163b86 !important; background: #eaf1ff !important; border-color: #6f93d7 !important; } /* More legible navigation. */ .tab-nav { border-color: #b9c6d6 !important; background: rgba(255,255,255,.97) !important; box-shadow: 0 8px 22px rgba(15,23,42,.10) !important; } .tab-nav button { color: #475569 !important; font-size: 12px !important; font-weight: 800 !important; } .tab-nav button:hover { background: #eef3f9 !important; color: #173b7a !important; } .tab-nav button.selected { color: #123b86 !important; background: #e7efff !important; border-color: #b3c8ee !important; } /* Buttons: stronger text and borders so actions remain obvious. */ .gradio-container button.secondary { color: #1e293b !important; border-color: #aebccc !important; } .gradio-container button.secondary:hover { color: #0f172a !important; background: #f1f5f9 !important; border-color: #8194aa !important; } .gradio-container button.primary { min-height: 42px !important; font-size: 12px !important; font-weight: 800 !important; } /* Tables need stronger header/body contrast. */ .gradio-container .dataframe th { background: #edf2f7 !important; color: #1e293b !important; border-bottom-color: #c5d0dc !important; } .gradio-container .dataframe td { color: #334155 !important; border-color: #e1e7ee !important; } .gradio-container .dataframe tr:hover td { background: #f3f7fb !important; } /* Give custom HTML notes/cards enough contrast against the page background. */ .clean-card, .admin-section, .results-summary, .ai-tool-card { border-color: #cbd5e1 !important; } .clean-section-note { font-size: 12.5px !important; } /* Remove accidental low-opacity text inherited from the theme. */ .gradio-container [style*="color:#94a3b8"], .gradio-container [style*="color: #94a3b8"] { color: #64748b !important; } /* ── Larger type and denser layout refinement ───────────────────────────── */ .gradio-container { max-width: 1600px !important; padding: 12px 18px 28px !important; } .gradio-container .row, .gradio-container [data-testid="row"] { gap: 16px !important; } .gradio-container [data-testid="column"] { min-height: 0 !important; } .app-header { margin-bottom: 12px !important; padding: 14px 18px !important; border-radius: 16px !important; } .app-header-row { gap: 18px !important; } .app-mark { width: 52px !important; height: 52px !important; flex-basis: 52px !important; } .app-title { font-size: 26px !important; } .app-subtitle { font-size: 14px !important; } .app-meta-chip { font-size: 12px !important; padding: 7px 11px !important; } .tab-nav { margin-bottom: 14px !important; padding: 4px !important; border-radius: 12px !important; } .tab-nav button { min-height: 42px !important; padding: 10px 15px !important; font-size: 13.5px !important; } .clean-card { padding: 14px 16px !important; } .clean-section-title { font-size: 18px !important; margin-bottom: 6px !important; } .clean-section-note { font-size: 14px !important; line-height: 1.55 !important; } .clean-kicker { font-size: 11px !important; margin-bottom: 5px !important; } .home-hero { padding: 20px 22px !important; border-radius: 16px !important; margin-bottom: 12px !important; } .gradio-container label, .gradio-container .block label, .gradio-container .block label span, .gradio-container [data-testid="block-label"], .gradio-container [data-testid="block-label"] span { font-size: 13.5px !important; line-height: 1.3 !important; } .gradio-container input, .gradio-container textarea, .gradio-container select, .gradio-container .wrap input, .gradio-container .wrap textarea { min-height: 46px !important; font-size: 15px !important; line-height: 1.4 !important; border-radius: 9px !important; } .gradio-container textarea { min-height: 100px !important; } .gradio-container input::placeholder, .gradio-container textarea::placeholder { font-size: 14px !important; } .gradio-container .checkbox-group label, .gradio-container .radio-group label { min-height: 42px !important; padding: 9px 12px !important; font-size: 13.5px !important; } .gradio-container button { min-height: 44px !important; padding: 9px 15px !important; font-size: 14px !important; } .gradio-container button.primary { min-height: 44px !important; font-size: 14px !important; } .gradio-container .dataframe table { font-size: 13.5px !important; } .gradio-container .dataframe th { padding: 11px 13px !important; font-size: 13.5px !important; } .gradio-container .dataframe td { padding: 10px 13px !important; font-size: 13.5px !important; } .results-summary { padding: 13px 15px !important; } .results-summary-title { font-size: 15px !important; } .results-summary-note { font-size: 13px !important; } .source-pill { font-size: 11.5px !important; padding: 5px 9px !important; } /* Custom result cards use inline font sizes; raise them consistently. */ .art-result-card { padding: 16px 18px !important; } .art-result-card a, .art-result-card span, .art-result-card div, .art-result-card em { font-size: 13.5px !important; } .art-result-card a { font-size: 16px !important; } .ai-tools-grid { grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)) !important; gap: 12px !important; } .ai-tool-card { padding: 15px 17px !important; } .ai-tool-card .card-name { font-size: 15px !important; } .ai-tool-card .card-desc { font-size: 13px !important; line-height: 1.55 !important; } .card-badge { font-size: 11px !important; padding: 4px 8px !important; } .crawler-banner { padding: 18px 20px !important; margin-bottom: 12px !important; border-radius: 16px !important; } .crawler-banner h2 { font-size: 23px !important; } .crawler-banner p { font-size: 14px !important; line-height: 1.55 !important; } .admin-login { margin: 18px auto !important; } .admin-section { padding: 14px !important; } .admin-section + .admin-section { margin-top: 10px !important; } /* Reduce oversized empty-state areas without removing the breathing room. */ .gradio-container .clean-card[style*="padding:70px"], .gradio-container .clean-card[style*="padding: 70px"] { padding: 36px 20px !important; } .gradio-container .clean-card[style*="padding:55px"], .gradio-container .clean-card[style*="padding: 55px"] { padding: 30px 20px !important; } @media (max-width: 1100px) { .gradio-container { padding: 10px 12px 22px !important; } .app-title { font-size: 22px !important; } .app-subtitle { font-size: 13px !important; } .tab-nav button { font-size: 13px !important; padding: 9px 12px !important; } .gradio-container input, .gradio-container textarea, .gradio-container select { font-size: 14px !important; } } """ # ── PubCrawler term management (admin-only) ────────────────────────────────── def _pubcrawler_admin_ok(request: gr.Request): return request_has_role(request, "senior") def get_pubcrawler_terms_df(): """Load the terms stored in the Supabase `pubcrawler` table.""" columns = ['ID', 'Term'] if not supabase: return pd.DataFrame(columns=columns) try: res = ( supabase.table('pubcrawler') .select('id,term') .order('id', desc=True) .execute() ) rows = [] for row in (res.data or []): rows.append({ 'ID': row.get('id'), 'Term': row.get('term', ''), }) return pd.DataFrame(rows, columns=columns) except Exception: return pd.DataFrame(columns=columns) def save_pubcrawler_term(term, request: gr.Request): """Insert a crawler term for an authenticated admin.""" if not _pubcrawler_admin_ok(request): return ' Admin login required. Please log in first.', get_pubcrawler_terms_df(), '', get_pubcrawler_articles_html(request) term = ' '.join(str(term or '').split()) if not term: return ' Enter a term before saving.', get_pubcrawler_terms_df(), '', get_pubcrawler_articles_html(request) if not supabase: return ' Supabase connection is unavailable.', get_pubcrawler_terms_df(), '', get_pubcrawler_articles_html(request) try: # Prevent accidental duplicate terms without requiring a unique DB constraint. existing = ( supabase.table('pubcrawler') .select('id,term') .eq('term', term) .limit(1) .execute() ) if existing.data: return 'Note: That term is already in PubCrawler.', get_pubcrawler_terms_df(), term, get_pubcrawler_articles_html(request) supabase.table('pubcrawler').insert({'term': term}).execute() return f' Saved PubCrawler term: **{term}**', get_pubcrawler_terms_df(), '', get_pubcrawler_articles_html(request) except Exception as e: return f' Could not save term: {e}', get_pubcrawler_terms_df(), term, get_pubcrawler_articles_html(request) def pubcrawler_row_selection(evt: gr.SelectData, current_df): try: row_idx = evt.index[0] row = current_df.iloc[row_idx] row_id = str(row.get('ID', '')).strip() term = str(row.get('Term', '')).strip() return row_id, f'Selected term: {term}' if term else 'No term selected.' except Exception as e: return '', f'Selection error: {e}' def get_pubcrawler_articles_html(request: gr.Request): """Search every saved PubCrawler term across the literature sources for the past 30 days.""" if not _pubcrawler_admin_ok(request): return "
Admin login required. Please log in first.
" if not supabase: return "
Supabase connection is unavailable.
" terms_df = get_pubcrawler_terms_df() terms = [] if not terms_df.empty: terms = [str(x).strip() for x in terms_df['Term'].tolist() if str(x).strip()] if not terms: return "
No PubCrawler terms have been saved yet.
" since = datetime.date.today() - datetime.timedelta(days=30) since_date = since.strftime('%Y/%m/%d') since_dash = since.strftime('%Y-%m-%d') all_results = [] term_logs = [] for term in terms: source_map = { 'PubMed': lambda t=term: _fetch_pubmed(t, since_date, max_results=200), 'Europe PMC': lambda t=term: _fetch_europe_pmc(t, since_dash, max_results=200), 'CrossRef': lambda t=term: _fetch_crossref(t, since_dash, max_results=100), 'Semantic Scholar': lambda t=term: _fetch_semantic_scholar(t, since_dash, max_results=100), 'bioRxiv / medRxiv': lambda t=term: _fetch_biorxiv(t, since_dash, max_results=100), } for source_name, fetch_fn in source_map.items(): try: results, log = fetch_fn() for result in results: result = dict(result) result['_crawler_term'] = term all_results.append(result) if results: term_logs.append(f"{term}: {source_name} {len(results)}") except Exception as e: term_logs.append(f"{term}: {source_name} error") if not all_results: return f"""
No publications matching the saved PubCrawler terms were found in the past 30 days.
""" # Deduplicate while retaining all saved terms that matched each article. merged = {} for r in all_results: doi = _extract_doi(r.get('doi', '')) title_key = str(r.get('title', '')).strip().lower() key = ('doi', doi.lower()) if doi else ('title', title_key[:140]) if key not in merged: merged[key] = dict(r) merged[key]['_crawler_terms'] = [r.get('_crawler_term', '')] else: hit = r.get('_crawler_term', '') if hit and hit not in merged[key]['_crawler_terms']: merged[key]['_crawler_terms'].append(hit) if not merged[key].get('doi') and doi: merged[key]['doi'] = doi results = _sort_results(list(merged.values())) reviewed_dois = load_reviewed_dois() source_counts = {} for r in results: src = r.get('source', 'Unknown') source_counts[src] = source_counts.get(src, 0) + 1 count_badges = ''.join( f"{html_lib.escape(src)}: {cnt}" for src, cnt in source_counts.items() ) html = f"""
{len(results)} unique publications · past 30 days
{count_badges}
Searching {len(terms)} saved PubCrawler term(s); duplicates merged.
""" for r in results: title = html_lib.escape(str(r.get('title', 'Untitled'))) url = html_lib.escape(str(r.get('url', '') or '')) authors = html_lib.escape(str(r.get('authors', 'Unknown Authors'))) journal = html_lib.escape(str(r.get('journal', ''))) date = html_lib.escape(str(r.get('date', 'N/A'))) doi = _extract_doi(r.get('doi', '')) doi_esc = html_lib.escape(doi) src = html_lib.escape(str(r.get('source', 'Unknown'))) src_color = r.get('source_color', '#2563eb') src_bg = r.get('source_bg', '#eff6ff') terms_hit = r.get('_crawler_terms', []) or [] term_html = ''.join( f"{html_lib.escape(t)}" for t in terms_hit ) reviewed_badge = ( ' Read' if doi and doi in reviewed_dois else '' ) title_html = ( f'{title}' if url else f'
{title}
' ) html += f"""
{title_html}
{src} {reviewed_badge} {term_html} {date}
Authors: {authors} {f' • {journal}' if journal else ''}
{f'
DOI: {doi_esc}
' if doi else ''}
""" html += "
" return html def refresh_pubcrawler_articles(request: gr.Request): """Refresh the PubCrawler article feed for the authenticated admin.""" return get_pubcrawler_articles_html(request) def delete_pubcrawler_term(selected_id, request: gr.Request): """Delete a selected PubCrawler term for an authenticated admin.""" if not _pubcrawler_admin_ok(request): return ' Admin login required. Please log in first.', get_pubcrawler_terms_df(), '', get_pubcrawler_articles_html(request) if not selected_id: return ' Select a term first.', get_pubcrawler_terms_df(), '', get_pubcrawler_articles_html(request) if not supabase: return ' Supabase connection is unavailable.', get_pubcrawler_terms_df(), '', get_pubcrawler_articles_html(request) try: supabase.table('pubcrawler').delete().eq('id', int(selected_id)).execute() return 'PubCrawler term removed.', get_pubcrawler_terms_df(), '', get_pubcrawler_articles_html(request) except Exception as e: return f' Could not remove term: {e}', get_pubcrawler_terms_df(), str(selected_id), get_pubcrawler_articles_html(request) # ── Layout ──────────────────────────────────────────────────────────────────── ALL_SOURCES = ['PubMed', 'Europe PMC', 'CrossRef', 'Semantic Scholar', 'bioRxiv / medRxiv'] with gr.Blocks() as demo: # ── Institute header ────────────────────────────────────────────────────── gr.HTML("""
ICAR Logo
भाकृअनुप-राष्ठ्रीय डेरी अनुसंधान संस्थान
ICAR – National Dairy Research Institute
KARNAL, HARYANA
ReproOmics Hub — Bioinformatics Hub for Animal Reproduction Research
""") gr.HTML("""
RH
ReproOmics Hub
Research workspace for animal reproduction data, literature, and analysis
GEO SRA Literature PubCrawler
""") # ── Paper Alert ── gr.HTML(value=generate_paper_alert_html()) # ── Shared state ── is_authenticated = gr.State(False) selected_accession = gr.State("") hidden_canvas_data = gr.Textbox(visible=False) # ── Hidden textbox for session_hash ── session_hash_state = gr.Textbox(visible=False, value="") # ── JavaScript to set session_hash_state ── gr.HTML(""" """) with gr.Tabs() as app_tabs: # ── TAB 1: Home ──────────────────────────────────────────────────────── with gr.TabItem("Home"): with gr.Column(visible=True) as home_locked_panel: home_view_component = gr.HTML(value=generate_view_html()) gr.HTML(HOME_CONTACT_HTML) with gr.Column(visible=False) as home_editor_panel: gr.HTML("""
Site editor

Homepage Content

Edit the homepage content below. Changes are saved to the local homepage file.

""") home_editor_component = gr.HTML(value=generate_editor_html()) with gr.Row(): save_home_btn = gr.Button("Save Homepage", variant="primary") home_save_status = gr.Markdown("") # ── TAB 2: Query Database Hub ────────────────────────────────────────── with gr.TabItem("Query Database Hub"): with gr.Column(elem_id="qt-search-panel", visible=True) as qt_search_panel: gr.HTML("""
Dataset search

Query Biological Records

Search your GEO and SRA records using animal, tissue, source, and accession filters.

""") with gr.Row(): with gr.Column(scale=1, min_width=300): species_input = gr.Dropdown(choices=["Bos taurus", "Buffalo"], label="Animal Type", value="Bos taurus") tissue_input = gr.Dropdown(choices=["Oocyte", "Blastocyst", "Sperm"], label="Tissue Type", value="Oocyte") tool_input = gr.Radio(choices=["GEO", "SRA"], label="Source", value="SRA") accession_input = gr.Textbox(label="Accession Number", placeholder="Optional: SRX12345, GSM6789...") search_btn = gr.Button("Search Records", variant="primary") with gr.Column(scale=2, min_width=450): gr.HTML("""
How it works
Search and inspect stored records

Results appear in a structured table. After a match is found, the analysis launchpad becomes available below it.

""") with gr.Column(elem_id="qt-results-panel", visible=False) as qt_results_panel: with gr.Row(): new_search_btn = gr.Button("New Search", variant="secondary") with gr.Row(): with gr.Column(scale=1, min_width=240): species_input2 = gr.Dropdown(choices=["Bos taurus", "Buffalo"], label="Animal Type", value="Bos taurus") tissue_input2 = gr.Dropdown(choices=["Oocyte", "Blastocyst", "Sperm"], label="Tissue Type", value="Oocyte") tool_input2 = gr.Radio(choices=["GEO", "SRA"], label="Source", value="SRA") accession_input2 = gr.Textbox(label="Accession Number", placeholder="Optional: SRX12345, GSM6789...") search_btn2 = gr.Button("Search Again", variant="primary") with gr.Column(scale=3, min_width=500): output_table = gr.Dataframe(value=BLANK_TEMPLATE_DF, interactive=False, wrap=False) with gr.Column(visible=False) as query_box_container: gr.HTML("""
Analysis
Available analysis tools
Launch the relevant GEO or SRA workflow for the returned record.
""") query_custom_box = gr.HTML() def do_search(species, tissue, tool, accession): df = search_supabase(species, tissue, tool, accession) tools_html = generate_tools_launchpad_html(dataset_type=tool) return ( gr.Column(visible=False), gr.Column(visible=True), df, gr.Column(visible=True), gr.HTML(value=tools_html), species, tissue, tool, accession ) def do_reset(): return gr.Column(visible=True), gr.Column(visible=False) search_btn.click( fn=do_search, inputs=[species_input, tissue_input, tool_input, accession_input], outputs=[qt_search_panel, qt_results_panel, output_table, query_box_container, query_custom_box, species_input2, tissue_input2, tool_input2, accession_input2] ) search_btn2.click( fn=do_search, inputs=[species_input2, tissue_input2, tool_input2, accession_input2], outputs=[qt_search_panel, qt_results_panel, output_table, query_box_container, query_custom_box, species_input2, tissue_input2, tool_input2, accession_input2] ) new_search_btn.click(fn=do_reset, inputs=[], outputs=[qt_search_panel, qt_results_panel]) # ── TAB 3: Literature Finder ──────────────────────────────────────────── with gr.TabItem("Literature Finder"): gr.HTML("""
Literature discovery

Multi-Database Literature Search

Search the most recent six months across PubMed, Europe PMC, CrossRef, Semantic Scholar, and bioRxiv/medRxiv. Results are deduplicated and sorted by date.

""") with gr.Row(): with gr.Column(scale=1, min_width=290): pm_topic_input = gr.Textbox(label="Search Topic / Keywords", placeholder="e.g. bovine oocyte transcriptomics, blastocyst CRISPR") pm_sources_input = gr.CheckboxGroup(choices=ALL_SOURCES, value=ALL_SOURCES, label="Databases") with gr.Row(): pm_search_btn = gr.Button("Search Literature", variant="primary") gr.HTML("""
Direct lookup
Search by DOI
Look up one DOI across CrossRef, Europe PMC, and PubMed.
""") pm_doi_search_input = gr.Textbox(label="DOI", placeholder="10.1016/j.example.2026.123456") pm_doi_search_btn = gr.Button("Search DOI", variant="secondary") with gr.Column(scale=3, min_width=480): pm_output_html = gr.HTML(value="
Enter a topic or DOI to begin.
") with gr.Column(scale=1, min_width=220, visible=False) as pm_doi_col: with gr.Group(): gr.HTML("""
Admin action
Mark DOI as Read

Store a reviewed DOI in Supabase.

""") pm_doi_input = gr.Textbox(label="DOI", placeholder="10.1093/nar/gkac123") pm_doi_btn = gr.Button("Mark as Read", variant="primary") pm_doi_status = gr.Markdown("") pm_search_btn.click(fn=fetch_multi_db_papers, inputs=[pm_topic_input, pm_sources_input], outputs=pm_output_html) pm_doi_search_btn.click(fn=search_by_doi, inputs=[pm_doi_search_input], outputs=pm_output_html) def manual_mark_doi_only(doi, request: gr.Request): return manual_mark_doi(doi, request) pm_doi_btn.click(fn=manual_mark_doi_only, inputs=[pm_doi_input], outputs=pm_doi_status) # ── TAB 4: Literature Search ──────────────────────────────────────────── with gr.TabItem("Literature Search"): gr.HTML("""
Full archive

Literature Search — All Time

Search by topic, database, and year range. Results are loaded page by page and deduplicated.

""") ls_state = gr.State(_ls_empty_state()) with gr.Row(): with gr.Column(scale=1, min_width=290): ls_topic_input = gr.Textbox(label="Search Topic / Keywords", placeholder="e.g. CRISPR gene editing, bovine embryo transfer") with gr.Row(): with gr.Column(): ls_year_from = gr.Slider(minimum=1900, maximum=datetime.date.today().year, value=1900, step=1, label="From year") with gr.Column(): ls_year_to = gr.Slider(minimum=1900, maximum=datetime.date.today().year, value=datetime.date.today().year, step=1, label="To year") ls_sources_input = gr.CheckboxGroup(choices=ALL_SOURCES, value=ALL_SOURCES, label="Databases") ls_search_btn = gr.Button("Search Archive", variant="primary") gr.HTML("
Direct lookup
Search by DOI

Open a publication directly from its DOI.

") ls_doi_search_input = gr.Textbox(label="DOI", placeholder="10.1016/j.example.2026.123456") ls_doi_search_btn = gr.Button("Search DOI", variant="secondary") with gr.Column(scale=3, min_width=480): ls_nav_html = gr.HTML(value="") ls_output_html = gr.HTML(value="
Enter a topic and click Search to begin.
") with gr.Row(): ls_prev_btn = gr.Button("Previous", variant="secondary") ls_next_btn = gr.Button("Next", variant="primary") with gr.Column(scale=1, min_width=220, visible=False) as ls_doi_col: with gr.Group(): gr.HTML("
Admin action
Mark DOI as Read

Store a reviewed DOI in Supabase.

") ls_doi_input = gr.Textbox(label="DOI", placeholder="10.1093/nar/gkac123") ls_doi_btn = gr.Button("Mark as Read", variant="primary") ls_doi_status = gr.Markdown("") ls_search_btn.click(fn=ls_init_search, inputs=[ls_topic_input, ls_sources_input, ls_year_from, ls_year_to], outputs=[ls_output_html, ls_nav_html, ls_state]) ls_doi_search_btn.click(fn=search_by_doi, inputs=[ls_doi_search_input], outputs=ls_output_html) ls_prev_btn.click(fn=ls_prev_page, inputs=[ls_state], outputs=[ls_output_html, ls_nav_html, ls_state]) ls_next_btn.click(fn=ls_next_page, inputs=[ls_state], outputs=[ls_output_html, ls_nav_html, ls_state]) ls_doi_btn.click(fn=manual_mark_doi_only, inputs=[ls_doi_input], outputs=ls_doi_status) # ── TAB 5: AI Tools Directory ────────────────────────────────────────── with gr.TabItem("AI Tools"): gr.HTML("""

AI / ML / DL Tools

Curated AI, Machine Learning & Deep Learning tools for cattle & buffalo reproduction research. Click any card to open the tool or source paper.

AI / ML / DL Tools for Cattle & Buffalo Reproduction

Click any card to open the source paper or tool.

Oocyte Assessment / Grading
Embryo / Blastocyst Grading
Blasto3Q
Fully automated ANN-based bovine blastocyst grading per IETS standard (Grade 1/2/3); 76.4% accuracy; MATLAB + multiplatform web interface.
iDAScore (Vitrolife)
Fully automated time-lapse analysis; ranks embryos by likelihood of fetal heartbeat at day 2/3/blastocyst stage. Widely cited in bovine ET literature.
EmbryoScope / Primo Vision / Eeva
Time-lapse incubation systems with integrated kinetics and cleavage-symmetry evaluation software; Primo Vision used to record bovine blastulation timing (tSB, tB).
ML Embryo Stage / Transferability Classifier
81.7% agreement with expert embryologists on developmental stage; 95.2% agreement on transferability decision.
Multi-Task DL + Dynamic Programming
2D CNN per time-lapse frame with dynamic-programming post-processing enforcing monotonic cell-count progression for embryo cell-stage classification.
Supervised Contrastive Learning Model
Benchmarked on public Bovine Embryo CS dataset and NYU Mouse Embryo dataset for cell-stage classification using contrastive CNN.
CNN Oocyte / Embryo Scoring (Mana et al.)
CNN-based scoring of 269 oocytes and 269 corresponding embryos from 104 women; preliminary high-quality embryo classification study.
ML Live-Birth Prediction (Miyagi et al.)
Multiple ML algorithms compared (LR, Naïve Bayes, KNN, RF, Neural Network, SVM) for predicting probability of live birth from blastocyst-stage images.
AI Pregnancy-Probability Platform (Khosravi et al.)
AI platform trained on embryologist-scored blastocyst images; predicted pregnancy chance 13.8–66.3% depending on blastocyst/patient factors.
Sperm / Semen Analysis (CASA + AI)
BGM — Open-Access CASA Software
Open-access CASA software validated against commercial Hamilton-Thorne (HTM) system for cattle and buffalo sperm motility/kinematics. Most directly relevant tool for cattle + buffalo work.
BSPMCsvm3casa
SVM-based bull sperm motility classifier using three CASA kinematic parameters (VCL, VSL, LIN); outperforms prior static-threshold classification methods.
Tracking-Grid + Mean-Angle Motion Tracker
Predicts position of sperm with failed detection using mean motion angle + Tracking-Grid; 5% fewer ID-switches and +15.6 MOTAL points vs Deep SORT baseline.
Hamilton-Thorne CASA (IVOS / HTM Series)
Industry-standard CASA combining camera, microscope, and pixel-recognition software for motility/velocity/morphology evaluation across cattle and buffalo.
DL Bovine Sperm Morphology Classifier (IFC)
Deep learning morphology analysis trained on ~1.8 million imaging flow cytometry images across 6 bulls, fresh and frozen-thawed semen.
AI Bull Sperm Morphology Evaluation System
AI algorithm benchmarked against manual morphology assessment in bull semen analysis; confirmed potential applicability for automated sperm morphology evaluation.
Animal Identification (Cattle / Buffalo)
""") # ── TAB 6: PubCrawler ──────────────────────────────────────────────────── with gr.TabItem("PubCrawler", visible=False) as pubcrawler_tab: gr.HTML("""
Admin research monitoring

PubCrawler

Maintain a list of watch terms in the pubcrawler Supabase table and search the literature for publications from the past 30 days.

""") with gr.Row(elem_classes=["crawler-grid"]): with gr.Column(scale=2, min_width=300): gr.HTML("
Watch list
Add a search term

Terms are saved to Supabase and used for the next crawl.

") pubcrawler_term_input = gr.Textbox(label="Term", placeholder="e.g. bovine oocyte CRISPR") with gr.Row(): pubcrawler_save_btn = gr.Button("Save Term", variant="primary") pubcrawler_refresh_btn = gr.Button("Reload", variant="secondary") pubcrawler_status = gr.Markdown("") with gr.Column(scale=3, min_width=420): gr.HTML("
Stored terms
PubCrawler Watch List

Select a row to remove that term.

") pubcrawler_table = gr.Dataframe(value=get_pubcrawler_terms_df(), headers=['ID', 'Term'], datatype=['number', 'str'], interactive=False, wrap=True, max_height=240) selected_pubcrawler_id = gr.State("") with gr.Row(): pubcrawler_delete_btn = gr.Button("Remove Selected Term", variant="stop") pubcrawler_selection_status = gr.Markdown("") gr.HTML("
Literature feed
Articles Matching Saved Terms

Every saved term is searched across the literature sources for the past 30 days. Duplicates are merged.

") with gr.Row(): pubcrawler_articles_refresh_btn = gr.Button("Refresh Articles", variant="secondary") pubcrawler_articles_status = gr.Markdown("") pubcrawler_articles_html = gr.HTML(value="
Save one or more terms, then refresh the article feed.
") # ── TAB 7: Administrative Portal ─────────────────────────────────────── with gr.TabItem("Administrative Portal"): with gr.Column(visible=True, elem_classes=["admin-login"]) as login_panel: gr.HTML("""
Restricted access

Administrative Portal

Sign in with a junior or senior administrator account. Junior access is limited to DOI review; senior access includes database management, PubCrawler, reviewed-article management, and homepage editing.

""") user_box = gr.Textbox(label="Username", placeholder="Enter username") pass_box = gr.Textbox(label="Password", type="password", placeholder="Enter password") login_btn = gr.Button("Sign In", variant="primary") with gr.Column(visible=False, elem_classes=["admin-junior-panel"]) as junior_access_panel: gr.HTML("""
Junior administration

DOI review access

Your account can mark literature DOIs as read. The database administration console, homepage editor, PubCrawler, and reviewed-article deletion require senior administrator access.

""") with gr.Column(visible=False) as upload_panel: gr.HTML("
Administration

Admin Console

Manage dataset records, reviewed DOIs, and site content from one workspace.

") with gr.Row(): with gr.Column(scale=1, min_width=290): with gr.Group(elem_classes=["admin-section"]): gr.HTML("
Data ingestion
Upload Records

Upload a CSV or Excel dataset and assign its biological metadata.

") file_input = gr.File(label="CSV or Excel", file_types=[".csv", ".xlsx"]) upload_species = gr.Dropdown(choices=["Bos taurus", "Buffalo"], label="Animal Type", value="Bos taurus") upload_tissue = gr.Dropdown(choices=["Oocyte", "Blastocyst", "Sperm"], label="Tissue Type", value="Oocyte") upload_dataset = gr.Radio(choices=["GEO", "SRA"], label="Dataset Type", value="SRA") submit_btn = gr.Button("Upload Records", variant="primary") with gr.Group(elem_classes=["admin-section"]): gr.HTML("
Record removal
Delete Selected Record

Select a row from the database table before deleting.

") delete_btn = gr.Button("Delete Selected Row", variant="stop") status_output = gr.Textbox(label="Console Log", placeholder="Awaiting action...") with gr.Column(scale=2, min_width=520): with gr.Group(elem_classes=["admin-section"]): gr.HTML("
Database browser
Filter and Browse Records
") admin_species = gr.Dropdown(choices=["Bos taurus", "Buffalo"], label="Animal Type", value="Bos taurus") admin_tissue = gr.Dropdown(choices=["Oocyte", "Blastocyst", "Sperm"], label="Tissue Type", value="Oocyte") admin_tool = gr.Radio(choices=["GEO", "SRA"], label="Source", value="SRA") with gr.Row(): admin_search_btn = gr.Button("Run Filter", variant="secondary") admin_reset_btn = gr.Button("Reload All", variant="secondary") admin_table_view = gr.Dataframe(value=BLANK_TEMPLATE_DF, interactive=False, wrap=False) with gr.Group(elem_classes=["admin-section"]): gr.HTML("
Review management
Mark DOI as Read

Store a reviewed DOI in Supabase.

") with gr.Row(): manual_doi_input = gr.Textbox(label="DOI", placeholder="10.1093/nar/gkac123") manual_doi_btn = gr.Button("Mark as Read", variant="primary") manual_doi_status = gr.Markdown("") with gr.Group(elem_classes=["admin-section"]): gr.HTML("
Review history
Marked Articles

Select an article, then remove it to clear the reviewed status.

") reviewed_table_view = gr.Dataframe(value=get_reviewed_table_df(), headers=['DOI', 'Reviewed At', 'Link'], datatype=['str', 'str', 'str'], interactive=False, wrap=True, max_height=300) selected_reviewed_doi = gr.State("") with gr.Row(): remove_reviewed_btn = gr.Button("Remove Selected Article", variant="stop") reviewed_status = gr.Markdown("") login_btn.click( fn=check_upload_credentials_generator, inputs=[user_box, pass_box], outputs=[login_panel, upload_panel, status_output, admin_table_view, home_locked_panel, home_editor_panel, home_editor_component, is_authenticated, pm_doi_col, ls_doi_col, reviewed_table_view, pubcrawler_table, pubcrawler_articles_html, pubcrawler_tab, junior_access_panel] ) pass_box.submit( fn=check_upload_credentials_generator, inputs=[user_box, pass_box], outputs=[login_panel, upload_panel, status_output, admin_table_view, home_locked_panel, home_editor_panel, home_editor_component, is_authenticated, pm_doi_col, ls_doi_col, reviewed_table_view, pubcrawler_table, pubcrawler_articles_html, pubcrawler_tab, junior_access_panel] ) pubcrawler_save_btn.click( fn=save_pubcrawler_term, inputs=[pubcrawler_term_input], outputs=[pubcrawler_status, pubcrawler_table, pubcrawler_term_input, pubcrawler_articles_html] ) pubcrawler_refresh_btn.click( fn=lambda request: (get_pubcrawler_terms_df(), get_pubcrawler_articles_html(request)), inputs=[], outputs=[pubcrawler_table, pubcrawler_articles_html] ) pubcrawler_term_input.submit( fn=save_pubcrawler_term, inputs=[pubcrawler_term_input], outputs=[pubcrawler_status, pubcrawler_table, pubcrawler_term_input, pubcrawler_articles_html] ) pubcrawler_table.select( fn=pubcrawler_row_selection, inputs=[pubcrawler_table], outputs=[selected_pubcrawler_id, pubcrawler_selection_status] ) pubcrawler_delete_btn.click( fn=delete_pubcrawler_term, inputs=[selected_pubcrawler_id], outputs=[pubcrawler_status, pubcrawler_table, selected_pubcrawler_id, pubcrawler_articles_html] ) pubcrawler_articles_refresh_btn.click( fn=refresh_pubcrawler_articles, inputs=[], outputs=[pubcrawler_articles_html] ) save_home_btn.click( fn=update_homepage_string, inputs=[hidden_canvas_data], outputs=[home_save_status, home_view_component], js="() => { const ed = document.getElementById('homepage-canvas-editor'); return [ed ? ed.innerHTML : '']; }" ) admin_table_view.select( fn=handle_row_selection, inputs=[admin_table_view], outputs=[selected_accession, status_output] ) admin_search_btn.click(fn=search_supabase, inputs=[admin_species, admin_tissue, admin_tool], outputs=admin_table_view) admin_reset_btn.click(fn=load_entire_table, inputs=[], outputs=admin_table_view) def post_upload_refresh(file, species, tissue, dataset, request: gr.Request): log_msg = upload_data_to_supabase(file, species, tissue, dataset, request) return log_msg, load_entire_table() submit_btn.click(fn=post_upload_refresh, inputs=[file_input, upload_species, upload_tissue, upload_dataset], outputs=[status_output, admin_table_view]) delete_btn.click(fn=delete_record_from_supabase, inputs=[selected_accession, admin_table_view], outputs=[status_output, admin_table_view, selected_accession]) manual_doi_btn.click(fn=manual_mark_doi_and_refresh, inputs=[manual_doi_input], outputs=[manual_doi_status, reviewed_table_view]) reviewed_table_view.select(fn=reviewed_row_selection, inputs=[reviewed_table_view], outputs=[selected_reviewed_doi, reviewed_status]) remove_reviewed_btn.click(fn=remove_selected_reviewed_doi, inputs=[selected_reviewed_doi], outputs=[reviewed_status, reviewed_table_view, selected_reviewed_doi]) # ── Public API ────────────────────────────────────────────────────────── with gr.Row(visible=False): _api_in_doi = gr.Textbox(value="", elem_id="api-doi") _api_in_action = gr.Textbox(value="", elem_id="api-action") _api_out = gr.Textbox(value="", elem_id="api-out") _api_btn = gr.Button("api", elem_id="api-btn") _api_btn.click(fn=mark_article_api, inputs=[_api_in_doi, _api_in_action], outputs=[_api_out], api_name="mark_article_api") # Add parameters directly to launch() demo.launch(css=css, theme=light_theme)