Paper2Lab / app.py
unknown
Fix HF hero CSS and expose Modal endpoint
d754586
Raw
History Blame Contribute Delete
46.4 kB
import json
import tempfile
import html
import os
import sys
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parent
SRC_DIR = ROOT_DIR / "src"
sys.path.insert(0, str(ROOT_DIR))
sys.path.insert(0, str(SRC_DIR))
print("DEBUG ROOT_DIR:", ROOT_DIR)
print("DEBUG SRC_DIR:", SRC_DIR)
print("DEBUG SRC_EXISTS:", SRC_DIR.exists())
print("DEBUG PIPELINE_EXISTS:", (SRC_DIR / "paper2lab" / "inference" / "pipeline.py").exists())
print("DEBUG sys.path[:5]:", sys.path[:5])
import gradio as gr
try:
from paper2lab.inference.pipeline import PaperPipeline
print("DEBUG PaperPipeline import: OK")
except Exception as e:
print("DEBUG PaperPipeline import failed:", repr(e))
PaperPipeline = None
try:
from paper2lab.rag.qa import answer_from_pipeline_result
print("DEBUG RAG import: OK")
except Exception as e:
print("DEBUG RAG import failed:", repr(e))
answer_from_pipeline_result = None
APP_NAME = "Paper2Lab"
def pretty_json(data):
return json.dumps(data or {}, indent=2, ensure_ascii=False)
def as_list(value):
if not value:
return []
if isinstance(value, list):
return value
return [value]
def bullet_list(items, empty="_Not found._"):
items = as_list(items)
if not items:
return empty
return "\n".join(f"- {x}" for x in items)
def clean_display_text(text, max_len=95):
text = str(text or "").replace("\n", " ").strip()
while " " in text:
text = text.replace(" ", " ")
if len(text) > max_len:
return text[:max_len].rstrip() + "..."
return text
def chip_list(items, empty="Not found"):
items = as_list(items)
if not items:
return f"<span class='muted'>{empty}</span>"
html = ""
for x in items[:6]:
clean = clean_display_text(x, max_len=70)
html += f"<span class='chip' title='{str(x)}'>{clean}</span>"
return html
def get_card(result):
return result.get("paper_card_final") or result.get("paper_card") or result
def quality_warnings(card):
warnings = []
rq = card.get("research_question", "")
if rq and ("BERTis" in rq or len(rq.split()) < 5):
warnings.append("Research question may contain PDF spacing noise.")
datasets = as_list(card.get("datasets_or_data_sources"))
if any(len(str(x)) > 220 for x in datasets):
warnings.append("Dataset extraction contains long noisy candidates.")
for field in ["contributions", "key_findings", "limitations"]:
if not card.get(field):
warnings.append(f"{field} is empty.")
return warnings
def build_quick_summary_html(result):
card = get_card(result)
warnings = quality_warnings(card)
status = "Ready"
if result:
status = f"{max(0, 6 - len(warnings))} / 6 fields complete"
return f"""
<div class="summary-card">
<div class="card-head">
<div><span class="icon">📄</span><b>Quick summary</b></div>
<span class="status-pill">{status}</span>
</div>
<div class="summary-row">
<div class="label">Title</div>
<div class="value">{card.get("title", "Untitled paper")}</div>
</div>
<div class="summary-row">
<div class="label">Research question</div>
<div class="value">{card.get("research_question", "Not found")}</div>
</div>
<div class="summary-row">
<div class="label">Datasets</div>
<div class="value">{chip_list(card.get("datasets_or_data_sources"))}</div>
</div>
<div class="summary-row">
<div class="label">Models</div>
<div class="value">{chip_list(card.get("models_or_methods"))}</div>
</div>
<div class="summary-row">
<div class="label">Key findings</div>
<div class="value highlight">{bullet_list(card.get("key_findings")).replace("- ", "• ")}</div>
</div>
</div>
"""
def build_paper_summary_md(result):
card = get_card(result)
warnings = quality_warnings(card)
warning_md = ""
if warnings:
warning_md = "## ⚠️ Extraction Quality Warnings\n" + "\n".join(f"- {w}" for w in warnings)
return f"""
# Structured Paper Summary
**Title:** {card.get("title", "Untitled paper")}
**Field:** {card.get("field", "Unknown")}
{warning_md}
## Research Question
{card.get("research_question", "_Not found._")}
## Contributions
{bullet_list(card.get("contributions"))}
## Methodology
{bullet_list(card.get("methodology"))}
## Datasets / Data Sources
{bullet_list(card.get("datasets_or_data_sources"))}
## Models / Methods
{bullet_list(card.get("models_or_methods"))}
## Metrics / Measurements
{bullet_list(card.get("metrics_or_measurements"))}
## Key Findings
{bullet_list(card.get("key_findings"))}
## Limitations
{bullet_list(card.get("limitations"))}
"""
def build_lab_md(result):
card = get_card(result)
kit = card.get("lab_starter_kit") or result.get("lab_starter_kit") or {}
return f"""
# Lab Starter Kit
**Starter type:** `{kit.get("starter_type", card.get("paper_type", "unknown"))}`
## Project Structure
{bullet_list(kit.get("project_structure"))}
## Requirements
{bullet_list(kit.get("requirements_txt"))}
## Dataset Plan
{bullet_list(kit.get("dataset_plan") or kit.get("required_data"))}
## Suggested Experiments
{bullet_list(kit.get("suggested_experiments") or kit.get("experiment_checklist"))}
## Evaluation Plan
{bullet_list(kit.get("evaluation_plan"))}
## Reproducibility Risks
{bullet_list(kit.get("reproducibility_risks") or card.get("missing_reproducibility_info"))}
"""
def build_evidence_md(result):
figs = result.get("figures_and_tables", [])
card = get_card(result)
lines = ["# Evidence Viewer"]
if figs:
lines.append("## Figures and Tables")
for item in figs[:12]:
lines.append(
f"- **{item.get('label', 'Item')}** — page {item.get('page_number', '?')}: "
f"{item.get('summary', 'No summary')}"
)
lines.append("\n## Extracted Evidence Fields")
for field in [
"methodology",
"datasets_or_data_sources",
"models_or_methods",
"metrics_or_measurements",
"key_findings",
]:
lines.append(f"\n### {field}")
lines.append(bullet_list(card.get(field)))
return "\n".join(lines)
def build_advanced_md(result):
card = get_card(result)
metadata = card.get("metadata", {}) if isinstance(card, dict) else {}
if not isinstance(metadata, dict):
metadata = {}
quality = metadata.get("quality", {})
if not isinstance(quality, dict):
quality = {}
repro = card.get("reproducibility_score", {}) if isinstance(card, dict) else {}
if not isinstance(repro, dict):
repro = {}
selection = result.get("auto_selection", {}).get("selection_report", {})
if not isinstance(selection, dict):
selection = {}
md = []
md.append("# ⚙️ Advanced Analysis")
md.append("")
md.append("## Extraction Quality")
md.append("")
md.append("| Metric | Value |")
md.append("|---|---|")
md.append(f"| Quality Score | {quality.get('quality_score', 'N/A')} |")
md.append(f"| Sections Found | {quality.get('num_sections', 'N/A')} |")
md.append(f"| References Found | {quality.get('num_references', metadata.get('references_count', 'N/A'))} |")
md.append(f"| Tables Found | {quality.get('num_tables', 'N/A')} |")
md.append(f"| Captions Found | {quality.get('num_captions', 'N/A')} |")
md.append("")
md.append("---")
md.append("")
md.append(f"**Reproducibility Level:** {repro.get('level', 'unknown')}")
md.append("")
md.append(f"**Score:** {repro.get('score', 'N/A')}")
md.append("")
md.append("### Detected Items")
md.append(bullet_list(repro.get("detected_items")))
md.append("")
md.append("### Missing Items")
md.append(bullet_list(repro.get("missing_items")))
md.append("")
md.append("---")
md.append("")
md.append("## Selection Report")
if selection.get("fields"):
md.append("Field-level local vs Nemotron selection was completed.")
md.append("")
md.append("```json")
md.append(pretty_json(selection))
md.append("```")
else:
md.append(
"No field-level comparison available for this paper. "
"The final card was generated from the best available extraction pipeline."
)
return "\n".join(md)
def safe_html(value):
return html.escape(str(value or ""), quote=True)
def short_card_text(value, max_len=48):
text = clean_display_text(value, max_len=max_len)
return safe_html(text)
def build_lab_cards_html(result):
card = get_card(result)
kit = card.get("lab_starter_kit") or result.get("lab_starter_kit") or {}
reqs = as_list(kit.get("requirements_txt"))
risks = as_list(
kit.get("reproducibility_risks")
or card.get("missing_reproducibility_info")
)
experiments = as_list(
kit.get("suggested_experiments")
or kit.get("experiment_checklist")
)
starter_type = kit.get("starter_type", card.get("paper_type", "unknown"))
tools = ", ".join(clean_display_text(x, max_len=28) for x in reqs[:3]) if reqs else "Not found"
return f"""
<div class="lab-board">
<div class="lab-board-header">
<span class="lab-emoji">🧪</span>
<span>Lab readiness</span>
</div>
<div class="lab-grid-2">
<div class="lab-tile lab-green">
<span>Starter type</span>
<b>{short_card_text(starter_type)}</b>
</div>
<div class="lab-tile lab-dark">
<span>Tools</span>
<b>{short_card_text(tools, max_len=58)}</b>
</div>
<div class="lab-tile lab-purple">
<span>Experiments</span>
<b>{len(experiments)} detected</b>
</div>
<div class="lab-tile lab-red">
<span>Risks</span>
<b>{len(risks)} issues</b>
</div>
</div>
</div>
"""
def analyze_paper(pdf_file, refinement_mode):
if pdf_file is None:
raise gr.Error("Please upload a PDF first.")
if PaperPipeline is None:
raise gr.Error("PaperPipeline import failed. Run from the project root or install your package.")
pdf_path = pdf_file if isinstance(pdf_file, str) else pdf_file.name
pipeline = PaperPipeline(refinement_mode=refinement_mode)
result = pipeline.run(pdf_path)
print("PDF PATH:", pdf_path)
print("MODE:", refinement_mode)
print("FINAL DATASETS:", result.get("paper_card_final", {}).get("datasets_or_data_sources"))
print("GET_CARD DATASETS:", get_card(result).get("datasets_or_data_sources"))
card = get_card(result)
if not card.get("datasets_or_data_sources"):
roadmap = card.get("reproduction_roadmap") or {}
kit = card.get("lab_starter_kit") or {}
fallback_datasets = []
if isinstance(roadmap, dict):
fallback_datasets += roadmap.get("datasets") or []
if isinstance(kit, dict):
fallback_datasets += kit.get("dataset_plan") or []
if fallback_datasets:
card["datasets_or_data_sources"] = list(dict.fromkeys(fallback_datasets))
result["paper_card_final"] = card
paper_md = build_paper_summary_md(result)
lab_md = build_lab_md(result)
evidence_md = build_evidence_md(result)
advanced_md = build_advanced_md(result)
quick_html = build_quick_summary_html(result)
lab_cards = build_lab_cards_html(result)
tmp = Path(tempfile.mkdtemp())
json_path = tmp / "paper2lab_output.json"
md_path = tmp / "paper2lab_report.md"
json_path.write_text(pretty_json(result), encoding="utf-8")
md_path.write_text(paper_md + "\n\n---\n\n" + lab_md, encoding="utf-8")
return (
result,
quick_html,
paper_md,
lab_md,
evidence_md,
advanced_md,
lab_cards,
card,
str(json_path),
str(md_path),
)
def ask_paper_question(result, question):
if not result:
return "⚠️ Analyze a paper first.", {}
if not question or not str(question).strip():
return "⚠️ Please enter a question.", {}
question_text = str(question).strip()
q = question_text.lower()
card = get_card(result)
def md_items(title, items):
items = as_list(items)
items = [x for x in items if str(x).strip()]
if not items:
return f"**{title}:**\n\n_Not found in the structured paper card._"
return f"**{title}:**\n\n" + "\n".join(f"- {x}" for x in items[:8])
# Fast structured answers for demo-critical questions
if any(k in q for k in ["dataset", "data source", "corpus", "benchmark"]):
return md_items("Datasets / data sources", card.get("datasets_or_data_sources")), {
"source": "structured_card",
"field": "datasets_or_data_sources",
}
if any(k in q for k in ["model", "architecture", "proposed"]):
rq = card.get("research_question", "")
if rq:
return (
f"**Proposed model:**\n\n- {rq}",
{
"source": "structured_card",
"field": "research_question",
},
)
if any(k in q for k in ["finding", "result", "conclusion", "contribution"]):
return md_items("Key findings", card.get("key_findings") or card.get("contributions")), {
"source": "structured_card",
"field": "key_findings",
}
if any(k in q for k in ["metric", "score", "performance", "evaluation"]):
return md_items("Metrics / measurements", card.get("metrics_or_measurements")), {
"source": "structured_card",
"field": "metrics_or_measurements",
}
if any(k in q for k in ["limitation", "risk", "missing"]):
return md_items("Limitations / reproducibility risks", card.get("limitations")), {
"source": "structured_card",
"field": "limitations",
}
if any(k in q for k in ["reproduce", "reproduction", "roadmap", "steps"]):
roadmap = card.get("reproduction_roadmap") or {}
steps = []
if isinstance(roadmap, dict):
steps = roadmap.get("experimental_steps") or roadmap.get("missing_for_reproduction") or []
return md_items("Reproduction roadmap", steps), {
"source": "structured_card",
"field": "reproduction_roadmap",
}
if answer_from_pipeline_result is None:
return (
"⚠️ RAG module import failed. Check that `paper2lab.rag.qa` is available "
"and dependencies are installed.",
{},
)
try:
qa = answer_from_pipeline_result(
pipeline_result=result,
question=question_text,
top_k=5,
embedder_backend="local",
)
answer = qa.get("answer", "No answer found.")
evidence = qa.get("evidence", [])
evidence_md = "\n\n## Evidence\n"
for i, ev in enumerate(evidence, 1):
page = ev.get("page_start") or ev.get("page_number") or "?"
title = ev.get("title", "Evidence")
text = ev.get("text", "")
evidence_md += f"\n**{i}. {title} — page {page}**\n\n> {text[:700]}\n"
return answer + evidence_md, qa
except Exception as exc:
return f"❌ RAG error: {exc}", {}
CSS = """
/* ─────────────────────────────────────────────
Paper2Lab UI — fixed layout
Fixes:
- no duplicate upload holder/native file input
- wider hero with Paper2Lab title
- clean 2-column app layout
- lab card placed under Quick Summary
───────────────────────────────────────────── */
body,
.gradio-container {
background: #eef2f7 !important;
color: #111827 !important;
}
* {
box-sizing: border-box;
}
footer {
display: none !important;
}
.gradio-container {
max-width: none !important;
padding-top: 28px !important;
}
/* Main page width */
.app-shell {
width: min(1220px, 94vw);
margin: 0 auto;
}
/* Remove unwanted Gradio block styling only inside our custom layout */
.top-layout .block,
.top-layout .gr-group,
.top-layout .gr-box,
.top-layout .gr-panel,
.result-stack .block,
.result-stack .gr-group,
.result-stack .gr-box,
.result-stack .gr-panel {
background: transparent !important;
border: none !important;
box-shadow: none !important;
padding: 0 !important;
margin: 0 !important;
overflow: visible !important;
}
/* ───────────────── HERO ───────────────── */
.hero {
position: relative;
overflow: hidden;
min-height: 245px;
border-radius: 24px;
padding: 34px 48px;
margin: 0 auto 28px;
background: linear-gradient(135deg, #0f172a 0%, #18253f 48%, #252659 100%);
border: 1px solid rgba(255,255,255,.08);
box-shadow: 0 22px 60px rgba(15,23,42,.18);
}
.hero::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 15% 25%, rgba(34,211,238,.14), transparent 30%),
radial-gradient(circle at 78% 26%, rgba(124,58,237,.22), transparent 35%);
pointer-events: none;
}
.hero-content {
position: relative;
z-index: 2;
max-width: 920px;
}
.logo-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.logo-mark {
width: 32px;
height: 32px;
display: inline-grid;
place-items: center;
border-radius: 10px;
background: rgba(103,232,249,.14);
border: 1px solid rgba(103,232,249,.22);
font-size: 18px;
}
.logo-text {
color: #ffffff !important;
font-size: 18px;
font-weight: 950;
letter-spacing: -0.035em;
}
.kicker {
color: #67e8f9 !important;
text-transform: uppercase;
letter-spacing: .16em;
font-size: 11px;
font-weight: 900;
margin-bottom: 8px;
}
.hero h1 {
max-width: 960px;
color: #ffffff !important;
font-size: 38px;
line-height: 1.05;
letter-spacing: -0.052em;
margin: 0 0 14px;
}
.hero h1 span {
color: #a78bfa !important;
}
.hero p {
max-width: 760px;
color: rgba(255,255,255,.86) !important;
font-size: 15px;
line-height: 1.62;
margin: 0 0 22px;
}
.hero-badges {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.hero-badge {
padding: 8px 13px;
border-radius: 10px;
background: rgba(255,255,255,.12);
color: #ffffff !important;
font-weight: 850;
font-size: 12px;
border: 1px solid rgba(255,255,255,.18);
}
/* ───────────────── TWO COLUMN LAYOUT ───────────────── */
.top-layout {
display: grid !important;
grid-template-columns: 340px minmax(0, 1fr) !important;
gap: 28px !important;
align-items: start !important;
}
.control-stack {
background: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 24px !important;
padding: 24px !important;
box-shadow: 0 14px 38px rgba(15,23,42,.06) !important;
display: flex !important;
flex-direction: column !important;
gap: 18px !important;
}
.result-stack {
display: flex !important;
flex-direction: column !important;
gap: 26px !important;
background: transparent !important;
border: none !important;
padding: 0 !important;
}
/* ───────────────── LEFT PANEL ───────────────── */
.panel-title {
font-size: 16px;
font-weight: 900;
color: #111827 !important;
margin: 0;
padding-bottom: 10px;
border-bottom: 1px solid #f1f5f9;
}
/* Button */
.gradio-container button.primary,
.gradio-container button[class*="primary"] {
width: 100% !important;
min-height: 50px !important;
border: none !important;
border-radius: 15px !important;
background: linear-gradient(135deg, #7c3aed 0%, #4f46e5 100%) !important;
color: #ffffff !important;
font-weight: 950 !important;
font-size: 15px !important;
box-shadow: 0 12px 26px rgba(124,58,237,.28) !important;
}
.gradio-container button.primary *,
.gradio-container button[class*="primary"] * {
color: #ffffff !important;
}
.tip-box {
padding: 12px 14px;
border-radius: 14px;
background: #fafbff;
border: 1px solid #e5e7eb;
color: #4b5563 !important;
font-size: 12px;
font-weight: 750;
}
/* ───────────────── QUICK SUMMARY ───────────────── */
.summary-card {
background: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 22px !important;
overflow: hidden !important;
box-shadow: 0 14px 38px rgba(15,23,42,.06) !important;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 17px 22px;
border-bottom: 1px solid #f1f5f9;
}
.card-head b {
color: #111827 !important;
font-size: 16px;
}
.icon {
display: inline-grid;
place-items: center;
width: 28px;
height: 28px;
margin-right: 9px;
border-radius: 9px;
background: #ede9fe;
font-size: 15px;
}
.status-pill {
background: #dcfce7;
color: #047857 !important;
padding: 6px 13px;
border-radius: 999px;
font-weight: 900;
font-size: 12px;
}
.summary-row {
display: grid;
grid-template-columns: 140px minmax(0, 1fr);
gap: 16px;
padding: 15px 22px;
border-bottom: 1px solid #f8fafc;
font-size: 14px;
}
.label {
color: #6b7280 !important;
font-weight: 850;
}
.value {
color: #111827 !important;
line-height: 1.55;
overflow-wrap: anywhere;
}
.highlight {
background: #f5f3ff;
border-left: 3px solid #7c3aed;
border-radius: 0 10px 10px 0;
padding: 10px 12px;
color: #312e81 !important;
}
.chip {
display: inline-block;
margin: 3px 5px 3px 0;
padding: 6px 10px;
border-radius: 999px;
background: #ede9fe;
color: #4c1d95 !important;
border: 1px solid #c4b5fd;
font-weight: 800;
font-size: 12px;
}
.muted {
color: #9ca3af !important;
}
/* ───────────────── LAB READINESS ───────────────── */
.lab-board {
background: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 22px !important;
padding: 18px !important;
box-shadow: 0 14px 38px rgba(15,23,42,.06) !important;
}
.lab-board-header {
display: flex;
align-items: center;
gap: 12px;
background: #f8f9fc;
border: 1px solid #ebebf0;
border-radius: 14px;
padding: 14px 18px;
margin-bottom: 16px;
color: #0f172a !important;
font-size: 18px;
font-weight: 950;
letter-spacing: -0.03em;
}
.lab-board-header * {
color: #0f172a !important;
}
.lab-emoji {
font-size: 22px;
line-height: 1;
flex: 0 0 auto;
}
.lab-grid-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.lab-tile {
min-height: 108px;
padding: 18px 20px;
border-radius: 16px;
border: 1px solid transparent;
overflow: hidden;
}
.lab-tile span {
display: block;
margin-bottom: 10px;
font-size: 12px;
line-height: 1.2;
font-weight: 900;
letter-spacing: .04em;
text-transform: uppercase;
}
.lab-tile b {
display: block;
font-size: 23px;
line-height: 1.15;
font-weight: 950;
letter-spacing: -0.035em;
overflow-wrap: break-word;
}
.lab-green {
background: #ecfdf5 !important;
color: #065f46 !important;
border-color: #a7f3d0 !important;
}
.lab-dark {
background:
radial-gradient(circle at 84% 18%, rgba(124,58,237,.28), transparent 36%),
#1e1b4b !important;
color: #ffffff !important;
border-color: #3730a3 !important;
}
.lab-purple {
background: #f5f3ff !important;
color: #4c1d95 !important;
border-color: #ddd6fe !important;
}
.lab-red {
background: #fff1f2 !important;
color: #9f1239 !important;
border-color: #fecdd3 !important;
}
.lab-green *,
.lab-dark *,
.lab-purple *,
.lab-red * {
color: inherit !important;
}
/* ───────────────── TABS ───────────────── */
.tabs-shell {
margin-top: 28px;
}
.gradio-container .tab-nav {
gap: 6px !important;
border-bottom: 1px solid #cbd5e1 !important;
}
.gradio-container .tab-nav button {
font-size: 14px !important;
font-weight: 750 !important;
color: #6b7280 !important;
padding: 11px 18px !important;
background: transparent !important;
border: none !important;
border-bottom: 3px solid transparent !important;
white-space: nowrap !important;
}
.gradio-container .tab-nav button.selected {
color: #6d28d9 !important;
border-bottom-color: #7c3aed !important;
}
.gradio-container .tabs > .block,
.gradio-container .tabitem {
background: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 18px !important;
box-shadow: 0 8px 24px rgba(15,23,42,.04) !important;
overflow: hidden !important;
}
/* General text - exclude hero */
.gradio-container .markdown:not(.hero):not(.hero *),
.gradio-container p:not(.hero p),
.gradio-container li,
.gradio-container td,
.gradio-container th,
.gradio-container label,
.gradio-container textarea,
.gradio-container input {
color: #111827 !important;
}
/* ───────────────── RESPONSIVE ───────────────── */
@media (max-width: 900px) {
.app-shell {
width: min(94vw, 760px);
}
.hero {
min-height: auto;
padding: 26px 22px;
}
.hero h1 {
font-size: 28px;
}
.hero p {
font-size: 14px;
}
.top-layout {
grid-template-columns: 1fr !important;
}
.summary-row {
grid-template-columns: 1fr;
gap: 6px;
}
.lab-grid-2 {
grid-template-columns: 1fr;
}
}
/* "Analyze Paper" text inside Quick Summary */
.summary-card .value,
.summary-card .value *,
.summary-row .value,
.summary-row .value * {
color: #111827 !important;
opacity: 1 !important;
visibility: visible !important;
}
/* Quick summary text */
.summary-card .value,
.summary-card .value *,
.summary-row .value,
.summary-row .value * {
color: #111827 !important;
opacity: 1 !important;
-webkit-text-fill-color: #111827 !important;
}
/* Tabs text */
.gradio-container button[role="tab"],
.gradio-container button[role="tab"] *,
.gradio-container .tab-nav button,
.gradio-container .tab-nav button * {
color: #111827 !important;
opacity: 1 !important;
visibility: visible !important;
-webkit-text-fill-color: #111827 !important;
}
.gradio-container button[role="tab"][aria-selected="true"],
.gradio-container button[role="tab"][aria-selected="true"] *,
.gradio-container .tab-nav button.selected,
.gradio-container .tab-nav button.selected * {
color: #7c3aed !important;
-webkit-text-fill-color: #7c3aed !important;
font-weight: 800 !important;
}
/* ===== FINAL LEFT PANEL UPLOAD FIX ===== */
.upload-caption {
display: inline-flex;
width: fit-content;
align-items: center;
gap: 8px;
padding: 11px 20px;
border-radius: 12px;
background: linear-gradient(135deg, #7c3aed, #6d28d9);
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
font-size: 15px;
font-weight: 900;
box-shadow: 0 12px 24px rgba(124,58,237,.22);
margin-bottom: 0 !important;
}
.upload-caption * {
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
}
#pdf_upload {
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
background: transparent !important;
border: none !important;
box-shadow: none !important;
}
/* Main drop area only */
#pdf_upload .wrap {
width: 100% !important;
min-height: 215px !important;
background: #ffffff !important;
border: 2px dashed #c4b5fd !important;
border-radius: 24px !important;
display: grid !important;
place-items: center !important;
text-align: center !important;
position: relative !important;
overflow: hidden !important;
}
/* Hide only Gradio placeholder text before upload */
#pdf_upload .wrap > div:first-child,
#pdf_upload .wrap > span,
#pdf_upload .wrap > p {
display: none !important;
}
/* Custom placeholder */
#pdf_upload .wrap::after {
content: "☁️\\A Drag & drop your PDF file here\\A or click to browse";
white-space: pre-line;
color: #334155 !important;
-webkit-text-fill-color: #334155 !important;
font-size: 16px !important;
line-height: 1.8 !important;
font-weight: 700 !important;
text-align: center !important;
}
/* After file upload: remove fake placeholder */
#pdf_upload:has([data-testid="file"]) .wrap::after,
#pdf_upload:has(.file-preview) .wrap::after {
content: "" !important;
}
/* Uploaded file card readability */
#pdf_upload [data-testid="file"],
#pdf_upload .file-preview,
#pdf_upload .file-preview *,
#pdf_upload a,
#pdf_upload span,
#pdf_upload p {
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
opacity: 1 !important;
}
/* Hide built-in label only */
#pdf_upload .label-wrap,
#pdf_upload label,
#pdf_upload [data-testid="block-label"],
#pdf_upload input[type="file"] {
display: none !important;
}
/* Engine wrapper */
#engine_radio,
#engine_radio *,
#engine_radio fieldset,
#engine_radio .wrap {
background: #FFFFFF !important;
border: none !important;
box-shadow: none !important;
}
.engine-caption {
display: inline-flex;
width: fit-content;
align-items: center;
padding: 8px 14px;
border-radius: 10px;
background: #7c3aed;
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
font-size: 16px;
font-weight: 900;
margin-bottom: 10px;
}
#engine_radio label,
#engine_radio label span {
background: transparent !important;
border: none !important;
box-shadow: none !important;
color: #8b5cf6 !important;
-webkit-text-fill-color: #8b5cf6 !important;
font-size: 14px !important;
font-weight: 900 !important;
}
/* Selected option purple */
#engine_radio label:has(input:checked),
#engine_radio label:has(input:checked) * {
color: #5b21b6 !important;
-webkit-text-fill-color: #5b21b6 !important;
}
/* Radio circles */
#engine_radio input[type="radio"] {
display: inline-block !important;
appearance: auto !important;
-webkit-appearance: radio !important;
opacity: 1 !important;
visibility: visible !important;
position: static !important;
width: 14px !important;
height: 14px !important;
margin-right: 8px !important;
accent-color: #5b21b6 !important;
}
#engine_radio .wrap {
display: flex !important;
flex-direction: row !important;
gap: 40px !important;
align-items: center !important;
}
/* Hide Gradio's built-in title completely */
#engine_radio legend,
#engine_radio .label-wrap,
#engine_radio [data-testid="block-label"] {
display: none !important;
}
/* EMERGENCY READABILITY FIX */
.gradio-container h1:not(.hero h1),
.gradio-container h2:not(.hero h2),
.gradio-container h3:not(.hero h3),
.gradio-container h4:not(.hero h4),
.gradio-container h5:not(.hero h5),
.gradio-container h6:not(.hero h6) {
color: #111827 !important;
opacity: 1 !important;
visibility: visible !important;
font-weight: 800 !important;
}
/* Safe readability fix: Markdown only, not JSON/code internals */
.gradio-container .markdown,
.gradio-container .markdown p,
.gradio-container .markdown li,
.gradio-container .markdown h1,
.gradio-container .markdown h2,
.gradio-container .markdown h3,
.gradio-container .markdown strong {
color: #111827 !important;
opacity: 1 !important;
}
/* Keep code/JSON readable */
.gradio-container pre,
.gradio-container code,
.gradio-container pre *,
.gradio-container code *,
.gradio-container .cm-editor,
.gradio-container .cm-editor * {
color: inherit !important;
-webkit-text-fill-color: inherit !important;
}
/* ================= FINAL TAB READABILITY PATCH ================= */
/* Normal markdown text, including _Not found._ italic text */
.gradio-container .markdown,
.gradio-container .markdown *,
.gradio-container .prose,
.gradio-container .prose * {
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
opacity: 1 !important;
}
/* Markdown italic fallback like _Not found._ */
.gradio-container em,
.gradio-container i {
color: #475569 !important;
-webkit-text-fill-color: #475569 !important;
opacity: 1 !important;
}
/* Inline code like `survey_or_review` */
.gradio-container .markdown code,
.gradio-container p code,
.gradio-container li code {
background: #f1f5f9 !important;
color: #5b21b6 !important;
-webkit-text-fill-color: #5b21b6 !important;
padding: 2px 6px !important;
border-radius: 6px !important;
font-weight: 800 !important;
}
/* Force tab panels to stay white */
.gradio-container .tabitem,
.gradio-container .tabitem > div,
.gradio-container .tabitem .block,
.gradio-container .tabitem .wrap {
background: #ffffff !important;
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
}
/* Advanced accordion fix */
.gradio-container .accordion,
.gradio-container .accordion *,
.gradio-container details,
.gradio-container details * {
background: #ffffff !important;
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
}
/* Tables in Advanced Analysis */
.gradio-container table,
.gradio-container table *,
.gradio-container th,
.gradio-container td {
background: #ffffff !important;
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
border-color: #cbd5e1 !important;
}
/* Code blocks inside markdown */
.gradio-container pre,
.gradio-container pre *,
.gradio-container code,
.gradio-container code * {
background: #f8fafc !important;
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
}
/* Export JSON/code viewer: make it readable */
.gradio-container .cm-editor,
.gradio-container .cm-editor *,
.gradio-container .cm-scroller,
.gradio-container .cm-content,
.gradio-container .cm-line {
background: #f8fafc !important;
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
}
/* Export file boxes */
.gradio-container [data-testid="file"],
.gradio-container [data-testid="file"] *,
.gradio-container .file-preview,
.gradio-container .file-preview * {
background: #f8fafc !important;
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
}
/* ================= LAB READINESS RESTORE ================= */
/* Restore dark Tools tile readability */
.lab-board .lab-dark,
.lab-board .lab-dark *,
.lab-board .lab-dark span,
.lab-board .lab-dark b {
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
opacity: 1 !important;
}
/* Optional: softer label inside dark card */
.lab-board .lab-dark span {
color: #c4b5fd !important;
-webkit-text-fill-color: #c4b5fd !important;
}
/* Keep other lab tiles clean */
.lab-board .lab-green,
.lab-board .lab-green * {
color: #065f46 !important;
-webkit-text-fill-color: #065f46 !important;
}
.lab-board .lab-purple,
.lab-board .lab-purple * {
color: #4c1d95 !important;
-webkit-text-fill-color: #4c1d95 !important;
}
.lab-board .lab-red,
.lab-board .lab-red * {
color: #9f1239 !important;
-webkit-text-fill-color: #9f1239 !important;
}
/* ================= HERO COLOR RESTORE ================= */
.hero,
.hero * {
color: inherit;
}
.hero .logo-text {
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
}
.hero .kicker {
color: #67e8f9 !important;
-webkit-text-fill-color: #67e8f9 !important;
}
.hero h1 {
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
}
.hero h1 span {
color: #a78bfa !important;
-webkit-text-fill-color: #a78bfa !important;
}
.hero p {
color: rgba(255,255,255,.88) !important;
-webkit-text-fill-color: rgba(255,255,255,.88) !important;
}
.hero .hero-badge,
.hero .hero-badge * {
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
}
.hero .logo-mark {
color: inherit !important;
-webkit-text-fill-color: inherit !important;
}
/* FINAL HERO READABILITY OVERRIDE */
.hero,
.hero .hero-content,
.hero .hero-copy {
opacity: 1 !important;
filter: none !important;
mix-blend-mode: normal !important;
}
.hero h1,
.hero h1 *,
.hero p,
.hero .kicker,
.hero .logo-text {
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
opacity: 1 !important;
filter: none !important;
mix-blend-mode: normal !important;
}
.hero .kicker {
color: #67e8f9 !important;
-webkit-text-fill-color: #67e8f9 !important;
}
.hero h1 span {
color: #c4b5fd !important;
-webkit-text-fill-color: #c4b5fd !important;
}
.hero p {
color: rgba(255,255,255,.92) !important;
-webkit-text-fill-color: rgba(255,255,255,.92) !important;
}
/* ================= ASK THE PAPER / RAG FIX ================= */
.rag-panel,
.rag-panel * {
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
}
.rag-question,
.rag-question *,
.rag-question textarea,
.rag-question input {
background: #ffffff !important;
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
border-color: #cbd5e1 !important;
}
.rag-question textarea::placeholder,
.rag-question input::placeholder {
color: #64748b !important;
-webkit-text-fill-color: #64748b !important;
opacity: 1 !important;
}
.rag-answer {
background: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 18px !important;
padding: 22px !important;
box-shadow: 0 8px 24px rgba(15,23,42,.04) !important;
line-height: 1.65 !important;
}
.rag-answer,
.rag-answer * {
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
}
.rag-answer h1,
.rag-answer h2,
.rag-answer h3 {
margin-top: 18px !important;
margin-bottom: 10px !important;
font-weight: 900 !important;
}
.rag-answer ul {
padding-left: 22px !important;
}
.rag-answer li {
margin-bottom: 7px !important;
}
.rag-answer blockquote {
margin: 12px 0 !important;
padding: 12px 16px !important;
border-left: 4px solid #7c3aed !important;
background: #f8fafc !important;
border-radius: 10px !important;
color: #334155 !important;
-webkit-text-fill-color: #334155 !important;
}
.rag-debug,
.rag-debug * {
background: #ffffff !important;
color: #111827 !important;
-webkit-text-fill-color: #111827 !important;
}
/* HF SPACE HERO FINAL OVERRIDE */
.gradio-container .hero,
.gradio-container .hero * {
opacity: 1 !important;
filter: none !important;
mix-blend-mode: normal !important;
}
.gradio-container .hero h1,
.gradio-container .hero h1 *,
.gradio-container .hero p,
.gradio-container .hero .logo-text {
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
}
.gradio-container .hero .kicker {
color: #67e8f9 !important;
-webkit-text-fill-color: #67e8f9 !important;
}
.gradio-container .hero h1 span {
color: #c4b5fd !important;
-webkit-text-fill-color: #c4b5fd !important;
}
.gradio-container .hero p {
color: rgba(255, 255, 255, 0.92) !important;
-webkit-text-fill-color: rgba(255, 255, 255, 0.92) !important;
}
/* ABSOLUTE FINAL HERO FIX FOR HF */
.gradio-container .hero {
color: #ffffff !important;
}
.gradio-container .hero h1,
.gradio-container .hero h1 *,
.gradio-container .hero p,
.gradio-container .hero p *,
.gradio-container .hero .logo-text,
.gradio-container .hero .hero-badge,
.gradio-container .hero .hero-badge * {
color: #ffffff !important;
-webkit-text-fill-color: #ffffff !important;
opacity: 1 !important;
filter: none !important;
}
.gradio-container .hero .kicker {
color: #67e8f9 !important;
-webkit-text-fill-color: #67e8f9 !important;
}
.gradio-container .hero h1 span {
color: #c4b5fd !important;
-webkit-text-fill-color: #c4b5fd !important;
}
"""
theme = gr.themes.Soft(
primary_hue="violet",
secondary_hue="blue",
neutral_hue="slate",
)
with gr.Blocks(title=APP_NAME, theme=theme, css=CSS) as demo:
state = gr.State({})
gr.HTML(
"""
<div class="app-shell">
<section class="hero">
<div class="hero-content">
<div class="logo-row">
<span class="logo-mark">🧪</span>
<span class="logo-text">Paper2Lab</span>
</div>
<div class="kicker">Research Assistant</div>
<h1>
Turn research papers into
<span>actionable lab plans</span>
</h1>
<p>
Upload a PDF and get a structured paper card, lab starter kit,
evidence grounding, reproducibility assessment and exportable reports.
</p>
<div class="hero-badges">
<div class="hero-badge">✦ AI-powered extraction</div>
<div class="hero-badge">□ Evidence grounded</div>
<div class="hero-badge">☘ Reproducibility ready</div>
</div>
</div>
</section>
</div>
"""
)
with gr.Row(elem_classes=["app-shell", "top-layout"]):
with gr.Column(scale=4, min_width=300, elem_classes=["control-stack"]):
gr.HTML("""<div class="panel-title">📤 1. Upload your paper</div>""")
gr.HTML("""<div class="upload-caption">📄 Drop your PDF here</div>""")
pdf_input = gr.File(
label=None,
show_label=False,
file_types=[".pdf"],
type="filepath",
elem_id="pdf_upload",
elem_classes=["clean-upload"],
)
gr.HTML("""<div class="panel-title">⚡ 2. Select analysis engine</div>""")
gr.HTML("""<div class="engine-caption">⚙️ Analysis engine</div>""")
refinement_mode = gr.Radio(
label=None,
container=False,
choices=["local", "nemotron"],
value="nemotron",
elem_id="engine_radio",
)
run_btn = gr.Button(
"✨ Analyze Paper",
variant="primary",
size="lg",
elem_classes=["clean-button"],
)
gr.HTML("""<div class="tip-box">💡 Use a clear-text PDF for best results.</div>""")
with gr.Column(scale=7, min_width=520, elem_classes=["result-stack"]):
quick_summary = gr.HTML(
"""
<div class="summary-card">
<div class="card-head">
<div><span class="icon">📄</span><b>Quick summary</b></div>
<span class="status-pill">Ready</span>
</div>
<div class="summary-row">
<div class="label">Status</div>
<div class="value">Upload a PDF and click <b>Analyze Paper</b>.</div>
</div>
</div>
"""
)
lab_cards_html = gr.HTML(
"""
<div class="lab-board">
<div class="lab-board-header">
<span class="lab-emoji">🧪</span>
<span>Lab readiness</span>
</div>
<div class="lab-grid-2">
<div class="lab-tile lab-green">
<span>Starter type</span>
<b>Waiting</b>
</div>
<div class="lab-tile lab-dark">
<span>Tools</span>
<b>Waiting</b>
</div>
<div class="lab-tile lab-purple">
<span>Experiments</span>
<b>Waiting</b>
</div>
<div class="lab-tile lab-red">
<span>Risks</span>
<b>Waiting</b>
</div>
</div>
</div>
"""
)
with gr.Row(elem_classes=["app-shell", "tabs-shell"]):
with gr.Column():
with gr.Tabs():
with gr.Tab("📄 Paper Summary"):
paper_md = gr.Markdown()
with gr.Tab("🧪 Lab Starter Kit"):
lab_md = gr.Markdown()
with gr.Tab("🔎 Evidence Viewer"):
evidence_md = gr.Markdown()
with gr.Tab("💬 Ask the Paper"):
with gr.Column(elem_classes=["rag-panel"]):
rag_question = gr.Textbox(
label="Ask a question about the uploaded paper",
placeholder="Example: What datasets were used?",
lines=2,
elem_classes=["rag-question"],
)
rag_btn = gr.Button("Ask", variant="primary")
rag_answer = gr.Markdown(elem_classes=["rag-answer"])
rag_json = gr.JSON(
label="RAG debug output",
elem_classes=["rag-debug"],
visible=False, # better for demo
)
with gr.Tab("⚙️ Advanced Analysis"):
with gr.Accordion("Model comparison, selection report, metadata", open=False):
advanced_md = gr.Markdown()
with gr.Tab("⬇️ Export"):
final_json = gr.JSON(label="Final paper card")
json_file = gr.File(label="Download JSON")
md_file = gr.File(label="Download Markdown report")
run_btn.click(
fn=analyze_paper,
inputs=[pdf_input, refinement_mode],
outputs=[
state,
quick_summary,
paper_md,
lab_md,
evidence_md,
advanced_md,
lab_cards_html,
final_json,
json_file,
md_file,
],
show_progress=True,
)
rag_btn.click(
fn=ask_paper_question,
inputs=[state, rag_question],
outputs=[rag_answer, rag_json],
show_progress=True,
)
if __name__ == "__main__":
demo.queue()
demo.launch()