misc improvements

This commit is contained in:
phact 2025-09-16 11:43:13 -04:00
parent 2c8cbc95a5
commit 9ae40dda50
4 changed files with 334 additions and 43 deletions

View file

@ -27,7 +27,7 @@ class OpenRAGTUI(App):
CSS = """ CSS = """
Screen { Screen {
background: $background; background: #0f172a;
} }
#main-container { #main-container {
@ -115,7 +115,8 @@ class OpenRAGTUI(App):
} }
#services-table { #services-table {
height: 1fr; height: auto;
max-height: 12;
margin-bottom: 1; margin-bottom: 1;
} }
@ -175,6 +176,82 @@ class OpenRAGTUI(App):
height: 100%; height: 100%;
padding: 1; 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): def __init__(self):

View file

@ -273,8 +273,6 @@ class MonitorScreen(Screen):
self.run_worker(self._stop_docling_serve()) self.run_worker(self._stop_docling_serve())
elif button_id.startswith("docling-restart-btn"): elif button_id.startswith("docling-restart-btn"):
self.run_worker(self._restart_docling_serve()) self.run_worker(self._restart_docling_serve())
elif button_id.startswith("docling-logs-btn"):
self._view_docling_logs()
elif button_id == "toggle-mode-btn": elif button_id == "toggle-mode-btn":
self.action_toggle_mode() self.action_toggle_mode()
elif button_id.startswith("refresh-btn"): elif button_id.startswith("refresh-btn"):
@ -621,9 +619,6 @@ class MonitorScreen(Screen):
docling_controls.mount( docling_controls.mount(
Button("Restart", variant="primary", id=f"docling-restart-btn{suffix}") Button("Restart", variant="primary", id=f"docling-restart-btn{suffix}")
) )
docling_controls.mount(
Button("View Logs", variant="default", id=f"docling-logs-btn{suffix}")
)
else: else:
docling_controls.mount( docling_controls.mount(
Button("Start", variant="success", id=f"docling-start-btn{suffix}") Button("Start", variant="success", id=f"docling-start-btn{suffix}")

View file

@ -12,6 +12,8 @@ from dotenv import load_dotenv
from ..managers.container_manager import ContainerManager, ServiceStatus from ..managers.container_manager import ContainerManager, ServiceStatus
from ..managers.env_manager import EnvManager from ..managers.env_manager import EnvManager
from ..managers.docling_manager import DoclingManager
from ..widgets.command_modal import CommandOutputModal
class WelcomeScreen(Screen): class WelcomeScreen(Screen):
@ -22,15 +24,19 @@ class WelcomeScreen(Screen):
("enter", "default_action", "Continue"), ("enter", "default_action", "Continue"),
("1", "no_auth_setup", "Basic Setup"), ("1", "no_auth_setup", "Basic Setup"),
("2", "full_setup", "Advanced Setup"), ("2", "full_setup", "Advanced Setup"),
("3", "monitor", "Monitor Services"), ("3", "monitor", "Status"),
("4", "diagnostics", "Diagnostics"), ("4", "diagnostics", "Diagnostics"),
("5", "start_stop_services", "Start/Stop Services"),
("6", "open_app", "Open App"),
] ]
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.container_manager = ContainerManager() self.container_manager = ContainerManager()
self.env_manager = EnvManager() self.env_manager = EnvManager()
self.docling_manager = DoclingManager()
self.services_running = False self.services_running = False
self.docling_running = False
self.has_oauth_config = False self.has_oauth_config = False
self.default_button_id = "basic-setup-btn" self.default_button_id = "basic-setup-btn"
self._state_checked = False self._state_checked = False
@ -38,8 +44,16 @@ class WelcomeScreen(Screen):
# Load .env file if it exists # Load .env file if it exists
load_dotenv() load_dotenv()
# Check OAuth config immediately
self.has_oauth_config = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool(
os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID")
)
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""Create the welcome screen layout.""" """Create the welcome screen layout."""
# Try to detect services synchronously before creating buttons
self._detect_services_sync()
yield Container( yield Container(
Vertical( Vertical(
Static(self._create_welcome_text(), id="welcome-text"), Static(self._create_welcome_text(), id="welcome-text"),
@ -50,6 +64,46 @@ class WelcomeScreen(Screen):
) )
yield Footer() yield Footer()
def _detect_services_sync(self) -> None:
"""Synchronously detect if services are running."""
if not self.container_manager.is_available():
self.services_running = False
self.docling_running = self.docling_manager.is_running()
return
try:
# Use synchronous docker command to check services
import subprocess
result = subprocess.run(
["docker", "compose", "ps", "--format", "json"],
capture_output=True,
text=True,
timeout=5
)
if result.returncode == 0:
import json
services = []
for line in result.stdout.strip().split('\n'):
if line.strip():
try:
service = json.loads(line)
services.append(service)
except json.JSONDecodeError:
continue
# Check if any services are running
running_services = [s for s in services if s.get('State') == 'running']
self.services_running = len(running_services) > 0
else:
self.services_running = False
except Exception:
# Fallback to False if detection fails
self.services_running = False
# Update native service state as part of detection
self.docling_running = self.docling_manager.is_running()
def _create_welcome_text(self) -> Text: def _create_welcome_text(self) -> Text:
"""Create a minimal welcome message.""" """Create a minimal welcome message."""
welcome_text = Text() welcome_text = Text()
@ -61,7 +115,7 @@ class WelcomeScreen(Screen):
""" """
welcome_text.append(ascii_art, style="bold blue") welcome_text.append(ascii_art, style="bold white")
welcome_text.append("Terminal User Interface for OpenRAG\n\n", style="dim") welcome_text.append("Terminal User Interface for OpenRAG\n\n", style="dim")
if self.services_running: if self.services_running:
@ -87,28 +141,56 @@ class WelcomeScreen(Screen):
buttons = [] buttons = []
if self.services_running: if self.services_running:
# Services running - only show monitor # Services running - show app link first, then stop services
buttons.append( buttons.append(
Button("Monitor Services", variant="success", id="monitor-btn") Button("Launch OpenRAG", variant="success", id="open-app-btn")
)
buttons.append(
Button("Stop Container Services", variant="error", id="stop-services-btn")
) )
else: else:
# Services not running - show setup options # Services not running - show setup options and start services
if has_oauth: if has_oauth:
# Only show advanced setup if OAuth is configured # If OAuth is configured, only show advanced setup
buttons.append( buttons.append(
Button("Advanced Setup", variant="success", id="advanced-setup-btn") Button("Advanced Setup", variant="success", id="advanced-setup-btn")
) )
else: else:
# Only show basic setup if no OAuth # If no OAuth, show both options with basic as primary
buttons.append( buttons.append(
Button("Basic Setup", variant="success", id="basic-setup-btn") Button("Basic Setup", variant="success", id="basic-setup-btn")
) )
buttons.append(
Button("Advanced Setup", variant="default", id="advanced-setup-btn")
)
# Always show monitor option
buttons.append( buttons.append(
Button("Monitor Services", variant="default", id="monitor-btn") Button("Start Container Services", variant="primary", id="start-services-btn")
) )
# Native services controls
if self.docling_running:
buttons.append(
Button(
"Stop Native Services",
variant="warning",
id="stop-native-services-btn",
)
)
else:
buttons.append(
Button(
"Start Native Services",
variant="primary",
id="start-native-services-btn",
)
)
# Always show status option
buttons.append(
Button("Status", variant="default", id="status-btn")
)
return Horizontal(*buttons, classes="button-row") return Horizontal(*buttons, classes="button-row")
async def on_mount(self) -> None: async def on_mount(self) -> None:
@ -121,6 +203,10 @@ class WelcomeScreen(Screen):
] ]
self.services_running = len(running_services) > 0 self.services_running = len(running_services) > 0
# Check native service state
self.docling_running = self.docling_manager.is_running()
# Check for OAuth configuration # Check for OAuth configuration
self.has_oauth_config = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool( self.has_oauth_config = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool(
os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID") os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID")
@ -128,38 +214,34 @@ class WelcomeScreen(Screen):
# Set default button focus # Set default button focus
if self.services_running: if self.services_running:
self.default_button_id = "monitor-btn" self.default_button_id = "open-app-btn"
elif self.has_oauth_config: elif self.has_oauth_config:
self.default_button_id = "advanced-setup-btn" self.default_button_id = "advanced-setup-btn"
else: else:
self.default_button_id = "basic-setup-btn" self.default_button_id = "basic-setup-btn"
# Update the welcome text and recompose with new state # Update the welcome text
try: try:
welcome_widget = self.query_one("#welcome-text") welcome_widget = self.query_one("#welcome-text")
welcome_widget.update( welcome_widget.update(self._create_welcome_text())
self._create_welcome_text()
) # This is fine for Static widgets
# Focus the appropriate button
if self.services_running:
try:
self.query_one("#monitor-btn").focus()
except:
pass
elif self.has_oauth_config:
try:
self.query_one("#advanced-setup-btn").focus()
except:
pass
else:
try:
self.query_one("#basic-setup-btn").focus()
except:
pass
except: except:
pass # Widgets might not be mounted yet pass # Widget might not be mounted yet
# Focus the appropriate button (the buttons are created correctly in compose,
# the issue was they weren't being updated after service operations)
self.call_after_refresh(self._focus_appropriate_button)
def _focus_appropriate_button(self) -> None:
"""Focus the appropriate button based on current state."""
try:
if self.services_running:
self.query_one("#open-app-btn").focus()
elif self.has_oauth_config:
self.query_one("#advanced-setup-btn").focus()
else:
self.query_one("#basic-setup-btn").focus()
except:
pass # Button might not exist
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button presses.""" """Handle button presses."""
@ -167,15 +249,25 @@ class WelcomeScreen(Screen):
self.action_no_auth_setup() self.action_no_auth_setup()
elif event.button.id == "advanced-setup-btn": elif event.button.id == "advanced-setup-btn":
self.action_full_setup() self.action_full_setup()
elif event.button.id == "monitor-btn": elif event.button.id == "status-btn":
self.action_monitor() self.action_monitor()
elif event.button.id == "diagnostics-btn": elif event.button.id == "diagnostics-btn":
self.action_diagnostics() self.action_diagnostics()
elif event.button.id == "start-services-btn":
self.action_start_stop_services()
elif event.button.id == "stop-services-btn":
self.action_start_stop_services()
elif event.button.id == "start-native-services-btn":
self.action_start_native_services()
elif event.button.id == "stop-native-services-btn":
self.action_stop_native_services()
elif event.button.id == "open-app-btn":
self.action_open_app()
def action_default_action(self) -> None: def action_default_action(self) -> None:
"""Handle Enter key - go to default action based on state.""" """Handle Enter key - go to default action based on state."""
if self.services_running: if self.services_running:
self.action_monitor() self.action_open_app()
elif self.has_oauth_config: elif self.has_oauth_config:
self.action_full_setup() self.action_full_setup()
else: else:
@ -205,6 +297,126 @@ class WelcomeScreen(Screen):
self.app.push_screen(DiagnosticsScreen()) self.app.push_screen(DiagnosticsScreen())
def action_start_stop_services(self) -> None:
"""Start or stop all services (containers + docling)."""
if self.services_running:
# Stop services - show modal with progress
if self.container_manager.is_available():
command_generator = self.container_manager.stop_services()
modal = CommandOutputModal(
"Stopping Services",
command_generator,
on_complete=self._on_services_operation_complete,
)
self.app.push_screen(modal)
else:
# Start services - show modal with progress
if self.container_manager.is_available():
command_generator = self.container_manager.start_services()
modal = CommandOutputModal(
"Starting Services",
command_generator,
on_complete=self._on_services_operation_complete,
)
self.app.push_screen(modal)
async def _on_services_operation_complete(self) -> None:
"""Handle completion of services start/stop operation."""
# Use the same sync detection method that worked on startup
self._detect_services_sync()
# Update OAuth config state
self.has_oauth_config = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool(
os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID")
)
await self._refresh_welcome_content()
def _update_default_button(self) -> None:
"""Update the default button target based on state."""
if self.services_running:
self.default_button_id = "open-app-btn"
elif self.has_oauth_config:
self.default_button_id = "advanced-setup-btn"
else:
self.default_button_id = "basic-setup-btn"
async def _refresh_welcome_content(self) -> None:
"""Refresh welcome text and buttons based on current state."""
self._update_default_button()
try:
welcome_widget = self.query_one("#welcome-text", Static)
welcome_widget.update(self._create_welcome_text())
welcome_container = self.query_one("#welcome-container")
# Remove existing button rows before mounting updated row
for button_row in list(welcome_container.query(".button-row")):
await button_row.remove()
await welcome_container.mount(self._create_dynamic_buttons())
except Exception:
# Fallback - just refresh the whole screen
self.refresh(layout=True)
self.call_after_refresh(self._focus_appropriate_button)
def action_start_native_services(self) -> None:
"""Start native services (docling)."""
if self.docling_running:
self.notify("Native services are already running.", severity="warning")
return
self.run_worker(self._start_native_services())
async def _start_native_services(self) -> None:
"""Worker task to start native services."""
try:
success, message = await self.docling_manager.start()
if success:
self.docling_running = True
self.notify(message, severity="information")
else:
self.notify(f"Failed to start native services: {message}", severity="error")
except Exception as exc:
self.notify(f"Error starting native services: {exc}", severity="error")
finally:
self.docling_running = self.docling_manager.is_running()
await self._refresh_welcome_content()
def action_stop_native_services(self) -> None:
"""Stop native services (docling)."""
if not self.docling_running and not self.docling_manager.is_running():
self.notify("Native services are not running.", severity="warning")
return
self.run_worker(self._stop_native_services())
async def _stop_native_services(self) -> None:
"""Worker task to stop native services."""
try:
success, message = await self.docling_manager.stop()
if success:
self.docling_running = False
self.notify(message, severity="information")
else:
self.notify(f"Failed to stop native services: {message}", severity="error")
except Exception as exc:
self.notify(f"Error stopping native services: {exc}", severity="error")
finally:
self.docling_running = self.docling_manager.is_running()
await self._refresh_welcome_content()
def action_open_app(self) -> None:
"""Open the OpenRAG app in the default browser."""
import webbrowser
try:
webbrowser.open("http://localhost:3000")
self.notify("Opening OpenRAG app in browser...", severity="information")
except Exception as e:
self.notify(f"Error opening app: {e}", severity="error")
def action_quit(self) -> None: def action_quit(self) -> None:
"""Quit the application.""" """Quit the application."""
self.app.exit() self.app.exit()

View file

@ -1,6 +1,7 @@
"""Command output modal dialog for OpenRAG TUI.""" """Command output modal dialog for OpenRAG TUI."""
import asyncio import asyncio
import inspect
from typing import Callable, List, Optional, AsyncIterator, Any from typing import Callable, List, Optional, AsyncIterator, Any
from textual.app import ComposeResult from textual.app import ComposeResult
@ -122,7 +123,13 @@ class CommandOutputModal(ModalScreen):
# Call the completion callback if provided # Call the completion callback if provided
if self.on_complete: if self.on_complete:
await asyncio.sleep(0.5) # Small delay for better UX await asyncio.sleep(0.5) # Small delay for better UX
self.on_complete()
def _invoke_callback() -> None:
callback_result = self.on_complete()
if inspect.isawaitable(callback_result):
asyncio.create_task(callback_result)
self.call_after_refresh(_invoke_callback)
except Exception as e: except Exception as e:
output.write(f"[bold red]Error: {e}[/bold red]\n") output.write(f"[bold red]Error: {e}[/bold red]\n")