"""Configuration screen for OpenRAG TUI.""" import re from textual.app import ComposeResult from textual.containers import Container, Vertical, Horizontal, ScrollableContainer from textual.screen import Screen from textual.widgets import ( Header, Footer, Static, Button, Input, Label, TabbedContent, TabPane, Checkbox, ) from textual.validation import ValidationResult, Validator from rich.text import Text from pathlib import Path from ..managers.env_manager import EnvManager from ..utils.validation import ( validate_openai_api_key, validate_anthropic_api_key, validate_ollama_endpoint, validate_watsonx_endpoint, validate_documents_paths, ) from pathlib import Path class OpenAIKeyValidator(Validator): """Validator for OpenAI API keys.""" def validate(self, value: str) -> ValidationResult: if not value: return self.success() if validate_openai_api_key(value): return self.success() else: return self.failure("Invalid OpenAI API key format (should start with sk-)") class AnthropicKeyValidator(Validator): """Validator for Anthropic API keys.""" def validate(self, value: str) -> ValidationResult: if not value: return self.success() if validate_anthropic_api_key(value): return self.success() else: return self.failure("Invalid Anthropic API key format (should start with sk-ant-)") class OllamaEndpointValidator(Validator): """Validator for Ollama endpoint URLs.""" def validate(self, value: str) -> ValidationResult: if not value: return self.success() if validate_ollama_endpoint(value): return self.success() else: return self.failure("Invalid Ollama endpoint URL format") class WatsonxEndpointValidator(Validator): """Validator for IBM watsonx.ai endpoint URLs.""" def validate(self, value: str) -> ValidationResult: if not value: return self.success() if validate_watsonx_endpoint(value): return self.success() else: return self.failure("Invalid watsonx.ai endpoint URL format") class DocumentsPathValidator(Validator): """Validator for documents paths.""" def validate(self, value: str) -> ValidationResult: # Optional: allow empty value if not value: return self.success() is_valid, error_msg, _ = validate_documents_paths(value) if is_valid: return self.success() else: return self.failure(error_msg) class PasswordValidator(Validator): """Validator for OpenSearch admin password.""" def validate(self, value: str) -> ValidationResult: # Allow empty value (will be auto-generated) if not value: return self.success() # Minimum length: 8 characters if len(value) < 8: return self.failure("Password must be at least 8 characters long") # Check for required character types has_uppercase = bool(re.search(r"[A-Z]", value)) has_lowercase = bool(re.search(r"[a-z]", value)) has_digit = bool(re.search(r"[0-9]", value)) has_special = bool(re.search(r"[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>/?]", value)) missing = [] if not has_uppercase: missing.append("uppercase letter") if not has_lowercase: missing.append("lowercase letter") if not has_digit: missing.append("digit") if not has_special: missing.append("special character") if missing: return self.failure(f"Password must contain: {', '.join(missing)}") return self.success() class ConfigScreen(Screen): """Configuration screen for environment setup.""" BINDINGS = [ ("escape", "back", "Back"), ("ctrl+s", "save", "Save"), ("ctrl+g", "generate", "Generate Passwords"), ] def __init__(self, mode: str = "full"): super().__init__() self.mode = mode # "no_auth" or "full" self.env_manager = EnvManager() self.inputs = {} # Check if .env file exists self.has_env_file = self.env_manager.env_file.exists() # Load existing config if available self.env_manager.load_existing_env() def compose(self) -> ComposeResult: """Create the configuration screen layout.""" # Removed top header bar and header text with Container(id="main-container"): with ScrollableContainer(id="config-scroll"): with Vertical(id="config-form"): yield from self._create_all_fields() # Create button row - conditionally include Back button buttons = [ Button("Generate Passwords", variant="default", id="generate-btn"), Button("Save Configuration", variant="success", id="save-btn"), ] # Only show Back button if .env file exists if self.has_env_file: buttons.append(Button("Back", variant="default", id="back-btn")) yield Horizontal(*buttons, classes="button-row") yield Footer() def _create_header_text(self) -> Text: """Create the configuration header text.""" header_text = Text() if self.mode == "no_auth": header_text.append("Quick Setup - No Authentication\n", style="bold green") header_text.append( "Configure OpenRAG for local document processing only.\n\n", style="dim" ) else: header_text.append("Full Setup - OAuth Integration\n", style="bold cyan") header_text.append( "Configure OpenRAG with cloud service integrations.\n\n", style="dim" ) header_text.append("Required fields are marked with *\n", style="yellow") header_text.append("Use Ctrl+G to generate admin passwords\n", style="dim") return header_text def _create_all_fields(self) -> ComposeResult: """Create all configuration fields in a single scrollable layout.""" # Admin Credentials Section yield Static("Admin Credentials", classes="tab-header") yield Static(" ") # OpenSearch Admin Password yield Label("OpenSearch Admin Password *") yield Static( "Min 8 chars with uppercase, lowercase, digit, and special character", classes="helper-text", ) current_value = getattr(self.env_manager.config, "opensearch_password", "") with Horizontal(id="opensearch-password-row"): input_widget = Input( placeholder="Auto-generated secure password", value=current_value, password=True, id="input-opensearch_password", validators=[PasswordValidator()], ) yield input_widget self.inputs["opensearch_password"] = input_widget yield Button("πŸ‘", id="toggle-opensearch-password", variant="default") yield Static(" ") # Langflow Admin Password with Horizontal(): yield Label("Langflow Admin Password (optional)") yield Checkbox("Generate password", id="generate-langflow-password") current_value = getattr( self.env_manager.config, "langflow_superuser_password", "" ) with Horizontal(id="langflow-password-row"): input_widget = Input( placeholder="Langflow password", value=current_value, password=True, id="input-langflow_superuser_password", ) yield input_widget self.inputs["langflow_superuser_password"] = input_widget yield Button("πŸ‘", id="toggle-langflow-password", variant="default") yield Static(" ") # Langflow Admin Username - conditionally displayed based on password current_password = getattr(self.env_manager.config, "langflow_superuser_password", "") yield Label("Langflow Admin Username *", id="langflow-username-label") current_value = getattr(self.env_manager.config, "langflow_superuser", "") input_widget = Input( placeholder="admin", value=current_value, id="input-langflow_superuser" ) yield input_widget self.inputs["langflow_superuser"] = input_widget yield Static(" ", id="langflow-username-spacer") yield Static(" ") # API Keys Section yield Static("API Keys", classes="tab-header") yield Static(" ") # OpenAI API Key yield Label("OpenAI API Key (optional)") yield Static( Text("Get a key: https://platform.openai.com/api-keys", style="dim"), classes="helper-text", ) yield Static( Text("Can be configured later in the UI", style="dim italic"), classes="helper-text", ) current_value = getattr(self.env_manager.config, "openai_api_key", "") with Horizontal(id="openai-key-row"): input_widget = Input( placeholder="sk-...", value=current_value, password=True, validators=[OpenAIKeyValidator()], id="input-openai_api_key", ) yield input_widget self.inputs["openai_api_key"] = input_widget yield Button("Show", id="toggle-openai-key", variant="default") yield Static(" ") # Anthropic API Key yield Label("Anthropic API Key (optional)") yield Static( Text("Get a key: https://console.anthropic.com/settings/keys", style="dim"), classes="helper-text", ) yield Static( Text("Can be configured later in the UI", style="dim italic"), classes="helper-text", ) current_value = getattr(self.env_manager.config, "anthropic_api_key", "") with Horizontal(id="anthropic-key-row"): input_widget = Input( placeholder="sk-ant-...", value=current_value, password=True, validators=[AnthropicKeyValidator()], id="input-anthropic_api_key", ) yield input_widget self.inputs["anthropic_api_key"] = input_widget yield Button("Show", id="toggle-anthropic-key", variant="default") yield Static(" ") # Ollama Endpoint yield Label("Ollama Base URL (optional)") yield Static( Text("Endpoint of your Ollama server", style="dim"), classes="helper-text", ) yield Static( Text("Can be configured later in the UI", style="dim italic"), classes="helper-text", ) current_value = getattr(self.env_manager.config, "ollama_endpoint", "") input_widget = Input( placeholder="http://localhost:11434", value=current_value, validators=[OllamaEndpointValidator()], id="input-ollama_endpoint", ) yield input_widget self.inputs["ollama_endpoint"] = input_widget yield Static(" ") # IBM watsonx.ai API Key yield Label("IBM watsonx.ai API Key (optional)") yield Static( Text("Get a key: https://cloud.ibm.com/iam/apikeys", style="dim"), classes="helper-text", ) yield Static( Text("Can be configured later in the UI", style="dim italic"), classes="helper-text", ) current_value = getattr(self.env_manager.config, "watsonx_api_key", "") with Horizontal(id="watsonx-key-row"): input_widget = Input( placeholder="", value=current_value, password=True, id="input-watsonx_api_key", ) yield input_widget self.inputs["watsonx_api_key"] = input_widget yield Button("Show", id="toggle-watsonx-key", variant="default") yield Static(" ") # IBM watsonx.ai Endpoint yield Label("IBM watsonx.ai Endpoint") yield Static( Text("Example: https://us-south.ml.cloud.ibm.com", style="dim"), classes="helper-text", ) current_value = getattr(self.env_manager.config, "watsonx_endpoint", "") input_widget = Input( placeholder="https://us-south.ml.cloud.ibm.com", value=current_value, validators=[WatsonxEndpointValidator()], id="input-watsonx_endpoint", ) yield input_widget self.inputs["watsonx_endpoint"] = input_widget yield Static(" ") # IBM watsonx.ai Project ID yield Label("IBM watsonx.ai Project ID") yield Static( Text("Find in your IBM Cloud project settings", style="dim"), classes="helper-text", ) current_value = getattr(self.env_manager.config, "watsonx_project_id", "") input_widget = Input( placeholder="", value=current_value, id="input-watsonx_project_id", ) yield input_widget self.inputs["watsonx_project_id"] = input_widget yield Static(" ") # Add OAuth fields only in full mode if self.mode == "full": # Google OAuth Client ID yield Label("Google OAuth Client ID") # Where to create Google OAuth credentials (helper above the box) yield Static( Text( "Create credentials: https://console.cloud.google.com/apis/credentials", style="dim", ), classes="helper-text", ) # Callback URL guidance for Google OAuth yield Static( Text( "Important: add an Authorized redirect URI to your Google OAuth app(s):\n" " - Local: http://localhost:3000/auth/callback\n" " - Prod: https://your-domain.com/auth/callback\n" "If you use separate apps for login and connectors, add this URL to BOTH.", style="dim", ), classes="helper-text", ) current_value = getattr( self.env_manager.config, "google_oauth_client_id", "" ) input_widget = Input( placeholder="xxx.apps.googleusercontent.com", value=current_value, id="input-google_oauth_client_id", ) yield input_widget self.inputs["google_oauth_client_id"] = input_widget yield Static(" ") # Google OAuth Client Secret yield Label("Google OAuth Client Secret") current_value = getattr( self.env_manager.config, "google_oauth_client_secret", "" ) with Horizontal(id="google-secret-row"): input_widget = Input( placeholder="", value=current_value, password=True, id="input-google_oauth_client_secret", ) yield input_widget self.inputs["google_oauth_client_secret"] = input_widget yield Button("Show", id="toggle-google-secret", variant="default") yield Static(" ") # Microsoft Graph Client ID yield Label("Microsoft Graph Client ID") # Where to create Microsoft app registrations (helper above the box) yield Static( Text( "Create app: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade", style="dim", ), classes="helper-text", ) # Callback URL guidance for Microsoft OAuth yield Static( Text( "Important: configure a Web redirect URI for your Microsoft app(s):\n" " - Local: http://localhost:3000/auth/callback\n" " - Prod: https://your-domain.com/auth/callback\n" "If you use separate apps for login and connectors, add this URI to BOTH.", style="dim", ), classes="helper-text", ) current_value = getattr( self.env_manager.config, "microsoft_graph_oauth_client_id", "" ) input_widget = Input( placeholder="", value=current_value, id="input-microsoft_graph_oauth_client_id", ) yield input_widget self.inputs["microsoft_graph_oauth_client_id"] = input_widget yield Static(" ") # Microsoft Graph Client Secret yield Label("Microsoft Graph Client Secret") current_value = getattr( self.env_manager.config, "microsoft_graph_oauth_client_secret", "" ) with Horizontal(id="microsoft-secret-row"): input_widget = Input( placeholder="", value=current_value, password=True, id="input-microsoft_graph_oauth_client_secret", ) yield input_widget self.inputs["microsoft_graph_oauth_client_secret"] = input_widget yield Button("Show", id="toggle-microsoft-secret", variant="default") yield Static(" ") # AWS Access Key ID yield Label("AWS Access Key ID") # Where to create AWS keys (helper above the box) yield Static( Text( "Create keys: https://console.aws.amazon.com/iam/home#/security_credentials", style="dim", ), classes="helper-text", ) current_value = getattr(self.env_manager.config, "aws_access_key_id", "") input_widget = Input( placeholder="", value=current_value, id="input-aws_access_key_id" ) yield input_widget self.inputs["aws_access_key_id"] = input_widget yield Static(" ") # AWS Secret Access Key yield Label("AWS Secret Access Key") current_value = getattr( self.env_manager.config, "aws_secret_access_key", "" ) input_widget = Input( placeholder="", value=current_value, password=True, id="input-aws_secret_access_key", ) yield input_widget self.inputs["aws_secret_access_key"] = input_widget yield Static(" ") yield Static(" ") # Other Settings Section yield Static("Others", classes="tab-header") yield Static(" ") # Documents Paths (optional) + picker action button on next line yield Label("Documents Paths") current_value = getattr(self.env_manager.config, "openrag_documents_paths", "") input_widget = Input( placeholder="~/.openrag/documents", value=current_value, validators=[DocumentsPathValidator()], id="input-openrag_documents_paths", ) yield input_widget # Actions row with pick button yield Horizontal( Button("Pick…", id="pick-docs-btn"), id="docs-path-actions", classes="controls-row", ) self.inputs["openrag_documents_paths"] = input_widget yield Static(" ") # OpenSearch Data Path yield Label("OpenSearch Data Path") yield Static( "Directory to persist OpenSearch indices across upgrades", classes="helper-text", ) current_value = getattr(self.env_manager.config, "opensearch_data_path", "$HOME/.openrag/data/opensearch-data") input_widget = Input( placeholder="~/.openrag/data/opensearch-data", value=current_value, id="input-opensearch_data_path", ) yield input_widget # Actions row with pick button yield Horizontal( Button("Pick…", id="pick-opensearch-data-btn"), id="opensearch-data-path-actions", classes="controls-row", ) self.inputs["opensearch_data_path"] = input_widget yield Static(" ") # Langflow Auth Settings - These are automatically configured based on password presence # Not shown in UI; set in env_manager.setup_secure_defaults() # Add optional fields only in full mode if self.mode == "full": # Webhook Base URL yield Label("Webhook Base URL") current_value = getattr(self.env_manager.config, "webhook_base_url", "") input_widget = Input( placeholder="https://your-domain.com", value=current_value, id="input-webhook_base_url", ) yield input_widget self.inputs["webhook_base_url"] = input_widget yield Static(" ") # Langflow Public URL yield Label("Langflow Public URL") current_value = getattr(self.env_manager.config, "langflow_public_url", "") input_widget = Input( placeholder="http://localhost:7860", value=current_value, id="input-langflow_public_url", ) yield input_widget self.inputs["langflow_public_url"] = input_widget yield Static(" ") def _create_field( self, field_name: str, display_name: str, placeholder: str, can_generate: bool, required: bool = False, ) -> ComposeResult: """Create a single form field.""" # Create label label_text = f"{display_name}" if required: label_text += " *" yield Label(label_text) # Get current value current_value = getattr(self.env_manager.config, field_name, "") # Create input with appropriate validator if field_name == "openai_api_key": input_widget = Input( placeholder=placeholder, value=current_value, password=True, validators=[OpenAIKeyValidator()], id=f"input-{field_name}", ) elif field_name == "openrag_documents_paths": input_widget = Input( placeholder=placeholder, value=current_value, validators=[DocumentsPathValidator()], id=f"input-{field_name}", ) elif "password" in field_name or "secret" in field_name: input_widget = Input( placeholder=placeholder, value=current_value, password=True, id=f"input-{field_name}", ) else: input_widget = Input( placeholder=placeholder, value=current_value, id=f"input-{field_name}" ) yield input_widget self.inputs[field_name] = input_widget # Add spacing yield Static(" ") def on_mount(self) -> None: """Initialize the screen when mounted.""" # Set initial visibility of username field based on password current_password = getattr(self.env_manager.config, "langflow_superuser_password", "") self._update_langflow_username_visibility(current_password) # Focus the first input field try: # Find the first input field and focus it inputs = self.query(Input) if inputs: inputs[0].focus() except Exception: pass def on_checkbox_changed(self, event: Checkbox.Changed) -> None: """Handle checkbox changes.""" if event.checkbox.id == "generate-langflow-password": langflow_password_input = self.inputs.get("langflow_superuser_password") if event.value: # Generate password when checked password = self.env_manager.generate_secure_password() if langflow_password_input: langflow_password_input.value = password # Show username field self._update_langflow_username_visibility(password) self.notify("Generated Langflow password", severity="information") else: # Clear password when unchecked (enable autologin) if langflow_password_input: langflow_password_input.value = "" # Hide username field self._update_langflow_username_visibility("") self.notify("Cleared Langflow password - autologin enabled", severity="information") def on_button_pressed(self, event: Button.Pressed) -> None: """Handle button presses.""" if event.button.id == "generate-btn": self.action_generate() elif event.button.id == "save-btn": self.action_save() elif event.button.id == "back-btn": self.action_back() elif event.button.id == "pick-docs-btn": self.action_pick_documents_path() elif event.button.id == "pick-opensearch-data-btn": self.action_pick_opensearch_data_path() elif event.button.id == "toggle-opensearch-password": # Toggle OpenSearch password visibility input_widget = self.inputs.get("opensearch_password") if input_widget: input_widget.password = not input_widget.password event.button.label = "πŸ™ˆ" if not input_widget.password else "πŸ‘" elif event.button.id == "toggle-langflow-password": # Toggle Langflow password visibility input_widget = self.inputs.get("langflow_superuser_password") if input_widget: input_widget.password = not input_widget.password event.button.label = "πŸ™ˆ" if not input_widget.password else "πŸ‘" elif event.button.id == "toggle-anthropic-key": # Toggle Anthropic API key visibility input_widget = self.inputs.get("anthropic_api_key") if input_widget: input_widget.password = not input_widget.password event.button.label = "Hide" if not input_widget.password else "Show" elif event.button.id == "toggle-watsonx-key": # Toggle watsonx API key visibility input_widget = self.inputs.get("watsonx_api_key") if input_widget: input_widget.password = not input_widget.password event.button.label = "Hide" if not input_widget.password else "Show" def action_generate(self) -> None: """Generate secure passwords for admin accounts.""" # First sync input values to config to get current state opensearch_input = self.inputs.get("opensearch_password") if opensearch_input: self.env_manager.config.opensearch_password = opensearch_input.value # Only generate OpenSearch password if empty if not self.env_manager.config.opensearch_password: self.env_manager.config.opensearch_password = self.env_manager.generate_secure_password() # Update secret keys if not self.env_manager.config.langflow_secret_key: self.env_manager.config.langflow_secret_key = self.env_manager.generate_langflow_secret_key() # Update input fields with generated values if opensearch_input: opensearch_input.value = self.env_manager.config.opensearch_password self.notify("Generated secure password for OpenSearch", severity="information") def action_save(self) -> None: """Save the configuration.""" # First, check Textual input validators validation_errors = [] for field_name, input_widget in self.inputs.items(): # Skip empty values as they may be optional or auto-generated if not input_widget.value: continue # Check if input has validators and manually validate if hasattr(input_widget, "validators") and input_widget.validators: for validator in input_widget.validators: result = validator.validate(input_widget.value) if result and not result.is_valid: for failure in result.failures: validation_errors.append(f"{field_name}: {failure.description}") if validation_errors: self.notify( f"Validation failed:\n" + "\n".join(validation_errors[:3]), severity="error", ) return # Update config from input fields for field_name, input_widget in self.inputs.items(): setattr(self.env_manager.config, field_name, input_widget.value) # Generate secure defaults for empty passwords/keys BEFORE validation self.env_manager.setup_secure_defaults() # Validate the configuration if not self.env_manager.validate_config(self.mode): error_messages = [] for field, error in self.env_manager.config.validation_errors.items(): error_messages.append(f"{field}: {error}") self.notify( f"Validation failed:\n" + "\n".join(error_messages[:3]), severity="error", ) return # Save to file if self.env_manager.save_env_file(): self.notify("Configuration saved successfully!", severity="information") # Go back to welcome screen self.dismiss() else: self.notify("Failed to save configuration", severity="error") def action_back(self) -> None: """Go back to welcome screen.""" self.app.pop_screen() def action_pick_documents_path(self) -> None: """Open textual-fspicker to select a path and append it to the input.""" try: import importlib fsp = importlib.import_module("textual_fspicker") except Exception: self.notify("textual-fspicker not available", severity="warning") return # Determine starting path from current input if possible input_widget = self.inputs.get("openrag_documents_paths") start = Path.home() if input_widget and input_widget.value: first = input_widget.value.split(",")[0].strip() if first: start = Path(first).expanduser() # Prefer SelectDirectory for directories; fallback to FileOpen PickerClass = getattr(fsp, "SelectDirectory", None) or getattr( fsp, "FileOpen", None ) if PickerClass is None: self.notify( "No compatible picker found in textual-fspicker", severity="warning" ) return try: picker = PickerClass(location=start) except Exception: try: picker = PickerClass(start) except Exception: self.notify("Could not initialize textual-fspicker", severity="warning") return def _append_path(result) -> None: if not result: return path_str = str(result) if input_widget is None: return current = input_widget.value or "" paths = [p.strip() for p in current.split(",") if p.strip()] if path_str not in paths: paths.append(path_str) input_widget.value = ",".join(paths) # Push with callback when supported; otherwise, use on_screen_dismissed fallback try: self.app.push_screen(picker, _append_path) # type: ignore[arg-type] except TypeError: self._docs_pick_callback = _append_path # type: ignore[attr-defined] self.app.push_screen(picker) def action_pick_opensearch_data_path(self) -> None: """Open textual-fspicker to select OpenSearch data directory.""" try: import importlib fsp = importlib.import_module("textual_fspicker") except Exception: self.notify("textual-fspicker not available", severity="warning") return # Determine starting path from current input if possible input_widget = self.inputs.get("opensearch_data_path") start = Path.home() if input_widget and input_widget.value: path_str = input_widget.value.strip() if path_str: candidate = Path(path_str).expanduser() # If path doesn't exist, use parent or fallback to home if candidate.exists(): start = candidate elif candidate.parent.exists(): start = candidate.parent # Prefer SelectDirectory for directories; fallback to FileOpen PickerClass = getattr(fsp, "SelectDirectory", None) or getattr( fsp, "FileOpen", None ) if PickerClass is None: self.notify( "No compatible picker found in textual-fspicker", severity="warning" ) return try: picker = PickerClass(location=start) except Exception: try: picker = PickerClass(start) except Exception: self.notify("Could not initialize textual-fspicker", severity="warning") return def _set_path(result) -> None: if not result: return path_str = str(result) if input_widget is None: return input_widget.value = path_str # Push with callback when supported; otherwise, use on_screen_dismissed fallback try: self.app.push_screen(picker, _set_path) # type: ignore[arg-type] except TypeError: self._opensearch_data_pick_callback = _set_path # type: ignore[attr-defined] self.app.push_screen(picker) def on_screen_dismissed(self, event) -> None: # type: ignore[override] try: # textual-fspicker screens should dismiss with a result; hand to callback if present cb = getattr(self, "_docs_pick_callback", None) if cb is not None: cb(getattr(event, "result", None)) try: delattr(self, "_docs_pick_callback") except Exception: pass # Handle OpenSearch data path picker callback cb = getattr(self, "_opensearch_data_pick_callback", None) if cb is not None: cb(getattr(event, "result", None)) try: delattr(self, "_opensearch_data_pick_callback") except Exception: pass except Exception: pass def on_input_changed(self, event: Input.Changed) -> None: """Handle input changes for real-time validation feedback.""" # Handle Langflow password changes - show/hide username field if event.input.id == "input-langflow_superuser_password": self._update_langflow_username_visibility(event.value) # This will trigger validation display in real-time pass def _update_langflow_username_visibility(self, password_value: str) -> None: """Show or hide the Langflow username field based on password presence.""" has_password = bool(password_value and password_value.strip()) # Get the widgets try: username_label = self.query_one("#langflow-username-label") username_input = self.query_one("#input-langflow_superuser") username_spacer = self.query_one("#langflow-username-spacer") # Show or hide based on password presence if has_password: username_label.display = True username_input.display = True username_spacer.display = True else: username_label.display = False username_input.display = False username_spacer.display = False except Exception: # Widgets don't exist yet, ignore pass