From 933e600e9d60d2ebddb465a4f308cb7d5dba912b Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Fri, 21 Nov 2025 14:00:55 -0500 Subject: [PATCH 01/10] Add support for Anthropic, Ollama, and Watsonx config Introduces fields and validation for Anthropic API key, Ollama endpoint, and IBM watsonx.ai API key, endpoint, and project ID in environment management and configuration screens. Updates validation utilities and config UI to support these providers, allowing users to set and validate credentials and endpoints for additional AI services. --- src/tui/managers/env_manager.py | 60 +++++++++++- src/tui/screens/config.py | 160 +++++++++++++++++++++++++++++++- src/tui/utils/validation.py | 21 +++++ 3 files changed, 237 insertions(+), 4 deletions(-) diff --git a/src/tui/managers/env_manager.py b/src/tui/managers/env_manager.py index 04ba0a2a..8bfb8369 100644 --- a/src/tui/managers/env_manager.py +++ b/src/tui/managers/env_manager.py @@ -35,6 +35,13 @@ class EnvConfig: langflow_ingest_flow_id: str = "5488df7c-b93f-4f87-a446-b67028bc0813" langflow_url_ingest_flow_id: str = "72c3d17c-2dac-4a73-b48a-6518473d7830" + # Provider API keys and endpoints + anthropic_api_key: str = "" + ollama_endpoint: str = "" + watsonx_api_key: str = "" + watsonx_endpoint: str = "" + watsonx_project_id: str = "" + # OAuth settings google_oauth_client_id: str = "" google_oauth_client_secret: str = "" @@ -128,6 +135,11 @@ class EnvManager: # Map env vars to config attributes attr_map = { "OPENAI_API_KEY": "openai_api_key", + "ANTHROPIC_API_KEY": "anthropic_api_key", + "OLLAMA_ENDPOINT": "ollama_endpoint", + "WATSONX_API_KEY": "watsonx_api_key", + "WATSONX_ENDPOINT": "watsonx_endpoint", + "WATSONX_PROJECT_ID": "watsonx_project_id", "OPENSEARCH_PASSWORD": "opensearch_password", "LANGFLOW_SECRET_KEY": "langflow_secret_key", "LANGFLOW_SUPERUSER": "langflow_superuser", @@ -197,6 +209,30 @@ class EnvManager: "Invalid OpenAI API key format (should start with sk-)" ) + # Import validation functions for new provider fields + from ..utils.validation import validate_anthropic_api_key + + # Validate Anthropic API key format if provided + if self.config.anthropic_api_key: + if not validate_anthropic_api_key(self.config.anthropic_api_key): + self.config.validation_errors["anthropic_api_key"] = ( + "Invalid Anthropic API key format (should start with sk-ant-)" + ) + + # Validate Ollama endpoint if provided + if self.config.ollama_endpoint: + if not validate_url(self.config.ollama_endpoint): + self.config.validation_errors["ollama_endpoint"] = ( + "Invalid Ollama endpoint URL format" + ) + + # Validate IBM watsonx.ai endpoint if provided + if self.config.watsonx_endpoint: + if not validate_url(self.config.watsonx_endpoint): + self.config.validation_errors["watsonx_endpoint"] = ( + "Invalid IBM watsonx.ai endpoint URL format" + ) + # Validate documents paths only if provided (optional) if self.config.openrag_documents_paths: is_valid, error_msg, _ = validate_documents_paths( @@ -289,9 +325,6 @@ class EnvManager: f.write(f"LANGFLOW_URL_INGEST_FLOW_ID={self._quote_env_value(self.config.langflow_url_ingest_flow_id)}\n") f.write(f"NUDGES_FLOW_ID={self._quote_env_value(self.config.nudges_flow_id)}\n") f.write(f"OPENSEARCH_PASSWORD={self._quote_env_value(self.config.opensearch_password)}\n") - # Only write OpenAI API key if provided (can be set during onboarding instead) - if self.config.openai_api_key: - f.write(f"OPENAI_API_KEY={self._quote_env_value(self.config.openai_api_key)}\n") f.write( f"OPENRAG_DOCUMENTS_PATHS={self._quote_env_value(self.config.openrag_documents_paths)}\n" ) @@ -300,6 +333,27 @@ class EnvManager: ) f.write("\n") + # Provider API keys and endpoints (optional - can be set during onboarding) + provider_vars = [] + if self.config.openai_api_key: + provider_vars.append(("OPENAI_API_KEY", self.config.openai_api_key)) + if self.config.anthropic_api_key: + provider_vars.append(("ANTHROPIC_API_KEY", self.config.anthropic_api_key)) + if self.config.ollama_endpoint: + provider_vars.append(("OLLAMA_ENDPOINT", self.config.ollama_endpoint)) + if self.config.watsonx_api_key: + provider_vars.append(("WATSONX_API_KEY", self.config.watsonx_api_key)) + if self.config.watsonx_endpoint: + provider_vars.append(("WATSONX_ENDPOINT", self.config.watsonx_endpoint)) + if self.config.watsonx_project_id: + provider_vars.append(("WATSONX_PROJECT_ID", self.config.watsonx_project_id)) + + if provider_vars: + f.write("# AI Provider API Keys and Endpoints\n") + for var_name, var_value in provider_vars: + f.write(f"{var_name}={self._quote_env_value(var_value)}\n") + f.write("\n") + # Ingestion settings f.write("# Ingestion settings\n") f.write(f"DISABLE_INGEST_WITH_LANGFLOW={self._quote_env_value(self.config.disable_ingest_with_langflow)}\n") diff --git a/src/tui/screens/config.py b/src/tui/screens/config.py index 52d2b291..83107538 100644 --- a/src/tui/screens/config.py +++ b/src/tui/screens/config.py @@ -20,7 +20,13 @@ from rich.text import Text from pathlib import Path from ..managers.env_manager import EnvManager -from ..utils.validation import validate_openai_api_key, validate_documents_paths +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 @@ -37,6 +43,45 @@ class OpenAIKeyValidator(Validator): 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.""" @@ -227,6 +272,107 @@ class ConfigScreen(Screen): yield Button("Show", id="toggle-openai-key", variant="default") yield Static(" ") + # Anthropic API Key + yield Label("Anthropic API Key") + yield Static( + Text("Get a key: https://console.anthropic.com/settings/keys", style="dim"), + classes="helper-text", + ) + yield Static( + Text("Can also be provided during onboarding", 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") + yield Static( + Text("Endpoint of your Ollama server", style="dim"), + classes="helper-text", + ) + yield Static( + Text("Can also be provided during onboarding", 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") + yield Static( + Text("Get a key: https://cloud.ibm.com/iam/apikeys", style="dim"), + classes="helper-text", + ) + yield Static( + Text("Can also be provided during onboarding", 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 @@ -550,6 +696,18 @@ class ConfigScreen(Screen): 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.""" diff --git a/src/tui/utils/validation.py b/src/tui/utils/validation.py index c9764d00..c91c4f00 100644 --- a/src/tui/utils/validation.py +++ b/src/tui/utils/validation.py @@ -63,6 +63,27 @@ def validate_openai_api_key(key: str) -> bool: return key.startswith("sk-") and len(key) > 20 +def validate_anthropic_api_key(key: str) -> bool: + """Validate Anthropic API key format.""" + if not key: + return False + return key.startswith("sk-ant-") and len(key) > 20 + + +def validate_ollama_endpoint(endpoint: str) -> bool: + """Validate Ollama endpoint URL format.""" + if not endpoint: + return False + return validate_url(endpoint) + + +def validate_watsonx_endpoint(endpoint: str) -> bool: + """Validate IBM watsonx.ai endpoint URL format.""" + if not endpoint: + return False + return validate_url(endpoint) + + def validate_google_oauth_client_id(client_id: str) -> bool: """Validate Google OAuth client ID format.""" if not client_id: From 84c0c8b4ede01632e384eab264d7eae32a6e0891 Mon Sep 17 00:00:00 2001 From: phact Date: Fri, 21 Nov 2025 14:39:36 -0500 Subject: [PATCH 02/10] copy --- src/tui/screens/config.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/tui/screens/config.py b/src/tui/screens/config.py index 83107538..df8d6e98 100644 --- a/src/tui/screens/config.py +++ b/src/tui/screens/config.py @@ -248,14 +248,13 @@ class ConfigScreen(Screen): yield Static(" ") # OpenAI API Key - yield Label("OpenAI API Key") - # Where to create OpenAI keys (helper above the box) + 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 also be provided during onboarding", style="dim italic"), + Text("Can be configured later in the UI", style="dim italic"), classes="helper-text", ) current_value = getattr(self.env_manager.config, "openai_api_key", "") @@ -273,13 +272,13 @@ class ConfigScreen(Screen): yield Static(" ") # Anthropic API Key - yield Label("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 also be provided during onboarding", style="dim italic"), + Text("Can be configured later in the UI", style="dim italic"), classes="helper-text", ) current_value = getattr(self.env_manager.config, "anthropic_api_key", "") @@ -297,13 +296,13 @@ class ConfigScreen(Screen): yield Static(" ") # Ollama Endpoint - yield Label("Ollama Base URL") + yield Label("Ollama Base URL (optional)") yield Static( Text("Endpoint of your Ollama server", style="dim"), classes="helper-text", ) yield Static( - Text("Can also be provided during onboarding", style="dim italic"), + Text("Can be configured later in the UI", style="dim italic"), classes="helper-text", ) current_value = getattr(self.env_manager.config, "ollama_endpoint", "") @@ -318,13 +317,13 @@ class ConfigScreen(Screen): yield Static(" ") # IBM watsonx.ai API Key - yield Label("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 also be provided during onboarding", style="dim italic"), + Text("Can be configured later in the UI", style="dim italic"), classes="helper-text", ) current_value = getattr(self.env_manager.config, "watsonx_api_key", "") From c21270e1ac2cffa627272812c104abcc76f4cd25 Mon Sep 17 00:00:00 2001 From: Mike Fortman Date: Fri, 21 Nov 2025 15:24:03 -0600 Subject: [PATCH 03/10] Fix copy for 1 vs multiple doc deletion --- frontend/app/knowledge/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/knowledge/page.tsx b/frontend/app/knowledge/page.tsx index 91ea9f8a..d9ebf1d6 100644 --- a/frontend/app/knowledge/page.tsx +++ b/frontend/app/knowledge/page.tsx @@ -391,7 +391,7 @@ function SearchPage() { 1 ? "Delete Documents" : "Delete Document"} description={`Are you sure you want to delete ${ selectedRows.length } document${ @@ -400,7 +400,7 @@ function SearchPage() { Documents to be deleted: ${selectedRows.map((row) => `• ${row.filename}`).join("\n")}`} - confirmText="Delete All" + confirmText={selectedRows.length > 1 ? "Delete All" : "Delete"} onConfirm={handleBulkDelete} isLoading={deleteDocumentMutation.isPending} /> From 9e4502d07b87c188383084eb4d080c61614db60d Mon Sep 17 00:00:00 2001 From: phact Date: Fri, 21 Nov 2025 16:59:26 -0500 Subject: [PATCH 04/10] pick up env vars for config (not just existing .env files) --- src/tui/managers/env_manager.py | 115 ++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 51 deletions(-) diff --git a/src/tui/managers/env_manager.py b/src/tui/managers/env_manager.py index 8bfb8369..b1fd73bb 100644 --- a/src/tui/managers/env_manager.py +++ b/src/tui/managers/env_manager.py @@ -116,62 +116,75 @@ class EnvManager: return f"'{escaped_value}'" def load_existing_env(self) -> bool: - """Load existing .env file if it exists.""" - if not self.env_file.exists(): - return False + """Load existing .env file if it exists, or fall back to environment variables.""" + import os + + # Map env vars to config attributes + attr_map = { + "OPENAI_API_KEY": "openai_api_key", + "ANTHROPIC_API_KEY": "anthropic_api_key", + "OLLAMA_ENDPOINT": "ollama_endpoint", + "WATSONX_API_KEY": "watsonx_api_key", + "WATSONX_ENDPOINT": "watsonx_endpoint", + "WATSONX_PROJECT_ID": "watsonx_project_id", + "OPENSEARCH_PASSWORD": "opensearch_password", + "LANGFLOW_SECRET_KEY": "langflow_secret_key", + "LANGFLOW_SUPERUSER": "langflow_superuser", + "LANGFLOW_SUPERUSER_PASSWORD": "langflow_superuser_password", + "LANGFLOW_CHAT_FLOW_ID": "langflow_chat_flow_id", + "LANGFLOW_INGEST_FLOW_ID": "langflow_ingest_flow_id", + "LANGFLOW_URL_INGEST_FLOW_ID": "langflow_url_ingest_flow_id", + "NUDGES_FLOW_ID": "nudges_flow_id", + "GOOGLE_OAUTH_CLIENT_ID": "google_oauth_client_id", + "GOOGLE_OAUTH_CLIENT_SECRET": "google_oauth_client_secret", + "MICROSOFT_GRAPH_OAUTH_CLIENT_ID": "microsoft_graph_oauth_client_id", + "MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET": "microsoft_graph_oauth_client_secret", + "WEBHOOK_BASE_URL": "webhook_base_url", + "AWS_ACCESS_KEY_ID": "aws_access_key_id", + "AWS_SECRET_ACCESS_KEY": "aws_secret_access_key", + "LANGFLOW_PUBLIC_URL": "langflow_public_url", + "OPENRAG_DOCUMENTS_PATHS": "openrag_documents_paths", + "OPENSEARCH_DATA_PATH": "opensearch_data_path", + "LANGFLOW_AUTO_LOGIN": "langflow_auto_login", + "LANGFLOW_NEW_USER_IS_ACTIVE": "langflow_new_user_is_active", + "LANGFLOW_ENABLE_SUPERUSER_CLI": "langflow_enable_superuser_cli", + "DISABLE_INGEST_WITH_LANGFLOW": "disable_ingest_with_langflow", + } + + loaded_from_file = False + + # Try to load from .env file first + if self.env_file.exists(): + try: + with open(self.env_file, "r") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue - try: - with open(self.env_file, "r") as f: - for line in f: - line = line.strip() - if not line or line.startswith("#"): - continue + if "=" in line: + key, value = line.split("=", 1) + key = key.strip() + value = sanitize_env_value(value) - if "=" in line: - key, value = line.split("=", 1) - key = key.strip() - value = sanitize_env_value(value) + if key in attr_map: + setattr(self.config, attr_map[key], value) - # Map env vars to config attributes - attr_map = { - "OPENAI_API_KEY": "openai_api_key", - "ANTHROPIC_API_KEY": "anthropic_api_key", - "OLLAMA_ENDPOINT": "ollama_endpoint", - "WATSONX_API_KEY": "watsonx_api_key", - "WATSONX_ENDPOINT": "watsonx_endpoint", - "WATSONX_PROJECT_ID": "watsonx_project_id", - "OPENSEARCH_PASSWORD": "opensearch_password", - "LANGFLOW_SECRET_KEY": "langflow_secret_key", - "LANGFLOW_SUPERUSER": "langflow_superuser", - "LANGFLOW_SUPERUSER_PASSWORD": "langflow_superuser_password", - "LANGFLOW_CHAT_FLOW_ID": "langflow_chat_flow_id", - "LANGFLOW_INGEST_FLOW_ID": "langflow_ingest_flow_id", - "LANGFLOW_URL_INGEST_FLOW_ID": "langflow_url_ingest_flow_id", - "NUDGES_FLOW_ID": "nudges_flow_id", - "GOOGLE_OAUTH_CLIENT_ID": "google_oauth_client_id", - "GOOGLE_OAUTH_CLIENT_SECRET": "google_oauth_client_secret", - "MICROSOFT_GRAPH_OAUTH_CLIENT_ID": "microsoft_graph_oauth_client_id", - "MICROSOFT_GRAPH_OAUTH_CLIENT_SECRET": "microsoft_graph_oauth_client_secret", - "WEBHOOK_BASE_URL": "webhook_base_url", - "AWS_ACCESS_KEY_ID": "aws_access_key_id", - "AWS_SECRET_ACCESS_KEY": "aws_secret_access_key", - "LANGFLOW_PUBLIC_URL": "langflow_public_url", - "OPENRAG_DOCUMENTS_PATHS": "openrag_documents_paths", - "OPENSEARCH_DATA_PATH": "opensearch_data_path", - "LANGFLOW_AUTO_LOGIN": "langflow_auto_login", - "LANGFLOW_NEW_USER_IS_ACTIVE": "langflow_new_user_is_active", - "LANGFLOW_ENABLE_SUPERUSER_CLI": "langflow_enable_superuser_cli", - "DISABLE_INGEST_WITH_LANGFLOW": "disable_ingest_with_langflow", - } - - if key in attr_map: - setattr(self.config, attr_map[key], value) + loaded_from_file = True + except Exception as e: + logger.error("Error loading .env file", error=str(e)) + + # Fall back to environment variables if .env file doesn't exist or failed to load + if not loaded_from_file: + logger.info("No .env file found, loading from environment variables") + for env_key, attr_name in attr_map.items(): + value = os.environ.get(env_key, "") + if value: + setattr(self.config, attr_name, value) return True - - except Exception as e: - logger.error("Error loading .env file", error=str(e)) - return False + + return loaded_from_file def setup_secure_defaults(self) -> None: """Set up secure default values for passwords and keys.""" From 406b783a1aafb28378a8c266229b0b43772460d0 Mon Sep 17 00:00:00 2001 From: phact Date: Fri, 21 Nov 2025 17:14:05 -0500 Subject: [PATCH 05/10] conditional buttons and copy --- src/tui/screens/config.py | 14 ++++++++++---- src/tui/screens/welcome.py | 32 ++++++++++++++++++++++++-------- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/tui/screens/config.py b/src/tui/screens/config.py index df8d6e98..51662964 100644 --- a/src/tui/screens/config.py +++ b/src/tui/screens/config.py @@ -145,6 +145,9 @@ class ConfigScreen(Screen): 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() @@ -156,12 +159,15 @@ class ConfigScreen(Screen): with ScrollableContainer(id="config-scroll"): with Vertical(id="config-form"): yield from self._create_all_fields() - yield Horizontal( + # Create button row - conditionally include Back button + buttons = [ Button("Generate Passwords", variant="default", id="generate-btn"), Button("Save Configuration", variant="success", id="save-btn"), - Button("Back", variant="default", id="back-btn"), - classes="button-row", - ) + ] + # 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: diff --git a/src/tui/screens/welcome.py b/src/tui/screens/welcome.py index 66321010..64ad888a 100644 --- a/src/tui/screens/welcome.py +++ b/src/tui/screens/welcome.py @@ -22,13 +22,6 @@ class WelcomeScreen(Screen): BINDINGS = [ ("q", "quit", "Quit"), - ("enter", "default_action", "Continue"), - ("1", "no_auth_setup", "Basic Setup"), - ("2", "full_setup", "Advanced Setup"), - ("3", "monitor", "Status"), - ("4", "diagnostics", "Diagnostics"), - ("5", "start_stop_services", "Start/Stop Services"), - ("6", "open_app", "Open App"), ] def __init__(self): @@ -41,6 +34,9 @@ class WelcomeScreen(Screen): self.has_oauth_config = False self.default_button_id = "basic-setup-btn" self._state_checked = False + + # Check if .env file exists + self.has_env_file = self.env_manager.env_file.exists() # Load .env file if it exists load_dotenv() @@ -161,6 +157,23 @@ class WelcomeScreen(Screen): 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 @@ -189,7 +202,7 @@ class WelcomeScreen(Screen): ) buttons.append( - Button("Start All Services", variant="primary", id="start-all-services-btn") + Button("Start OpenRAG", variant="primary", id="start-all-services-btn") ) # Always show status option @@ -253,6 +266,9 @@ class WelcomeScreen(Screen): 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) From 2322c0e14f0a687768422a4d6fabfef1f040ae7e Mon Sep 17 00:00:00 2001 From: phact Date: Fri, 21 Nov 2025 17:25:40 -0500 Subject: [PATCH 06/10] longer notifications --- src/tui/main.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/tui/main.py b/src/tui/main.py index e0f60df2..19468473 100644 --- a/src/tui/main.py +++ b/src/tui/main.py @@ -365,6 +365,21 @@ class OpenRAGTUI(App): self.container_manager = ContainerManager() self.env_manager = EnvManager() self.docling_manager = DoclingManager() # Initialize singleton instance + + def notify( + self, + message: str, + *, + title: str = "", + severity: str = "information", + timeout: float | None = None, + markup: bool = True, + ) -> None: + """Override notify to make notifications last 20 seconds by default.""" + # If timeout is None (default), make it 20 seconds + if timeout is None: + timeout = 20.0 + super().notify(message, title=title, severity=severity, timeout=timeout, markup=markup) def on_mount(self) -> None: """Initialize the application.""" From 4b3d22a64f71780b33d60b4f0b1ab745dd038c38 Mon Sep 17 00:00:00 2001 From: Mike Fortman Date: Fri, 21 Nov 2025 16:28:24 -0600 Subject: [PATCH 07/10] Update step count --- frontend/app/onboarding/_components/onboarding-content.tsx | 4 ++-- frontend/lib/constants.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/app/onboarding/_components/onboarding-content.tsx b/frontend/app/onboarding/_components/onboarding-content.tsx index 6c6cffac..ee47f347 100644 --- a/frontend/app/onboarding/_components/onboarding-content.tsx +++ b/frontend/app/onboarding/_components/onboarding-content.tsx @@ -171,7 +171,7 @@ export function OnboardingContent({ /> - {/* Step 2 */} + {/* Step 3 */} = 2} isCompleted={currentStep > 2 || !!selectedNudge} @@ -209,7 +209,7 @@ export function OnboardingContent({ /> )} - {/* Step 3 */} + {/* Step 4 */} = 3 && !isLoading && !!displayMessage} isCompleted={currentStep > 3} diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts index daa5d778..402ace74 100644 --- a/frontend/lib/constants.ts +++ b/frontend/lib/constants.ts @@ -28,7 +28,7 @@ export const UI_CONSTANTS = { export const ANIMATION_DURATION = 0.4; export const SIDEBAR_WIDTH = 280; export const HEADER_HEIGHT = 54; -export const TOTAL_ONBOARDING_STEPS = 5; +export const TOTAL_ONBOARDING_STEPS = 4; /** * Local Storage Keys From f242a700a828f8d0e568275b2cb9e045c01cec95 Mon Sep 17 00:00:00 2001 From: phact Date: Sun, 23 Nov 2025 19:33:28 -0500 Subject: [PATCH 08/10] crank langflow client config to 20 minutes --- src/config/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/settings.py b/src/config/settings.py index 56c85292..df221986 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -334,7 +334,7 @@ class AppClients: # Initialize Langflow HTTP client self.langflow_http_client = httpx.AsyncClient( - base_url=LANGFLOW_URL, timeout=300.0 + base_url=LANGFLOW_URL, timeout=1200.0 ) return self From b13380448a1d08392e1bf0b94e03b56718bf50a4 Mon Sep 17 00:00:00 2001 From: phact Date: Sun, 23 Nov 2025 19:43:00 -0500 Subject: [PATCH 09/10] folder upload file extensions --- frontend/components/knowledge-dropdown.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/components/knowledge-dropdown.tsx b/frontend/components/knowledge-dropdown.tsx index 643ee0e5..27725b0c 100644 --- a/frontend/components/knowledge-dropdown.tsx +++ b/frontend/components/knowledge-dropdown.tsx @@ -251,10 +251,26 @@ export function KnowledgeDropdown() { ".pdf", ".doc", ".docx", + ".pptx", + ".ppt", + ".xlsx", + ".xls", + ".csv", ".txt", ".md", + ".html", + ".htm", ".rtf", ".odt", + ".asciidoc", + ".adoc", + ".png", + ".jpg", + ".jpeg", + ".gif", + ".bmp", + ".tiff", + ".webp", ]; const filteredFiles = fileList.filter((file) => { From 9cee47071eae72c94f080f7d833274bf13503bde Mon Sep 17 00:00:00 2001 From: phact Date: Mon, 24 Nov 2025 09:58:14 -0500 Subject: [PATCH 10/10] validate and filter in one place + fix copy --- frontend/components/knowledge-dropdown.tsx | 58 +++++++++++----------- uv.lock | 4 +- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/frontend/components/knowledge-dropdown.tsx b/frontend/components/knowledge-dropdown.tsx index 27725b0c..d0100790 100644 --- a/frontend/components/knowledge-dropdown.tsx +++ b/frontend/components/knowledge-dropdown.tsx @@ -43,6 +43,33 @@ import { } from "@/lib/upload-utils"; import { cn } from "@/lib/utils"; +// Supported file extensions - single source of truth +const SUPPORTED_EXTENSIONS = [ + ".pdf", + ".doc", + ".docx", + ".pptx", + ".ppt", + ".xlsx", + ".xls", + ".csv", + ".txt", + ".md", + ".html", + ".htm", + ".rtf", + ".odt", + ".asciidoc", + ".adoc", + ".png", + ".jpg", + ".jpeg", + ".gif", + ".bmp", + ".tiff", + ".webp", +]; + export function KnowledgeDropdown() { const { addTask } = useTask(); const { refetch: refetchTasks } = useGetTasksQuery(); @@ -247,43 +274,18 @@ export function KnowledgeDropdown() { try { const fileList = Array.from(files); - const supportedExtensions = [ - ".pdf", - ".doc", - ".docx", - ".pptx", - ".ppt", - ".xlsx", - ".xls", - ".csv", - ".txt", - ".md", - ".html", - ".htm", - ".rtf", - ".odt", - ".asciidoc", - ".adoc", - ".png", - ".jpg", - ".jpeg", - ".gif", - ".bmp", - ".tiff", - ".webp", - ]; const filteredFiles = fileList.filter((file) => { const ext = file.name .substring(file.name.lastIndexOf(".")) .toLowerCase(); - return supportedExtensions.includes(ext); + return SUPPORTED_EXTENSIONS.includes(ext); }); if (filteredFiles.length === 0) { toast.error("No supported files found", { description: - "Please select a folder containing PDF, DOC, DOCX, TXT, MD, RTF, or ODT files.", + "Please select a folder containing supported document files (PDF, DOCX, PPTX, XLSX, CSV, HTML, images, etc.).", }); return; } @@ -543,7 +545,7 @@ export function KnowledgeDropdown() { type="file" onChange={handleFileChange} className="hidden" - accept=".pdf,.doc,.docx,.txt,.md,.rtf,.odt" + accept={SUPPORTED_EXTENSIONS.join(",")} />