import requests import pandas as pd import time from datetime import datetime import gradio as gr import os import re import bibtexparser from bibtexparser.bparser import BibTexParser from bibtexparser.customization import convert_to_unicode from difflib import SequenceMatcher import numpy as np from collections import Counter import math # ============================================ # CLEAN RESPONSIVE CSS - (same as your original) # ============================================ 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; } /* ================================================= */ /* REFERENCE CHECKER TABLE STYLING */ /* ================================================= */ .ref-valid { color: #10b981 !important; font-weight: 600 !important; } .ref-invalid { color: #ef4444 !important; font-weight: 600 !important; } .ref-partial { color: #f59e0b !important; font-weight: 600 !important; } .comparison-table { border-collapse: collapse !important; width: 100% !important; } .comparison-table th { background-color: #2563eb !important; color: white !important; padding: 10px !important; text-align: left !important; } .comparison-table td { padding: 8px !important; border-bottom: 1px solid #e5e7eb !important; vertical-align: top !important; } .comparison-table tr:hover { background-color: #f9fafb !important; } .original-ref { background-color: #fee2e2 !important; padding: 4px 8px !important; border-radius: 4px !important; border-left: 3px solid #ef4444 !important; } .corrected-ref { background-color: #dcfce7 !important; padding: 4px 8px !important; border-radius: 4px !important; border-left: 3px solid #10b981 !important; } """ # ============================================ # SEARCH ENGINE - ENHANCED WITH GOOGLE SCHOLAR LIKE SENSITIVITY # ============================================ class PaperSearchPro: def __init__(self): self.base_url = "https://api.openalex.org/works" self.per_page = 200 # Common stop words to ignore in matching self.stop_words = set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'may', 'might', 'must', 'can']) def calculate_text_similarity(self, text1, text2): """Calculate similarity between two texts using multiple methods""" if not text1 or not text2: return 0 text1 = text1.lower() text2 = text2.lower() # Method 1: Sequence matching (good for phrases) seq_similarity = SequenceMatcher(None, text1, text2).ratio() # Method 2: Word overlap (good for keywords) words1 = set(re.findall(r'\w+', text1)) words2 = set(re.findall(r'\w+', text2)) # Remove stop words for better matching words1 = words1 - self.stop_words words2 = words2 - self.stop_words if not words1 or not words2: return seq_similarity intersection = words1.intersection(words2) union = words1.union(words2) word_similarity = len(intersection) / len(union) if union else 0 # Method 3: Partial word matching (for stem variations) partial_matches = 0 for w1 in words1: for w2 in words2: if len(w1) > 3 and len(w2) > 3: if w1 in w2 or w2 in w1 or SequenceMatcher(None, w1, w2).ratio() > 0.8: partial_matches += 1 break partial_ratio = partial_matches / len(words1) if words1 else 0 # Weighted combination return (seq_similarity * 0.3 + word_similarity * 0.5 + partial_ratio * 0.2) def extract_keywords_from_phrase(self, phrase): """Extract meaningful keywords from a phrase""" words = re.findall(r'\w+', phrase.lower()) # Remove stop words and short words keywords = [w for w in words if w not in self.stop_words and len(w) > 2] # Also include bigrams if phrase has multiple words if len(words) > 1: bigrams = [' '.join(words[i:i+2]) for i in range(len(words)-1)] keywords.extend(bigrams) return list(set(keywords)) # Remove duplicates 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 # =============================== # ENHANCED KEYWORD FILTERING - GOOGLE SCHOLAR LIKE SENSITIVITY # Supports unlimited keywords with fuzzy matching and relevance scoring # =============================== keyword_phrases = [k.strip() for k in keywords.split(",") if k.strip()] if len(keyword_phrases) >= 1: # At least one keyword required filtered = [] # Pre-process each keyword phrase into search terms keyword_search_terms = [] for phrase in keyword_phrases: # Extract meaningful keywords from each phrase terms = self.extract_keywords_from_phrase(phrase) keyword_search_terms.append({ 'original': phrase, 'terms': terms, 'weight': 1.0 }) # Calculate IDF-like weights for terms (rarer terms get higher weight) all_terms = [] for kst in keyword_search_terms: all_terms.extend(kst['terms']) term_freq = Counter(all_terms) total_terms = len(all_terms) for kst in keyword_search_terms: # Adjust weight based on term rarity avg_freq = sum(term_freq[t] for t in kst['terms']) / len(kst['terms']) if kst['terms'] else 1 # Rarer terms (lower frequency) get higher weight kst['weight'] = 1.0 / (avg_freq / total_terms + 0.1) if avg_freq > 0 else 1.0 for paper in all_papers: # Combine all text fields for searching combined_text = f"{paper['Title']} {paper['Abstract']} {paper['Authors']}".lower() # Calculate relevance score using multiple factors score = 0 matched_terms = [] matched_phrases = [] # Check each keyword phrase for i, kst in enumerate(keyword_search_terms): phrase_score = 0 phrase_matched_terms = [] # Check each term in the phrase for term in kst['terms']: # Exact match (highest score) if term in combined_text: term_score = 1.0 * kst['weight'] phrase_matched_terms.append(term) else: # Fuzzy/partial matching # Check if term appears as part of a word words = re.findall(r'\w+', combined_text) best_partial = 0 for word in words: if len(word) > 3 and len(term) > 3: # Check if term is a substring of word if term in word: best_partial = max(best_partial, 0.8) # Check word similarity elif SequenceMatcher(None, term, word).ratio() > 0.7: sim = SequenceMatcher(None, term, word).ratio() best_partial = max(best_partial, sim * 0.7) if best_partial > 0: term_score = best_partial * kst['weight'] phrase_matched_terms.append(f"{term}~") else: term_score = 0 if term_score > 0: phrase_score += term_score # Normalize phrase score by number of terms if kst['terms']: phrase_score = phrase_score / len(kst['terms']) # First two phrases are required (must have at least partial match) if i < 2: if phrase_score > 0.3: # Partial match threshold for required phrases score += phrase_score * 2 # Higher weight for first two matched_phrases.append(kst['original']) matched_terms.extend(phrase_matched_terms) else: # Required phrase not matched well enough - skip this paper score = 0 break else: # Additional phrases boost score if phrase_score > 0: score += phrase_score matched_phrases.append(kst['original']) matched_terms.extend(phrase_matched_terms) if score > 0: # Additional boost factors # 1. Title match boost (keywords in title are more important) title_text = paper['Title'].lower() title_boost = 0 for term in set(matched_terms): term_clean = term.replace('~', '') if term_clean in title_text: title_boost += 0.2 score += title_boost # 2. Recent papers boost (slightly) if paper['Year'] != 'N/A' and paper['Year'] >= 2020: score += 0.1 # 3. Citation boost (log scale) if paper['Citations'] > 0: citation_boost = math.log10(paper['Citations'] + 1) * 0.05 score += min(citation_boost, 0.3) paper['Relevance'] = round(score, 2) paper['Matched Keywords'] = f"Phrases: {', '.join(matched_phrases[:5])}" + \ (f" (+{len(matched_phrases)-5} more)" if len(matched_phrases) > 5 else "") + \ f" | Terms: {', '.join(matched_terms[:10])}" + \ (f" (+{len(matched_terms)-10} more)" if len(matched_terms) > 10 else "") filtered.append(paper) # Sort by relevance score (descending) filtered.sort(key=lambda x: x['Relevance'], reverse=True) all_papers = filtered[:max_papers] # =============================== # AUTHOR FILTERING (if no keywords) # =============================== elif authors and authors.strip() and not keyword_phrases: # 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 with fuzzy matching params = { "search": search_value, "per-page": 20, "sort": "relevance_score:desc" } try: response = requests.get(self.base_url, params=params, timeout=15) data = response.json() results = data.get("results", []) best_match = None best_score = 0 for work in results: title = work.get("title", "") if title: # Calculate similarity score similarity = self.calculate_text_similarity(search_value, title) if similarity > best_score and similarity > 0.4: # Threshold for acceptance best_score = similarity best_match = work if best_match: paper = self.extract_info(best_match) paper['Match_Score'] = round(best_score, 2) all_papers.append(paper) 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'] if 'Match_Score' in df.columns: column_order.insert(1, 'Match_Score') # 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, "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) # ============================================ # BIBTEX CHECKER FEATURE # ============================================ def parse_bibtex_file(self, bibtex_file): """Parse a BibTeX file and extract entries""" entries = [] try: with open(bibtex_file, 'r', encoding='utf-8') as f: bibtex_str = f.read() # Parse BibTeX parser = BibTexParser(common_strings=True) parser.ignore_nonstandard_types = False parser.homogenize_fields = True bib_database = bibtexparser.loads(bibtex_str, parser=parser) for entry in bib_database.entries: # Extract key fields bib_entry = { 'id': entry.get('ID', ''), 'title': entry.get('title', ''), 'author': entry.get('author', ''), 'year': entry.get('year', ''), 'journal': entry.get('journal', entry.get('booktitle', '')), 'doi': entry.get('doi', ''), 'volume': entry.get('volume', ''), 'number': entry.get('number', ''), 'pages': entry.get('pages', ''), 'publisher': entry.get('publisher', ''), 'entry_type': entry.get('ENTRYTYPE', ''), 'raw_bibtex': self._reconstruct_bibtex(entry) } entries.append(bib_entry) return entries except Exception as e: print(f"Error parsing BibTeX: {e}") return [] def _reconstruct_bibtex(self, entry): """Reconstruct BibTeX string from entry""" bib_lines = [f"@{entry['ENTRYTYPE']}{{{entry['ID']},"] for field, value in entry.items(): if field not in ['ENTRYTYPE', 'ID'] and value: bib_lines.append(f" {field} = {{{value}}},") bib_lines.append("}") return "\n".join(bib_lines) def check_bibtex_entry(self, entry): """Check a BibTeX entry against OpenAlex database""" # Try DOI first if available if entry.get('doi'): doi = entry['doi'].strip() url = f"https://api.openalex.org/works/doi/{doi}" try: response = requests.get(url, timeout=10) if response.status_code == 200: work = response.json() corrected = self.extract_info(work) # Generate corrected BibTeX corrected_bibtex = self.generate_bibtex(work, corrected['Authors'].split(', ')) return { 'original': entry, 'corrected': corrected, 'corrected_bibtex': corrected_bibtex, 'status': 'valid', 'confidence': 1.0, 'method': 'DOI' } except: pass # Try title search with enhanced matching if entry.get('title'): params = { 'search': entry['title'], 'per-page': 10, 'sort': 'relevance_score:desc' } try: response = requests.get(self.base_url, params=params, timeout=10) data = response.json() results = data.get('results', []) if results: best_match = None best_confidence = 0 for work in results: if work.get('title'): # Calculate similarity similarity = self.calculate_text_similarity(entry['title'], work['title']) confidence = similarity * 0.6 # Base confidence from title match # Check year if entry.get('year') and work.get('publication_year'): if str(work['publication_year']) == entry['year']: confidence += 0.25 # Check author if entry.get('author'): work_authors = [] for auth in work.get('authorships', []): name = auth.get('raw_author_name', '') or auth.get('author', {}).get('display_name', '') if name: work_authors.append(name.lower()) work_authors_str = ' '.join(work_authors) entry_authors = entry['author'].lower() # Check if any author matches for author in entry_authors.split(' and '): author_parts = author.strip().split() for part in author_parts: if len(part) > 2 and part in work_authors_str: confidence += 0.15 break confidence = min(confidence, 1.0) if confidence > best_confidence: best_confidence = confidence best_match = work if best_match and best_confidence > 0.4: corrected = self.extract_info(best_match) corrected_bibtex = self.generate_bibtex(best_match, corrected['Authors'].split(', ')) status = 'valid' if best_confidence > 0.8 else 'partial' if best_confidence > 0.5 else 'not_found' return { 'original': entry, 'corrected': corrected, 'corrected_bibtex': corrected_bibtex, 'status': status, 'confidence': best_confidence, 'method': 'title search' } except: pass # No match found return { 'original': entry, 'corrected': None, 'corrected_bibtex': None, 'status': 'not_found', 'confidence': 0, 'method': 'none' } def check_bibtex_file(self, bibtex_file): """Main function to check BibTeX file""" # Parse BibTeX file entries = self.parse_bibtex_file(bibtex_file) if not entries: return None, None, "❌ No valid BibTeX entries found" # Check each entry results = [] valid_count = 0 partial_count = 0 invalid_count = 0 for i, entry in enumerate(entries): result = self.check_bibtex_entry(entry) results.append(result) if result['status'] == 'valid': valid_count += 1 elif result['status'] == 'partial': partial_count += 1 else: invalid_count += 1 # Small delay to avoid rate limiting time.sleep(0.2) # Create comparison HTML html_output = f"""

