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

View file

@ -273,8 +273,6 @@ class MonitorScreen(Screen):
self.run_worker(self._stop_docling_serve())
elif button_id.startswith("docling-restart-btn"):
self.run_worker(self._restart_docling_serve())
elif button_id.startswith("docling-logs-btn"):
self._view_docling_logs()
elif button_id == "toggle-mode-btn":
self.action_toggle_mode()
elif button_id.startswith("refresh-btn"):
@ -621,9 +619,6 @@ class MonitorScreen(Screen):
docling_controls.mount(
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:
docling_controls.mount(
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.env_manager import EnvManager
from ..managers.docling_manager import DoclingManager
from ..widgets.command_modal import CommandOutputModal
class WelcomeScreen(Screen):
@ -22,15 +24,19 @@ class WelcomeScreen(Screen):
("enter", "default_action", "Continue"),
("1", "no_auth_setup", "Basic Setup"),
("2", "full_setup", "Advanced Setup"),
("3", "monitor", "Monitor Services"),
("3", "monitor", "Status"),
("4", "diagnostics", "Diagnostics"),
("5", "start_stop_services", "Start/Stop Services"),
("6", "open_app", "Open App"),
]
def __init__(self):
super().__init__()
self.container_manager = ContainerManager()
self.env_manager = EnvManager()
self.docling_manager = DoclingManager()
self.services_running = False
self.docling_running = False
self.has_oauth_config = False
self.default_button_id = "basic-setup-btn"
self._state_checked = False
@ -38,8 +44,16 @@ class WelcomeScreen(Screen):
# Load .env file if it exists
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:
"""Create the welcome screen layout."""
# Try to detect services synchronously before creating buttons
self._detect_services_sync()
yield Container(
Vertical(
Static(self._create_welcome_text(), id="welcome-text"),
@ -50,6 +64,46 @@ class WelcomeScreen(Screen):
)
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:
"""Create a minimal welcome message."""
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")
if self.services_running:
@ -87,28 +141,56 @@ class WelcomeScreen(Screen):
buttons = []
if self.services_running:
# Services running - only show monitor
# Services running - show app link first, then stop services
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:
# Services not running - show setup options
# Services not running - show setup options and start services
if has_oauth:
# Only show advanced setup if OAuth is configured
# If OAuth is configured, only show advanced setup
buttons.append(
Button("Advanced Setup", variant="success", id="advanced-setup-btn")
)
else:
# Only show basic setup if no OAuth
# If no OAuth, show both options with basic as primary
buttons.append(
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(
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")
async def on_mount(self) -> None:
@ -121,6 +203,10 @@ class WelcomeScreen(Screen):
]
self.services_running = len(running_services) > 0
# Check native service state
self.docling_running = self.docling_manager.is_running()
# Check for OAuth configuration
self.has_oauth_config = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool(
os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID")
@ -128,38 +214,34 @@ class WelcomeScreen(Screen):
# Set default button focus
if self.services_running:
self.default_button_id = "monitor-btn"
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"
# Update the welcome text and recompose with new state
# Update the welcome text
try:
welcome_widget = self.query_one("#welcome-text")
welcome_widget.update(
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
welcome_widget.update(self._create_welcome_text())
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:
"""Handle button presses."""
@ -167,15 +249,25 @@ class WelcomeScreen(Screen):
self.action_no_auth_setup()
elif event.button.id == "advanced-setup-btn":
self.action_full_setup()
elif event.button.id == "monitor-btn":
elif event.button.id == "status-btn":
self.action_monitor()
elif event.button.id == "diagnostics-btn":
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:
"""Handle Enter key - go to default action based on state."""
if self.services_running:
self.action_monitor()
self.action_open_app()
elif self.has_oauth_config:
self.action_full_setup()
else:
@ -205,6 +297,126 @@ class WelcomeScreen(Screen):
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:
"""Quit the application."""
self.app.exit()

View file

@ -1,6 +1,7 @@
"""Command output modal dialog for OpenRAG TUI."""
import asyncio
import inspect
from typing import Callable, List, Optional, AsyncIterator, Any
from textual.app import ComposeResult
@ -122,7 +123,13 @@ class CommandOutputModal(ModalScreen):
# Call the completion callback if provided
if self.on_complete:
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:
output.write(f"[bold red]Error: {e}[/bold red]\n")