|
|
|
|
|
""" |
|
|
BibGuard Gradio Web Application |
|
|
|
|
|
A web interface for checking bibliography and LaTeX quality. |
|
|
""" |
|
|
import gradio as gr |
|
|
import tempfile |
|
|
import shutil |
|
|
from pathlib import Path |
|
|
from typing import Optional, Tuple |
|
|
import base64 |
|
|
|
|
|
from src.parsers import BibParser, TexParser |
|
|
from src.fetchers import ArxivFetcher, CrossRefFetcher, SemanticScholarFetcher, OpenAlexFetcher, DBLPFetcher |
|
|
from src.analyzers import MetadataComparator, UsageChecker, DuplicateDetector |
|
|
from src.report.generator import ReportGenerator, EntryReport |
|
|
from src.config.yaml_config import BibGuardConfig, FilesConfig, BibliographyConfig, SubmissionConfig, OutputConfig, WorkflowStep |
|
|
from src.config.workflow import WorkflowConfig, WorkflowStep as WFStep, get_default_workflow |
|
|
from src.checkers import CHECKER_REGISTRY |
|
|
from src.report.line_report import LineByLineReportGenerator |
|
|
from app_helper import fetch_and_compare_with_workflow |
|
|
|
|
|
|
|
|
|
|
|
CUSTOM_CSS = """ |
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); |
|
|
|
|
|
* { |
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; |
|
|
} |
|
|
""" |
|
|
|
|
|
WELCOME_HTML = """ |
|
|
<div class="scrollable-report-area"> |
|
|
<div class="report-card" style="max-width: 800px; margin: 0 auto;"> |
|
|
<div class="card-header"> |
|
|
<h3 class="card-title" style="font-size: 1.5em;">π Welcome to BibGuard</h3> |
|
|
</div> |
|
|
<div class="card-content" style="line-height: 1.6; color: #374151;"> |
|
|
<p style="font-size: 1.1em; margin-bottom: 24px;"> |
|
|
Ensure your academic paper is flawless. Upload your <code>.bib</code> and <code>.tex</code> files on the left and click <strong>"Check Now"</strong>. |
|
|
</p> |
|
|
|
|
|
<div style="display: grid; gap: 20px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));"> |
|
|
<div style="background: #fefce8; padding: 16px; border-radius: 8px; border: 1px solid #fde047;"> |
|
|
<strong style="color: #854d0e; display: block; margin-bottom: 8px;">β οΈ Metadata Check Defaults</strong> |
|
|
"π Metadata" is <strong>disabled by default</strong>. It verifies your entries against ArXiv/DBLP/Crossref but takes time (1-3 mins) to fetch data. Enable it if you want strict verification. |
|
|
</div> |
|
|
|
|
|
<div style="background: #eff6ff; padding: 16px; border-radius: 8px; border: 1px solid #bfdbfe;"> |
|
|
<strong style="color: #1e40af; display: block; margin-bottom: 8px;">π Go Pro with Local Version</strong> |
|
|
LLM-based context relevance checking (is this citation actually relevant?) is excluded here. Clone the <a href="https://github.com/thinkwee/BibGuard" target="_blank" style="color: #2563eb; text-decoration: underline; font-weight: 600;">GitHub repo</a> to use the full power with your API key. |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<h4 style="margin: 24px 0 12px 0; color: #111827; font-size: 1.1em;">π Understanding Your Reports</h4> |
|
|
<div style="display: grid; gap: 12px;"> |
|
|
<div style="display: flex; gap: 12px; align-items: baseline;"> |
|
|
<span style="background: #e0e7ff; color: #3730a3; padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 600; white-space: nowrap;">π Bibliography</span> |
|
|
<span>Validates metadata fields, detects duplicates, and checks citation counts.</span> |
|
|
</div> |
|
|
<div style="display: flex; gap: 12px; align-items: baseline;"> |
|
|
<span style="background: #dcfce7; color: #166534; padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 600; white-space: nowrap;">π LaTeX Quality</span> |
|
|
<span>Syntax check, caption validation, acronym consistency, and style suggestions.</span> |
|
|
</div> |
|
|
<div style="display: flex; gap: 12px; align-items: baseline;"> |
|
|
<span style="background: #f3f4f6; color: #4b5563; padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 600; white-space: nowrap;">π Line-by-Line</span> |
|
|
<span>Maps every issue found directly to the line number in your source file.</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
""" |
|
|
|
|
|
CUSTOM_CSS += """ |
|
|
/* Global Reset */ |
|
|
body, gradio-app { |
|
|
overflow: hidden !important; /* Prevent double scrollbars on the page */ |
|
|
} |
|
|
|
|
|
.gradio-container { |
|
|
max-width: none !important; |
|
|
width: 100% !important; |
|
|
/* height: 100vh !important; <-- Removed to prevent iframe infinite loop */ |
|
|
padding: 0 !important; |
|
|
margin: 0 !important; |
|
|
} |
|
|
|
|
|
/* Header Styling */ |
|
|
.app-header { |
|
|
padding: 20px; |
|
|
background: white; |
|
|
border-bottom: 1px solid #e5e7eb; |
|
|
} |
|
|
|
|
|
/* Sidebar Styling */ |
|
|
.app-sidebar { |
|
|
height: auto !important; |
|
|
max-height: calc(100vh - 100px) !important; |
|
|
overflow-y: auto !important; |
|
|
padding: 20px !important; |
|
|
border-right: 1px solid #e5e7eb; |
|
|
} |
|
|
|
|
|
/* Main Content Area */ |
|
|
.app-content { |
|
|
height: auto !important; |
|
|
max-height: calc(100vh - 100px) !important; |
|
|
padding: 0 !important; |
|
|
} |
|
|
|
|
|
/* The Magic Scroll Container - Clean and Explicit */ |
|
|
.scrollable-report-area { |
|
|
/* Fixed height relative to viewport can cause loops in Spaces */ |
|
|
max-height: 800px !important; |
|
|
height: auto !important; |
|
|
min-height: 500px !important; |
|
|
overflow-y: auto !important; |
|
|
padding: 24px; |
|
|
background-color: #f9fafb; |
|
|
border: 1px solid #e5e7eb; |
|
|
border-radius: 8px; |
|
|
margin-top: 10px; |
|
|
} |
|
|
|
|
|
/* Report Card Styling */ |
|
|
.report-card { |
|
|
background: white; |
|
|
border-radius: 12px; |
|
|
padding: 24px; |
|
|
margin-bottom: 16px; /* Spacing between cards */ |
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
|
|
border: 1px solid #e5e7eb; |
|
|
transition: transform 0.2s, box-shadow 0.2s; |
|
|
} |
|
|
|
|
|
.report-card:hover { |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
|
transform: translateY(-2px); |
|
|
} |
|
|
|
|
|
/* Card Internals */ |
|
|
.card-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: flex-start; |
|
|
margin-bottom: 16px; |
|
|
padding-bottom: 16px; |
|
|
border-bottom: 1px solid #f3f4f6; |
|
|
} |
|
|
|
|
|
.card-title { |
|
|
font-size: 1.1em; |
|
|
font-weight: 600; |
|
|
color: #111827; |
|
|
margin: 0 0 4px 0; |
|
|
} |
|
|
|
|
|
.card-subtitle { |
|
|
font-size: 0.9em; |
|
|
color: #6b7280; |
|
|
font-family: monospace; |
|
|
} |
|
|
|
|
|
.card-content { |
|
|
font-size: 0.95em; |
|
|
color: #374151; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
/* Badges */ |
|
|
.badge { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
padding: 4px 10px; |
|
|
border-radius: 9999px; |
|
|
font-size: 0.8em; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.badge-success { background-color: #dcfce7; color: #166534; } |
|
|
.badge-warning { background-color: #fef9c3; color: #854d0e; } |
|
|
.badge-error { background-color: #fee2e2; color: #991b1b; } |
|
|
.badge-info { background-color: #dbeafe; color: #1e40af; } |
|
|
.badge-neutral { background-color: #f3f4f6; color: #4b5563; } |
|
|
|
|
|
/* Stats Grid */ |
|
|
.stats-container { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); |
|
|
gap: 16px; |
|
|
margin-bottom: 24px; |
|
|
} |
|
|
|
|
|
.stat-card { |
|
|
padding: 16px; |
|
|
border-radius: 12px; |
|
|
color: white; |
|
|
text-align: center; |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
|
|
|
.stat-value { font-size: 1.8em; font-weight: 700; } |
|
|
.stat-label { font-size: 0.9em; opacity: 0.9; } |
|
|
|
|
|
/* Detail Grid - Flexbox for better filling */ |
|
|
.detail-grid { |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
gap: 12px; |
|
|
margin-bottom: 16px; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
.detail-item { |
|
|
background: #f9fafb; |
|
|
padding: 10px 12px; |
|
|
border-radius: 8px; |
|
|
border: 1px solid #f3f4f6; |
|
|
|
|
|
/* Flex sizing: grow, shrink, min-basis */ |
|
|
flex: 1 1 160px; |
|
|
min-width: 0; /* Important for word-break to work in flex children */ |
|
|
|
|
|
/* Layout control */ |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
|
|
|
/* Height constraint to prevent one huge card from stretching the row */ |
|
|
max-height: 100px; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
/* Custom scrollbar for detail items */ |
|
|
.detail-item::-webkit-scrollbar { |
|
|
width: 4px; |
|
|
} |
|
|
.detail-item::-webkit-scrollbar-thumb { |
|
|
background-color: #d1d5db; |
|
|
border-radius: 4px; |
|
|
} |
|
|
|
|
|
.detail-label { |
|
|
font-size: 0.75em; |
|
|
color: #6b7280; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.05em; |
|
|
margin-bottom: 2px; |
|
|
position: sticky; |
|
|
top: 0; |
|
|
background: #f9fafb; /* Maintain bg on scroll */ |
|
|
z-index: 1; |
|
|
} |
|
|
|
|
|
.detail-value { |
|
|
font-weight: 500; |
|
|
color: #1f2937; |
|
|
font-size: 0.9em; |
|
|
line-height: 1.4; |
|
|
word-break: break-word; /* Fix overflow */ |
|
|
overflow-wrap: break-word; |
|
|
} border: 1px solid #e5e7eb; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.report-card:hover { |
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); |
|
|
} |
|
|
|
|
|
/* Card Header */ |
|
|
.card-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: flex-start; |
|
|
margin-bottom: 12px; |
|
|
border-bottom: 1px solid #f3f4f6; |
|
|
padding-bottom: 12px; |
|
|
} |
|
|
|
|
|
.card-title { |
|
|
font-size: 1.1em; |
|
|
font-weight: 600; |
|
|
color: #1f2937; |
|
|
margin: 0; |
|
|
} |
|
|
|
|
|
.card-subtitle { |
|
|
font-size: 0.9em; |
|
|
color: #6b7280; |
|
|
margin-top: 4px; |
|
|
} |
|
|
|
|
|
/* Status Badges */ |
|
|
.badge { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
padding: 4px 10px; |
|
|
border-radius: 9999px; |
|
|
font-size: 0.8em; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.badge-success { background-color: #dcfce7; color: #166534; } |
|
|
.badge-warning { background-color: #fef9c3; color: #854d0e; } |
|
|
.badge-error { background-color: #fee2e2; color: #991b1b; } |
|
|
.badge-info { background-color: #dbeafe; color: #1e40af; } |
|
|
.badge-neutral { background-color: #f3f4f6; color: #374151; } |
|
|
|
|
|
/* Content Styling */ |
|
|
.card-content { |
|
|
font-size: 15px; |
|
|
color: #374151; |
|
|
line-height: 1.6; |
|
|
} |
|
|
|
|
|
.card-content code { |
|
|
background-color: #f3f4f6; |
|
|
padding: 2px 6px; |
|
|
border-radius: 4px; |
|
|
font-family: monospace; |
|
|
font-size: 0.9em; |
|
|
color: #c2410c; |
|
|
} |
|
|
|
|
|
/* Grid for details */ |
|
|
.detail-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
|
|
gap: 12px; |
|
|
margin-top: 12px; |
|
|
} |
|
|
|
|
|
.detail-item { |
|
|
background: #f9fafb; |
|
|
padding: 10px; |
|
|
border-radius: 6px; |
|
|
} |
|
|
|
|
|
.detail-label { |
|
|
font-size: 0.8em; |
|
|
color: #6b7280; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.05em; |
|
|
} |
|
|
|
|
|
.detail-value { |
|
|
font-weight: 500; |
|
|
color: #111827; |
|
|
} |
|
|
|
|
|
/* Summary Stats */ |
|
|
.stats-container { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(3, 1fr); |
|
|
gap: 16px; |
|
|
margin-bottom: 24px; |
|
|
} |
|
|
|
|
|
.stat-card { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
color: white; |
|
|
padding: 20px; |
|
|
border-radius: 12px; |
|
|
text-align: center; |
|
|
box-shadow: 0 4px 6px rgba(102, 126, 234, 0.25); |
|
|
} |
|
|
|
|
|
.stat-value { |
|
|
font-size: 2em; |
|
|
font-weight: 700; |
|
|
} |
|
|
|
|
|
.stat-label { |
|
|
font-size: 0.9em; |
|
|
opacity: 0.9; |
|
|
margin-top: 4px; |
|
|
} |
|
|
|
|
|
/* Button styling */ |
|
|
.primary-btn { |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; |
|
|
border: none !important; |
|
|
font-weight: 600 !important; |
|
|
} |
|
|
|
|
|
/* Tab styling */ |
|
|
.tab-nav button { |
|
|
font-weight: 500 !important; |
|
|
font-size: 15px !important; |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
def create_config_from_ui( |
|
|
check_metadata: bool, |
|
|
check_usage: bool, |
|
|
check_duplicates: bool, |
|
|
check_preprint_ratio: bool, |
|
|
caption: bool, |
|
|
reference: bool, |
|
|
formatting: bool, |
|
|
equation: bool, |
|
|
ai_artifacts: bool, |
|
|
sentence: bool, |
|
|
consistency: bool, |
|
|
acronym: bool, |
|
|
number: bool, |
|
|
citation_quality: bool, |
|
|
anonymization: bool |
|
|
) -> BibGuardConfig: |
|
|
"""Create a BibGuardConfig from UI settings.""" |
|
|
config = BibGuardConfig() |
|
|
|
|
|
config.bibliography = BibliographyConfig( |
|
|
check_metadata=check_metadata, |
|
|
check_usage=check_usage, |
|
|
check_duplicates=check_duplicates, |
|
|
check_preprint_ratio=check_preprint_ratio, |
|
|
check_relevance=False |
|
|
) |
|
|
|
|
|
config.submission = SubmissionConfig( |
|
|
caption=caption, |
|
|
reference=reference, |
|
|
formatting=formatting, |
|
|
equation=equation, |
|
|
ai_artifacts=ai_artifacts, |
|
|
sentence=sentence, |
|
|
consistency=consistency, |
|
|
acronym=acronym, |
|
|
number=number, |
|
|
citation_quality=citation_quality, |
|
|
anonymization=anonymization |
|
|
) |
|
|
|
|
|
config.output = OutputConfig(quiet=True, minimal_verified=False) |
|
|
|
|
|
return config |
|
|
|
|
|
|
|
|
def generate_bibliography_html(report_gen: ReportGenerator, entries: list) -> str: |
|
|
"""Generate HTML content for bibliography report.""" |
|
|
html = ['<div class="scrollable-report-area">'] |
|
|
|
|
|
|
|
|
total = len(entries) |
|
|
verified = sum(1 for e in report_gen.entries if e.comparison and e.comparison.is_match) |
|
|
used = sum(1 for e in report_gen.entries if e.usage and e.usage.is_used) |
|
|
|
|
|
html.append('<div class="stats-container">') |
|
|
html.append(f'<div class="stat-card"><div class="stat-value">{total}</div><div class="stat-label">Total Entries</div></div>') |
|
|
html.append(f'<div class="stat-card"><div class="stat-value">{verified}</div><div class="stat-label">Verified</div></div>') |
|
|
html.append(f'<div class="stat-card"><div class="stat-value">{used}</div><div class="stat-label">Used in Text</div></div>') |
|
|
html.append('</div>') |
|
|
|
|
|
|
|
|
for report in report_gen.entries: |
|
|
entry = report.entry |
|
|
status_badges = [] |
|
|
|
|
|
|
|
|
if report.comparison: |
|
|
if report.comparison.is_match: |
|
|
status_badges.append('<span class="badge badge-success">β Verified</span>') |
|
|
if report.comparison.source: |
|
|
status_badges.append(f'<span class="badge badge-info">{report.comparison.source.upper()}</span>') |
|
|
else: |
|
|
status_badges.append('<span class="badge badge-error">β Metadata Mismatch</span>') |
|
|
else: |
|
|
status_badges.append('<span class="badge badge-neutral">No Metadata Check</span>') |
|
|
|
|
|
|
|
|
if report.usage: |
|
|
if report.usage.is_used: |
|
|
status_badges.append(f'<span class="badge badge-success">Used: {report.usage.usage_count}x</span>') |
|
|
else: |
|
|
status_badges.append('<span class="badge badge-warning">Unused</span>') |
|
|
|
|
|
|
|
|
html.append(f''' |
|
|
<div class="report-card"> |
|
|
<div class="card-header"> |
|
|
<div> |
|
|
<h3 class="card-title">{entry.title or "No Title"}</h3> |
|
|
<div class="card-subtitle">{entry.key} β’ {entry.year} β’ {entry.entry_type}</div> |
|
|
</div> |
|
|
<div style="display: flex; gap: 8px;"> |
|
|
{" ".join(status_badges)} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="card-content"> |
|
|
<div class="detail-grid"> |
|
|
{ |
|
|
(lambda e: "".join([ |
|
|
f'<div class="detail-item"><div class="detail-label">{k}</div><div class="detail-value">{v}</div></div>' |
|
|
for k, v in filter(None, [ |
|
|
("Authors", e.author or "N/A"), |
|
|
("Venue", e.journal or e.booktitle or e.publisher or "N/A"), |
|
|
("DOI", e.doi) if e.doi else None, |
|
|
("ArXiv", e.arxiv_id) if e.arxiv_id and not e.doi else None, |
|
|
("Volume/Pages", f"{'Vol.'+e.volume if e.volume else ''} {'pp.'+e.pages if e.pages else ''}".strip()) if e.volume or e.pages else None, |
|
|
("URL", f'<a href="{e.url}" target="_blank" style="text-decoration:underline;">Link</a>') if e.url else None |
|
|
]) |
|
|
]))(entry) |
|
|
} |
|
|
</div> |
|
|
''') |
|
|
|
|
|
|
|
|
issues = [] |
|
|
if report.comparison and not report.comparison.is_match: |
|
|
|
|
|
if report.comparison.issues: |
|
|
for issue in report.comparison.issues: |
|
|
issues.append(f'<div style="margin-left: 20px; font-size: 0.9em; color: #b91c1c;">β’ {issue}</div>') |
|
|
else: |
|
|
issues.append(f'<div style="margin-left: 20px; font-size: 0.9em; color: #b91c1c;">β’ Verification failed</div>') |
|
|
|
|
|
if issues: |
|
|
html.append('<div style="margin-top: 16px; padding-top: 12px; border-top: 1px solid #eee;">') |
|
|
html.append("".join(issues)) |
|
|
html.append('</div>') |
|
|
|
|
|
html.append('</div></div>') |
|
|
|
|
|
html.append('</div>') |
|
|
return "".join(html) |
|
|
|
|
|
def generate_latex_html(results: list) -> str: |
|
|
"""Generate HTML for LaTeX quality check.""" |
|
|
from src.checkers import CheckSeverity |
|
|
|
|
|
html = ['<div class="scrollable-report-area">'] |
|
|
|
|
|
|
|
|
errors = sum(1 for r in results if r.severity == CheckSeverity.ERROR) |
|
|
warnings = sum(1 for r in results if r.severity == CheckSeverity.WARNING) |
|
|
infos = sum(1 for r in results if r.severity == CheckSeverity.INFO) |
|
|
|
|
|
html.append('<div class="stats-container">') |
|
|
html.append(f'<div class="stat-card" style="background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%);"><div class="stat-value">{errors}</div><div class="stat-label">Errors</div></div>') |
|
|
html.append(f'<div class="stat-card" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);"><div class="stat-value">{warnings}</div><div class="stat-label">Warnings</div></div>') |
|
|
html.append(f'<div class="stat-card" style="background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);"><div class="stat-value">{infos}</div><div class="stat-label">Suggestions</div></div>') |
|
|
html.append('</div>') |
|
|
|
|
|
if not results: |
|
|
html.append('<div class="report-card"><div class="card-content" style="text-align: center; padding: 40px; color: #166534; font-size: 1.2em;">β
No issues found in LaTeX code!</div></div>') |
|
|
else: |
|
|
|
|
|
results.sort(key=lambda x: x.checker_name) |
|
|
current_checker = None |
|
|
|
|
|
for result in results: |
|
|
badge_class = "badge-neutral" |
|
|
if result.severity == CheckSeverity.ERROR: badge_class = "badge-error" |
|
|
elif result.severity == CheckSeverity.WARNING: badge_class = "badge-warning" |
|
|
elif result.severity == CheckSeverity.INFO: badge_class = "badge-info" |
|
|
|
|
|
html.append(f''' |
|
|
<div class="report-card"> |
|
|
<div class="card-header"> |
|
|
<div> |
|
|
<h3 class="card-title">{result.checker_name}</h3> |
|
|
<div class="card-subtitle">Line {result.line_number}</div> |
|
|
</div> |
|
|
<span class="badge {badge_class}">{result.severity.name}</span> |
|
|
</div> |
|
|
<div class="card-content"> |
|
|
{result.message} |
|
|
{f'<div style="margin-top: 8px; background: #f3f4f6; padding: 8px; border-radius: 4px; font-family: monospace;">{result.line_content}</div>' if result.line_content else ''} |
|
|
{f'<div style="margin-top: 8px; color: #166534;">π‘ Suggestion: {result.suggestion}</div>' if result.suggestion else ''} |
|
|
</div> |
|
|
</div> |
|
|
''') |
|
|
|
|
|
html.append('</div>') |
|
|
return "".join(html) |
|
|
|
|
|
def generate_line_html(content: str, results: list) -> str: |
|
|
"""Generate HTML for Line-by-Line report.""" |
|
|
|
|
|
issues_by_line = {} |
|
|
for r in results: |
|
|
if r.line_number not in issues_by_line: |
|
|
issues_by_line[r.line_number] = [] |
|
|
issues_by_line[r.line_number].append(r) |
|
|
|
|
|
lines = content.split('\n') |
|
|
|
|
|
html = ['<div class="scrollable-report-area">'] |
|
|
|
|
|
html.append('<div class="report-card"><div class="card-content">Issues are mapped to specific lines below.</div></div>') |
|
|
|
|
|
for i, line in enumerate(lines, 1): |
|
|
if i in issues_by_line: |
|
|
|
|
|
line_issues = issues_by_line[i] |
|
|
|
|
|
html.append(f''' |
|
|
<div class="report-card" style="border-left: 4px solid #ef4444; padding: 12px;"> |
|
|
<div style="font-family: monospace; color: #6b7280; font-size: 0.9em; margin-bottom: 4px;">Line {i}</div> |
|
|
<div style="font-family: monospace; background: #fee2e2; padding: 4px; border-radius: 4px; overflow-x: auto; white-space: pre;">{line}</div> |
|
|
<div style="margin-top: 8px;"> |
|
|
''') |
|
|
|
|
|
for issue in line_issues: |
|
|
html.append(f'<div style="color: #991b1b; font-size: 0.95em; margin-top: 4px;">β’ {issue.message}</div>') |
|
|
|
|
|
html.append('</div></div>') |
|
|
|
|
|
html.append('</div>') |
|
|
return "".join(html) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_check( |
|
|
bib_file, |
|
|
tex_file, |
|
|
check_metadata: bool, |
|
|
check_usage: bool, |
|
|
check_duplicates: bool, |
|
|
check_preprint_ratio: bool, |
|
|
caption: bool, |
|
|
reference: bool, |
|
|
formatting: bool, |
|
|
equation: bool, |
|
|
ai_artifacts: bool, |
|
|
sentence: bool, |
|
|
consistency: bool, |
|
|
acronym: bool, |
|
|
number: bool, |
|
|
citation_quality: bool, |
|
|
anonymization: bool, |
|
|
progress=gr.Progress() |
|
|
) -> Tuple[str, str, str]: |
|
|
"""Run BibGuard checks and return three reports.""" |
|
|
|
|
|
if bib_file is None or tex_file is None: |
|
|
return ( |
|
|
"β οΈ Please upload both `.bib` and `.tex` files.", |
|
|
"β οΈ Please upload both `.bib` and `.tex` files.", |
|
|
"β οΈ Please upload both `.bib` and `.tex` files." |
|
|
) |
|
|
|
|
|
try: |
|
|
|
|
|
config = create_config_from_ui( |
|
|
check_metadata, check_usage, check_duplicates, check_preprint_ratio, |
|
|
caption, reference, formatting, equation, ai_artifacts, |
|
|
sentence, consistency, acronym, number, citation_quality, anonymization |
|
|
) |
|
|
|
|
|
|
|
|
bib_path = bib_file.name |
|
|
tex_path = tex_file.name |
|
|
|
|
|
|
|
|
tex_content = Path(tex_path).read_text(encoding='utf-8', errors='replace') |
|
|
|
|
|
|
|
|
bib_parser = BibParser() |
|
|
entries = bib_parser.parse_file(bib_path) |
|
|
|
|
|
tex_parser = TexParser() |
|
|
tex_parser.parse_file(tex_path) |
|
|
|
|
|
bib_config = config.bibliography |
|
|
|
|
|
|
|
|
arxiv_fetcher = None |
|
|
crossref_fetcher = None |
|
|
semantic_scholar_fetcher = None |
|
|
openalex_fetcher = None |
|
|
dblp_fetcher = None |
|
|
comparator = None |
|
|
usage_checker = None |
|
|
duplicate_detector = None |
|
|
|
|
|
if bib_config.check_metadata: |
|
|
arxiv_fetcher = ArxivFetcher() |
|
|
semantic_scholar_fetcher = SemanticScholarFetcher() |
|
|
openalex_fetcher = OpenAlexFetcher() |
|
|
dblp_fetcher = DBLPFetcher() |
|
|
crossref_fetcher = CrossRefFetcher() |
|
|
comparator = MetadataComparator() |
|
|
|
|
|
if bib_config.check_usage: |
|
|
usage_checker = UsageChecker(tex_parser) |
|
|
|
|
|
if bib_config.check_duplicates: |
|
|
duplicate_detector = DuplicateDetector() |
|
|
|
|
|
|
|
|
report_gen = ReportGenerator( |
|
|
minimal_verified=False, |
|
|
check_preprint_ratio=bib_config.check_preprint_ratio, |
|
|
preprint_warning_threshold=bib_config.preprint_warning_threshold |
|
|
) |
|
|
report_gen.set_metadata([bib_file.name], [tex_file.name]) |
|
|
|
|
|
|
|
|
progress(0.2, desc="Running LaTeX quality checks...") |
|
|
submission_results = [] |
|
|
enabled_checkers = config.submission.get_enabled_checkers() |
|
|
|
|
|
for checker_name in enabled_checkers: |
|
|
if checker_name in CHECKER_REGISTRY: |
|
|
checker = CHECKER_REGISTRY[checker_name]() |
|
|
results = checker.check(tex_content, {}) |
|
|
for r in results: |
|
|
r.file_path = tex_file.name |
|
|
submission_results.extend(results) |
|
|
|
|
|
report_gen.set_submission_results(submission_results, None) |
|
|
|
|
|
|
|
|
if bib_config.check_duplicates and duplicate_detector: |
|
|
duplicate_groups = duplicate_detector.find_duplicates(entries) |
|
|
report_gen.set_duplicate_groups(duplicate_groups) |
|
|
|
|
|
|
|
|
if bib_config.check_usage and usage_checker: |
|
|
missing = usage_checker.get_missing_entries(entries) |
|
|
report_gen.set_missing_citations(missing) |
|
|
|
|
|
|
|
|
workflow_config = get_default_workflow() |
|
|
|
|
|
|
|
|
progress(0.3, desc="Processing bibliography entries...") |
|
|
total_entries = len(entries) |
|
|
|
|
|
for i, entry in enumerate(entries): |
|
|
progress(0.3 + 0.5 * (i / total_entries), desc=f"Checking: {entry.key}") |
|
|
|
|
|
|
|
|
usage_result = None |
|
|
if usage_checker: |
|
|
usage_result = usage_checker.check_usage(entry) |
|
|
|
|
|
|
|
|
comparison_result = None |
|
|
if bib_config.check_metadata and comparator: |
|
|
comparison_result = fetch_and_compare_with_workflow( |
|
|
entry, workflow_config, arxiv_fetcher, crossref_fetcher, |
|
|
semantic_scholar_fetcher, openalex_fetcher, dblp_fetcher, comparator |
|
|
) |
|
|
|
|
|
|
|
|
entry_report = EntryReport( |
|
|
entry=entry, |
|
|
comparison=comparison_result, |
|
|
usage=usage_result, |
|
|
evaluations=[] |
|
|
) |
|
|
report_gen.add_entry_report(entry_report) |
|
|
|
|
|
progress(0.85, desc="Generating structured reports...") |
|
|
|
|
|
|
|
|
bib_report = generate_bibliography_html(report_gen, entries) |
|
|
|
|
|
|
|
|
latex_report = generate_latex_html(submission_results) |
|
|
|
|
|
|
|
|
line_report = "" |
|
|
if submission_results: |
|
|
line_report = generate_line_html(tex_content, submission_results) |
|
|
else: |
|
|
line_report = '<div class="report-container"><div class="report-card"><div class="card-content">No issues to display line-by-line.</div></div></div>' |
|
|
|
|
|
progress(1.0, desc="Done!") |
|
|
|
|
|
return bib_report, latex_report, line_report |
|
|
|
|
|
except Exception as e: |
|
|
error_msg = f"β Error: {str(e)}" |
|
|
import traceback |
|
|
error_msg += f"\n\n```\n{traceback.format_exc()}\n```" |
|
|
return error_msg, error_msg, error_msg |
|
|
|
|
|
|
|
|
|
|
|
def create_app(): |
|
|
"""Create and configure the Gradio app.""" |
|
|
|
|
|
|
|
|
icon_html = "" |
|
|
try: |
|
|
icon_path = Path("assets/icon-192.png") |
|
|
if icon_path.exists(): |
|
|
with open(icon_path, "rb") as f: |
|
|
encoding = base64.b64encode(f.read()).decode() |
|
|
icon_html = f'<img src="data:image/png;base64,{encoding}" style="width: 48px; height: 48px; border-radius: 8px;" alt="BibGuard">' |
|
|
else: |
|
|
icon_html = '<span style="font-size: 48px;">π</span>' |
|
|
except Exception: |
|
|
icon_html = '<span style="font-size: 48px;">π</span>' |
|
|
|
|
|
with gr.Blocks(title="BibGuard - Bibliography & LaTeX Quality Checker") as app: |
|
|
|
|
|
|
|
|
with gr.Row(elem_classes=["app-header"]): |
|
|
gr.HTML(f""" |
|
|
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;"> |
|
|
{icon_html} |
|
|
<div> |
|
|
<h1 style="margin: 0; font-size: 1.8em;">BibGuard</h1> |
|
|
<p style="margin: 0; color: #666; font-size: 14px;">Bibliography & LaTeX Quality Checker</p> |
|
|
</div> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
with gr.Row(elem_classes=["app-body"]): |
|
|
|
|
|
with gr.Column(scale=1, min_width=280, elem_classes=["app-sidebar"]): |
|
|
gr.Markdown("### π Upload Files") |
|
|
|
|
|
bib_file = gr.File( |
|
|
label="Bibliography (.bib)", |
|
|
file_types=[".bib"], |
|
|
file_count="single" |
|
|
) |
|
|
|
|
|
tex_file = gr.File( |
|
|
label="LaTeX Source (.tex)", |
|
|
file_types=[".tex"], |
|
|
file_count="single" |
|
|
) |
|
|
|
|
|
|
|
|
gr.Markdown("#### βοΈ Options") |
|
|
|
|
|
with gr.Row(): |
|
|
check_metadata = gr.Checkbox(label="π Metadata", value=False) |
|
|
check_usage = gr.Checkbox(label="π Usage", value=True) |
|
|
|
|
|
with gr.Row(): |
|
|
check_duplicates = gr.Checkbox(label="π― Duplicates", value=True) |
|
|
check_preprint_ratio = gr.Checkbox(label="π Preprints", value=True) |
|
|
|
|
|
with gr.Row(): |
|
|
caption = gr.Checkbox(label="πΌοΈ Captions", value=True) |
|
|
reference = gr.Checkbox(label="π References", value=True) |
|
|
|
|
|
with gr.Row(): |
|
|
formatting = gr.Checkbox(label="β¨ Formatting", value=True) |
|
|
equation = gr.Checkbox(label="π’ Equations", value=True) |
|
|
|
|
|
with gr.Row(): |
|
|
ai_artifacts = gr.Checkbox(label="π€ AI Artifacts", value=True) |
|
|
sentence = gr.Checkbox(label="π Sentences", value=True) |
|
|
|
|
|
with gr.Row(): |
|
|
consistency = gr.Checkbox(label="π Consistency", value=True) |
|
|
acronym = gr.Checkbox(label="π€ Acronyms", value=True) |
|
|
|
|
|
with gr.Row(): |
|
|
number = gr.Checkbox(label="π’ Numbers", value=True) |
|
|
citation_quality = gr.Checkbox(label="π Citations", value=True) |
|
|
|
|
|
with gr.Row(): |
|
|
anonymization = gr.Checkbox(label="π Anonymization", value=True) |
|
|
|
|
|
run_btn = gr.Button("π Check Now", variant="primary", size="lg") |
|
|
|
|
|
gr.HTML(""" |
|
|
<div style="text-align: center; margin-top: 16px;"> |
|
|
<a href="https://github.com/thinkwee/BibGuard" target="_blank" style="text-decoration: none; color: #666; display: inline-flex; align-items: center; gap: 6px;"> |
|
|
<svg height="20" width="20" viewBox="0 0 16 16"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg> |
|
|
GitHub |
|
|
</a> |
|
|
<p style="margin: 8px 0 0 0; color: #999; font-size: 12px;">Developed with β€οΈ for researchers</p> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
with gr.Column(scale=4, elem_classes=["app-content"]): |
|
|
with gr.Tabs(): |
|
|
with gr.Tab("π Bibliography Report"): |
|
|
bib_report = gr.HTML( |
|
|
value=WELCOME_HTML, |
|
|
elem_classes=["report-panel"] |
|
|
) |
|
|
|
|
|
with gr.Tab("π LaTeX Quality"): |
|
|
latex_report = gr.HTML( |
|
|
value=WELCOME_HTML, |
|
|
elem_classes=["report-panel"] |
|
|
) |
|
|
|
|
|
with gr.Tab("π Line-by-Line"): |
|
|
line_report = gr.HTML( |
|
|
value=WELCOME_HTML, |
|
|
elem_classes=["report-panel"] |
|
|
) |
|
|
|
|
|
|
|
|
run_btn.click( |
|
|
fn=run_check, |
|
|
inputs=[ |
|
|
bib_file, tex_file, |
|
|
check_metadata, check_usage, check_duplicates, check_preprint_ratio, |
|
|
caption, reference, formatting, equation, ai_artifacts, |
|
|
sentence, consistency, acronym, number, citation_quality, anonymization |
|
|
], |
|
|
outputs=[bib_report, latex_report, line_report] |
|
|
) |
|
|
|
|
|
return app |
|
|
|
|
|
|
|
|
|
|
|
app = create_app() |
|
|
|
|
|
if __name__ == "__main__": |
|
|
app.launch( |
|
|
favicon_path="assets/icon-192.png", |
|
|
show_error=True, |
|
|
css=CUSTOM_CSS, |
|
|
theme=gr.themes.Soft() |
|
|
) |
|
|
|