πŸ“Š BibTeX Check Summary

βœ… Valid: {valid_count} | ⚠️ Partial: {partial_count} | ❌ Not Found: {invalid_count}

Total entries checked: {len(entries)}

""" for i, result in enumerate(results): status_class = { 'valid': 'ref-valid', 'partial': 'ref-partial', 'not_found': 'ref-invalid' }.get(result['status'], '') status_text = { 'valid': 'βœ… VALID', 'partial': '⚠️ PARTIAL', 'not_found': '❌ NOT FOUND' }.get(result['status'], '') original = result['original'] original_title = original['title'][:50] + "..." if len(original['title']) > 50 else original['title'] if result['corrected']: corrected_title = result['corrected']['Title'][:50] + "..." if len(result['corrected']['Title']) > 50 else result['corrected']['Title'] confidence = f"{result['confidence']:.0%}" else: corrected_title = "No match found" confidence = "0%" html_output += f""" """ html_output += "
# Original BibTeX Key Original Title Corrected Title Confidence Status
{i+1} {original['id']}
{original_title}
{corrected_title}
{confidence} {status_text}
" # Create downloadable files timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # Create corrected BibTeX file corrected_bib_path = f"corrected_bibliography_{timestamp}.bib" with open(corrected_bib_path, 'w', encoding='utf-8') as f: for result in results: if result['corrected_bibtex']: f.write(result['corrected_bibtex'] + "\n\n") else: # Keep original if no correction found f.write(result['original']['raw_bibtex'] + "\n\n") # Create comparison CSV comparison_data = [] for result in results: if result['corrected']: corr = result['corrected'] comparison_data.append({ 'Entry_Key': result['original']['id'], 'Original_Title': result['original']['title'], 'Status': result['status'], 'Confidence': result['confidence'], 'Found_By': result['method'], 'Corrected_Title': corr['Title'], 'Corrected_Authors': corr['Authors'], 'Corrected_Year': corr['Year'], 'Corrected_Journal': corr['Journal'], 'Corrected_DOI': corr.get('DOI', ''), 'PDF_URL': corr.get('PDF_URL', '') }) else: comparison_data.append({ 'Entry_Key': result['original']['id'], 'Original_Title': result['original']['title'], 'Status': result['status'], 'Confidence': result['confidence'], 'Found_By': result['method'], 'Corrected_Title': 'NOT FOUND', 'Corrected_Authors': '', 'Corrected_Year': '', 'Corrected_Journal': '', 'Corrected_DOI': '', 'PDF_URL': '' }) df = pd.DataFrame(comparison_data) csv_path = f"bibtex_comparison_{timestamp}.csv" df.to_csv(csv_path, index=False) return corrected_bib_path, csv_path, html_output # ============================================ # 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("""

