Recovered state: integrated TrendSurferAgent, restored frontend/backend files, and cleaned up recovery scripts

This commit is contained in:
ajaysi
2026-02-08 13:56:57 +05:30
parent 1db10ccd0f
commit e404a86502
333 changed files with 42223 additions and 10875 deletions

View File

@@ -31,13 +31,13 @@ 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
from services.onboarding import OnboardingDatabaseService
# 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
@@ -67,12 +67,33 @@ def clerk_user_id_to_int(user_id: str) -> int:
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."""
db_service = OnboardingDatabaseService(db_session)
session = db_service.get_session_by_user(user_id, db_session)
if not session and create_if_missing:
session = db_service.get_or_create_session(user_id, db_session)
return session
"""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
@@ -218,8 +239,12 @@ async def validate_content_style(request: ContentStyleRequest):
)
except Exception as e:
logger.error(f"Error in validate_content_style: {str(e)}")
raise HTTPException(status_code=500, detail=str(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):
@@ -242,8 +267,12 @@ async def configure_brand_voice(request: BrandVoiceRequest):
)
except Exception as e:
logger.error(f"Error in configure_brand_voice: {str(e)}")
raise HTTPException(status_code=500, detail=str(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):
@@ -278,8 +307,12 @@ async def process_personalization_settings(request: PersonalizationSettingsReque
)
except Exception as e:
logger.error(f"Error in process_personalization_settings: {str(e)}")
raise HTTPException(status_code=500, detail=str(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():
@@ -295,8 +328,21 @@ async def get_personalization_configuration_options():
}
except Exception as e:
logger.error(f"Error in get_personalization_configuration_options: {str(e)}")
raise HTTPException(status_code=500, detail=str(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]):
@@ -395,10 +441,14 @@ async def generate_research_report(results: Dict[str, Any]):
# Style Detection Endpoints
@router.post("/style-detection/analyze", response_model=StyleAnalysisResponse)
async def analyze_content_style(request: StyleAnalysisRequest):
async def analyze_content_style(
request: StyleAnalysisRequest,
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Analyze content style using AI."""
try:
logger.info("[analyze_content_style] Starting style analysis")
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()
@@ -414,9 +464,9 @@ async def analyze_content_style(request: StyleAnalysisRequest):
# Perform style analysis
if request.analysis_type == "comprehensive":
result = style_logic.analyze_content_style(validation['content'])
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'])
result = style_logic.analyze_style_patterns(validation['content'], user_id=user_id)
else:
return StyleAnalysisResponse(
success=False,
@@ -515,7 +565,7 @@ async def complete_style_detection(
logger.info(f"[complete_style_detection] Starting complete style detection for user: {user_id}")
# Get database session
db_session = get_db_session()
db_session = get_db_session(user_id)
if not db_session:
return StyleDetectionResponse(
success=False,
@@ -527,6 +577,7 @@ async def complete_style_detection(
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:
@@ -573,19 +624,49 @@ async def complete_style_detection(
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']))
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']))
return await loop.run_in_executor(None, partial(style_logic.analyze_style_patterns, crawl_result['content'], user_id=user_id))
# Execute style and patterns analysis in parallel
style_analysis, patterns_result = await asyncio.gather(
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
)
@@ -622,13 +703,27 @@ async def complete_style_detection(
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', {}))
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')
@@ -644,6 +739,8 @@ async def complete_style_detection(
'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
}
@@ -659,6 +756,8 @@ async def complete_style_detection(
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()
)
@@ -682,19 +781,20 @@ async def check_existing_analysis(
logger.info(f"[check_existing_analysis] Checking for URL: {website_url} (user: {user_id})")
# Get database session
db_session = get_db_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)
# Use authenticated Clerk user ID for proper user isolation
# Use consistent SHA256-based conversion
user_id_int = clerk_user_id_to_int(user_id)
# 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 ONLY
existing_analysis = analysis_service.check_existing_analysis(user_id_int, website_url)
# Check for existing analysis for THIS USER'S SESSION
existing_analysis = analysis_service.check_existing_analysis(session.id, website_url)
return existing_analysis
@@ -703,23 +803,33 @@ async def check_existing_analysis(
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):
async def get_analysis_by_id(
analysis_id: int,
current_user: Dict[str, Any] = Depends(get_current_user)
):
"""Get analysis by ID."""
try:
logger.info(f"[get_analysis_by_id] Getting analysis: {analysis_id}")
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()
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)
if analysis:
# 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"}
@@ -733,22 +843,23 @@ async def get_session_analyses(current_user: Dict[str, Any] = Depends(get_curren
"""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}")
logger.info(f"[get_session_analyses] Getting analyses for user: {user_id})")
# Get database session
db_session = get_db_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)
# Use authenticated Clerk user ID for proper user isolation
# Use consistent SHA256-based conversion
user_id_int = clerk_user_id_to_int(user_id)
# 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 ONLY (not all users!)
analyses = analysis_service.get_session_analyses(user_id_int)
# 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}
@@ -757,28 +868,107 @@ async def get_session_analyses(current_user: Dict[str, Any] = Depends(get_curren
logger.error(f"[get_session_analyses] Error: {str(e)}")
return {"error": f"Error retrieving session analyses: {str(e)}"}
@router.delete("/style-detection/analysis/{analysis_id}")
async def delete_analysis(analysis_id: int):
"""Delete an analysis."""
@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:
logger.info(f"[delete_analysis] Deleting analysis: {analysis_id}")
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()
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, "message": "Analysis deleted successfully"}
return {"success": True}
else:
return {"success": False, "error": "Analysis not found or could not be deleted"}
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)}"}