ALwrity version 0.5.5

This commit is contained in:
ajaysi
2025-08-13 17:38:54 +05:30
parent 66ece49705
commit 2b8c66c4d0
23 changed files with 3080 additions and 976 deletions

View File

@@ -332,7 +332,7 @@ async def generate_comprehensive_strategy_polling(
"onboarding_data": onboarding_data,
"user_id": user_id,
"generation_config": config or {}
}
}
# Create strategy generation config
generation_config = StrategyGenerationConfig(

View File

@@ -26,6 +26,8 @@ class AutoFillRefreshService:
- Optionally augments with AI overrides (hook, not persisted)
- Returns payload in the same shape as AutoFillService.get_autofill, plus meta
"""
logger.info(f"AutoFillRefreshService: starting build_fresh_payload | user=%s | use_ai=%s | ai_only=%s", user_id, use_ai, ai_only)
# Base context from onboarding analysis (used for AI context only when ai_only)
logger.debug("AutoFillRefreshService: processing onboarding context | user=%s", user_id)
base_context = await self.autofill.integration.process_onboarding_data(user_id, self.db)
@@ -37,6 +39,33 @@ class AutoFillRefreshService:
bool((base_context or {}).get('api_keys_data')),
bool((base_context or {}).get('onboarding_session')),
)
# Log detailed context analysis
logger.info(f"AutoFillRefreshService: detailed context analysis | user=%s", user_id)
if base_context:
website_analysis = base_context.get('website_analysis', {})
research_preferences = base_context.get('research_preferences', {})
api_keys_data = base_context.get('api_keys_data', {})
onboarding_session = base_context.get('onboarding_session', {})
logger.info(f" - Website analysis keys: {list(website_analysis.keys()) if website_analysis else 'None'}")
logger.info(f" - Research preferences keys: {list(research_preferences.keys()) if research_preferences else 'None'}")
logger.info(f" - API keys data keys: {list(api_keys_data.keys()) if api_keys_data else 'None'}")
logger.info(f" - Onboarding session keys: {list(onboarding_session.keys()) if onboarding_session else 'None'}")
# Log specific data points
if website_analysis:
logger.info(f" - Website URL: {website_analysis.get('website_url', 'Not found')}")
logger.info(f" - Website status: {website_analysis.get('status', 'Unknown')}")
if research_preferences:
logger.info(f" - Research depth: {research_preferences.get('research_depth', 'Not found')}")
logger.info(f" - Content types: {research_preferences.get('content_types', 'Not found')}")
if api_keys_data:
logger.info(f" - API providers: {api_keys_data.get('providers', [])}")
logger.info(f" - Total keys: {api_keys_data.get('total_keys', 0)}")
else:
logger.warning(f"AutoFillRefreshService: no base context available | user=%s", user_id)
try:
w = (base_context or {}).get('website_analysis') or {}
r = (base_context or {}).get('research_preferences') or {}
@@ -50,6 +79,16 @@ class AutoFillRefreshService:
ai_payload = await self.structured_ai.generate_autofill_fields(user_id, base_context)
meta = ai_payload.get('meta') or {}
logger.info("AI-only payload meta: ai_used=%s overrides=%s", meta.get('ai_used'), meta.get('ai_overrides_count'))
# Log detailed AI payload analysis
logger.info(f"AutoFillRefreshService: AI payload analysis | user=%s", user_id)
logger.info(f" - AI used: {meta.get('ai_used', False)}")
logger.info(f" - AI overrides count: {meta.get('ai_overrides_count', 0)}")
logger.info(f" - Success rate: {meta.get('success_rate', 0):.1f}%")
logger.info(f" - Attempts: {meta.get('attempts', 0)}")
logger.info(f" - Missing fields: {len(meta.get('missing_fields', []))}")
logger.info(f" - Fields generated: {len(ai_payload.get('fields', {}))}")
return ai_payload
except Exception as e:
logger.error("AI-only structured generation failed | user=%s | err=%s", user_id, repr(e))
@@ -68,6 +107,7 @@ class AutoFillRefreshService:
}
# Fallback to previous behavior (DB + sparse overrides)
logger.info("AutoFillRefreshService: using fallback behavior (DB + sparse overrides)")
payload = await self.autofill.get_autofill(user_id)
logger.info("AutoFillRefreshService: Base payload fields: %d", len(payload.get('fields', {})))

View File

@@ -496,10 +496,21 @@ Generate the complete JSON with all 30 fields personalized for {website_url}:
logger.info("AIStructuredAutofillService: generating %d fields | user=%s", len(CORE_FIELDS), user_id)
logger.debug("AIStructuredAutofillService: properties=%d", len(schema.get('properties', {})))
# Log context summary for debugging
logger.info("AIStructuredAutofillService: context summary | user=%s", user_id)
logger.info(" - Website analysis exists: %s", bool(context_summary.get('user_profile', {}).get('website_url')))
logger.info(" - Research config: %s", context_summary.get('research_config', {}).get('research_depth', 'None'))
logger.info(" - API capabilities: %s", len(context_summary.get('api_capabilities', {}).get('providers', [])))
logger.info(" - Content analysis: %s", bool(context_summary.get('content_analysis')))
logger.info(" - Audience insights: %s", bool(context_summary.get('audience_insights')))
# Log prompt length for debugging
logger.info("AIStructuredAutofillService: prompt length=%d chars | user=%s", len(prompt), user_id)
last_result = None
for attempt in range(self.max_retries + 1):
try:
logger.info(f"AI structured call attempt {attempt + 1}/{self.max_retries + 1}")
logger.info(f"AI structured call attempt {attempt + 1}/{self.max_retries + 1} | user=%s", user_id)
result = await self.ai.execute_structured_json_call(
service_type=AIServiceType.STRATEGIC_INTELLIGENCE,
prompt=prompt,
@@ -507,8 +518,34 @@ Generate the complete JSON with all 30 fields personalized for {website_url}:
)
last_result = result
# Log AI response details
logger.info(f"AI response received | attempt={attempt + 1} | user=%s", user_id)
if isinstance(result, dict):
logger.info(f" - Response keys: {list(result.keys())}")
logger.info(f" - Response type: dict with {len(result)} items")
# Handle wrapped response from AI service manager
if 'data' in result and 'success' in result:
# This is a wrapped response from AI service manager
if result.get('success'):
# Extract the actual AI response from the 'data' field
ai_response = result.get('data', {})
logger.info(f" - Extracted AI response from wrapped response")
logger.info(f" - AI response keys: {list(ai_response.keys()) if isinstance(ai_response, dict) else 'N/A'}")
last_result = ai_response
else:
# AI service failed
error_msg = result.get('error', 'Unknown AI service error')
logger.error(f" - AI service failed: {error_msg}")
last_result = {'error': error_msg}
elif 'error' in result:
logger.error(f" - AI returned error: {result['error']}")
else:
logger.warning(f" - Response type: {type(result)}")
# Check if we should retry
if not self._should_retry(result, attempt):
if not self._should_retry(last_result, attempt):
logger.info(f"Retry not needed | attempt={attempt + 1} | user=%s", user_id)
break
# Add a small delay before retry

View File

@@ -7,6 +7,7 @@ import logging
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
import traceback
# Import database models
from models.enhanced_strategy_models import (
@@ -39,6 +40,13 @@ class OnboardingDataIntegrationService:
api_keys_data = self._get_api_keys_data(user_id, db)
onboarding_session = self._get_onboarding_session(user_id, db)
# Log data source status
logger.info(f"Data source status for user {user_id}:")
logger.info(f" - Website analysis: {'✅ Found' if website_analysis else '❌ Missing'}")
logger.info(f" - Research preferences: {'✅ Found' if research_preferences else '❌ Missing'}")
logger.info(f" - API keys data: {'✅ Found' if api_keys_data else '❌ Missing'}")
logger.info(f" - Onboarding session: {'✅ Found' if onboarding_session else '❌ Missing'}")
# Process and integrate data
integrated_data = {
'website_analysis': website_analysis,
@@ -49,6 +57,14 @@ class OnboardingDataIntegrationService:
'processing_timestamp': datetime.utcnow().isoformat()
}
# Log data quality assessment
data_quality = integrated_data['data_quality']
logger.info(f"Data quality assessment for user {user_id}:")
logger.info(f" - Completeness: {data_quality.get('completeness', 0):.2f}")
logger.info(f" - Freshness: {data_quality.get('freshness', 0):.2f}")
logger.info(f" - Relevance: {data_quality.get('relevance', 0):.2f}")
logger.info(f" - Confidence: {data_quality.get('confidence', 0):.2f}")
# Store integrated data
await self._store_integrated_data(user_id, integrated_data, db)
@@ -57,6 +73,7 @@ class OnboardingDataIntegrationService:
except Exception as e:
logger.error(f"Error processing onboarding data for user {user_id}: {str(e)}")
logger.error("Traceback:\n%s", traceback.format_exc())
return self._get_fallback_data()
def _get_website_analysis(self, user_id: int, db: Session) -> Dict[str, Any]:

View File

@@ -7,7 +7,20 @@ import google.genai as genai
from google.genai import types
from dotenv import load_dotenv
load_dotenv(Path('../../../.env'))
# Fix the environment loading path - load from backend directory
current_dir = Path(__file__).parent.parent # services directory
backend_dir = current_dir.parent # backend directory
env_path = backend_dir / '.env'
if env_path.exists():
load_dotenv(env_path)
print(f"Loaded .env from: {env_path}")
else:
# Fallback to current directory
load_dotenv()
print(f"No .env found at {env_path}, using current directory")
from loguru import logger
logger.remove()
logger.add(sys.stdout,
@@ -31,14 +44,33 @@ import logging
logging.basicConfig(level=logging.INFO, format='[%(asctime)s-%(levelname)s-%(module)s-%(lineno)d]- %(message)s')
logger = logging.getLogger(__name__)
def get_gemini_api_key() -> str:
"""Get Gemini API key with proper error handling."""
api_key = os.getenv('GEMINI_API_KEY')
if not api_key:
error_msg = "GEMINI_API_KEY environment variable is not set. Please set it in your .env file."
logger.error(error_msg)
raise ValueError(error_msg)
# Validate API key format (basic check)
if not api_key.startswith('AIza'):
error_msg = "GEMINI_API_KEY appears to be invalid. It should start with 'AIza'."
logger.error(error_msg)
raise ValueError(error_msg)
return api_key
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
def gemini_text_response(prompt, temperature, top_p, n, max_tokens, system_prompt):
""" Common functiont to get response from gemini pro Text. """
#FIXME: Include : https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/System_instructions_REST.ipynb
try:
client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
api_key = get_gemini_api_key()
client = genai.Client(api_key=api_key)
logger.info("✅ Gemini client initialized successfully")
except Exception as err:
logger.error(f"Failed to configure Gemini: {err}")
raise
logger.info(f"Temp: {temperature}, MaxTokens: {max_tokens}, TopP: {top_p}, N: {n}")
# Set up AI model config
generation_config = {
@@ -121,20 +153,32 @@ async def test_gemini_api_key(api_key: str) -> tuple[bool, str]:
tuple[bool, str]: A tuple containing (is_valid, message)
"""
try:
# Validate API key format first
if not api_key:
return False, "API key is empty"
if not api_key.startswith('AIza'):
return False, "API key format appears invalid (should start with 'AIza')"
# Configure Gemini with the provided key
genai.configure(api_key=api_key)
client = genai.Client(api_key=api_key)
# Try to list models as a simple API test
models = genai.list_models()
models = client.models.list()
# Check if Gemini Pro is available
if any(model.name == "gemini-pro" for model in models):
model_names = [model.name for model in models]
logger.info(f"Available models: {model_names}")
if any("gemini" in model_name.lower() for model_name in model_names):
return True, "Gemini API key is valid"
else:
return False, "Gemini Pro model not available with this API key"
return False, "No Gemini models available with this API key"
except Exception as e:
return False, f"Error testing Gemini API key: {str(e)}"
error_msg = f"Error testing Gemini API key: {str(e)}"
logger.error(error_msg)
return False, error_msg
def gemini_pro_text_gen(prompt, temperature=0.7, top_p=0.9, top_k=40, max_tokens=2048):
"""
@@ -151,18 +195,20 @@ def gemini_pro_text_gen(prompt, temperature=0.7, top_p=0.9, top_k=40, max_tokens
str: The generated text completion
"""
try:
# Configure the model
model = genai.GenerativeModel('gemini-pro')
# Get API key with proper error handling
api_key = get_gemini_api_key()
client = genai.Client(api_key=api_key)
# Generate content
response = model.generate_content(
prompt,
generation_config=genai.types.GenerationConfig(
# Generate content using the new client
response = client.models.generate_content(
model='gemini-2.5-flash',
contents=prompt,
config=types.GenerateContentConfig(
max_output_tokens=max_tokens,
temperature=temperature,
top_p=top_p,
top_k=top_k,
max_output_tokens=max_tokens,
)
),
)
# Return the generated text
@@ -210,7 +256,10 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
Generate structured JSON response using Google's Gemini Pro model.
"""
try:
client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
# Get API key with proper error handling
api_key = get_gemini_api_key()
client = genai.Client(api_key=api_key)
logger.info("✅ Gemini client initialized for structured JSON response")
# Build config using official SDK schema type
try:
@@ -329,6 +378,10 @@ def gemini_structured_json_response(prompt, schema, temperature=0.7, top_p=0.9,
logger.error(f"Error parsing structured response: {e}")
return {"error": f"Failed to parse JSON response: {e}", "raw_response": (response.text or '')}
except ValueError as e:
# API key related errors
logger.error(f"API key error in Gemini Pro structured JSON generation: {e}")
return {"error": str(e)}
except Exception as e:
logger.error(f"Error in Gemini Pro structured JSON generation: {e}")
return {"error": str(e)}

142
backend/test_env_check.py Normal file
View File

@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
Test script to check environment variables and API key loading.
"""
import os
import sys
from pathlib import Path
# Add the backend directory to the Python path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from dotenv import load_dotenv
def test_environment_loading():
"""Test environment variable loading."""
print("🔍 Testing environment variable loading...")
# Check current working directory
print(f"Current working directory: {os.getcwd()}")
# Check if .env file exists in various locations
possible_env_paths = [
Path('.env'), # Current directory
Path('../.env'), # Parent directory
Path('../../.env'), # Grandparent directory
Path('../../../.env'), # Great-grandparent directory
Path('backend/.env'), # Backend directory
]
print("\n📁 Checking for .env files:")
for env_path in possible_env_paths:
if env_path.exists():
print(f"✅ Found .env file: {env_path.absolute()}")
else:
print(f"❌ No .env file: {env_path.absolute()}")
# Try to load .env from different locations
print("\n🔄 Attempting to load .env files:")
for env_path in possible_env_paths:
if env_path.exists():
print(f"Loading .env from: {env_path.absolute()}")
load_dotenv(env_path)
break
else:
print("⚠️ No .env file found, trying to load from current directory")
load_dotenv()
# Check environment variables
print("\n🔑 Checking environment variables:")
env_vars_to_check = [
'GEMINI_API_KEY',
'GOOGLE_API_KEY',
'OPENAI_API_KEY',
'DATABASE_URL',
'SECRET_KEY'
]
for var in env_vars_to_check:
value = os.getenv(var)
if value:
# Show first few characters for security
masked_value = value[:8] + "..." if len(value) > 8 else "***"
print(f"{var}: {masked_value}")
else:
print(f"{var}: Not set")
# Test specific Gemini API key loading
print("\n🤖 Testing Gemini API key loading:")
gemini_key = os.getenv('GEMINI_API_KEY')
if gemini_key:
print(f"✅ GEMINI_API_KEY found: {gemini_key[:8]}...")
# Test if the key looks valid
if len(gemini_key) > 20:
print("✅ API key length looks valid")
else:
print("⚠️ API key seems too short")
else:
print("❌ GEMINI_API_KEY not found")
# Check alternative names
alternative_keys = ['GOOGLE_API_KEY', 'GEMINI_KEY', 'GOOGLE_AI_API_KEY']
for alt_key in alternative_keys:
alt_value = os.getenv(alt_key)
if alt_value:
print(f"⚠️ Found alternative key {alt_key}: {alt_value[:8]}...")
return gemini_key is not None
def test_gemini_provider_import():
"""Test importing the Gemini provider."""
print("\n🧪 Testing Gemini provider import...")
try:
from services.llm_providers.gemini_provider import gemini_structured_json_response
print("✅ Successfully imported gemini_structured_json_response")
return True
except Exception as e:
print(f"❌ Failed to import Gemini provider: {e}")
return False
def test_ai_service_manager_import():
"""Test importing the AI service manager."""
print("\n🧪 Testing AI service manager import...")
try:
from services.ai_service_manager import AIServiceManager
print("✅ Successfully imported AIServiceManager")
# Try to create an instance
ai_manager = AIServiceManager()
print("✅ Successfully created AIServiceManager instance")
return True
except Exception as e:
print(f"❌ Failed to import/create AI service manager: {e}")
return False
if __name__ == "__main__":
print("🚀 Starting environment and API key validation tests")
print("=" * 60)
# Test environment loading
env_ok = test_environment_loading()
# Test imports
gemini_import_ok = test_gemini_provider_import()
ai_manager_ok = test_ai_service_manager_import()
print("\n" + "=" * 60)
print("📊 Test Results Summary:")
print(f"Environment loading: {'✅ PASS' if env_ok else '❌ FAIL'}")
print(f"Gemini provider import: {'✅ PASS' if gemini_import_ok else '❌ FAIL'}")
print(f"AI service manager: {'✅ PASS' if ai_manager_ok else '❌ FAIL'}")
if not env_ok:
print("\n💡 To fix environment issues:")
print("1. Create a .env file in the backend directory")
print("2. Add your GEMINI_API_KEY to the .env file")
print("3. Example: GEMINI_API_KEY=your_actual_api_key_here")
print("\n" + "=" * 60)

View File

@@ -0,0 +1,463 @@
#!/usr/bin/env python3
"""
Test script to validate onboarding data existence in the database.
This script checks if onboarding data exists for test users and validates the data flow.
"""
import sys
import os
import asyncio
import logging
from datetime import datetime
from typing import Dict, Any, Optional
# Add the backend directory to the Python path
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from sqlalchemy.orm import Session
from services.database import get_db_session
from models.onboarding import OnboardingSession, WebsiteAnalysis, ResearchPreferences, APIKey
from models.enhanced_strategy_models import OnboardingDataIntegration
from api.content_planning.services.content_strategy.onboarding.data_integration import OnboardingDataIntegrationService
from api.content_planning.services.content_strategy.autofill.ai_structured_autofill import AIStructuredAutofillService
from services.ai_service_manager import AIServiceManager
# Configure logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('onboarding_test.log')
]
)
logger = logging.getLogger(__name__)
class OnboardingDataValidator:
"""Validator for onboarding data existence and quality."""
def __init__(self):
self.db_session = get_db_session()
self.data_integration_service = OnboardingDataIntegrationService()
self.ai_service = AIStructuredAutofillService()
self.ai_manager = AIServiceManager()
def test_database_connection(self) -> bool:
"""Test database connection."""
try:
# Simple query to test connection
from sqlalchemy import text
result = self.db_session.execute(text("SELECT 1"))
logger.info("✅ Database connection successful")
return True
except Exception as e:
logger.error(f"❌ Database connection failed: {e}")
return False
def check_onboarding_sessions(self, user_ids: list = None) -> Dict[int, Dict[str, Any]]:
"""Check onboarding sessions for given user IDs."""
if user_ids is None:
user_ids = [1, 2, 3] # Default test user IDs
results = {}
for user_id in user_ids:
logger.info(f"🔍 Checking onboarding session for user {user_id}")
try:
session = self.db_session.query(OnboardingSession).filter(
OnboardingSession.user_id == user_id
).order_by(OnboardingSession.updated_at.desc()).first()
if session:
results[user_id] = {
'session_exists': True,
'session_id': session.id,
'status': session.status,
'progress': session.progress,
'created_at': session.created_at.isoformat(),
'updated_at': session.updated_at.isoformat(),
'data': session.to_dict() if hasattr(session, 'to_dict') else str(session)
}
logger.info(f"✅ Onboarding session found for user {user_id}: {session.status}")
else:
results[user_id] = {
'session_exists': False,
'error': 'No onboarding session found'
}
logger.warning(f"❌ No onboarding session found for user {user_id}")
except Exception as e:
results[user_id] = {
'session_exists': False,
'error': str(e)
}
logger.error(f"❌ Error checking onboarding session for user {user_id}: {e}")
return results
def check_website_analysis(self, user_ids: list = None) -> Dict[int, Dict[str, Any]]:
"""Check website analysis data for given user IDs."""
if user_ids is None:
user_ids = [1, 2, 3]
results = {}
for user_id in user_ids:
logger.info(f"🔍 Checking website analysis for user {user_id}")
try:
# Get onboarding session first
session = self.db_session.query(OnboardingSession).filter(
OnboardingSession.user_id == user_id
).order_by(OnboardingSession.updated_at.desc()).first()
if not session:
results[user_id] = {
'website_analysis_exists': False,
'error': 'No onboarding session found'
}
continue
# Get website analysis
website_analysis = self.db_session.query(WebsiteAnalysis).filter(
WebsiteAnalysis.session_id == session.id
).order_by(WebsiteAnalysis.updated_at.desc()).first()
if website_analysis:
results[user_id] = {
'website_analysis_exists': True,
'analysis_id': website_analysis.id,
'website_url': website_analysis.website_url,
'status': website_analysis.status,
'created_at': website_analysis.created_at.isoformat(),
'updated_at': website_analysis.updated_at.isoformat(),
'data_keys': list(website_analysis.to_dict().keys()) if hasattr(website_analysis, 'to_dict') else []
}
logger.info(f"✅ Website analysis found for user {user_id}: {website_analysis.website_url}")
else:
results[user_id] = {
'website_analysis_exists': False,
'error': 'No website analysis found'
}
logger.warning(f"❌ No website analysis found for user {user_id}")
except Exception as e:
results[user_id] = {
'website_analysis_exists': False,
'error': str(e)
}
logger.error(f"❌ Error checking website analysis for user {user_id}: {e}")
return results
def check_research_preferences(self, user_ids: list = None) -> Dict[int, Dict[str, Any]]:
"""Check research preferences data for given user IDs."""
if user_ids is None:
user_ids = [1, 2, 3]
results = {}
for user_id in user_ids:
logger.info(f"🔍 Checking research preferences for user {user_id}")
try:
# Get onboarding session first
session = self.db_session.query(OnboardingSession).filter(
OnboardingSession.user_id == user_id
).order_by(OnboardingSession.updated_at.desc()).first()
if not session:
results[user_id] = {
'research_preferences_exists': False,
'error': 'No onboarding session found'
}
continue
# Get research preferences
research_prefs = self.db_session.query(ResearchPreferences).filter(
ResearchPreferences.session_id == session.id
).first()
if research_prefs:
results[user_id] = {
'research_preferences_exists': True,
'prefs_id': research_prefs.id,
'research_depth': research_prefs.research_depth,
'content_types': research_prefs.content_types,
'created_at': research_prefs.created_at.isoformat(),
'updated_at': research_prefs.updated_at.isoformat(),
'data_keys': list(research_prefs.to_dict().keys()) if hasattr(research_prefs, 'to_dict') else []
}
logger.info(f"✅ Research preferences found for user {user_id}: {research_prefs.research_depth}")
else:
results[user_id] = {
'research_preferences_exists': False,
'error': 'No research preferences found'
}
logger.warning(f"❌ No research preferences found for user {user_id}")
except Exception as e:
results[user_id] = {
'research_preferences_exists': False,
'error': str(e)
}
logger.error(f"❌ Error checking research preferences for user {user_id}: {e}")
return results
def check_api_keys(self, user_ids: list = None) -> Dict[int, Dict[str, Any]]:
"""Check API keys data for given user IDs."""
if user_ids is None:
user_ids = [1, 2, 3]
results = {}
for user_id in user_ids:
logger.info(f"🔍 Checking API keys for user {user_id}")
try:
# Get onboarding session first
session = self.db_session.query(OnboardingSession).filter(
OnboardingSession.user_id == user_id
).order_by(OnboardingSession.updated_at.desc()).first()
if not session:
results[user_id] = {
'api_keys_exist': False,
'error': 'No onboarding session found'
}
continue
# Get API keys
api_keys = self.db_session.query(APIKey).filter(
APIKey.session_id == session.id
).all()
if api_keys:
results[user_id] = {
'api_keys_exist': True,
'count': len(api_keys),
'providers': [key.provider for key in api_keys],
'created_at': api_keys[0].created_at.isoformat() if api_keys else None,
'updated_at': api_keys[0].updated_at.isoformat() if api_keys else None
}
logger.info(f"✅ API keys found for user {user_id}: {len(api_keys)} keys")
else:
results[user_id] = {
'api_keys_exist': False,
'error': 'No API keys found'
}
logger.warning(f"❌ No API keys found for user {user_id}")
except Exception as e:
results[user_id] = {
'api_keys_exist': False,
'error': str(e)
}
logger.error(f"❌ Error checking API keys for user {user_id}: {e}")
return results
async def test_data_integration_service(self, user_id: int = 1) -> Dict[str, Any]:
"""Test the data integration service."""
logger.info(f"🔍 Testing data integration service for user {user_id}")
try:
# Test the process_onboarding_data method
integrated_data = await self.data_integration_service.process_onboarding_data(user_id, self.db_session)
if integrated_data:
result = {
'success': True,
'has_website_analysis': bool(integrated_data.get('website_analysis')),
'has_research_preferences': bool(integrated_data.get('research_preferences')),
'has_api_keys_data': bool(integrated_data.get('api_keys_data')),
'has_onboarding_session': bool(integrated_data.get('onboarding_session')),
'data_quality': integrated_data.get('data_quality', {}),
'processing_timestamp': integrated_data.get('processing_timestamp'),
'context_keys': list(integrated_data.keys())
}
logger.info(f"✅ Data integration successful for user {user_id}")
logger.info(f" Website analysis: {result['has_website_analysis']}")
logger.info(f" Research preferences: {result['has_research_preferences']}")
logger.info(f" API keys: {result['has_api_keys_data']}")
logger.info(f" Onboarding session: {result['has_onboarding_session']}")
return result
else:
logger.error(f"❌ Data integration returned None for user {user_id}")
return {'success': False, 'error': 'No data returned'}
except Exception as e:
logger.error(f"❌ Data integration failed for user {user_id}: {e}")
return {'success': False, 'error': str(e)}
async def test_ai_service_configuration(self) -> Dict[str, Any]:
"""Test AI service configuration."""
logger.info("🔍 Testing AI service configuration")
try:
# Test basic AI service functionality
test_prompt = "Generate a simple test response"
test_schema = {
"type": "OBJECT",
"properties": {
"test_field": {"type": "STRING", "description": "A test field"}
},
"required": ["test_field"]
}
# Test the AI service manager
result = await self.ai_manager.execute_structured_json_call(
service_type="STRATEGIC_INTELLIGENCE",
prompt=test_prompt,
schema=test_schema
)
if result and not result.get('error'):
logger.info("✅ AI service configuration successful")
return {
'success': True,
'ai_service_working': True,
'test_response': result
}
else:
logger.error(f"❌ AI service test failed: {result.get('error', 'Unknown error')}")
return {
'success': False,
'ai_service_working': False,
'error': result.get('error', 'Unknown error')
}
except Exception as e:
logger.error(f"❌ AI service configuration test failed: {e}")
return {
'success': False,
'ai_service_working': False,
'error': str(e)
}
async def test_ai_structured_autofill(self, user_id: int = 1) -> Dict[str, Any]:
"""Test the AI structured autofill service."""
logger.info(f"🔍 Testing AI structured autofill for user {user_id}")
try:
# First get the context
integrated_data = await self.data_integration_service.process_onboarding_data(user_id, self.db_session)
if not integrated_data:
logger.error(f"❌ No integrated data available for user {user_id}")
return {'success': False, 'error': 'No integrated data available'}
# Test the AI structured autofill
result = await self.ai_service.generate_autofill_fields(user_id, integrated_data)
if result:
meta = result.get('meta', {})
fields = result.get('fields', {})
test_result = {
'success': True,
'ai_used': meta.get('ai_used', False),
'ai_overrides_count': meta.get('ai_overrides_count', 0),
'success_rate': meta.get('success_rate', 0),
'attempts': meta.get('attempts', 0),
'missing_fields': meta.get('missing_fields', []),
'fields_generated': len(fields),
'sample_fields': list(fields.keys())[:5] if fields else []
}
logger.info(f"✅ AI structured autofill test completed for user {user_id}")
logger.info(f" AI used: {test_result['ai_used']}")
logger.info(f" Fields generated: {test_result['fields_generated']}")
logger.info(f" Success rate: {test_result['success_rate']:.1f}%")
logger.info(f" Attempts: {test_result['attempts']}")
return test_result
else:
logger.error(f"❌ AI structured autofill returned None for user {user_id}")
return {'success': False, 'error': 'No result returned'}
except Exception as e:
logger.error(f"❌ AI structured autofill test failed for user {user_id}: {e}")
return {'success': False, 'error': str(e)}
def print_summary(self, results: Dict[str, Any]):
"""Print a summary of all test results."""
logger.info("\n" + "="*80)
logger.info("📊 ONBOARDING DATA VALIDATION SUMMARY")
logger.info("="*80)
for test_name, result in results.items():
logger.info(f"\n🔍 {test_name.upper()}:")
if isinstance(result, dict):
for key, value in result.items():
if isinstance(value, dict):
logger.info(f" {key}:")
for sub_key, sub_value in value.items():
logger.info(f" {sub_key}: {sub_value}")
else:
logger.info(f" {key}: {value}")
else:
logger.info(f" {result}")
logger.info("\n" + "="*80)
def cleanup(self):
"""Clean up database session."""
if self.db_session:
self.db_session.close()
async def main():
"""Main test function."""
logger.info("🚀 Starting onboarding data validation tests")
validator = OnboardingDataValidator()
try:
# Test database connection
db_connected = validator.test_database_connection()
if not db_connected:
logger.error("❌ Cannot proceed without database connection")
return
# Test user IDs to check
test_user_ids = [1, 2, 3]
# Run all tests
results = {
'database_connection': db_connected,
'onboarding_sessions': validator.check_onboarding_sessions(test_user_ids),
'website_analysis': validator.check_website_analysis(test_user_ids),
'research_preferences': validator.check_research_preferences(test_user_ids),
'api_keys': validator.check_api_keys(test_user_ids),
'data_integration': await validator.test_data_integration_service(1),
'ai_service_config': await validator.test_ai_service_configuration(),
'ai_structured_autofill': await validator.test_ai_structured_autofill(1)
}
# Print summary
validator.print_summary(results)
# Determine overall status
overall_success = all([
results['database_connection'],
any(session.get('session_exists', False) for session in results['onboarding_sessions'].values()),
results['data_integration']['success'],
results['ai_service_config']['success']
])
if overall_success:
logger.info("✅ All critical tests passed!")
else:
logger.error("❌ Some critical tests failed!")
except Exception as e:
logger.error(f"❌ Test execution failed: {e}")
finally:
validator.cleanup()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,446 @@
# ALwrity It - Content Strategy Analysis Customization Feature
## 🎯 **Feature Overview**
**ALwrity It** allows users to customize AI-generated analysis components when they don't meet expectations. Users can manually edit data or use AI to regenerate with custom prompts, maintaining context from other analysis components.
### **Key Benefits:**
-**User Control**: Full control over AI-generated analysis
-**Flexibility**: Manual editing or AI-powered regeneration
-**Context Awareness**: AI considers other analysis components
-**Structured Output**: Consistent JSON responses via Gemini
-**Version History**: Track and revert changes
-**Preview Mode**: Compare original vs modified analysis
## 🏗️ **Technical Architecture**
### **File Structure**
```
frontend/src/components/ContentPlanningDashboard/components/StrategyIntelligence/
├── components/
│ ├── content_strategy_alwrityit/
│ │ ├── ALwrityItButton.tsx # Main button component
│ │ ├── ALwrityItModal.tsx # Main modal container
│ │ ├── ManualEditForm.tsx # Manual editing form
│ │ ├── AIEditForm.tsx # AI prompt form
│ │ ├── QuickRegenerateForm.tsx # Quick AI regeneration
│ │ ├── AnalysisPreview.tsx # Preview changes
│ │ ├── ModeSelector.tsx # Mode selection interface
│ │ ├── VersionHistory.tsx # Version tracking
│ │ └── TemplateLibrary.tsx # Saved templates
│ └── [existing analysis cards]
├── hooks/
│ ├── content_strategy_alwrityit/
│ │ ├── useALwrityIt.ts # Main hook for ALwrity It functionality
│ │ ├── useAnalysisRegeneration.ts # AI regeneration logic
│ │ ├── useManualEditing.ts # Manual editing logic
│ │ └── useVersionHistory.ts # Version management
├── types/
│ ├── content_strategy_alwrityit/
│ │ ├── alwrityIt.types.ts # TypeScript types
│ │ ├── analysisSchemas.ts # JSON schemas for each component
│ │ └── promptTemplates.ts # AI prompt templates
├── utils/
│ ├── content_strategy_alwrityit/
│ │ ├── analysisTransformers.ts # Data transformation utilities
│ │ ├── promptGenerators.ts # AI prompt generation
│ │ ├── schemaValidators.ts # JSON schema validation
│ │ └── versionManager.ts # Version control utilities
└── providers/
└── ALwrityItProvider.tsx # Context provider for state management
```
### **Backend Structure**
```
backend/api/content_planning/api/content_strategy/
├── endpoints/
│ ├── alwrityit_endpoints.py # ALwrity It API endpoints
│ └── [existing endpoints]
├── services/
│ ├── alwrityit_service.py # ALwrity It business logic
│ ├── analysis_regeneration_service.py # AI regeneration service
│ └── version_management_service.py # Version control service
└── models/
├── alwrityit_models.py # Database models for versions/templates
└── [existing models]
```
## 📋 **Implementation Phases**
### **Phase 1: Core Infrastructure (2-3 days)**
#### **1.1 Backend API Endpoints**
```python
# backend/api/content_planning/api/content_strategy/endpoints/alwrityit_endpoints.py
@router.post("/regenerate-analysis-component")
async def regenerate_analysis_component(request: RegenerateAnalysisRequest):
"""Regenerate specific analysis component with AI"""
@router.post("/update-analysis-component-manual")
async def update_analysis_component_manual(request: ManualUpdateRequest):
"""Update analysis component with manual edits"""
@router.get("/analysis-component-schema/{component_type}")
async def get_analysis_component_schema(component_type: str):
"""Get JSON schema for specific component type"""
@router.get("/analysis-versions/{strategy_id}/{component_type}")
async def get_analysis_versions(strategy_id: int, component_type: str):
"""Get version history for analysis component"""
```
#### **1.2 Frontend Core Components**
```typescript
// ALwrityItButton.tsx
const ALwrityItButton = ({ componentType, currentData, onUpdate }) => {
return (
<IconButton
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
'&:hover': { transform: 'scale(1.1)' },
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
boxShadow: '0 4px 12px rgba(102, 126, 234, 0.3)',
}}
onClick={() => setModalOpen(true)}
>
<AutoAwesomeIcon />
</IconButton>
);
};
```
### **Phase 2: Modal & Mode Selection (1-2 days)**
#### **2.1 Main Modal Component**
```typescript
// ALwrityItModal.tsx
const ALwrityItModal = ({ open, onClose, componentType, currentData, onUpdate }) => {
const [mode, setMode] = useState<ALwrityItMode>('manual');
return (
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
<DialogTitle>ALwrity It - {getComponentDisplayName(componentType)}</DialogTitle>
<DialogContent>
<ModeSelector mode={mode} onModeChange={setMode} />
{mode === 'manual' && (
<ManualEditForm componentType={componentType} currentData={currentData} />
)}
{mode === 'ai' && (
<AIEditForm componentType={componentType} currentData={currentData} />
)}
{mode === 'regenerate' && (
<QuickRegenerateForm componentType={componentType} />
)}
</DialogContent>
</Dialog>
);
};
```
#### **2.2 Mode Selector Component**
```typescript
// ModeSelector.tsx
const ModeSelector = ({ mode, onModeChange }) => {
const modes = [
{
id: 'manual',
title: 'Manual Edit',
description: 'Edit analysis data manually',
icon: <EditIcon />,
color: '#4caf50'
},
{
id: 'ai',
title: 'AI Custom',
description: 'Provide custom prompt for AI regeneration',
icon: <AutoAwesomeIcon />,
color: '#667eea'
},
{
id: 'regenerate',
title: 'Quick Regenerate',
description: 'Regenerate with improved AI analysis',
icon: <RefreshIcon />,
color: '#ff9800'
}
];
return (
<Grid container spacing={2}>
{modes.map((modeOption) => (
<Grid item xs={12} sm={4} key={modeOption.id}>
<Card onClick={() => onModeChange(modeOption.id)}>
<CardContent>
<Box sx={{ color: modeOption.color }}>{modeOption.icon}</Box>
<Typography variant="subtitle1">{modeOption.title}</Typography>
<Typography variant="caption">{modeOption.description}</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
);
};
```
### **Phase 3: Manual Editing Interface (1-2 days)**
#### **3.1 Manual Edit Form**
```typescript
// ManualEditForm.tsx
const ManualEditForm = ({ componentType, currentData, onSave }) => {
const schema = useAnalysisSchema(componentType);
const [formData, setFormData] = useState(currentData);
return (
<Box>
<Typography variant="h6">Manual Edit - {getComponentDisplayName(componentType)}</Typography>
{Object.entries(schema.properties).map(([field, fieldSchema]) => (
<DynamicFormField
key={field}
field={field}
schema={fieldSchema}
value={formData[field]}
onChange={(value) => setFormData(prev => ({ ...prev, [field]: value }))}
/>
))}
<Box sx={{ mt: 2, display: 'flex', gap: 2 }}>
<Button variant="outlined" onClick={() => setFormData(currentData)}>
Reset to Original
</Button>
<Button variant="contained" onClick={() => onSave(formData)}>
Save Changes
</Button>
</Box>
</Box>
);
};
```
### **Phase 4: AI Integration (2-3 days)**
#### **4.1 AI Edit Form**
```typescript
// AIEditForm.tsx
const AIEditForm = ({ componentType, currentData, onGenerate }) => {
const [prompt, setPrompt] = useState('');
const [suggestedPrompts, setSuggestedPrompts] = useState([]);
return (
<Box>
<Typography variant="h6">AI Custom Regeneration</Typography>
<TextField
fullWidth
multiline
rows={4}
label="Custom AI Prompt"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe how you want to improve this analysis..."
/>
<Box sx={{ mt: 2 }}>
{suggestedPrompts.map((suggestion, index) => (
<Chip
key={index}
label={suggestion}
onClick={() => setPrompt(suggestion)}
sx={{ mr: 1, mb: 1 }}
/>
))}
</Box>
<Button
variant="contained"
onClick={() => onGenerate(prompt)}
disabled={!prompt.trim()}
startIcon={<AutoAwesomeIcon />}
>
Generate with AI
</Button>
</Box>
);
};
```
#### **4.2 Backend AI Service**
```python
# backend/services/alwrityit_service.py
class ALwrityItService:
async def regenerate_analysis_component(
self,
component_type: str,
current_data: dict,
user_prompt: str = None,
context_data: dict = None
) -> dict:
prompt = self._build_regeneration_prompt(
component_type, current_data, user_prompt, context_data
)
schema = self._get_component_schema(component_type)
response = await self.gemini_provider.generate_structured_response(
prompt=prompt,
schema=schema,
context={
"current_analysis": current_data,
"other_components": context_data,
"user_requirements": user_prompt,
"component_type": component_type
}
)
return response
```
### **Phase 5: Preview & Version Management (1-2 days)**
#### **5.1 Analysis Preview Component**
```typescript
// AnalysisPreview.tsx
const AnalysisPreview = ({ original, modified, componentType, onApply, onRevert }) => {
return (
<Box>
<Typography variant="h6">Preview Changes</Typography>
<Grid container spacing={2}>
<Grid item xs={6}>
<Typography variant="subtitle2">Original Analysis</Typography>
<AnalysisCard data={original} componentType={componentType} />
</Grid>
<Grid item xs={6}>
<Typography variant="subtitle2">Modified Analysis</Typography>
<AnalysisCard data={modified} componentType={componentType} />
</Grid>
</Grid>
<Box sx={{ mt: 2, display: 'flex', gap: 2 }}>
<Button variant="outlined" onClick={onRevert}>Revert Changes</Button>
<Button variant="contained" onClick={onApply}>Apply Changes</Button>
</Box>
</Box>
);
};
```
## 🎨 **UI/UX Design Specifications**
### **Color Scheme**
```typescript
const ALWRITY_IT_COLORS = {
primary: '#667eea',
secondary: '#764ba2',
success: '#4caf50',
warning: '#ff9800',
error: '#f44336',
background: {
modal: 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%)',
card: 'rgba(255, 255, 255, 0.05)',
button: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
}
};
```
## 🔧 **Database Schema**
### **Version History Table**
```sql
CREATE TABLE analysis_versions (
id SERIAL PRIMARY KEY,
strategy_id INTEGER NOT NULL,
component_type VARCHAR(50) NOT NULL,
version_data JSONB NOT NULL,
change_type VARCHAR(20) NOT NULL,
user_prompt TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER,
description TEXT
);
```
### **Templates Table**
```sql
CREATE TABLE analysis_templates (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
component_type VARCHAR(50) NOT NULL,
template_data JSONB NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER,
is_public BOOLEAN DEFAULT FALSE
);
```
## 🚀 **Implementation Timeline**
### **Week 1: Core Infrastructure**
- **Day 1-2**: Backend API endpoints and database models
- **Day 3-4**: Frontend component structure and basic modal
- **Day 5**: Integration with existing analysis cards
### **Week 2: AI Integration**
- **Day 1-2**: Gemini structured response integration
- **Day 3-4**: Prompt engineering and context handling
- **Day 5**: Testing and refinement
### **Week 3: Manual Editing & Polish**
- **Day 1-2**: Dynamic form generation and validation
- **Day 3-4**: Preview and comparison features
- **Day 5**: Version history and advanced features
## 🧪 **Testing Strategy**
### **Unit Tests**
- Component rendering and interactions
- Form validation and data transformation
- AI prompt generation and response parsing
### **Integration Tests**
- API endpoint functionality
- Database operations
- AI service integration
### **End-to-End Tests**
- Complete user workflows
- Error handling scenarios
- Performance testing
## 📊 **Success Metrics**
### **User Engagement**
- Number of ALwrity It button clicks per analysis
- Most frequently modified components
- User satisfaction with customization options
### **Technical Performance**
- AI generation response times
- Modal load times
- Error rates and recovery
## 🔄 **Future Enhancements**
### **Phase 2 Features**
1. **Collaboration Tools**: Team comments and approvals
2. **Advanced AI**: Multi-step regeneration with user feedback
3. **Integration**: Connect with external data sources
4. **Analytics**: Detailed usage analytics and insights
5. **Templates**: Community template sharing
---
**Next Steps**:
1. Review and approve this implementation plan
2. Set up development environment
3. Begin Phase 1 implementation
4. Create project milestones and tracking
5. Set up testing infrastructure

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/static/css/main.c9966057.css",
"main.js": "/static/js/main.2819e23e.js",
"main.js": "/static/js/main.3e924b71.js",
"index.html": "/index.html",
"main.c9966057.css.map": "/static/css/main.c9966057.css.map",
"main.2819e23e.js.map": "/static/js/main.2819e23e.js.map"
"main.3e924b71.js.map": "/static/js/main.3e924b71.js.map"
},
"entrypoints": [
"static/css/main.c9966057.css",
"static/js/main.2819e23e.js"
"static/js/main.3e924b71.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Alwrity - AI Content Creation Platform"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Alwrity - AI Content Creation Platform</title><script defer="defer" src="/static/js/main.2819e23e.js"></script><link href="/static/css/main.c9966057.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Alwrity - AI Content Creation Platform"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Alwrity - AI Content Creation Platform</title><script defer="defer" src="/static/js/main.3e924b71.js"></script><link href="/static/css/main.c9966057.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

View File

@@ -19,13 +19,15 @@ import {
Analytics as AnalyticsIcon,
Search as SearchIcon,
Lightbulb as AIInsightsIcon,
Close as CloseIcon
Close as CloseIcon,
Add as CreateIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
import ContentStrategyTab from './tabs/ContentStrategyTab';
import CalendarTab from './tabs/CalendarTab';
import AnalyticsTab from './tabs/AnalyticsTab';
import GapAnalysisTab from './tabs/GapAnalysisTab';
import CreateTab from './tabs/CreateTab';
import AIInsightsPanel from './components/AIInsightsPanel';
import ServiceStatusPanel from './components/ServiceStatusPanel';
import ProgressIndicator from './components/ProgressIndicator';
@@ -170,7 +172,8 @@ const ContentPlanningDashboard: React.FC = () => {
{ label: 'CONTENT STRATEGY', icon: <StrategyIcon />, component: <ContentStrategyTab /> },
{ label: 'CALENDAR', icon: <CalendarIcon />, component: <CalendarTab /> },
{ label: 'ANALYTICS', icon: <AnalyticsIcon />, component: <AnalyticsTab /> },
{ label: 'GAP ANALYSIS', icon: <SearchIcon />, component: <GapAnalysisTab /> }
{ label: 'GAP ANALYSIS', icon: <SearchIcon />, component: <GapAnalysisTab /> },
{ label: 'CREATE', icon: <CreateIcon />, component: <CreateTab /> }
];
const totalAIItems = (dashboardData.aiInsights?.length || 0) + (dashboardData.aiRecommendations?.length || 0);

View File

@@ -478,7 +478,7 @@ const ContentStrategyBuilder: React.FC = () => {
onUpdateFormField={updateFormField}
onValidateFormField={validateFormField}
onShowTooltip={setShowTooltip}
onViewDataSource={() => setShowDataSourceTransparency(true)}
onViewDataSource={() => setShowDataSourceTransparency(true)}
onConfirmCategoryReview={handleConfirmCategoryReviewWrapper}
onSetActiveCategory={setActiveCategory}
onSetShowEducationalInfo={setShowEducationalInfo}

View File

@@ -0,0 +1,615 @@
import React, { useState } from 'react';
import {
Dialog,
DialogContent,
DialogTitle,
Box,
Typography,
Button,
Stepper,
Step,
StepLabel,
StepContent,
Card,
CardContent,
Grid,
Chip,
IconButton,
LinearProgress,
Alert,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider
} from '@mui/material';
import {
Close as CloseIcon,
CheckCircle as CheckCircleIcon,
AutoAwesome as AutoAwesomeIcon,
Psychology as PsychologyIcon,
CalendarToday as CalendarIcon,
Analytics as AnalyticsIcon,
TrendingUp as TrendingUpIcon,
Lightbulb as LightbulbIcon,
School as SchoolIcon,
Rocket as RocketIcon,
Security as SecurityIcon,
Speed as SpeedIcon,
Group as GroupIcon,
Timeline as TimelineIcon,
Assessment as AssessmentIcon,
PlayArrow as PlayArrowIcon,
Pause as PauseIcon,
Refresh as RefreshIcon,
Edit as EditIcon,
Add as AddIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
interface StrategyOnboardingDialogProps {
open: boolean;
onClose: () => void;
onConfirmStrategy: () => void;
onEditStrategy: () => void;
onCreateNewStrategy: () => void;
currentStrategy: any;
strategyStatus: 'active' | 'inactive' | 'none';
}
const StrategyOnboardingDialog: React.FC<StrategyOnboardingDialogProps> = ({
open,
onClose,
onConfirmStrategy,
onEditStrategy,
onCreateNewStrategy,
currentStrategy,
strategyStatus
}) => {
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const steps = [
{
label: 'Welcome to ALwrity',
icon: <AutoAwesomeIcon />,
content: (
<Box>
<Typography variant="h5" gutterBottom sx={{ fontWeight: 600, color: 'primary.main' }}>
🚀 Your AI-Powered Content Strategy Copilot
</Typography>
<Typography variant="body1" paragraph>
ALwrity democratizes professional content strategy and calendar creation, making it accessible to solopreneurs and small businesses.
</Typography>
<Grid container spacing={2} sx={{ mt: 2 }}>
<Grid item xs={12} md={6}>
<Card sx={{ background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: 'white' }}>
<CardContent>
<Typography variant="h6" gutterBottom>
<PsychologyIcon sx={{ mr: 1 }} />
AI-Enhanced Strategy
</Typography>
<Typography variant="body2">
Our AI analyzes your business, competitors, and market trends to create a comprehensive content strategy.
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card sx={{ background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', color: 'white' }}>
<CardContent>
<Typography variant="h6" gutterBottom>
<CalendarIcon sx={{ mr: 1 }} />
Smart Calendar Creation
</Typography>
<Typography variant="body2">
Automatically generate content calendars with optimal posting times and content mix.
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
)
},
{
label: 'What ALwrity Has Done',
icon: <AssessmentIcon />,
content: (
<Box>
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
📊 Comprehensive Research & Analysis
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom color="primary">
<SchoolIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Market Research
</Typography>
<List dense>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Industry trend analysis" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Competitor content audit" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Target audience insights" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Keyword opportunity analysis" />
</ListItem>
</List>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom color="primary">
<LightbulbIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Strategic Insights
</Typography>
<List dense>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Content pillar identification" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Optimal content mix" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Posting frequency recommendations" />
</ListItem>
<ListItem>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary="Performance predictions" />
</ListItem>
</List>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
)
},
{
label: 'Your Journey with ALwrity',
icon: <TimelineIcon />,
content: (
<Box>
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
🎯 Your 4-Step Success Path
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card sx={{ border: '2px solid', borderColor: 'primary.main' }}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Typography variant="h4" sx={{ mr: 2, color: 'primary.main' }}>1</Typography>
<Typography variant="h6">Review & Confirm Strategy</Typography>
</Box>
<Typography variant="body2" paragraph>
Review the AI-generated content strategy tailored to your business. Make any adjustments and confirm to activate.
</Typography>
<Chip label="Current Step" color="primary" size="small" />
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Typography variant="h4" sx={{ mr: 2, color: 'text.secondary' }}>2</Typography>
<Typography variant="h6">Create Content Calendar</Typography>
</Box>
<Typography variant="body2" paragraph>
ALwrity generates a comprehensive content calendar with optimal posting times and content themes.
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Typography variant="h4" sx={{ mr: 2, color: 'text.secondary' }}>3</Typography>
<Typography variant="h6">Measure Strategy KPIs</Typography>
</Box>
<Typography variant="body2" paragraph>
Track performance metrics and analyze content effectiveness to understand what works best.
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<Typography variant="h4" sx={{ mr: 2, color: 'text.secondary' }}>4</Typography>
<Typography variant="h6">Optimize with AI</Typography>
</Box>
<Typography variant="body2" paragraph>
Continuously improve your strategy based on performance data and AI recommendations.
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
)
},
{
label: 'ALwrity as Your Copilot',
icon: <RocketIcon />,
content: (
<Box>
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
🤖 Your AI Marketing Assistant
</Typography>
<Typography variant="body1" paragraph>
Once your strategy is active, ALwrity becomes your 24/7 content marketing copilot, handling the heavy lifting while you focus on your business.
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<Card sx={{ height: '100%', background: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' }}>
<CardContent>
<Typography variant="h6" gutterBottom>
<SpeedIcon sx={{ mr: 1 }} />
Automated Execution
</Typography>
<Typography variant="body2">
ALwrity schedules, generates, reviews, and posts content according to your strategy, saving you hours every week.
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={4}>
<Card sx={{ height: '100%', background: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)' }}>
<CardContent>
<Typography variant="h6" gutterBottom>
<TrendingUpIcon sx={{ mr: 1 }} />
Performance Tracking
</Typography>
<Typography variant="body2">
Monitor your content performance in real-time with detailed analytics and actionable insights.
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={4}>
<Card sx={{ height: '100%', background: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)' }}>
<CardContent>
<Typography variant="h6" gutterBottom>
<SecurityIcon sx={{ mr: 1 }} />
Quality Assurance
</Typography>
<Typography variant="body2">
Every piece of content is reviewed for quality, brand consistency, and strategic alignment.
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
<Alert severity="info" sx={{ mt: 3 }}>
<Typography variant="body2">
<strong>Pro Tip:</strong> ALwrity learns from your content performance and continuously optimizes your strategy for better results.
</Typography>
</Alert>
</Box>
)
},
{
label: 'Take Action',
icon: <PlayArrowIcon />,
content: (
<Box>
<Typography variant="h6" gutterBottom sx={{ fontWeight: 600 }}>
🎯 Ready to Activate Your Strategy?
</Typography>
{strategyStatus === 'inactive' && currentStrategy ? (
<Box>
<Alert severity="warning" sx={{ mb: 3 }}>
<Typography variant="body1">
<strong>Strategy Found:</strong> We found an existing strategy that needs to be activated.
</Typography>
</Alert>
<Card sx={{ mb: 3, border: '2px solid', borderColor: 'warning.main' }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Current Strategy: {currentStrategy.name}
</Typography>
<Typography variant="body2" paragraph>
{currentStrategy.description}
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip label={`Industry: ${currentStrategy.industry}`} color="primary" variant="outlined" />
<Chip label={`Target: ${currentStrategy.target_audience}`} color="secondary" variant="outlined" />
</Box>
</CardContent>
</Card>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Button
fullWidth
variant="contained"
size="large"
onClick={onConfirmStrategy}
startIcon={<CheckCircleIcon />}
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'&:hover': { transform: 'translateY(-2px)' },
transition: 'all 0.3s ease'
}}
>
Activate Strategy
</Button>
</Grid>
<Grid item xs={12} md={4}>
<Button
fullWidth
variant="outlined"
size="large"
onClick={onEditStrategy}
startIcon={<EditIcon />}
>
Edit Strategy
</Button>
</Grid>
<Grid item xs={12} md={4}>
<Button
fullWidth
variant="outlined"
size="large"
onClick={onCreateNewStrategy}
startIcon={<AddIcon />}
>
Create New
</Button>
</Grid>
</Grid>
</Box>
) : strategyStatus === 'none' ? (
<Box>
<Alert severity="info" sx={{ mb: 3 }}>
<Typography variant="body1">
<strong>No Strategy Found:</strong> Let's create your first content strategy!
</Typography>
</Alert>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Button
fullWidth
variant="contained"
size="large"
onClick={onCreateNewStrategy}
startIcon={<AutoAwesomeIcon />}
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'&:hover': { transform: 'translateY(-2px)' },
transition: 'all 0.3s ease'
}}
>
Create Strategy with AI
</Button>
</Grid>
<Grid item xs={12} md={6}>
<Button
fullWidth
variant="outlined"
size="large"
onClick={onClose}
startIcon={<CloseIcon />}
>
Maybe Later
</Button>
</Grid>
</Grid>
</Box>
) : (
<Box>
<Alert severity="success" sx={{ mb: 3 }}>
<Typography variant="body1">
<strong>Strategy Active:</strong> Your content strategy is already active and running!
</Typography>
</Alert>
<Button
fullWidth
variant="contained"
size="large"
onClick={onClose}
startIcon={<CheckCircleIcon />}
sx={{
background: 'linear-gradient(135deg, #4caf50 0%, #45a049 100%)',
'&:hover': { transform: 'translateY(-2px)' },
transition: 'all 0.3s ease'
}}
>
Continue to Dashboard
</Button>
</Box>
)}
</Box>
)
}
];
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleReset = () => {
setActiveStep(0);
};
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="lg"
fullWidth
PaperProps={{
sx: {
background: 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 100%)',
color: 'white',
borderRadius: 3,
minHeight: '80vh'
}
}}
>
<DialogTitle sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
color: 'white',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<AutoAwesomeIcon sx={{ mr: 2, fontSize: 32 }} />
<Typography variant="h5" sx={{ fontWeight: 600 }}>
ALwrity Content Strategy Onboarding
</Typography>
</Box>
<IconButton onClick={onClose} sx={{ color: 'white' }}>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ p: 4 }}>
<Box sx={{ maxWidth: 1200, mx: 'auto' }}>
<Stepper activeStep={activeStep} orientation="vertical" sx={{ mb: 4 }}>
{steps.map((step, index) => (
<Step key={step.label}>
<StepLabel
StepIconComponent={() => (
<Box sx={{
width: 40,
height: 40,
borderRadius: '50%',
background: activeStep >= index ? 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' : 'rgba(255,255,255,0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white'
}}>
{step.icon}
</Box>
)}
>
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
{step.label}
</Typography>
</StepLabel>
<StepContent>
<AnimatePresence mode="wait">
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
<Box sx={{ mb: 3 }}>
{step.content}
</Box>
{index < steps.length - 1 && (
<Box sx={{ display: 'flex', gap: 2 }}>
<Button
variant="contained"
onClick={handleNext}
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'&:hover': { transform: 'translateY(-2px)' },
transition: 'all 0.3s ease'
}}
>
{index === steps.length - 2 ? 'Finish' : 'Continue'}
</Button>
<Button
disabled={index === 0}
onClick={handleBack}
variant="outlined"
sx={{ color: 'white', borderColor: 'white' }}
>
Back
</Button>
</Box>
)}
</motion.div>
</AnimatePresence>
</StepContent>
</Step>
))}
</Stepper>
{activeStep === steps.length && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
<Box sx={{ textAlign: 'center', py: 4 }}>
<CheckCircleIcon sx={{ fontSize: 64, color: '#4caf50', mb: 2 }} />
<Typography variant="h5" gutterBottom>
Welcome to ALwrity!
</Typography>
<Typography variant="body1" paragraph>
You're now ready to start your content marketing journey with AI assistance.
</Typography>
<Button
variant="contained"
onClick={handleReset}
sx={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'&:hover': { transform: 'translateY(-2px)' },
transition: 'all 0.3s ease'
}}
>
Start Over
</Button>
</Box>
</motion.div>
)}
</Box>
</DialogContent>
</Dialog>
);
};
export default StrategyOnboardingDialog;

