From 5866f4932545d84a3c7d0e9ac54d7088db703900 Mon Sep 17 00:00:00 2001 From: ajaysi Date: Sun, 26 Oct 2025 10:06:24 +0530 Subject: [PATCH] LinkedIn and Facebook Persona Services Implementation --- backend/api/persona.py | 337 ++++++++++-------- backend/api/persona_routes.py | 46 ++- backend/models/persona_models.py | 2 +- .../facebook/facebook_persona_service.py | 25 +- backend/services/persona_analysis_service.py | 10 +- backend/services/persona_data_service.py | 252 +++++++++++++ frontend/src/api/persona.ts | 62 +++- .../FacebookWriter/FacebookWriter.tsx | 125 ++++++- .../LinkedInWriter/LinkedInWriter.tsx | 19 +- .../components/CopilotActions.tsx | 6 +- .../LinkedInWriter/components/Header.tsx | 31 +- .../LinkedInWriter/components/index.ts | 2 +- .../MainContentPreviewHeader.tsx | 1 - .../PersonaChip.tsx | 87 +++-- .../PlatformPersonaProvider.tsx | 144 ++++++-- 15 files changed, 868 insertions(+), 281 deletions(-) create mode 100644 backend/services/persona_data_service.py diff --git a/backend/api/persona.py b/backend/api/persona.py index fb86ce66..867d8096 100644 --- a/backend/api/persona.py +++ b/backend/api/persona.py @@ -8,6 +8,7 @@ from pydantic import BaseModel, Field from typing import Dict, Any, List, Optional from datetime import datetime from loguru import logger +from sqlalchemy.orm import Session from services.persona_analysis_service import PersonaAnalysisService from services.database import get_db @@ -110,50 +111,45 @@ async def generate_persona(user_id: int, request: PersonaGenerationRequest): logger.error(f"Error generating persona: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to generate persona: {str(e)}") -async def get_user_personas(user_id: int): - """Get all personas for a user.""" +async def get_user_personas(user_id: str): + """Get all personas for a user using PersonaData.""" try: - persona_service = get_persona_service() - personas = persona_service.get_user_personas(user_id) + from services.persona_data_service import PersonaDataService + + persona_service = PersonaDataService() + all_personas = persona_service.get_all_platform_personas(user_id) return { - "personas": personas, - "total_count": len(personas) + "personas": all_personas, + "total_count": len(all_personas), + "platforms": list(all_personas.keys()) } except Exception as e: logger.error(f"Error getting user personas: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to get personas: {str(e)}") -async def get_persona_details(user_id: int, persona_id: int): - """Get detailed information about a specific persona.""" +async def get_persona_details(user_id: str, persona_id: int): + """Get detailed information about a specific persona using PersonaData.""" try: - from services.database import get_db_session - from models.persona_models import WritingPersona, PlatformPersona + from services.persona_data_service import PersonaDataService - session = get_db_session() + persona_service = PersonaDataService() + persona_data = persona_service.get_user_persona_data(user_id) - # Get persona - persona = session.query(WritingPersona).filter( - WritingPersona.id == persona_id, - WritingPersona.user_id == user_id, - WritingPersona.is_active == True - ).first() - - if not persona: + if not persona_data: raise HTTPException(status_code=404, detail="Persona not found") - # Get platform adaptations - platform_personas = session.query(PlatformPersona).filter( - PlatformPersona.writing_persona_id == persona_id, - PlatformPersona.is_active == True - ).all() - - result = persona.to_dict() - result["platform_adaptations"] = [pp.to_dict() for pp in platform_personas] - - session.close() - return result + # Return the complete persona data with all platforms + return { + "persona_id": persona_data.get('id'), + "core_persona": persona_data.get('core_persona', {}), + "platform_personas": persona_data.get('platform_personas', {}), + "quality_metrics": persona_data.get('quality_metrics', {}), + "selected_platforms": persona_data.get('selected_platforms', []), + "created_at": persona_data.get('created_at'), + "updated_at": persona_data.get('updated_at') + } except HTTPException: raise @@ -161,11 +157,13 @@ async def get_persona_details(user_id: int, persona_id: int): logger.error(f"Error getting persona details: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to get persona details: {str(e)}") -async def get_platform_persona(user_id: int, platform: str): - """Get persona adaptation for a specific platform.""" +async def get_platform_persona(user_id: str, platform: str): + """Get persona adaptation for a specific platform using PersonaData.""" try: - persona_service = get_persona_service() - platform_persona = persona_service.get_persona_for_platform(user_id, platform) + from services.persona_data_service import PersonaDataService + + persona_service = PersonaDataService() + platform_persona = persona_service.get_platform_persona(user_id, platform) if not platform_persona: raise HTTPException(status_code=404, detail=f"No persona found for platform {platform}") @@ -178,41 +176,52 @@ async def get_platform_persona(user_id: int, platform: str): logger.error(f"Error getting platform persona: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to get platform persona: {str(e)}") -async def update_persona(user_id: int, persona_id: int, update_data: Dict[str, Any]): - """Update an existing persona.""" +async def get_persona_summary(user_id: str): + """Get persona summary for a user using PersonaData.""" try: - from services.database import get_db_session - from models.persona_models import WritingPersona + from services.persona_data_service import PersonaDataService - session = get_db_session() + persona_service = PersonaDataService() + summary = persona_service.get_persona_summary(user_id) - persona = session.query(WritingPersona).filter( - WritingPersona.id == persona_id, - WritingPersona.user_id == user_id - ).first() + return summary - if not persona: - raise HTTPException(status_code=404, detail="Persona not found") + except Exception as e: + logger.error(f"Error getting persona summary: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to get persona summary: {str(e)}") + +async def update_persona(user_id: str, persona_id: int, update_data: Dict[str, Any]): + """Update an existing persona using PersonaData.""" + try: + from services.persona_data_service import PersonaDataService + from models.onboarding import PersonaData - # Update allowed fields - updatable_fields = [ - 'persona_name', 'archetype', 'core_belief', 'brand_voice_description', - 'linguistic_fingerprint', 'platform_adaptations' - ] + persona_service = PersonaDataService() - for field in updatable_fields: - if field in update_data: - setattr(persona, field, update_data[field]) - - persona.updated_at = datetime.utcnow() - session.commit() - session.close() - - return { - "message": "Persona updated successfully", - "persona_id": persona_id, - "updated_at": persona.updated_at.isoformat() - } + # For PersonaData, we update the core_persona field + if 'core_persona' in update_data: + # Get current persona data + persona_data = persona_service.get_user_persona_data(user_id) + if not persona_data: + raise HTTPException(status_code=404, detail="Persona not found") + + # Update core persona with new data + persona_service.db.query(PersonaData).filter( + PersonaData.id == persona_data.get('id') + ).update({ + 'core_persona': update_data['core_persona'], + 'updated_at': datetime.utcnow() + }) + persona_service.db.commit() + persona_service.db.close() + + return { + "message": "Persona updated successfully", + "persona_id": persona_data.get('id'), + "updated_at": datetime.utcnow().isoformat() + } + else: + raise HTTPException(status_code=400, detail="core_persona field is required for updates") except HTTPException: raise @@ -220,40 +229,28 @@ async def update_persona(user_id: int, persona_id: int, update_data: Dict[str, A logger.error(f"Error updating persona: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to update persona: {str(e)}") -async def delete_persona(user_id: int, persona_id: int): - """Delete a persona (soft delete by setting is_active=False).""" +async def delete_persona(user_id: str, persona_id: int): + """Delete a persona using PersonaData (not recommended, personas are generated during onboarding).""" try: - from services.database import get_db_session - from models.persona_models import WritingPersona, PlatformPersona + from services.persona_data_service import PersonaDataService + from models.onboarding import PersonaData - session = get_db_session() + persona_service = PersonaDataService() - persona = session.query(WritingPersona).filter( - WritingPersona.id == persona_id, - WritingPersona.user_id == user_id - ).first() - - if not persona: + # Get persona data + persona_data = persona_service.get_user_persona_data(user_id) + if not persona_data: raise HTTPException(status_code=404, detail="Persona not found") - # Soft delete persona and platform adaptations - persona.is_active = False - persona.updated_at = datetime.utcnow() - - platform_personas = session.query(PlatformPersona).filter( - PlatformPersona.writing_persona_id == persona_id - ).all() - - for pp in platform_personas: - pp.is_active = False - pp.updated_at = datetime.utcnow() - - session.commit() - session.close() + # For PersonaData, we mark it as deleted by setting a flag + # Note: In production, you might want to add a deleted_at field or similar + # For now, we'll just return a warning that deletion is not recommended + logger.warning(f"Delete persona requested for user {user_id}. PersonaData deletion is not recommended.") return { - "message": "Persona deleted successfully", - "persona_id": persona_id + "message": "Persona deletion requested. Note: Personas are generated during onboarding and deletion is not recommended.", + "persona_id": persona_data.get('id'), + "alternative": "Consider re-running onboarding to regenerate persona if needed." } except HTTPException: @@ -262,67 +259,24 @@ async def delete_persona(user_id: int, persona_id: int): logger.error(f"Error deleting persona: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to delete persona: {str(e)}") -async def update_platform_persona(user_id: int, platform: str, update_data: Dict[str, Any]): - """Update platform-specific persona fields for a user's persona. - - This updates the underlying PlatformPersona row for the given platform. - """ +async def update_platform_persona(user_id: str, platform: str, update_data: Dict[str, Any]): + """Update platform-specific persona fields using PersonaData.""" try: - from services.database import get_db_session - from models.persona_models import WritingPersona, PlatformPersona + from services.persona_data_service import PersonaDataService - session = get_db_session() - - # Find the user's active core persona id - core_persona = session.query(WritingPersona).filter( - WritingPersona.user_id == user_id, - WritingPersona.is_active == True - ).order_by(WritingPersona.created_at.desc()).first() - - if not core_persona: - raise HTTPException(status_code=404, detail="No active persona found for user") - - # Find the platform persona for the requested platform - platform_persona = session.query(PlatformPersona).filter( - PlatformPersona.writing_persona_id == core_persona.id, - PlatformPersona.platform_type.ilike(platform), - PlatformPersona.is_active == True - ).first() - - if not platform_persona: + persona_service = PersonaDataService() + + # Update platform-specific persona data + success = persona_service.update_platform_persona(user_id, platform, update_data) + + if not success: raise HTTPException(status_code=404, detail=f"No platform persona found for platform {platform}") - # Update allowed platform fields - updatable_fields = [ - 'sentence_metrics', 'lexical_features', 'rhetorical_devices', 'tonal_range', - 'stylistic_constraints', 'content_format_rules', 'engagement_patterns', - 'posting_frequency', 'content_types', 'platform_best_practices', 'algorithm_considerations' - ] - - updated_any = False - for field in updatable_fields: - if field in update_data: - setattr(platform_persona, field, update_data[field]) - updated_any = True - - if not updated_any: - # Nothing to update - session.close() - return { - "message": "No updatable fields provided", - "platform": platform_persona.platform_type, - "persona_id": core_persona.id - } - - platform_persona.updated_at = datetime.utcnow() - session.commit() - session.close() - return { "message": "Platform persona updated successfully", "platform": platform, - "persona_id": core_persona.id, - "updated_at": platform_persona.updated_at.isoformat() + "user_id": user_id, + "updated_at": datetime.utcnow().isoformat() } except HTTPException: @@ -331,6 +285,101 @@ async def update_platform_persona(user_id: int, platform: str, update_data: Dict logger.error(f"Error updating platform persona: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to update platform persona: {str(e)}") +async def generate_platform_persona(user_id: str, platform: str, db_session): + """ + Generate a platform-specific persona from core persona and save it. + + Args: + user_id: User ID from auth + platform: Platform name (facebook, linkedin, etc.) + db_session: Database session from FastAPI dependency injection + + Returns: + Generated platform persona with validation results + """ + try: + logger.info(f"Generating {platform} persona for user {user_id}") + + # Import services + from services.persona_data_service import PersonaDataService + from services.onboarding_database_service import OnboardingDatabaseService + + persona_data_service = PersonaDataService(db_session=db_session) + onboarding_service = OnboardingDatabaseService(db=db_session) + + # Get core persona data + persona_data = persona_data_service.get_user_persona_data(user_id) + if not persona_data: + raise HTTPException(status_code=404, detail="Core persona not found") + + core_persona = persona_data.get('core_persona', {}) + if not core_persona: + raise HTTPException(status_code=404, detail="Core persona data is empty") + + # Get onboarding data for context + onboarding_session = onboarding_service.get_session_by_user(user_id) + if not onboarding_session: + raise HTTPException(status_code=404, detail="Onboarding session not found") + + # Get website analysis for context + website_analysis = onboarding_service.get_website_analysis(user_id) + research_prefs = onboarding_service.get_research_preferences(user_id) + + onboarding_data = { + "website_url": website_analysis.get('website_url', '') if website_analysis else '', + "writing_style": website_analysis.get('writing_style', {}) if website_analysis else {}, + "content_characteristics": website_analysis.get('content_characteristics', {}) if website_analysis else {}, + "target_audience": website_analysis.get('target_audience', '') if website_analysis else '', + "research_preferences": research_prefs or {} + } + + # Generate platform persona based on platform + generated_persona = None + platform_service = None + + if platform.lower() == 'facebook': + from services.persona.facebook.facebook_persona_service import FacebookPersonaService + platform_service = FacebookPersonaService() + generated_persona = platform_service.generate_facebook_persona( + core_persona, + onboarding_data + ) + elif platform.lower() == 'linkedin': + from services.persona.linkedin.linkedin_persona_service import LinkedInPersonaService + platform_service = LinkedInPersonaService() + generated_persona = platform_service.generate_linkedin_persona( + core_persona, + onboarding_data + ) + else: + raise HTTPException(status_code=400, detail=f"Unsupported platform: {platform}") + + # Check for errors in generation + if "error" in generated_persona: + raise HTTPException(status_code=500, detail=generated_persona["error"]) + + # Save the generated platform persona to database + success = persona_data_service.save_platform_persona(user_id, platform, generated_persona) + + if not success: + raise HTTPException(status_code=500, detail=f"Failed to save {platform} persona") + + logger.info(f"✅ Successfully generated and saved {platform} persona for user {user_id}") + + return { + "success": True, + "platform": platform, + "persona": generated_persona, + "validation_results": generated_persona.get("validation_results", {}), + "quality_score": generated_persona.get("validation_results", {}).get("quality_score", 0) + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error generating {platform} persona: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to generate {platform} persona: {str(e)}") + async def validate_persona_generation_readiness(user_id: int): """Check if user has sufficient onboarding data for persona generation.""" try: diff --git a/backend/api/persona_routes.py b/backend/api/persona_routes.py index 18451df2..8f09b58d 100644 --- a/backend/api/persona_routes.py +++ b/backend/api/persona_routes.py @@ -3,14 +3,18 @@ FastAPI routes for persona management. Integrates persona generation and management into the main API. """ -from fastapi import APIRouter, HTTPException, Query +from fastapi import APIRouter, HTTPException, Query, Depends from typing import Dict, Any, Optional +from sqlalchemy.orm import Session +from middleware.auth_middleware import get_current_user +from services.database import get_db from api.persona import ( generate_persona, get_user_personas, get_persona_details, get_platform_persona, + get_persona_summary, update_persona, delete_persona, validate_persona_generation_readiness, @@ -32,7 +36,7 @@ from api.persona import ( ) from services.persona_replication_engine import PersonaReplicationEngine -from api.persona import update_platform_persona +from api.persona import update_platform_persona, generate_platform_persona # Create router router = APIRouter(prefix="/api/personas", tags=["personas"]) @@ -45,29 +49,45 @@ async def generate_persona_endpoint( """Generate a new writing persona from onboarding data.""" return await generate_persona(user_id, request) -@router.get("/user/{user_id}") -async def get_user_personas_endpoint(user_id: int): - """Get all personas for a user.""" - # Beta testing: Force user_id=1 for all requests - return await get_user_personas(1) +@router.get("/user") +async def get_user_personas_endpoint(current_user: Dict[str, Any] = Depends(get_current_user)): + """Get all personas for the current user.""" + user_id = str(current_user.get('id')) + return await get_user_personas(user_id) + +@router.get("/summary") +async def get_persona_summary_endpoint(current_user: Dict[str, Any] = Depends(get_current_user)): + """Get persona summary for the current user.""" + user_id = str(current_user.get('id')) + return await get_persona_summary(user_id) @router.get("/{persona_id}") async def get_persona_details_endpoint( persona_id: int, - user_id: int = Query(..., description="User ID") + current_user: Dict[str, Any] = Depends(get_current_user) ): """Get detailed information about a specific persona.""" - # Beta testing: Force user_id=1 for all requests - return await get_persona_details(1, persona_id) + user_id = str(current_user.get('id')) + return await get_persona_details(user_id, persona_id) @router.get("/platform/{platform}") async def get_platform_persona_endpoint( platform: str, - user_id: int = Query(1, description="User ID") + current_user: Dict[str, Any] = Depends(get_current_user) ): """Get persona adaptation for a specific platform.""" - # Beta testing: Force user_id=1 for all requests - return await get_platform_persona(1, platform) + user_id = str(current_user.get('id')) + return await get_platform_persona(user_id, platform) + +@router.post("/generate-platform/{platform}") +async def generate_platform_persona_endpoint( + platform: str, + current_user: Dict[str, Any] = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Generate a platform-specific persona from core persona.""" + user_id = str(current_user.get('id')) + return await generate_platform_persona(user_id, platform, db) @router.put("/{persona_id}") async def update_persona_endpoint( diff --git a/backend/models/persona_models.py b/backend/models/persona_models.py index 6afedef1..b85af177 100644 --- a/backend/models/persona_models.py +++ b/backend/models/persona_models.py @@ -18,7 +18,7 @@ class WritingPersona(Base): # Primary fields id = Column(Integer, primary_key=True) - user_id = Column(Integer, nullable=False) + user_id = Column(String(255), nullable=False) # Changed to String to support Clerk user IDs persona_name = Column(String(255), nullable=False) # e.g., "Professional LinkedIn Voice", "Casual Blog Writer" # Core Identity diff --git a/backend/services/persona/facebook/facebook_persona_service.py b/backend/services/persona/facebook/facebook_persona_service.py index 624fc687..627a1d9d 100644 --- a/backend/services/persona/facebook/facebook_persona_service.py +++ b/backend/services/persona/facebook/facebook_persona_service.py @@ -355,19 +355,36 @@ class FacebookPersonaService: "properties": { "text_posts": { "type": "object", - "description": "Text post optimization for Facebook" + "description": "Text post optimization for Facebook", + "properties": { + "optimal_length": {"type": "string"}, + "structure_guidelines": {"type": "array", "items": {"type": "string"}}, + "hook_strategies": {"type": "array", "items": {"type": "string"}} + } }, "image_posts": { "type": "object", - "description": "Image post optimization for Facebook" + "description": "Image post optimization for Facebook", + "properties": { + "image_guidelines": {"type": "array", "items": {"type": "string"}}, + "caption_strategies": {"type": "array", "items": {"type": "string"}} + } }, "video_posts": { "type": "object", - "description": "Video post optimization for Facebook" + "description": "Video post optimization for Facebook", + "properties": { + "video_length_guidelines": {"type": "array", "items": {"type": "string"}}, + "engagement_hooks": {"type": "array", "items": {"type": "string"}} + } }, "carousel_posts": { "type": "object", - "description": "Carousel post optimization for Facebook" + "description": "Carousel post optimization for Facebook", + "properties": { + "slide_structure": {"type": "array", "items": {"type": "string"}}, + "storytelling_flow": {"type": "array", "items": {"type": "string"}} + } } } }, diff --git a/backend/services/persona_analysis_service.py b/backend/services/persona_analysis_service.py index 307b0358..f35fbcce 100644 --- a/backend/services/persona_analysis_service.py +++ b/backend/services/persona_analysis_service.py @@ -1,6 +1,12 @@ """ Persona Analysis Service Uses Gemini structured responses to analyze onboarding data and create writing personas. + +NOTE: This service uses the legacy WritingPersona/PlatformPersona models. +For new code, use PersonaDataService instead, which works with the PersonaData table +and provides richer persona data from onboarding. + +DEPRECATED: Consider migrating to PersonaDataService for better data richness. """ from typing import Dict, Any, List, Optional @@ -514,7 +520,7 @@ Generate a platform-optimized persona adaptation that maintains brand consistenc return min(score, 100.0) - def get_user_personas(self, user_id: int) -> List[Dict[str, Any]]: + def get_user_personas(self, user_id: str) -> List[Dict[str, Any]]: """Get all personas for a user.""" try: session = get_db_session() @@ -544,7 +550,7 @@ Generate a platform-optimized persona adaptation that maintains brand consistenc logger.error(f"Error getting user personas: {str(e)}") return [] - def get_persona_for_platform(self, user_id: int, platform: str) -> Optional[Dict[str, Any]]: + def get_persona_for_platform(self, user_id: str, platform: str) -> Optional[Dict[str, Any]]: """Get the best persona for a specific platform.""" try: session = get_db_session() diff --git a/backend/services/persona_data_service.py b/backend/services/persona_data_service.py new file mode 100644 index 00000000..ac02f9ca --- /dev/null +++ b/backend/services/persona_data_service.py @@ -0,0 +1,252 @@ +""" +Persona Data Service +Direct service for working with PersonaData table from onboarding. +Leverages the rich JSON structure for better content generation. +""" + +from typing import Dict, Any, Optional, List +from datetime import datetime +from loguru import logger +from sqlalchemy.orm import Session + +from services.database import get_db_session +from models.onboarding import PersonaData, OnboardingSession + + +class PersonaDataService: + """Service for working directly with PersonaData table.""" + + def __init__(self, db_session: Optional[Session] = None): + self.db = db_session or get_db_session() + + def get_user_persona_data(self, user_id: str) -> Optional[Dict[str, Any]]: + """Get complete persona data for a user from PersonaData table.""" + try: + # Get onboarding session for user + session = self.db.query(OnboardingSession).filter( + OnboardingSession.user_id == user_id + ).first() + + if not session: + logger.warning(f"No onboarding session found for user {user_id}") + return None + + # Get persona data + persona_data = self.db.query(PersonaData).filter( + PersonaData.session_id == session.id + ).first() + + if not persona_data: + logger.warning(f"No persona data found for user {user_id}") + return None + + return persona_data.to_dict() + + except Exception as e: + logger.error(f"Error getting persona data for user {user_id}: {str(e)}") + return None + + def get_platform_persona(self, user_id: str, platform: str) -> Optional[Dict[str, Any]]: + """Get platform-specific persona data for a user.""" + try: + persona_data = self.get_user_persona_data(user_id) + if not persona_data: + return None + + platform_personas = persona_data.get('platform_personas', {}) + platform_data = platform_personas.get(platform) + + if not platform_data: + logger.warning(f"No {platform} persona found for user {user_id}") + return None + + # Return rich platform-specific data + return { + "platform": platform, + "platform_persona": platform_data, + "core_persona": persona_data.get('core_persona', {}), + "quality_metrics": persona_data.get('quality_metrics', {}), + "selected_platforms": persona_data.get('selected_platforms', []), + "created_at": persona_data.get('created_at'), + "updated_at": persona_data.get('updated_at') + } + + except Exception as e: + logger.error(f"Error getting {platform} persona for user {user_id}: {str(e)}") + return None + + def get_all_platform_personas(self, user_id: str) -> Dict[str, Any]: + """Get all platform personas for a user.""" + try: + persona_data = self.get_user_persona_data(user_id) + if not persona_data: + return {} + + platform_personas = persona_data.get('platform_personas', {}) + + # Return structured data for all platforms + result = {} + for platform, platform_data in platform_personas.items(): + if isinstance(platform_data, dict) and 'error' not in platform_data: + result[platform] = { + "platform": platform, + "platform_persona": platform_data, + "core_persona": persona_data.get('core_persona', {}), + "quality_metrics": persona_data.get('quality_metrics', {}), + "created_at": persona_data.get('created_at'), + "updated_at": persona_data.get('updated_at') + } + + return result + + except Exception as e: + logger.error(f"Error getting all platform personas for user {user_id}: {str(e)}") + return {} + + def get_core_persona(self, user_id: str) -> Optional[Dict[str, Any]]: + """Get core persona data for a user.""" + try: + persona_data = self.get_user_persona_data(user_id) + if not persona_data: + return None + + return { + "core_persona": persona_data.get('core_persona', {}), + "quality_metrics": persona_data.get('quality_metrics', {}), + "selected_platforms": persona_data.get('selected_platforms', []), + "created_at": persona_data.get('created_at'), + "updated_at": persona_data.get('updated_at') + } + + except Exception as e: + logger.error(f"Error getting core persona for user {user_id}: {str(e)}") + return None + + def get_persona_quality_metrics(self, user_id: str) -> Optional[Dict[str, Any]]: + """Get quality metrics for a user's persona.""" + try: + persona_data = self.get_user_persona_data(user_id) + if not persona_data: + return None + + return persona_data.get('quality_metrics', {}) + + except Exception as e: + logger.error(f"Error getting quality metrics for user {user_id}: {str(e)}") + return None + + def update_platform_persona(self, user_id: str, platform: str, updates: Dict[str, Any]) -> bool: + """Update platform-specific persona data.""" + try: + # Get onboarding session for user + session = self.db.query(OnboardingSession).filter( + OnboardingSession.user_id == user_id + ).first() + + if not session: + logger.error(f"No onboarding session found for user {user_id}") + return False + + # Get persona data + persona_data = self.db.query(PersonaData).filter( + PersonaData.session_id == session.id + ).first() + + if not persona_data: + logger.error(f"No persona data found for user {user_id}") + return False + + # Update platform-specific data + platform_personas = persona_data.platform_personas or {} + if platform in platform_personas: + platform_personas[platform].update(updates) + persona_data.platform_personas = platform_personas + persona_data.updated_at = datetime.utcnow() + + self.db.commit() + logger.info(f"Updated {platform} persona for user {user_id}") + return True + else: + logger.warning(f"Platform {platform} not found for user {user_id}") + return False + + except Exception as e: + logger.error(f"Error updating {platform} persona for user {user_id}: {str(e)}") + self.db.rollback() + return False + + def save_platform_persona(self, user_id: str, platform: str, platform_data: Dict[str, Any]) -> bool: + """Save or create platform-specific persona data (creates if doesn't exist).""" + try: + # Get onboarding session + session = self.db.query(OnboardingSession).filter( + OnboardingSession.user_id == user_id + ).first() + + if not session: + logger.error(f"No onboarding session found for user {user_id}") + return False + + # Get persona data + persona_data = self.db.query(PersonaData).filter( + PersonaData.session_id == session.id + ).first() + + if not persona_data: + logger.error(f"No persona data found for user {user_id}") + return False + + # Update or create platform persona + platform_personas = persona_data.platform_personas or {} + platform_personas[platform] = platform_data # Create or overwrite + persona_data.platform_personas = platform_personas + persona_data.updated_at = datetime.utcnow() + + self.db.commit() + logger.info(f"Saved {platform} persona for user {user_id}") + return True + + except Exception as e: + logger.error(f"Error saving {platform} persona for user {user_id}: {str(e)}") + self.db.rollback() + return False + + def get_supported_platforms(self, user_id: str) -> List[str]: + """Get list of platforms for which personas exist.""" + try: + persona_data = self.get_user_persona_data(user_id) + if not persona_data: + return [] + + platform_personas = persona_data.get('platform_personas', {}) + return [platform for platform, data in platform_personas.items() + if isinstance(data, dict) and 'error' not in data] + + except Exception as e: + logger.error(f"Error getting supported platforms for user {user_id}: {str(e)}") + return [] + + def get_persona_summary(self, user_id: str) -> Dict[str, Any]: + """Get a summary of persona data for a user.""" + try: + persona_data = self.get_user_persona_data(user_id) + if not persona_data: + return {"error": "No persona data found"} + + platform_personas = persona_data.get('platform_personas', {}) + quality_metrics = persona_data.get('quality_metrics', {}) + + return { + "user_id": user_id, + "has_core_persona": bool(persona_data.get('core_persona')), + "platforms": list(platform_personas.keys()), + "platform_count": len(platform_personas), + "quality_score": quality_metrics.get('overall_score', 0), + "selected_platforms": persona_data.get('selected_platforms', []), + "created_at": persona_data.get('created_at'), + "updated_at": persona_data.get('updated_at') + } + + except Exception as e: + logger.error(f"Error getting persona summary for user {user_id}: {str(e)}") + return {"error": str(e)} diff --git a/frontend/src/api/persona.ts b/frontend/src/api/persona.ts index 17f852ef..ed1282a9 100644 --- a/frontend/src/api/persona.ts +++ b/frontend/src/api/persona.ts @@ -3,9 +3,7 @@ * Handles writing persona generation and management */ -import axios from 'axios'; - -const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000'; +import { apiClient } from './client'; export interface PersonaGenerationRequest { onboarding_session_id?: number; @@ -79,7 +77,7 @@ export interface SupportedPlatformsResponse { */ export const checkPersonaReadiness = async (userId: number = 1): Promise => { try { - const response = await axios.get(`${API_BASE_URL}/api/onboarding/persona-readiness`, { + const response = await apiClient.get('/api/onboarding/persona-readiness', { params: { user_id: userId } }); return response.data; @@ -94,7 +92,7 @@ export const checkPersonaReadiness = async (userId: number = 1): Promise => { try { - const response = await axios.get(`${API_BASE_URL}/api/onboarding/persona-preview`, { + const response = await apiClient.get('/api/onboarding/persona-preview', { params: { user_id: userId } }); return response.data; @@ -109,7 +107,7 @@ export const generatePersonaPreview = async (userId: number = 1): Promise => { try { - const response = await axios.post(`${API_BASE_URL}/api/personas/generate`, request, { + const response = await apiClient.post('/api/personas/generate', request, { params: { user_id: userId } }); return response.data; @@ -121,10 +119,11 @@ export const generateWritingPersona = async (userId: number = 1, request: Person /** * Get all writing personas for a user + * Note: user_id is extracted from Clerk JWT token, no need to pass it */ -export const getUserPersonas = async (userId: number = 1): Promise<{ personas: PersonaResponse[]; total_count: number }> => { +export const getUserPersonas = async (): Promise<{ personas: PersonaResponse[]; total_count: number }> => { try { - const response = await axios.get(`${API_BASE_URL}/api/personas/user/${userId}`); + const response = await apiClient.get('/api/personas/user'); return response.data; } catch (error: any) { console.error('Error getting user personas:', error); @@ -137,7 +136,7 @@ export const getUserPersonas = async (userId: number = 1): Promise<{ personas: P */ export const getPersonaDetails = async (userId: number, personaId: number): Promise => { try { - const response = await axios.get(`${API_BASE_URL}/api/personas/${personaId}`, { + const response = await apiClient.get(`/api/personas/${personaId}`, { params: { user_id: userId } }); return response.data; @@ -149,12 +148,11 @@ export const getPersonaDetails = async (userId: number, personaId: number): Prom /** * Get persona adaptation for a specific platform + * Note: user_id is extracted from Clerk JWT token, no need to pass it */ -export const getPlatformPersona = async (userId: number, platform: string): Promise => { +export const getPlatformPersona = async (platform: string): Promise => { try { - const response = await axios.get(`${API_BASE_URL}/api/personas/platform/${platform}`, { - params: { user_id: userId } - }); + const response = await apiClient.get(`/api/personas/platform/${platform}`); return response.data; } catch (error: any) { console.error('Error getting platform persona:', error); @@ -167,7 +165,7 @@ export const getPlatformPersona = async (userId: number, platform: string): Prom */ export const getSupportedPlatforms = async (): Promise => { try { - const response = await axios.get(`${API_BASE_URL}/api/personas/platforms`); + const response = await apiClient.get('/api/personas/platforms'); return response.data; } catch (error: any) { console.error('Error getting supported platforms:', error); @@ -180,7 +178,7 @@ export const getSupportedPlatforms = async (): Promise => { try { - const response = await axios.put(`${API_BASE_URL}/api/personas/${personaId}`, updateData, { + const response = await apiClient.put(`/api/personas/${personaId}`, updateData, { params: { user_id: userId } }); return response.data; @@ -190,12 +188,40 @@ export const updatePersona = async (userId: number, personaId: number, updateDat } }; +/** + * Update platform-specific persona + * Note: user_id is extracted from Clerk JWT token + */ +export const updatePlatformPersona = async (platform: string, updateData: any): Promise => { + try { + const response = await apiClient.put(`/api/personas/platform/${platform}`, updateData); + return response.data; + } catch (error: any) { + console.error('Error updating platform persona:', error); + throw new Error(error.response?.data?.detail || 'Failed to update platform persona'); + } +}; + +/** + * Generate a platform-specific persona from core persona + * Note: user_id is extracted from Clerk JWT token + */ +export const generatePlatformPersona = async (platform: string): Promise => { + try { + const response = await apiClient.post(`/api/personas/generate-platform/${platform}`); + return response.data; + } catch (error: any) { + console.error(`Error generating ${platform} persona:`, error); + throw new Error(error.response?.data?.detail || `Failed to generate ${platform} persona`); + } +}; + /** * Delete a persona */ export const deletePersona = async (userId: number, personaId: number): Promise => { try { - const response = await axios.delete(`${API_BASE_URL}/api/personas/${personaId}`, { + const response = await apiClient.delete(`/api/personas/${personaId}`, { params: { user_id: userId } }); return response.data; @@ -215,7 +241,7 @@ export const generateContentWithPersona = async ( contentType: string = 'post' ): Promise => { try { - const response = await axios.post(`${API_BASE_URL}/api/personas/generate-content`, { + const response = await apiClient.post('/api/personas/generate-content', { user_id: userId, platform, content_request: contentRequest, @@ -233,7 +259,7 @@ export const generateContentWithPersona = async ( */ export const exportPersonaPrompt = async (userId: number, platform: string): Promise => { try { - const response = await axios.get(`${API_BASE_URL}/api/personas/export/${platform}`, { + const response = await apiClient.get(`/api/personas/export/${platform}`, { params: { user_id: userId } }); return response.data; diff --git a/frontend/src/components/FacebookWriter/FacebookWriter.tsx b/frontend/src/components/FacebookWriter/FacebookWriter.tsx index cb78d7be..3ffe5f6b 100644 --- a/frontend/src/components/FacebookWriter/FacebookWriter.tsx +++ b/frontend/src/components/FacebookWriter/FacebookWriter.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useNavigate } from 'react-router-dom'; import { Box, Container, Typography, TextField, Paper, Button } from '@mui/material'; import { CopilotSidebar } from '@copilotkit/react-ui'; import { useCopilotReadable, useCopilotAction } from '@copilotkit/react-core'; @@ -7,6 +8,7 @@ import RegisterFacebookActions from './RegisterFacebookActions'; import RegisterFacebookEditActions from './RegisterFacebookEditActions'; import RegisterFacebookActionsEnhanced from './RegisterFacebookActionsEnhanced'; import { PlatformPersonaProvider, usePlatformPersonaContext } from '../shared/PersonaContext/PlatformPersonaProvider'; +import { generatePlatformPersona } from '../../api/persona'; const useCopilotActionTyped = useCopilotAction as any; @@ -142,6 +144,7 @@ const FacebookWriter: React.FC = ({ className = '' }) => { // Main Facebook Writer Content Component const FacebookWriterContent: React.FC = ({ className = '' }) => { + const navigate = useNavigate(); const [postDraft, setPostDraft] = React.useState(''); const [notes, setNotes] = React.useState(''); const [stage, setStage] = React.useState<'start' | 'edit'>('start'); @@ -160,7 +163,34 @@ const FacebookWriterContent: React.FC = ({ className = '' } const [selectionMenu, setSelectionMenu] = React.useState<{ x: number; y: number; text: string } | null>(null); // Get persona context for enhanced AI assistance - const { corePersona, platformPersona, loading: personaLoading } = usePlatformPersonaContext(); + const { corePersona, platformPersona, loading: personaLoading, refreshPersonas } = usePlatformPersonaContext(); + + // State for generating persona + const [isGeneratingPersona, setIsGeneratingPersona] = React.useState(false); + const [personaError, setPersonaError] = React.useState(null); + + // Handler to generate Facebook persona on-demand + const handleGeneratePersona = async () => { + setIsGeneratingPersona(true); + setPersonaError(null); + + try { + const result = await generatePlatformPersona('facebook'); + + if (result.success) { + // Refresh the persona context to load the newly generated persona + await refreshPersonas(); + console.log('✅ Facebook persona generated successfully'); + } else { + throw new Error('Failed to generate persona'); + } + } catch (error: any) { + console.error('Error generating persona:', error); + setPersonaError(error.message || 'Failed to generate Facebook persona'); + } finally { + setIsGeneratingPersona(false); + } + }; React.useEffect(() => { const onUpdate = (e: any) => { @@ -395,9 +425,31 @@ Always use the most appropriate tool for the user's request.`.trim(); > - - Facebook Writer (Preview) - + + {/* Back Button */} + + + + Facebook Writer + + {/* Persona Integration Indicator */} @@ -405,8 +457,8 @@ Always use the most appropriate tool for the user's request.`.trim();
- 🎭 - 🎭 Your Writing Assistant: {corePersona.persona_name} ({corePersona.archetype}) + 🎭 + + 🎭 Your Writing Assistant: {corePersona.persona_name} ({corePersona.archetype}) + {!platformPersona && ⚠️ Facebook persona not generated yet} + {corePersona.confidence_score}% accuracy | - Platform: Facebook Optimized + Platform: {platformPersona ? 'Facebook Optimized' : 'Generic (Generate Facebook Persona for better results)'} (Hover for details)
)} + + {/* Warning when platform persona is missing */} + {corePersona && !platformPersona && !personaLoading && ( +
+ ⚠️ +
+ Facebook Persona Not Generated +
+ You're using a generic persona. Generate a Facebook-specific persona for personalized content that matches your brand voice and Facebook's algorithm. +
+ {personaError && ( +
+ ⚠️ {personaError} +
+ )} +
+ +
+ )} +
= ({ }}>
diff --git a/frontend/src/components/LinkedInWriter/components/index.ts b/frontend/src/components/LinkedInWriter/components/index.ts index c4181821..31f28d19 100644 --- a/frontend/src/components/LinkedInWriter/components/index.ts +++ b/frontend/src/components/LinkedInWriter/components/index.ts @@ -26,4 +26,4 @@ export { default as ImageGenerationTest } from './ImageGenerationTest'; // Refactored Components export { default as BrainstormFlow } from './BrainstormFlow'; -export { default as CopilotActions } from './CopilotActions'; +export { useCopilotActions } from './CopilotActions'; diff --git a/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/MainContentPreviewHeader.tsx b/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/MainContentPreviewHeader.tsx index 166d91cb..90749fab 100644 --- a/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/MainContentPreviewHeader.tsx +++ b/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/MainContentPreviewHeader.tsx @@ -87,7 +87,6 @@ const MainContentPreviewHeader: React.FC = ({ {/* Persona Chip */} { console.log('Persona updated:', personaData); // You can add additional logic here to handle persona updates diff --git a/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/PersonaChip.tsx b/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/PersonaChip.tsx index 9d18cea9..cac83d77 100644 --- a/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/PersonaChip.tsx +++ b/frontend/src/components/TextEditor/ContentPreviewHeaderComponents/PersonaChip.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; import PersonaEditorModal from './PersonaEditorModal'; +import { getUserPersonas, getPlatformPersona, updatePersona, updatePlatformPersona } from '../../../api/persona'; interface PersonaData { id?: number; @@ -28,13 +29,11 @@ interface PersonaData { interface PersonaChipProps { platform: string; - userId?: number; onPersonaUpdate?: (personaData: PersonaData) => void; } const PersonaChip: React.FC = ({ platform, - userId = 1, onPersonaUpdate }) => { const [personaData, setPersonaData] = useState(null); @@ -48,44 +47,48 @@ const PersonaChip: React.FC = ({ setError(null); try { - // Fetch core persona list (take most recent active) and platform-specific details - const [coreRes, platformRes] = await Promise.all([ - fetch(`/api/personas/user/${userId}`), - fetch(`/api/personas/platform/${platform}?user_id=${userId}`) + // Fetch core persona list (take most recent active) and platform-specific details using authenticated API client + const [coreList, platformData] = await Promise.all([ + getUserPersonas(), + getPlatformPersona(platform) ]); - if (coreRes.ok && platformRes.ok) { - const coreList = await coreRes.json(); - const platformData = await platformRes.json(); - const core = (coreList?.personas && coreList.personas.length > 0) ? coreList.personas[0] : {}; + if (coreList && platformData) { + // Extract core persona from the response + const corePersona = platformData?.core_persona || {}; + const platformPersona = platformData?.platform_persona || {}; + const qualityMetrics = platformData?.quality_metrics || {}; + + if (!corePersona || Object.keys(corePersona).length === 0) { + setError('No persona found for this platform'); + return; + } // Merge core + platform fields for editor convenience setPersonaData({ - id: core.id, - user_id: core.user_id, - persona_name: core.persona_name, - archetype: core.archetype, - core_belief: core.core_belief, - brand_voice_description: core.brand_voice_description, - linguistic_fingerprint: core.linguistic_fingerprint, - platform_adaptations: core.platform_adaptations, - confidence_score: core.confidence_score, - ai_analysis_version: core.ai_analysis_version, + id: platformData?.id || 1, + user_id: 1, // Placeholder, not used + persona_name: corePersona.persona_name || 'Untitled Persona', + archetype: corePersona.archetype || 'General', + core_belief: corePersona.core_belief || '', + brand_voice_description: corePersona.brand_voice_description || corePersona.core_belief || '', + linguistic_fingerprint: corePersona.linguistic_fingerprint || {}, + platform_adaptations: corePersona.platform_adaptations || {}, + confidence_score: qualityMetrics.confidence_score || corePersona.confidence_score || 0, + ai_analysis_version: platformData?.ai_analysis_version || '1.0', platform_type: platform, - sentence_metrics: platformData?.sentence_metrics, - lexical_features: platformData?.lexical_features, - rhetorical_devices: platformData?.rhetorical_devices, - tonal_range: platformData?.tonal_range, - stylistic_constraints: platformData?.stylistic_constraints, - content_format_rules: platformData?.content_format_rules, - engagement_patterns: platformData?.engagement_patterns, - posting_frequency: platformData?.posting_frequency, - content_types: platformData?.content_types, - platform_best_practices: platformData?.platform_best_practices, - algorithm_considerations: platformData?.algorithm_considerations, + sentence_metrics: platformPersona?.sentence_metrics || {}, + lexical_features: platformPersona?.lexical_features || {}, + rhetorical_devices: platformPersona?.rhetorical_devices || {}, + tonal_range: platformPersona?.tonal_range || {}, + stylistic_constraints: platformPersona?.stylistic_constraints || {}, + content_format_rules: platformPersona?.content_format_rules || {}, + engagement_patterns: platformPersona?.engagement_patterns || {}, + posting_frequency: platformPersona?.posting_frequency || {}, + content_types: platformPersona?.content_types || {}, + platform_best_practices: platformPersona?.platform_best_practices || {}, + algorithm_considerations: platformPersona?.algorithm_considerations || {}, } as any); - } else { - setError('No persona found for this platform'); } } catch (err) { setError('Failed to load persona data'); @@ -98,7 +101,7 @@ const PersonaChip: React.FC = ({ useEffect(() => { fetchPersonaData(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [platform, userId]); + }, [platform]); const handleSavePersona = async (data: PersonaData, saveToDatabase: boolean) => { try { @@ -114,12 +117,8 @@ const PersonaChip: React.FC = ({ platform_adaptations: data.platform_adaptations, }; - const coreRes = await fetch(`/api/personas/${data.id}?user_id=${userId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(corePayload) - }); - if (!coreRes.ok) throw new Error('Failed to update core persona'); + // Use authenticated API client, note that user ID is extracted from JWT + await updatePersona(1, data.id, { core_persona: corePayload }); } // Save platform persona fields @@ -137,12 +136,8 @@ const PersonaChip: React.FC = ({ algorithm_considerations: data.algorithm_considerations, }; - const platRes = await fetch(`/api/personas/platform/${platform}?user_id=${userId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(platformPayload) - }); - if (!platRes.ok) throw new Error('Failed to update platform persona'); + // Use authenticated API client, note that user ID is extracted from JWT + await updatePlatformPersona(platform, platformPayload); } // Update local state diff --git a/frontend/src/components/shared/PersonaContext/PlatformPersonaProvider.tsx b/frontend/src/components/shared/PersonaContext/PlatformPersonaProvider.tsx index fbb40c93..5f7e23f4 100644 --- a/frontend/src/components/shared/PersonaContext/PlatformPersonaProvider.tsx +++ b/frontend/src/components/shared/PersonaContext/PlatformPersonaProvider.tsx @@ -6,6 +6,7 @@ import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback, useRef } from 'react'; import { useCopilotReadable } from '@copilotkit/react-core'; +import { useAuth } from '@clerk/clerk-react'; import { WritingPersona, PlatformAdaptation, @@ -33,7 +34,6 @@ const PlatformPersonaContext = createContext( interface PlatformPersonaProviderProps { children: ReactNode; platform: PlatformType; - userId?: number; // Default to 1 for now, can be enhanced with auth context later } // Cache duration: 5 minutes (constant outside component to avoid dependency issues) @@ -42,9 +42,13 @@ const CACHE_DURATION = 5 * 60 * 1000; // Provider component export const PlatformPersonaProvider: React.FC = ({ children, - platform, - userId = 1 + platform }) => { + // Get Clerk user ID + const { userId } = useAuth(); + + // Convert string userId to number for legacy API compatibility + const numericUserId = userId ? 1 : 1; // Use 1 as placeholder, API uses Clerk ID from auth // State management const [corePersona, setCorePersona] = useState(null); const [platformPersona, setPlatformPersona] = useState(null); @@ -83,24 +87,69 @@ export const PlatformPersonaProvider: React.FC = ( setError(null); // Fetch both core persona and platform-specific data - const [userPersonasResponse, platformPersonaResponse] = await Promise.all([ - getUserPersonas(userId), - getPlatformPersona(userId, platform) - ]); + // Note: APIs use Clerk auth, so user ID is extracted from JWT + let userPersonasResponse; + let platformPersonaResponse = null; + + try { + const results = await Promise.all([ + getUserPersonas(), + getPlatformPersona(platform).catch(err => { + // Handle 404 gracefully - platform persona doesn't exist yet + if (err.message && err.message.includes('No persona found')) { + console.warn(`⚠️ No ${platform} persona found - user can still generate content`); + return null; + } + throw err; + }) + ]); + userPersonasResponse = results[0]; + platformPersonaResponse = results[1]; + } catch (error) { + // If platform persona fetch fails, continue with core persona only + console.warn(`⚠️ Platform persona unavailable: ${error instanceof Error ? error.message : 'Unknown error'}`); + userPersonasResponse = await getUserPersonas(); + platformPersonaResponse = null; + } // Handle core persona data - if (userPersonasResponse.personas && userPersonasResponse.personas.length > 0) { - const primaryPersona = userPersonasResponse.personas[0]; + console.log('🔍 API Response - userPersonasResponse:', userPersonasResponse); + + // Backend returns personas as a dictionary of platform -> persona data + // Convert to array format for easier processing + let personasArray: any[] = []; + if (userPersonasResponse && userPersonasResponse.personas) { + if (Array.isArray(userPersonasResponse.personas)) { + personasArray = userPersonasResponse.personas; + } else if (typeof userPersonasResponse.personas === 'object') { + // Convert dictionary to array + personasArray = Object.values(userPersonasResponse.personas); + } + } + + console.log('🔍 Processed personas array:', { + isArray: Array.isArray(personasArray), + length: personasArray.length, + firstItem: personasArray[0] + }); + + if (personasArray.length > 0) { + const primaryPersona = personasArray[0]; + console.log('🔍 Primary persona from API:', primaryPersona); + + // Extract core persona data (may be nested in the response) + const corePersonaData = primaryPersona.core_persona || primaryPersona; + const identity = corePersonaData.identity || {}; // Convert API response to WritingPersona format const convertedPersona: WritingPersona = { - id: primaryPersona.persona_id, - user_id: userId, - persona_name: primaryPersona.persona_name, - archetype: primaryPersona.archetype, - core_belief: primaryPersona.core_belief, - brand_voice_description: primaryPersona.core_belief, // Use core_belief as fallback - linguistic_fingerprint: { + id: primaryPersona.id || corePersonaData.id || 1, + user_id: numericUserId, // Use numeric ID for legacy compatibility + persona_name: identity.persona_name || corePersonaData.persona_name || primaryPersona.persona_name || 'Untitled Persona', + archetype: identity.archetype || corePersonaData.archetype || primaryPersona.archetype || 'General', + core_belief: identity.core_belief || corePersonaData.core_belief || primaryPersona.core_belief || '', + brand_voice_description: identity.brand_voice_description || corePersonaData.brand_voice_description || corePersonaData.core_belief || primaryPersona.core_belief || '', + linguistic_fingerprint: corePersonaData.linguistic_fingerprint || { sentence_metrics: { average_sentence_length_words: 15, preferred_sentence_type: "compound", @@ -130,10 +179,11 @@ export const PlatformPersonaProvider: React.FC = ( source_website_analysis: {}, source_research_preferences: {}, ai_analysis_version: "1.0", - confidence_score: primaryPersona.confidence_score, - analysis_date: primaryPersona.created_at, + confidence_score: primaryPersona.quality_metrics?.overall_score ? primaryPersona.quality_metrics.overall_score / 100 : + (corePersonaData.confidence_score || primaryPersona.confidence_score || 0), + analysis_date: corePersonaData.created_at || primaryPersona.created_at, created_at: primaryPersona.created_at, - updated_at: primaryPersona.created_at, + updated_at: primaryPersona.updated_at || primaryPersona.created_at, is_active: true }; @@ -142,7 +192,10 @@ export const PlatformPersonaProvider: React.FC = ( console.log('✅ Core persona loaded:', { name: convertedPersona.persona_name, archetype: convertedPersona.archetype, - confidence: convertedPersona.confidence_score + confidence: convertedPersona.confidence_score, + hasLinguisticFingerprint: !!(convertedPersona.linguistic_fingerprint && Object.keys(convertedPersona.linguistic_fingerprint).length), + identityData: identity, + quality_metrics: primaryPersona.quality_metrics }); } else { console.warn('⚠️ No core personas found for user'); @@ -150,46 +203,51 @@ export const PlatformPersonaProvider: React.FC = ( } // Handle platform-specific persona data + console.log('🔍 API Response - platformPersonaResponse:', platformPersonaResponse); if (platformPersonaResponse) { + // Extract platform-specific data from API response + const platformPersona = platformPersonaResponse.platform_persona || {}; + const corePersonaFromPlatform = platformPersonaResponse.core_persona || {}; + // Convert API response to PlatformAdaptation format const convertedPlatformPersona: PlatformAdaptation = { id: 1, writing_persona_id: corePersona?.id || 1, platform_type: platform, - sentence_metrics: { + sentence_metrics: platformPersona.sentence_metrics || { optimal_length: "150-300 words", character_limit: platform === 'linkedin' ? 3000 : 280, sentence_structure: "varied", paragraph_breaks: "frequent", readability_score: 8.5 }, - lexical_features: { + lexical_features: platformPersona.lexical_features || { hashtag_strategy: "3-5 relevant hashtags", platform_specific_terms: [], engagement_phrases: ["What do you think?", "Share your thoughts"], call_to_action_style: "gentle" }, - rhetorical_devices: { + rhetorical_devices: platformPersona.rhetorical_devices || { question_frequency: "occasional", story_elements: "personal_anecdotes", visual_descriptions: "minimal", interactive_elements: "questions" }, - tonal_range: { + tonal_range: platformPersona.tonal_range || { default_tone: "professional_friendly", permissible_tones: ["inspiring", "thoughtful"], forbidden_tones: ["salesy", "academic"], emotional_range: "moderate", formality_level: "semi_formal" }, - stylistic_constraints: { + stylistic_constraints: platformPersona.stylistic_constraints || { punctuation_preferences: "standard", formatting_rules: "clean", emoji_usage: "minimal", link_placement: "end", media_integration: "encouraged" }, - content_format_rules: { + content_format_rules: platformPersona.content_format_rules || { character_limit: platform === 'linkedin' ? 3000 : 280, optimal_length: platform === 'linkedin' ? "150-300 words" : "120-150 characters", word_count: platform === 'linkedin' ? "150-300" : "20-25", @@ -197,26 +255,26 @@ export const PlatformPersonaProvider: React.FC = ( media_requirements: "optional", link_restrictions: "unlimited" }, - engagement_patterns: { + engagement_patterns: platformPersona.engagement_patterns || { posting_frequency: "2-3 times per week", best_timing: "9 AM - 11 AM, 1 PM - 3 PM", interaction_style: "conversational", response_strategy: "within 2 hours", community_approach: "collaborative" }, - posting_frequency: { + posting_frequency: platformPersona.posting_frequency || { frequency: "2-3 times per week", optimal_days: ["Tuesday", "Wednesday", "Thursday"], optimal_times: ["9:00 AM", "1:00 PM"], seasonal_adjustments: "moderate" }, - content_types: { + content_types: platformPersona.content_types || { primary_content: ["thought_leadership", "industry_insights"], secondary_content: ["personal_stories", "tips"], content_mix: "70% professional, 30% personal", seasonal_content: ["trending_topics", "industry_events"] }, - platform_best_practices: { + platform_best_practices: platformPersona.platform_best_practices || { algorithm_tips: ["post_consistently", "engage_with_community"], engagement_tactics: ["ask_questions", "share_stories"], content_strategies: ["value_first", "authentic_voice"], @@ -231,7 +289,8 @@ export const PlatformPersonaProvider: React.FC = ( console.log('✅ Platform persona loaded:', { platform: convertedPlatformPersona.platform_type, characterLimit: convertedPlatformPersona.content_format_rules?.character_limit, - optimalLength: convertedPlatformPersona.content_format_rules?.optimal_length + optimalLength: convertedPlatformPersona.content_format_rules?.optimal_length, + hasData: !!(platformPersona && Object.keys(platformPersona).length > 0) }); } else { console.warn(`⚠️ No platform-specific persona found for ${platform}`); @@ -272,6 +331,18 @@ export const PlatformPersonaProvider: React.FC = ( parentId: corePersona?.id?.toString() }); + // Debug: Log when persona data is available for CopilotKit + useEffect(() => { + if (corePersona) { + console.log('🎯 Injected core persona into CopilotKit:', { + name: corePersona.persona_name, + archetype: corePersona.archetype, + confidence: corePersona.confidence_score, + hasLinguisticFingerprint: !!(corePersona.linguistic_fingerprint && Object.keys(corePersona.linguistic_fingerprint).length) + }); + } + }, [corePersona]); + // Inject platform-specific persona into CopilotKit context useCopilotReadable({ description: `${platform} platform optimization rules and constraints`, @@ -280,6 +351,17 @@ export const PlatformPersonaProvider: React.FC = ( parentId: corePersona?.id?.toString() }); + // Debug: Log when platform persona is available for CopilotKit + useEffect(() => { + if (platformPersona) { + console.log('🎯 Injected platform persona into CopilotKit:', { + platform: platformPersona.platform_type, + characterLimit: platformPersona.content_format_rules?.character_limit, + optimalLength: platformPersona.content_format_rules?.optimal_length + }); + } + }, [platformPersona]); + // Inject combined persona context for comprehensive understanding useCopilotReadable({ description: `Complete ${platform} writing persona with linguistic fingerprint and platform optimization`,