openrag/src/tui/main.py
2025-09-18 12:02:08 -04:00

358 lines
8.1 KiB
Python

"""Main TUI application for OpenRAG."""
import sys
from pathlib import Path
from textual.app import App, ComposeResult
from utils.logging_config import get_logger
try:
from importlib.resources import files
except ImportError:
from importlib_resources import files
logger = get_logger(__name__)
from .screens.welcome import WelcomeScreen
from .screens.config import ConfigScreen
from .screens.monitor import MonitorScreen
from .screens.logs import LogsScreen
from .screens.diagnostics import DiagnosticsScreen
from .managers.env_manager import EnvManager
from .managers.container_manager import ContainerManager
from .managers.docling_manager import DoclingManager
from .utils.platform import PlatformDetector
from .widgets.diagnostics_notification import notify_with_diagnostics
class OpenRAGTUI(App):
"""OpenRAG Terminal User Interface application."""
TITLE = "OpenRAG TUI"
SUB_TITLE = "Container Management & Configuration"
CSS = """
Screen {
background: #0f172a;
}
#main-container {
height: 100%;
padding: 1;
}
#welcome-container {
align: center middle;
width: 100%;
height: 100%;
}
#welcome-text {
text-align: center;
margin-bottom: 2;
}
.button-row {
align: center middle;
height: auto;
margin: 1 0;
}
.button-row Button {
margin: 0 1;
min-width: 20;
}
#config-header {
text-align: center;
margin-bottom: 2;
}
#config-scroll {
height: 1fr;
overflow-y: auto;
}
#config-form {
width: 80%;
max-width: 100;
margin: 0;
padding: 1;
height: auto;
}
#config-form Input {
margin-bottom: 1;
width: 100%;
}
/* Actions under Documents Paths input */
#docs-path-actions {
width: 100%;
padding-left: 0;
margin-top: -1;
height: auto;
}
#docs-path-actions Button {
width: auto;
min-width: 12;
}
#config-form Label {
margin-bottom: 0;
padding-left: 1;
}
.helper-text {
margin: 0 0 1 1;
}
/* Docs path actions row */
#services-content {
height: 100%;
}
#runtime-status {
background: $panel;
border: solid $primary;
padding: 1;
margin-bottom: 1;
}
#services-table {
height: auto;
max-height: 12;
margin-bottom: 1;
}
#images-table {
height: auto;
max-height: 8;
margin-bottom: 1;
}
#logs-scroll {
height: 1fr;
border: solid $primary;
background: $surface;
}
.controls-row {
align: left middle;
height: auto;
margin: 1 0;
}
.controls-row > * {
margin-right: 1;
}
.label {
width: auto;
margin-right: 1;
text-style: bold;
}
#system-info {
background: $panel;
border: solid $primary;
padding: 2;
height: 1fr;
}
TabbedContent {
height: 1fr;
}
TabPane {
padding: 1;
height: 1fr;
}
.tab-header {
text-style: bold;
color: $accent;
margin-bottom: 1;
}
TabPane ScrollableContainer {
height: 100%;
padding: 1;
}
/* Frontend-inspired color scheme */
Static {
color: #f1f5f9;
}
Button.success {
background: #4ade80;
color: #000;
}
Button.error {
background: #ef4444;
color: #fff;
}
Button.warning {
background: #eab308;
color: #000;
}
Button.primary {
background: #2563eb;
color: #fff;
}
Button.default {
background: #475569;
color: #f1f5f9;
border: solid #64748b;
}
DataTable {
background: #1e293b;
color: #f1f5f9;
}
DataTable > .datatable--header {
background: #334155;
color: #f1f5f9;
}
DataTable > .datatable--cursor {
background: #475569;
}
Input {
background: #334155;
color: #f1f5f9;
border: solid #64748b;
}
Label {
color: #f1f5f9;
}
Footer {
background: #334155;
color: #f1f5f9;
}
#runtime-status {
background: #1e293b;
border: solid #64748b;
color: #f1f5f9;
}
#system-info {
background: #1e293b;
border: solid #64748b;
color: #f1f5f9;
}
#services-table, #images-table {
background: #1e293b;
}
"""
def __init__(self):
super().__init__()
self.platform_detector = PlatformDetector()
self.container_manager = ContainerManager()
self.env_manager = EnvManager()
self.docling_manager = DoclingManager() # Initialize singleton instance
def on_mount(self) -> None:
"""Initialize the application."""
# Check for runtime availability and show appropriate screen
if not self.container_manager.is_available():
notify_with_diagnostics(
self,
"No container runtime found. Please install Docker or Podman.",
severity="warning",
timeout=10,
)
# Load existing config if available
config_exists = self.env_manager.load_existing_env()
# Start with welcome screen
self.push_screen(WelcomeScreen())
async def action_quit(self) -> None:
"""Quit the application."""
# Cleanup docling manager before exiting
self.docling_manager.cleanup()
self.exit()
def check_runtime_requirements(self) -> tuple[bool, str]:
"""Check if runtime requirements are met."""
if not self.container_manager.is_available():
return False, self.platform_detector.get_installation_instructions()
# Check Podman macOS memory if applicable
runtime_info = self.container_manager.get_runtime_info()
if runtime_info.runtime_type.value == "podman":
is_sufficient, _, message = (
self.platform_detector.check_podman_macos_memory()
)
if not is_sufficient:
return False, f"Podman VM memory insufficient:\n{message}"
return True, "Runtime requirements satisfied"
def copy_sample_documents():
"""Copy sample documents from package to current directory if they don't exist."""
documents_dir = Path("documents")
# Check if documents directory already exists and has files
if documents_dir.exists() and any(documents_dir.glob("*.pdf")):
return # Documents already exist, don't overwrite
try:
# Get sample documents from package assets
assets_files = files("tui._assets.documents")
# Create documents directory if it doesn't exist
documents_dir.mkdir(exist_ok=True)
# Copy each sample document
for resource in assets_files.iterdir():
if resource.is_file() and resource.name.endswith('.pdf'):
dest_path = documents_dir / resource.name
if not dest_path.exists():
content = resource.read_bytes()
dest_path.write_bytes(content)
logger.info(f"Copied sample document: {resource.name}")
except Exception as e:
logger.debug(f"Could not copy sample documents: {e}")
# This is not a critical error - the app can work without sample documents
def run_tui():
"""Run the OpenRAG TUI application."""
app = None
try:
# Copy sample documents on first run
copy_sample_documents()
app = OpenRAGTUI()
app.run()
except KeyboardInterrupt:
logger.info("OpenRAG TUI interrupted by user")
except Exception as e:
logger.error("Error running OpenRAG TUI", error=str(e))
finally:
# Ensure cleanup happens even on exceptions
if app and hasattr(app, 'docling_manager'):
app.docling_manager.cleanup()
sys.exit(0)
if __name__ == "__main__":
run_tui()