ALwrity version 0.5.5
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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', {})))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
142
backend/test_env_check.py
Normal 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)
|
||||
463
backend/test_onboarding_data.py
Normal file
463
backend/test_onboarding_data.py
Normal 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())
|
||||
446
docs/content_strategy_alwrityit_implementation_plan.md
Normal file
446
docs/content_strategy_alwrityit_implementation_plan.md
Normal 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
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user