diff --git a/lib/utils/api_key_manager/components/__init__.py b/lib/utils/api_key_manager/components/__init__.py index 97061d65..858f76b6 100644 --- a/lib/utils/api_key_manager/components/__init__.py +++ b/lib/utils/api_key_manager/components/__init__.py @@ -1,20 +1,20 @@ """API key manager components package.""" -from .website_setup import render_website_setup from .ai_research_setup import render_ai_research_setup from .ai_providers import render_ai_providers from .final_setup import render_final_setup from .personalization_setup import render_personalization_setup from .alwrity_integrations import render_alwrity_integrations from .base import render_navigation_buttons, render_step_indicator +from .website_setup import render_website_setup __all__ = [ - 'render_website_setup', 'render_ai_research_setup', 'render_ai_providers', 'render_final_setup', 'render_personalization_setup', 'render_alwrity_integrations', 'render_navigation_buttons', - 'render_step_indicator' + 'render_step_indicator', + 'render_website_setup' ] \ No newline at end of file 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 6eeaf853..37567968 100644 --- a/lib/utils/api_key_manager/components/ai_research_setup.py +++ b/lib/utils/api_key_manager/components/ai_research_setup.py @@ -41,24 +41,12 @@ def render_ai_research_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: with col1: st.markdown("### The Usual") - # SerpAPI Card - st.markdown(""" -
-
-
🔎
-
SerpAPI
-
-
- Access search engine results for research -
-
- """, unsafe_allow_html=True) - serpapi_key = st.text_input( - "SerpAPI Key", + "## Enter 🔎 SerpAPI", type="password", key="serpapi_key", - help="Enter your SerpAPI key" + help="Enter your SerpAPI key", + placeholder="Access search engine results for research" ) if serpapi_key: @@ -88,31 +76,19 @@ def render_ai_research_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: """, unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) - - # Firecrawl Card - st.markdown(""" -
-
-
đŸ•ˇī¸
-
Firecrawl
-
-
- Web content extraction and analysis -
-
- """, unsafe_allow_html=True) firecrawl_key = st.text_input( - "Firecrawl API Key", + "Enter đŸ•ˇī¸ Firecrawl API Key", type="password", key="firecrawl_key", - help="Enter your Firecrawl API key" + help="Enter your Firecrawl API key", + placeholder="Web content extraction and analysis" ) if firecrawl_key: st.markdown("""
- ✓ API key configured + ✓ Firecrawl API key configured
""", unsafe_allow_html=True) @@ -140,30 +116,18 @@ def render_ai_research_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: with col2: st.markdown("### AI Deep Research") - # Tavily Card - st.markdown(""" -
-
-
🤖
-
Tavily AI
-
-
- AI-powered search with semantic understanding -
-
- """, unsafe_allow_html=True) - tavily_key = st.text_input( - "Tavily API Key", + "Enter 🤖 Tavily API Key", type="password", key="tavily_key", - help="Enter your Tavily API key" + help="Enter your Tavily API key", + placeholder="AI-powered search with semantic understanding" ) if tavily_key: st.markdown("""
- ✓ API key configured + ✓ Tavily API key configured
""", unsafe_allow_html=True) @@ -188,24 +152,12 @@ def render_ai_research_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: st.markdown("
", unsafe_allow_html=True) - # Metaphor/Exa Card - st.markdown(""" -
-
-
🧠
-
Metaphor/Exa
-
-
- Neural search engine for deep research -
-
- """, unsafe_allow_html=True) - metaphor_key = st.text_input( - "Metaphor/Exa API Key", + "Enter 🧠 Metaphor/Exa API Key", type="password", key="metaphor_key", - help="Enter your Metaphor/Exa API key" + help="Enter your Metaphor/Exa API key", + placeholder="Neural search engine for deep research" ) if metaphor_key: diff --git a/lib/utils/api_key_manager/components/base.py b/lib/utils/api_key_manager/components/base.py index 953df736..3ea77723 100644 --- a/lib/utils/api_key_manager/components/base.py +++ b/lib/utils/api_key_manager/components/base.py @@ -68,11 +68,10 @@ def render_step_indicator(current_step: int, total_steps: int) -> None: steps = [ ("🔑", "AI LLM", 1), - ("🤖", "Website Setup", 2), - ("👤", "AI Research", 3), - ("🎨", "Personalization", 4), - ("🔄", "Integrations", 5), - ("✅", "Complete", 6) + ("👤", "AI Research", 2), + ("🎨", "Personalization", 3), + ("🔄", "Integrations", 4), + ("✅", "Complete", 5) ] html = '
' @@ -97,7 +96,7 @@ def render_step_indicator(current_step: int, total_steps: int) -> None: logger.error(f"Error rendering step indicator: {str(e)}") st.error("Error displaying step indicator") -def render_navigation_buttons(current_step: int, total_steps: int, changes_made: bool = False) -> bool: +def render_navigation_buttons(current_step: int, total_steps: int, changes_made: bool = True) -> bool: """Render the navigation buttons with modern glassmorphic styling. Args: @@ -113,13 +112,15 @@ def render_navigation_buttons(current_step: int, total_steps: int, changes_made: with col1: if current_step > 1: if st.button("**← Back**", use_container_width=True, key="back_button"): - st.session_state['current_step'] = current_step - 1 + from ..wizard_state import previous_step + previous_step() st.rerun() with col3: if current_step < total_steps: next_text = "**Continue →**" if st.button(next_text, use_container_width=True, disabled=not changes_made, key="next_button"): + # Don't call next_step() here, let the component handle it return True else: if st.button("**Complete Setup ✓**", use_container_width=True, type="primary", key="complete_button"): diff --git a/lib/utils/api_key_manager/components/personalization_setup.py b/lib/utils/api_key_manager/components/personalization_setup.py index cd61fb15..7f332052 100644 --- a/lib/utils/api_key_manager/components/personalization_setup.py +++ b/lib/utils/api_key_manager/components/personalization_setup.py @@ -702,8 +702,8 @@ def render_personalization_setup(api_key_manager: APIKeyManager) -> Dict[str, An st.success("Configuration saved successfully!") # Navigation buttons with correct arguments - if render_navigation_buttons(4, 5, changes_made=True): - st.session_state.current_step = 5 + if render_navigation_buttons(3, 4, changes_made=True): + st.session_state.current_step = 4 st.rerun() - return {"current_step": 4, "changes_made": True} \ No newline at end of file + return {"current_step": 3, "changes_made": True} \ No newline at end of file diff --git a/lib/utils/api_key_manager/components/website_setup.py b/lib/utils/api_key_manager/components/website_setup.py index 51bad20f..6ae840ca 100644 --- a/lib/utils/api_key_manager/components/website_setup.py +++ b/lib/utils/api_key_manager/components/website_setup.py @@ -2,208 +2,265 @@ 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 +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.") +from .base import render_navigation_buttons +# 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="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}" +) def render_website_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: - """Render the website setup step with immediate feedback.""" + """Render the website setup step. + + Args: + api_key_manager (APIKeyManager): The API key manager instance + + Returns: + Dict[str, Any]: Current state + """ 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) + st.markdown("### Step 2: Website Setup") - # Get current value from session state if available, otherwise empty - current_url = st.session_state.get('configured_website_url', "") + # Create two columns for input and results + col1, col2 = st.columns([1, 1]) - 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 - ) + with col1: + st.markdown("#### Enter Website URL") + url = st.text_input("Website URL", placeholder="https://example.com") + logger.debug(f"[render_website_setup] URL input value: {url}") + + analyze_type = st.radio( + "Analysis Type", + ["Basic Analysis", "Full Analysis with SEO"], + help="Choose between basic website analysis or comprehensive SEO analysis" + ) + + if st.button("Analyze Website"): + if url: + with st.spinner("Analyzing website..."): + try: + logger.info(f"[render_website_setup] Starting website analysis for URL: {url}") + + # Call the analyze_website function + results = analyze_website(url) + + # If full analysis is selected, add SEO analysis + if analyze_type == "Full Analysis with SEO": + seo_results = analyze_seo(url) + if seo_results.success: + results['data']['seo_analysis'] = { + 'overall_score': seo_results.overall_score, + 'meta_tags': { + 'title': seo_results.meta_tags.title, + 'description': seo_results.meta_tags.description, + 'keywords': seo_results.meta_tags.keywords, + 'has_robots': seo_results.meta_tags.has_robots, + 'has_sitemap': seo_results.meta_tags.has_sitemap + }, + 'content': { + 'word_count': seo_results.content.word_count, + 'readability_score': seo_results.content.readability_score, + 'content_quality_score': seo_results.content.content_quality_score, + 'headings_structure': seo_results.content.headings_structure, + 'keyword_density': seo_results.content.keyword_density + }, + 'recommendations': [ + { + 'priority': rec.priority, + 'category': rec.category, + 'issue': rec.issue, + 'recommendation': rec.recommendation, + 'impact': rec.impact + } + for rec in seo_results.recommendations + ] + } + + logger.debug(f"[render_website_setup] Analysis results received: {results.get('success', False)}") + + # Store results in session state + st.session_state.website_analysis = results + logger.info("[render_website_setup] Results stored in session state") + + if not results.get('success', False): + error_msg = results.get('error', 'Analysis failed') + logger.error(f"[render_website_setup] Analysis failed: {error_msg}") + st.error(error_msg) + else: + logger.info("[render_website_setup] Analysis completed successfully") + st.success("✅ Website analysis completed successfully!") + except Exception as e: + error_msg = f"Analysis failed: {str(e)}" + logger.error(f"[render_website_setup] {error_msg}") + st.error(error_msg) + else: + logger.warning("[render_website_setup] No URL provided") + st.warning("Please enter a valid URL") - # --- Feedback Area --- - status, message = st.session_state.get(status_key, ("unsaved", "")) - feedback_placeholder = st.empty() + with col2: + st.markdown("#### Analysis Results") + + # Check if we have analysis results + if 'website_analysis' in st.session_state: + results = st.session_state.website_analysis + + if results.get('success', False): + data = results.get('data', {}) + analysis = data.get('analysis', {}) + + # Create tabs for different sections + if analyze_type == "Full Analysis with SEO": + tab1, tab2, tab3, tab4, tab5 = st.tabs([ + "Basic Metrics", + "Content Analysis", + "SEO Analysis", + "Technical SEO", + "Strategy" + ]) + else: + tab1, tab2, tab3, tab4 = st.tabs([ + "Basic Metrics", + "Content Analysis", + "Technical Info", + "Strategy" + ]) + + with tab1: + st.markdown("##### Basic Metrics") + basic_info = analysis.get('basic_info', {}) + st.write(f"Status Code: {basic_info.get('status_code')}") + st.write(f"Content Type: {basic_info.get('content_type')}") + st.write(f"Title: {basic_info.get('title')}") + st.write(f"Meta Description: {basic_info.get('meta_description')}") + + # SSL Info + ssl_info = analysis.get('ssl_info', {}) + if ssl_info.get('has_ssl'): + st.success("SSL Certificate is valid") + st.write(f"Expiry: {ssl_info.get('expiry')}") + else: + st.error("No valid SSL certificate found") + + with tab2: + st.markdown("##### Content Analysis") + content_info = analysis.get('content_info', {}) + + # Content Overview + st.markdown("###### 📊 Content Overview") + col1, col2, col3, col4 = st.columns(4) + with col1: + st.metric("Word Count", content_info.get('word_count', 0)) + with col2: + st.metric("Headings", content_info.get('heading_count', 0)) + with col3: + st.metric("Images", content_info.get('image_count', 0)) + with col4: + st.metric("Links", content_info.get('link_count', 0)) + + if analyze_type == "Full Analysis with SEO": + with tab3: + st.markdown("##### SEO Analysis") + seo_data = data.get('seo_analysis', {}) + + # Display SEO Score + seo_score = seo_data.get('overall_score', 0) + st.markdown(f"### SEO Score: {seo_score}/100") + st.progress(seo_score / 100) + + # Meta Tags Analysis + st.markdown("#### Meta Tags Analysis") + meta_analysis = seo_data.get('meta_tags', {}) + for key, value in meta_analysis.items(): + if isinstance(value, bool): + st.write(f"{'✅' if value else '❌'} {key.replace('_', ' ').title()}") + elif isinstance(value, dict): + st.write(f"**{key.replace('_', ' ').title()}:**") + st.write(f"Status: {value.get('status', 'N/A')}") + st.write(f"Value: {value.get('value', 'N/A')}") + if value.get('recommendation'): + st.write(f"Recommendation: {value['recommendation']}") + else: + st.write(f"**{key.replace('_', ' ').title()}:** {value}") + + # Content Analysis + st.markdown("#### AI Content Analysis") + content_analysis = seo_data.get('content', {}) + st.write(f"**Word Count:** {content_analysis.get('word_count', 0)}") + st.write(f"**Readability Score:** {content_analysis.get('readability_score', 0)}/100") + st.write(f"**Content Quality Score:** {content_analysis.get('content_quality_score', 0)}/100") + + # Recommendations + st.markdown("#### SEO Recommendations") + recommendations = seo_data.get('recommendations', []) + for rec in recommendations: + st.write(f"**{rec.get('priority', '').upper()} Priority - {rec.get('category', '')}**") + st.write(f"Issue: {rec.get('issue', '')}") + st.write(f"Recommendation: {rec.get('recommendation', '')}") + st.write(f"Impact: {rec.get('impact', '')}") + st.write("---") + + with tab4: + st.markdown("##### Technical SEO") + technical_seo = seo_data.get('technical_analysis', {}) + + # Mobile Friendliness + st.markdown("#### Mobile Friendliness") + mobile_friendly = technical_seo.get('mobile_friendly', False) + st.write(f"{'✅' if mobile_friendly else '❌'} Mobile Friendly") + + # Page Speed + st.markdown("#### Page Speed") + speed_metrics = technical_seo.get('speed_metrics', {}) + for metric, value in speed_metrics.items(): + st.write(f"**{metric.replace('_', ' ').title()}:** {value}") + + # Technical Issues + st.markdown("#### Technical Issues") + issues = technical_seo.get('issues', []) + for issue in issues: + st.write(f"â€ĸ {issue}") + + with tab4 if analyze_type == "Basic Analysis" else tab5: + st.markdown("##### Strategy Recommendations") + strategy_info = analysis.get('strategy', {}) + + if strategy_info: + for category, recommendations in strategy_info.items(): + st.markdown(f"###### {category.replace('_', ' ').title()}") + for rec in recommendations: + st.write(f"â€ĸ {rec}") + else: + st.info("No strategy recommendations available") + else: + error_msg = results.get('error', 'Analysis failed') + logger.error(f"[render_website_setup] Displaying error: {error_msg}") + st.error(error_msg) + else: + logger.debug("[render_website_setup] No analysis results in session state") + st.info("Enter a URL and click 'Analyze Website' to see results") - 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 + # Navigation buttons + if render_navigation_buttons(2, 5, True): + # Move to next step (AI Research Setup) + st.session_state.current_step = 3 + st.session_state.next_step = "ai_research_setup" + st.rerun() + + return {"current_step": 2, "changes_made": True} \ No newline at end of file diff --git a/lib/utils/config_manager.py b/lib/utils/config_manager.py deleted file mode 100644 index 3000e555..00000000 --- a/lib/utils/config_manager.py +++ /dev/null @@ -1,13 +0,0 @@ -import json -import os -import streamlit as st - -def save_config(config): - """ - Saves the provided configuration dictionary to a JSON file specified by the environment variable. - """ - try: - with open(os.getenv("ALWRITY_CONFIG"), "w") as config_file: - json.dump(config, config_file, indent=4) - except Exception as e: - st.error(f"An error occurred while saving the configuration: {e}")