LinkedIn and Facebook Persona Services Implementation

This commit is contained in:
ajaysi
2025-10-26 10:06:24 +05:30
parent caeb6e56a9
commit 5866f49325
15 changed files with 868 additions and 281 deletions

View File

@@ -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:

View File

@@ -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(

View File

@@ -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

View File

@@ -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"}}
}
}
}
},

View File

@@ -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()

View File

@@ -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)}

View File

@@ -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<PersonaReadinessResponse> => {
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<Persona
*/
export const generatePersonaPreview = async (userId: number = 1): Promise<PersonaPreviewResponse> => {
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<Person
*/
export const generateWritingPersona = async (userId: number = 1, request: PersonaGenerationRequest = {}): Promise<PersonaGenerationResponse> => {
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<any> => {
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<any> => {
export const getPlatformPersona = async (platform: string): Promise<any> => {
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<SupportedPlatformsResponse> => {
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<SupportedPlatformsRespons
*/
export const updatePersona = async (userId: number, personaId: number, updateData: any): Promise<any> => {
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<any> => {
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<any> => {
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<any> => {
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<any> => {
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<any> => {
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;

View File

@@ -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<FacebookWriterProps> = ({ className = '' }) => {
// Main Facebook Writer Content Component
const FacebookWriterContent: React.FC<FacebookWriterProps> = ({ className = '' }) => {
const navigate = useNavigate();
const [postDraft, setPostDraft] = React.useState<string>('');
const [notes, setNotes] = React.useState<string>('');
const [stage, setStage] = React.useState<'start' | 'edit'>('start');
@@ -160,7 +163,34 @@ const FacebookWriterContent: React.FC<FacebookWriterProps> = ({ 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<boolean>(false);
const [personaError, setPersonaError] = React.useState<string | null>(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();
>
<Container maxWidth="md" sx={{ position: 'relative', zIndex: 1, py: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 800, letterSpacing: 0.3 }}>
Facebook Writer (Preview)
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
{/* Back Button */}
<Button
onClick={() => navigate('/dashboard')}
sx={{
padding: '8px 16px',
background: 'rgba(255, 255, 255, 0.1)',
color: 'white',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: 2,
textTransform: 'none',
fontSize: 14,
fontWeight: 600,
'&:hover': {
background: 'rgba(255, 255, 255, 0.2)',
}
}}
>
Back to Dashboard
</Button>
<Typography variant="h4" sx={{ fontWeight: 800, letterSpacing: 0.3 }}>
Facebook Writer
</Typography>
</Box>
</Box>
{/* Persona Integration Indicator */}
@@ -405,8 +457,8 @@ Always use the most appropriate tool for the user's request.`.trim();
<div
style={{
padding: '8px 16px',
backgroundColor: 'rgba(24, 119, 242, 0.1)',
borderBottom: '1px solid rgba(24, 119, 242, 0.3)',
backgroundColor: platformPersona ? 'rgba(24, 119, 242, 0.1)' : 'rgba(255, 152, 0, 0.1)',
borderBottom: `1px solid ${platformPersona ? 'rgba(24, 119, 242, 0.3)' : 'rgba(255, 152, 0, 0.3)'}`,
fontSize: '12px',
color: 'rgba(255, 255, 255, 0.8)',
display: 'flex',
@@ -416,7 +468,7 @@ Always use the most appropriate tool for the user's request.`.trim();
position: 'relative',
marginBottom: '16px',
borderRadius: '8px',
border: '1px solid rgba(24, 119, 242, 0.2)'
border: `1px solid ${platformPersona ? 'rgba(24, 119, 242, 0.2)' : 'rgba(255, 152, 0, 0.2)'}`
}}
title={`🎭 YOUR PERSONALIZED WRITING ASSISTANT
@@ -468,17 +520,70 @@ Instead of generic content, you get:
💡 TRY THIS: Ask the AI to "generate a Facebook post about [your topic]" and watch how it automatically applies your persona to create content that sounds like you!`}
>
<span style={{ color: '#1877f2' }}>🎭</span>
<span><strong>🎭 Your Writing Assistant:</strong> {corePersona.persona_name} ({corePersona.archetype})</span>
<span style={{ color: platformPersona ? '#1877f2' : '#FF9800' }}>🎭</span>
<span>
<strong>🎭 Your Writing Assistant:</strong> {corePersona.persona_name} ({corePersona.archetype})
{!platformPersona && <span style={{ color: '#FF9800', marginLeft: '8px' }}> Facebook persona not generated yet</span>}
</span>
<span style={{ marginLeft: 'auto', fontSize: '11px' }}>
{corePersona.confidence_score}% accuracy |
Platform: Facebook Optimized
Platform: {platformPersona ? 'Facebook Optimized' : 'Generic (Generate Facebook Persona for better results)'}
</span>
<span style={{ fontSize: '10px', color: 'rgba(255, 255, 255, 0.6)', marginLeft: '8px' }}>
(Hover for details)
</span>
</div>
)}
{/* Warning when platform persona is missing */}
{corePersona && !platformPersona && !personaLoading && (
<div
style={{
padding: '12px 16px',
backgroundColor: 'rgba(255, 152, 0, 0.15)',
border: '1px solid rgba(255, 152, 0, 0.3)',
fontSize: '13px',
color: 'rgba(255, 255, 255, 0.9)',
marginBottom: '16px',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
gap: '12px'
}}
>
<span style={{ fontSize: '20px' }}></span>
<div style={{ flex: 1 }}>
<strong>Facebook Persona Not Generated</strong>
<div style={{ fontSize: '12px', color: 'rgba(255, 255, 255, 0.7)', marginTop: '4px' }}>
You're using a generic persona. Generate a Facebook-specific persona for personalized content that matches your brand voice and Facebook's algorithm.
</div>
{personaError && (
<div style={{ fontSize: '11px', color: '#ff6b6b', marginTop: '4px' }}>
{personaError}
</div>
)}
</div>
<Button
onClick={handleGeneratePersona}
disabled={isGeneratingPersona}
size="small"
sx={{
background: 'rgba(255, 152, 0, 0.2)',
color: 'white',
border: '1px solid rgba(255, 152, 0, 0.4)',
textTransform: 'none',
'&:hover': {
background: 'rgba(255, 152, 0, 0.3)',
},
'&:disabled': {
opacity: 0.6
}
}}
>
{isGeneratingPersona ? 'Generating...' : 'Generate Persona →'}
</Button>
</div>
)}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Button size="small" variant="outlined" disabled sx={{ color: 'rgba(255,255,255,0.7)', borderColor: 'rgba(255,255,255,0.25)' }}>
DashBoard

View File

@@ -6,7 +6,8 @@ import './styles/alwrity-copilot.css';
import RegisterLinkedInActions from './RegisterLinkedInActions';
import RegisterLinkedInEditActions from './RegisterLinkedInEditActions';
import RegisterLinkedInActionsEnhanced from './RegisterLinkedInActionsEnhanced';
import { Header, ContentEditor, LoadingIndicator, WelcomeMessage, ProgressTracker, CopilotActions } from './components';
import { Header, ContentEditor, LoadingIndicator, WelcomeMessage, ProgressTracker } from './components';
import { useCopilotActions } from './components/CopilotActions';
import { useLinkedInWriter } from './hooks/useLinkedInWriter';
import { useCopilotPersistence } from './utils/enhancedPersistence';
import { PlatformPersonaProvider, usePlatformPersonaContext } from '../shared/PersonaContext/PlatformPersonaProvider';
@@ -226,8 +227,8 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
});
// Initialize CopilotActions component to handle all copilot-related functionality
const getIntelligentSuggestions = CopilotActions({
// Use the CopilotActions hook to handle all copilot-related functionality
const getIntelligentSuggestions = useCopilotActions({
draft,
context,
userPreferences,
@@ -237,7 +238,15 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
});
return (
<div className={`linkedin-writer ${className}`} style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
<div
className={`linkedin-writer ${className}`}
style={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
backgroundColor: '#ffffff' // White professional background
}}
>
{/* Header */}
<Header
userPreferences={userPreferences}
@@ -267,7 +276,7 @@ const LinkedInWriterContent: React.FC<LinkedInWriterProps> = ({ className = '' }
{/* Main Content */}
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', backgroundColor: '#ffffff' }}>
{/* Loading Indicator */}
<LoadingIndicator
isGenerating={isGenerating}

View File

@@ -16,9 +16,9 @@ interface CopilotActionsProps {
setDraft: (draft: string) => void;
}
// Note: This is implemented as a hook-like utility, not a rendered component.
// Note: This is implemented as a custom hook.
// It returns the getIntelligentSuggestions function for use by the caller.
const CopilotActions = ({
export const useCopilotActions = ({
draft,
context,
userPreferences,
@@ -428,5 +428,3 @@ const CopilotActions = ({
// Return the suggestions function directly
return getIntelligentSuggestions;
};
export default CopilotActions;

View File

@@ -1,4 +1,5 @@
import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { LinkedInPreferences } from '../utils/storageUtils';
import { PersonaChip } from '../../TextEditor/ContentPreviewHeaderComponents';
import { usePlatformPersonaContext } from '../../shared/PersonaContext/PlatformPersonaProvider';
@@ -25,6 +26,7 @@ export const Header: React.FC<HeaderProps> = ({
onClearHistory,
getHistoryLength
}) => {
const navigate = useNavigate();
const [personaOverride, setPersonaOverride] = useState<any>(null);
const { corePersona, platformPersona } = usePlatformPersonaContext();
@@ -89,6 +91,34 @@ export const Header: React.FC<HeaderProps> = ({
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{/* Left Section - Logo and Title */}
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
{/* Back Button */}
<button
onClick={() => navigate('/dashboard')}
style={{
padding: '8px 12px',
background: 'rgba(255, 255, 255, 0.1)',
color: 'white',
border: '1px solid rgba(255, 255, 255, 0.2)',
borderRadius: 8,
cursor: 'pointer',
fontSize: 14,
fontWeight: 600,
display: 'flex',
alignItems: 'center',
gap: 6,
transition: 'all 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.2)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
}}
title="Back to Dashboard"
>
Back to Dashboard
</button>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<img
src={alwrityLogo}
@@ -211,7 +241,6 @@ export const Header: React.FC<HeaderProps> = ({
}}>
<PersonaChip
platform="linkedin"
userId={1}
onPersonaUpdate={handlePersonaUpdate}
/>
</div>

View File

@@ -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';

View File

@@ -87,7 +87,6 @@ const MainContentPreviewHeader: React.FC<MainContentPreviewHeaderProps> = ({
{/* Persona Chip */}
<PersonaChip
platform="linkedin"
userId={1}
onPersonaUpdate={(personaData) => {
console.log('Persona updated:', personaData);
// You can add additional logic here to handle persona updates

View File

@@ -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<PersonaChipProps> = ({
platform,
userId = 1,
onPersonaUpdate
}) => {
const [personaData, setPersonaData] = useState<PersonaData | null>(null);
@@ -48,44 +47,48 @@ const PersonaChip: React.FC<PersonaChipProps> = ({
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<PersonaChipProps> = ({
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<PersonaChipProps> = ({
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<PersonaChipProps> = ({
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

View File

@@ -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<PlatformPersonaContextType | null>(
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<PlatformPersonaProviderProps> = ({
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<WritingPersona | null>(null);
const [platformPersona, setPlatformPersona] = useState<PlatformAdaptation | null>(null);
@@ -83,24 +87,69 @@ export const PlatformPersonaProvider: React.FC<PlatformPersonaProviderProps> = (
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<PlatformPersonaProviderProps> = (
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<PlatformPersonaProviderProps> = (
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<PlatformPersonaProviderProps> = (
}
// 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<PlatformPersonaProviderProps> = (
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<PlatformPersonaProviderProps> = (
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<PlatformPersonaProviderProps> = (
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<PlatformPersonaProviderProps> = (
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`,