From 904eec392e4c79abadab56291b6ac456edc79c25 Mon Sep 17 00:00:00 2001 From: rain-bus Date: Sat, 6 Jun 2026 00:38:56 +0800 Subject: [PATCH] feat: overhaul UI styling, improve templates, enhance services and tests --- app/cli.py | 3 + app/config.py | 2 +- app/main.py | 4 +- app/models.py | 68 ++++-- app/routes/admin.py | 8 +- app/routes/pages.py | 42 ++-- app/routes/search.py | 12 +- app/services/cleaner.py | 26 ++- app/services/crawler.py | 48 +++- app/services/embedder.py | 12 +- app/services/pdf_downloader.py | 1 + app/services/pi_client.py | 8 +- app/services/scheduler.py | 16 +- app/services/schemas.py | 20 +- app/services/searcher.py | 50 +++-- app/services/summarizer.py | 23 +- app/services/trends.py | 45 ++-- app/static/css/style.css | 297 ++++++++++++++++++++----- app/templates/admin_logs.html | 238 +++++++++++++------- app/templates/base.html | 72 +++--- app/templates/compare.html | 38 ++-- app/templates/detail.html | 193 ++++++++-------- app/templates/index.html | 18 +- app/templates/partials/paper_card.html | 41 ++-- app/templates/reading_list.html | 75 ++++--- app/templates/search.html | 155 ++++++++----- app/templates/trends.html | 295 ++++++++++++------------ app/utils.py | 4 +- scripts/init_db.py | 1 + scripts/manual_crawl.py | 1 + tests/conftest.py | 87 ++++---- tests/test_admin.py | 90 ++++++-- tests/test_cleaner.py | 71 ++++-- tests/test_embedder.py | 14 +- tests/test_image_extractor.py | 37 ++- tests/test_pages.py | 20 +- tests/test_searcher.py | 37 ++- tests/test_summarizer.py | 94 +++++--- 38 files changed, 1471 insertions(+), 795 deletions(-) diff --git a/app/cli.py b/app/cli.py index 0f25c7e..ef1216c 100644 --- a/app/cli.py +++ b/app/cli.py @@ -31,6 +31,7 @@ def crawl( # 确保数据库和表存在 import os + os.makedirs(settings.db_path.parent, exist_ok=True) _init(engine) typer.echo(f"📡 开始抓取 {target} ...") @@ -63,6 +64,7 @@ def summarize( from app.services.summarizer import summarize_batch, summarize_single import os + os.makedirs(settings.db_path.parent, exist_ok=True) _init(engine) @@ -97,6 +99,7 @@ def init_db(): from app.models import init_db as _init import os + os.makedirs(settings.db_path.parent, exist_ok=True) _init(engine) typer.echo(f"✅ 数据库已初始化:{settings.db_path}") diff --git a/app/config.py b/app/config.py index e316855..f537bb7 100644 --- a/app/config.py +++ b/app/config.py @@ -62,7 +62,7 @@ class Settings(BaseSettings): # sqlite:///data/db/papers.db → data/db/papers.db url = self.DATABASE_URL if url.startswith("sqlite:///"): - return BASE_DIR / url[len("sqlite:///"):] + return BASE_DIR / url[len("sqlite:///") :] raise ValueError(f"Unsupported DATABASE_URL: {url}") @property diff --git a/app/main.py b/app/main.py index 33ceaaf..8943786 100644 --- a/app/main.py +++ b/app/main.py @@ -58,7 +58,9 @@ def create_app() -> FastAPI: # 安全警告 if settings.ADMIN_TOKEN == "change-me": - logger.warning("⚠️ ADMIN_TOKEN is the default value 'change-me'. Please change it in .env!") + logger.warning( + "⚠️ ADMIN_TOKEN is the default value 'change-me'. Please change it in .env!" + ) if settings.APP_HOST not in ("127.0.0.1", "localhost", "::1"): logger.warning( diff --git a/app/models.py b/app/models.py index eb5006a..378cd5b 100644 --- a/app/models.py +++ b/app/models.py @@ -43,13 +43,39 @@ class Paper(Base): raw_output_path = Column(String) summary_quality = Column(String) - authors = relationship("PaperAuthor", back_populates="paper", cascade="all, delete-orphan") - tags = relationship("PaperTag", back_populates="paper", cascade="all, delete-orphan") - summary = relationship("PaperSummary", back_populates="paper", uselist=False, cascade="all, delete-orphan") - summary_status = relationship("SummaryStatus", back_populates="paper", uselist=False, cascade="all, delete-orphan") - bookmark = relationship("UserBookmark", back_populates="paper", uselist=False, cascade="all, delete-orphan") - reading_status = relationship("UserReadingStatus", back_populates="paper", uselist=False, cascade="all, delete-orphan") - note = relationship("UserNote", back_populates="paper", uselist=False, cascade="all, delete-orphan") + authors = relationship( + "PaperAuthor", back_populates="paper", cascade="all, delete-orphan" + ) + tags = relationship( + "PaperTag", back_populates="paper", cascade="all, delete-orphan" + ) + summary = relationship( + "PaperSummary", + back_populates="paper", + uselist=False, + cascade="all, delete-orphan", + ) + summary_status = relationship( + "SummaryStatus", + back_populates="paper", + uselist=False, + cascade="all, delete-orphan", + ) + bookmark = relationship( + "UserBookmark", + back_populates="paper", + uselist=False, + cascade="all, delete-orphan", + ) + reading_status = relationship( + "UserReadingStatus", + back_populates="paper", + uselist=False, + cascade="all, delete-orphan", + ) + note = relationship( + "UserNote", back_populates="paper", uselist=False, cascade="all, delete-orphan" + ) # ── paper_authors ─────────────────────────────────────────────────────── @@ -58,7 +84,9 @@ class PaperAuthor(Base): __table_args__ = (UniqueConstraint("paper_id", "name"),) id = Column(Integer, primary_key=True, autoincrement=True) - paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False) + paper_id = Column( + Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False + ) name = Column(String, nullable=False) position = Column(Integer, default=0) @@ -71,7 +99,9 @@ class PaperTag(Base): __table_args__ = (UniqueConstraint("paper_id", "tag", "source"),) id = Column(Integer, primary_key=True, autoincrement=True) - paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False) + paper_id = Column( + Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False + ) tag = Column(String, nullable=False) source = Column(String, default="hf") @@ -82,7 +112,9 @@ class PaperTag(Base): class PaperSummary(Base): __tablename__ = "paper_summaries" - paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), primary_key=True) + paper_id = Column( + Integer, ForeignKey("papers.id", ondelete="CASCADE"), primary_key=True + ) one_line = Column(Text) difficulty = Column(String) prerequisites_json = Column(Text) @@ -111,7 +143,9 @@ class SummaryStatus(Base): __table_args__ = (UniqueConstraint("paper_id"),) id = Column(Integer, primary_key=True, autoincrement=True) - paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False) + paper_id = Column( + Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False + ) status = Column(String, nullable=False, default="pending") quality = Column(String) error_type = Column(String) @@ -158,7 +192,9 @@ class UserBookmark(Base): __table_args__ = (UniqueConstraint("paper_id"),) id = Column(Integer, primary_key=True, autoincrement=True) - paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False) + paper_id = Column( + Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False + ) note = Column(Text) created_at = Column(DateTime, nullable=False) @@ -170,7 +206,9 @@ class UserReadingStatus(Base): __table_args__ = (UniqueConstraint("paper_id"),) id = Column(Integer, primary_key=True, autoincrement=True) - paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False) + paper_id = Column( + Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False + ) status = Column(String, nullable=False, default="unread") updated_at = Column(DateTime, nullable=False) @@ -182,7 +220,9 @@ class UserNote(Base): __table_args__ = (UniqueConstraint("paper_id"),) id = Column(Integer, primary_key=True, autoincrement=True) - paper_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False) + paper_id = Column( + Integer, ForeignKey("papers.id", ondelete="CASCADE"), nullable=False + ) content = Column(Text, nullable=False) created_at = Column(DateTime, nullable=False) updated_at = Column(DateTime, nullable=False) diff --git a/app/routes/admin.py b/app/routes/admin.py index bac16d2..8947c3a 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -74,7 +74,9 @@ async def admin_crawl( db.commit() except Exception: db.rollback() - raise HTTPException(status_code=409, detail=f"Crawl already running for {target_date}") + raise HTTPException( + status_code=409, detail=f"Crawl already running for {target_date}" + ) try: result = await crawl_daily(db, target_date) @@ -96,7 +98,9 @@ async def admin_summarize_batch( """批量总结所有 pending 论文。""" result = await summarize_batch(db) if result.get("status") == "conflict": - raise HTTPException(status_code=409, detail=result.get("error", "batch already running")) + raise HTTPException( + status_code=409, detail=result.get("error", "batch already running") + ) return result diff --git a/app/routes/pages.py b/app/routes/pages.py index b249046..4c12c8d 100644 --- a/app/routes/pages.py +++ b/app/routes/pages.py @@ -58,10 +58,13 @@ def day_page(date_str: str, request: Request, db: Session = Depends(get_db)): .limit(30) .all() ) - available_dates = [d[0].isoformat() if isinstance(d[0], date) else str(d[0]) for d in dates_raw] + available_dates = [ + d[0].isoformat() if isinstance(d[0], date) else str(d[0]) for d in dates_raw + ] return templates.TemplateResponse( - request, "index.html", + request, + "index.html", { "papers": papers, "current_date": date_str, @@ -105,7 +108,8 @@ def paper_detail(arxiv_id: str, request: Request, db: Session = Depends(get_db)) images = _get_paper_images(arxiv_id) return templates.TemplateResponse( - request, "detail.html", + request, + "detail.html", { "paper": paper, "summary_state": summary_state, @@ -166,7 +170,11 @@ def _get_similar_papers(db: Session, arxiv_id: str, top_k: int = 6) -> list[dict # 从 DB 加载论文信息 similar_ids = results["ids"][0] - distances = results["distances"][0] if results["distances"] else [0.0] * len(similar_ids) + distances = ( + results["distances"][0] + if results["distances"] + else [0.0] * len(similar_ids) + ) # 排除自身 papers_info = {} @@ -186,13 +194,15 @@ def _get_similar_papers(db: Session, arxiv_id: str, top_k: int = 6) -> list[dict items = [] for p in papers: - items.append({ - "arxiv_id": p.arxiv_id, - "title_zh": p.title_zh or p.title_en, - "distance": papers_info.get(p.arxiv_id, 0.0), - "paper_date": p.paper_date.isoformat() if p.paper_date else "", - "tags": [t.tag for t in p.tags[:3]], - }) + items.append( + { + "arxiv_id": p.arxiv_id, + "title_zh": p.title_zh or p.title_en, + "distance": papers_info.get(p.arxiv_id, 0.0), + "paper_date": p.paper_date.isoformat() if p.paper_date else "", + "tags": [t.tag for t in p.tags[:3]], + } + ) # 按距离排序 items.sort(key=lambda x: x["distance"]) @@ -215,8 +225,10 @@ def _get_paper_images(arxiv_id: str) -> list[dict]: images = [] for img_file in sorted(images_dir.iterdir()): if img_file.suffix.lower() in (".png", ".jpg", ".jpeg", ".gif", ".svg"): - images.append({ - "url": f"/papers/{arxiv_id}/images/{img_file.name}", - "name": img_file.name, - }) + images.append( + { + "url": f"/papers/{arxiv_id}/images/{img_file.name}", + "name": img_file.name, + } + ) return images diff --git a/app/routes/search.py b/app/routes/search.py index 0ec43be..df4bbf4 100644 --- a/app/routes/search.py +++ b/app/routes/search.py @@ -34,7 +34,9 @@ def search_page( db: Session = Depends(get_db), ): """搜索页面,支持 keyword 和 semantic 模式。""" - result = search_papers(db, query=q or None, tag=tag or None, sort=sort, page=page, mode=mode) + result = search_papers( + db, query=q or None, tag=tag or None, sort=sort, page=page, mode=mode + ) all_tags = get_all_tags(db) return templates.TemplateResponse( @@ -72,7 +74,9 @@ def search_api( db: Session = Depends(get_db), ): """搜索 JSON API,支持 keyword 和 semantic 模式。""" - result = search_papers(db, query=q or None, tag=tag or None, sort=sort, page=page, mode=mode) + result = search_papers( + db, query=q or None, tag=tag or None, sort=sort, page=page, mode=mode + ) distances = result.get("distances", {}) items = [] @@ -170,7 +174,9 @@ def _generate_rss_xml(papers: list[Paper], base_url: str, tag: str | None) -> st channel_title += f" — {tag}" lines.append(f" {escape(channel_title)}") lines.append(f" {escape(base_url)}") - lines.append(" HuggingFace Daily Papers — 中文论文导览站") + lines.append( + " HuggingFace Daily Papers — 中文论文导览站" + ) lines.append(" zh-CN") for paper in papers: diff --git a/app/services/cleaner.py b/app/services/cleaner.py index 5916a43..414b6a9 100644 --- a/app/services/cleaner.py +++ b/app/services/cleaner.py @@ -61,7 +61,9 @@ def cleanup_tmp(max_age_hours: int = _MAX_TMP_AGE_HOURS) -> dict: errors.append(err_msg) logger.warning("Failed to clean tmp dir %s: %s", entry.name, exc) - logger.info("Tmp cleanup: scanned=%d removed=%d errors=%d", scanned, removed, len(errors)) + logger.info( + "Tmp cleanup: scanned=%d removed=%d errors=%d", scanned, removed, len(errors) + ) return {"scanned": scanned, "removed": removed, "errors": errors} @@ -109,7 +111,12 @@ async def delete_papers_by_date_range( ) total = len(papers) - logger.info("Delete papers by date range: %s ~ %s, found %d papers", date_start, date_end, total) + logger.info( + "Delete papers by date range: %s ~ %s, found %d papers", + date_start, + date_end, + total, + ) # 创建 delete job 记录 job = DataDeleteJob( @@ -139,9 +146,12 @@ async def delete_papers_by_date_range( # 1.5 Phase 5: 从 ChromaDB 删除语义索引 try: from app.services.embedder import delete_paper + delete_paper(arxiv_id) except Exception: - logger.warning("Failed to delete %s from ChromaDB", arxiv_id, exc_info=True) + logger.warning( + "Failed to delete %s from ChromaDB", arxiv_id, exc_info=True + ) # 2. 删除本地文件 data/papers/{arxiv_id}/ paper_dir = PAPERS_DIR / arxiv_id @@ -179,7 +189,9 @@ async def delete_papers_by_date_range( job_status = "success" if failed_items: job_status = "failed" if deleted == 0 else "success" - job_error = "; ".join(f"{f['arxiv_id']}: {f['error']}" for f in failed_items[:20]) + job_error = "; ".join( + f"{f['arxiv_id']}: {f['error']}" for f in failed_items[:20] + ) job.status = job_status job.paper_count = deleted @@ -210,6 +222,10 @@ async def delete_papers_by_date_range( } logger.info( "Delete job completed: date_range=%s~%s total=%d deleted=%d failed=%d", - date_start, date_end, total, deleted, len(failed_items), + date_start, + date_end, + total, + deleted, + len(failed_items), ) return result diff --git a/app/services/crawler.py b/app/services/crawler.py index 371c90e..33897e2 100644 --- a/app/services/crawler.py +++ b/app/services/crawler.py @@ -38,20 +38,29 @@ async def fetch_daily(target_date: str, top_n: int | None = None) -> list[dict]: async with make_http_client() as client: for attempt in range(1, settings.HTTP_MAX_RETRIES + 1): try: - logger.info("Fetching HF Daily Papers: date=%s attempt=%d", target_date, attempt) + logger.info( + "Fetching HF Daily Papers: date=%s attempt=%d", target_date, attempt + ) resp = await client.get(url, params=params) resp.raise_for_status() data = resp.json() break except (httpx.HTTPError, httpx.HTTPStatusError) as exc: - logger.warning("Fetch failed (attempt %d/%d): %s", attempt, settings.HTTP_MAX_RETRIES, exc) + logger.warning( + "Fetch failed (attempt %d/%d): %s", + attempt, + settings.HTTP_MAX_RETRIES, + exc, + ) if attempt == settings.HTTP_MAX_RETRIES: raise else: data = [] papers = data[:top_n] - logger.info("Fetched %d papers for %s (raw=%d)", len(papers), target_date, len(data)) + logger.info( + "Fetched %d papers for %s (raw=%d)", len(papers), target_date, len(data) + ) return papers @@ -75,8 +84,14 @@ def _parse_paper(item: dict) -> dict: "hf_url": f"https://huggingface.co/papers/{arxiv_id}" if arxiv_id else "", "arxiv_url": f"https://arxiv.org/abs/{arxiv_id}" if arxiv_id else "", "pdf_url": f"https://arxiv.org/pdf/{arxiv_id}.pdf" if arxiv_id else "", - "authors": [a.get("name", a) if isinstance(a, dict) else a for a in paper_info.get("authors", [])], - "tags": [t.get("name", t) if isinstance(t, dict) else t for t in (paper_info.get("tags") or [])], + "authors": [ + a.get("name", a) if isinstance(a, dict) else a + for a in paper_info.get("authors", []) + ], + "tags": [ + t.get("name", t) if isinstance(t, dict) else t + for t in (paper_info.get("tags") or []) + ], } @@ -133,15 +148,25 @@ def upsert_papers(db: Session, papers_raw: list[dict], paper_date: str) -> list[ "INSERT INTO papers_fts(rowid, title_en, abstract, authors, tags) " "VALUES (:id, :title, :abstract, :authors, :tags)" ), - {"id": paper.id, "title": meta["title_en"], "abstract": meta["abstract"] or "", - "authors": authors_text, "tags": tags_text}, + { + "id": paper.id, + "title": meta["title_en"], + "abstract": meta["abstract"] or "", + "authors": authors_text, + "tags": tags_text, + }, ) new_papers.append(paper) logger.debug("Inserted new paper: %s", arxiv_id) db.commit() - logger.info("Upserted %d papers (%d new) for %s", len(papers_raw), len(new_papers), paper_date) + logger.info( + "Upserted %d papers (%d new) for %s", + len(papers_raw), + len(new_papers), + paper_date, + ) return new_papers @@ -165,7 +190,12 @@ async def crawl_daily(db: Session, target_date: str, top_n: int | None = None) - log_entry.papers_new = len(new_papers) log_entry.completed_at = datetime.now(timezone.utc) db.commit() - return {"found": len(raw_papers), "new": len(new_papers), "status": "success", "error": None} + return { + "found": len(raw_papers), + "new": len(new_papers), + "status": "success", + "error": None, + } except Exception as exc: logger.exception("Crawl failed for %s", target_date) log_entry.status = "failed" diff --git a/app/services/embedder.py b/app/services/embedder.py index 015a288..698931b 100644 --- a/app/services/embedder.py +++ b/app/services/embedder.py @@ -50,7 +50,9 @@ class ChromaManager: """获取或创建 papers_embeddings collection。""" try: col = self._client.get_collection("papers_embeddings") - logger.info("ChromaDB collection 'papers_embeddings' loaded, count=%d", col.count()) + logger.info( + "ChromaDB collection 'papers_embeddings' loaded, count=%d", col.count() + ) return col except Exception: pass @@ -228,7 +230,9 @@ def index_paper(paper_id: str, texts_dict: dict | None = None) -> bool: col.upsert( ids=[arxiv_id], embeddings=[vec], - metadatas=[{"arxiv_id": arxiv_id, "title_zh": title_zh, "paper_date": paper_date}], + metadatas=[ + {"arxiv_id": arxiv_id, "title_zh": title_zh, "paper_date": paper_date} + ], ) logger.info("Indexed paper %s in ChromaDB", arxiv_id) return True @@ -262,7 +266,9 @@ def index_batch(paper_ids: list[str]) -> dict: else: failed += 1 - logger.info("Batch index: total=%d success=%d failed=%d", len(paper_ids), success, failed) + logger.info( + "Batch index: total=%d success=%d failed=%d", len(paper_ids), success, failed + ) return {"total": len(paper_ids), "success": success, "failed": failed} diff --git a/app/services/pdf_downloader.py b/app/services/pdf_downloader.py index cfb0d5a..6db50d0 100644 --- a/app/services/pdf_downloader.py +++ b/app/services/pdf_downloader.py @@ -78,6 +78,7 @@ async def download_source_zip(arxiv_id: str, source_url: str, dest_dir: Path) -> except zipfile.BadZipFile: # 可能是 tar.gz import tarfile + try: with tarfile.open(zip_path, "r:*") as tf: tf.extractall(dest_dir, filter="data") diff --git a/app/services/pi_client.py b/app/services/pi_client.py index b7aae2b..b51b576 100644 --- a/app/services/pi_client.py +++ b/app/services/pi_client.py @@ -53,7 +53,9 @@ def write_meta_json(paper) -> Path: "tags": tags, "upvotes": paper.upvotes, } - meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8") + meta_path.write_text( + json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8" + ) return meta_path @@ -88,9 +90,7 @@ async def call_pi(meta_path: Path, pdf_path: Path) -> str: except asyncio.TimeoutError: proc.kill() await proc.wait() - raise PiTimeoutError( - f"pi timed out after {settings.SUMMARY_TIMEOUT_SECONDS}s" - ) + raise PiTimeoutError(f"pi timed out after {settings.SUMMARY_TIMEOUT_SECONDS}s") if proc.returncode != 0: raise PiProcessError(proc.returncode, stderr.decode("utf-8", errors="replace")) diff --git a/app/services/scheduler.py b/app/services/scheduler.py index 9f80c7b..f0915ff 100644 --- a/app/services/scheduler.py +++ b/app/services/scheduler.py @@ -132,18 +132,26 @@ async def _daily_pipeline() -> None: # Step 1: 抓取 logger.info("Scheduler pipeline: crawl %s", today) crawl_result = await crawl_daily(db, today) - logger.info("Scheduler pipeline: crawl done, found=%d new=%d", - crawl_result.get("found", 0), crawl_result.get("new", 0)) + logger.info( + "Scheduler pipeline: crawl done, found=%d new=%d", + crawl_result.get("found", 0), + crawl_result.get("new", 0), + ) # Step 2: 总结 pending 论文 logger.info("Scheduler pipeline: summarize batch") summarize_result = await summarize_batch(db) - logger.info("Scheduler pipeline: summarize done, result=%s", summarize_result) + logger.info( + "Scheduler pipeline: summarize done, result=%s", summarize_result + ) # Step 3: 清理临时文件 logger.info("Scheduler pipeline: cleanup tmp") cleanup_result = cleanup_tmp() - logger.info("Scheduler pipeline: cleanup done, removed=%d", cleanup_result.get("removed", 0)) + logger.info( + "Scheduler pipeline: cleanup done, removed=%d", + cleanup_result.get("removed", 0), + ) log_entry.status = "success" diff --git a/app/services/schemas.py b/app/services/schemas.py index 2b2838b..9dd8fd3 100644 --- a/app/services/schemas.py +++ b/app/services/schemas.py @@ -132,7 +132,9 @@ def flatten_for_db(schema: SummarySchema) -> dict: return { "one_line": schema.one_line, "difficulty": schema.difficulty, - "prerequisites_json": json.dumps(schema.prerequisites.model_dump(), ensure_ascii=False), + "prerequisites_json": json.dumps( + schema.prerequisites.model_dump(), ensure_ascii=False + ), "motivation_problem": schema.motivation.problem, "motivation_goal": schema.motivation.goal, "motivation_gap": schema.motivation.gap, @@ -140,11 +142,19 @@ def flatten_for_db(schema: SummarySchema) -> dict: "method_key_idea": schema.method.key_idea, "method_steps_json": json.dumps(schema.method.steps, ensure_ascii=False), "method_novelty": schema.method.novelty, - "results_main_json": json.dumps(schema.results.main_findings, ensure_ascii=False), - "results_benchmarks_json": json.dumps(schema.results.benchmarks, ensure_ascii=False), + "results_main_json": json.dumps( + schema.results.main_findings, ensure_ascii=False + ), + "results_benchmarks_json": json.dumps( + schema.results.benchmarks, ensure_ascii=False + ), "limitations_json": json.dumps(schema.results.limitations, ensure_ascii=False), - "weaknesses_json": json.dumps(schema.improvements.weaknesses, ensure_ascii=False), - "future_work_json": json.dumps(schema.improvements.future_work, ensure_ascii=False), + "weaknesses_json": json.dumps( + schema.improvements.weaknesses, ensure_ascii=False + ), + "future_work_json": json.dumps( + schema.improvements.future_work, ensure_ascii=False + ), "reproducibility": schema.improvements.reproducibility, "full_json": schema.model_dump_json(ensure_ascii=False), "updated_at": datetime.now(timezone.utc), diff --git a/app/services/searcher.py b/app/services/searcher.py index 9dd88a1..7cf1385 100644 --- a/app/services/searcher.py +++ b/app/services/searcher.py @@ -90,12 +90,24 @@ def search_papers( if match_expr: return _search_with_fts( - db, match_expr, tag_join, tag_where, tag_params, - sort, page, page_size, offset, + db, + match_expr, + tag_join, + tag_where, + tag_params, + sort, + page, + page_size, + offset, ) else: return _search_tag_only( - db, tag, sort, page, page_size, offset, + db, + tag, + sort, + page, + page_size, + offset, ) @@ -114,7 +126,11 @@ def _search_with_fts( params = {"query": match_expr, "limit": page_size, "offset": offset} params.update(tag_params) - order = "bm25(papers_fts)" if sort == "relevance" else "p.paper_date DESC, p.upvotes DESC" + order = ( + "bm25(papers_fts)" + if sort == "relevance" + else "p.paper_date DESC, p.upvotes DESC" + ) # ── 主查询:取 ID + rank + snippet ── rows_sql = text(f""" @@ -145,12 +161,11 @@ def _search_with_fts( total = db.execute(count_sql, params).scalar() or 0 paper_ids = [row[0] for row in fts_rows] - snippets = { - row[0]: {"title_zh": row[2], "abstract": row[3]} - for row in fts_rows - } + snippets = {row[0]: {"title_zh": row[2], "abstract": row[3]} for row in fts_rows} - papers = _load_papers_by_ids(db, paper_ids, sort, {row[0]: row[1] for row in fts_rows}) + papers = _load_papers_by_ids( + db, paper_ids, sort, {row[0]: row[1] for row in fts_rows} + ) return { "results": papers, @@ -188,7 +203,10 @@ def _search_semantic( "JOIN paper_tags pt ON pt.paper_id = p.id" if tag else "", "AND pt.tag = :tag" if tag else "", {"tag": tag} if tag else {}, - sort, page, page_size, (page - 1) * page_size, + sort, + page, + page_size, + (page - 1) * page_size, ) # 按 arxiv_id 从 DB 加载完整数据 @@ -218,7 +236,7 @@ def _search_semantic( # 分页 total = len(papers) start = (page - 1) * page_size - page_papers = papers[start:start + page_size] + page_papers = papers[start : start + page_size] return { "results": page_papers, @@ -239,7 +257,11 @@ def _search_tag_only( offset: int, ) -> dict: """只有标签筛选,无关键词。""" - order = "p.paper_date DESC, p.upvotes DESC" if sort == "date" else "p.paper_date DESC, p.upvotes DESC" + order = ( + "p.paper_date DESC, p.upvotes DESC" + if sort == "date" + else "p.paper_date DESC, p.upvotes DESC" + ) rows_sql = text(f""" SELECT p.id @@ -249,7 +271,9 @@ def _search_tag_only( ORDER BY {order} LIMIT :limit OFFSET :offset """) - rows = db.execute(rows_sql, {"tag": tag, "limit": page_size, "offset": offset}).fetchall() + rows = db.execute( + rows_sql, {"tag": tag, "limit": page_size, "offset": offset} + ).fetchall() count_sql = text(""" SELECT COUNT(DISTINCT p.id) diff --git a/app/services/summarizer.py b/app/services/summarizer.py index b3450d8..94d70d5 100644 --- a/app/services/summarizer.py +++ b/app/services/summarizer.py @@ -191,7 +191,11 @@ async def summarize_one( # 跳过 permanent_failure(除非 force) if status.status == "permanent_failure" and not force: - return {"arxiv_id": arxiv_id, "status": "skipped", "reason": "permanent_failure"} + return { + "arxiv_id": arxiv_id, + "status": "skipped", + "reason": "permanent_failure", + } if semaphore: await semaphore.acquire() @@ -270,7 +274,9 @@ async def _do_summarize_one(db: Session, paper: Paper) -> dict: } index_paper(arxiv_id, texts_dict) except Exception: - logger.warning("Failed to index paper %s in ChromaDB", arxiv_id, exc_info=True) + logger.warning( + "Failed to index paper %s in ChromaDB", arxiv_id, exc_info=True + ) logger.info("Summarize done: %s quality=%s", arxiv_id, quality) return {"arxiv_id": arxiv_id, "status": "done", "quality": quality} @@ -430,7 +436,13 @@ async def summarize_batch( log_entry.papers_new = 0 log_entry.completed_at = datetime.now(timezone.utc) release_lock(db, lock) - return {"status": "success", "done": 0, "failed": 0, "skipped": 0, "total": 0} + return { + "status": "success", + "done": 0, + "failed": 0, + "skipped": 0, + "total": 0, + } # 并发控制 semaphore = asyncio.Semaphore(settings.SUMMARY_CONCURRENCY) @@ -482,7 +494,10 @@ async def summarize_batch( logger.info( "Summarize batch done: total=%d done=%d failed=%d skipped=%d", - total, done, failed, skipped, + total, + done, + failed, + skipped, ) return { "status": "success" if failed == 0 else "partial", diff --git a/app/services/trends.py b/app/services/trends.py index 51e7824..e460092 100644 --- a/app/services/trends.py +++ b/app/services/trends.py @@ -13,33 +13,33 @@ def get_trends_data(db: Session) -> dict: thirty_days_ago = (date.today() - timedelta(days=30)).isoformat() # 1. 按日论文数量(近 30 天) - daily_rows = db.execute(text(""" + daily_rows = db.execute( + text(""" SELECT paper_date, COUNT(*) as cnt FROM papers WHERE paper_date >= :start_date GROUP BY paper_date ORDER BY paper_date ASC - """), {"start_date": thirty_days_ago}).fetchall() - daily_counts = [ - {"date": str(row[0]), "count": row[1]} - for row in daily_rows - ] + """), + {"start_date": thirty_days_ago}, + ).fetchall() + daily_counts = [{"date": str(row[0]), "count": row[1]} for row in daily_rows] # 2. 热门标签 Top 20 - tag_rows = db.execute(text(""" + tag_rows = db.execute( + text(""" SELECT tag, COUNT(*) as cnt FROM paper_tags GROUP BY tag ORDER BY cnt DESC LIMIT 20 - """)).fetchall() - top_tags = [ - {"tag": row[0], "count": row[1]} - for row in tag_rows - ] + """) + ).fetchall() + top_tags = [{"tag": row[0], "count": row[1]} for row in tag_rows] # 3. Upvotes 分布 - upvote_rows = db.execute(text(""" + upvote_rows = db.execute( + text(""" SELECT CASE WHEN upvotes >= 100 THEN '100+' @@ -53,25 +53,22 @@ def get_trends_data(db: Session) -> dict: FROM papers GROUP BY bucket ORDER BY MIN(upvotes) DESC - """)).fetchall() - upvotes_dist = [ - {"range": row[0], "count": row[1]} - for row in upvote_rows - ] + """) + ).fetchall() + upvotes_dist = [{"range": row[0], "count": row[1]} for row in upvote_rows] # 4. 总结完成率 - summary_rows = db.execute(text(""" + summary_rows = db.execute( + text(""" SELECT COALESCE(ss.status, 'none') as status, COUNT(*) as cnt FROM papers p LEFT JOIN summary_status ss ON ss.paper_id = p.id GROUP BY status - """)).fetchall() - summary_completion = [ - {"status": row[0], "count": row[1]} - for row in summary_rows - ] + """) + ).fetchall() + summary_completion = [{"status": row[0], "count": row[1]} for row in summary_rows] return { "daily_counts": daily_counts, diff --git a/app/static/css/style.css b/app/static/css/style.css index a8f85d1..5c16b04 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -14,7 +14,13 @@ --max-width: 960px; } -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} body { font-family: var(--font-sans); @@ -24,8 +30,14 @@ body { -webkit-font-smoothing: antialiased; } -a { color: var(--accent); text-decoration: none; } -a:hover { color: var(--accent-hover); text-decoration: underline; } +a { + color: var(--accent); + text-decoration: none; +} +a:hover { + color: var(--accent-hover); + text-decoration: underline; +} /* ── Header ─────────────────────────────────────────────────────── */ .site-header { @@ -52,9 +64,18 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } color: var(--ink); } -.nav-links { display: flex; gap: 16px; margin-left: auto; } -.nav-links a { font-size: 0.9rem; color: var(--ink-light); } -.nav-links a:hover { color: var(--accent); } +.nav-links { + display: flex; + gap: 16px; + margin-left: auto; +} +.nav-links a { + font-size: 0.9rem; + color: var(--ink-light); +} +.nav-links a:hover { + color: var(--accent); +} /* ── Container ──────────────────────────────────────────────────── */ .container { @@ -88,7 +109,11 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } color: var(--ink-light); transition: all 0.2s; } -.date-nav-btn:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; } +.date-nav-btn:hover { + border-color: var(--accent); + color: var(--accent); + text-decoration: none; +} /* ── Date Chips ─────────────────────────────────────────────────── */ .date-quick-nav { @@ -111,11 +136,23 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } font-size: 0.8rem; color: var(--ink-light); } -.date-chip:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; } -.date-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); } +.date-chip:hover { + border-color: var(--accent); + color: var(--accent); + text-decoration: none; +} +.date-chip.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} /* ── Paper Card ─────────────────────────────────────────────────── */ -.paper-list { display: flex; flex-direction: column; gap: 16px; } +.paper-list { + display: flex; + flex-direction: column; + gap: 16px; +} .paper-card { background: var(--surface); @@ -124,7 +161,9 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } padding: 20px 24px; transition: box-shadow 0.2s; } -.paper-card:hover { box-shadow: 0 2px 12px var(--shadow); } +.paper-card:hover { + box-shadow: 0 2px 12px var(--shadow); +} .paper-card-header { display: flex; @@ -140,8 +179,12 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } line-height: 1.5; flex: 1; } -.paper-title a { color: var(--ink); } -.paper-title a:hover { color: var(--accent); } +.paper-title a { + color: var(--ink); +} +.paper-title a:hover { + color: var(--accent); +} .paper-upvotes { font-size: 0.85rem; @@ -149,7 +192,8 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } white-space: nowrap; } -.paper-one-line, .paper-abstract-preview { +.paper-one-line, +.paper-abstract-preview { margin-top: 8px; color: var(--ink-light); font-size: 0.92rem; @@ -191,11 +235,27 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } padding: 2px 8px; border-radius: 3px; } -.summary-none { background: #f0f0f0; color: #888; } -.summary-pending { background: #fff3e0; color: #e67e22; } -.summary-processing { background: #e3f2fd; color: #1976d2; } -.summary-done { background: #e8f5e9; color: #388e3c; } -.summary-failed, .summary-permanent_failure { background: #fce4ec; color: #c62828; } +.summary-none { + background: #f0f0f0; + color: #888; +} +.summary-pending { + background: #fff3e0; + color: #e67e22; +} +.summary-processing { + background: #e3f2fd; + color: #1976d2; +} +.summary-done { + background: #e8f5e9; + color: #388e3c; +} +.summary-failed, +.summary-permanent_failure { + background: #fce4ec; + color: #c62828; +} .btn-detail { font-size: 0.85rem; @@ -209,11 +269,19 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } padding: 60px 20px; color: var(--ink-light); } -.empty-state p:first-child { font-size: 1.2rem; } -.hint { font-size: 0.85rem; margin-top: 8px; } +.empty-state p:first-child { + font-size: 1.2rem; +} +.hint { + font-size: 0.85rem; + margin-top: 8px; +} /* ── Paper Detail ───────────────────────────────────────────────── */ -.paper-detail { max-width: 780px; margin: 0 auto; } +.paper-detail { + max-width: 780px; + margin: 0 auto; +} .back-link { display: inline-block; @@ -246,7 +314,12 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } margin-bottom: 12px; } -.detail-tags { margin-bottom: 12px; display: flex; gap: 6px; flex-wrap: wrap; } +.detail-tags { + margin-bottom: 12px; + display: flex; + gap: 6px; + flex-wrap: wrap; +} .detail-links { display: flex; @@ -261,7 +334,11 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } font-size: 0.85rem; color: var(--ink-light); } -.ext-link:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; } +.ext-link:hover { + border-color: var(--accent); + color: var(--accent); + text-decoration: none; +} /* ── Summary Sections ───────────────────────────────────────────── */ .summary-section { @@ -291,8 +368,14 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } line-height: 1.6; } -.abstract-section { background: #faf8f5; } -.abstract-en { font-size: 0.9rem; color: var(--ink-light); font-style: italic; } +.abstract-section { + background: #faf8f5; +} +.abstract-en { + font-size: 0.9rem; + color: var(--ink-light); + font-style: italic; +} /* ── Summary Placeholders ───────────────────────────────────────── */ .summary-placeholder { @@ -301,10 +384,20 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } border-radius: var(--radius); margin-bottom: 24px; } -.summary-placeholder.processing { background: #e3f2fd; } -.summary-placeholder.failed { background: #fce4ec; } -.summary-placeholder.none { background: #f5f5f5; } -.error-detail { font-size: 0.85rem; color: #c62828; margin-top: 8px; } +.summary-placeholder.processing { + background: #e3f2fd; +} +.summary-placeholder.failed { + background: #fce4ec; +} +.summary-placeholder.none { + background: #f5f5f5; +} +.error-detail { + font-size: 0.85rem; + color: #c62828; + margin-top: 8px; +} .quality-warning { padding: 10px 16px; @@ -384,7 +477,9 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } cursor: pointer; transition: background 0.2s; } -.search-btn:hover { background: var(--accent-hover); } +.search-btn:hover { + background: var(--accent-hover); +} /* ── Tag Filter ─────────────────────────────────────────────────── */ .tag-filter { @@ -407,8 +502,16 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } font-size: 0.8rem; color: var(--ink-light); } -.tag-chip:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; } -.tag-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); } +.tag-chip:hover { + border-color: var(--accent); + color: var(--accent); + text-decoration: none; +} +.tag-chip.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} /* ── Search Meta & Sort ─────────────────────────────────────────── */ .search-meta { @@ -423,9 +526,18 @@ a:hover { color: var(--accent-hover); text-decoration: underline; } color: var(--ink-light); font-size: 0.85rem; } -.sort-toggle a.active { color: var(--accent); font-weight: 600; } -.sort-toggle a:hover { color: var(--accent); text-decoration: none; } -.sort-divider { color: var(--border); margin: 0 4px; } +.sort-toggle a.active { + color: var(--accent); + font-weight: 600; +} +.sort-toggle a:hover { + color: var(--accent); + text-decoration: none; +} +.sort-divider { + color: var(--border); + margin: 0 4px; +} /* ── Search Highlight ───────────────────────────────────────────── */ mark { @@ -464,7 +576,11 @@ mark { font-size: 0.85rem; color: var(--ink-light); } -.page-btn:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; } +.page-btn:hover { + border-color: var(--accent); + color: var(--accent); + text-decoration: none; +} .page-info { font-size: 0.85rem; color: var(--ink-light); @@ -494,8 +610,16 @@ mark { font-size: 0.85rem; color: var(--ink-light); } -.filter-chip:hover { border-color: var(--accent); color: var(--accent); text-decoration: none; } -.filter-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); } +.filter-chip:hover { + border-color: var(--accent); + color: var(--accent); + text-decoration: none; +} +.filter-chip.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} /* ── Paper Card Footer (enhanced) ──────────────────────────────── */ .paper-footer { @@ -528,8 +652,12 @@ mark { transition: color 0.2s; line-height: 1; } -.btn-bookmark:hover { color: var(--accent); } -.btn-bookmark.active { color: #f0a500; } +.btn-bookmark:hover { + color: var(--accent); +} +.btn-bookmark.active { + color: #f0a500; +} /* ── Reading Badge ──────────────────────────────────────────────── */ .reading-badge { @@ -537,24 +665,61 @@ mark { padding: 2px 6px; border-radius: 3px; } -.reading-unread { background: #f0f0f0; color: #888; } -.reading-skimmed { background: #e3f2fd; color: #1976d2; } -.reading-read_summary { background: #e8f5e9; color: #388e3c; } -.reading-read_full { background: #e8f5e9; color: #2e7d32; font-weight: 500; } +.reading-unread { + background: #f0f0f0; + color: #888; +} +.reading-skimmed { + background: #e3f2fd; + color: #1976d2; +} +.reading-read_summary { + background: #e8f5e9; + color: #388e3c; +} +.reading-read_full { + background: #e8f5e9; + color: #2e7d32; + font-weight: 500; +} /* ── Responsive ─────────────────────────────────────────────────── */ @media (max-width: 640px) { - .container { padding: 16px; } - .nav-bar { padding: 10px 16px; } - .nav-search-input { width: 120px; } - .date-nav { gap: 8px; } - .date-title { font-size: 1.2rem; } - .paper-card { padding: 14px 16px; } - .detail-title { font-size: 1.3rem; } - .detail-meta { flex-direction: column; gap: 4px; } - .search-form { flex-direction: column; } - .reading-list-filters { gap: 4px; } - .filter-chip { padding: 4px 10px; font-size: 0.8rem; } + .container { + padding: 16px; + } + .nav-bar { + padding: 10px 16px; + } + .nav-search-input { + width: 120px; + } + .date-nav { + gap: 8px; + } + .date-title { + font-size: 1.2rem; + } + .paper-card { + padding: 14px 16px; + } + .detail-title { + font-size: 1.3rem; + } + .detail-meta { + flex-direction: column; + gap: 4px; + } + .search-form { + flex-direction: column; + } + .reading-list-filters { + gap: 4px; + } + .filter-chip { + padding: 4px 10px; + font-size: 0.8rem; + } } /* ── Search Mode Toggle (Phase 5) ─────────────────────────────── */ @@ -575,8 +740,12 @@ mark { align-items: center; gap: 4px; } -.mode-option input[type="radio"] { display: none; } -.mode-option:hover { background: var(--bg); } +.mode-option input[type="radio"] { + display: none; +} +.mode-option:hover { + background: var(--bg); +} .mode-option.active { background: var(--accent); color: #fff; @@ -612,12 +781,16 @@ mark { padding: 10px 0; border-bottom: 1px solid var(--border); } -.similar-paper-item:last-child { border-bottom: none; } +.similar-paper-item:last-child { + border-bottom: none; +} .similar-paper-title a { font-size: 0.92rem; color: var(--ink); } -.similar-paper-title a:hover { color: var(--accent); } +.similar-paper-title a:hover { + color: var(--accent); +} .similar-paper-dist { font-size: 0.8rem; color: var(--ink-light); @@ -654,7 +827,9 @@ mark { max-height: 300px; } @media (max-width: 768px) { - .charts-grid { grid-template-columns: 1fr; } + .charts-grid { + grid-template-columns: 1fr; + } } /* ── Compare Page (Phase 5) ────────────────────────────────────── */ diff --git a/app/templates/admin_logs.html b/app/templates/admin_logs.html index ce14513..805ee8b 100644 --- a/app/templates/admin_logs.html +++ b/app/templates/admin_logs.html @@ -1,8 +1,5 @@ -{% extends "base.html" %} - -{% block title %}管理日志 — HF Daily Papers{% endblock %} - -{% block content %} +{% extends "base.html" %} {% block title %}管理日志 — HF Daily Papers{% endblock +%} {% block content %}

📋 管理日志

@@ -34,21 +31,31 @@ {% for log in crawl_logs %} {{ log.id }} - {{ log.task }} + + {{ log.task }} + - {% if log.status == 'success' %}✓ 成功 - {% elif log.status == 'running' %}⟳ 运行中 - {% elif log.status == 'failed' %}✗ 失败 - {% else %}{{ log.status }}{% endif %} + {% if log.status == 'success' %}✓ 成功 {% elif log.status == + 'running' %}⟳ 运行中 {% elif log.status == 'failed' %}✗ 失败 {% + else %}{{ log.status }}{% endif %} {{ log.date or '-' }} {{ log.papers_found or 0 }} {{ log.papers_new or 0 }} - {{ log.started_at.strftime('%m-%d %H:%M') if log.started_at else '-' }} - {{ log.completed_at.strftime('%m-%d %H:%M') if log.completed_at else '-' }} - {{ log.error[:80] + '...' if log.error and log.error|length > 80 else (log.error or '-') }} + + {{ log.started_at.strftime('%m-%d %H:%M') if log.started_at else + '-' }} + + + {{ log.completed_at.strftime('%m-%d %H:%M') if log.completed_at + else '-' }} + + + {{ log.error[:80] + '...' if log.error and log.error|length > 80 + else (log.error or '-') }} + {% endfor %} @@ -90,15 +97,23 @@ {{ job.paper_count or 0 }} - {% if job.status == 'success' %}✓ 成功 - {% elif job.status == 'running' %}⟳ 运行中 - {% elif job.status == 'failed' %}✗ 失败 - {% else %}{{ job.status }}{% endif %} + {% if job.status == 'success' %}✓ 成功 {% elif job.status == + 'running' %}⟳ 运行中 {% elif job.status == 'failed' %}✗ 失败 {% + else %}{{ job.status }}{% endif %} - {{ job.started_at.strftime('%m-%d %H:%M') if job.started_at else '-' }} - {{ job.completed_at.strftime('%m-%d %H:%M') if job.completed_at else '-' }} - {{ job.error[:80] + '...' if job.error and job.error|length > 80 else (job.error or '-') }} + + {{ job.started_at.strftime('%m-%d %H:%M') if job.started_at else + '-' }} + + + {{ job.completed_at.strftime('%m-%d %H:%M') if job.completed_at + else '-' }} + + + {{ job.error[:80] + '...' if job.error and job.error|length > 80 + else (job.error or '-') }} + {% endfor %} @@ -116,16 +131,24 @@

管理操作

- - - + + +
-{% endblock %} - -{% block scripts %} +{% endblock %} {% block scripts %} {% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index 6e272f9..aab7fe7 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,38 +1,44 @@ - + - - - - {% block title %}HF Daily Papers{% endblock %} - - - - + + + + {% block title %}HF Daily Papers{% endblock %} + + + + -
- {% block content %}{% endblock %} -
+
{% block content %}{% endblock %}
- + - - - {% block scripts %}{% endblock %} - + + + {% block scripts %}{% endblock %} + diff --git a/app/templates/compare.html b/app/templates/compare.html index c59a934..0d9f6d0 100644 --- a/app/templates/compare.html +++ b/app/templates/compare.html @@ -1,16 +1,17 @@ -{% extends "base.html" %} - -{% block title %}{{ page_title }} — HF Daily Papers{% endblock %} - -{% block content %} +{% extends "base.html" %} {% block title %}{{ page_title }} — HF Daily Papers{% +endblock %} {% block content %}

论文对比

{# ID 输入表单 #}
- +
@@ -18,9 +19,7 @@

{{ error }}

- {% endif %} - - {% if papers %} + {% endif %} {% if papers %}
@@ -29,8 +28,8 @@ {% for paper in papers %} @@ -42,7 +41,9 @@ {% for paper in papers %} - + {% endfor %} @@ -58,16 +59,13 @@ {% endfor %} - {# 结构化对比字段 #} - {% for row in rows %} + {# 结构化对比字段 #} {% for row in rows %} {% for cell in row.cells %} {% endfor %} diff --git a/app/templates/detail.html b/app/templates/detail.html index 3979d6d..255ae99 100644 --- a/app/templates/detail.html +++ b/app/templates/detail.html @@ -1,140 +1,141 @@ -{% extends "base.html" %} - -{% block title %}{{ page_title }} — HF Daily Papers{% endblock %} - -{% block content %} +{% extends "base.html" %} {% block title %}{{ page_title }} — HF Daily Papers{% +endblock %} {% block content %}
- ← 返回 {{ paper.paper_date.isoformat() }} + ← 返回 {{ paper.paper_date.isoformat() }} {# 标题 #}

- {{ paper.title_zh or paper.title_en }} - {% if paper.title_zh and paper.title_en != paper.title_zh %} + {{ paper.title_zh or paper.title_en }} {% if paper.title_zh and + paper.title_en != paper.title_zh %} {{ paper.title_en }} {% endif %}

{# 元信息 #}
- {{ paper.authors|map(attribute='name')|join(', ') }} - 📅 {{ paper.published_at or paper.paper_date }} + {{ paper.authors|map(attribute='name')|join(', ') }} + 📅 {{ paper.published_at or paper.paper_date }} 👍 {{ paper.upvotes }}
- {# 标签 #} - {% if paper.tags %} + {# 标签 #} {% if paper.tags %}
{% for tag in paper.tags %} {{ tag.tag }} {% endfor %}
- {% endif %} - - {# 链接 #} + {% endif %} {# 链接 #} - {# 总结内容 — 按状态降级 #} - {% if summary_state == 'done' and paper.summary %} - {% if paper.summary_status and paper.summary_status.quality == 'low' %} -
⚠️ AI 总结质量较低,仅供参考
- {% elif paper.summary_status and paper.summary_status.quality == 'degraded' %} -
📝 总结部分字段不完整
- {% endif %} - - {% if paper.summary.one_line %} -
-

一句话摘要

-

{{ paper.summary.one_line }}

-
- {% endif %} - - {% if paper.summary.difficulty %} -
-

难度

-

{{ paper.summary.difficulty }}

-
- {% endif %} - + {# 总结内容 — 按状态降级 #} {% if summary_state == 'done' and paper.summary %} + {% if paper.summary_status and paper.summary_status.quality == 'low' %} +
⚠️ AI 总结质量较低,仅供参考
+ {% elif paper.summary_status and paper.summary_status.quality == 'degraded' %} +
📝 总结部分字段不完整
+ {% endif %} {% if paper.summary.one_line %} +
+

一句话摘要

+

{{ paper.summary.one_line }}

+
+ {% endif %} {% if paper.summary.difficulty %} +
+

难度

+

{{ paper.summary.difficulty }}

+
+ {% endif %} {% if paper.summary.motivation_problem %} +
+

研究动机

{% if paper.summary.motivation_problem %} -
-

研究动机

- {% if paper.summary.motivation_problem %}

问题:{{ paper.summary.motivation_problem }}

{% endif %} - {% if paper.summary.motivation_goal %}

目标:{{ paper.summary.motivation_goal }}

{% endif %} - {% if paper.summary.motivation_gap %}

差距:{{ paper.summary.motivation_gap }}

{% endif %} -
+

问题:{{ paper.summary.motivation_problem }}

+ {% endif %} {% if paper.summary.motivation_goal %} +

目标:{{ paper.summary.motivation_goal }}

+ {% endif %} {% if paper.summary.motivation_gap %} +

差距:{{ paper.summary.motivation_gap }}

{% endif %} - - {% if paper.summary.method_key_idea %} -
-

核心方法

- {% if paper.summary.method_overview %}

{{ paper.summary.method_overview }}

{% endif %} -

关键思路:{{ paper.summary.method_key_idea }}

- {% if paper.summary.method_novelty %}

新颖性:{{ paper.summary.method_novelty }}

{% endif %} -
+
+ {% endif %} {% if paper.summary.method_key_idea %} +
+

核心方法

+ {% if paper.summary.method_overview %} +

{{ paper.summary.method_overview }}

{% endif %} - - {% if paper.summary.results_main_json %} -
-

实验结果

-

{{ paper.summary.results_main_json }}

-
+

关键思路:{{ paper.summary.method_key_idea }}

+ {% if paper.summary.method_novelty %} +

新颖性:{{ paper.summary.method_novelty }}

{% endif %} - - {% if paper.summary.limitations_json %} -
-

局限与改进

-

{{ paper.summary.limitations_json }}

-
- {% endif %} - - {% elif summary_state == 'processing' %} -
-

🔄 正在生成 AI 总结,请稍后刷新页面

-
+
+ {% endif %} {% if paper.summary.results_main_json %} +
+

实验结果

+

{{ paper.summary.results_main_json }}

+
+ {% endif %} {% if paper.summary.limitations_json %} +
+

局限与改进

+

{{ paper.summary.limitations_json }}

+
+ {% endif %} {% elif summary_state == 'processing' %} +
+

🔄 正在生成 AI 总结,请稍后刷新页面

+
{% elif summary_state in ('failed', 'permanent_failure') %} -
-

❌ 总结生成失败{% if paper.summary_status and paper.summary_status.error_type %}({{ paper.summary_status.error_type }}){% endif %}

- {% if paper.summary_status and paper.summary_status.error %} -

{{ paper.summary_status.error }}

- {% endif %} -
+
+

+ ❌ 总结生成失败{% if paper.summary_status and + paper.summary_status.error_type %}({{ paper.summary_status.error_type + }}){% endif %} +

+ {% if paper.summary_status and paper.summary_status.error %} +

{{ paper.summary_status.error }}

+ {% endif %} +
{% else %} -
-

📝 AI 总结尚未生成

-
- {% endif %} - - {# 英文摘要 — 始终显示 #} - {% if paper.abstract %} +
+

📝 AI 总结尚未生成

+
+ {% endif %} {# 英文摘要 — 始终显示 #} {% if paper.abstract %}

Abstract

{{ paper.abstract }}

- {% endif %} - - {# Phase 5: 图片画廊 #} - {% if paper_images %} + {% endif %} {# Phase 5: 图片画廊 #} {% if paper_images %} - {% endif %} - - {# Phase 5: 相似论文推荐 #} - {% if similar_papers %} + {% endif %} {# Phase 5: 相似论文推荐 #} {% if similar_papers %}

相似论文推荐

{% for sp in similar_papers %} @@ -142,7 +143,9 @@ {{ sp.title_zh }} - 🎯 {{ "%.3f"|format(sp.distance) }} + 🎯 {{ "%.3f"|format(sp.distance) }} {% endfor %}
diff --git a/app/templates/index.html b/app/templates/index.html index f659ed1..58089d0 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,8 +1,5 @@ -{% extends "base.html" %} - -{% block title %}{{ page_title }} — HF Daily Papers{% endblock %} - -{% block content %} +{% extends "base.html" %} {% block title %}{{ page_title }} — HF Daily Papers{% +endblock %} {% block content %}
{% if prev_day %} ← 前一天 @@ -16,9 +13,8 @@ {% if papers %}
- {% for paper in papers %} - {% include "partials/paper_card.html" %} - {% endfor %} + {% for paper in papers %} {% include "partials/paper_card.html" %} {% endfor + %}
{% else %}
@@ -30,7 +26,11 @@
有数据的日期: {% for d in available_dates[:10] %} - {{ d }} + {{ d }} {% endfor %}
{% endblock %} diff --git a/app/templates/partials/paper_card.html b/app/templates/partials/paper_card.html index 79cb8aa..08c53f1 100644 --- a/app/templates/partials/paper_card.html +++ b/app/templates/partials/paper_card.html @@ -12,7 +12,9 @@ {% if paper.summary and paper.summary.one_line %}

{{ paper.summary.one_line }}

{% elif paper.abstract %} -

{{ paper.abstract[:200] }}{% if paper.abstract|length > 200 %}…{% endif %}

+

+ {{ paper.abstract[:200] }}{% if paper.abstract|length > 200 %}…{% endif %} +

{% endif %}
@@ -29,32 +31,31 @@
{{ paper.arxiv_id }} -
- +
+ {{ paper.upvotes }} 👍 · {{ paper.paper_date }}
作者{{ paper.authors|map(attribute='name')|join(', ') }} + {{ paper.authors|map(attribute='name')|join(', ') }} +
{{ row.label }} - {% if cell %} - {{ cell }} - {% else %} - 暂无总结 + {% if cell %} {{ cell }} {% else %} + 暂无总结 {% endif %}