Introduces a new 'r' key binding to refresh the service state and update the welcome screen. The refresh action asynchronously checks the status of container and native services, updates OAuth config state, and refreshes the UI to ensure consistency between displayed text and button states.
650 lines
28 KiB
Python
650 lines
28 KiB
Python
"""Welcome screen for OpenRAG TUI."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from textual.app import ComposeResult
|
|
from textual.containers import Container, Vertical, Horizontal
|
|
from textual.screen import Screen
|
|
from textual.widgets import Header, Footer, Static, Button
|
|
from rich.text import Text
|
|
from rich.align import Align
|
|
from dotenv import load_dotenv
|
|
|
|
from .. import __version__
|
|
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
|
|
from ..widgets.version_mismatch_warning_modal import VersionMismatchWarningModal
|
|
|
|
|
|
class WelcomeScreen(Screen):
|
|
"""Initial welcome screen with setup options."""
|
|
|
|
BINDINGS = [
|
|
("q", "quit", "Quit"),
|
|
("r", "refresh", "Refresh"),
|
|
]
|
|
|
|
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
|
|
self.has_flow_backups = False
|
|
|
|
# Check if .env file exists
|
|
self.has_env_file = self.env_manager.env_file.exists()
|
|
|
|
# Load .env file if it exists
|
|
# override=True ensures .env file values take precedence over existing environment variables
|
|
load_dotenv(override=True)
|
|
|
|
# Check OAuth config immediately
|
|
self.has_oauth_config = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool(
|
|
os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID")
|
|
)
|
|
|
|
# Check for flow backups
|
|
self.has_flow_backups = self._check_flow_backups()
|
|
|
|
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"),
|
|
self._create_dynamic_buttons(),
|
|
id="welcome-container",
|
|
),
|
|
id="main-container",
|
|
)
|
|
yield Footer()
|
|
|
|
def _check_flow_backups(self) -> bool:
|
|
"""Check if there are any flow backups in flows/backup directory."""
|
|
from ..managers.env_manager import EnvManager
|
|
|
|
# Get flows path from env config
|
|
env_manager = EnvManager()
|
|
env_manager.load_existing_env()
|
|
flows_path = Path(env_manager.config.openrag_flows_path.replace("$HOME", str(Path.home()))).expanduser()
|
|
backup_dir = flows_path / "backup"
|
|
if not backup_dir.exists():
|
|
return False
|
|
|
|
try:
|
|
# Check if there are any .json files in the backup directory
|
|
backup_files = list(backup_dir.glob("*.json"))
|
|
return len(backup_files) > 0
|
|
except Exception:
|
|
return False
|
|
|
|
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 detected runtime command to check services
|
|
import subprocess
|
|
compose_cmd = self.container_manager.runtime_info.compose_command + [
|
|
"-f", str(self.container_manager.compose_file),
|
|
"ps", "--format", "json"
|
|
]
|
|
result = subprocess.run(
|
|
compose_cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=5
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
import json
|
|
services = []
|
|
|
|
# Try parsing as a single JSON array first (podman format)
|
|
try:
|
|
parsed = json.loads(result.stdout.strip())
|
|
if isinstance(parsed, list):
|
|
services = parsed
|
|
else:
|
|
services = [parsed] if isinstance(parsed, dict) else []
|
|
except json.JSONDecodeError:
|
|
# Fallback: try parsing line-by-line (docker format)
|
|
for line in result.stdout.strip().split('\n'):
|
|
if line.strip():
|
|
try:
|
|
service = json.loads(line)
|
|
if isinstance(service, dict):
|
|
services.append(service)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
|
|
# Check if services are running (exclude starting/created states)
|
|
# State can be lowercase or mixed case, so normalize it
|
|
# Only consider expected services (filter out stale/leftover containers)
|
|
expected = set(self.container_manager.expected_services)
|
|
name_map = self.container_manager.container_name_map
|
|
running_services = set()
|
|
starting_services = set()
|
|
for s in services:
|
|
if not isinstance(s, dict):
|
|
continue
|
|
# Get service name - try compose label first (most reliable for Podman)
|
|
labels = s.get('Labels', {}) or {}
|
|
service_name = labels.get('com.docker.compose.service', '')
|
|
if not service_name:
|
|
# Fall back to container name mapping
|
|
container_name = s.get('Name') or s.get('Service', '')
|
|
if not container_name:
|
|
names = s.get('Names', [])
|
|
if names and isinstance(names, list):
|
|
container_name = names[0]
|
|
# Map container name to service name using container_name_map
|
|
service_name = name_map.get(container_name, container_name)
|
|
# Skip if not an expected service
|
|
if service_name not in expected:
|
|
continue
|
|
state = str(s.get('State', '')).lower()
|
|
if state == 'running':
|
|
running_services.add(service_name)
|
|
elif 'starting' in state or 'created' in state:
|
|
starting_services.add(service_name)
|
|
|
|
# Services are running if all expected services are in running state
|
|
# (i.e., we have all expected services running and none are still starting)
|
|
self.services_running = len(running_services) == len(expected) and len(starting_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()
|
|
ascii_art = """
|
|
██████╗ ██████╗ ███████╗███╗ ██╗██████╗ █████╗ ██████╗
|
|
██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔══██╗██╔══██╗██╔════╝
|
|
██║ ██║██████╔╝█████╗ ██╔██╗ ██║██████╔╝███████║██║ ███╗
|
|
██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██╔══██╗██╔══██║██║ ██║
|
|
╚██████╔╝██║ ███████╗██║ ╚████║██║ ██║██║ ██║╚██████╔╝
|
|
╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝
|
|
"""
|
|
welcome_text.append(ascii_art, style="bold white")
|
|
welcome_text.append("Terminal User Interface for OpenRAG\n", style="dim")
|
|
welcome_text.append(f"v{__version__}\n\n", style="white")
|
|
|
|
# Check if all services are running
|
|
all_services_running = self.services_running and self.docling_running
|
|
|
|
if all_services_running:
|
|
welcome_text.append(
|
|
"✓ All services are running\n\n", style="bold green"
|
|
)
|
|
elif self.services_running or self.docling_running:
|
|
welcome_text.append(
|
|
"⚠ Some services are running\n\n", style="bold yellow"
|
|
)
|
|
elif self.has_oauth_config:
|
|
welcome_text.append(
|
|
"OAuth credentials detected — Advanced Setup recommended\n\n",
|
|
style="bold green",
|
|
)
|
|
else:
|
|
welcome_text.append("Select a setup below to continue\n\n", style="white")
|
|
return welcome_text
|
|
|
|
def _create_dynamic_buttons(self) -> Horizontal:
|
|
"""Create buttons based on current state."""
|
|
# Check OAuth config early to determine which buttons to show
|
|
has_oauth = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool(
|
|
os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID")
|
|
)
|
|
|
|
buttons = []
|
|
|
|
# If no .env file exists, only show setup buttons
|
|
if not self.has_env_file:
|
|
if has_oauth:
|
|
# If OAuth is configured, only show advanced setup
|
|
buttons.append(
|
|
Button("Advanced Setup", variant="success", id="advanced-setup-btn")
|
|
)
|
|
else:
|
|
# 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")
|
|
)
|
|
return Horizontal(*buttons, classes="button-row")
|
|
|
|
# Check if all services (native + container) are running
|
|
all_services_running = self.services_running and self.docling_running
|
|
|
|
if all_services_running:
|
|
# All services running - show app link first, then stop all
|
|
buttons.append(
|
|
Button("Launch OpenRAG", variant="success", id="open-app-btn")
|
|
)
|
|
buttons.append(
|
|
Button("Stop All Services", variant="error", id="stop-all-services-btn")
|
|
)
|
|
else:
|
|
# Some or no services running - show setup options and start all
|
|
if has_oauth:
|
|
# If OAuth is configured, only show advanced setup
|
|
buttons.append(
|
|
Button("Advanced Setup", variant="success", id="advanced-setup-btn")
|
|
)
|
|
else:
|
|
# 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")
|
|
)
|
|
|
|
buttons.append(
|
|
Button("Start OpenRAG", variant="primary", id="start-all-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:
|
|
"""Initialize screen state when mounted."""
|
|
# Check if services are running
|
|
if self.container_manager.is_available():
|
|
services = await self.container_manager.get_service_status()
|
|
expected = set(self.container_manager.expected_services)
|
|
running_services = [
|
|
s.name for s in services.values() if s.status == ServiceStatus.RUNNING
|
|
]
|
|
starting_services = [
|
|
s.name for s in services.values() if s.status == ServiceStatus.STARTING
|
|
]
|
|
# Services are running if all expected services are in running state
|
|
self.services_running = len(running_services) == len(expected) and len(starting_services) == 0
|
|
else:
|
|
self.services_running = False
|
|
|
|
# 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")
|
|
)
|
|
|
|
# Set default button focus
|
|
if self.services_running and self.docling_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"
|
|
|
|
# Refresh the welcome text AND buttons based on the updated async state
|
|
# This ensures buttons match the actual service state (fixes issue where
|
|
# text showed "All services running" but buttons weren't updated)
|
|
await self._refresh_welcome_content()
|
|
|
|
def _focus_appropriate_button(self) -> None:
|
|
"""Focus the appropriate button based on current state."""
|
|
try:
|
|
if self.services_running and self.docling_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
|
|
|
|
async def on_screen_resume(self) -> None:
|
|
"""Called when returning from another screen (e.g., config screen)."""
|
|
# Check if .env file exists (may have been created)
|
|
self.has_env_file = self.env_manager.env_file.exists()
|
|
|
|
# Reload environment variables
|
|
load_dotenv(override=True)
|
|
|
|
# Update OAuth config state
|
|
self.has_oauth_config = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool(
|
|
os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID")
|
|
)
|
|
|
|
# Re-detect container services using async method for accuracy
|
|
if self.container_manager.is_available():
|
|
services = await self.container_manager.get_service_status(force_refresh=True)
|
|
expected = set(self.container_manager.expected_services)
|
|
running_services = [
|
|
s.name for s in services.values() if s.status == ServiceStatus.RUNNING
|
|
]
|
|
starting_services = [
|
|
s.name for s in services.values() if s.status == ServiceStatus.STARTING
|
|
]
|
|
self.services_running = len(running_services) == len(expected) and len(starting_services) == 0
|
|
else:
|
|
self.services_running = False
|
|
|
|
# Re-detect native service state
|
|
self.docling_running = self.docling_manager.is_running()
|
|
|
|
# Refresh the welcome content and buttons
|
|
await self._refresh_welcome_content()
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
"""Handle button presses."""
|
|
if event.button.id == "basic-setup-btn":
|
|
self.action_no_auth_setup()
|
|
elif event.button.id == "advanced-setup-btn":
|
|
self.action_full_setup()
|
|
elif event.button.id == "status-btn":
|
|
self.action_monitor()
|
|
elif event.button.id == "diagnostics-btn":
|
|
self.action_diagnostics()
|
|
elif event.button.id == "start-all-services-btn":
|
|
self.action_start_all_services()
|
|
elif event.button.id == "stop-all-services-btn":
|
|
self.action_stop_all_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 and self.docling_running:
|
|
self.action_open_app()
|
|
elif self.has_oauth_config:
|
|
self.action_full_setup()
|
|
else:
|
|
self.action_no_auth_setup()
|
|
|
|
def action_no_auth_setup(self) -> None:
|
|
"""Switch to basic configuration screen."""
|
|
from .config import ConfigScreen
|
|
|
|
self.app.push_screen(ConfigScreen(mode="no_auth"))
|
|
|
|
def action_full_setup(self) -> None:
|
|
"""Switch to advanced configuration screen."""
|
|
from .config import ConfigScreen
|
|
|
|
self.app.push_screen(ConfigScreen(mode="full"))
|
|
|
|
def action_monitor(self) -> None:
|
|
"""Switch to monitoring screen."""
|
|
from .monitor import MonitorScreen
|
|
|
|
self.app.push_screen(MonitorScreen())
|
|
|
|
def action_diagnostics(self) -> None:
|
|
"""Switch to diagnostics screen."""
|
|
from .diagnostics import DiagnosticsScreen
|
|
|
|
self.app.push_screen(DiagnosticsScreen())
|
|
|
|
def action_refresh(self) -> None:
|
|
"""Refresh service state and update welcome screen."""
|
|
self.run_worker(self._refresh_state())
|
|
|
|
async def _refresh_state(self) -> None:
|
|
"""Async refresh of service state."""
|
|
# Re-detect container services using async method for accuracy
|
|
if self.container_manager.is_available():
|
|
services = await self.container_manager.get_service_status(force_refresh=True)
|
|
expected = set(self.container_manager.expected_services)
|
|
running_services = [
|
|
s.name for s in services.values() if s.status == ServiceStatus.RUNNING
|
|
]
|
|
starting_services = [
|
|
s.name for s in services.values() if s.status == ServiceStatus.STARTING
|
|
]
|
|
self.services_running = len(running_services) == len(expected) and len(starting_services) == 0
|
|
else:
|
|
self.services_running = False
|
|
|
|
# Re-detect native service state
|
|
self.docling_running = self.docling_manager.is_running()
|
|
|
|
# Update OAuth config state
|
|
self.has_oauth_config = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool(
|
|
os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID")
|
|
)
|
|
|
|
# Refresh the welcome content and buttons
|
|
await self._refresh_welcome_content()
|
|
self.notify("Refreshed", severity="information", timeout=2)
|
|
|
|
def action_start_all_services(self) -> None:
|
|
"""Start all services (native first, then containers)."""
|
|
self.run_worker(self._start_all_services())
|
|
|
|
def action_stop_all_services(self) -> None:
|
|
"""Stop all services (containers first, then native)."""
|
|
self.run_worker(self._stop_all_services())
|
|
|
|
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 and self.docling_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)
|
|
|
|
async def _start_all_services(self) -> None:
|
|
"""Start all services: containers first, then native services."""
|
|
# Check for port conflicts before attempting to start anything
|
|
conflicts = []
|
|
|
|
# Check container ports only if services are not already running
|
|
if self.container_manager.is_available() and not self.services_running:
|
|
ports_available, port_conflicts = await self.container_manager.check_ports_available()
|
|
if not ports_available:
|
|
for service_name, port, error_msg in port_conflicts[:3]: # Show first 3
|
|
conflicts.append(f"{service_name} (port {port})")
|
|
if len(port_conflicts) > 3:
|
|
conflicts.append(f"and {len(port_conflicts) - 3} more")
|
|
|
|
# Check native service port only if it's not already running
|
|
if not self.docling_manager.is_running():
|
|
port_available, error_msg = self.docling_manager.check_port_available()
|
|
if not port_available:
|
|
conflicts.append(f"docling (port {self.docling_manager._port})")
|
|
|
|
# If there are any conflicts, show error and return
|
|
if conflicts:
|
|
conflict_str = ", ".join(conflicts)
|
|
self.notify(
|
|
f"Cannot start services: Port conflicts detected for {conflict_str}. "
|
|
f"Please stop the conflicting services first.",
|
|
severity="error",
|
|
timeout=10
|
|
)
|
|
return
|
|
|
|
# Step 1: Start container services first (to create the network)
|
|
if self.container_manager.is_available() and not self.services_running:
|
|
# Check for version mismatch before starting
|
|
has_mismatch, container_version, tui_version = await self.container_manager.check_version_mismatch()
|
|
if has_mismatch and container_version:
|
|
# Show warning modal and wait for user decision
|
|
should_continue = await self.app.push_screen_wait(
|
|
VersionMismatchWarningModal(container_version, tui_version)
|
|
)
|
|
if not should_continue:
|
|
self.notify("Start cancelled", severity="information")
|
|
return
|
|
# Ensure OPENRAG_VERSION is set in .env BEFORE starting services
|
|
# This ensures docker compose reads the correct version
|
|
try:
|
|
from ..managers.env_manager import EnvManager
|
|
env_manager = EnvManager()
|
|
env_manager.ensure_openrag_version()
|
|
# Small delay to ensure .env file is written and flushed
|
|
import asyncio
|
|
await asyncio.sleep(0.5)
|
|
except Exception:
|
|
pass # Continue even if version setting fails
|
|
|
|
command_generator = self.container_manager.start_services()
|
|
modal = CommandOutputModal(
|
|
"Starting Container Services",
|
|
command_generator,
|
|
on_complete=self._on_containers_started_start_native,
|
|
)
|
|
self.app.push_screen(modal)
|
|
elif self.services_running:
|
|
# Containers already running, just start native services
|
|
self.notify("Container services already running", severity="information")
|
|
await self._start_native_services_after_containers()
|
|
else:
|
|
self.notify("No container runtime available", severity="warning")
|
|
# Still try to start native services
|
|
await self._start_native_services_after_containers()
|
|
|
|
async def _on_containers_started_start_native(self) -> None:
|
|
"""Called after containers start successfully, now start native services."""
|
|
# Update container state
|
|
self._detect_services_sync()
|
|
|
|
# Now start native services (docling-serve can now detect the compose network)
|
|
await self._start_native_services_after_containers()
|
|
|
|
async def _start_native_services_after_containers(self) -> None:
|
|
"""Start native services after containers have been started."""
|
|
if not self.docling_manager.is_running():
|
|
# Check for port conflicts before attempting to start
|
|
port_available, error_msg = self.docling_manager.check_port_available()
|
|
if not port_available:
|
|
self.notify(
|
|
f"Cannot start native services: {error_msg}. "
|
|
f"Please stop the conflicting service first.",
|
|
severity="error",
|
|
timeout=10
|
|
)
|
|
# Update state and return
|
|
self.docling_running = False
|
|
await self._refresh_welcome_content()
|
|
return
|
|
|
|
self.notify("Starting native services...", severity="information")
|
|
success, message = await self.docling_manager.start()
|
|
if success:
|
|
self.notify(message, severity="information")
|
|
else:
|
|
self.notify(f"Failed to start native services: {message}", severity="error")
|
|
else:
|
|
self.notify("Native services already running", severity="information")
|
|
|
|
# Update state
|
|
self.docling_running = self.docling_manager.is_running()
|
|
await self._refresh_welcome_content()
|
|
|
|
async def _stop_all_services(self) -> None:
|
|
"""Stop all services: containers first, then native."""
|
|
# Step 1: Stop container services
|
|
if self.container_manager.is_available() and self.services_running:
|
|
command_generator = self.container_manager.stop_services()
|
|
modal = CommandOutputModal(
|
|
"Stopping Container Services",
|
|
command_generator,
|
|
on_complete=self._on_stop_containers_complete,
|
|
)
|
|
self.app.push_screen(modal)
|
|
else:
|
|
# No containers to stop, go directly to stopping native services
|
|
await self._stop_native_services_after_containers()
|
|
|
|
async def _on_stop_containers_complete(self) -> None:
|
|
"""Called after containers are stopped, now stop native services."""
|
|
# Update container state
|
|
self._detect_services_sync()
|
|
|
|
# Now stop native services
|
|
await self._stop_native_services_after_containers()
|
|
|
|
async def _stop_native_services_after_containers(self) -> None:
|
|
"""Stop native services after containers have been stopped."""
|
|
if self.docling_manager.is_running():
|
|
self.notify("Stopping native services...", severity="information")
|
|
success, message = await self.docling_manager.stop()
|
|
if success:
|
|
self.notify(message, severity="information")
|
|
else:
|
|
self.notify(f"Failed to stop native services: {message}", severity="error")
|
|
else:
|
|
self.notify("Native services already stopped", severity="information")
|
|
|
|
# Update state
|
|
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()
|