πŸ“±XYZπŸ“±PaperInfo

Easy Academic Paper/journal details by Paul

""") # Create tabs for different search modes with gr.Tabs(elem_classes="tab-nav") as tabs_component: # Tab 1: General Search with gr.TabItem("πŸ” General Search", id=0): with gr.Column(elem_classes="white-card"): query = gr.Textbox( label="Search Topic", value="photoacoustic imaging", placeholder="Enter research topic or keywords..." ) with gr.Row(): with gr.Column(): keywords = gr.Textbox( label="Required Keywords (unlimited, first 2 required for match)", value="blood, photoacoustics, imaging, vascular, hemoglobin", placeholder="e.g., blood, photoacoustics, imaging, vascular (comma-separated)" ) gr.Markdown(""" ✨ **Enhanced matching**: Supports unlimited keywords with fuzzy/partial matching like Google Scholar """) with gr.Column(): authors = gr.Textbox( label="Filter by Authors (optional)", value="", placeholder="e.g., Smith, Zhang, Wang (comma-separated)" ) # ========================== # MAX PAPERS SLIDER WITH BLACK REGION COMMENT # ========================== max_papers = gr.Slider(1000, 5000000, value=10000, step=1000, label="Max Papers") # Add black region comment below slider gr.HTML("""
⚑ Tip: Set this number as high as possible or more than 5000 for better scanning of all available papers
""") with gr.Row(): year_from = gr.Number(value=2000, label="Year From", precision=0) year_to = gr.Number(value=2026, label="Year To", precision=0) open_access = gr.Checkbox(label="Open Access Only") # ========================== # SORTING OPTIONS # ========================== with gr.Row(): sort_by = gr.Dropdown( label="Sort By", choices=["Relevance", "Year", "Citations"], value="Relevance" ) sort_order = gr.Dropdown( label="Sort Order", choices=["Descending", "Ascending"], value="Descending" ) search_btn = gr.Button("πŸ” SEARCH PAPERS", variant="primary", size="lg") # Tab 2: Specific Paper Search with gr.TabItem("🎯 Find Specific Paper", id=1): with gr.Column(elem_classes="white-card"): gr.Markdown(""" ### Find a specific paper by: - **DOI** (e.g., 10.1038/nature12373) - **Exact Title** (e.g., "Photoacoustic imaging in biomedicine") - **Author** (find papers by a specific researcher) ✨ **Enhanced**: Title matching now uses fuzzy similarity scoring """) with gr.Row(): specific_search_type = gr.Dropdown( label="Search by", choices=["DOI", "Exact Title", "Specific Author"], value="DOI" ) specific_search_value = gr.Textbox( label="Enter value", placeholder="e.g., 10.1038/nature12373 or exact paper title or author name", scale=2 ) specific_search_btn = gr.Button("πŸ” FIND PAPER", variant="primary") specific_summary = gr.Markdown("Ready") # Tab 3: BibTeX Checker with gr.TabItem("πŸ“š Check BibTeX", id=2): with gr.Column(elem_classes="white-card"): gr.Markdown(""" ### πŸ“‹ Upload a BibTeX file to check its entries This tool will: 1. Parse all BibTeX entries from your file 2. Check each entry against the OpenAlex database 3. Show you which entries are correct and which need fixing 4. Generate a corrected BibTeX file 5. Provide a comparison CSV with all details ✨ **Enhanced**: Better title matching with fuzzy similarity scoring """) bibtex_file = gr.File( label="Upload BibTeX File (.bib)", file_types=[".bib"], type="filepath" ) check_bibtex_btn = gr.Button("πŸ” CHECK BIBTEX", variant="primary", size="lg") bibtex_results = gr.HTML(label="BibTeX Comparison") with gr.Row(): corrected_bibtex_download = gr.File(label="Download Corrected BibTeX") comparison_csv_download = gr.File(label="Download Comparison CSV") # Metrics section (common for first two tabs) with gr.Row(visible=True) as metrics_row: total_display = gr.Markdown("**0**", elem_classes="metric-value") avg_display = gr.Markdown("**0.0**", elem_classes="metric-value") year_display = gr.Markdown("**N/A**", elem_classes="metric-value") # Add labels for metrics with gr.Row(visible=True) as metrics_labels_row: gr.Markdown("**Total Papers**", elem_classes="metric-label") gr.Markdown("**Avg Relevance**", elem_classes="metric-label") gr.Markdown("**Year Range**", elem_classes="metric-label") # Results section (common for first two tabs) results = gr.Dataframe( label="Results", row_count=15, column_widths=["25%", "15%", "5%", "5%", "10%", "15%", "15%", "5%", "5%"], wrap=True, visible=True ) state = gr.State([]) # ========================== # EXPORT SECTION (for first two tabs) # ========================== with gr.Column(visible=True) as export_section: gr.HTML("""

