import requests import pandas as pd import time from datetime import datetime import gradio as gr 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; } /* Primary button */ button.variant-primary { background-color: #2563eb !important; color: #ffffff !important; } /* Secondary button */ button.variant-secondary { background-color: #6b7280 !important; color: #ffffff !important; } /* 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; color: #ffffff !important; 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; color: #ffffff !important; 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; } /* ================================================= */ /* DROPDOWN POPUP PAGE - WHITE BACKGROUND */ /* ================================================= */ /* The popup menu that appears when clicking dropdown */ .gr-dropdown-list, .dropdown-list, div[role="listbox"], .options-container, select option, .gr-dropdown option, .gradio-dropdown option, div.gr-box select option { background-color: #ffffff !important; /* Pure white background */ color: #111827 !important; /* Dark text */ } /* Individual options */ select option, .gr-dropdown option, .gradio-dropdown option { background-color: #ffffff !important; color: #111827 !important; padding: 8px 12px !important; } /* Hover effect */ select option:hover, .gr-dropdown option:hover, .gradio-dropdown option:hover { background-color: #f3f4f6 !important; /* Light gray on hover */ color: #111827 !important; } /* Selected option */ select option:checked, .gr-dropdown option:checked, .gradio-dropdown option:checked { background-color: #2563eb !important; color: white !important; } /* ================================================= */ /* PDF LINK STYLING */ /* ================================================= */ .pdf-link { color: #2563eb !important; text-decoration: none !important; font-weight: 500 !important; } .pdf-link:hover { text-decoration: underline !important; color: #1d4ed8 !important; } .no-pdf { color: #6b7280 !important; font-style: italic !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 # =============================== # IMPROVED HIERARCHICAL KEYWORD FILTERING # Now supports partial word matching like Google # =============================== keyword_phrases = [k.strip() for k in keywords.split(",") if k.strip()] if len(keyword_phrases) >= 2: filtered = [] # Pre-process keyword phrases into words for partial matching # For each phrase, get all individual words (split by spaces) keyword_words = [] for phrase in keyword_phrases: # Split phrase into individual words, filter out common stop words and short words words = [w.lower() for w in phrase.split() if len(w) > 2] # Only words longer than 2 chars if not words: # If phrase was a single short word, use the whole phrase words = [phrase.lower()] keyword_words.append(words) for p in all_papers: combined = f"{p['Title']} {p['Abstract']} {p['Authors']} {p['All Authors']}".lower() # Check first keyword phrase (ANY word from it must match) first_phrase_matched = False for word in keyword_words[0]: if word in combined: first_phrase_matched = True break # Check second keyword phrase (ANY word from it must match) second_phrase_matched = False for word in keyword_words[1]: if word in combined: second_phrase_matched = True break # First 2 keyword phrases are REQUIRED (at least one word from each must match) if not first_phrase_matched or not second_phrase_matched: continue # Calculate score and track matched words score = 2 # Base score for first two phrases matched_phrases = [keyword_phrases[0], keyword_phrases[1]] matched_words = [] # Add the specific words that matched from first two phrases for word in keyword_words[0]: if word in combined: matched_words.append(word) break # Just take one representative word for word in keyword_words[1]: if word in combined: matched_words.append(word) break # Check additional keyword phrases for i, phrase_words in enumerate(keyword_words[2:], start=2): phrase_matched = False for word in phrase_words: if word in combined: phrase_matched = True matched_words.append(word) break if phrase_matched: score += 1 matched_phrases.append(keyword_phrases[i]) # Remove duplicates from matched words while preserving order seen = set() unique_matched_words = [] for word in matched_words: if word not in seen: seen.add(word) unique_matched_words.append(word) p["Relevance"] = score p["Matched Keywords"] = f"Phrases: {', '.join(matched_phrases)} | Matched words: {', '.join(unique_matched_words)}" 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: # Split author name into parts for partial matching author_parts = author.split() author_matched = False # Check if any part of the author name appears for part in author_parts: if len(part) > 2 and part in all_authors_lower: # Only check significant parts author_matched = True matched_authors.append(part) break if not author_matched: # Fallback: check if the whole author string appears if author in all_authors_lower: author_matched = True 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', 'PDF_URL', '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', 'PDF_URL', '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/", "") # Get PDF URL if available pdf_url = "" if work.get("open_access", {}).get("oa_url"): pdf_url = work["open_access"]["oa_url"] elif work.get("primary_location", {}).get("pdf_url"): pdf_url = work["primary_location"]["pdf_url"] else: pdf_url = "No free PDF available" # 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, "PDF_URL": pdf_url, # Add PDF link "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(): # Define the theme here to pass to launch later default_theme = gr.themes.Default( primary_hue="blue", neutral_hue="gray" ) with gr.Blocks(title="PaperSearchPro", css=custom_css, theme=default_theme) as demo: gr.HTML("""
Easy Academic Paper/journal details by Paul