ALwrity version 0.5.5
This commit is contained in:
@@ -332,7 +332,7 @@ async def generate_comprehensive_strategy_polling(
|
|||||||
"onboarding_data": onboarding_data,
|
"onboarding_data": onboarding_data,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"generation_config": config or {}
|
"generation_config": config or {}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Create strategy generation config
|
# Create strategy generation config
|
||||||
generation_config = StrategyGenerationConfig(
|
generation_config = StrategyGenerationConfig(
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ class AutoFillRefreshService:
|
|||||||
- Optionally augments with AI overrides (hook, not persisted)
|
- Optionally augments with AI overrides (hook, not persisted)
|
||||||
- Returns payload in the same shape as AutoFillService.get_autofill, plus meta
|
- 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)
|
# Base context from onboarding analysis (used for AI context only when ai_only)
|
||||||
logger.debug("AutoFillRefreshService: processing onboarding context | user=%s", user_id)
|
logger.debug("AutoFillRefreshService: processing onboarding context | user=%s", user_id)
|
||||||
base_context = await self.autofill.integration.process_onboarding_data(user_id, self.db)
|
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('api_keys_data')),
|
||||||
bool((base_context or {}).get('onboarding_session')),
|
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:
|
try:
|
||||||
w = (base_context or {}).get('website_analysis') or {}
|
w = (base_context or {}).get('website_analysis') or {}
|
||||||
r = (base_context or {}).get('research_preferences') 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)
|
ai_payload = await self.structured_ai.generate_autofill_fields(user_id, base_context)
|
||||||
meta = ai_payload.get('meta') or {}
|
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'))
|
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
|
return ai_payload
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("AI-only structured generation failed | user=%s | err=%s", user_id, repr(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)
|
# Fallback to previous behavior (DB + sparse overrides)
|
||||||
|
logger.info("AutoFillRefreshService: using fallback behavior (DB + sparse overrides)")
|
||||||
payload = await self.autofill.get_autofill(user_id)
|
payload = await self.autofill.get_autofill(user_id)
|
||||||
logger.info("AutoFillRefreshService: Base payload fields: %d", len(payload.get('fields', {})))
|
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.info("AIStructuredAutofillService: generating %d fields | user=%s", len(CORE_FIELDS), user_id)
|
||||||
logger.debug("AIStructuredAutofillService: properties=%d", len(schema.get('properties', {})))
|
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
|
last_result = None
|
||||||
for attempt in range(self.max_retries + 1):
|
for attempt in range(self.max_retries + 1):
|
||||||
try:
|
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(
|
result = await self.ai.execute_structured_json_call(
|
||||||
service_type=AIServiceType.STRATEGIC_INTELLIGENCE,
|
service_type=AIServiceType.STRATEGIC_INTELLIGENCE,
|
||||||
prompt=prompt,
|
prompt=prompt,
|
||||||
@@ -507,8 +518,34 @@ Generate the complete JSON with all 30 fields personalized for {website_url}:
|
|||||||
)
|
)
|
||||||
last_result = result
|
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
|
# 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
|
break
|
||||||
|
|
||||||
# Add a small delay before retry
|
# Add a small delay before retry
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import logging
|
|||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, Optional, List
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
import traceback
|
||||||
|
|
||||||
# Import database models
|
# Import database models
|
||||||
from models.enhanced_strategy_models import (
|
from models.enhanced_strategy_models import (
|
||||||
@@ -39,6 +40,13 @@ class OnboardingDataIntegrationService:
|
|||||||
api_keys_data = self._get_api_keys_data(user_id, db)
|
api_keys_data = self._get_api_keys_data(user_id, db)
|
||||||
onboarding_session = self._get_onboarding_session(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
|
# Process and integrate data
|
||||||
integrated_data = {
|
integrated_data = {
|
||||||
'website_analysis': website_analysis,
|
'website_analysis': website_analysis,
|
||||||
@@ -49,6 +57,14 @@ class OnboardingDataIntegrationService:
|
|||||||
'processing_timestamp': datetime.utcnow().isoformat()
|
'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
|
# Store integrated data
|
||||||
await self._store_integrated_data(user_id, integrated_data, db)
|
await self._store_integrated_data(user_id, integrated_data, db)
|
||||||
|
|
||||||
@@ -57,6 +73,7 @@ class OnboardingDataIntegrationService:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing onboarding data for user {user_id}: {str(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()
|
return self._get_fallback_data()
|
||||||
|
|
||||||
def _get_website_analysis(self, user_id: int, db: Session) -> Dict[str, Any]:
|
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 google.genai import types
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
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
|
from loguru import logger
|
||||||
logger.remove()
|
logger.remove()
|
||||||
logger.add(sys.stdout,
|
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')
|
logging.basicConfig(level=logging.INFO, format='[%(asctime)s-%(levelname)s-%(module)s-%(lineno)d]- %(message)s')
|
||||||
logger = logging.getLogger(__name__)
|
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))
|
@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):
|
def gemini_text_response(prompt, temperature, top_p, n, max_tokens, system_prompt):
|
||||||
""" Common functiont to get response from gemini pro Text. """
|
""" 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
|
#FIXME: Include : https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/System_instructions_REST.ipynb
|
||||||
try:
|
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:
|
except Exception as err:
|
||||||
logger.error(f"Failed to configure Gemini: {err}")
|
logger.error(f"Failed to configure Gemini: {err}")
|
||||||
|
raise
|
||||||
logger.info(f"Temp: {temperature}, MaxTokens: {max_tokens}, TopP: {top_p}, N: {n}")
|
logger.info(f"Temp: {temperature}, MaxTokens: {max_tokens}, TopP: {top_p}, N: {n}")
|
||||||
# Set up AI model config
|
# Set up AI model config
|
||||||
generation_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)
|
tuple[bool, str]: A tuple containing (is_valid, message)
|
||||||
"""
|
"""
|
||||||
try:
|
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
|
# 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
|
# Try to list models as a simple API test
|
||||||
models = genai.list_models()
|
models = client.models.list()
|
||||||
|
|
||||||
# Check if Gemini Pro is available
|
# 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"
|
return True, "Gemini API key is valid"
|
||||||
else:
|
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:
|
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):
|
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
|
str: The generated text completion
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Configure the model
|
# Get API key with proper error handling
|
||||||
model = genai.GenerativeModel('gemini-pro')
|
api_key = get_gemini_api_key()
|
||||||
|
client = genai.Client(api_key=api_key)
|
||||||
|
|
||||||
# Generate content
|
# Generate content using the new client
|
||||||
response = model.generate_content(
|
response = client.models.generate_content(
|
||||||
prompt,
|
model='gemini-2.5-flash',
|
||||||
generation_config=genai.types.GenerationConfig(
|
contents=prompt,
|
||||||
|
config=types.GenerateContentConfig(
|
||||||
|
max_output_tokens=max_tokens,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
top_p=top_p,
|
top_p=top_p,
|
||||||
top_k=top_k,
|
top_k=top_k,
|
||||||
max_output_tokens=max_tokens,
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return the generated text
|
# 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.
|
Generate structured JSON response using Google's Gemini Pro model.
|
||||||
"""
|
"""
|
||||||
try:
|
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
|
# Build config using official SDK schema type
|
||||||
try:
|
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}")
|
logger.error(f"Error parsing structured response: {e}")
|
||||||
return {"error": f"Failed to parse JSON response: {e}", "raw_response": (response.text or '')}
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error in Gemini Pro structured JSON generation: {e}")
|
logger.error(f"Error in Gemini Pro structured JSON generation: {e}")
|
||||||
return {"error": str(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": {
|
"files": {
|
||||||
"main.css": "/static/css/main.c9966057.css",
|
"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",
|
"index.html": "/index.html",
|
||||||
"main.c9966057.css.map": "/static/css/main.c9966057.css.map",
|
"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": [
|
"entrypoints": [
|
||||||
"static/css/main.c9966057.css",
|
"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,
|
Analytics as AnalyticsIcon,
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
Lightbulb as AIInsightsIcon,
|
Lightbulb as AIInsightsIcon,
|
||||||
Close as CloseIcon
|
Close as CloseIcon,
|
||||||
|
Add as CreateIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import ContentStrategyTab from './tabs/ContentStrategyTab';
|
import ContentStrategyTab from './tabs/ContentStrategyTab';
|
||||||
import CalendarTab from './tabs/CalendarTab';
|
import CalendarTab from './tabs/CalendarTab';
|
||||||
import AnalyticsTab from './tabs/AnalyticsTab';
|
import AnalyticsTab from './tabs/AnalyticsTab';
|
||||||
import GapAnalysisTab from './tabs/GapAnalysisTab';
|
import GapAnalysisTab from './tabs/GapAnalysisTab';
|
||||||
|
import CreateTab from './tabs/CreateTab';
|
||||||
import AIInsightsPanel from './components/AIInsightsPanel';
|
import AIInsightsPanel from './components/AIInsightsPanel';
|
||||||
import ServiceStatusPanel from './components/ServiceStatusPanel';
|
import ServiceStatusPanel from './components/ServiceStatusPanel';
|
||||||
import ProgressIndicator from './components/ProgressIndicator';
|
import ProgressIndicator from './components/ProgressIndicator';
|
||||||
@@ -170,7 +172,8 @@ const ContentPlanningDashboard: React.FC = () => {
|
|||||||
{ label: 'CONTENT STRATEGY', icon: <StrategyIcon />, component: <ContentStrategyTab /> },
|
{ label: 'CONTENT STRATEGY', icon: <StrategyIcon />, component: <ContentStrategyTab /> },
|
||||||
{ label: 'CALENDAR', icon: <CalendarIcon />, component: <CalendarTab /> },
|
{ label: 'CALENDAR', icon: <CalendarIcon />, component: <CalendarTab /> },
|
||||||
{ label: 'ANALYTICS', icon: <AnalyticsIcon />, component: <AnalyticsTab /> },
|
{ 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);
|
const totalAIItems = (dashboardData.aiInsights?.length || 0) + (dashboardData.aiRecommendations?.length || 0);
|
||||||
|
|||||||
@@ -478,7 +478,7 @@ const ContentStrategyBuilder: React.FC = () => {
|
|||||||
onUpdateFormField={updateFormField}
|
onUpdateFormField={updateFormField}
|
||||||
onValidateFormField={validateFormField}
|
onValidateFormField={validateFormField}
|
||||||
onShowTooltip={setShowTooltip}
|
onShowTooltip={setShowTooltip}
|
||||||
onViewDataSource={() => setShowDataSourceTransparency(true)}
|
onViewDataSource={() => setShowDataSourceTransparency(true)}
|
||||||
onConfirmCategoryReview={handleConfirmCategoryReviewWrapper}
|
onConfirmCategoryReview={handleConfirmCategoryReviewWrapper}
|
||||||
onSetActiveCategory={setActiveCategory}
|
onSetActiveCategory={setActiveCategory}
|
||||||
onSetShowEducationalInfo={setShowEducationalInfo}
|
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,
|
TextField,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardActions,
|
|
||||||
Chip,
|
Chip,
|
||||||
IconButton,
|
IconButton,
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -42,7 +41,6 @@ import {
|
|||||||
CalendarToday as CalendarIcon,
|
CalendarToday as CalendarIcon,
|
||||||
Event as EventIcon,
|
Event as EventIcon,
|
||||||
Refresh as RefreshIcon,
|
Refresh as RefreshIcon,
|
||||||
AutoAwesome as AIIcon,
|
|
||||||
TrendingUp as TrendingIcon,
|
TrendingUp as TrendingIcon,
|
||||||
ContentCopy as RepurposeIcon,
|
ContentCopy as RepurposeIcon,
|
||||||
Analytics as AnalyticsIcon,
|
Analytics as AnalyticsIcon,
|
||||||
@@ -64,7 +62,6 @@ import {
|
|||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||||
import CalendarGenerationWizard from '../components/CalendarGenerationWizard';
|
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -100,10 +97,8 @@ const CalendarTab: React.FC = () => {
|
|||||||
updateCalendarEvents,
|
updateCalendarEvents,
|
||||||
// New calendar generation state
|
// New calendar generation state
|
||||||
generatedCalendar,
|
generatedCalendar,
|
||||||
contentOptimization,
|
|
||||||
performancePrediction,
|
performancePrediction,
|
||||||
contentRepurposing,
|
contentRepurposing,
|
||||||
trendingTopics,
|
|
||||||
aiInsights,
|
aiInsights,
|
||||||
calendarGenerationError,
|
calendarGenerationError,
|
||||||
dataLoading
|
dataLoading
|
||||||
@@ -131,7 +126,7 @@ const CalendarTab: React.FC = () => {
|
|||||||
aiAnalysisResults: []
|
aiAnalysisResults: []
|
||||||
});
|
});
|
||||||
|
|
||||||
const [calendarGenerationMode, setCalendarGenerationMode] = useState<'transparency' | 'wizard'>('transparency');
|
const safeCalendarEvents = Array.isArray(calendarEvents) ? calendarEvents : [];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCalendarData();
|
loadCalendarData();
|
||||||
@@ -214,22 +209,6 @@ const CalendarTab: React.FC = () => {
|
|||||||
await loadCalendarData();
|
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) => {
|
const handleDataUpdate = (updatedData: any) => {
|
||||||
setUserData((prev: any) => ({ ...prev, ...updatedData }));
|
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 (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 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 }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||||
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)}>
|
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)}>
|
||||||
<Tab label="Calendar Events" icon={<CalendarIcon />} iconPosition="start" />
|
<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>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -416,102 +389,6 @@ const CalendarTab: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</TabPanel>
|
</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 */}
|
{/* Event Dialog */}
|
||||||
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
|
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
|
||||||
<DialogTitle>
|
<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,
|
Paper,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
TextField,
|
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardActions,
|
CardActions,
|
||||||
Chip,
|
Chip,
|
||||||
Divider,
|
|
||||||
Alert,
|
Alert,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
@@ -39,7 +37,6 @@ import {
|
|||||||
Lightbulb as LightbulbIcon,
|
Lightbulb as LightbulbIcon,
|
||||||
CheckCircle as CheckCircleIcon,
|
CheckCircle as CheckCircleIcon,
|
||||||
Warning as WarningIcon,
|
Warning as WarningIcon,
|
||||||
Search as SearchIcon,
|
|
||||||
Analytics as AnalyticsIcon,
|
Analytics as AnalyticsIcon,
|
||||||
Timeline as TimelineIcon,
|
Timeline as TimelineIcon,
|
||||||
Assessment as AssessmentIcon,
|
Assessment as AssessmentIcon,
|
||||||
@@ -48,36 +45,14 @@ import {
|
|||||||
Add as AddIcon,
|
Add as AddIcon,
|
||||||
Edit as EditIcon,
|
Edit as EditIcon,
|
||||||
Visibility as VisibilityIcon,
|
Visibility as VisibilityIcon,
|
||||||
BarChart as BarChartIcon,
|
|
||||||
PieChart as PieChartIcon,
|
|
||||||
ShowChart as ShowChartIcon,
|
ShowChart as ShowChartIcon,
|
||||||
AutoAwesome as AutoAwesomeIcon
|
AutoAwesome as AutoAwesomeIcon,
|
||||||
|
PlayArrow as PlayArrowIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||||
import ContentStrategyBuilder from '../components/ContentStrategyBuilder';
|
|
||||||
import StrategyIntelligenceTab from '../components/StrategyIntelligenceTab';
|
import StrategyIntelligenceTab from '../components/StrategyIntelligenceTab';
|
||||||
|
import StrategyOnboardingDialog from '../components/StrategyOnboardingDialog';
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContentStrategyTab: React.FC = () => {
|
const ContentStrategyTab: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -85,7 +60,6 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
currentStrategy,
|
currentStrategy,
|
||||||
aiInsights,
|
aiInsights,
|
||||||
aiRecommendations,
|
aiRecommendations,
|
||||||
performanceMetrics,
|
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
loadStrategies,
|
loadStrategies,
|
||||||
@@ -93,7 +67,6 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
loadAIRecommendations
|
loadAIRecommendations
|
||||||
} = useContentPlanningStore();
|
} = useContentPlanningStore();
|
||||||
|
|
||||||
const [tabValue, setTabValue] = useState(0);
|
|
||||||
const [strategyForm, setStrategyForm] = useState({
|
const [strategyForm, setStrategyForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
@@ -104,25 +77,53 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
|
|
||||||
// Real data states
|
// Real data states
|
||||||
const [strategicIntelligence, setStrategicIntelligence] = useState<any>(null);
|
const [strategicIntelligence, setStrategicIntelligence] = useState<any>(null);
|
||||||
const [keywordResearch, setKeywordResearch] = useState<any>(null);
|
|
||||||
const [contentPillars, setContentPillars] = useState<any[]>([]);
|
|
||||||
const [dataLoading, setDataLoading] = useState({
|
const [dataLoading, setDataLoading] = useState({
|
||||||
strategies: false,
|
strategies: false,
|
||||||
insights: false,
|
insights: false,
|
||||||
recommendations: false,
|
recommendations: false,
|
||||||
strategicIntelligence: false,
|
strategicIntelligence: false
|
||||||
keywordResearch: false,
|
|
||||||
pillars: 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
|
// Load data on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadInitialData();
|
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 () => {
|
const loadInitialData = async () => {
|
||||||
try {
|
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
|
// Load strategies
|
||||||
await loadStrategies();
|
await loadStrategies();
|
||||||
@@ -136,16 +137,10 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
// Load strategic intelligence
|
// Load strategic intelligence
|
||||||
await loadStrategicIntelligence();
|
await loadStrategicIntelligence();
|
||||||
|
|
||||||
// Load keyword research
|
|
||||||
await loadKeywordResearch();
|
|
||||||
|
|
||||||
// Load content pillars
|
|
||||||
await loadContentPillars();
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading initial data:', error);
|
console.error('Error loading initial data:', error);
|
||||||
} finally {
|
} 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) => {
|
const handleStrategyFormChange = (field: string, value: string) => {
|
||||||
setStrategyForm(prev => ({
|
setStrategyForm(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -428,6 +273,38 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
await loadInitialData();
|
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 (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
{error && (
|
{error && (
|
||||||
@@ -436,260 +313,72 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
</Alert>
|
</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 }}>
|
<Paper sx={{ width: '100%', mb: 3 }}>
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
<StrategyIntelligenceTab />
|
||||||
<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>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* Strategy Onboarding Dialog */}
|
||||||
|
<StrategyOnboardingDialog
|
||||||
|
open={showOnboarding}
|
||||||
|
onClose={handleCloseOnboarding}
|
||||||
|
onConfirmStrategy={handleConfirmStrategy}
|
||||||
|
onEditStrategy={handleEditStrategy}
|
||||||
|
onCreateNewStrategy={handleCreateNewStrategy}
|
||||||
|
currentStrategy={currentStrategy}
|
||||||
|
strategyStatus={strategyStatus}
|
||||||
|
/>
|
||||||
</Box>
|
</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 {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Grid,
|
Tabs,
|
||||||
Paper,
|
Tab,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Alert
|
||||||
TextField,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
Chip,
|
|
||||||
Divider,
|
|
||||||
Alert,
|
|
||||||
CircularProgress,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
ListItemIcon
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
|
Analytics as AnalyticsIcon,
|
||||||
|
TrendingUp as TrendingIcon,
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
Add as AddIcon,
|
Assessment as AssessmentIcon,
|
||||||
Warning as WarningIcon,
|
BarChart as BarChartIcon,
|
||||||
CheckCircle as CheckCircleIcon,
|
PieChart as PieChartIcon
|
||||||
TrendingUp as TrendingUpIcon,
|
|
||||||
Assessment as AssessmentIcon
|
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
import RefineAnalysisTab from './RefineAnalysisTab';
|
||||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
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 GapAnalysisTab: React.FC = () => {
|
||||||
const {
|
const [tabValue, setTabValue] = useState(0);
|
||||||
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(() => {
|
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||||
loadGapAnalysisData();
|
setTabValue(newValue);
|
||||||
}, []);
|
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
<Typography variant="h4" gutterBottom>
|
<Typography variant="h4" gutterBottom>
|
||||||
Content Gap Analysis
|
Gap Analysis & Optimization
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{error && (
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
<Tabs value={tabValue} onChange={handleTabChange} aria-label="gap analysis tabs">
|
||||||
{error}
|
<Tab label="Content Optimizer" icon={<AnalyticsIcon />} iconPosition="start" />
|
||||||
</Alert>
|
<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}>
|
<TabPanel value={tabValue} index={0}>
|
||||||
{/* Analysis Setup */}
|
<ContentOptimizerTab />
|
||||||
<Grid item xs={12} md={4}>
|
</TabPanel>
|
||||||
<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 */}
|
<TabPanel value={tabValue} index={1}>
|
||||||
<Grid item xs={12} md={8}>
|
<TrendingTopicsTab />
|
||||||
<Paper sx={{ p: 3, mb: 3 }}>
|
</TabPanel>
|
||||||
<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 */}
|
<TabPanel value={tabValue} index={2}>
|
||||||
{transformedGapAnalyses.length > 0 && (
|
<KeywordResearchTab />
|
||||||
<Paper sx={{ p: 3 }}>
|
</TabPanel>
|
||||||
<Typography variant="h6" gutterBottom>
|
|
||||||
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
|
<TabPanel value={tabValue} index={3}>
|
||||||
Detailed Analysis Results
|
<PerformanceAnalyticsTab />
|
||||||
</Typography>
|
</TabPanel>
|
||||||
<Divider sx={{ mb: 2 }} />
|
|
||||||
|
<TabPanel value={tabValue} index={4}>
|
||||||
{transformedGapAnalyses.map((analysis, index) => (
|
<ContentPillarsTab />
|
||||||
<Box key={index} sx={{ mb: 3 }}>
|
</TabPanel>
|
||||||
<Typography variant="subtitle1" gutterBottom>
|
|
||||||
Analysis for {analysis.website_url}
|
<TabPanel value={tabValue} index={5}>
|
||||||
</Typography>
|
<RefineAnalysisTab />
|
||||||
|
</TabPanel>
|
||||||
{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>
|
</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