|
5 | 5 | from collections import deque |
6 | 6 | from dataclasses import dataclass |
7 | 7 | from time import monotonic |
8 | | -from typing import Any |
| 8 | +from typing import TYPE_CHECKING, Any |
9 | 9 |
|
10 | 10 | from rich.console import Console, RenderableType |
11 | 11 | from rich.layout import Layout |
|
24 | 24 |
|
25 | 25 | from .progress import StreamObserver |
26 | 26 |
|
| 27 | +if TYPE_CHECKING: |
| 28 | + from .error_codes import ClassifiedError |
| 29 | + |
| 30 | + |
| 31 | +class TopicBuildingMixin: |
| 32 | + """Mixin providing shared functionality for Tree and Graph building TUIs. |
| 33 | +
|
| 34 | + Provides common implementations for: |
| 35 | + - _refresh_left(): Update events panel in left column |
| 36 | + - on_error(): Handle error events from progress reporter |
| 37 | + - on_step_start()/on_step_complete(): No-op handlers for step events |
| 38 | + - update_status_panel(): Update status panel (requires _status_panel() in subclass) |
| 39 | +
|
| 40 | + Subclasses must have these attributes: |
| 41 | + - tui: DeepFabricTUI instance |
| 42 | + - live_layout: Layout | None |
| 43 | + - events_log: deque |
| 44 | + """ |
| 45 | + |
| 46 | + tui: "DeepFabricTUI" |
| 47 | + live_layout: "Layout | None" |
| 48 | + events_log: "deque" |
| 49 | + |
| 50 | + def _refresh_left(self) -> None: |
| 51 | + """Update events panel in left column.""" |
| 52 | + if self.live_layout is not None: |
| 53 | + try: |
| 54 | + self.live_layout["main"]["left"]["events"].update( |
| 55 | + self.tui.build_events_panel(list(self.events_log)) |
| 56 | + ) |
| 57 | + except Exception: |
| 58 | + return |
| 59 | + |
| 60 | + def on_error(self, error: "ClassifiedError", metadata: dict[str, Any]) -> None: # noqa: ARG002 |
| 61 | + """Handle error events - log to events panel.""" |
| 62 | + error_event = error.to_event() |
| 63 | + self.events_log.append(f"X {error_event}") |
| 64 | + self._refresh_left() |
| 65 | + |
| 66 | + def on_step_start(self, step_name: str, metadata: dict[str, Any]) -> None: # noqa: ARG002 |
| 67 | + """Handle step start - topic building doesn't need specific handling.""" |
| 68 | + pass |
| 69 | + |
| 70 | + def on_step_complete(self, step_name: str, metadata: dict[str, Any]) -> None: # noqa: ARG002 |
| 71 | + """Handle step complete - topic building doesn't need specific handling.""" |
| 72 | + pass |
| 73 | + |
| 74 | + def update_status_panel(self) -> None: |
| 75 | + """Update the status panel in the right column.""" |
| 76 | + if self.live_layout is None: |
| 77 | + return |
| 78 | + try: |
| 79 | + self.live_layout["main"]["right"]["status"].update(self._status_panel()) |
| 80 | + except Exception: |
| 81 | + return |
| 82 | + |
| 83 | + def _status_panel(self) -> Panel: |
| 84 | + """Create status panel - must be implemented by subclass.""" |
| 85 | + raise NotImplementedError |
| 86 | + |
| 87 | + |
27 | 88 | # Constants |
28 | 89 | STREAM_BUFFER_DISPLAY_THRESHOLD = 1000 # Show ellipsis if accumulated text exceeds this |
29 | 90 | STREAM_TEXT_MAX_LENGTH = 8000 # Max characters to display in streaming text |
@@ -104,7 +165,19 @@ def build_events_panel(self, events: list[str], title: str = "Events") -> Panel: |
104 | 165 | text = Text("Waiting...", style="dim") |
105 | 166 | else: |
106 | 167 | # Keep events short; show newest at bottom |
107 | | - text = Text("\n".join(events[-EVENT_LOG_MAX_LINES:])) |
| 168 | + # Colorize based on prefix: X = red (error), checkmark = green (success) |
| 169 | + text = Text() |
| 170 | + for i, event in enumerate(events[-EVENT_LOG_MAX_LINES:]): |
| 171 | + if i > 0: |
| 172 | + text.append("\n") |
| 173 | + if event.startswith("X "): |
| 174 | + text.append("X ", style="bold red") |
| 175 | + text.append(event[2:]) |
| 176 | + elif event.startswith("✓ ") or event.startswith("✔ "): |
| 177 | + text.append(event[0] + " ", style="bold green") |
| 178 | + text.append(event[2:]) |
| 179 | + else: |
| 180 | + text.append(event) |
108 | 181 | return Panel(text, title=title, border_style="dim", padding=(0, 1)) |
109 | 182 |
|
110 | 183 | def create_footer(self, layout: Layout, title: str = "Run Status") -> Progress: |
@@ -183,7 +256,7 @@ def info(self, message: str) -> None: |
183 | 256 | self.console.print(f" {message}", style="blue") |
184 | 257 |
|
185 | 258 |
|
186 | | -class TreeBuildingTUI(StreamObserver): |
| 259 | +class TreeBuildingTUI(TopicBuildingMixin, StreamObserver): |
187 | 260 | """TUI for tree building operations with simplified progress and streaming.""" |
188 | 261 |
|
189 | 262 | def __init__(self, tui: DeepFabricTUI): |
@@ -355,24 +428,6 @@ def _refresh_context(self) -> None: |
355 | 428 | except Exception: |
356 | 429 | return |
357 | 430 |
|
358 | | - def _refresh_left(self) -> None: |
359 | | - if self.live_layout is not None: |
360 | | - try: |
361 | | - # Update events panel in left column |
362 | | - self.live_layout["main"]["left"]["events"].update( |
363 | | - self.tui.build_events_panel(list(self.events_log)) |
364 | | - ) |
365 | | - except Exception: |
366 | | - return |
367 | | - |
368 | | - def on_step_start(self, step_name: str, metadata: dict[str, Any]) -> None: |
369 | | - """Handle step start - tree building doesn't need specific handling.""" |
370 | | - pass |
371 | | - |
372 | | - def on_step_complete(self, step_name: str, metadata: dict[str, Any]) -> None: |
373 | | - """Handle step complete - tree building doesn't need specific handling.""" |
374 | | - pass |
375 | | - |
376 | 431 | def finish_building(self, total_paths: int, failed_generations: int) -> None: |
377 | 432 | """Finish the tree building process.""" |
378 | 433 | if self.live_display: |
@@ -400,16 +455,8 @@ def _status_panel(self) -> Panel: |
400 | 455 | table.add_row("Failed:", str(self.failed_attempts)) |
401 | 456 | return Panel(table, title="Status", border_style="dim", padding=(0, 1)) |
402 | 457 |
|
403 | | - def update_status_panel(self) -> None: |
404 | | - if self.live_layout is None: |
405 | | - return |
406 | | - try: |
407 | | - self.live_layout["main"]["right"]["status"].update(self._status_panel()) |
408 | | - except Exception: |
409 | | - return |
410 | 458 |
|
411 | | - |
412 | | -class GraphBuildingTUI(StreamObserver): |
| 459 | +class GraphBuildingTUI(TopicBuildingMixin, StreamObserver): |
413 | 460 | """TUI for graph building operations with simplified progress and streaming.""" |
414 | 461 |
|
415 | 462 | def __init__(self, tui: DeepFabricTUI): |
@@ -576,23 +623,6 @@ def _refresh_context(self) -> None: |
576 | 623 | except Exception: |
577 | 624 | return |
578 | 625 |
|
579 | | - def _refresh_left(self) -> None: |
580 | | - if self.live_layout is not None: |
581 | | - try: |
582 | | - self.live_layout["main"]["left"]["events"].update( |
583 | | - self.tui.build_events_panel(list(self.events_log)) |
584 | | - ) |
585 | | - except Exception: |
586 | | - return |
587 | | - |
588 | | - def on_step_start(self, step_name: str, metadata: dict[str, Any]) -> None: |
589 | | - """Handle step start - graph building doesn't need specific handling.""" |
590 | | - pass |
591 | | - |
592 | | - def on_step_complete(self, step_name: str, metadata: dict[str, Any]) -> None: |
593 | | - """Handle step complete - graph building doesn't need specific handling.""" |
594 | | - pass |
595 | | - |
596 | 626 | def finish_building(self, failed_generations: int) -> None: |
597 | 627 | """Finish the graph building process.""" |
598 | 628 | if self.live_display: |
@@ -629,14 +659,6 @@ def _status_panel(self) -> Panel: |
629 | 659 | table.add_row("Failed:", str(self.failed_attempts)) |
630 | 660 | return Panel(table, title="Status", border_style="dim", padding=(0, 1)) |
631 | 661 |
|
632 | | - def update_status_panel(self) -> None: |
633 | | - if self.live_layout is None: |
634 | | - return |
635 | | - try: |
636 | | - self.live_layout["main"]["right"]["status"].update(self._status_panel()) |
637 | | - except Exception: |
638 | | - return |
639 | | - |
640 | 662 |
|
641 | 663 | class DatasetGenerationTUI(StreamObserver): |
642 | 664 | """Enhanced TUI for dataset generation with rich integration and streaming display.""" |
@@ -945,6 +967,27 @@ def log_event(self, message: str) -> None: |
945 | 967 | self.tui.build_events_panel(list(self.events_log)) |
946 | 968 | ) |
947 | 969 |
|
| 970 | + def on_error(self, error: "ClassifiedError", metadata: dict[str, Any]) -> None: |
| 971 | + """Handle error events from the progress reporter. |
| 972 | +
|
| 973 | + Displays concise error information in the Events panel using |
| 974 | + standardized DeepFabric error codes. |
| 975 | +
|
| 976 | + Args: |
| 977 | + error: ClassifiedError with error code and details |
| 978 | + metadata: Additional context (sample_idx, etc.) |
| 979 | + """ |
| 980 | + # Format concise error message for Events panel |
| 981 | + error_event = error.to_event() |
| 982 | + |
| 983 | + # Add sample context if available |
| 984 | + sample_idx = metadata.get("sample_idx") |
| 985 | + if sample_idx is not None: |
| 986 | + error_event = f"[{sample_idx}] {error_event}" |
| 987 | + |
| 988 | + # Log to events panel with error indicator |
| 989 | + self.log_event(f"X {error_event}") |
| 990 | + |
948 | 991 |
|
949 | 992 | # Global TUI instances |
950 | 993 | _tui_instance = None |
|
0 commit comments