from rich.console import Console from rich.table import Table from rich.panel import Panel from rich.tree import Tree import copy class BibUI: """Handles all terminal UI interactions for BibGuard.""" def __init__(self): self.console = Console() def show_analysis_report(self, ok_entries, to_fix, to_review, to_remove): """Display the initial analysis summary table.""" table = Table(title="๐Ÿ“Š Analysis Report", show_header=True, header_style="bold magenta") table.add_column("Category", style="cyan") table.add_column("Count", justify="right") table.add_column("Description") table.add_row("โœ… Correct", str(len(ok_entries)), "Entries match valid metadata") table.add_row("๐Ÿ› ๏ธ To Fix", str(len(to_fix)), "[green]High confidence auto-fixes[/green]") table.add_row("๐Ÿ” Review", str(len(to_review)), "[yellow]Ambiguous or low confidence[/yellow]") table.add_row("๐Ÿ—‘๏ธ Remove", str(len(to_remove)), "[red]No metadata found (Hallucinations)[/red]") self.console.print(table) if not (to_fix or to_review or to_remove): self.console.print(Panel("[green]โœ“ No issues found. All entries are valid.[/green]", title="Status")) def show_manual_review(self, entry, best_res, candidates, apply_fix_func): """Display manual review table for a single entry.""" self.console.print(f"\n[bold]Entry: {entry.key}[/bold]") self.console.print(f"Title: {entry.title}") self.console.print(f"Year: {entry.year}") self.console.print(f"Auth: {entry.author}") cand_table = Table(show_header=True, header_style="bold blue") cand_table.add_column("#", style="dim", width=4) cand_table.add_column("Source", style="cyan", width=12) cand_table.add_column("Conf", justify="right") cand_table.add_column("Candidate Metadata (Fetched)", style="white") cand_table.add_column("Proposed Changes", style="green") for i, cand in enumerate(candidates, 1): # We need to simulate the fix to show changes # We pass the apply_fix function to avoid circular dependency or logic duplication temp_entry = copy.deepcopy(entry) changes = apply_fix_func(temp_entry, cand.fetched_data) change_desc = "\n".join(changes) if changes else "[dim]No changes[/dim]" conf_style = "green" if cand.confidence > 0.7 else "yellow" if cand.confidence > 0.4 else "red" # Format the candidate's actual metadata fd = cand.fetched_data meta_lines = [] if getattr(fd, 'title', None): meta_lines.append(f"[bold]Title:[/bold] {fd.title[:60] + '...' if len(fd.title) > 60 else fd.title}") if getattr(fd, 'authors', None): a_str = " and ".join(fd.authors) meta_lines.append(f"[bold]Authors:[/bold] {a_str[:60] + '...' if len(a_str) > 60 else a_str}") if getattr(fd, 'year', None): meta_lines.append(f"[bold]Year:[/bold] {fd.year}") if getattr(fd, 'doi', None): meta_lines.append(f"[bold]DOI:[/bold] {fd.doi}") meta_desc = "\n".join(meta_lines) if meta_lines else "[dim]No metadata details[/dim]" cand_table.add_row( str(i), cand.source, f"[{conf_style}]{cand.confidence:.2f}[/{conf_style}]", meta_desc, change_desc ) self.console.print(cand_table) def show_final_report(self, total, verified, issues, not_found, reports, fixed_count, fixed_details, removed_details): """Display the verification status and modification tree.""" # Visual Final Status status_table = Table(box=None, padding=(0, 2)) status_table.add_column("Metric", style="bold") status_table.add_column("Value", justify="right") status_table.add_row("Total Entries", str(total)) status_table.add_row("Verified", f"[green]{verified}[/green]") status_table.add_row("Issues", f"[red]{issues}[/red]" if issues > 0 else "0") status_table.add_row("Not Found", f"[yellow]{not_found}[/yellow]" if not_found > 0 else "0") self.console.print(Panel(status_table, title="๐Ÿ“Š Final Status", expand=False)) if issues > 0: self.console.print("\n[bold red]โš  Remaining Issues (Not Auto-Fixed):[/bold red]") for r in reports: if r.comparison and r.comparison.has_issues: self.console.print(f" - [bold]{r.entry.key}[/bold] (Conf: {r.comparison.confidence:.2f}): {', '.join(r.comparison.issues)}") # Report fixes and removals if fixed_count > 0 or removed_details: tree = Tree("โœ๏ธ Modifications Report") if removed_details: rem_node = tree.add(f"[red]Removed {len(removed_details)} entries[/red]") for entry, reason in removed_details: rem_node.add(f"[bold]{entry.key}[/bold]: \"{entry.title}\" ([italic]{reason}[/italic])") if fixed_count > 0: fix_node = tree.add(f"[green]Fixed {fixed_count} entries[/green]") for key, changes in fixed_details.items(): entry_node = fix_node.add(f"[bold]{key}[/bold]") for change in changes: entry_node.add(change) self.console.print(tree) self.console.print("\n[green]โœ“ Changes applied and saved to file.[/green]") else: self.console.print("\n[green]โœ“ No changes were needed.[/green]") def show_sanitize_report(self, sanitize_fixes: dict): """Display sanitization results as a rich tree.""" if not sanitize_fixes: self.console.print("[green]โœ“ No formatting issues found.[/green]\n") return # Category display info category_info = { "dblp_id": ("๐Ÿ”ข", "DBLP Disambiguation ID Cleanup", "red"), "corporate_author": ("๐Ÿข", "Corporate Author Protection", "yellow"), "entry_type": ("๐Ÿ“‹", "Entry Type Correction", "cyan"), "title_case": ("๐Ÿ”ค", "Title Capitalization Protection", "blue"), "doi_mismatch": ("๐Ÿ”—", "DOI Mismatch", "red"), "future_year": ("๐Ÿ“…", "Future Year Detection", "magenta"), "field_cleanup": ("๐Ÿงน", "Junk Field Removal", "dim"), } total_fixes = sum(len(fixes) for fixes in sanitize_fixes.values()) tree = Tree(f"๐Ÿงน Sanitization Report ({total_fixes} fixes in {len(sanitize_fixes)} entries)") # Group fixes by category across all entries by_category = {} for entry_key, fixes in sanitize_fixes.items(): for fix in fixes: if fix.category not in by_category: by_category[fix.category] = [] by_category[fix.category].append(fix) for cat, fixes in by_category.items(): icon, label, color = category_info.get(cat, ("โ“", cat, "white")) cat_node = tree.add(f"{icon} [{color}]{label} ({len(fixes)})[/{color}]") for fix in fixes: cat_node.add(f"[bold]{fix.entry_key}[/bold]: {fix.description}") self.console.print(tree) self.console.print("")