OPEN SOURCE
software/src/tui/widgets.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
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:16from 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."""2627def __init__(self, book: str, chapter: int, verse: int) -> None:28"""Initialize the message.2930Args:31book: Book name.32chapter: Chapter number.33verse: Verse number.34"""35self.book = book36self.chapter = chapter37self.verse = verse38super().__init__()394041class VerseTree(Tree[dict]):42"""Hierarchical tree for Book > Chapter > Verse navigation."""4344def __init__(45self,46data_provider: TUIDataProvider,47*,48id: str | None = None,49) -> None:50"""Initialize the verse tree.5152Args:53data_provider: Data provider for loading verse information.54id: Widget ID.55"""56super().__init__("Books", id=id)57self.data_provider = data_provider5859def on_mount(self) -> None:60"""Load available books when mounted."""61self._populate_tree()6263def _populate_tree(self) -> None:64"""Populate the tree with books, chapters, and verses."""65books = self.data_provider.get_available_books()6667for book in books:68book_node = self.root.add(book.upper(), data={"type": "book", "book": book})69chapters = self.data_provider.get_book_chapters(book)7071for chapter in chapters:72chapter_label = f"Chapter {chapter}"73if self.data_provider.is_chapter_evaluated(book, chapter):74chapter_label += " *"75chapter_node = book_node.add(76chapter_label,77data={"type": "chapter", "book": book, "chapter": chapter},78)7980verses = self.data_provider.get_chapter_verses(book, chapter)81flagged = self.data_provider.get_flagged_verses(book, chapter)8283for verse in verses:84verse_label = f"Verse {verse}"85if verse in flagged:86verse_label += " !"87chapter_node.add(88verse_label,89data={90"type": "verse",91"book": book,92"chapter": chapter,93"verse": verse,94},95)9697self.root.expand()9899def _handle_node_activation(self, node_data: dict | None) -> None: # pragma: no cover100"""Handle node activation (selection or highlight).101102Args:103node_data: The data attached to the node.104"""105if node_data and node_data.get("type") == "verse":106self.post_message(107VerseSelected(108book=node_data["book"],109chapter=node_data["chapter"],110verse=node_data["verse"],111)112)113114def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: # pragma: no cover115"""Handle node selection (Enter/double-click)."""116self._handle_node_activation(event.node.data)117118def on_tree_node_highlighted(self, event: Tree.NodeHighlighted) -> None: # pragma: no cover119"""Handle node highlight (single-click/arrow keys)."""120self._handle_node_activation(event.node.data)121122123class DetailPanel(Static):124"""Panel showing verse details: sources, translation, evaluation."""125126def __init__(127self,128data_provider: TUIDataProvider,129*,130id: str | None = None,131) -> None:132"""Initialize the detail panel.133134Args:135data_provider: Data provider for loading verse data.136id: Widget ID.137"""138super().__init__(id=id)139self.data_provider = data_provider140self.current_verse: VerseData | None = None141142def update_verse(self, book: str, chapter: int, verse: int) -> None:143"""Update the panel to show a specific verse.144145Args:146book: Book name.147chapter: Chapter number.148verse: Verse number.149"""150self.current_verse = self.data_provider.get_verse_data(book, chapter, verse)151# Use update() to force content refresh in Static widget152self.update(self.render())153154def render(self) -> RenderableType:155"""Render the panel content."""156if self.current_verse is None:157return Panel(158"Select a verse from the tree to view details.",159title="Details",160)161162verse = self.current_verse163content = Text()164165# Header166content.append(167f"{verse.book.upper()} {verse.chapter}:{verse.verse}\n",168style="bold cyan",169)170content.append("\n")171172# Source texts section173content.append("SOURCE TEXTS\n", style="bold yellow")174content.append("-" * 50 + "\n")175176if verse.greek_text:177content.append("Greek: ", style="bold")178content.append(f"{verse.greek_text}\n")179else:180content.append("Greek: ", style="bold")181content.append("(not available)\n", style="dim")182183if verse.kjv_text:184content.append("KJV: ", style="bold")185content.append(f"{verse.kjv_text}\n")186else:187content.append("KJV: ", style="bold")188content.append("(not available)\n", style="dim")189190if verse.japanese_reference:191content.append("Ref (JP): ", style="bold")192content.append(f"{verse.japanese_reference}\n")193else:194content.append("Ref (JP): ", style="bold")195content.append("(not available)\n", style="dim")196197if verse.glossary_terms:198content.append("Glossary: ", style="bold")199content.append(", ".join(verse.glossary_terms) + "\n")200201content.append("\n")202203# Translation section204content.append("TRANSLATION\n", style="bold yellow")205content.append("-" * 50 + "\n")206207if verse.translation_text:208content.append(f"{verse.translation_text}\n", style="green")209if verse.translation_model:210content.append(f"Model: {verse.translation_model}", style="dim")211if verse.translation_timestamp:212content.append(f" | {verse.translation_timestamp}", style="dim")213content.append("\n")214else:215content.append("(not translated)\n", style="dim")216217content.append("\n")218219# Evaluation section220content.append("EVALUATION\n", style="bold yellow")221content.append("-" * 50 + "\n")222223if verse.evaluation:224eval_data = verse.evaluation225226# Overall score with color-coded indicator227overall = eval_data.overall_score228overall_color = self._get_score_color(overall)229overall_indicator = self._get_quality_indicator(overall)230231content.append("Overall: ", style="bold")232content.append(f"{overall_indicator} ", style=overall_color)233content.append(f"{overall:.1f}%\n", style=f"bold {overall_color}")234content.append("\n")235236# Score bars for each dimension with improved display237for dim, score in eval_data.scores.items():238dim_display = self._format_dimension_name(dim)239color = self._get_score_color(score.score)240241# Create visual progress bar242filled = int(score.score / 10)243bar = "█" * filled + "░" * (10 - filled)244245content.append(f"{dim_display:22} ", style="bold")246content.append(f"{bar} ", style=color)247content.append(f"{score.score:5.1f}%", style=color)248249# Confidence indicator250conf_indicator = "●" if score.confidence >= 80 else "○"251conf_color = "green" if score.confidence >= 80 else "yellow"252content.append(f" {conf_indicator}", style=conf_color)253content.append(f" [{score.confidence:.0f}%]\n", style="dim")254255content.append("\n")256257# Flags section with icons258if eval_data.flags:259content.append("⚠ Flags: ", style="bold red")260flag_text = ", ".join(self._format_flag(f) for f in eval_data.flags)261content.append(f"{flag_text}\n", style="red")262else:263content.append("✓ No flags\n", style="green")264265# Reasoning summary (if available)266if eval_data.scores:267content.append("\nDetails:\n", style="bold")268for dim, score in list(eval_data.scores.items())[:3]:269if score.reasoning:270dim_display = self._format_dimension_name(dim)271content.append(f" {dim_display}: ", style="cyan")272# Truncate long reasoning273reasoning = score.reasoning[:80]274if len(score.reasoning) > 80:275reasoning += "..."276content.append(f"{reasoning}\n", style="dim")277278# Notes279if eval_data.notes:280content.append("\nNotes: ", style="bold")281content.append(f"{eval_data.notes}\n")282else:283content.append("(not evaluated)\n", style="dim")284content.append(285"Run translation with --evaluate flag to get quality metrics.\n",286style="dim italic",287)288289return Panel(content, title="Verse Details")290291def _get_score_color(self, score: float) -> str:292"""Get color for a score value."""293if score >= QUALITY_EXCELLENT:294return "green"295elif score >= QUALITY_GOOD:296return "yellow"297elif score >= QUALITY_CONCERN:298return "orange1"299return "red"300301def _get_quality_indicator(self, score: float) -> str:302"""Get quality indicator emoji for a score."""303if score >= QUALITY_EXCELLENT:304return "●"305elif score >= QUALITY_GOOD:306return "◐"307elif score >= QUALITY_CONCERN:308return "○"309return "✗"310311def _format_dimension_name(self, dim: str) -> str:312"""Format dimension name for display."""313names = {314"theological_accuracy": "Theology",315"source_fidelity": "Fidelity",316"naturalness": "Naturalness",317"readability": "Readability",318"glossary_consistency": "Glossary",319"literary_quality": "Literary",320}321return names.get(dim, dim.replace("_", " ").title())322323def _format_flag(self, flag: str) -> str:324"""Format flag name for display."""325return flag.replace("_", " ").replace("low ", "↓")326327328class EvaluationSummaryTable(Static):329"""Display evaluation scores in a table format."""330331def __init__(self, id: str | None = None) -> None:332"""Initialize the evaluation summary table."""333super().__init__(id=id)334self.scores: dict[str, tuple[float, float]] = {}335336def set_scores(self, scores: dict[str, tuple[float, float]]) -> None:337"""Set the scores to display.338339Args:340scores: Dictionary mapping dimension to (score, confidence) tuple.341"""342self.scores = scores343# Use update() to force content refresh in Static widget344self.update(self.render())345346def render(self) -> RenderableType:347"""Render the evaluation table."""348if not self.scores:349return Text("No evaluation data", style="dim")350351table = Table(title="Evaluation Scores", box=None)352table.add_column("Dimension", style="cyan", width=20)353table.add_column("Score", justify="right", width=12)354table.add_column("Bar", width=12)355table.add_column("Confidence", justify="right", width=10)356table.add_column("Status", justify="center", width=8)357358for dim, (score, confidence) in self.scores.items():359dim_display = self._format_dimension_name(dim)360361# Score with color362score_color = self._get_score_color(score)363score_text = Text(f"{score:.1f}%", style=score_color)364365# Visual bar366filled = int(score / 10)367bar = Text("█" * filled + "░" * (10 - filled), style=score_color)368369# Confidence370conf_text = Text(f"{confidence:.1f}%", style="dim")371372# Status indicator373if score >= QUALITY_EXCELLENT:374status = Text("●", style="green")375elif score >= QUALITY_GOOD:376status = Text("◐", style="yellow")377elif score >= QUALITY_CONCERN:378status = Text("○", style="orange1")379else:380status = Text("✗", style="red")381382table.add_row(dim_display, score_text, bar, conf_text, status)383384return table385386def _get_score_color(self, score: float) -> str:387"""Get color for a score value."""388if score >= QUALITY_EXCELLENT:389return "green"390elif score >= QUALITY_GOOD:391return "yellow"392elif score >= QUALITY_CONCERN:393return "orange1"394return "red"395396def _format_dimension_name(self, dim: str) -> str:397"""Format dimension name for display."""398names = {399"theological_accuracy": "Theology",400"source_fidelity": "Fidelity",401"naturalness": "Naturalness",402"readability": "Readability",403"glossary_consistency": "Glossary",404"literary_quality": "Literary",405}406return names.get(dim, dim.replace("_", " ").title())407408409class ChapterQualitySummary(Static):410"""Display quality summary for a chapter."""411412def __init__(413self,414data_provider: TUIDataProvider,415*,416id: str | None = None,417) -> None:418"""Initialize the chapter quality summary.419420Args:421data_provider: Data provider for loading evaluation data.422id: Widget ID.423"""424super().__init__(id=id)425self.data_provider = data_provider426self.book: str | None = None427self.chapter: int | None = None428429def update_chapter(self, book: str, chapter: int) -> None:430"""Update to show a specific chapter.431432Args:433book: Book name.434chapter: Chapter number.435"""436self.book = book437self.chapter = chapter438# Use update() to force content refresh in Static widget439self.update(self.render())440441def render(self) -> RenderableType:442"""Render the chapter quality summary."""443if self.book is None or self.chapter is None:444return Panel(445Text("Select a chapter to view quality summary.", style="dim"),446title="Chapter Quality",447)448449# Load chapter evaluation450chapter_eval = self.data_provider.eval_storage.load_chapter_evaluation(451self.book, self.chapter452)453454content = Text()455content.append(f"{self.book.upper()} Chapter {self.chapter}\n", style="bold cyan")456content.append("\n")457458if not chapter_eval:459content.append("No evaluation data available.\n", style="dim")460content.append(461"Translate with --evaluate to generate quality metrics.",462style="dim italic",463)464return Panel(content, title="Chapter Quality")465466# Overall score467overall = chapter_eval.overall_score468overall_color = self._get_score_color(overall)469content.append("Overall Quality: ", style="bold")470content.append(f"{overall:.1f}%\n", style=f"bold {overall_color}")471content.append("\n")472473# Verse statistics474total_verses = len(chapter_eval.verses)475flagged_count = chapter_eval.flagged_count476clean_count = total_verses - flagged_count477478content.append("Verses: ", style="bold")479content.append(f"{total_verses} total, ", style="white")480content.append(f"{clean_count} clean", style="green")481if flagged_count > 0:482content.append(f", {flagged_count} flagged", style="red")483content.append("\n\n")484485# Aggregate scores by dimension486content.append("Dimension Averages:\n", style="bold")487for dim, avg_score in chapter_eval.aggregate_scores.items():488dim_display = self._format_dimension_name(dim)489color = self._get_score_color(avg_score)490filled = int(avg_score / 10)491bar = "█" * filled + "░" * (10 - filled)492content.append(f" {dim_display:15} ", style="bold")493content.append(f"{bar} ", style=color)494content.append(f"{avg_score:.1f}%\n", style=color)495496# Flagged verses list497if chapter_eval.flagged_verses:498content.append("\n⚠ Flagged Verses: ", style="bold red")499flagged_str = ", ".join(str(v) for v in sorted(chapter_eval.flagged_verses))500content.append(f"{flagged_str}\n", style="red")501502return Panel(content, title="Chapter Quality Summary")503504def _get_score_color(self, score: float) -> str:505"""Get color for a score value."""506if score >= QUALITY_EXCELLENT:507return "green"508elif score >= QUALITY_GOOD:509return "yellow"510elif score >= QUALITY_CONCERN:511return "orange1"512return "red"513514def _format_dimension_name(self, dim: str) -> str:515"""Format dimension name for display."""516names = {517"theological_accuracy": "Theology",518"source_fidelity": "Fidelity",519"naturalness": "Naturalness",520"readability": "Readability",521"glossary_consistency": "Glossary",522"literary_quality": "Literary",523}524return names.get(dim, dim.replace("_", " ").title())525