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 from supabase import create_client, Client url: str = os.environ.get("SUPABASE_URL") key: str = os.environ.get("SUPABASE_KEY") AUTH_USER = os.environ.get("UPLOAD_USER") AUTH_PASS = os.environ.get("UPLOAD_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" # ── 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."} ] # ── 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 "
" 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(topic, since_date_str, max_results=200): """Fetch from NCBI PubMed via Entrez eUtils.""" results = [] try: term_raw = f"{topic}[Title/Abstract] AND (\"{since_date_str}\"[Date - Publication] : \"3000\"[Date - Publication])" term_enc = urllib.parse.quote(term_raw) search_url = ( f"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi" f"?db=pubmed&retmax={max_results}&retmode=json&sort=date&term={term_enc}" ) 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(topic, since_date_str, max_results=200): """Fetch from Europe PMC REST API.""" results = [] try: # Europe PMC date format: YYYY-MM-DD since_epmc = since_date_str.replace('/', '-') query = urllib.parse.quote(f"{topic} FIRST_PDATE:[{since_epmc} TO 3000-12-31]") url = ( f"https://www.ebi.ac.uk/europepmc/webservices/rest/search" f"?query={query}&resultType=core&pageSize={max_results}&format=json&sort=P_PDATE_D%20desc" ) 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(topic, since_date_str, max_results=100): """Fetch from CrossRef Works API.""" results = [] try: # CrossRef date format: YYYY-MM-DD since_cr = since_date_str.replace('/', '-') query_enc = urllib.parse.quote(topic) 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: # Skip non-journal items (books, components, etc.) 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(topic, since_date_str, max_results=100): """Fetch from Semantic Scholar Graph API.""" results = [] try: since_ss = since_date_str.replace('/', '-') query_enc = urllib.parse.quote(topic) 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 _fetch_biorxiv(topic, since_date_str, max_results=100): """Fetch from bioRxiv and medRxiv preprint servers.""" results = [] try: today_str = datetime.date.today().strftime("%Y-%m-%d") since_bxr = since_date_str.replace('/', '-') 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') # Simple keyword check since API returns all preprints in date range if topic.lower().split()[0] not in title.lower() and \ not any(kw.lower() in title.lower() for kw in topic.lower().split()[:3]): abstract = item.get('abstract', '').lower() if not any(kw.lower() in abstract for kw in topic.lower().split()[:3]): 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 # Try partial date 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(topic, 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 topic or not topic.strip(): return "
⚠️ Please enter a valid search topic.
" all_results = [] source_logs = [] source_map = { 'PubMed': lambda: _fetch_pubmed(topic, since_date), 'Europe PMC': lambda: _fetch_europe_pmc(topic, since_date_dash), 'CrossRef': lambda: _fetch_crossref(topic, since_date_dash), 'Semantic Scholar': lambda: _fetch_semantic_scholar(topic, since_date_dash), 'bioRxiv / medRxiv': lambda: _fetch_biorxiv(topic, 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 "{topic}" {window_label}.
{log_html}
""" unique_results = _deduplicate(all_results) sorted_results = _sort_results(unique_results) # ── Count by source ── 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) html = f"""

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

{badge_row}

{log_html}

""" 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 = r.get('doi', '') 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} {doi_link}
""" html += "
" return html def fetch_multi_db_papers(topic, selected_sources): """ Entry-point for the recent (past 6 months) multi-database literature search. selected_sources: list of source names to query. Returns rendered HTML string. """ 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(topic, selected_sources, since_date, since_date_dash, "past 6 months") # ── Paged All-Time Literature Search ───────────────────────────────────────── LS_FETCH_SIZE = 50 # results fetched per source per API call LS_PAGE_SIZE = 20 # results displayed per page LS_YEAR_MIN = 1900 LS_YEAR_MAX = datetime.date.today().year def _fetch_pubmed_paged(topic, start_year, end_year, offset): results, exhausted = [], False try: term_raw = ( f"{topic}[Title/Abstract] AND " f"(\"{start_year}/01/01\"[Date - Publication] : \"{end_year}/12/31\"[Date - Publication])" ) term_enc = urllib.parse.quote(term_raw) 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={term_enc}" ) 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(topic, start_year, end_year, offset): results, exhausted = [], False try: page_num = offset // LS_FETCH_SIZE query = urllib.parse.quote( f"{topic} FIRST_PDATE:[{start_year}-01-01 TO {end_year}-12-31]" ) url = ( f"https://www.ebi.ac.uk/europepmc/webservices/rest/search" f"?query={query}&resultType=core&pageSize={LS_FETCH_SIZE}" f"&page={page_num}&format=json&sort=P_PDATE_D%20desc" ) 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(topic, start_year, end_year, offset): results, exhausted = [], False try: query_enc = urllib.parse.quote(topic) 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(topic, start_year, end_year, offset): results, exhausted = [], False try: query_enc = urllib.parse.quote(topic) 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(topic, start_year, end_year, cursor): results, exhausted = [], False try: start_dt = f"{start_year}-01-01" end_dt = f"{end_year}-12-31" kws = [w.lower() for w in topic.lower().split()[:4] if len(w) > 3] 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 # The API returns up to 100 items; if fewer than 100 returned, we're at the end if len(collection) < 100: exhausted = True for item in collection: title = item.get('title', 'Untitled') abstract = item.get('abstract', '').lower() if not any(kw in title.lower() or kw in abstract for kw in kws): 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) # Deduplicate new results against pool 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): """Render HTML cards for the current page.""" 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.
") 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 = r.get('doi', '') 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} {doi_link}
""" return html def _ls_render_nav(state): """Render the pagination bar HTML.""" 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 display: always show up to 10, then "..." 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(topic, sources, start_year, end_year): """Kick off a new search; returns (results_html, nav_html, new_state).""" 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 topic or not topic.strip(): err = ("
" "⚠️ Please enter a valid search topic.
") return err, "", _ls_empty_state() state = { 'topic': topic.strip(), 'sources': list(sources), 'start_year': start_year, 'end_year': end_year, 'page': 1, 'pool': [], 'offsets': {}, 'exhausted': [], 'seen_dois': [], 'seen_titles': [], } # Fetch first two batches to ensure a full first page even with heavy deduplication _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 “" + topic + "” 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): """Navigate to the previous page.""" 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): """Navigate to next page, fetching more results from APIs if needed.""" if not state: return "", "", state state = dict(state) next_page = state['page'] + 1 needed = next_page * LS_PAGE_SIZE # Keep fetching until we have enough or all sources are exhausted max_batches = 6 # safety limit per click 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 # Advance only if results exist for next page 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."]}) 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": [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(): html = """

🌐 Biological Data Located. Launch an Analytical Suite below:

""" for tool in GEO_TOOLS_DATA: 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() return df_result, gr.Column(visible=True), gr.HTML(value=f"
{tools_launchpad}
") def check_upload_credentials_generator(username, password): if not AUTH_USER or not AUTH_PASS: yield (gr.Column(visible=True), gr.Column(visible=False), "Configuration Error: Secrets missing.", BLANK_TEMPLATE_DF, gr.Column(visible=True), gr.Column(visible=False), gr.HTML(value=generate_editor_html()), False) return if username == AUTH_USER and password == AUTH_PASS: yield (gr.Column(visible=False), gr.Column(visible=True), "Authenticated. Loading data...", BLANK_TEMPLATE_DF, gr.Column(visible=False), gr.Column(visible=True), gr.HTML(value=generate_editor_html()), True) entire_table_df = load_entire_table() yield (gr.Column(visible=False), gr.Column(visible=True), "Admin Dashboard Ready.", entire_table_df, gr.Column(visible=False), gr.Column(visible=True), gr.HTML(value=generate_editor_html()), True) else: 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) def upload_data_to_supabase(file_obj, species, tissue, dataset_type): 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): 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): log_msg = save_homepage_content(html_content) return log_msg, generate_view_html() # ── Theme ───────────────────────────────────────────────────────────────────── light_theme = gr.themes.Base( primary_hue=gr.themes.colors.blue, neutral_hue=gr.themes.colors.slate, font=gr.themes.GoogleFont("Inter"), ).set( body_background_fill="#f8fafc", body_background_fill_dark="#f8fafc", body_text_color="#1e293b", body_text_color_dark="#1e293b", block_background_fill="#ffffff", block_background_fill_dark="#ffffff", block_border_color="#e2e8f0", block_border_color_dark="#e2e8f0", block_label_text_color="#475569", block_label_text_color_dark="#475569", block_title_text_color="#0f172a", block_title_text_color_dark="#0f172a", input_background_fill="#ffffff", input_background_fill_dark="#ffffff", input_border_color="#cbd5e1", input_border_color_dark="#cbd5e1", input_placeholder_color="#94a3b8", input_placeholder_color_dark="#94a3b8", checkbox_background_color="#ffffff", checkbox_background_color_dark="#ffffff", button_primary_background_fill="#2563eb", button_primary_background_fill_dark="#2563eb", button_primary_text_color="#ffffff", button_primary_text_color_dark="#ffffff", button_secondary_background_fill="#ffffff", button_secondary_background_fill_dark="#ffffff", button_secondary_text_color="#475569", button_secondary_text_color_dark="#475569", button_secondary_border_color="#cbd5e1", button_secondary_border_color_dark="#cbd5e1", 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="#e2e8f0", table_border_color_dark="#e2e8f0", ) css = """ *, *::before, *::after { color-scheme: light !important; } .gradio-container, .gradio-container.dark, body, html { background-color: #f8fafc !important; color: #1e293b !important; } .gradio-container h1, .gradio-container h2, .gradio-container h3, .gradio-container h4 { color: #0f172a !important; font-weight: 700 !important; } .gradio-container label, .gradio-container .block label span, .gradio-container p, .gradio-container span { color: #334155 !important; } .gradio-container input, .gradio-container textarea, .gradio-container select { background-color: #ffffff !important; color: #0f172a !important; border-color: #cbd5e1 !important; } .gradio-container .options, .gradio-container ul.options, .gradio-container [class*="options"] { background-color: #ffffff !important; border: 1px solid #e2e8f0 !important; box-shadow: 0 4px 12px rgba(0,0,0,0.08) !important; } .gradio-container .options li, .gradio-container [class*="options"] li { color: #334155 !important; background-color: #ffffff !important; } .gradio-container .options li:hover, .gradio-container [class*="options"] li:hover { background-color: #f1f5f9 !important; color: #1e293b !important; } .gradio-container .wrap label { background-color: #ffffff !important; border: 1px solid #e2e8f0 !important; color: #475569 !important; } .gradio-container .wrap label.selected, .gradio-container .wrap input[type="radio"]:checked + span { border-color: #2563eb !important; color: #2563eb !important; background-color: #eff6ff !important; font-weight: 600 !important; } /* Only give the white-box treatment to actual leaf component blocks, not to gr.Column / gr.Row layout containers (.gap) */ .gradio-container .block { background-color: #ffffff !important; border-color: #e2e8f0 !important; } /* Layout containers: transparent, no border, no shadow */ .gradio-container .gap, .gradio-container .gap > .block > .gap, .gradio-container fieldset, .gradio-container .form { background-color: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; } .gradio-container table { background-color: #ffffff !important; border-color: #e2e8f0 !important; } .gradio-container table th { background-color: #f8fafc !important; color: #475569 !important; border-bottom: 1px solid #e2e8f0 !important; font-weight: 600 !important; } .gradio-container table td { background-color: #ffffff !important; color: #334155 !important; border-bottom: 1px solid #f1f5f9 !important; } .gradio-container table tr:hover td, .gradio-container table tr.hover td, .gradio-container table tr[class*="selected"] td, .gradio-container table tr[class*="hover"] td, .gradio-container tbody tr:hover td, .gradio-container tbody tr:hover, .gradio-container [class*="row"]:hover, .gradio-container [class*="row"]:hover td, .gradio-container [class*="table"] tr:hover, .gradio-container [class*="table"] tr:hover td { background-color: #dbeafe !important; color: #1e40af !important; } .gradio-container table tr:nth-child(odd) td { background-color: #f8fafc !important; } .gradio-container .tabs .tab-nav, div[class*="tab-nav"], .tab-nav { border-bottom: 1px solid #e2e8f0 !important; background-color: #f8fafc !important; } .gradio-container .tabs button, .gradio-container [class*="tab-nav"] button, .gradio-container [role="tab"], button[class*="tab"], div[class*="tab-nav"] button { color: #64748b !important; background-color: transparent !important; background: transparent !important; border: none !important; font-weight: 500 !important; padding: 10px 18px !important; transition: background-color 0.15s, color 0.15s !important; } .gradio-container .tabs button:hover, .gradio-container [class*="tab-nav"] button:hover, .gradio-container [role="tab"]:hover, button[class*="tab"]:hover, div[class*="tab-nav"] button:hover { background-color: #f1f5f9 !important; background: #f1f5f9 !important; color: #1e293b !important; } .gradio-container .tabs button.selected, .gradio-container [class*="tab-nav"] button.selected, .gradio-container [role="tab"][aria-selected="true"], div[class*="tab-nav"] button.selected { color: #2563eb !important; background-color: #ffffff !important; background: #ffffff !important; border-bottom: 2px solid #2563eb !important; font-weight: 700 !important; } .gradio-container .tabs button.selected:hover, .gradio-container [role="tab"][aria-selected="true"]:hover { background-color: #ffffff !important; background: #ffffff !important; color: #2563eb !important; } .gradio-container button.primary, .gradio-container button[variant="primary"] { background: #2563eb !important; color: #ffffff !important; border: none !important; font-weight: 600 !important; border-radius: 6px !important; } .gradio-container button.primary:hover { background: #1d4ed8 !important; } .gradio-container button.stop { background: #dc2626 !important; color: #ffffff !important; border: none !important; font-weight: 600 !important; border-radius: 6px !important; } .gradio-container button.secondary { background: #ffffff !important; color: #475569 !important; border: 1px solid #cbd5e1 !important; border-radius: 6px !important; } .gradio-container .upload-container, .gradio-container [class*="upload"] { background-color: #f8fafc !important; border: 1px dashed #cbd5e1 !important; color: #475569 !important; } .gradio-container .textbox textarea { background-color: #f8fafc !important; color: #1e293b !important; border-color: #e2e8f0 !important; } /* Prevent layout containers from flashing a background on hover */ .gradio-container .gap:hover, .gradio-container .gap > .block:hover { background-color: transparent !important; } footer { display: none !important; } #homepage-canvas-editor { color: #1e293b !important; background-color: #f8fafc !important; } #qt-search-panel > div { max-width: 480px !important; margin: 0 auto !important; } #qt-form-inner { max-width: 480px !important; } #qt-search-panel { animation: qtFadeIn 0.35s ease both; } #qt-results-panel { animation: qtSlideIn 0.4s cubic-bezier(0.4,0,0.2,1) both; } @keyframes qtFadeIn { from { opacity:0; transform:scale(0.98); } to { opacity:1; transform:scale(1); } } @keyframes qtSlideIn { from { opacity:0; transform:translateX(40px); } to { opacity:1; transform:translateX(0); } } #qt-search-panel > div > div { max-width: 440px !important; margin: 0 auto !important; } .panel-card { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } """ # ── Layout ──────────────────────────────────────────────────────────────────── ALL_SOURCES = ['PubMed', 'Europe PMC', 'CrossRef', 'Semantic Scholar', 'bioRxiv / medRxiv'] with gr.Blocks() as demo: # Removed css and theme parameters from here (moved to launch) # ── Institute header ── gr.HTML("""
ICAR Logo
भाकृअनुप-राष्ठ्रीय डेरी अनुसंधान संस्थान
ICAR – National Dairy Research Institute
KARNAL, HARYANA
Bioinformatics Data Hub & Analysis Launchpad
NDRI Logo
""") # ── 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) with gr.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()) with gr.Column(visible=False) as home_editor_panel: gr.Markdown("## Homepage Canvas Editor") home_editor_component = gr.HTML() with gr.Row(): save_home_btn = gr.Button("💾 Save Homepage", variant="primary", scale=2) home_save_status = gr.Markdown("*Click Save to write changes.*") # ── 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("""
🔬

Query Biological Records

Select filters and click Search to retrieve data.

""") with gr.Column(scale=1, min_width=300, elem_id="qt-form-inner"): 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 Filter", value="SRA") accession_input = gr.Textbox(label="Accession Number (optional)", placeholder="e.g. SRX12345, GSM6789...") search_btn = gr.Button("🔍 Search Records", variant="primary") 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", scale=1) gr.HTML("
", elem_id="qt-spacer") with gr.Row(): with gr.Column(scale=1, min_width=220): 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 Filter", value="SRA") accession_input2 = gr.Textbox(label="Accession Number (optional)", placeholder="e.g. SRX12345, GSM6789...") search_btn2 = gr.Button("🔍 Search Again", variant="primary") with gr.Column(scale=4): output_table = gr.Dataframe(value=BLANK_TEMPLATE_DF, interactive=False, wrap=False) with gr.Column(visible=False) as query_box_container: gr.Markdown("### Analytical Tools Launcher") 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() 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: PubMed Literature Finder (past 6 months) ──────────────────── with gr.TabItem("Literature Finder"): gr.HTML("""
📚

Multi-Database Literature Search (Past 6 Months)

Search across PubMed, Europe PMC, CrossRef, Semantic Scholar, and bioRxiv/medRxiv simultaneously. Results are deduplicated and sorted by date.

""") with gr.Row(): with gr.Column(scale=1, min_width=300): pm_topic_input = gr.Textbox( label="Search Topic / Keywords", placeholder="e.g. bovine oocyte transcriptomics, blastocyst CRISPR, sperm RNA-seq..." ) pm_sources_input = gr.CheckboxGroup( choices=ALL_SOURCES, value=ALL_SOURCES, label="Databases to Search", ) pm_search_btn = gr.Button("🔍 Search All Databases", variant="primary") # ── Source legend ── gr.HTML("""

DATABASE LEGEND

PubMed — NCBI indexed biomedical literature Europe PMC — EBI life-science literature + open access CrossRef — DOI registry; broad journal coverage Semantic Scholar — AI-enriched citation graph bioRxiv / medRxiv — Biology & medicine preprints
""") with gr.Column(scale=3): pm_output_html = gr.HTML( value="
Select databases and enter a topic to begin searching…
" ) pm_search_btn.click( fn=fetch_multi_db_papers, inputs=[pm_topic_input, pm_sources_input], outputs=pm_output_html ) # ── TAB 4: Literature Search (Paged, Year-Range, All-Time) ──────────── with gr.TabItem("Literature Search"): gr.HTML("""
🌐

Literature Search

Full cross-database search with year-range filtering and page-by-page loading. Results are deduplicated and sorted newest-first.

""") ls_state = gr.State(_ls_empty_state()) with gr.Row(): # ── Left sidebar ────────────────────────────────────────── with gr.Column(scale=1, min_width=310): ls_topic_input = gr.Textbox( label="Search Topic / Keywords", placeholder="e.g. CRISPR gene editing, bovine embryo transfer…" ) # ── Native year-range sliders (reliable Gradio state) ── ls_year_from = gr.Slider( minimum=1900, maximum=2026, value=1900, step=1, label="From year" ) ls_year_to = gr.Slider( minimum=1900, maximum=2026, value=2026, step=1, label="To year" ) ls_sources_input = gr.CheckboxGroup( choices=ALL_SOURCES, value=ALL_SOURCES, label="Databases to Search", ) ls_search_btn = gr.Button("🔍 Search", variant="primary") gr.HTML("""

DATABASE LEGEND

PubMed — NCBI biomedical literature Europe PMC — EBI life-science + open access CrossRef — DOI registry; broad coverage Semantic Scholar — AI-enriched citation graph bioRxiv / medRxiv — Preprints
""") # ── Results column ──────────────────────────────────────── with gr.Column(scale=3): 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("← Prev", variant="secondary", size="sm") ls_next_btn = gr.Button("Next →", variant="primary", size="sm") # ── Event wiring ────────────────────────────────────────────── 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_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], ) # ── TAB 5: AI Tools Directory ────────────────────────────────────────── with gr.TabItem("AI Tools"): gr.HTML("""
🤖

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: Administrative Portal ─────────────────────────────────────── with gr.TabItem("Administrative Portal"): with gr.Column(visible=True) as login_panel: gr.Markdown("### Admin Authentication Required") user_box = gr.Textbox(label="Username", placeholder="Enter username...") pass_box = gr.Textbox(label="Password", type="password", placeholder="Enter password...") login_btn = gr.Button("Unlock Admin Console", variant="primary") with gr.Column(visible=False) as upload_panel: gr.Markdown("## Admin Console") with gr.Row(): with gr.Column(scale=2): gr.Markdown("### Upload Records") file_input = gr.File(label="Upload 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("Push Records", variant="primary") gr.Markdown("---") gr.Markdown("### Delete Record") delete_btn = gr.Button("Delete Selected Row", variant="stop") status_output = gr.Textbox(label="Console Log", placeholder="Awaiting actions...") with gr.Column(scale=3): gr.Markdown("### Filter & 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") gr.Markdown("### Database Table (All Records)") admin_table_view = gr.Dataframe(value=BLANK_TEMPLATE_DF, interactive=False, wrap=False) 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] ) 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] ) 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): log_msg = upload_data_to_supabase(file, species, tissue, dataset) 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] ) # Add parameters directly to launch() to resolve the Gradio 6.0 warning demo.launch(css=css, theme=light_theme)