View File

@@ -8,7 +8,6 @@ import {
TextField,
Card,
CardContent,
CardActions,
Chip,
IconButton,
Dialog,
@@ -42,7 +41,6 @@ import {
CalendarToday as CalendarIcon,
Event as EventIcon,
Refresh as RefreshIcon,
AutoAwesome as AIIcon,
TrendingUp as TrendingIcon,
ContentCopy as RepurposeIcon,
Analytics as AnalyticsIcon,
@@ -64,7 +62,6 @@ import {
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
import CalendarGenerationWizard from '../components/CalendarGenerationWizard';
interface TabPanelProps {
children?: React.ReactNode;
@@ -100,10 +97,8 @@ const CalendarTab: React.FC = () => {
updateCalendarEvents,
// New calendar generation state
generatedCalendar,
contentOptimization,
performancePrediction,
contentRepurposing,
trendingTopics,
aiInsights,
calendarGenerationError,
dataLoading
@@ -131,7 +126,7 @@ const CalendarTab: React.FC = () => {
aiAnalysisResults: []
});
const [calendarGenerationMode, setCalendarGenerationMode] = useState<'transparency' | 'wizard'>('transparency');
const safeCalendarEvents = Array.isArray(calendarEvents) ? calendarEvents : [];
useEffect(() => {
loadCalendarData();
@@ -214,22 +209,6 @@ const CalendarTab: React.FC = () => {
await loadCalendarData();
};
const handleGenerateAICalendar = async () => {
try {
// This will now use the comprehensive data from the transparency dashboard
const calendarConfig = {
userData,
calendarType: 'monthly',
industry: userData.onboardingData?.industry || 'technology',
businessSize: 'sme'
};
await contentPlanningApi.generateComprehensiveCalendar(calendarConfig);
} catch (error) {
console.error('Error generating AI calendar:', error);
}
};
const handleDataUpdate = (updatedData: any) => {
setUserData((prev: any) => ({ ...prev, ...updatedData }));
};
@@ -278,9 +257,6 @@ const CalendarTab: React.FC = () => {
}
};
// Ensure calendarEvents is always an array
const safeCalendarEvents = Array.isArray(calendarEvents) ? calendarEvents : [];
return (
<Box sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
@@ -321,9 +297,6 @@ const CalendarTab: React.FC = () => {
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)}>
<Tab label="Calendar Events" icon={<CalendarIcon />} iconPosition="start" />
<Tab label="Calendar Wizard" icon={<AIIcon />} iconPosition="start" />
<Tab label="Content Optimizer" icon={<AnalyticsIcon />} iconPosition="start" />
<Tab label="Trending Topics" icon={<TrendingIcon />} iconPosition="start" />
</Tabs>
</Box>
@@ -416,102 +389,6 @@ const CalendarTab: React.FC = () => {
)}
</TabPanel>
<TabPanel value={tabValue} index={1}>
{/* Calendar Generation Wizard with Data Transparency */}
<CalendarGenerationWizard
userData={userData}
onGenerateCalendar={handleGenerateCalendar}
loading={loading}
/>
</TabPanel>
<TabPanel value={tabValue} index={2}>
{/* Content Optimizer Tab */}
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Content Optimization
</Typography>
{contentOptimization ? (
<Box>
<Typography variant="body1" gutterBottom>
Optimization Recommendations
</Typography>
<List>
{contentOptimization.recommendations?.map((rec: any, index: number) => (
<ListItem key={index}>
<ListItemIcon>
<LightbulbIcon color="primary" />
</ListItemIcon>
<ListItemText
primary={rec.title}
secondary={rec.description}
/>
</ListItem>
))}
</List>
</Box>
) : (
<Box sx={{ textAlign: 'center', py: 4 }}>
<AnalyticsIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No optimization data
</Typography>
<Typography variant="body2" color="text.secondary">
Generate content optimization recommendations
</Typography>
</Box>
)}
</Paper>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={tabValue} index={3}>
{/* Trending Topics Tab */}
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<TrendingIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Trending Topics
</Typography>
{trendingTopics ? (
<Box>
<Typography variant="body1" gutterBottom>
Current Trending Topics
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{trendingTopics.trending_topics?.map((topic: any, index: number) => (
<Chip
key={index}
label={topic.name || topic.keyword}
color="primary"
variant="outlined"
/>
))}
</Box>
</Box>
) : (
<Box sx={{ textAlign: 'center', py: 4 }}>
<TrendingIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No trending topics
</Typography>
<Typography variant="body2" color="text.secondary">
Get trending topics for your industry
</Typography>
</Box>
)}
</Paper>
</Grid>
</Grid>
</TabPanel>
{/* Event Dialog */}
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>

View File

@@ -0,0 +1,73 @@
import React from 'react';
import {
Box,
Grid,
Paper,
Typography,
List,
ListItem,
ListItemText,
ListItemIcon,
Chip
} from '@mui/material';
import {
Analytics as AnalyticsIcon,
Lightbulb as LightbulbIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
const ContentOptimizerTab: React.FC = () => {
const { contentOptimization } = useContentPlanningStore();
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Content Optimizer
</Typography>
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Content Optimization
</Typography>
{contentOptimization ? (
<Box>
<Typography variant="body1" gutterBottom>
Optimization Recommendations
</Typography>
<List>
{contentOptimization.recommendations?.map((rec: any, index: number) => (
<ListItem key={index}>
<ListItemIcon>
<LightbulbIcon color="primary" />
</ListItemIcon>
<ListItemText
primary={rec.title}
secondary={rec.description}
/>
</ListItem>
))}
</List>
</Box>
) : (
<Box sx={{ textAlign: 'center', py: 4 }}>
<AnalyticsIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No optimization data
</Typography>
<Typography variant="body2" color="text.secondary">
Generate content optimization recommendations
</Typography>
</Box>
)}
</Paper>
</Grid>
</Grid>
</Box>
);
};
export default ContentOptimizerTab;

View File

@@ -0,0 +1,125 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Grid,
Card,
CardContent,
CardActions,
Typography,
Button,
CircularProgress
} from '@mui/material';
import {
PieChart as PieChartIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
const ContentPillarsTab: React.FC = () => {
const { currentStrategy } = useContentPlanningStore();
const [contentPillars, setContentPillars] = useState<any[]>([]);
const [dataLoading, setDataLoading] = useState(false);
useEffect(() => {
loadContentPillars();
}, [currentStrategy]);
const loadContentPillars = async () => {
try {
setDataLoading(true);
// Get content pillars from current strategy
if (currentStrategy && currentStrategy.content_pillars) {
const pillars = currentStrategy.content_pillars.map((pillar: any, index: number) => ({
name: pillar.name || `Pillar ${index + 1}`,
content_count: pillar.content_count || Math.floor(Math.random() * 20) + 5,
avg_engagement: pillar.avg_engagement || (Math.random() * 30 + 60).toFixed(1),
performance_score: pillar.performance_score || (Math.random() * 20 + 75).toFixed(0)
}));
setContentPillars(pillars);
} else {
// Default pillars if no strategy exists
setContentPillars([
{ name: 'Educational Content', content_count: 15, avg_engagement: 78.5, performance_score: 85 },
{ name: 'Thought Leadership', content_count: 8, avg_engagement: 92.3, performance_score: 91 },
{ name: 'Case Studies', content_count: 12, avg_engagement: 85.7, performance_score: 88 },
{ name: 'Industry Insights', content_count: 10, avg_engagement: 79.2, performance_score: 82 }
]);
}
} catch (error) {
console.error('Error loading content pillars:', error);
} finally {
setDataLoading(false);
}
};
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Content Pillars
</Typography>
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : contentPillars.length > 0 ? (
<Grid container spacing={3}>
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
Content Pillars Overview
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Your content is organized into these strategic pillars to ensure comprehensive coverage of your topics.
</Typography>
</Grid>
{contentPillars.map((pillar, index) => (
<Grid item xs={12} md={6} key={index}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{pillar.name}
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Content Count
</Typography>
<Typography variant="h6">
{pillar.content_count}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Avg. Engagement
</Typography>
<Typography variant="h6">
{pillar.avg_engagement}%
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" color="text.secondary">
Performance Score
</Typography>
<Typography variant="h6" color="success.main">
{pillar.performance_score}/100
</Typography>
</Box>
</CardContent>
<CardActions>
<Button size="small">View Content</Button>
<Button size="small">Optimize</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No content pillars data available
</Typography>
)}
</Box>
);
};
export default ContentPillarsTab;

View File

@@ -5,12 +5,10 @@ import {
Paper,
Typography,
Button,
TextField,
Card,
CardContent,
CardActions,
Chip,
Divider,
Alert,
List,
ListItem,
@@ -39,7 +37,6 @@ import {
Lightbulb as LightbulbIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
Search as SearchIcon,
Analytics as AnalyticsIcon,
Timeline as TimelineIcon,
Assessment as AssessmentIcon,
@@ -48,36 +45,14 @@ import {
Add as AddIcon,
Edit as EditIcon,
Visibility as VisibilityIcon,
BarChart as BarChartIcon,
PieChart as PieChartIcon,
ShowChart as ShowChartIcon,
AutoAwesome as AutoAwesomeIcon
AutoAwesome as AutoAwesomeIcon,
PlayArrow as PlayArrowIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
import ContentStrategyBuilder from '../components/ContentStrategyBuilder';
import StrategyIntelligenceTab from '../components/StrategyIntelligenceTab';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`strategy-tabpanel-${index}`}
aria-labelledby={`strategy-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
import StrategyOnboardingDialog from '../components/StrategyOnboardingDialog';
const ContentStrategyTab: React.FC = () => {
const {
@@ -85,7 +60,6 @@ const ContentStrategyTab: React.FC = () => {
currentStrategy,
aiInsights,
aiRecommendations,
performanceMetrics,
loading,
error,
loadStrategies,
@@ -93,7 +67,6 @@ const ContentStrategyTab: React.FC = () => {
loadAIRecommendations
} = useContentPlanningStore();
const [tabValue, setTabValue] = useState(0);
const [strategyForm, setStrategyForm] = useState({
name: '',
description: '',
@@ -104,25 +77,53 @@ const ContentStrategyTab: React.FC = () => {
// Real data states
const [strategicIntelligence, setStrategicIntelligence] = useState<any>(null);
const [keywordResearch, setKeywordResearch] = useState<any>(null);
const [contentPillars, setContentPillars] = useState<any[]>([]);
const [dataLoading, setDataLoading] = useState({
strategies: false,
insights: false,
recommendations: false,
strategicIntelligence: false,
keywordResearch: false,
pillars: false
strategicIntelligence: false
});
// Strategy status and onboarding
const [strategyStatus, setStrategyStatus] = useState<'active' | 'inactive' | 'none'>('none');
const [showOnboarding, setShowOnboarding] = useState(false);
const [hasCheckedStrategy, setHasCheckedStrategy] = useState(false);
// Load data on component mount
useEffect(() => {
loadInitialData();
}, []);
// Check strategy status when strategies are loaded
useEffect(() => {
if (strategies && strategies.length > 0 && !hasCheckedStrategy) {
checkStrategyStatus();
} else if ((!strategies || strategies.length === 0) && !hasCheckedStrategy) {
setStrategyStatus('none');
setHasCheckedStrategy(true);
setShowOnboarding(true);
}
}, [strategies, hasCheckedStrategy]);
const checkStrategyStatus = () => {
if (strategies && strategies.length > 0) {
// Find the most recent strategy
const latestStrategy = strategies[0]; // Assuming strategies are sorted by date
// For now, we'll assume strategies are active if they exist
// In a real implementation, you would check a status field from the database
setStrategyStatus('active');
setShowOnboarding(false);
} else {
setStrategyStatus('none');
setShowOnboarding(true);
}
setHasCheckedStrategy(true);
};
const loadInitialData = async () => {
try {
setDataLoading({ strategies: true, insights: true, recommendations: true, strategicIntelligence: true, keywordResearch: true, pillars: true });
setDataLoading({ strategies: true, insights: true, recommendations: true, strategicIntelligence: true });
// Load strategies
await loadStrategies();
@@ -136,16 +137,10 @@ const ContentStrategyTab: React.FC = () => {
// Load strategic intelligence
await loadStrategicIntelligence();
// Load keyword research
await loadKeywordResearch();
// Load content pillars
await loadContentPillars();
} catch (error) {
console.error('Error loading initial data:', error);
} finally {
setDataLoading({ strategies: false, insights: false, recommendations: false, strategicIntelligence: false, keywordResearch: false, pillars: false });
setDataLoading({ strategies: false, insights: false, recommendations: false, strategicIntelligence: false });
}
};
@@ -236,156 +231,6 @@ const ContentStrategyTab: React.FC = () => {
}
};
const loadKeywordResearch = async () => {
try {
setDataLoading(prev => ({ ...prev, keywordResearch: true }));
// Use streaming endpoint for real-time updates
const eventSource = await contentPlanningApi.streamKeywordResearch(1);
contentPlanningApi.handleSSEData(
eventSource,
(data) => {
console.log('Keyword Research SSE Data:', data);
if (data.type === 'status') {
// Update loading message
console.log('Status:', data.message);
} else if (data.type === 'progress') {
// Update progress (could be used for progress bar)
console.log('Progress:', data.progress, '%');
} else if (data.type === 'result' && data.status === 'success') {
// Set the keyword research data
setKeywordResearch(data.data);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
} else if (data.type === 'error') {
console.error('Keyword Research Error:', data.message);
// Set fallback data on error
const keywordData = {
trend_analysis: {
high_volume_keywords: [
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
],
trending_keywords: [
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
]
},
intent_analysis: {
informational: ['how to', 'what is', 'guide to'],
navigational: ['company name', 'brand name', 'website'],
transactional: ['buy', 'purchase', 'download', 'sign up']
},
opportunities: [
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
]
};
setKeywordResearch(keywordData);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
}
},
(error) => {
console.error('Keyword Research SSE Error:', error);
// Set fallback data on error
const keywordData = {
trend_analysis: {
high_volume_keywords: [
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
],
trending_keywords: [
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
]
},
intent_analysis: {
informational: ['how to', 'what is', 'guide to'],
navigational: ['company name', 'brand name', 'website'],
transactional: ['buy', 'purchase', 'download', 'sign up']
},
opportunities: [
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
]
};
setKeywordResearch(keywordData);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
}
);
} catch (error) {
console.error('Error loading keyword research:', error);
// Set fallback data on error
const keywordData = {
trend_analysis: {
high_volume_keywords: [
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
],
trending_keywords: [
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
]
},
intent_analysis: {
informational: ['how to', 'what is', 'guide to'],
navigational: ['company name', 'brand name', 'website'],
transactional: ['buy', 'purchase', 'download', 'sign up']
},
opportunities: [
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
]
};
setKeywordResearch(keywordData);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
}
};
const loadContentPillars = async () => {
try {
setDataLoading(prev => ({ ...prev, pillars: true }));
// Get content pillars from current strategy
if (currentStrategy && currentStrategy.content_pillars) {
const pillars = currentStrategy.content_pillars.map((pillar: any, index: number) => ({
name: pillar.name || `Pillar ${index + 1}`,
content_count: pillar.content_count || Math.floor(Math.random() * 20) + 5,
avg_engagement: pillar.avg_engagement || (Math.random() * 30 + 60).toFixed(1),
performance_score: pillar.performance_score || (Math.random() * 20 + 75).toFixed(0)
}));
setContentPillars(pillars);
} else {
// Default pillars if no strategy exists
setContentPillars([
{ name: 'Educational Content', content_count: 15, avg_engagement: 78.5, performance_score: 85 },
{ name: 'Thought Leadership', content_count: 8, avg_engagement: 92.3, performance_score: 91 },
{ name: 'Case Studies', content_count: 12, avg_engagement: 85.7, performance_score: 88 },
{ name: 'Industry Insights', content_count: 10, avg_engagement: 79.2, performance_score: 82 }
]);
}
} catch (error) {
console.error('Error loading content pillars:', error);
} finally {
setDataLoading(prev => ({ ...prev, pillars: false }));
}
};
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
const handleStrategyFormChange = (field: string, value: string) => {
setStrategyForm(prev => ({
...prev,
@@ -428,6 +273,38 @@ const ContentStrategyTab: React.FC = () => {
await loadInitialData();
};
// Onboarding dialog handlers
const handleConfirmStrategy = async () => {
try {
if (currentStrategy) {
// For now, we'll just close the dialog since we can't update status
// In a real implementation, you would update the strategy status in the database
setShowOnboarding(false);
// Reload strategies to get updated data
await loadStrategies();
}
} catch (error) {
console.error('Error activating strategy:', error);
}
};
const handleEditStrategy = () => {
setShowOnboarding(false);
// Navigate to Create tab to edit strategy
// This would typically involve changing the active tab in the parent component
};
const handleCreateNewStrategy = () => {
setShowOnboarding(false);
// Navigate to Create tab to create new strategy
// This would typically involve changing the active tab in the parent component
};
const handleCloseOnboarding = () => {
setShowOnboarding(false);
};
return (
<Box sx={{ p: 3 }}>
{error && (
@@ -436,260 +313,72 @@ const ContentStrategyTab: React.FC = () => {
</Alert>
)}
{/* Strategy Builder Tabs */}
{/* Strategy Status Banner */}
{strategyStatus === 'inactive' && (
<Alert
severity="warning"
sx={{ mb: 3 }}
action={
<Button
color="inherit"
size="small"
onClick={() => setShowOnboarding(true)}
startIcon={<PlayArrowIcon />}
>
Activate Strategy
</Button>
}
>
<Typography variant="body1">
<strong>Strategy Pending Activation:</strong> Your content strategy is ready but needs to be activated to start your AI-powered content marketing journey.
</Typography>
</Alert>
)}
{strategyStatus === 'none' && (
<Alert
severity="info"
sx={{ mb: 3 }}
action={
<Button
color="inherit"
size="small"
onClick={() => setShowOnboarding(true)}
startIcon={<AutoAwesomeIcon />}
>
Create Strategy
</Button>
}
>
<Typography variant="body1">
<strong>No Strategy Found:</strong> Let's create your first AI-powered content strategy to start your digital marketing journey.
</Typography>
</Alert>
)}
{strategyStatus === 'active' && (
<Alert severity="success" sx={{ mb: 3 }}>
<Typography variant="body1">
<strong>Strategy Active:</strong> Your content strategy is running and ALwrity is managing your content marketing automatically.
</Typography>
</Alert>
)}
{/* Strategic Intelligence */}
<Paper sx={{ width: '100%', mb: 3 }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="strategy builder tabs">
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<AutoAwesomeIcon sx={{ mr: 1 }} />
Enhanced Strategy Builder
</Box>
}
/>
<Tab label="Strategic Intelligence" icon={<AssessmentIcon />} />
<Tab label="Keyword Research" icon={<SearchIcon />} />
<Tab label="Performance Analytics" icon={<BarChartIcon />} />
<Tab label="Content Pillars" icon={<PieChartIcon />} />
</Tabs>
</Box>
{/* Enhanced Strategy Builder Tab */}
<TabPanel value={tabValue} index={0}>
<ContentStrategyBuilder />
</TabPanel>
{/* Strategic Intelligence Tab */}
<TabPanel value={tabValue} index={1}>
<StrategyIntelligenceTab />
</TabPanel>
{/* Keyword Research Tab */}
<TabPanel value={tabValue} index={2}>
{dataLoading.keywordResearch ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : keywordResearch && keywordResearch.trend_analysis ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
High Volume Keywords
</Typography>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Keyword</TableCell>
<TableCell>Volume</TableCell>
<TableCell>Difficulty</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(keywordResearch.trend_analysis.high_volume_keywords || []).map((keyword: any, index: number) => (
<TableRow key={index}>
<TableCell>{keyword.keyword}</TableCell>
<TableCell>{keyword.volume}</TableCell>
<TableCell>
<Chip
label={keyword.difficulty}
color={keyword.difficulty === 'Low' ? 'success' : keyword.difficulty === 'Medium' ? 'warning' : 'error'}
size="small"
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Trending Keywords
</Typography>
{(keywordResearch.trend_analysis.trending_keywords || []).map((keyword: any, index: number) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle1">
{keyword.keyword}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Chip
label={keyword.growth}
color="success"
size="small"
/>
<Chip
label={keyword.opportunity}
color={keyword.opportunity === 'High' ? 'success' : 'primary'}
size="small"
/>
</Box>
</Box>
))}
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Keyword Opportunities
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Keyword</TableCell>
<TableCell>Search Volume</TableCell>
<TableCell>Competition</TableCell>
<TableCell>CPC</TableCell>
<TableCell>Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(keywordResearch.opportunities || []).map((opportunity: any, index: number) => (
<TableRow key={index}>
<TableCell>{opportunity.keyword}</TableCell>
<TableCell>{opportunity.search_volume}</TableCell>
<TableCell>
<Chip
label={opportunity.competition}
color={opportunity.competition === 'Low' ? 'success' : opportunity.competition === 'Medium' ? 'warning' : 'error'}
size="small"
/>
</TableCell>
<TableCell>${opportunity.cpc}</TableCell>
<TableCell>
<Button size="small" variant="outlined">
Add to Strategy
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No keyword research data available
</Typography>
)}
</TabPanel>
{/* Performance Analytics Tab */}
<TabPanel value={tabValue} index={3}>
{performanceMetrics ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Content Performance by Type
</Typography>
<Typography variant="body2" color="text.secondary">
No content performance data available
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Growth Trends
</Typography>
<Typography variant="body2" color="text.secondary">
No trend data available
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No performance analytics data available
</Typography>
)}
</TabPanel>
{/* Content Pillars Tab */}
<TabPanel value={tabValue} index={4}>
{dataLoading.pillars ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : contentPillars.length > 0 ? (
<Grid container spacing={3}>
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
Content Pillars Overview
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Your content is organized into these strategic pillars to ensure comprehensive coverage of your topics.
</Typography>
</Grid>
{contentPillars.map((pillar, index) => (
<Grid item xs={12} md={6} key={index}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{pillar.name}
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Content Count
</Typography>
<Typography variant="h6">
{pillar.content_count}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Avg. Engagement
</Typography>
<Typography variant="h6">
{pillar.avg_engagement}%
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" color="text.secondary">
Performance Score
</Typography>
<Typography variant="h6" color="success.main">
{pillar.performance_score}/100
</Typography>
</Box>
</CardContent>
<CardActions>
<Button size="small">View Content</Button>
<Button size="small">Optimize</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No content pillars data available
</Typography>
)}
</TabPanel>
<StrategyIntelligenceTab />
</Paper>
{/* Strategy Onboarding Dialog */}
<StrategyOnboardingDialog
open={showOnboarding}
onClose={handleCloseOnboarding}
onConfirmStrategy={handleConfirmStrategy}
onEditStrategy={handleEditStrategy}
onCreateNewStrategy={handleCreateNewStrategy}
currentStrategy={currentStrategy}
strategyStatus={strategyStatus}
/>
</Box>
);
};

View File

@@ -0,0 +1,113 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Tabs,
Tab,
Typography
} from '@mui/material';
import {
AutoAwesome as AutoAwesomeIcon,
CalendarToday as CalendarIcon
} from '@mui/icons-material';
import ContentStrategyBuilder from '../components/ContentStrategyBuilder';
import CalendarGenerationWizard from '../components/CalendarGenerationWizard';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`create-tabpanel-${index}`}
aria-labelledby={`create-tab-${index}`}
{...other}
>
{value === index && <Box>{children}</Box>}
</div>
);
}
const CreateTab: React.FC = () => {
const [tabValue, setTabValue] = useState(0);
const [userData, setUserData] = useState<any>({});
useEffect(() => {
loadUserData();
}, []);
const loadUserData = async () => {
try {
// Load comprehensive user data for calendar generation
const comprehensiveData = await contentPlanningApi.getComprehensiveUserData(1); // Pass user ID
setUserData(comprehensiveData.data); // Extract the data from the response
} catch (error) {
console.error('Error loading user data:', error);
}
};
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
const handleGenerateCalendar = async (calendarConfig: any) => {
try {
await contentPlanningApi.generateComprehensiveCalendar({
...calendarConfig,
userData
});
} catch (error) {
console.error('Error generating calendar:', error);
}
};
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Create
</Typography>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="create tabs">
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<AutoAwesomeIcon sx={{ mr: 1 }} />
Enhanced Strategy Builder
</Box>
}
/>
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CalendarIcon sx={{ mr: 1 }} />
Calendar Wizard
</Box>
}
/>
</Tabs>
</Box>
<TabPanel value={tabValue} index={0}>
<ContentStrategyBuilder />
</TabPanel>
<TabPanel value={tabValue} index={1}>
<CalendarGenerationWizard
userData={userData}
onGenerateCalendar={handleGenerateCalendar}
loading={false}
/>
</TabPanel>
</Box>
);
};
export default CreateTab;

View File

@@ -1,404 +1,95 @@
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import {
Box,
Grid,
Paper,
Tabs,
Tab,
Typography,
Button,
TextField,
Card,
CardContent,
Chip,
Divider,
Alert,
CircularProgress,
List,
ListItem,
ListItemText,
ListItemIcon
Alert
} from '@mui/material';
import {
Analytics as AnalyticsIcon,
TrendingUp as TrendingIcon,
Search as SearchIcon,
Add as AddIcon,
Warning as WarningIcon,
CheckCircle as CheckCircleIcon,
TrendingUp as TrendingUpIcon,
Assessment as AssessmentIcon
Assessment as AssessmentIcon,
BarChart as BarChartIcon,
PieChart as PieChartIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
import RefineAnalysisTab from './RefineAnalysisTab';
import ContentOptimizerTab from './ContentOptimizerTab';
import TrendingTopicsTab from './TrendingTopicsTab';
import KeywordResearchTab from './KeywordResearchTab';
import PerformanceAnalyticsTab from './PerformanceAnalyticsTab';
import ContentPillarsTab from './ContentPillarsTab';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`gap-analysis-tabpanel-${index}`}
aria-labelledby={`gap-analysis-tab-${index}`}
{...other}
>
{value === index && <Box>{children}</Box>}
</div>
);
}
const GapAnalysisTab: React.FC = () => {
const {
gapAnalyses,
loading,
error,
loadGapAnalyses,
analyzeContentGaps,
updateGapAnalyses
} = useContentPlanningStore();
const [analysisForm, setAnalysisForm] = useState({
website_url: '',
competitors: [] as string[],
keywords: [] as string[]
});
const [newCompetitor, setNewCompetitor] = useState('');
const [newKeyword, setNewKeyword] = useState('');
const [dataLoading, setDataLoading] = useState(false);
const [tabValue, setTabValue] = useState(0);
useEffect(() => {
loadGapAnalysisData();
}, []);
const loadGapAnalysisData = async () => {
try {
setDataLoading(true);
const response = await contentPlanningApi.getGapAnalysesSafe();
console.log('Gap Analysis Response:', response);
// Transform the backend response to match frontend expectations
if (response && response.gap_analyses) {
const transformedAnalyses = response.gap_analyses.map((analysis: any, index: number) => ({
id: analysis.id || `analysis_${index}`,
website_url: analysis.website_url || 'example.com',
competitors: analysis.competitors || [],
keywords: analysis.keywords || [],
gaps: analysis.gaps || [],
recommendations: analysis.recommendations || [],
created_at: analysis.created_at || new Date().toISOString()
}));
console.log('Transformed Analyses:', transformedAnalyses);
// Update the store with transformed data
updateGapAnalyses(transformedAnalyses);
} else {
console.log('No gap analyses found in response');
updateGapAnalyses([]);
}
} catch (error) {
console.error('Error loading gap analysis data:', error);
updateGapAnalyses([]);
} finally {
setDataLoading(false);
}
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
const handleAddCompetitor = () => {
if (newCompetitor.trim() && !analysisForm.competitors.includes(newCompetitor.trim())) {
setAnalysisForm(prev => ({
...prev,
competitors: [...prev.competitors, newCompetitor.trim()]
}));
setNewCompetitor('');
}
};
const handleRemoveCompetitor = (competitorToRemove: string) => {
setAnalysisForm(prev => ({
...prev,
competitors: prev.competitors.filter(comp => comp !== competitorToRemove)
}));
};
const handleAddKeyword = () => {
if (newKeyword.trim() && !analysisForm.keywords.includes(newKeyword.trim())) {
setAnalysisForm(prev => ({
...prev,
keywords: [...prev.keywords, newKeyword.trim()]
}));
setNewKeyword('');
}
};
const handleRemoveKeyword = (keywordToRemove: string) => {
setAnalysisForm(prev => ({
...prev,
keywords: prev.keywords.filter(keyword => keyword !== keywordToRemove)
}));
};
const handleRunAnalysis = async () => {
if (!analysisForm.website_url) {
return;
}
try {
setDataLoading(true);
await analyzeContentGaps({
website_url: analysisForm.website_url,
competitors: analysisForm.competitors,
keywords: analysisForm.keywords
});
// Reload data after analysis
await loadGapAnalyses();
// Reset form
setAnalysisForm({
website_url: '',
competitors: [],
keywords: []
});
} catch (error) {
console.error('Error running gap analysis:', error);
} finally {
setDataLoading(false);
}
};
// Ensure gapAnalyses is always an array and transform the data structure
const safeGapAnalyses = Array.isArray(gapAnalyses) ? gapAnalyses : [];
// Transform backend data structure to frontend expected structure
const transformedGapAnalyses = safeGapAnalyses.map((analysis, index) => {
// Handle the actual backend structure: { recommendations: [...] }
const recommendations = analysis.recommendations || [];
return {
id: analysis.id || `analysis-${index}`,
website_url: analysis.website_url || 'Unknown Website',
competitors: analysis.competitors || [],
keywords: analysis.keywords || [],
recommendations: recommendations,
created_at: analysis.created_at || new Date().toISOString(),
// Extract gaps from recommendations if available
gaps: recommendations.length > 0 ?
recommendations.filter((rec: any) => rec.type === 'gap').map((rec: any) => rec.title || rec.description || 'Content gap identified') :
[]
};
});
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Content Gap Analysis
Gap Analysis & Optimization
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="gap analysis tabs">
<Tab label="Content Optimizer" icon={<AnalyticsIcon />} iconPosition="start" />
<Tab label="Trending Topics" icon={<TrendingIcon />} iconPosition="start" />
<Tab label="Keyword Research" icon={<SearchIcon />} iconPosition="start" />
<Tab label="Performance Analytics" icon={<BarChartIcon />} iconPosition="start" />
<Tab label="Content Pillars" icon={<PieChartIcon />} iconPosition="start" />
<Tab label="Refine Analysis" icon={<AssessmentIcon />} iconPosition="start" />
</Tabs>
</Box>
<Grid container spacing={3}>
{/* Analysis Setup */}
<Grid item xs={12} md={4}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<SearchIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Analysis Setup
</Typography>
<Divider sx={{ mb: 2 }} />
<TextField
fullWidth
label="Website URL"
value={analysisForm.website_url}
onChange={(e) => setAnalysisForm(prev => ({ ...prev, website_url: e.target.value }))}
placeholder="https://example.com"
sx={{ mb: 2 }}
/>
<Typography variant="subtitle2" gutterBottom>
Competitors
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<TextField
fullWidth
label="Add Competitor"
value={newCompetitor}
onChange={(e) => setNewCompetitor(e.target.value)}
placeholder="competitor.com"
onKeyPress={(e) => e.key === 'Enter' && handleAddCompetitor()}
/>
<Button
variant="outlined"
onClick={handleAddCompetitor}
disabled={!newCompetitor.trim()}
>
<AddIcon />
</Button>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
{analysisForm.competitors.map((competitor, index) => (
<Chip
key={index}
label={competitor}
onDelete={() => handleRemoveCompetitor(competitor)}
color="primary"
variant="outlined"
/>
))}
</Box>
<Typography variant="subtitle2" gutterBottom>
Keywords
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<TextField
fullWidth
label="Add Keyword"
value={newKeyword}
onChange={(e) => setNewKeyword(e.target.value)}
placeholder="target keyword"
onKeyPress={(e) => e.key === 'Enter' && handleAddKeyword()}
/>
<Button
variant="outlined"
onClick={handleAddKeyword}
disabled={!newKeyword.trim()}
>
<AddIcon />
</Button>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
{analysisForm.keywords.map((keyword, index) => (
<Chip
key={index}
label={keyword}
onDelete={() => handleRemoveKeyword(keyword)}
color="secondary"
variant="outlined"
/>
))}
</Box>
<Button
variant="contained"
fullWidth
onClick={handleRunAnalysis}
disabled={loading || dataLoading || !analysisForm.website_url}
startIcon={<AssessmentIcon />}
>
{loading || dataLoading ? 'Running Analysis...' : 'Run Gap Analysis'}
</Button>
</Paper>
</Grid>
<TabPanel value={tabValue} index={0}>
<ContentOptimizerTab />
</TabPanel>
{/* Content Gaps */}
<Grid item xs={12} md={8}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<WarningIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Content Gaps
</Typography>
<Divider sx={{ mb: 2 }} />
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : transformedGapAnalyses.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
No previous analyses found. Run your first analysis to see results here.
</Typography>
) : (
<Grid container spacing={2}>
{transformedGapAnalyses.map((analysis) => (
<Grid item xs={12} md={6} lg={4} key={analysis.id}>
<Card>
<CardContent>
<Typography variant="h6" component="div">
{analysis.website_url}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{new Date(analysis.created_at).toLocaleDateString()}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
<Chip
label={`${analysis.competitors?.length || 0} competitors`}
size="small"
variant="outlined"
/>
<Chip
label={`${analysis.keywords?.length || 0} keywords`}
size="small"
variant="outlined"
/>
<Chip
label={`${analysis.gaps?.length || 0} gaps found`}
size="small"
color="warning"
/>
<Chip
label={`${analysis.recommendations?.length || 0} recommendations`}
size="small"
color="success"
/>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Paper>
<TabPanel value={tabValue} index={1}>
<TrendingTopicsTab />
</TabPanel>
{/* Detailed Analysis Results */}
{transformedGapAnalyses.length > 0 && (
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Detailed Analysis Results
</Typography>
<Divider sx={{ mb: 2 }} />
{transformedGapAnalyses.map((analysis, index) => (
<Box key={index} sx={{ mb: 3 }}>
<Typography variant="subtitle1" gutterBottom>
Analysis for {analysis.website_url}
</Typography>
{analysis.gaps && analysis.gaps.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Identified Content Gaps:
</Typography>
<List dense>
{analysis.gaps.map((gap, gapIndex) => (
<ListItem key={gapIndex}>
<ListItemIcon>
<WarningIcon color="warning" />
</ListItemIcon>
<ListItemText primary={gap} />
</ListItem>
))}
</List>
</Box>
)}
{analysis.recommendations && analysis.recommendations.length > 0 && (
<Box>
<Typography variant="subtitle2" gutterBottom>
Recommendations:
</Typography>
<List dense>
{analysis.recommendations.map((rec, recIndex) => (
<ListItem key={recIndex}>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText
primary={rec.title || rec.description || 'Recommendation'}
secondary={rec.description}
/>
</ListItem>
))}
</List>
</Box>
)}
</Box>
))}
</Paper>
)}
</Grid>
</Grid>
<TabPanel value={tabValue} index={2}>
<KeywordResearchTab />
</TabPanel>
<TabPanel value={tabValue} index={3}>
<PerformanceAnalyticsTab />
</TabPanel>
<TabPanel value={tabValue} index={4}>
<ContentPillarsTab />
</TabPanel>
<TabPanel value={tabValue} index={5}>
<RefineAnalysisTab />
</TabPanel>
</Box>
);
};

View File

@@ -0,0 +1,189 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Grid,
Paper,
Typography,
Card,
CardContent,
Chip,
CircularProgress,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Button
} from '@mui/material';
import {
Search as SearchIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
const KeywordResearchTab: React.FC = () => {
const [keywordResearch, setKeywordResearch] = useState<any>(null);
const [dataLoading, setDataLoading] = useState(false);
useEffect(() => {
loadKeywordResearch();
}, []);
const loadKeywordResearch = async () => {
try {
setDataLoading(true);
const eventSource = await contentPlanningApi.streamKeywordResearch();
contentPlanningApi.handleSSEData(
eventSource,
(data) => {
console.log('Keyword Research SSE Data:', data);
if (data.type === 'result' && data.data) {
setKeywordResearch(data.data);
}
},
(error) => {
console.error('Keyword Research SSE Error:', error);
},
() => {
setDataLoading(false);
}
);
} catch (error) {
console.error('Error loading keyword research:', error);
setDataLoading(false);
}
};
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Keyword Research
</Typography>
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : keywordResearch && keywordResearch.trend_analysis ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
High Volume Keywords
</Typography>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Keyword</TableCell>
<TableCell>Volume</TableCell>
<TableCell>Difficulty</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(keywordResearch.trend_analysis.high_volume_keywords || []).map((keyword: any, index: number) => (
<TableRow key={index}>
<TableCell>{keyword.keyword}</TableCell>
<TableCell>{keyword.volume}</TableCell>
<TableCell>
<Chip
label={keyword.difficulty}
color={keyword.difficulty === 'Low' ? 'success' : keyword.difficulty === 'Medium' ? 'warning' : 'error'}
size="small"
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Trending Keywords
</Typography>
{(keywordResearch.trend_analysis.trending_keywords || []).map((keyword: any, index: number) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle1">
{keyword.keyword}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Chip
label={keyword.growth}
color="success"
size="small"
/>
<Chip
label={keyword.opportunity}
color={keyword.opportunity === 'High' ? 'success' : 'primary'}
size="small"
/>
</Box>
</Box>
))}
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Keyword Opportunities
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Keyword</TableCell>
<TableCell>Search Volume</TableCell>
<TableCell>Competition</TableCell>
<TableCell>CPC</TableCell>
<TableCell>Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(keywordResearch.opportunities || []).map((opportunity: any, index: number) => (
<TableRow key={index}>
<TableCell>{opportunity.keyword}</TableCell>
<TableCell>{opportunity.search_volume}</TableCell>
<TableCell>
<Chip
label={opportunity.competition}
color={opportunity.competition === 'Low' ? 'success' : opportunity.competition === 'Medium' ? 'warning' : 'error'}
size="small"
/>
</TableCell>
<TableCell>${opportunity.cpc}</TableCell>
<TableCell>
<Button size="small" variant="outlined">
Add to Strategy
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No keyword research data available
</Typography>
)}
</Box>
);
};
export default KeywordResearchTab;

View File

@@ -0,0 +1,60 @@
import React from 'react';
import {
Box,
Grid,
Card,
CardContent,
Typography
} from '@mui/material';
import {
BarChart as BarChartIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
const PerformanceAnalyticsTab: React.FC = () => {
const { performanceMetrics } = useContentPlanningStore();
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Performance Analytics
</Typography>
{performanceMetrics ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Content Performance by Type
</Typography>
<Typography variant="body2" color="text.secondary">
No content performance data available
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Growth Trends
</Typography>
<Typography variant="body2" color="text.secondary">
No trend data available
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No performance analytics data available
</Typography>
)}
</Box>
);
};
export default PerformanceAnalyticsTab;

View File

@@ -0,0 +1,406 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Grid,
Paper,
Typography,
Button,
TextField,
Card,
CardContent,
Chip,
Divider,
Alert,
CircularProgress,
List,
ListItem,
ListItemText,
ListItemIcon
} from '@mui/material';
import {
Search as SearchIcon,
Add as AddIcon,
Warning as WarningIcon,
CheckCircle as CheckCircleIcon,
TrendingUp as TrendingUpIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
const RefineAnalysisTab: React.FC = () => {
const {
gapAnalyses,
loading,
error,
loadGapAnalyses,
analyzeContentGaps,
updateGapAnalyses
} = useContentPlanningStore();
const [analysisForm, setAnalysisForm] = useState({
website_url: '',
competitors: [] as string[],
keywords: [] as string[]
});
const [newCompetitor, setNewCompetitor] = useState('');
const [newKeyword, setNewKeyword] = useState('');
const [dataLoading, setDataLoading] = useState(false);
useEffect(() => {
loadGapAnalysisData();
}, []);
const loadGapAnalysisData = async () => {
try {
setDataLoading(true);
const response = await contentPlanningApi.getGapAnalysesSafe();
console.log('Gap Analysis Response:', response);
// Transform the backend response to match frontend expectations
if (response && response.gap_analyses) {
const transformedAnalyses = response.gap_analyses.map((analysis: any, index: number) => ({
id: analysis.id || `analysis_${index}`,
website_url: analysis.website_url || 'example.com',
competitors: analysis.competitors || [],
keywords: analysis.keywords || [],
gaps: analysis.gaps || [],
recommendations: analysis.recommendations || [],
created_at: analysis.created_at || new Date().toISOString()
}));
console.log('Transformed Analyses:', transformedAnalyses);
// Update the store with transformed data
updateGapAnalyses(transformedAnalyses);
} else {
console.log('No gap analyses found in response');
updateGapAnalyses([]);
}
} catch (error) {
console.error('Error loading gap analysis data:', error);
updateGapAnalyses([]);
} finally {
setDataLoading(false);
}
};
const handleAddCompetitor = () => {
if (newCompetitor.trim() && !analysisForm.competitors.includes(newCompetitor.trim())) {
setAnalysisForm(prev => ({
...prev,
competitors: [...prev.competitors, newCompetitor.trim()]
}));
setNewCompetitor('');
}
};
const handleRemoveCompetitor = (competitorToRemove: string) => {
setAnalysisForm(prev => ({
...prev,
competitors: prev.competitors.filter(comp => comp !== competitorToRemove)
}));
};
const handleAddKeyword = () => {
if (newKeyword.trim() && !analysisForm.keywords.includes(newKeyword.trim())) {
setAnalysisForm(prev => ({
...prev,
keywords: [...prev.keywords, newKeyword.trim()]
}));
setNewKeyword('');
}
};
const handleRemoveKeyword = (keywordToRemove: string) => {
setAnalysisForm(prev => ({
...prev,
keywords: prev.keywords.filter(keyword => keyword !== keywordToRemove)
}));
};
const handleRunAnalysis = async () => {
if (!analysisForm.website_url) {
return;
}
try {
setDataLoading(true);
await analyzeContentGaps({
website_url: analysisForm.website_url,
competitors: analysisForm.competitors,
keywords: analysisForm.keywords
});
// Reload data after analysis
await loadGapAnalyses();
// Reset form
setAnalysisForm({
website_url: '',
competitors: [],
keywords: []
});
} catch (error) {
console.error('Error running gap analysis:', error);
} finally {
setDataLoading(false);
}
};
// Ensure gapAnalyses is always an array and transform the data structure
const safeGapAnalyses = Array.isArray(gapAnalyses) ? gapAnalyses : [];
// Transform backend data structure to frontend expected structure
const transformedGapAnalyses = safeGapAnalyses.map((analysis, index) => {
// Handle the actual backend structure: { recommendations: [...] }
const recommendations = analysis.recommendations || [];
return {
id: analysis.id || `analysis-${index}`,
website_url: analysis.website_url || 'Unknown Website',
competitors: analysis.competitors || [],
keywords: analysis.keywords || [],
recommendations: recommendations,
created_at: analysis.created_at || new Date().toISOString(),
// Extract gaps from recommendations if available
gaps: recommendations.length > 0 ?
recommendations.filter((rec: any) => rec.type === 'gap').map((rec: any) => rec.title || rec.description || 'Content gap identified') :
[]
};
});
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Refine Analysis
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<Grid container spacing={3}>
{/* Analysis Setup */}
<Grid item xs={12} md={4}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<SearchIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Analysis Setup
</Typography>
<Divider sx={{ mb: 2 }} />
<TextField
fullWidth
label="Website URL"
value={analysisForm.website_url}
onChange={(e) => setAnalysisForm(prev => ({ ...prev, website_url: e.target.value }))}
placeholder="https://example.com"
sx={{ mb: 2 }}
/>
<Typography variant="subtitle2" gutterBottom>
Competitors
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<TextField
fullWidth
label="Add Competitor"
value={newCompetitor}
onChange={(e) => setNewCompetitor(e.target.value)}
placeholder="competitor.com"
onKeyPress={(e) => e.key === 'Enter' && handleAddCompetitor()}
/>
<Button
variant="outlined"
onClick={handleAddCompetitor}
disabled={!newCompetitor.trim()}
>
<AddIcon />
</Button>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
{analysisForm.competitors.map((competitor, index) => (
<Chip
key={index}
label={competitor}
onDelete={() => handleRemoveCompetitor(competitor)}
color="primary"
variant="outlined"
/>
))}
</Box>
<Typography variant="subtitle2" gutterBottom>
Keywords
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<TextField
fullWidth
label="Add Keyword"
value={newKeyword}
onChange={(e) => setNewKeyword(e.target.value)}
placeholder="target keyword"
onKeyPress={(e) => e.key === 'Enter' && handleAddKeyword()}
/>
<Button
variant="outlined"
onClick={handleAddKeyword}
disabled={!newKeyword.trim()}
>
<AddIcon />
</Button>
</Box>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 3 }}>
{analysisForm.keywords.map((keyword, index) => (
<Chip
key={index}
label={keyword}
onDelete={() => handleRemoveKeyword(keyword)}
color="secondary"
variant="outlined"
/>
))}
</Box>
<Button
variant="contained"
fullWidth
onClick={handleRunAnalysis}
disabled={loading || dataLoading || !analysisForm.website_url}
startIcon={<AssessmentIcon />}
>
{loading || dataLoading ? 'Running Analysis...' : 'Run Gap Analysis'}
</Button>
</Paper>
</Grid>
{/* Content Gaps */}
<Grid item xs={12} md={8}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<WarningIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Content Gaps
</Typography>
<Divider sx={{ mb: 2 }} />
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : transformedGapAnalyses.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', py: 2 }}>
No previous analyses found. Run your first analysis to see results here.
</Typography>
) : (
<Grid container spacing={2}>
{transformedGapAnalyses.map((analysis) => (
<Grid item xs={12} md={6} lg={4} key={analysis.id}>
<Card>
<CardContent>
<Typography variant="h6" component="div">
{analysis.website_url}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{new Date(analysis.created_at).toLocaleDateString()}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
<Chip
label={`${analysis.competitors?.length || 0} competitors`}
size="small"
variant="outlined"
/>
<Chip
label={`${analysis.keywords?.length || 0} keywords`}
size="small"
variant="outlined"
/>
<Chip
label={`${analysis.gaps?.length || 0} gaps found`}
size="small"
color="warning"
/>
<Chip
label={`${analysis.recommendations?.length || 0} recommendations`}
size="small"
color="success"
/>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Paper>
{/* Detailed Analysis Results */}
{transformedGapAnalyses.length > 0 && (
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Detailed Analysis Results
</Typography>
<Divider sx={{ mb: 2 }} />
{transformedGapAnalyses.map((analysis, index) => (
<Box key={index} sx={{ mb: 3 }}>
<Typography variant="subtitle1" gutterBottom>
Analysis for {analysis.website_url}
</Typography>
{analysis.gaps && analysis.gaps.length > 0 && (
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Identified Content Gaps:
</Typography>
<List dense>
{analysis.gaps.map((gap, gapIndex) => (
<ListItem key={gapIndex}>
<ListItemIcon>
<WarningIcon color="warning" />
</ListItemIcon>
<ListItemText primary={gap} />
</ListItem>
))}
</List>
</Box>
)}
{analysis.recommendations && analysis.recommendations.length > 0 && (
<Box>
<Typography variant="subtitle2" gutterBottom>
Recommendations:
</Typography>
<List dense>
{analysis.recommendations.map((rec, recIndex) => (
<ListItem key={recIndex}>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText
primary={rec.title || rec.description || 'Recommendation'}
secondary={rec.description}
/>
</ListItem>
))}
</List>
</Box>
)}
</Box>
))}
</Paper>
)}
</Grid>
</Grid>
</Box>
);
};
export default RefineAnalysisTab;

View File

@@ -0,0 +1,65 @@
import React from 'react';
import {
Box,
Grid,
Paper,
Typography,
Chip
} from '@mui/material';
import {
TrendingUp as TrendingIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
const TrendingTopicsTab: React.FC = () => {
const { trendingTopics } = useContentPlanningStore();
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Trending Topics
</Typography>
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<TrendingIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Trending Topics
</Typography>
{trendingTopics ? (
<Box>
<Typography variant="body1" gutterBottom>
Current Trending Topics
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{trendingTopics.trending_topics?.map((topic: any, index: number) => (
<Chip
key={index}
label={topic.name || topic.keyword}
color="primary"
variant="outlined"
/>
))}
</Box>
</Box>
) : (
<Box sx={{ textAlign: 'center', py: 4 }}>
<TrendingIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No trending topics
</Typography>
<Typography variant="body2" color="text.secondary">
Get trending topics for your industry
</Typography>
</Box>
)}
</Paper>
</Grid>
</Grid>
</Box>
);
};
export default TrendingTopicsTab;