""" PaperSearchPro - FULL Professional Version Hierarchical Keyword Search + Export + Responsive UI + BibTeX Support + Author Filter + Specific Paper Search """ import requests import pandas as pd import time from datetime import datetime import gradio as gr # Make sure this import is present import os # ============================================ # CLEAN RESPONSIVE CSS - UPDATED WITH DARKER GRAY BACKGROUND # ============================================ custom_css = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); :root { --primary: #2563eb; --secondary: #7c3aed; --border: #d1d5db; --bg-soft: #e5e7eb !important; --text-main: #111827; } /* ================================================= */ /* GENERAL LAYOUT */ /* ================================================= */ body, .gradio-container { font-family: 'Inter', sans-serif !important; background-color: #e5e7eb !important; color: var(--text-main) !important; } .gradio-container { max-width: 1600px !important; margin: auto !important; } .white-card { background: #ffffff !important; border: 1px solid var(--border) !important; border-radius: 12px !important; padding: 2rem !important; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important; } h1 span { background: linear-gradient(135deg, var(--primary), var(--secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .subtitle-text { color: #2563eb !important; font-weight: 500 !important; } /* ================================================= */ /* METRICS */ /* ================================================= */ .metric-value { font-size: 2rem !important; font-weight: 700 !important; color: var(--primary) !important; } /* ================================================= */ /* BUTTONS */ /* ================================================= */ button, .gr-button { color: #ffffff !important; font-weight: 600 !important; } button.variant-primary { background-color: var(--primary) !important; } button.variant-secondary { background-color: #6b7280 !important; } /* ================================================= */ /* TABS */ /* ================================================= */ .tab-nav { background-color: #f3f4f6 !important; border-radius: 8px !important; padding: 4px !important; margin-bottom: 20px !important; } .tab-nav button { border-radius: 6px !important; padding: 8px 16px !important; font-weight: 500 !important; color: #4b5563 !important; } .tab-nav button.selected { background-color: var(--primary) !important; color: white !important; } /* ================================================= */ /* SLIDER COMMENT */ /* ================================================= */ .slider-comment { color: #ffffff !important; font-size: 0.9rem !important; margin-top: 8px !important; margin-bottom: 8px !important; font-style: italic !important; background: #1f2937 !important; padding: 8px 12px !important; border-radius: 6px !important; border-left: 4px solid var(--primary) !important; font-weight: 500 !important; } /* ================================================= */ /* DROPDOWN BUTTON (Closed State) */ /* ================================================= */ .gr-dropdown, .gr-dropdown select, select { background-color: #374151 !important; color: #ffffff !important; border: 1px solid #4b5563 !important; border-radius: 6px !important; } /* ========================================= */ /* FORCE BUTTON TEXT TO ALWAYS BE WHITE */ /* ========================================= */ /* All buttons */ button, .gr-button { color: #ffffff !important; /* TEXT WHITE */ } /* Primary button */ button.variant-primary { background-color: #2563eb !important; /* keep blue */ color: #ffffff !important; /* TEXT WHITE */ } /* Secondary button */ button.variant-secondary { background-color: #6b7280 !important; /* keep gray */ color: #ffffff !important; /* TEXT WHITE */ } /* Keep text white even on hover */ button:hover { color: #ffffff !important; } /* ================================================= */ /* METRIC LABEL BLACK BACKGROUND */ /* ================================================= */ .metric-label { display: inline-block !important; background-color: #111827 !important; /* Black */ color: #ffffff !important; /* White text */ padding: 6px 12px !important; border-radius: 6px !important; font-weight: 600 !important; } /* ================================================= */ /* METRIC VALUE BLACK BACKGROUND FIX */ /* ================================================= */ .metric-value { display: inline-block !important; background-color: #111827 !important; /* Black */ color: #ffffff !important; /* White text */ padding: 6px 12px !important; border-radius: 6px !important; font-weight: 700 !important; } /* Remove default white background from strong */ .metric-value strong { background: transparent !important; color: #ffffff !important; } """ # ============================================ # SEARCH ENGINE # ============================================ class PaperSearchPro: def __init__(self): self.base_url = "https://api.openalex.org/works" self.per_page = 200 def search_papers(self, query, keywords, authors, max_papers, year_from, year_to, open_access, sort_by, sort_order): filters = [] if year_from: filters.append(f"publication_year:>{int(year_from)-1}") if year_to: filters.append(f"publication_year:<{int(year_to)+1}") if open_access: filters.append("open_access.is_oa:true") # Add author filter if provided if authors and authors.strip(): # Clean and format author names for filtering author_list = [a.strip() for a in authors.split(",") if a.strip()] if author_list: # OpenAlex uses author.search filter author_filter = "|".join(author_list) filters.append(f"author.search:{author_filter}") filter_str = ",".join(filters) if filters else None # Map sort options to API parameters sort_field_map = { "Relevance": "relevance_score", "Year": "publication_date", "Citations": "cited_by_count" } sort_field = sort_field_map.get(sort_by, "relevance_score") sort_direction = "desc" if sort_order == "Descending" else "asc" sort_param = f"{sort_field}:{sort_direction}" all_papers = [] page = 1 while len(all_papers) < max_papers: params = { "search": query, "per-page": self.per_page, "page": page, "sort": sort_param } if filter_str: params["filter"] = filter_str try: response = requests.get(self.base_url, params=params, timeout=20) data = response.json() results = data.get("results", []) if not results: break for work in results: paper = self.extract_info(work) all_papers.append(paper) page += 1 time.sleep(0.2) except Exception as e: print(f"Error: {e}") break # =============================== # HIERARCHICAL KEYWORD FILTERING # =============================== keyword_list = [k.strip().lower() for k in keywords.split(",") if k.strip()] if len(keyword_list) >= 2: filtered = [] for p in all_papers: combined = f"{p['Title']} {p['Abstract']} {p['Authors']} {p['All Authors']}".lower() # first 2 keywords REQUIRED if keyword_list[0] not in combined or keyword_list[1] not in combined: continue score = 2 matched = [keyword_list[0], keyword_list[1]] for kw in keyword_list[2:]: if kw in combined: score += 1 matched.append(kw) p["Relevance"] = score p["Matched Keywords"] = ", ".join(matched) filtered.append(p) # Sort by relevance score (our calculated score) filtered.sort(key=lambda x: x["Relevance"], reverse=True) all_papers = filtered[:max_papers] # =============================== # AUTHOR FILTERING (post-API) # =============================== elif authors and authors.strip(): # If we have author filter but no keyword hierarchy author_list = [a.strip().lower() for a in authors.split(",") if a.strip()] filtered = [] for p in all_papers: all_authors_lower = p['All Authors'].lower() match_all = True matched_authors = [] for author in author_list: if author in all_authors_lower: matched_authors.append(author) else: match_all = False break if match_all: p["Relevance"] = len(matched_authors) p["Matched Keywords"] = f"Authors: {', '.join(matched_authors)}" filtered.append(p) all_papers = filtered[:max_papers] total = len(all_papers) avg_rel = sum(p.get("Relevance", 0) for p in all_papers) / total if total else 0 years = [p["Year"] for p in all_papers if p["Year"] != "N/A"] year_range = f"{min(years)} - {max(years)}" if years else "N/A" df = pd.DataFrame(all_papers) # Reorder columns to include BibTeX and remove All Authors from display column_order = ['Title', 'Authors', 'Year', 'Citations', 'Journal', 'Abstract', 'BibTeX', 'Relevance', 'Matched Keywords'] df = df[column_order] return df, all_papers, total, avg_rel, year_range def search_specific_paper(self, search_type, search_value): """Search for a specific paper by title, author, or DOI""" all_papers = [] if search_type == "DOI": # Clean DOI (remove URL if present) if "doi.org/" in search_value: search_value = search_value.split("doi.org/")[-1] # Direct lookup by DOI url = f"https://api.openalex.org/works/doi/{search_value}" try: response = requests.get(url, timeout=15) if response.status_code == 200: work = response.json() paper = self.extract_info(work) all_papers.append(paper) except Exception as e: print(f"Error fetching DOI: {e}") elif search_type == "Exact Title": # Search by exact title params = { "search": f'"{search_value}"', # Quote for exact match "per-page": 10, "sort": "relevance_score:desc" } try: response = requests.get(self.base_url, params=params, timeout=15) data = response.json() results = data.get("results", []) for work in results: # Check for close title match title = work.get("title", "").lower() if search_value.lower() in title or title in search_value.lower(): paper = self.extract_info(work) all_papers.append(paper) break # Take the first good match except Exception as e: print(f"Error searching title: {e}") elif search_type == "Specific Author": # Search for papers by a specific author params = { "search": search_value, "filter": "authorships.author.display_name.search:" + search_value, "per-page": 50, "sort": "cited_by_count:desc" } try: response = requests.get(self.base_url, params=params, timeout=15) data = response.json() results = data.get("results", []) for work in results[:20]: # Limit to 20 papers paper = self.extract_info(work) all_papers.append(paper) except Exception as e: print(f"Error searching author: {e}") if all_papers: for i, p in enumerate(all_papers): p['Rank'] = i + 1 df = pd.DataFrame(all_papers) column_order = ['Title', 'Authors', 'Year', 'Citations', 'Journal', 'Abstract', 'BibTeX'] # Only include columns that exist available_cols = [col for col in column_order if col in df.columns] df = df[available_cols] summary = f"โ Found **{len(all_papers)}** paper(s) matching your query" return df, all_papers, summary return pd.DataFrame(), [], "โ No papers found" def extract_info(self, work): # Rebuild abstract abstract = "" if work.get("abstract_inverted_index"): words = [] for word, positions in work["abstract_inverted_index"].items(): for pos in positions: words.append((pos, word)) words.sort() abstract = " ".join([w[1] for w in words]) else: abstract = "No abstract available" # Get all authors - FULL LIST (no et al.) all_authors = [] for authorship in work.get("authorships", []): # FIRST PRIORITY: Use raw_author_name (full name as published) raw_name = authorship.get("raw_author_name", "") display_name = authorship.get("author", {}).get("display_name", "") # Choose the best name if raw_name and len(raw_name) > 2: # Raw name is usually more complete (e.g., "Mamoru SHOJI" vs "M. Shoji") author_name = raw_name elif display_name: author_name = display_name else: author_name = "" if author_name: all_authors.append(author_name) # Show ALL authors, no truncation authors_str = ", ".join(all_authors) # Get DOI for reference doi = work.get("doi", "") if doi: doi = doi.replace("https://doi.org/", "") # Generate BibTeX entry bibtex = self.generate_bibtex(work, all_authors) return { "Title": work.get("title", "No title"), "Authors": authors_str, "All Authors": ", ".join(all_authors), # Hidden field for filtering "Year": work.get("publication_year", "N/A"), "Citations": work.get("cited_by_count", 0), "Journal": (work.get("primary_location", {}).get("source") or {}).get("display_name", "Unknown"), "DOI": doi, "Abstract": abstract, "BibTeX": bibtex, "Relevance": 0, "Matched Keywords": "" } def generate_bibtex(self, work, authors): """Generate BibTeX entry for a paper""" # Create citation key first_author = authors[0].split()[-1] if authors else "Unknown" year = work.get("publication_year", "0000") title_words = work.get("title", "Untitled").split()[:3] title_short = "".join([w[0] for w in title_words if w]) if title_words else "Paper" citation_key = f"{first_author}{year}{title_short}" # Clean the citation key (remove special characters) citation_key = ''.join(c for c in citation_key if c.isalnum()) # Get DOI or URL doi = work.get("doi", "") url = work.get("primary_location", {}).get("landing_page_url", "") if not url and work.get("open_access", {}).get("oa_url"): url = work.get("open_access", {}).get("oa_url") # Determine entry type (article, book, etc.) pub_type = "article" if work.get("type") == "book": pub_type = "book" elif work.get("type") == "book-chapter": pub_type = "incollection" # Build BibTeX string bibtex_lines = [ f"@{pub_type}{{{citation_key},", f" title = {{{work.get('title', 'Untitled')}}},", f" author = {{{' and '.join(authors)}}},", f" year = {{{work.get('publication_year', '0000')}}}," ] # Add journal/booktitle source = (work.get("primary_location", {}).get("source") or {}) if pub_type == "article": journal = source.get("display_name", "Unknown Journal") bibtex_lines.append(f" journal = {{{journal}}},") else: booktitle = source.get("display_name", "Unknown Publication") bibtex_lines.append(f" booktitle = {{{booktitle}}},") # Add volume, issue if available if work.get("biblio", {}).get("volume"): bibtex_lines.append(f" volume = {{{work['biblio']['volume']}}},") if work.get("biblio", {}).get("issue"): bibtex_lines.append(f" number = {{{work['biblio']['issue']}}},") # Add pages if available if work.get("biblio", {}).get("first_page") and work.get("biblio", {}).get("last_page"): pages = f"{work['biblio']['first_page']}--{work['biblio']['last_page']}" bibtex_lines.append(f" pages = {{{pages}}},") # Add DOI/URL if doi: bibtex_lines.append(f" doi = {{{doi}}},") elif url: bibtex_lines.append(f" url = {{{url}}},") # Add abstract if available if work.get("abstract_inverted_index"): # Rebuild abstract for BibTeX words = [] for word, positions in work["abstract_inverted_index"].items(): for pos in positions: words.append((pos, word)) words.sort() abstract = " ".join([w[1] for w in words]) if abstract: bibtex_lines.append(f" abstract = {{{abstract}}},") # Close the entry bibtex_lines.append("}") return "\n".join(bibtex_lines) # ============================================ # INTERFACE # ============================================ def create_interface(): with gr.Blocks( title="PaperSearchPro", css=custom_css, theme=gr.themes.Default( primary_hue="blue", neutral_hue="gray" )) as demo: gr.HTML("""
Easy Academic Paper Searching tool by Paul