|
import gradio as gr |
|
import requests |
|
from datetime import datetime, timezone |
|
import logging |
|
|
|
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
|
|
|
API_URL = "https://huggingface.co/api/daily_papers" |
|
|
|
class PaperManager: |
|
def __init__(self, papers_per_page=10, max_papers=100): |
|
self.papers_per_page = papers_per_page |
|
self.max_papers = max_papers |
|
self.cache = [] |
|
self.last_fetch_date = None |
|
self.total_pages = 1 |
|
|
|
def fetch_papers(self): |
|
""" |
|
Fetch the first 100 most recent papers from the API, sorted by upvotes descending. |
|
Caches the result and fetches only once a day. |
|
Returns: |
|
tuple: (success: bool, data: list, error_message: str) |
|
""" |
|
today = datetime.now(timezone.utc).date() |
|
if self.last_fetch_date == today and self.cache: |
|
logging.info("Using cached papers.") |
|
return True, self.cache, "" |
|
else: |
|
try: |
|
|
|
response = requests.get(f"{API_URL}?page=1&limit={self.max_papers}") |
|
response.raise_for_status() |
|
data = response.json() |
|
|
|
if not isinstance(data, list): |
|
raise ValueError("API response is not a list of papers.") |
|
|
|
|
|
sorted_papers = sorted( |
|
data, |
|
key=lambda x: x.get('paper', {}).get('upvotes', 0), |
|
reverse=True |
|
) |
|
|
|
|
|
self.cache = sorted_papers[:self.max_papers] |
|
self.last_fetch_date = today |
|
self.total_pages = (len(self.cache) + self.papers_per_page - 1) // self.papers_per_page |
|
|
|
logging.info(f"Fetched {len(self.cache)} papers. Total pages: {self.total_pages}") |
|
return True, self.cache, "" |
|
except requests.HTTPError as http_err: |
|
error_message = f"HTTP error occurred: {http_err}" |
|
logging.error(error_message) |
|
return False, [], error_message |
|
except requests.ConnectionError as conn_err: |
|
error_message = f"Connection error occurred: {conn_err}" |
|
logging.error(error_message) |
|
return False, [], error_message |
|
except requests.Timeout as timeout_err: |
|
error_message = f"Timeout error occurred: {timeout_err}" |
|
logging.error(error_message) |
|
return False, [], error_message |
|
except requests.RequestException as req_err: |
|
error_message = f"An error occurred: {req_err}" |
|
logging.error(error_message) |
|
return False, [], error_message |
|
except ValueError as val_err: |
|
error_message = f"Data format error: {val_err}" |
|
logging.error(error_message) |
|
return False, [], error_message |
|
|
|
def format_paper(self, paper): |
|
""" |
|
Format a single paper's information into HTML. |
|
""" |
|
title = paper.get('title', 'No title') |
|
paper_id = paper.get('paper', {}).get('id', '') |
|
url = f"https://huggingface.co/papers/{paper_id}" if paper_id else "#" |
|
authors = ', '.join([author.get('name', 'Unknown') for author in paper.get('paper', {}).get('authors', [])]) |
|
upvotes = paper.get('paper', {}).get('upvotes', 0) |
|
comments = paper.get('numComments', 0) |
|
published_at = paper.get('publishedAt', datetime.now(timezone.utc).isoformat()) |
|
|
|
try: |
|
published_time = datetime.fromisoformat(published_at.replace('Z', '+00:00')) |
|
time_ago = (datetime.now(timezone.utc) - published_time).days |
|
except ValueError: |
|
time_ago = "Unknown" |
|
|
|
return f"""<div style='border-bottom: 1px solid #eee; padding: 10px 0;'> |
|
<a href='{url}' target='_blank' style='color: #000; text-decoration: none; font-weight: bold;'>{title}</a> |
|
<div style='font-size: 0.8em; color: #666; margin-top: 5px;'> |
|
{upvotes} upvotes | by {authors} | {time_ago} days ago | {comments} comments |
|
</div> |
|
</div>""" |
|
|
|
def render_papers(self, papers, page=1): |
|
""" |
|
Render HTML for a list of papers based on the current page. |
|
""" |
|
if not papers: |
|
return "<div>No papers available for this page.</div>" |
|
start = (page - 1) * self.papers_per_page |
|
end = start + self.papers_per_page |
|
page_papers = papers[start:end] |
|
return "".join([self.format_paper(paper) for paper in page_papers]) |
|
|
|
def search_papers(self, query): |
|
""" |
|
Filter papers based on the search query. |
|
""" |
|
if not query: |
|
return self.cache |
|
return [paper for paper in self.cache if query.lower() in paper.get('title', '').lower()] |
|
|
|
css = """ |
|
html, body { |
|
height: 100%; |
|
margin: 0; |
|
padding: 0; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
background-color: #f0f0f0; |
|
} |
|
.container { |
|
font-family: Arial, sans-serif; |
|
max-width: 800px; |
|
width: 100%; |
|
background-color: white; |
|
padding: 20px; |
|
border-radius: 10px; |
|
box-shadow: 0 0 10px rgba(0,0,0,0.1); |
|
} |
|
.paper-list { |
|
max-height: 400px; |
|
overflow-y: auto; |
|
border: 1px solid #eee; |
|
border-radius: 5px; |
|
padding: 10px; |
|
margin-bottom: 10px; |
|
} |
|
.search-row { |
|
display: flex; |
|
gap: 10px; |
|
margin-bottom: 20px; |
|
} |
|
.title { |
|
text-align: center; |
|
color: #333; |
|
} |
|
.footer { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-top: 10px; |
|
} |
|
.error-message { |
|
color: red; |
|
margin-bottom: 10px; |
|
text-align: center; |
|
} |
|
""" |
|
|
|
|
|
paper_manager = PaperManager() |
|
|
|
def initialize(): |
|
""" |
|
Initialize the app by fetching papers and setting initial states. |
|
""" |
|
success, papers, error_message = paper_manager.fetch_papers() |
|
if success: |
|
page = 1 |
|
total_pages = paper_manager.total_pages |
|
page_papers = paper_manager.render_papers(papers, page) |
|
page_info_text = f"Page {page} of {total_pages}" |
|
return page_papers, page, total_pages, papers, page_info_text, "" |
|
else: |
|
error_html = f"<div class='error-message'>Error fetching papers: {error_message}</div>" |
|
return error_html, 1, 1, [], "Page 1 of 1", error_message |
|
|
|
def refresh_papers(current_page, query): |
|
""" |
|
Refresh the papers. Fetch new data if a new day has started. |
|
""" |
|
success, papers, error_message = paper_manager.fetch_papers() |
|
if success: |
|
|
|
new_page = 1 |
|
if query: |
|
filtered = paper_manager.search_papers(query) |
|
total_pages = (len(filtered) + paper_manager.papers_per_page - 1) // paper_manager.papers_per_page if filtered else 1 |
|
page_papers = paper_manager.render_papers(filtered, new_page) |
|
page_info_text = f"Page {new_page} of {total_pages}" |
|
return page_papers, new_page, total_pages, filtered, page_info_text, "" |
|
else: |
|
papers = paper_manager.cache |
|
total_pages = paper_manager.total_pages |
|
page_papers = paper_manager.render_papers(papers, new_page) |
|
page_info_text = f"Page {new_page} of {total_pages}" |
|
return page_papers, new_page, total_pages, papers, page_info_text, "" |
|
else: |
|
error_html = f"<div class='error-message'>Error fetching papers: {error_message}</div>" |
|
return error_html, current_page, paper_manager.total_pages, [], f"Page {current_page} of {paper_manager.total_pages}", error_message |
|
|
|
def search_papers(query, papers): |
|
""" |
|
Search for papers based on the query and update the display. |
|
""" |
|
if query: |
|
filtered = paper_manager.search_papers(query) |
|
else: |
|
filtered = papers |
|
total_pages = (len(filtered) + paper_manager.papers_per_page - 1) // paper_manager.papers_per_page if filtered else 1 |
|
page = 1 |
|
page_papers = paper_manager.render_papers(filtered, page) |
|
page_info_text = f"Page {page} of {total_pages}" |
|
if not filtered: |
|
error_html = "<div class='error-message'>No papers match your search query.</div>" |
|
else: |
|
error_html = "" |
|
return page_papers, page, total_pages, filtered, page_info_text, error_html |
|
|
|
def change_page(direction, current_page, total_pages, papers, query): |
|
""" |
|
Change the current page based on the direction ('next' or 'prev'). |
|
""" |
|
if direction == "next" and current_page < total_pages: |
|
new_page = current_page + 1 |
|
elif direction == "prev" and current_page > 1: |
|
new_page = current_page - 1 |
|
else: |
|
new_page = current_page |
|
|
|
if not papers: |
|
page_papers = "<div>No papers available for this page.</div>" |
|
else: |
|
page_papers = paper_manager.render_papers(papers, new_page) |
|
page_info_text = f"Page {new_page} of {total_pages}" |
|
return page_papers, new_page, total_pages, papers, page_info_text, "" |
|
|
|
def go_prev(current_page, total_pages, papers, query): |
|
""" |
|
Handle the 'Previous Page' button click. |
|
""" |
|
return change_page("prev", current_page, total_pages, papers, query) |
|
|
|
def go_next(current_page, total_pages, papers, query): |
|
""" |
|
Handle the 'Next Page' button click. |
|
""" |
|
return change_page("next", current_page, total_pages, papers, query) |
|
|
|
|
|
demo = gr.Blocks(css=css) |
|
|
|
with demo: |
|
with gr.Column(elem_classes=["container"]): |
|
|
|
gr.Markdown("# Daily Papers - HackerNews Style", elem_classes=["title"]) |
|
|
|
|
|
with gr.Row(elem_classes=["search-row"]): |
|
search_input = gr.Textbox(label="Search papers", placeholder="Enter search term...") |
|
refresh_button = gr.Button("Refresh") |
|
|
|
|
|
error_display = gr.HTML(elem_classes=["error-message"]) |
|
|
|
|
|
paper_list = gr.HTML(elem_classes=["paper-list"]) |
|
|
|
|
|
with gr.Row(elem_classes=["footer"]): |
|
prev_button = gr.Button("Previous Page") |
|
page_info = gr.Markdown("Page 1 of 1") |
|
next_button = gr.Button("Next Page") |
|
|
|
|
|
current_page_state = gr.State(1) |
|
total_pages_state = gr.State(1) |
|
papers_state = gr.State([]) |
|
|
|
|
|
demo.load( |
|
initialize, |
|
outputs=[paper_list, current_page_state, total_pages_state, papers_state, page_info, error_display] |
|
) |
|
|
|
|
|
search_input.submit( |
|
search_papers, |
|
inputs=[search_input, papers_state], |
|
outputs=[paper_list, current_page_state, total_pages_state, papers_state, page_info, error_display] |
|
) |
|
search_input.change( |
|
search_papers, |
|
inputs=[search_input, papers_state], |
|
outputs=[paper_list, current_page_state, total_pages_state, papers_state, page_info, error_display] |
|
) |
|
|
|
|
|
refresh_button.click( |
|
refresh_papers, |
|
inputs=[current_page_state, search_input], |
|
outputs=[paper_list, current_page_state, total_pages_state, papers_state, page_info, error_display] |
|
) |
|
|
|
|
|
prev_button.click( |
|
go_prev, |
|
inputs=[current_page_state, total_pages_state, papers_state, search_input], |
|
outputs=[paper_list, current_page_state, total_pages_state, papers_state, page_info, error_display] |
|
) |
|
next_button.click( |
|
go_next, |
|
inputs=[current_page_state, total_pages_state, papers_state, search_input], |
|
outputs=[paper_list, current_page_state, total_pages_state, papers_state, page_info, error_display] |
|
) |
|
|
|
|
|
demo.launch() |
|
|