diff --git a/lib/utils/api_key_manager/components/ai_providers_setup.py b/lib/utils/api_key_manager/components/ai_providers_setup.py
index 1774e18c..198d7011 100644
--- a/lib/utils/api_key_manager/components/ai_providers_setup.py
+++ b/lib/utils/api_key_manager/components/ai_providers_setup.py
@@ -5,12 +5,12 @@ from loguru import logger
import streamlit as st
import os
import sys
-# Assuming api_key_tests are in the parent directory relative to this component
-from ..api_key_tests import (
+# Corrected import: Assuming validation functions are in validation.py in the parent directory
+from ..validation import (
test_openai_api_key,
test_gemini_api_key,
- test_anthropic_api_key, # Keep if needed elsewhere, not used in this step currently
- test_deepseek_api_key, # Keep if needed elsewhere
+ # test_anthropic_api_key, # Keep commented if not used or add if needed
+ # test_deepseek_api_key, # Keep commented if not used or add if needed
test_mistral_api_key
)
@@ -23,11 +23,24 @@ def _validate_provider_key(provider_name: str, key_value: str) -> bool:
try:
logger.debug(f"Validating key for {provider_name}...")
if provider_name == "openai":
- is_valid = test_openai_api_key(key_value)
+ # Ensure the function exists in validation.py
+ if callable(getattr(sys.modules[__name__], 'test_openai_api_key', None)):
+ is_valid = test_openai_api_key(key_value)
+ else:
+ logger.error("test_openai_api_key not found in validation module")
+ is_valid = False # Assume invalid if test func missing
elif provider_name == "gemini":
- is_valid = test_gemini_api_key(key_value)
+ if callable(getattr(sys.modules[__name__], 'test_gemini_api_key', None)):
+ is_valid = test_gemini_api_key(key_value)
+ else:
+ logger.error("test_gemini_api_key not found in validation module")
+ is_valid = False
elif provider_name == "mistral":
- is_valid = test_mistral_api_key(key_value)
+ if callable(getattr(sys.modules[__name__], 'test_mistral_api_key', None)):
+ is_valid = test_mistral_api_key(key_value)
+ else:
+ logger.error("test_mistral_api_key not found in validation module")
+ is_valid = False
else:
logger.warning(f"Validation not implemented for provider: {provider_name}")
return False # Or True if unknown providers are allowed without validation
@@ -134,14 +147,15 @@ def render_ai_providers_setup(api_key_manager) -> Dict[str, Any]:
)
# Feedback Area for OpenAI
openai_status = st.session_state.get("openai_status", "unsaved")
+ feedback_placeholder_openai = st.empty()
if openai_status == "saving":
- st.spinner("Validating OpenAI key...")
+ feedback_placeholder_openai.info("Validating OpenAI key...", icon="⏳")
elif openai_status == "valid":
- st.success("OpenAI key saved and valid!", icon="✅")
+ feedback_placeholder_openai.success("OpenAI key saved and valid!", icon="✅")
elif openai_status == "invalid":
- st.error("Invalid OpenAI key. Please check and try again.", icon="❌")
+ feedback_placeholder_openai.error("Invalid OpenAI key. Please check and try again.", icon="❌")
elif openai_status == "error":
- st.error("Error saving/validating OpenAI key.", icon="⚠️")
+ feedback_placeholder_openai.error("Error saving/validating OpenAI key.", icon="⚠️")
# --- Google Gemini ---
st.subheader("Google Gemini (Required)")
@@ -157,14 +171,15 @@ def render_ai_providers_setup(api_key_manager) -> Dict[str, Any]:
)
# Feedback Area for Gemini
gemini_status = st.session_state.get("gemini_status", "unsaved")
+ feedback_placeholder_gemini = st.empty()
if gemini_status == "saving":
- st.spinner("Validating Gemini key...")
+ feedback_placeholder_gemini.info("Validating Gemini key...", icon="⏳")
elif gemini_status == "valid":
- st.success("Gemini key saved and valid!", icon="✅")
+ feedback_placeholder_gemini.success("Gemini key saved and valid!", icon="✅")
elif gemini_status == "invalid":
- st.error("Invalid Gemini key. Please check and try again.", icon="❌")
+ feedback_placeholder_gemini.error("Invalid Gemini key. Please check and try again.", icon="❌")
elif gemini_status == "error":
- st.error("Error saving/validating Gemini key.", icon="⚠️")
+ feedback_placeholder_gemini.error("Error saving/validating Gemini key.", icon="⚠️")
# --- Mistral AI (Optional) ---
st.subheader("Mistral AI (Optional)")
@@ -180,20 +195,19 @@ def render_ai_providers_setup(api_key_manager) -> Dict[str, Any]:
)
# Feedback Area for Mistral
mistral_status = st.session_state.get("mistral_status", "unsaved")
+ feedback_placeholder_mistral = st.empty()
if mistral_status == "saving":
- st.spinner("Validating Mistral key...")
+ feedback_placeholder_mistral.info("Validating Mistral key...", icon="⏳")
elif mistral_status == "valid":
- st.success("Mistral key saved and valid!", icon="✅")
+ feedback_placeholder_mistral.success("Mistral key saved and valid!", icon="✅")
elif mistral_status == "invalid":
- st.error("Invalid Mistral key. Please check and try again.", icon="❌")
+ feedback_placeholder_mistral.error("Invalid Mistral key. Please check and try again.", icon="❌")
elif mistral_status == "error":
- st.error("Error saving/validating Mistral key.", icon="⚠️")
+ feedback_placeholder_mistral.error("Error saving/validating Mistral key.", icon="⚠️")
# --- Final Notes ---
st.info("Note: At least one AI provider (OpenAI or Google Gemini) must have a valid API key to proceed.")
- # REMOVED the old saving logic block here
-
# Return value is not strictly needed if navigation relies on session state status
return {}
diff --git a/lib/utils/api_key_manager/components/ai_research_setup.py b/lib/utils/api_key_manager/components/ai_research_setup.py
index 83707f6c..3df488b7 100644
--- a/lib/utils/api_key_manager/components/ai_research_setup.py
+++ b/lib/utils/api_key_manager/components/ai_research_setup.py
@@ -4,12 +4,11 @@ import streamlit as st
from loguru import logger
from typing import Dict, Any
from ..manager import APIKeyManager
-# Removed import of render_navigation_buttons as it's handled in base
import os
from dotenv import load_dotenv # Keep if api_key_manager uses it
import sys
-# Import test functions (adjust path if needed)
-from ..api_key_tests import (
+# Corrected import: Assuming validation functions are in validation.py in the parent directory
+from ..validation import (
test_serpapi_key,
test_tavily_key,
test_metaphor_key,
@@ -28,14 +27,31 @@ def _validate_research_key(provider_name: str, key_value: str) -> bool:
return False
try:
logger.debug(f"Validating key for {provider_name}...")
+ # Ensure the function exists in validation.py before calling
if provider_name == "serpapi":
- is_valid = test_serpapi_key(key_value)
+ if callable(getattr(sys.modules[__name__], 'test_serpapi_key', None)):
+ is_valid = test_serpapi_key(key_value)
+ else:
+ logger.error("test_serpapi_key not found in validation module")
+ is_valid = False
elif provider_name == "tavily":
- is_valid = test_tavily_key(key_value)
+ if callable(getattr(sys.modules[__name__], 'test_tavily_key', None)):
+ is_valid = test_tavily_key(key_value)
+ else:
+ logger.error("test_tavily_key not found in validation module")
+ is_valid = False
elif provider_name == "metaphor":
- is_valid = test_metaphor_key(key_value)
+ if callable(getattr(sys.modules[__name__], 'test_metaphor_key', None)):
+ is_valid = test_metaphor_key(key_value)
+ else:
+ logger.error("test_metaphor_key not found in validation module")
+ is_valid = False
elif provider_name == "firecrawl":
- is_valid = test_firecrawl_key(key_value)
+ if callable(getattr(sys.modules[__name__], 'test_firecrawl_key', None)):
+ is_valid = test_firecrawl_key(key_value)
+ else:
+ logger.error("test_firecrawl_key not found in validation module")
+ is_valid = False
else:
logger.warning(f"Validation not implemented for research provider: {provider_name}")
return False # Default to False for unknown providers
@@ -219,7 +235,5 @@ def render_ai_research_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]:
st.info("Integrations for Bing Search and Google Search APIs are planned.")
st.info("You can skip this step if you don't need these research tools. Click Continue to proceed.")
-
- # Removed the old saving logic block here
-
+
return {}
diff --git a/lib/utils/api_key_manager/components/website_setup.py b/lib/utils/api_key_manager/components/website_setup.py
index 6ae840ca..66f47ddb 100644
--- a/lib/utils/api_key_manager/components/website_setup.py
+++ b/lib/utils/api_key_manager/components/website_setup.py
@@ -2,265 +2,208 @@
import streamlit as st
from loguru import logger
-from ...website_analyzer import analyze_website
-from ...website_analyzer.seo_analyzer import analyze_seo
+# Removed website_analyzer imports as analysis is separate now
+# from ...website_analyzer import analyze_website
+# from ...website_analyzer.seo_analyzer import analyze_seo
import asyncio
import sys
from typing import Dict, Any
-from ..manager import APIKeyManager
-from .base import render_navigation_buttons
+import requests
+import ssl
+import socket
+from urllib.parse import urlparse
+
+from ..manager import APIKeyManager
+# Navigation is handled in base.py now
+# from .base import render_navigation_buttons
+
+# Configure logger (minimal example)
+logger.add(sys.stderr, level="INFO")
+
+# --- Validation Helpers ---
+def _is_valid_url_format(url: str) -> bool:
+ """Checks if the URL has a valid basic format (scheme and netloc)."""
+ try:
+ result = urlparse(url)
+ return all([result.scheme in ['http', 'https'], result.netloc])
+ except ValueError:
+ return False
+
+def _check_url_reachability(url: str) -> tuple[bool, str]:
+ """Checks if the URL is reachable and returns status code or error."""
+ try:
+ response = requests.head(url, allow_redirects=True, timeout=5) # HEAD request is faster
+ response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
+ logger.info(f"URL {url} reachable, status code: {response.status_code}")
+ return True, f"Reachable (Status: {response.status_code})"
+ except requests.exceptions.Timeout:
+ logger.warning(f"URL {url} timed out.")
+ return False, "Timeout: Server did not respond in time."
+ except requests.exceptions.RequestException as e:
+ logger.warning(f"URL {url} not reachable: {e}")
+ # Provide a more user-friendly message for common errors
+ if isinstance(e, requests.exceptions.ConnectionError):
+ return False, "Connection Error: Could not connect to the server."
+ elif isinstance(e, requests.exceptions.HTTPError):
+ return False, f"HTTP Error: {e.response.status_code}"
+ return False, f"Error: {type(e).__name__}"
+
+def _check_ssl_certificate(url: str) -> tuple[bool, str]:
+ """Checks if the URL has a valid SSL certificate (for https)."""
+ parsed_url = urlparse(url)
+ if parsed_url.scheme != 'https':
+ return True, "(HTTP URL)" # Not applicable for http
+
+ hostname = parsed_url.netloc
+ port = 443
+ context = ssl.create_default_context()
+ try:
+ with socket.create_connection((hostname, port), timeout=3) as sock:
+ with context.wrap_socket(sock, server_hostname=hostname) as ssock:
+ cert = ssock.getpeercert()
+ # Basic check: does it exist? More thorough checks (expiry, chain) are possible
+ if cert:
+ logger.info(f"SSL certificate found for {hostname}")
+ return True, "Valid SSL Certificate"
+ else:
+ logger.warning(f"No SSL certificate found for {hostname}")
+ return False, "No SSL Certificate found"
+ except ssl.SSLCertVerificationError as e:
+ logger.warning(f"SSL Verification Error for {hostname}: {e}")
+ return False, f"SSL Verification Error: {e.verify_message}"
+ except socket.timeout:
+ logger.warning(f"SSL check timed out for {hostname}")
+ return False, "SSL Check Timeout"
+ except Exception as e:
+ logger.error(f"Error checking SSL for {hostname}: {e}", exc_info=True)
+ return False, f"SSL Check Error: {type(e).__name__}"
+
+
+# --- Main Component Logic ---
+
+def _validate_website_url(url: str) -> tuple[str, str]:
+ """Performs quick validation (format, reachability, basic SSL)."""
+ if not url:
+ return "unsaved", ""
+
+ # 1. Format Check
+ if not _is_valid_url_format(url):
+ logger.warning(f"Invalid URL format: {url}")
+ return "invalid_format", "Invalid URL format. Please include http:// or https://"
+
+ # 2. Reachability Check
+ reachable, reach_status = _check_url_reachability(url)
+ if not reachable:
+ logger.warning(f"URL not reachable: {url} ({reach_status})")
+ return "unreachable", reach_status # Return specific error message
+
+ # 3. Basic SSL Check (only if reachable and HTTPS)
+ if urlparse(url).scheme == 'https':
+ ssl_valid, ssl_status = _check_ssl_certificate(url)
+ if not ssl_valid:
+ logger.warning(f"SSL check failed for {url} ({ssl_status})")
+ return "ssl_error", ssl_status # Return specific error message
+
+ logger.info(f"URL validation successful for: {url}")
+ return "valid", "URL is valid and reachable."
+
+
+def _handle_website_url_change(api_key_manager: APIKeyManager):
+ """Save and validate website URL when input changes."""
+ url_input_widget_key = "website_url_input"
+ status_widget_key = "website_url_status"
+
+ if url_input_widget_key not in st.session_state:
+ logger.warning(f"Input widget key '{url_input_widget_key}' not found.")
+ return
+
+ url_value = st.session_state[url_input_widget_key]
+ logger.debug(f"Handling website URL change. URL: {url_value}")
+
+ # Save the URL regardless of validity for now (maybe refine later)
+ # api_key_manager might not be the right place, consider storing directly in session state
+ # or a dedicated config manager if this isn't an API key.
+ # Let's store in session_state for now.
+ st.session_state['configured_website_url'] = url_value
+ logger.info(f"Saved website URL to session state: {url_value}")
+
+ if not url_value:
+ st.session_state[status_widget_key] = ("unsaved", "")
+ logger.info("Cleared website URL.")
+ return
+
+ st.session_state[status_widget_key] = ("saving", "") # Indicate validation is running
+ st.rerun()
+
+ try:
+ validation_status, message = _validate_website_url(url_value)
+ st.session_state[status_widget_key] = (validation_status, message)
+ logger.info(f"Website URL validation complete. Status: {validation_status}, Msg: {message}")
+
+ except Exception as e:
+ logger.error(f"Error during website URL validation: {e}", exc_info=True)
+ st.session_state[status_widget_key] = ("error", "An unexpected error occurred during validation.")
-# Configure logger to output to both file and stdout
-logger.remove() # Remove default handler
-logger.add(
- "logs/website_setup.log",
- rotation="500 MB",
- retention="10 days",
- level="DEBUG",
- format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}"
-)
-logger.add(
- sys.stdout,
- level="INFO",
- format="
Enter your primary website URL. This helps Alwrity personalize suggestions and analyze your content.
+