Spaces:
Sleeping
Sleeping
Download app.py from Subahdip627/S: direct link, hf CLI and curl.
- Browser
- Download file 35.2 kB
-
https://huggingface.co/spaces/Subahdip627/S/resolve/main/app.py
- Command line
-
hf download hf://spaces/Subahdip627/S/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Subahdip627/S/resolve/main/app.py
35.2 kB
| 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(""" | |
| <div style='text-align:center; padding:3rem 0'> | |
| <h1><span>📱XYZ📱PaperInfo</span></h1> | |
| <p class='subtitle-text'>Easy Academic Paper/journal details by Paul</p> | |
| </div> | |
| """) | |
| # Create tabs for different search modes | |
| with gr.Tabs(elem_classes="tab-nav"): | |
| # 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 (first 2 REQUIRED for hierarchical search)", | |
| value="blood, photoacoustics, imaging", | |
| placeholder="e.g., blood, photoacoustics, imaging (comma-separated)" | |
| ) | |
| 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(""" | |
| <div class="slider-comment"> | |
| ⚡ Tip: Set this number as high as possible or more than 5000 for better scanning of all available papers | |
| </div> | |
| """) | |
| 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) | |
| """) | |
| 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") | |
| # Metrics section (common for both tabs) | |
| with gr.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(): | |
| 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 both tabs) | |
| results = gr.Dataframe( | |
| label="Results", | |
| row_count=15, | |
| column_widths=["25%", "15%", "5%", "5%", "10%", "15%", "15%", "5%", "5%"], | |
| wrap=True | |
| ) | |
| state = gr.State([]) | |
| # ========================== | |
| # EXPORT SECTION | |
| # ========================== | |
| gr.HTML(""" | |
| <h3 style="color: #2563eb !important; font-weight: 600; margin-bottom: 16px; margin-top: 24px;">📥 Export Results</h3> | |
| """) | |
| 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" | |
| ) | |
| with gr.Row(): | |
| 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 = 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**" | |
| 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] | |
| ) | |
| 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] | |
| ) | |
| return demo | |
| # ============================================ | |
| # MAIN | |
| # ============================================ | |
| if __name__ == "__main__": | |
| # Unpack the returned values from create_interface | |
| demo = create_interface() | |
| demo.launch( | |
| inbrowser=True, | |
| height=1000, | |
| server_name="0.0.0.0", # Makes it accessible on network | |
| share=False | |
| ) |