| |
| """ |
| 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() |
| ) |
|
|