πŸ“₯ Export Results

""") with gr.Row(): filename = gr.Textbox( label="Filename", value="papers_results", placeholder="Enter base filename without extension" ) format_type = gr.Dropdown( label="Format", choices=["TXT", "CSV", "Excel", "BibTeX"], value="TXT" ) export_btn = gr.Button("πŸ“₯ Download Results", variant="secondary") export_status = gr.Markdown("") download_file = gr.File(visible=False) # Move searcher instantiation and click bindings inside the Blocks context searcher = PaperSearchPro() def search_wrapper(q, k, a, m, yf, yt, oa, sb, so): try: df, papers, total, avg, yr = searcher.search_papers(q, k, a, m, yf, yt, oa, sb, so) return df, papers, f"**{total}**", f"**{avg:.2f}**", f"**{yr}**" except Exception as e: empty_df = pd.DataFrame(columns=['Title', 'Authors', 'Year', 'Citations', 'Journal', 'PDF_URL', 'Abstract', 'BibTeX', 'Relevance', 'Matched Keywords']) return empty_df, [], "**0**", "**0.0**", "**N/A**" def specific_search_wrapper(search_type, search_value): try: df, papers, summary = searcher.search_specific_paper(search_type, search_value) if not papers: return df, papers, summary, "**0**", "**0.0**", "**N/A**" total = len(papers) avg_rel = sum(p.get('Match_Score', 0) for p in papers) / total if total else 0 years = [p["Year"] for p in papers if p["Year"] != "N/A"] year_range = f"{min(years)} - {max(years)}" if years else "N/A" return df, papers, summary, f"**{total}**", f"**{avg_rel:.2f}**", f"**{year_range}**" except Exception as e: empty_df = pd.DataFrame(columns=['Title', 'Authors', 'Year', 'Citations', 'Journal', 'PDF_URL', 'Abstract', 'BibTeX']) return empty_df, [], f"❌ Error: {str(e)}", "**0**", "**0.0**", "**N/A**" def check_bibtex_wrapper(bibtex_file): if bibtex_file is None: return None, None, "❌ Please upload a BibTeX file", None, None try: corrected_bib_path, csv_path, html_output = searcher.check_bibtex_file(bibtex_file.name) return corrected_bib_path, csv_path, html_output, corrected_bib_path, csv_path except Exception as e: return None, None, f"❌ Error: {str(e)}", None, None search_btn.click( search_wrapper, inputs=[query, keywords, authors, max_papers, year_from, year_to, open_access, sort_by, sort_order], outputs=[results, state, total_display, avg_display, year_display] ) specific_search_btn.click( specific_search_wrapper, inputs=[specific_search_type, specific_search_value], outputs=[results, state, specific_summary, total_display, avg_display, year_display] ) check_bibtex_btn.click( check_bibtex_wrapper, inputs=[bibtex_file], outputs=[corrected_bibtex_download, comparison_csv_download, bibtex_results, corrected_bibtex_download, comparison_csv_download] ) def export_results(papers, fname, fmt): if not papers: return None, "❌ No papers to export", gr.File(visible=False) df = pd.DataFrame(papers) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") if 'All Authors' in df.columns: df = df.drop(columns=['All Authors']) if fmt == "CSV": path = f"{fname}_{timestamp}.csv" df.to_csv(path, index=False) elif fmt == "Excel": path = f"{fname}_{timestamp}.xlsx" df.to_excel(path, index=False, engine='openpyxl') elif fmt == "BibTeX": path = f"{fname}_{timestamp}.bib" with open(path, "w", encoding="utf-8") as f: for _, row in df.iterrows(): if row['BibTeX'] and row['BibTeX'] != "": f.write(row['BibTeX'] + "\n\n") else: # TXT format path = f"{fname}_{timestamp}.txt" with open(path, "w", encoding="utf-8") as f: f.write("PAPER SEARCH PRO RESULTS\n") f.write("="*80 + "\n") f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"Total Papers: {len(df)}\n") f.write("="*80 + "\n\n") for i, row in df.iterrows(): f.write(f"PAPER #{i+1}\n") f.write("-"*40 + "\n") for col in df.columns: if col == "BibTeX": f.write(f"\n{col}:\n{row[col]}\n") elif col == "PDF_URL": f.write(f"{col}: {row[col]}\n") else: f.write(f"{col}: {row[col]}\n") f.write("\n" + "="*80 + "\n\n") return path, f"βœ… File ready: {os.path.basename(path)}", gr.File(visible=True) export_btn.click( export_results, inputs=[state, filename, format_type], outputs=[download_file, export_status, download_file] ) # Show/hide elements based on active tab def on_tab_select(evt: gr.SelectData): if evt.value == "πŸ“š Check BibTeX": return { metrics_row: gr.update(visible=False), metrics_labels_row: gr.update(visible=False), results: gr.update(visible=False), export_section: gr.update(visible=False) } else: return { metrics_row: gr.update(visible=True), metrics_labels_row: gr.update(visible=True), results: gr.update(visible=True), export_section: gr.update(visible=True) } tabs_component.select( on_tab_select, inputs=None, outputs=[metrics_row, metrics_labels_row, results, export_section], queue=False ) return demo # ============================================ # MAIN # ============================================ if __name__ == "__main__": # Install required package if not present try: import bibtexparser except ImportError: os.system("pip install bibtexparser") import bibtexparser demo = create_interface() demo.launch( inbrowser=True, height=1000, server_name="0.0.0.0", # Makes it accessible on network share=False )