"""Website setup component for the API key manager.""" import streamlit as st from loguru import logger # 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 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.") def render_website_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: """Render the website setup step with immediate feedback.""" logger.info("[render_website_setup] Rendering website setup component") status_key = "website_url_status" # Initialize status if status_key not in st.session_state: st.session_state[status_key] = ("unsaved", "") # Optionally pre-validate if a URL exists from previous session/config # pre_existing_url = api_key_manager.get_config("website_url") # Example # if pre_existing_url: # st.session_state[status_key] = _validate_website_url(pre_existing_url) st.markdown("""

Step 2: Website Setup (Optional)

Enter your primary website URL. This helps Alwrity personalize suggestions and analyze your content.

""", unsafe_allow_html=True) # Get current value from session state if available, otherwise empty current_url = st.session_state.get('configured_website_url', "") st.text_input( "Website URL", value=current_url, placeholder="https://example.com", key="website_url_input", on_change=_handle_website_url_change, args=(api_key_manager,) # Pass manager if needed by save logic ) # --- Feedback Area --- status, message = st.session_state.get(status_key, ("unsaved", "")) feedback_placeholder = st.empty() if status == "saving": feedback_placeholder.info("Validating URL...", icon="⏳") elif status == "valid": feedback_placeholder.success(message, icon="✅") elif status == "invalid_format": feedback_placeholder.error(f"Format Error: {message}", icon="❌") elif status == "unreachable": feedback_placeholder.error(f"Reachability Error: {message}", icon="❌") elif status == "ssl_error": feedback_placeholder.warning(f"SSL Warning: {message}", icon="⚠️") # Warning for SSL elif status == "error": feedback_placeholder.error(f"Validation Error: {message}", icon="⚠️") elif status == "unsaved" and current_url: # Show warning if field has text but isn't validated yet feedback_placeholder.warning("URL not yet validated.", icon="⚠️") # --- Removed Analysis Section --- # The detailed website analysis should be a separate feature, not part of the initial setup validation. st.markdown("---") st.markdown("ℹ️ *The detailed Website Analyzer tool is available separately in the main application.*") st.info("Entering your website URL is optional. Click Continue to proceed.") # Return value is not strictly needed if navigation relies on session state status return {} # Removed old analysis logic and button handling as it's handled in base.py