Pre-publication review draft
日常訳NICHIJOYAKU日本語 ↗

OPEN SOURCE

software/src/tui/widgets.py

Read on this site and download the files you need.

Nichijoyaku was created using AI. A complete review by human biblical-language and biblical-studies specialists has not been established. Read alongside established translations and original-language resources.

← Back to the open-source guide

software/src/tui/widgets.py525 linesDownload this file ↓

This is the same source included in the download. Each line number is a link you can save or share.

File verification detailsSHA-256 4784832f14cbe4553be0c9b2f798d198683aa1d732fde6cb1abda9c73c408278
1"""Custom widgets for the Translation Viewer TUI."""23from __future__ import annotations45from typing import TYPE_CHECKING67from rich.console import RenderableType8from rich.panel import Panel9from rich.progress_bar import ProgressBar10from rich.table import Table11from rich.text import Text12from textual.message import Message13from textual.widgets import Static, Tree1415if TYPE_CHECKING:16    from src.tui.data_provider import TUIDataProvider, VerseData1718# Color thresholds for quality scores19QUALITY_EXCELLENT = 90  # Green20QUALITY_GOOD = 75  # Yellow21QUALITY_CONCERN = 60  # Orange/Red222324class VerseSelected(Message):25    """Message sent when a verse is selected."""2627    def __init__(self, book: str, chapter: int, verse: int) -> None:28        """Initialize the message.2930        Args:31            book: Book name.32            chapter: Chapter number.33            verse: Verse number.34        """35        self.book = book36        self.chapter = chapter37        self.verse = verse38        super().__init__()394041class VerseTree(Tree[dict]):42    """Hierarchical tree for Book > Chapter > Verse navigation."""4344    def __init__(45        self,46        data_provider: TUIDataProvider,47        *,48        id: str | None = None,49    ) -> None:50        """Initialize the verse tree.5152        Args:53            data_provider: Data provider for loading verse information.54            id: Widget ID.55        """56        super().__init__("Books", id=id)57        self.data_provider = data_provider5859    def on_mount(self) -> None:60        """Load available books when mounted."""61        self._populate_tree()6263    def _populate_tree(self) -> None:64        """Populate the tree with books, chapters, and verses."""65        books = self.data_provider.get_available_books()6667        for book in books:68            book_node = self.root.add(book.upper(), data={"type": "book", "book": book})69            chapters = self.data_provider.get_book_chapters(book)7071            for chapter in chapters:72                chapter_label = f"Chapter {chapter}"73                if self.data_provider.is_chapter_evaluated(book, chapter):74                    chapter_label += " *"75                chapter_node = book_node.add(76                    chapter_label,77                    data={"type": "chapter", "book": book, "chapter": chapter},78                )7980                verses = self.data_provider.get_chapter_verses(book, chapter)81                flagged = self.data_provider.get_flagged_verses(book, chapter)8283                for verse in verses:84                    verse_label = f"Verse {verse}"85                    if verse in flagged:86                        verse_label += " !"87                    chapter_node.add(88                        verse_label,89                        data={90                            "type": "verse",91                            "book": book,92                            "chapter": chapter,93                            "verse": verse,94                        },95                    )9697        self.root.expand()9899    def _handle_node_activation(self, node_data: dict | None) -> None:  # pragma: no cover100        """Handle node activation (selection or highlight).101102        Args:103            node_data: The data attached to the node.104        """105        if node_data and node_data.get("type") == "verse":106            self.post_message(107                VerseSelected(108                    book=node_data["book"],109                    chapter=node_data["chapter"],110                    verse=node_data["verse"],111                )112            )113114    def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:  # pragma: no cover115        """Handle node selection (Enter/double-click)."""116        self._handle_node_activation(event.node.data)117118    def on_tree_node_highlighted(self, event: Tree.NodeHighlighted) -> None:  # pragma: no cover119        """Handle node highlight (single-click/arrow keys)."""120        self._handle_node_activation(event.node.data)121122123class DetailPanel(Static):124    """Panel showing verse details: sources, translation, evaluation."""125126    def __init__(127        self,128        data_provider: TUIDataProvider,129        *,130        id: str | None = None,131    ) -> None:132        """Initialize the detail panel.133134        Args:135            data_provider: Data provider for loading verse data.136            id: Widget ID.137        """138        super().__init__(id=id)139        self.data_provider = data_provider140        self.current_verse: VerseData | None = None141142    def update_verse(self, book: str, chapter: int, verse: int) -> None:143        """Update the panel to show a specific verse.144145        Args:146            book: Book name.147            chapter: Chapter number.148            verse: Verse number.149        """150        self.current_verse = self.data_provider.get_verse_data(book, chapter, verse)151        # Use update() to force content refresh in Static widget152        self.update(self.render())153154    def render(self) -> RenderableType:155        """Render the panel content."""156        if self.current_verse is None:157            return Panel(158                "Select a verse from the tree to view details.",159                title="Details",160            )161162        verse = self.current_verse163        content = Text()164165        # Header166        content.append(167            f"{verse.book.upper()} {verse.chapter}:{verse.verse}\n",168            style="bold cyan",169        )170        content.append("\n")171172        # Source texts section173        content.append("SOURCE TEXTS\n", style="bold yellow")174        content.append("-" * 50 + "\n")175176        if verse.greek_text:177            content.append("Greek: ", style="bold")178            content.append(f"{verse.greek_text}\n")179        else:180            content.append("Greek: ", style="bold")181            content.append("(not available)\n", style="dim")182183        if verse.kjv_text:184            content.append("KJV: ", style="bold")185            content.append(f"{verse.kjv_text}\n")186        else:187            content.append("KJV: ", style="bold")188            content.append("(not available)\n", style="dim")189190        if verse.japanese_reference:191            content.append("Ref (JP): ", style="bold")192            content.append(f"{verse.japanese_reference}\n")193        else:194            content.append("Ref (JP): ", style="bold")195            content.append("(not available)\n", style="dim")196197        if verse.glossary_terms:198            content.append("Glossary: ", style="bold")199            content.append(", ".join(verse.glossary_terms) + "\n")200201        content.append("\n")202203        # Translation section204        content.append("TRANSLATION\n", style="bold yellow")205        content.append("-" * 50 + "\n")206207        if verse.translation_text:208            content.append(f"{verse.translation_text}\n", style="green")209            if verse.translation_model:210                content.append(f"Model: {verse.translation_model}", style="dim")211                if verse.translation_timestamp:212                    content.append(f" | {verse.translation_timestamp}", style="dim")213                content.append("\n")214        else:215            content.append("(not translated)\n", style="dim")216217        content.append("\n")218219        # Evaluation section220        content.append("EVALUATION\n", style="bold yellow")221        content.append("-" * 50 + "\n")222223        if verse.evaluation:224            eval_data = verse.evaluation225226            # Overall score with color-coded indicator227            overall = eval_data.overall_score228            overall_color = self._get_score_color(overall)229            overall_indicator = self._get_quality_indicator(overall)230231            content.append("Overall: ", style="bold")232            content.append(f"{overall_indicator} ", style=overall_color)233            content.append(f"{overall:.1f}%\n", style=f"bold {overall_color}")234            content.append("\n")235236            # Score bars for each dimension with improved display237            for dim, score in eval_data.scores.items():238                dim_display = self._format_dimension_name(dim)239                color = self._get_score_color(score.score)240241                # Create visual progress bar242                filled = int(score.score / 10)243                bar = "█" * filled + "░" * (10 - filled)244245                content.append(f"{dim_display:22} ", style="bold")246                content.append(f"{bar} ", style=color)247                content.append(f"{score.score:5.1f}%", style=color)248249                # Confidence indicator250                conf_indicator = "●" if score.confidence >= 80 else "○"251                conf_color = "green" if score.confidence >= 80 else "yellow"252                content.append(f" {conf_indicator}", style=conf_color)253                content.append(f" [{score.confidence:.0f}%]\n", style="dim")254255            content.append("\n")256257            # Flags section with icons258            if eval_data.flags:259                content.append("⚠ Flags: ", style="bold red")260                flag_text = ", ".join(self._format_flag(f) for f in eval_data.flags)261                content.append(f"{flag_text}\n", style="red")262            else:263                content.append("✓ No flags\n", style="green")264265            # Reasoning summary (if available)266            if eval_data.scores:267                content.append("\nDetails:\n", style="bold")268                for dim, score in list(eval_data.scores.items())[:3]:269                    if score.reasoning:270                        dim_display = self._format_dimension_name(dim)271                        content.append(f"  {dim_display}: ", style="cyan")272                        # Truncate long reasoning273                        reasoning = score.reasoning[:80]274                        if len(score.reasoning) > 80:275                            reasoning += "..."276                        content.append(f"{reasoning}\n", style="dim")277278            # Notes279            if eval_data.notes:280                content.append("\nNotes: ", style="bold")281                content.append(f"{eval_data.notes}\n")282        else:283            content.append("(not evaluated)\n", style="dim")284            content.append(285                "Run translation with --evaluate flag to get quality metrics.\n",286                style="dim italic",287            )288289        return Panel(content, title="Verse Details")290291    def _get_score_color(self, score: float) -> str:292        """Get color for a score value."""293        if score >= QUALITY_EXCELLENT:294            return "green"295        elif score >= QUALITY_GOOD:296            return "yellow"297        elif score >= QUALITY_CONCERN:298            return "orange1"299        return "red"300301    def _get_quality_indicator(self, score: float) -> str:302        """Get quality indicator emoji for a score."""303        if score >= QUALITY_EXCELLENT:304            return "●"305        elif score >= QUALITY_GOOD:306            return "◐"307        elif score >= QUALITY_CONCERN:308            return "○"309        return "✗"310311    def _format_dimension_name(self, dim: str) -> str:312        """Format dimension name for display."""313        names = {314            "theological_accuracy": "Theology",315            "source_fidelity": "Fidelity",316            "naturalness": "Naturalness",317            "readability": "Readability",318            "glossary_consistency": "Glossary",319            "literary_quality": "Literary",320        }321        return names.get(dim, dim.replace("_", " ").title())322323    def _format_flag(self, flag: str) -> str:324        """Format flag name for display."""325        return flag.replace("_", " ").replace("low ", "↓")326327328class EvaluationSummaryTable(Static):329    """Display evaluation scores in a table format."""330331    def __init__(self, id: str | None = None) -> None:332        """Initialize the evaluation summary table."""333        super().__init__(id=id)334        self.scores: dict[str, tuple[float, float]] = {}335336    def set_scores(self, scores: dict[str, tuple[float, float]]) -> None:337        """Set the scores to display.338339        Args:340            scores: Dictionary mapping dimension to (score, confidence) tuple.341        """342        self.scores = scores343        # Use update() to force content refresh in Static widget344        self.update(self.render())345346    def render(self) -> RenderableType:347        """Render the evaluation table."""348        if not self.scores:349            return Text("No evaluation data", style="dim")350351        table = Table(title="Evaluation Scores", box=None)352        table.add_column("Dimension", style="cyan", width=20)353        table.add_column("Score", justify="right", width=12)354        table.add_column("Bar", width=12)355        table.add_column("Confidence", justify="right", width=10)356        table.add_column("Status", justify="center", width=8)357358        for dim, (score, confidence) in self.scores.items():359            dim_display = self._format_dimension_name(dim)360361            # Score with color362            score_color = self._get_score_color(score)363            score_text = Text(f"{score:.1f}%", style=score_color)364365            # Visual bar366            filled = int(score / 10)367            bar = Text("█" * filled + "░" * (10 - filled), style=score_color)368369            # Confidence370            conf_text = Text(f"{confidence:.1f}%", style="dim")371372            # Status indicator373            if score >= QUALITY_EXCELLENT:374                status = Text("●", style="green")375            elif score >= QUALITY_GOOD:376                status = Text("◐", style="yellow")377            elif score >= QUALITY_CONCERN:378                status = Text("○", style="orange1")379            else:380                status = Text("✗", style="red")381382            table.add_row(dim_display, score_text, bar, conf_text, status)383384        return table385386    def _get_score_color(self, score: float) -> str:387        """Get color for a score value."""388        if score >= QUALITY_EXCELLENT:389            return "green"390        elif score >= QUALITY_GOOD:391            return "yellow"392        elif score >= QUALITY_CONCERN:393            return "orange1"394        return "red"395396    def _format_dimension_name(self, dim: str) -> str:397        """Format dimension name for display."""398        names = {399            "theological_accuracy": "Theology",400            "source_fidelity": "Fidelity",401            "naturalness": "Naturalness",402            "readability": "Readability",403            "glossary_consistency": "Glossary",404            "literary_quality": "Literary",405        }406        return names.get(dim, dim.replace("_", " ").title())407408409class ChapterQualitySummary(Static):410    """Display quality summary for a chapter."""411412    def __init__(413        self,414        data_provider: TUIDataProvider,415        *,416        id: str | None = None,417    ) -> None:418        """Initialize the chapter quality summary.419420        Args:421            data_provider: Data provider for loading evaluation data.422            id: Widget ID.423        """424        super().__init__(id=id)425        self.data_provider = data_provider426        self.book: str | None = None427        self.chapter: int | None = None428429    def update_chapter(self, book: str, chapter: int) -> None:430        """Update to show a specific chapter.431432        Args:433            book: Book name.434            chapter: Chapter number.435        """436        self.book = book437        self.chapter = chapter438        # Use update() to force content refresh in Static widget439        self.update(self.render())440441    def render(self) -> RenderableType:442        """Render the chapter quality summary."""443        if self.book is None or self.chapter is None:444            return Panel(445                Text("Select a chapter to view quality summary.", style="dim"),446                title="Chapter Quality",447            )448449        # Load chapter evaluation450        chapter_eval = self.data_provider.eval_storage.load_chapter_evaluation(451            self.book, self.chapter452        )453454        content = Text()455        content.append(f"{self.book.upper()} Chapter {self.chapter}\n", style="bold cyan")456        content.append("\n")457458        if not chapter_eval:459            content.append("No evaluation data available.\n", style="dim")460            content.append(461                "Translate with --evaluate to generate quality metrics.",462                style="dim italic",463            )464            return Panel(content, title="Chapter Quality")465466        # Overall score467        overall = chapter_eval.overall_score468        overall_color = self._get_score_color(overall)469        content.append("Overall Quality: ", style="bold")470        content.append(f"{overall:.1f}%\n", style=f"bold {overall_color}")471        content.append("\n")472473        # Verse statistics474        total_verses = len(chapter_eval.verses)475        flagged_count = chapter_eval.flagged_count476        clean_count = total_verses - flagged_count477478        content.append("Verses: ", style="bold")479        content.append(f"{total_verses} total, ", style="white")480        content.append(f"{clean_count} clean", style="green")481        if flagged_count > 0:482            content.append(f", {flagged_count} flagged", style="red")483        content.append("\n\n")484485        # Aggregate scores by dimension486        content.append("Dimension Averages:\n", style="bold")487        for dim, avg_score in chapter_eval.aggregate_scores.items():488            dim_display = self._format_dimension_name(dim)489            color = self._get_score_color(avg_score)490            filled = int(avg_score / 10)491            bar = "█" * filled + "░" * (10 - filled)492            content.append(f"  {dim_display:15} ", style="bold")493            content.append(f"{bar} ", style=color)494            content.append(f"{avg_score:.1f}%\n", style=color)495496        # Flagged verses list497        if chapter_eval.flagged_verses:498            content.append("\n⚠ Flagged Verses: ", style="bold red")499            flagged_str = ", ".join(str(v) for v in sorted(chapter_eval.flagged_verses))500            content.append(f"{flagged_str}\n", style="red")501502        return Panel(content, title="Chapter Quality Summary")503504    def _get_score_color(self, score: float) -> str:505        """Get color for a score value."""506        if score >= QUALITY_EXCELLENT:507            return "green"508        elif score >= QUALITY_GOOD:509            return "yellow"510        elif score >= QUALITY_CONCERN:511            return "orange1"512        return "red"513514    def _format_dimension_name(self, dim: str) -> str:515        """Format dimension name for display."""516        names = {517            "theological_accuracy": "Theology",518            "source_fidelity": "Fidelity",519            "naturalness": "Naturalness",520            "readability": "Readability",521            "glossary_consistency": "Glossary",522            "literary_quality": "Literary",523        }524        return names.get(dim, dim.replace("_", " ").title())525