From 14dfb2e5c0031702321fe68657664b6a1c95eb7d Mon Sep 17 00:00:00 2001 From: ajaysi Date: Sat, 4 Oct 2025 13:24:41 +0530 Subject: [PATCH] ALwrity onboarding fixes --- ...ress_user_33Gz1FPI86VDXhRY8QN4ragRFGN.json | 178 +++++++- backend/api/component_logic.py | 98 ++-- backend/middleware/auth_middleware.py | 12 +- backend/services/website_analysis_service.py | 52 ++- .../CompetitorAnalysisStep.tsx | 233 ++++++++-- .../OnboardingWizard/WebsiteStep.tsx | 28 +- .../components/AnalysisResultsDisplay.tsx | 427 +++++++----------- .../ContentCharacteristicsSection.tsx | 171 +++++++ .../components/KeyInsightsGrid.tsx | 20 +- .../TargetAudienceAnalysisSection.tsx | 178 ++++++++ .../WebsiteStep/components/index.ts | 2 + .../WebsiteStep/utils/renderUtils.tsx | 154 +++++-- .../WebsiteStep/utils/websiteUtils.ts | 26 +- .../common/useOnboardingStyles.ts | 49 +- 14 files changed, 1182 insertions(+), 446 deletions(-) create mode 100644 frontend/src/components/OnboardingWizard/WebsiteStep/components/ContentCharacteristicsSection.tsx create mode 100644 frontend/src/components/OnboardingWizard/WebsiteStep/components/TargetAudienceAnalysisSection.tsx diff --git a/backend/.onboarding_progress_user_33Gz1FPI86VDXhRY8QN4ragRFGN.json b/backend/.onboarding_progress_user_33Gz1FPI86VDXhRY8QN4ragRFGN.json index 221ef948..e64949c4 100644 --- a/backend/.onboarding_progress_user_33Gz1FPI86VDXhRY8QN4ragRFGN.json +++ b/backend/.onboarding_progress_user_33Gz1FPI86VDXhRY8QN4ragRFGN.json @@ -5,7 +5,7 @@ "title": "AI LLM Providers", "description": "Configure AI language model providers", "status": "completed", - "completed_at": "2025-09-30T11:54:21.688932", + "completed_at": "2025-10-03T17:29:12.878656", "data": { "api_keys": { "gemini": "AIzaSyB6QrCiOBAzh8xLdmSumec2ysdHeyqyxgw", @@ -19,9 +19,175 @@ "step_number": 2, "title": "Website Analysis", "description": "Set up website analysis and crawling", - "status": "pending", - "completed_at": null, - "data": null, + "status": "completed", + "completed_at": "2025-10-03T17:42:17.953305", + "data": { + "website": "https://alwrity.com", + "domainName": "Alwrity.com", + "analysis": { + "writing_style": { + "tone": "Informative, enthusiastic", + "voice": "Helpful, direct", + "complexity": "Moderate", + "engagement_level": "High", + "brand_personality": "Innovative, friendly", + "formality_level": "Semi-formal", + "emotional_appeal": "Benefit-driven" + }, + "content_characteristics": { + "sentence_structure": "Varied", + "vocabulary_level": "Accessible", + "paragraph_organization": "Clear, concise", + "content_flow": "Logical, user-centric", + "readability_score": "Good", + "content_density": "Moderate", + "visual_elements_usage": "High" + }, + "target_audience": { + "demographics": [ + "Marketers", + "Bloggers", + "Content creators" + ], + "expertise_level": "Beginner to intermediate", + "industry_focus": "General, tech", + "geographic_focus": "", + "psychographic_profile": [ + "Tech-savvy", + "Value-conscious", + "Efficiency-seeking" + ], + "pain_points": [ + "Time-consuming content creation", + "Lack of SEO knowledge", + "Writer's block" + ], + "motivations": [ + "Efficiency", + "Increased website traffic", + "Content quality" + ] + }, + "content_type": { + "primary_type": "AI writing tool promotion", + "secondary_types": [ + "Blog posts", + "Marketing materials" + ], + "purpose": "Promote AI tools, generate leads", + "call_to_action": "Try Now!", + "conversion_focus": "Tool usage", + "educational_value": "Moderate" + }, + "recommended_settings": { + "writing_tone": "Enthusiastic, informative", + "target_audience": "Content creators, marketers", + "content_type": "AI tool promotion, blog", + "creativity_level": "High", + "geographic_location": "", + "industry_context": "AI, Content Creation", + "brand_alignment": "Strong" + }, + "guidelines": { + "tone_recommendations": [ + "Informative & Enthusiastic: Maintain a helpful and engaging tone. Example: 'Let's dive into how AI can revolutionize your content creation!'" + ], + "structure_guidelines": [ + "Clear & Concise: Use headings, subheadings, and bullet points for easy readability. Example: Break down complex topics into digestible sections.", + "Logical Flow: Organize content with a user-centric approach, starting with the problem and offering solutions. Example: Start with the pain points of content creation and then introduce your AI tools." + ], + "vocabulary_suggestions": [ + "Accessible Language: Avoid jargon; use clear and concise language. Example: Instead of 'leverage AI,' use 'use AI.'" + ], + "engagement_tips": [ + "Visuals: Incorporate images, screenshots, and videos to enhance understanding. Example: Include screenshots of your AI tools in action.", + "Benefit-Driven: Focus on the benefits for the user. Example: 'Save time and create high-quality content with our AI.'" + ], + "audience_considerations": [ + "Targeted Content: Address the needs of marketers, bloggers, and content creators. Example: Provide specific examples relevant to their workflows.", + "Address Pain Points: Acknowledge and solve common content creation challenges. Example: Offer solutions to writer's block and SEO optimization." + ], + "brand_alignment": [ + "Helpful & Innovative Voice: Maintain a helpful, innovative, and friendly brand voice. Example: Offer free resources and tutorials.", + "Open Source Focus: Highlight the open-source nature of the tools. Example: Mention the benefits of open-source for users.", + "Value Proposition: Clearly communicate the value of the tools. Example: 'Create fact-based, multilingual content efficiently.'" + ], + "seo_optimization": [ + "Keyword Research: Identify relevant keywords and incorporate them naturally. Example: Use keywords like 'AI content creation,' 'SEO optimization,' and 'free AI tools.'", + "Optimize Headings & Meta Descriptions: Use keywords in headings and create compelling meta descriptions. Example: Write a meta description that includes a clear call to action and keyword." + ], + "conversion_optimization": [ + "Clear CTAs: Include clear calls to action. Example: 'Try our free AI tool today!'", + "Focus on Benefits: Emphasize the value proposition. Example: 'Sign up to get instant access to AI-powered content creation.'" + ] + }, + "best_practices": [ + "Provide In-Depth Tutorials: Offer detailed guides and tutorials to showcase the tools' capabilities.", + "Showcase Diverse Use Cases: Demonstrate how the tools can be applied in various scenarios.", + "Build Community: Encourage user interaction and feedback.", + "Integrate with Platforms: Explore integrations with popular content platforms." + ], + "avoid_elements": [ + "Overly Technical Jargon: Steer clear of overly complex technical terms that may alienate the audience.", + "Misleading Claims: Avoid making exaggerated claims about AI capabilities.", + "Negative Brand Association: Do not use language that portrays the brand as anything other than helpful and accessible." + ], + "content_strategy": "Focus on creating informative, user-centric content that highlights the benefits of open-source AI tools for content creation, addressing the pain points of the target audience while providing practical solutions and SEO optimization.", + "ai_generation_tips": [ + "Fact-Checking: Always verify the information generated by AI tools.", + "Human Oversight: Review and refine AI-generated content for accuracy, clarity, and brand voice.", + "Experimentation: Test different prompts and inputs to optimize output." + ], + "competitive_advantages": [ + "Fact-Based Content: Emphasize the ability to generate fact-based content.", + "Open Source: Highlight the benefits of open-source for users (e.g., transparency, community support, customization).", + "Multilingual Support: Promote the multilingual capabilities of the tools." + ], + "content_calendar_suggestions": [ + "Tutorials: Create step-by-step guides on using the AI tools for different content types.", + "Use Case Studies: Showcase successful implementations of the tools.", + "SEO Optimization Guides: Provide tips and best practices for improving search engine rankings.", + "Industry News & Trends: Share insights on the latest developments in AI and content creation." + ], + "style_patterns": { + "patterns": { + "sentence_length": "short to medium", + "vocabulary_patterns": [ + "keywords related to AI and content generation", + "action-oriented verbs" + ], + "rhetorical_devices": [ + "repetition", + "call to action" + ], + "paragraph_structure": "varied, often short and focused", + "transition_phrases": [ + "Click to", + "and" + ] + }, + "style_consistency": "consistent in tone and purpose", + "unique_elements": [ + "focus on AI-powered content creation tools", + "integration with platforms like WordPress" + ], + "meta": { + "schema_version": "1.1", + "confidence": 0.8, + "notes": "The content is promotional and tool-focused.", + "uncertainty": { + "fields": [] + } + } + }, + "style_consistency": "consistent in tone and purpose", + "unique_elements": [ + "focus on AI-powered content creation tools", + "integration with platforms like WordPress" + ] + }, + "useAnalysisForGenAI": true + }, "validation_errors": [] }, { @@ -61,9 +227,9 @@ "validation_errors": [] } ], - "current_step": 2, + "current_step": 3, "started_at": "2025-09-29T17:22:14.375002", - "last_updated": "2025-09-30T11:54:21.688938", + "last_updated": "2025-10-03T17:42:17.953324", "is_completed": false, "completed_at": null } \ No newline at end of file diff --git a/backend/api/component_logic.py b/backend/api/component_logic.py index db9c1ff0..5bfe2a90 100644 --- a/backend/api/component_logic.py +++ b/backend/api/component_logic.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import Session from loguru import logger from typing import Dict, Any from datetime import datetime +import hashlib from models.component_logic import ( UserInfoRequest, UserInfoResponse, @@ -45,6 +46,23 @@ research_utilities = ResearchUtilities() # Create router router = APIRouter(prefix="/api/onboarding", tags=["component_logic"]) +# Utility function for consistent user ID to integer conversion +def clerk_user_id_to_int(user_id: str) -> int: + """ + Convert Clerk user ID to consistent integer for database session_id. + Uses SHA256 hashing for deterministic, consistent results across all requests. + + Args: + user_id: Clerk user ID (e.g., 'user_2qA6V8bFFnhPRGp8JYxP4YTJtHl') + + Returns: + int: Deterministic integer derived from user ID + """ + # Use SHA256 for consistent hashing (unlike Python's hash() which varies per process) + user_id_hash = hashlib.sha256(user_id.encode()).hexdigest() + # Take first 8 characters of hex and convert to int, mod to fit in INT range + return int(user_id_hash[:8], 16) % 2147483647 + # AI Research Endpoints @router.post("/ai-research/validate-user", response_model=UserInfoResponse) @@ -99,11 +117,8 @@ async def configure_research_preferences( preferences_service = ResearchPreferencesService(db) # Use authenticated Clerk user ID for proper user isolation - # Convert user_id to int if service expects it, or update service to accept string - try: - user_id_int = int(user_id.replace('user_', '').replace('-', '')[:8], 16) % 2147483647 - except: - user_id_int = hash(user_id) % 2147483647 + # Use consistent SHA256-based conversion + user_id_int = clerk_user_id_to_int(user_id) # Save preferences with user ID (not session_id) preferences_id = preferences_service.save_preferences_with_style_data(user_id_int, preferences) @@ -504,10 +519,8 @@ async def complete_style_detection( analysis_service = WebsiteAnalysisService(db_session) # Use authenticated Clerk user ID for proper user isolation - try: - user_id_int = int(user_id.replace('user_', '').replace('-', '')[:8], 16) % 2147483647 - except: - user_id_int = hash(user_id) % 2147483647 + # Use consistent SHA256-based conversion + user_id_int = clerk_user_id_to_int(user_id) # Check for existing analysis if URL is provided existing_analysis = None @@ -536,11 +549,44 @@ async def complete_style_detection( timestamp=datetime.now().isoformat() ) - # Step 2: Analyze style - style_analysis = style_logic.analyze_content_style(crawl_result['content']) + # Step 2-4: Parallelize AI API calls for performance (3 calls → 1 parallel batch) + import asyncio + from functools import partial + + # Prepare parallel tasks + logger.info("[complete_style_detection] Starting parallel AI analysis...") + + async def run_style_analysis(): + """Run style analysis in executor""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, partial(style_logic.analyze_content_style, crawl_result['content'])) + + async def run_patterns_analysis(): + """Run patterns analysis in executor (if requested)""" + if not request.include_patterns: + return None + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, partial(style_logic.analyze_style_patterns, crawl_result['content'])) + + # Execute style and patterns analysis in parallel + style_analysis, patterns_result = await asyncio.gather( + run_style_analysis(), + run_patterns_analysis(), + return_exceptions=True + ) + + # Check if style_analysis failed + if isinstance(style_analysis, Exception): + error_msg = str(style_analysis) + logger.error(f"Style analysis failed with exception: {error_msg}") + analysis_service.save_error_analysis(user_id_int, request.url or "text_sample", error_msg) + return StyleDetectionResponse( + success=False, + error=f"Style analysis failed: {error_msg}", + timestamp=datetime.now().isoformat() + ) if not style_analysis or not style_analysis.get('success'): - # Check if it's an API key issue error_msg = style_analysis.get('error', 'Unknown error') if style_analysis else 'Analysis failed' if 'API key' in error_msg or 'configure' in error_msg: return StyleDetectionResponse( @@ -549,7 +595,6 @@ async def complete_style_detection( timestamp=datetime.now().isoformat() ) else: - # Save error analysis analysis_service.save_error_analysis(user_id_int, request.url or "text_sample", error_msg) return StyleDetectionResponse( success=False, @@ -557,17 +602,20 @@ async def complete_style_detection( timestamp=datetime.now().isoformat() ) - # Step 3: Analyze patterns (optional) + # Process patterns result style_patterns = None - if request.include_patterns: - patterns_result = style_logic.analyze_style_patterns(crawl_result['content']) - if patterns_result and patterns_result.get('success'): + if request.include_patterns and patterns_result and not isinstance(patterns_result, Exception): + if patterns_result.get('success'): style_patterns = patterns_result.get('patterns') - # Step 4: Generate guidelines (optional) + # Step 4: Generate guidelines (depends on style_analysis, must run after) style_guidelines = None if request.include_guidelines: - guidelines_result = style_logic.generate_style_guidelines(style_analysis.get('analysis', {})) + loop = asyncio.get_event_loop() + guidelines_result = await loop.run_in_executor( + None, + partial(style_logic.generate_style_guidelines, style_analysis.get('analysis', {})) + ) if guidelines_result and guidelines_result.get('success'): style_guidelines = guidelines_result.get('guidelines') @@ -628,10 +676,8 @@ async def check_existing_analysis( analysis_service = WebsiteAnalysisService(db_session) # Use authenticated Clerk user ID for proper user isolation - try: - user_id_int = int(user_id.replace('user_', '').replace('-', '')[:8], 16) % 2147483647 - except: - user_id_int = hash(user_id) % 2147483647 + # Use consistent SHA256-based conversion + user_id_int = clerk_user_id_to_int(user_id) # Check for existing analysis for THIS USER ONLY existing_analysis = analysis_service.check_existing_analysis(user_id_int, website_url) @@ -684,10 +730,8 @@ async def get_session_analyses(current_user: Dict[str, Any] = Depends(get_curren analysis_service = WebsiteAnalysisService(db_session) # Use authenticated Clerk user ID for proper user isolation - try: - user_id_int = int(user_id.replace('user_', '').replace('-', '')[:8], 16) % 2147483647 - except: - user_id_int = hash(user_id) % 2147483647 + # Use consistent SHA256-based conversion + user_id_int = clerk_user_id_to_int(user_id) # Get analyses for THIS USER ONLY (not all users!) analyses = analysis_service.get_session_analyses(user_id_int) diff --git a/backend/middleware/auth_middleware.py b/backend/middleware/auth_middleware.py index f0f8d323..dac09a98 100644 --- a/backend/middleware/auth_middleware.py +++ b/backend/middleware/auth_middleware.py @@ -117,26 +117,24 @@ class ClerkAuthMiddleware: # Use cached PyJWKClient to avoid repeated JWKS fetches if jwks_url not in self._jwks_client_cache: logger.info(f"Creating new PyJWKClient for {jwks_url} with caching enabled") - # Create client with caching: cache_keys=True, max_cached_keys=16, cache_jwk_set_timeout=3600 (1 hour) + # Create client with caching enabled (cache_keys=True keeps keys in memory) self._jwks_client_cache[jwks_url] = PyJWKClient( jwks_url, cache_keys=True, - max_cached_keys=16, - cache_jwk_set_timeout=3600, # Cache JWKS for 1 hour - timeout=10 # 10 second timeout for JWKS fetch + max_cached_keys=16 ) jwks_client = self._jwks_client_cache[jwks_url] signing_key = jwks_client.get_signing_key_from_jwt(token) # Verify and decode the token with clock skew tolerance - # Add 60 seconds leeway to handle clock skew between client/server + # Add 300 seconds (5 minutes) leeway to handle clock skew and token refresh delays decoded_token = jwt.decode( token, signing_key.key, algorithms=["RS256"], options={"verify_signature": True, "verify_exp": True}, - leeway=60 # Allow 60 seconds clock skew + leeway=300 # Allow 5 minutes leeway for token refresh during navigation ) # Extract user information @@ -171,7 +169,7 @@ class ClerkAuthMiddleware: decoded_token = jwt.decode( token, options={"verify_signature": False}, - leeway=60 # Allow 60 seconds clock skew + leeway=300 # Allow 5 minutes leeway for token refresh ) # Extract user information from the token diff --git a/backend/services/website_analysis_service.py b/backend/services/website_analysis_service.py index 895b8a59..2c5f1b6d 100644 --- a/backend/services/website_analysis_service.py +++ b/backend/services/website_analysis_service.py @@ -41,11 +41,17 @@ class WebsiteAnalysisService: if existing_analysis: # Update existing analysis - existing_analysis.writing_style = analysis_data.get('style_analysis', {}).get('writing_style') - existing_analysis.content_characteristics = analysis_data.get('style_analysis', {}).get('content_characteristics') - existing_analysis.target_audience = analysis_data.get('style_analysis', {}).get('target_audience') - existing_analysis.content_type = analysis_data.get('style_analysis', {}).get('content_type') - existing_analysis.recommended_settings = analysis_data.get('style_analysis', {}).get('recommended_settings') + style_analysis = analysis_data.get('style_analysis', {}) + existing_analysis.writing_style = style_analysis.get('writing_style') + existing_analysis.content_characteristics = style_analysis.get('content_characteristics') + existing_analysis.target_audience = style_analysis.get('target_audience') + existing_analysis.content_type = style_analysis.get('content_type') + existing_analysis.recommended_settings = style_analysis.get('recommended_settings') + # Store brand_analysis and content_strategy_insights if model supports it + if hasattr(existing_analysis, 'brand_analysis'): + existing_analysis.brand_analysis = style_analysis.get('brand_analysis') + if hasattr(existing_analysis, 'content_strategy_insights'): + existing_analysis.content_strategy_insights = style_analysis.get('content_strategy_insights') existing_analysis.crawl_result = analysis_data.get('crawl_result') existing_analysis.style_patterns = analysis_data.get('style_patterns') existing_analysis.style_guidelines = analysis_data.get('style_guidelines') @@ -59,20 +65,28 @@ class WebsiteAnalysisService: return existing_analysis.id else: # Create new analysis - analysis = WebsiteAnalysis( - session_id=session_id, - website_url=website_url, - writing_style=analysis_data.get('style_analysis', {}).get('writing_style'), - content_characteristics=analysis_data.get('style_analysis', {}).get('content_characteristics'), - target_audience=analysis_data.get('style_analysis', {}).get('target_audience'), - content_type=analysis_data.get('style_analysis', {}).get('content_type'), - recommended_settings=analysis_data.get('style_analysis', {}).get('recommended_settings'), - crawl_result=analysis_data.get('crawl_result'), - style_patterns=analysis_data.get('style_patterns'), - style_guidelines=analysis_data.get('style_guidelines'), - status='completed', - warning_message=analysis_data.get('warning') - ) + style_analysis = analysis_data.get('style_analysis', {}) + analysis_args = { + 'session_id': session_id, + 'website_url': website_url, + 'writing_style': style_analysis.get('writing_style'), + 'content_characteristics': style_analysis.get('content_characteristics'), + 'target_audience': style_analysis.get('target_audience'), + 'content_type': style_analysis.get('content_type'), + 'recommended_settings': style_analysis.get('recommended_settings'), + 'crawl_result': analysis_data.get('crawl_result'), + 'style_patterns': analysis_data.get('style_patterns'), + 'style_guidelines': analysis_data.get('style_guidelines'), + 'status': 'completed', + 'warning_message': analysis_data.get('warning') + } + # Add brand_analysis and content_strategy_insights if model supports it + if hasattr(WebsiteAnalysis, 'brand_analysis'): + analysis_args['brand_analysis'] = style_analysis.get('brand_analysis') + if hasattr(WebsiteAnalysis, 'content_strategy_insights'): + analysis_args['content_strategy_insights'] = style_analysis.get('content_strategy_insights') + + analysis = WebsiteAnalysis(**analysis_args) self.db.add(analysis) self.db.commit() diff --git a/frontend/src/components/OnboardingWizard/CompetitorAnalysisStep.tsx b/frontend/src/components/OnboardingWizard/CompetitorAnalysisStep.tsx index a0f77181..5eeda133 100644 --- a/frontend/src/components/OnboardingWizard/CompetitorAnalysisStep.tsx +++ b/frontend/src/components/OnboardingWizard/CompetitorAnalysisStep.tsx @@ -82,13 +82,64 @@ const CompetitorAnalysisStep: React.FC = ({ const [showHighlightsModal, setShowHighlightsModal] = useState(false); const [selectedCompetitorHighlights, setSelectedCompetitorHighlights] = useState([]); const [selectedCompetitorTitle, setSelectedCompetitorTitle] = useState(''); + const [usingCachedData, setUsingCachedData] = useState(false); + + // Check for cached competitor analysis data + const loadCachedAnalysis = useCallback(() => { + try { + const cachedData = localStorage.getItem('competitor_analysis_data'); + const cachedUrl = localStorage.getItem('competitor_analysis_url'); + const cacheTimestamp = localStorage.getItem('competitor_analysis_timestamp'); + + // Get current URL for comparison + const finalUserUrl = userUrl || localStorage.getItem('website_url') || ''; + + if (cachedData && cachedUrl === finalUserUrl && cacheTimestamp) { + const cacheAge = Date.now() - parseInt(cacheTimestamp); + const cacheValidDuration = 24 * 60 * 60 * 1000; // 24 hours + + // Check if cache is still valid (less than 24 hours old) + if (cacheAge < cacheValidDuration) { + const parsedData = JSON.parse(cachedData); + + console.log('CompetitorAnalysisStep: Loading cached competitor analysis:', { + url: cachedUrl, + cacheAge: Math.round(cacheAge / (60 * 1000)), + competitors: parsedData.competitors?.length || 0 + }); + + setCompetitors(parsedData.competitors || []); + setSocialMediaAccounts(parsedData.social_media_accounts || {}); + setSocialMediaCitations(parsedData.social_media_citations || []); + setResearchSummary(parsedData.research_summary || null); + setUsingCachedData(true); + + return true; // Successfully loaded from cache + } else { + console.log('CompetitorAnalysisStep: Cache expired, will run fresh analysis'); + } + } + + return false; // No valid cache found + } catch (err) { + console.error('Error loading cached analysis:', err); + return false; + } + }, [userUrl]); + + const startCompetitorDiscovery = useCallback(async (force = false) => { + // Check cache first unless forced + if (!force && loadCachedAnalysis()) { + console.log('CompetitorAnalysisStep: Using cached competitor analysis'); + return; + } - const startCompetitorDiscovery = useCallback(async () => { setIsAnalyzing(true); setShowProgressModal(true); setError(null); setAnalysisProgress(0); setAnalysisStep('Initializing competitor discovery...'); + setUsingCachedData(false); try { setAnalysisStep('Validating session...'); @@ -139,10 +190,28 @@ const CompetitorAnalysisStep: React.FC = ({ setAnalysisProgress(100); await new Promise(resolve => setTimeout(resolve, 500)); - setCompetitors(result.competitors || []); - setSocialMediaAccounts(result.social_media_accounts || {}); - setSocialMediaCitations(result.social_media_citations || []); - setResearchSummary(result.research_summary || null); + const analysisData = { + competitors: result.competitors || [], + social_media_accounts: result.social_media_accounts || {}, + social_media_citations: result.social_media_citations || [], + research_summary: result.research_summary || null + }; + + setCompetitors(analysisData.competitors); + setSocialMediaAccounts(analysisData.social_media_accounts); + setSocialMediaCitations(analysisData.social_media_citations); + setResearchSummary(analysisData.research_summary); + + // Cache the analysis results + try { + localStorage.setItem('competitor_analysis_data', JSON.stringify(analysisData)); + localStorage.setItem('competitor_analysis_url', finalUserUrl); + localStorage.setItem('competitor_analysis_timestamp', Date.now().toString()); + console.log('CompetitorAnalysisStep: Cached competitor analysis for future use'); + } catch (cacheErr) { + console.warn('Failed to cache competitor analysis:', cacheErr); + } + setShowProgressModal(false); setIsAnalyzing(false); } else { @@ -154,11 +223,23 @@ const CompetitorAnalysisStep: React.FC = ({ setIsAnalyzing(false); setShowProgressModal(false); } - }, [userUrl, industryContext]); // sessionId removed from dependencies + }, [userUrl, industryContext, loadCachedAnalysis]); // sessionId removed from dependencies + // Initialize: Check cache first, then run analysis if needed useEffect(() => { - startCompetitorDiscovery(); - }, [startCompetitorDiscovery]); + const initialize = async () => { + // Try to load from cache first + const cacheLoaded = loadCachedAnalysis(); + + // If no cache found, run fresh analysis + if (!cacheLoaded) { + await startCompetitorDiscovery(false); + } + }; + + initialize(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Run only once on mount const handleContinue = () => { const researchData = { @@ -180,20 +261,58 @@ const CompetitorAnalysisStep: React.FC = ({ return ( - + Research Your Competition - + Discover your competitors and analyze their strategies to gain competitive advantage + {usingCachedData && !error && ( + + Loaded previously analyzed competitor data. + + + )} + {error && ( {error}