"""Custom widgets for the Translation Viewer TUI.""" from __future__ import annotations from typing import TYPE_CHECKING from rich.console import RenderableType from rich.panel import Panel from rich.progress_bar import ProgressBar from rich.table import Table from rich.text import Text from textual.message import Message from textual.widgets import Static, Tree if TYPE_CHECKING: from src.tui.data_provider import TUIDataProvider, VerseData # Color thresholds for quality scores QUALITY_EXCELLENT = 90 # Green QUALITY_GOOD = 75 # Yellow QUALITY_CONCERN = 60 # Orange/Red class VerseSelected(Message): """Message sent when a verse is selected.""" def __init__(self, book: str, chapter: int, verse: int) -> None: """Initialize the message. Args: book: Book name. chapter: Chapter number. verse: Verse number. """ self.book = book self.chapter = chapter self.verse = verse super().__init__() class VerseTree(Tree[dict]): """Hierarchical tree for Book > Chapter > Verse navigation.""" def __init__( self, data_provider: TUIDataProvider, *, id: str | None = None, ) -> None: """Initialize the verse tree. Args: data_provider: Data provider for loading verse information. id: Widget ID. """ super().__init__("Books", id=id) self.data_provider = data_provider def on_mount(self) -> None: """Load available books when mounted.""" self._populate_tree() def _populate_tree(self) -> None: """Populate the tree with books, chapters, and verses.""" books = self.data_provider.get_available_books() for book in books: book_node = self.root.add(book.upper(), data={"type": "book", "book": book}) chapters = self.data_provider.get_book_chapters(book) for chapter in chapters: chapter_label = f"Chapter {chapter}" if self.data_provider.is_chapter_evaluated(book, chapter): chapter_label += " *" chapter_node = book_node.add( chapter_label, data={"type": "chapter", "book": book, "chapter": chapter}, ) verses = self.data_provider.get_chapter_verses(book, chapter) flagged = self.data_provider.get_flagged_verses(book, chapter) for verse in verses: verse_label = f"Verse {verse}" if verse in flagged: verse_label += " !" chapter_node.add( verse_label, data={ "type": "verse", "book": book, "chapter": chapter, "verse": verse, }, ) self.root.expand() def _handle_node_activation(self, node_data: dict | None) -> None: # pragma: no cover """Handle node activation (selection or highlight). Args: node_data: The data attached to the node. """ if node_data and node_data.get("type") == "verse": self.post_message( VerseSelected( book=node_data["book"], chapter=node_data["chapter"], verse=node_data["verse"], ) ) def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: # pragma: no cover """Handle node selection (Enter/double-click).""" self._handle_node_activation(event.node.data) def on_tree_node_highlighted(self, event: Tree.NodeHighlighted) -> None: # pragma: no cover """Handle node highlight (single-click/arrow keys).""" self._handle_node_activation(event.node.data) class DetailPanel(Static): """Panel showing verse details: sources, translation, evaluation.""" def __init__( self, data_provider: TUIDataProvider, *, id: str | None = None, ) -> None: """Initialize the detail panel. Args: data_provider: Data provider for loading verse data. id: Widget ID. """ super().__init__(id=id) self.data_provider = data_provider self.current_verse: VerseData | None = None def update_verse(self, book: str, chapter: int, verse: int) -> None: """Update the panel to show a specific verse. Args: book: Book name. chapter: Chapter number. verse: Verse number. """ self.current_verse = self.data_provider.get_verse_data(book, chapter, verse) # Use update() to force content refresh in Static widget self.update(self.render()) def render(self) -> RenderableType: """Render the panel content.""" if self.current_verse is None: return Panel( "Select a verse from the tree to view details.", title="Details", ) verse = self.current_verse content = Text() # Header content.append( f"{verse.book.upper()} {verse.chapter}:{verse.verse}\n", style="bold cyan", ) content.append("\n") # Source texts section content.append("SOURCE TEXTS\n", style="bold yellow") content.append("-" * 50 + "\n") if verse.greek_text: content.append("Greek: ", style="bold") content.append(f"{verse.greek_text}\n") else: content.append("Greek: ", style="bold") content.append("(not available)\n", style="dim") if verse.kjv_text: content.append("KJV: ", style="bold") content.append(f"{verse.kjv_text}\n") else: content.append("KJV: ", style="bold") content.append("(not available)\n", style="dim") if verse.japanese_reference: content.append("Ref (JP): ", style="bold") content.append(f"{verse.japanese_reference}\n") else: content.append("Ref (JP): ", style="bold") content.append("(not available)\n", style="dim") if verse.glossary_terms: content.append("Glossary: ", style="bold") content.append(", ".join(verse.glossary_terms) + "\n") content.append("\n") # Translation section content.append("TRANSLATION\n", style="bold yellow") content.append("-" * 50 + "\n") if verse.translation_text: content.append(f"{verse.translation_text}\n", style="green") if verse.translation_model: content.append(f"Model: {verse.translation_model}", style="dim") if verse.translation_timestamp: content.append(f" | {verse.translation_timestamp}", style="dim") content.append("\n") else: content.append("(not translated)\n", style="dim") content.append("\n") # Evaluation section content.append("EVALUATION\n", style="bold yellow") content.append("-" * 50 + "\n") if verse.evaluation: eval_data = verse.evaluation # Overall score with color-coded indicator overall = eval_data.overall_score overall_color = self._get_score_color(overall) overall_indicator = self._get_quality_indicator(overall) content.append("Overall: ", style="bold") content.append(f"{overall_indicator} ", style=overall_color) content.append(f"{overall:.1f}%\n", style=f"bold {overall_color}") content.append("\n") # Score bars for each dimension with improved display for dim, score in eval_data.scores.items(): dim_display = self._format_dimension_name(dim) color = self._get_score_color(score.score) # Create visual progress bar filled = int(score.score / 10) bar = "█" * filled + "░" * (10 - filled) content.append(f"{dim_display:22} ", style="bold") content.append(f"{bar} ", style=color) content.append(f"{score.score:5.1f}%", style=color) # Confidence indicator conf_indicator = "●" if score.confidence >= 80 else "○" conf_color = "green" if score.confidence >= 80 else "yellow" content.append(f" {conf_indicator}", style=conf_color) content.append(f" [{score.confidence:.0f}%]\n", style="dim") content.append("\n") # Flags section with icons if eval_data.flags: content.append("⚠ Flags: ", style="bold red") flag_text = ", ".join(self._format_flag(f) for f in eval_data.flags) content.append(f"{flag_text}\n", style="red") else: content.append("✓ No flags\n", style="green") # Reasoning summary (if available) if eval_data.scores: content.append("\nDetails:\n", style="bold") for dim, score in list(eval_data.scores.items())[:3]: if score.reasoning: dim_display = self._format_dimension_name(dim) content.append(f" {dim_display}: ", style="cyan") # Truncate long reasoning reasoning = score.reasoning[:80] if len(score.reasoning) > 80: reasoning += "..." content.append(f"{reasoning}\n", style="dim") # Notes if eval_data.notes: content.append("\nNotes: ", style="bold") content.append(f"{eval_data.notes}\n") else: content.append("(not evaluated)\n", style="dim") content.append( "Run translation with --evaluate flag to get quality metrics.\n", style="dim italic", ) return Panel(content, title="Verse Details") def _get_score_color(self, score: float) -> str: """Get color for a score value.""" if score >= QUALITY_EXCELLENT: return "green" elif score >= QUALITY_GOOD: return "yellow" elif score >= QUALITY_CONCERN: return "orange1" return "red" def _get_quality_indicator(self, score: float) -> str: """Get quality indicator emoji for a score.""" if score >= QUALITY_EXCELLENT: return "●" elif score >= QUALITY_GOOD: return "◐" elif score >= QUALITY_CONCERN: return "○" return "✗" def _format_dimension_name(self, dim: str) -> str: """Format dimension name for display.""" names = { "theological_accuracy": "Theology", "source_fidelity": "Fidelity", "naturalness": "Naturalness", "readability": "Readability", "glossary_consistency": "Glossary", "literary_quality": "Literary", } return names.get(dim, dim.replace("_", " ").title()) def _format_flag(self, flag: str) -> str: """Format flag name for display.""" return flag.replace("_", " ").replace("low ", "↓") class EvaluationSummaryTable(Static): """Display evaluation scores in a table format.""" def __init__(self, id: str | None = None) -> None: """Initialize the evaluation summary table.""" super().__init__(id=id) self.scores: dict[str, tuple[float, float]] = {} def set_scores(self, scores: dict[str, tuple[float, float]]) -> None: """Set the scores to display. Args: scores: Dictionary mapping dimension to (score, confidence) tuple. """ self.scores = scores # Use update() to force content refresh in Static widget self.update(self.render()) def render(self) -> RenderableType: """Render the evaluation table.""" if not self.scores: return Text("No evaluation data", style="dim") table = Table(title="Evaluation Scores", box=None) table.add_column("Dimension", style="cyan", width=20) table.add_column("Score", justify="right", width=12) table.add_column("Bar", width=12) table.add_column("Confidence", justify="right", width=10) table.add_column("Status", justify="center", width=8) for dim, (score, confidence) in self.scores.items(): dim_display = self._format_dimension_name(dim) # Score with color score_color = self._get_score_color(score) score_text = Text(f"{score:.1f}%", style=score_color) # Visual bar filled = int(score / 10) bar = Text("█" * filled + "░" * (10 - filled), style=score_color) # Confidence conf_text = Text(f"{confidence:.1f}%", style="dim") # Status indicator if score >= QUALITY_EXCELLENT: status = Text("●", style="green") elif score >= QUALITY_GOOD: status = Text("◐", style="yellow") elif score >= QUALITY_CONCERN: status = Text("○", style="orange1") else: status = Text("✗", style="red") table.add_row(dim_display, score_text, bar, conf_text, status) return table def _get_score_color(self, score: float) -> str: """Get color for a score value.""" if score >= QUALITY_EXCELLENT: return "green" elif score >= QUALITY_GOOD: return "yellow" elif score >= QUALITY_CONCERN: return "orange1" return "red" def _format_dimension_name(self, dim: str) -> str: """Format dimension name for display.""" names = { "theological_accuracy": "Theology", "source_fidelity": "Fidelity", "naturalness": "Naturalness", "readability": "Readability", "glossary_consistency": "Glossary", "literary_quality": "Literary", } return names.get(dim, dim.replace("_", " ").title()) class ChapterQualitySummary(Static): """Display quality summary for a chapter.""" def __init__( self, data_provider: TUIDataProvider, *, id: str | None = None, ) -> None: """Initialize the chapter quality summary. Args: data_provider: Data provider for loading evaluation data. id: Widget ID. """ super().__init__(id=id) self.data_provider = data_provider self.book: str | None = None self.chapter: int | None = None def update_chapter(self, book: str, chapter: int) -> None: """Update to show a specific chapter. Args: book: Book name. chapter: Chapter number. """ self.book = book self.chapter = chapter # Use update() to force content refresh in Static widget self.update(self.render()) def render(self) -> RenderableType: """Render the chapter quality summary.""" if self.book is None or self.chapter is None: return Panel( Text("Select a chapter to view quality summary.", style="dim"), title="Chapter Quality", ) # Load chapter evaluation chapter_eval = self.data_provider.eval_storage.load_chapter_evaluation( self.book, self.chapter ) content = Text() content.append(f"{self.book.upper()} Chapter {self.chapter}\n", style="bold cyan") content.append("\n") if not chapter_eval: content.append("No evaluation data available.\n", style="dim") content.append( "Translate with --evaluate to generate quality metrics.", style="dim italic", ) return Panel(content, title="Chapter Quality") # Overall score overall = chapter_eval.overall_score overall_color = self._get_score_color(overall) content.append("Overall Quality: ", style="bold") content.append(f"{overall:.1f}%\n", style=f"bold {overall_color}") content.append("\n") # Verse statistics total_verses = len(chapter_eval.verses) flagged_count = chapter_eval.flagged_count clean_count = total_verses - flagged_count content.append("Verses: ", style="bold") content.append(f"{total_verses} total, ", style="white") content.append(f"{clean_count} clean", style="green") if flagged_count > 0: content.append(f", {flagged_count} flagged", style="red") content.append("\n\n") # Aggregate scores by dimension content.append("Dimension Averages:\n", style="bold") for dim, avg_score in chapter_eval.aggregate_scores.items(): dim_display = self._format_dimension_name(dim) color = self._get_score_color(avg_score) filled = int(avg_score / 10) bar = "█" * filled + "░" * (10 - filled) content.append(f" {dim_display:15} ", style="bold") content.append(f"{bar} ", style=color) content.append(f"{avg_score:.1f}%\n", style=color) # Flagged verses list if chapter_eval.flagged_verses: content.append("\n⚠ Flagged Verses: ", style="bold red") flagged_str = ", ".join(str(v) for v in sorted(chapter_eval.flagged_verses)) content.append(f"{flagged_str}\n", style="red") return Panel(content, title="Chapter Quality Summary") def _get_score_color(self, score: float) -> str: """Get color for a score value.""" if score >= QUALITY_EXCELLENT: return "green" elif score >= QUALITY_GOOD: return "yellow" elif score >= QUALITY_CONCERN: return "orange1" return "red" def _format_dimension_name(self, dim: str) -> str: """Format dimension name for display.""" names = { "theological_accuracy": "Theology", "source_fidelity": "Fidelity", "naturalness": "Naturalness", "readability": "Readability", "glossary_consistency": "Glossary", "literary_quality": "Literary", } return names.get(dim, dim.replace("_", " ").title())