"""Component Logic API endpoints for ALwrity Backend. This module provides API endpoints for the extracted component logic services. """ from fastapi import APIRouter, HTTPException, Depends 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, ResearchPreferencesRequest, ResearchPreferencesResponse, ResearchRequest, ResearchResponse, ContentStyleRequest, ContentStyleResponse, BrandVoiceRequest, BrandVoiceResponse, PersonalizationSettingsRequest, PersonalizationSettingsResponse, ResearchTopicRequest, ResearchResultResponse, StyleAnalysisRequest, StyleAnalysisResponse, WebCrawlRequest, WebCrawlResponse, StyleDetectionRequest, StyleDetectionResponse ) from models.onboarding import OnboardingSession from services.component_logic.ai_research_logic import AIResearchLogic from services.component_logic.personalization_logic import PersonalizationLogic from services.component_logic.research_utilities import ResearchUtilities from services.component_logic.style_detection_logic import StyleDetectionLogic from services.component_logic.web_crawler_logic import WebCrawlerLogic from services.research_preferences_service import ResearchPreferencesService from services.database import get_db # Import authentication for user isolation from middleware.auth_middleware import get_current_user # Import the website analysis service from services.website_analysis_service import WebsiteAnalysisService from services.seo_tools.sitemap_service import SitemapService from services.database import get_db_session # Initialize services ai_research_logic = AIResearchLogic() personalization_logic = PersonalizationLogic() 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 def _get_onboarding_session(db_session: Session, user_id: str, create_if_missing: bool = False) -> Optional[OnboardingSession]: """Fetch onboarding session for a user, optionally creating one. Refactored to use direct DB access instead of legacy OnboardingDatabaseService. """ try: session = db_session.query(OnboardingSession).filter( OnboardingSession.user_id == user_id ).first() if not session and create_if_missing: logger.info(f"Creating new onboarding session for user {user_id}") session = OnboardingSession( user_id=user_id, current_step=1, progress=0.0, started_at=datetime.utcnow(), updated_at=datetime.utcnow() ) db_session.add(session) db_session.commit() db_session.refresh(session) return session except Exception as e: logger.error(f"Error getting/creating onboarding session: {e}") if create_if_missing: db_session.rollback() return None # AI Research Endpoints @router.post("/ai-research/validate-user", response_model=UserInfoResponse) async def validate_user_info(request: UserInfoRequest): """Validate user information for AI research configuration.""" try: logger.info("Validating user information via API") user_data = { 'full_name': request.full_name, 'email': request.email, 'company': request.company, 'role': request.role } result = ai_research_logic.validate_user_info(user_data) return UserInfoResponse( valid=result['valid'], user_info=result.get('user_info'), errors=result.get('errors', []) ) except Exception as e: logger.error(f"Error in validate_user_info: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/ai-research/configure-preferences", response_model=ResearchPreferencesResponse) async def configure_research_preferences( request: ResearchPreferencesRequest, db: Session = Depends(get_db), current_user: Dict[str, Any] = Depends(get_current_user) ): """Configure research preferences for AI research and save to database with user isolation.""" try: user_id = str(current_user.get('id')) logger.info(f"Configuring research preferences for user: {user_id}") # Validate preferences using business logic preferences = { 'research_depth': request.research_depth, 'content_types': request.content_types, 'auto_research': request.auto_research, 'factual_content': request.factual_content } result = ai_research_logic.configure_research_preferences(preferences) if result['valid']: try: # Save to database preferences_service = ResearchPreferencesService(db) session = _get_onboarding_session(db, user_id, create_if_missing=True) if not session: logger.warning(f"Could not resolve onboarding session for user {user_id}") else: # Save preferences with onboarding session ID preferences_id = preferences_service.save_preferences_with_style_data(session.id, preferences) if preferences_id: logger.info(f"Research preferences saved to database with ID: {preferences_id}") result['preferences']['id'] = preferences_id else: logger.warning("Failed to save research preferences to database") except Exception as db_error: logger.error(f"Database error: {db_error}") # Don't fail the request if database save fails, just log it result['preferences']['database_save_failed'] = True return ResearchPreferencesResponse( valid=result['valid'], preferences=result.get('preferences'), errors=result.get('errors', []) ) except Exception as e: logger.error(f"Error in configure_research_preferences: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/ai-research/process-research", response_model=ResearchResponse) async def process_research_request(request: ResearchRequest): """Process research request with configured preferences.""" try: logger.info("Processing research request via API") preferences = { 'research_depth': request.preferences.research_depth, 'content_types': request.preferences.content_types, 'auto_research': request.preferences.auto_research } result = ai_research_logic.process_research_request(request.topic, preferences) return ResearchResponse( success=result['success'], topic=result['topic'], results=result.get('results'), error=result.get('error') ) except Exception as e: logger.error(f"Error in process_research_request: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/ai-research/configuration-options") async def get_research_configuration_options(): """Get available configuration options for AI research.""" try: logger.info("Getting research configuration options via API") options = ai_research_logic.get_research_configuration_options() return { 'success': True, 'options': options } except Exception as e: logger.error(f"Error in get_research_configuration_options: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) # Personalization Endpoints @router.post("/personalization/validate-style", response_model=ContentStyleResponse) async def validate_content_style(request: ContentStyleRequest): """Validate content style configuration.""" try: logger.info("Validating content style via API") style_data = { 'writing_style': request.writing_style, 'tone': request.tone, 'content_length': request.content_length } result = personalization_logic.validate_content_style(style_data) return ContentStyleResponse( valid=result['valid'], style_config=result.get('style_config'), errors=result.get('errors', []) ) except Exception as e: logger.error(f"Error in validate_content_style: {str(e)}", exc_info=True) return ContentStyleResponse( valid=False, style_config=None, errors=[f"Internal error validating content style: {str(e)}"] ) @router.post("/personalization/configure-brand", response_model=BrandVoiceResponse) async def configure_brand_voice(request: BrandVoiceRequest): """Configure brand voice settings.""" try: logger.info("Configuring brand voice via API") brand_data = { 'personality_traits': request.personality_traits, 'voice_description': request.voice_description, 'keywords': request.keywords } result = personalization_logic.configure_brand_voice(brand_data) return BrandVoiceResponse( valid=result['valid'], brand_config=result.get('brand_config'), errors=result.get('errors', []) ) except Exception as e: logger.error(f"Error in configure_brand_voice: {str(e)}", exc_info=True) return BrandVoiceResponse( valid=False, brand_config=None, errors=[f"Internal error configuring brand voice: {str(e)}"] ) @router.post("/personalization/process-settings", response_model=PersonalizationSettingsResponse) async def process_personalization_settings(request: PersonalizationSettingsRequest): """Process complete personalization settings.""" try: logger.info("Processing personalization settings via API") settings = { 'content_style': { 'writing_style': request.content_style.writing_style, 'tone': request.content_style.tone, 'content_length': request.content_style.content_length }, 'brand_voice': { 'personality_traits': request.brand_voice.personality_traits, 'voice_description': request.brand_voice.voice_description, 'keywords': request.brand_voice.keywords }, 'advanced_settings': { 'seo_optimization': request.advanced_settings.seo_optimization, 'readability_level': request.advanced_settings.readability_level, 'content_structure': request.advanced_settings.content_structure } } result = personalization_logic.process_personalization_settings(settings) return PersonalizationSettingsResponse( valid=result['valid'], settings=result.get('settings'), errors=result.get('errors', []) ) except Exception as e: logger.error(f"Error in process_personalization_settings: {str(e)}", exc_info=True) return PersonalizationSettingsResponse( valid=False, settings=None, errors=[f"Internal error processing settings: {str(e)}"] ) @router.get("/personalization/configuration-options") async def get_personalization_configuration_options(): """Get available configuration options for personalization.""" try: logger.info("Getting personalization configuration options via API") options = personalization_logic.get_personalization_configuration_options() return { 'success': True, 'options': options } except Exception as e: logger.error(f"Error in get_personalization_configuration_options: {str(e)}", exc_info=True) # Fallback to default options to prevent 500 error return { 'success': False, 'options': { 'writing_styles': ["Professional", "Casual", "Technical", "Conversational", "Academic"], 'tones': ["Formal", "Semi-Formal", "Neutral", "Friendly", "Humorous"], 'content_lengths': ["Concise", "Standard", "Detailed", "Comprehensive"], 'personality_traits': ["Professional", "Innovative", "Friendly", "Trustworthy", "Creative", "Expert"], 'readability_levels': ["Simple", "Standard", "Advanced", "Expert"], 'content_structures': ["Introduction", "Key Points", "Examples", "Conclusion", "Call-to-Action"], 'seo_optimization_options': [True, False] }, 'message': f"Error loading options: {str(e)}" } @router.post("/personalization/generate-guidelines") async def generate_content_guidelines(settings: Dict[str, Any]): """Generate content guidelines from personalization settings.""" try: logger.info("Generating content guidelines via API") result = personalization_logic.generate_content_guidelines(settings) return result except Exception as e: logger.error(f"Error in generate_content_guidelines: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) # Research Utilities Endpoints @router.post("/research/process-topic", response_model=ResearchResultResponse) async def process_research_topic(request: ResearchTopicRequest): """Process research for a specific topic.""" try: logger.info("Processing research topic via API") result = await research_utilities.research_topic(request.topic, request.api_keys) return ResearchResultResponse( success=result['success'], topic=result['topic'], data=result.get('results'), error=result.get('error'), metadata=result.get('metadata') ) except Exception as e: logger.error(f"Error in process_research_topic: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/research/process-results") async def process_research_results(results: Dict[str, Any]): """Process and format research results.""" try: logger.info("Processing research results via API") result = research_utilities.process_research_results(results) return result except Exception as e: logger.error(f"Error in process_research_results: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/research/validate-request") async def validate_research_request(topic: str, api_keys: Dict[str, str]): """Validate a research request before processing.""" try: logger.info("Validating research request via API") result = research_utilities.validate_research_request(topic, api_keys) return result except Exception as e: logger.error(f"Error in validate_research_request: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/research/providers-info") async def get_research_providers_info(): """Get information about available research providers.""" try: logger.info("Getting research providers info via API") result = research_utilities.get_research_providers_info() return { 'success': True, 'providers_info': result } except Exception as e: logger.error(f"Error in get_research_providers_info: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/research/generate-report") async def generate_research_report(results: Dict[str, Any]): """Generate a formatted research report from processed results.""" try: logger.info("Generating research report via API") result = research_utilities.generate_research_report(results) return result except Exception as e: logger.error(f"Error in generate_research_report: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) # Style Detection Endpoints @router.post("/style-detection/analyze", response_model=StyleAnalysisResponse) async def analyze_content_style( request: StyleAnalysisRequest, current_user: Dict[str, Any] = Depends(get_current_user) ): """Analyze content style using AI.""" try: user_id = str(current_user.get('id')) logger.info(f"[analyze_content_style] Starting style analysis for user: {user_id}") # Initialize style detection logic style_logic = StyleDetectionLogic() # Validate request validation = style_logic.validate_style_analysis_request(request.dict()) if not validation['valid']: return StyleAnalysisResponse( success=False, error=f"Validation failed: {', '.join(validation['errors'])}", timestamp=datetime.now().isoformat() ) # Perform style analysis if request.analysis_type == "comprehensive": result = style_logic.analyze_content_style(validation['content'], user_id=user_id) elif request.analysis_type == "patterns": result = style_logic.analyze_style_patterns(validation['content'], user_id=user_id) else: return StyleAnalysisResponse( success=False, error="Invalid analysis type", timestamp=datetime.now().isoformat() ) if not result['success']: return StyleAnalysisResponse( success=False, error=result.get('error', 'Analysis failed'), timestamp=datetime.now().isoformat() ) # Return appropriate response based on analysis type if request.analysis_type == "comprehensive": return StyleAnalysisResponse( success=True, analysis=result['analysis'], timestamp=result['timestamp'] ) elif request.analysis_type == "patterns": return StyleAnalysisResponse( success=True, patterns=result['patterns'], timestamp=result['timestamp'] ) except Exception as e: logger.error(f"[analyze_content_style] Error: {str(e)}") return StyleAnalysisResponse( success=False, error=f"Analysis error: {str(e)}", timestamp=datetime.now().isoformat() ) @router.post("/style-detection/crawl", response_model=WebCrawlResponse) async def crawl_website_content(request: WebCrawlRequest): """Crawl website content for style analysis.""" try: logger.info("[crawl_website_content] Starting web crawl") # Initialize web crawler logic crawler_logic = WebCrawlerLogic() # Validate request validation = crawler_logic.validate_crawl_request(request.dict()) if not validation['valid']: return WebCrawlResponse( success=False, error=f"Validation failed: {', '.join(validation['errors'])}", timestamp=datetime.now().isoformat() ) # Perform crawling if validation['url']: # Crawl website result = await crawler_logic.crawl_website(validation['url']) else: # Process text sample result = crawler_logic.extract_content_from_text(validation['text_sample']) if not result['success']: return WebCrawlResponse( success=False, error=result.get('error', 'Crawling failed'), timestamp=datetime.now().isoformat() ) # Calculate metrics metrics = crawler_logic.get_crawl_metrics(result['content']) return WebCrawlResponse( success=True, content=result['content'], metrics=metrics.get('metrics') if metrics['success'] else None, timestamp=result['timestamp'] ) except Exception as e: logger.error(f"[crawl_website_content] Error: {str(e)}") return WebCrawlResponse( success=False, error=f"Crawling error: {str(e)}", timestamp=datetime.now().isoformat() ) @router.post("/style-detection/complete", response_model=StyleDetectionResponse) async def complete_style_detection( request: StyleDetectionRequest, current_user: Dict[str, Any] = Depends(get_current_user) ): """Complete style detection workflow (crawl + analyze + guidelines) with database storage and user isolation.""" try: user_id = str(current_user.get('id')) logger.info(f"[complete_style_detection] Starting complete style detection for user: {user_id}") # Get database session db_session = get_db_session(user_id) if not db_session: return StyleDetectionResponse( success=False, error="Database connection not available", timestamp=datetime.now().isoformat() ) # Initialize services crawler_logic = WebCrawlerLogic() style_logic = StyleDetectionLogic() analysis_service = WebsiteAnalysisService(db_session) sitemap_service = SitemapService() session = _get_onboarding_session(db_session, user_id, create_if_missing=True) if not session: return StyleDetectionResponse( success=False, error="Onboarding session not available", timestamp=datetime.now().isoformat() ) # Check for existing analysis if URL is provided existing_analysis = None if request.url: existing_analysis = analysis_service.check_existing_analysis(session.id, request.url) # Step 1: Crawl content if request.url: crawl_result = await crawler_logic.crawl_website(request.url) elif request.text_sample: crawl_result = crawler_logic.extract_content_from_text(request.text_sample) else: return StyleDetectionResponse( success=False, error="Either URL or text sample is required", timestamp=datetime.now().isoformat() ) if not crawl_result['success']: # Save error analysis analysis_service.save_error_analysis(session.id, request.url or "text_sample", crawl_result.get('error', 'Crawling failed')) return StyleDetectionResponse( success=False, error=f"Crawling failed: {crawl_result.get('error', 'Unknown error')}", timestamp=datetime.now().isoformat() ) # 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'], user_id=user_id)) 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'], user_id=user_id)) async def run_seo_audit(): """Run SEO audit in executor""" if not request.url: return None loop = asyncio.get_event_loop() return await loop.run_in_executor(None, partial(style_logic.perform_seo_audit, request.url, crawl_result['content'])) async def run_sitemap_analysis(): """Run AI sitemap analysis for home page""" if not request.url: return None try: # Discover sitemap URL sitemap_url = await sitemap_service.discover_sitemap_url(request.url) if sitemap_url: # Analyze sitemap with AI insights return await sitemap_service.analyze_sitemap( sitemap_url=sitemap_url, analyze_content_trends=True, analyze_publishing_patterns=True, include_ai_insights=True, user_id=user_id ) return None except Exception as e: logger.error(f"Sitemap analysis failed: {e}") return None # Execute style, patterns, SEO analysis and sitemap analysis in parallel style_analysis, patterns_result, seo_audit_result, sitemap_result = await asyncio.gather( run_style_analysis(), run_patterns_analysis(), run_seo_audit(), run_sitemap_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(session.id, 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'): 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( success=False, error="API keys not configured. Please complete step 1 of onboarding to configure your AI provider API keys.", timestamp=datetime.now().isoformat() ) else: analysis_service.save_error_analysis(session.id, request.url or "text_sample", error_msg) return StyleDetectionResponse( success=False, error=f"Style analysis failed: {error_msg}", timestamp=datetime.now().isoformat() ) # Process patterns result style_patterns = None if request.include_patterns and patterns_result and not isinstance(patterns_result, Exception): if patterns_result.get('success'): style_patterns = patterns_result.get('patterns') # Process SEO audit result seo_audit = None if seo_audit_result and not isinstance(seo_audit_result, Exception): seo_audit = seo_audit_result elif isinstance(seo_audit_result, Exception): logger.warning(f"SEO audit failed: {seo_audit_result}") # Process sitemap analysis result sitemap_analysis = None if sitemap_result and not isinstance(sitemap_result, Exception): sitemap_analysis = sitemap_result elif isinstance(sitemap_result, Exception): logger.warning(f"Sitemap analysis failed: {sitemap_result}") # Step 4: Generate guidelines (depends on style_analysis, must run after) style_guidelines = None if request.include_guidelines: loop = asyncio.get_event_loop() guidelines_result = await loop.run_in_executor( None, partial(style_logic.generate_style_guidelines, style_analysis.get('analysis', {}), user_id=user_id) ) if guidelines_result and guidelines_result.get('success'): style_guidelines = guidelines_result.get('guidelines') # Check if there's a warning about fallback data warning = None if style_analysis and 'warning' in style_analysis: warning = style_analysis['warning'] # Prepare response data response_data = { 'crawl_result': crawl_result, 'style_analysis': style_analysis.get('analysis') if style_analysis else None, 'style_patterns': style_patterns, 'style_guidelines': style_guidelines, 'seo_audit': seo_audit, 'sitemap_analysis': sitemap_analysis, 'warning': warning } # Save analysis to database if request.url: # Only save for URL-based analysis analysis_id = analysis_service.save_analysis(session.id, request.url, response_data) if analysis_id: response_data['analysis_id'] = analysis_id return StyleDetectionResponse( success=True, crawl_result=crawl_result, style_analysis=style_analysis.get('analysis') if style_analysis else None, style_patterns=style_patterns, style_guidelines=style_guidelines, seo_audit=seo_audit, sitemap_analysis=sitemap_analysis, warning=warning, timestamp=datetime.now().isoformat() ) except Exception as e: logger.error(f"[complete_style_detection] Error: {str(e)}") return StyleDetectionResponse( success=False, error=f"Style detection error: {str(e)}", timestamp=datetime.now().isoformat() ) @router.get("/style-detection/check-existing/{website_url:path}") async def check_existing_analysis( website_url: str, current_user: Dict[str, Any] = Depends(get_current_user) ): """Check if analysis exists for a website URL with user isolation.""" try: user_id = str(current_user.get('id')) logger.info(f"[check_existing_analysis] Checking for URL: {website_url} (user: {user_id})") # Get database session db_session = get_db_session(user_id) if not db_session: return {"error": "Database connection not available"} # Initialize service analysis_service = WebsiteAnalysisService(db_session) # Get onboarding session to ensure we check the correct session session = _get_onboarding_session(db_session, user_id) if not session: return {'exists': False} # Check for existing analysis for THIS USER'S SESSION existing_analysis = analysis_service.check_existing_analysis(session.id, website_url) return existing_analysis except Exception as e: logger.error(f"[check_existing_analysis] Error: {str(e)}") return {"error": f"Error checking existing analysis: {str(e)}"} @router.get("/style-detection/analysis/{analysis_id}") async def get_analysis_by_id( analysis_id: int, current_user: Dict[str, Any] = Depends(get_current_user) ): """Get analysis by ID.""" try: user_id = str(current_user.get('id')) logger.info(f"[get_analysis_by_id] Getting analysis: {analysis_id} (user: {user_id})") # Get database session db_session = get_db_session(user_id) if not db_session: return {"error": "Database connection not available"} # Initialize service analysis_service = WebsiteAnalysisService(db_session) # Get onboarding session to ensure ownership session = _get_onboarding_session(db_session, user_id) if not session: return {"success": False, "error": "Analysis not found"} # Get analysis analysis = analysis_service.get_analysis(analysis_id) # Verify ownership (session_id must match) if analysis and analysis.get('session_id') == session.id: return {"success": True, "analysis": analysis} else: return {"success": False, "error": "Analysis not found"} except Exception as e: logger.error(f"[get_analysis_by_id] Error: {str(e)}") return {"error": f"Error retrieving analysis: {str(e)}"} @router.get("/style-detection/session-analyses") async def get_session_analyses(current_user: Dict[str, Any] = Depends(get_current_user)): """Get all analyses for the current user with proper user isolation.""" try: user_id = str(current_user.get('id')) logger.info(f"[get_session_analyses] Getting analyses for user: {user_id})") # Get database session db_session = get_db_session(user_id) if not db_session: return {"error": "Database connection not available"} # Initialize service analysis_service = WebsiteAnalysisService(db_session) # Get onboarding session to ensure we fetch analyses for the correct session session = _get_onboarding_session(db_session, user_id) if not session: return {"success": True, "analyses": []} # Get analyses for THIS USER'S SESSION analyses = analysis_service.get_session_analyses(session.id) logger.info(f"[get_session_analyses] Found {len(analyses) if analyses else 0} analyses for user {user_id}") return {"success": True, "analyses": analyses} except Exception as e: logger.error(f"[get_session_analyses] Error: {str(e)}") return {"error": f"Error retrieving session analyses: {str(e)}"} @router.put("/style-detection/analysis/{analysis_id}") async def update_analysis( analysis_id: int, analysis_data: Dict[str, Any], current_user: Dict[str, Any] = Depends(get_current_user) ): """Update an existing analysis with edited content.""" try: user_id = str(current_user.get('id')) logger.info(f"[update_analysis] Updating analysis: {analysis_id} (user: {user_id})") # Get database session db_session = get_db_session(user_id) if not db_session: return {"error": "Database connection not available"} # Initialize service analysis_service = WebsiteAnalysisService(db_session) # Get onboarding session to ensure ownership session = _get_onboarding_session(db_session, user_id) if not session: return {"success": False, "error": "Analysis not found"} # Check ownership first analysis = analysis_service.get_analysis(analysis_id) if not analysis or analysis.get('session_id') != session.id: return {"success": False, "error": "Analysis not found"} # Update analysis # Reconstruct style_guidelines if individual fields are passed # The frontend flat structure: guidelines, best_practices, etc. # The DB structure: style_guidelines JSON if any(k in analysis_data for k in ['guidelines', 'best_practices', 'avoid_elements', 'content_strategy', 'ai_generation_tips', 'competitive_advantages', 'content_calendar_suggestions']): # Fetch existing style_guidelines to merge or create new existing_guidelines = analysis.get('style_guidelines') or {} mapping = { 'guidelines': 'guidelines', 'best_practices': 'best_practices', 'avoid_elements': 'avoid_elements', 'content_strategy': 'content_strategy', 'ai_generation_tips': 'ai_generation_tips', 'competitive_advantages': 'competitive_advantages', 'content_calendar_suggestions': 'content_calendar_suggestions' } for frontend_key, db_key in mapping.items(): if frontend_key in analysis_data: existing_guidelines[db_key] = analysis_data[frontend_key] analysis_data['style_guidelines'] = existing_guidelines success = analysis_service.update_analysis_content(analysis_id, analysis_data) if success: return {"success": True} else: return {"success": False, "error": "Failed to update analysis"} except Exception as e: logger.error(f"[update_analysis] Error: {str(e)}") return {"error": f"Error updating analysis: {str(e)}"} @router.delete("/style-detection/analysis/{analysis_id}") async def delete_analysis( analysis_id: int, current_user: Dict[str, Any] = Depends(get_current_user) ): """Delete an analysis.""" try: user_id = str(current_user.get('id')) logger.info(f"[delete_analysis] Deleting analysis: {analysis_id} (user: {user_id})") # Get database session db_session = get_db_session(user_id) if not db_session: return {"error": "Database connection not available"} # Initialize service analysis_service = WebsiteAnalysisService(db_session) # Get onboarding session to ensure ownership session = _get_onboarding_session(db_session, user_id) if not session: return {"success": False, "error": "Analysis not found"} # Check ownership first analysis = analysis_service.get_analysis(analysis_id) if not analysis or analysis.get('session_id') != session.id: return {"success": False, "error": "Analysis not found"} # Delete analysis success = analysis_service.delete_analysis(analysis_id) if success: return {"success": True} else: return {"success": False, "error": "Failed to delete analysis"} except Exception as e: logger.error(f"[delete_analysis] Error: {str(e)}") return {"error": f"Error deleting analysis: {str(e)}"} @router.get("/style-detection/configuration-options") async def get_style_detection_configuration(): """Get configuration options for style detection.""" try: return { "analysis_types": [ {"value": "comprehensive", "label": "Comprehensive Analysis", "description": "Full writing style analysis"}, {"value": "patterns", "label": "Pattern Analysis", "description": "Focus on writing patterns"} ], "content_sources": [ {"value": "url", "label": "Website URL", "description": "Analyze content from a website"}, {"value": "text", "label": "Text Sample", "description": "Analyze provided text content"} ], "limits": { "max_content_length": 10000, "min_content_length": 50, "max_urls_per_request": 1 }, "features": { "style_analysis": True, "pattern_analysis": True, "guidelines_generation": True, "metrics_calculation": True } } except Exception as e: logger.error(f"[get_style_detection_configuration] Error: {str(e)}") return {"error": f"Configuration error: {str(e)}"}