ALwrity Backend and Frontend - Stability and Error Handling Improvements
This commit is contained in:
@@ -1,108 +0,0 @@
|
||||
# Stability AI Configuration Example
|
||||
# Copy this file to .env and fill in your actual values
|
||||
|
||||
# Required: Your Stability AI API Key
|
||||
# Get your API key from: https://platform.stability.ai/account/keys
|
||||
STABILITY_API_KEY=your_stability_api_key_here
|
||||
|
||||
# Optional: Stability AI API Base URL (default: https://api.stability.ai)
|
||||
STABILITY_BASE_URL=https://api.stability.ai
|
||||
|
||||
# Optional: Request timeout in seconds (default: 300)
|
||||
STABILITY_TIMEOUT=300
|
||||
|
||||
# Optional: Maximum retries for failed requests (default: 3)
|
||||
STABILITY_MAX_RETRIES=3
|
||||
|
||||
# Optional: Maximum file size for uploads in bytes (default: 10MB)
|
||||
STABILITY_MAX_FILE_SIZE=10485760
|
||||
|
||||
# Optional: Enable debug mode for detailed logging (default: false)
|
||||
STABILITY_DEBUG=false
|
||||
|
||||
# Optional: Enable caching for responses (default: true)
|
||||
STABILITY_ENABLE_CACHE=true
|
||||
|
||||
# Optional: Cache duration in seconds (default: 3600)
|
||||
STABILITY_CACHE_DURATION=3600
|
||||
|
||||
# Optional: Enable rate limiting (default: true)
|
||||
STABILITY_ENABLE_RATE_LIMIT=true
|
||||
|
||||
# Optional: Rate limit - requests per window (default: 150)
|
||||
STABILITY_RATE_LIMIT_REQUESTS=150
|
||||
|
||||
# Optional: Rate limit window in seconds (default: 10)
|
||||
STABILITY_RATE_LIMIT_WINDOW=10
|
||||
|
||||
# Optional: Enable content moderation (default: true)
|
||||
STABILITY_ENABLE_MODERATION=true
|
||||
|
||||
# Optional: Enable request logging (default: true)
|
||||
STABILITY_ENABLE_LOGGING=true
|
||||
|
||||
# Optional: Maximum log entries to keep in memory (default: 1000)
|
||||
STABILITY_MAX_LOG_ENTRIES=1000
|
||||
|
||||
# Optional: Enable experimental features (default: false)
|
||||
STABILITY_ENABLE_EXPERIMENTAL=false
|
||||
|
||||
# Optional: Default output format for images (default: png)
|
||||
STABILITY_DEFAULT_IMAGE_FORMAT=png
|
||||
|
||||
# Optional: Default output format for audio (default: mp3)
|
||||
STABILITY_DEFAULT_AUDIO_FORMAT=mp3
|
||||
|
||||
# Optional: Enable webhook support (default: false)
|
||||
STABILITY_ENABLE_WEBHOOKS=false
|
||||
|
||||
# Optional: Webhook URL for generation completion notifications
|
||||
STABILITY_WEBHOOK_URL=
|
||||
|
||||
# Optional: Webhook secret for signature validation
|
||||
STABILITY_WEBHOOK_SECRET=
|
||||
|
||||
# Optional: Enable batch processing (default: true)
|
||||
STABILITY_ENABLE_BATCH=true
|
||||
|
||||
# Optional: Maximum batch size (default: 10)
|
||||
STABILITY_MAX_BATCH_SIZE=10
|
||||
|
||||
# Optional: Enable quality analysis features (default: true)
|
||||
STABILITY_ENABLE_QUALITY_ANALYSIS=true
|
||||
|
||||
# Optional: Enable prompt optimization features (default: true)
|
||||
STABILITY_ENABLE_PROMPT_OPTIMIZATION=true
|
||||
|
||||
# Optional: Default creativity level for upscaling (default: 0.35)
|
||||
STABILITY_DEFAULT_CREATIVITY=0.35
|
||||
|
||||
# Optional: Default control strength for control operations (default: 0.7)
|
||||
STABILITY_DEFAULT_CONTROL_STRENGTH=0.7
|
||||
|
||||
# Optional: Default style fidelity for style operations (default: 0.5)
|
||||
STABILITY_DEFAULT_STYLE_FIDELITY=0.5
|
||||
|
||||
# Optional: Enable automatic image format optimization (default: true)
|
||||
STABILITY_AUTO_OPTIMIZE_FORMAT=true
|
||||
|
||||
# Optional: Enable automatic parameter optimization (default: true)
|
||||
STABILITY_AUTO_OPTIMIZE_PARAMS=true
|
||||
|
||||
# Optional: Default model for generate operations (default: core)
|
||||
STABILITY_DEFAULT_GENERATE_MODEL=core
|
||||
|
||||
# Optional: Default model for upscale operations (default: fast)
|
||||
STABILITY_DEFAULT_UPSCALE_MODEL=fast
|
||||
|
||||
# Optional: Enable cost tracking and warnings (default: true)
|
||||
STABILITY_ENABLE_COST_TRACKING=true
|
||||
|
||||
# Optional: Credit warning threshold (default: 10)
|
||||
STABILITY_CREDIT_WARNING_THRESHOLD=10
|
||||
|
||||
# Optional: Enable performance monitoring (default: true)
|
||||
STABILITY_ENABLE_MONITORING=true
|
||||
|
||||
# Optional: Performance monitoring interval in seconds (default: 60)
|
||||
STABILITY_MONITORING_INTERVAL=60
|
||||
@@ -16,72 +16,98 @@ class DatabaseSetup:
|
||||
|
||||
def setup_essential_tables(self) -> bool:
|
||||
"""Set up essential database tables."""
|
||||
print("📊 Setting up essential database tables...")
|
||||
import os
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
if verbose:
|
||||
print("📊 Setting up essential database tables...")
|
||||
|
||||
try:
|
||||
from services.database import init_database, engine
|
||||
|
||||
# Initialize database connection
|
||||
init_database()
|
||||
print(" ✅ Database connection initialized")
|
||||
if verbose:
|
||||
print(" ✅ Database connection initialized")
|
||||
|
||||
# Create essential tables
|
||||
self._create_monitoring_tables()
|
||||
self._create_subscription_tables()
|
||||
self._create_persona_tables()
|
||||
|
||||
print("✅ Essential database tables created")
|
||||
if verbose:
|
||||
print("✅ Essential database tables created")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Warning: Database setup failed: {e}")
|
||||
if self.production_mode:
|
||||
print(" Continuing in production mode...")
|
||||
return True
|
||||
else:
|
||||
print(" This may affect functionality")
|
||||
return True # Don't fail startup for database issues
|
||||
if verbose:
|
||||
print(f"⚠️ Warning: Database setup failed: {e}")
|
||||
if self.production_mode:
|
||||
print(" Continuing in production mode...")
|
||||
else:
|
||||
print(" This may affect functionality")
|
||||
return True # Don't fail startup for database issues
|
||||
|
||||
def _create_monitoring_tables(self) -> bool:
|
||||
"""Create API monitoring tables."""
|
||||
import os
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
try:
|
||||
from models.api_monitoring import Base as MonitoringBase
|
||||
MonitoringBase.metadata.create_all(bind=engine)
|
||||
print(" ✅ Monitoring tables created")
|
||||
if verbose:
|
||||
print(" ✅ Monitoring tables created")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Monitoring tables failed: {e}")
|
||||
if verbose:
|
||||
print(f" ⚠️ Monitoring tables failed: {e}")
|
||||
return True # Non-critical
|
||||
|
||||
def _create_subscription_tables(self) -> bool:
|
||||
"""Create subscription and billing tables."""
|
||||
import os
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
try:
|
||||
from models.subscription_models import Base as SubscriptionBase
|
||||
SubscriptionBase.metadata.create_all(bind=engine)
|
||||
print(" ✅ Subscription tables created")
|
||||
if verbose:
|
||||
print(" ✅ Subscription tables created")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Subscription tables failed: {e}")
|
||||
if verbose:
|
||||
print(f" ⚠️ Subscription tables failed: {e}")
|
||||
return True # Non-critical
|
||||
|
||||
def _create_persona_tables(self) -> bool:
|
||||
"""Create persona analysis tables."""
|
||||
import os
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
try:
|
||||
from models.persona_models import Base as PersonaBase
|
||||
PersonaBase.metadata.create_all(bind=engine)
|
||||
print(" ✅ Persona tables created")
|
||||
if verbose:
|
||||
print(" ✅ Persona tables created")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Persona tables failed: {e}")
|
||||
if verbose:
|
||||
print(f" ⚠️ Persona tables failed: {e}")
|
||||
return True # Non-critical
|
||||
|
||||
def verify_tables(self) -> bool:
|
||||
"""Verify that essential tables exist."""
|
||||
import os
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
if self.production_mode:
|
||||
print("⚠️ Skipping table verification in production mode")
|
||||
if verbose:
|
||||
print("⚠️ Skipping table verification in production mode")
|
||||
return True
|
||||
|
||||
print("🔍 Verifying database tables...")
|
||||
if verbose:
|
||||
print("🔍 Verifying database tables...")
|
||||
|
||||
try:
|
||||
from services.database import engine
|
||||
@@ -97,11 +123,13 @@ class DatabaseSetup:
|
||||
]
|
||||
|
||||
existing_tables = [table for table in essential_tables if table in tables]
|
||||
print(f" ✅ Found tables: {existing_tables}")
|
||||
if verbose:
|
||||
print(f" ✅ Found tables: {existing_tables}")
|
||||
|
||||
if len(existing_tables) < len(essential_tables):
|
||||
missing = [table for table in essential_tables if table not in existing_tables]
|
||||
print(f" ⚠️ Missing tables: {missing}")
|
||||
if verbose:
|
||||
print(f" ⚠️ Missing tables: {missing}")
|
||||
|
||||
return True
|
||||
|
||||
@@ -124,11 +152,11 @@ class DatabaseSetup:
|
||||
# Set up billing tables
|
||||
self._setup_billing_tables()
|
||||
|
||||
print("✅ Advanced database features configured")
|
||||
logger.debug("✅ Advanced database features configured")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Advanced table setup failed: {e}")
|
||||
logger.warning(f"Advanced table setup failed: {e}")
|
||||
return True # Non-critical
|
||||
|
||||
def _setup_monitoring_tables(self) -> bool:
|
||||
@@ -157,16 +185,16 @@ class DatabaseSetup:
|
||||
|
||||
# Check if tables already exist
|
||||
if check_existing_tables(engine):
|
||||
print(" ✅ Billing tables already exist")
|
||||
logger.debug("✅ Billing tables already exist")
|
||||
return True
|
||||
|
||||
if create_billing_tables():
|
||||
print(" ✅ Billing tables created")
|
||||
logger.debug("✅ Billing tables created")
|
||||
return True
|
||||
else:
|
||||
print(" ⚠️ Billing setup failed")
|
||||
logger.warning("Billing setup failed")
|
||||
return True # Non-critical
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Billing setup failed: {e}")
|
||||
logger.warning(f"Billing setup failed: {e}")
|
||||
return True # Non-critical
|
||||
|
||||
@@ -51,40 +51,54 @@ class DependencyManager:
|
||||
|
||||
def check_critical_dependencies(self) -> Tuple[bool, List[str]]:
|
||||
"""Check if critical dependencies are available."""
|
||||
print("🔍 Checking critical dependencies...")
|
||||
import os
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
if verbose:
|
||||
print("🔍 Checking critical dependencies...")
|
||||
|
||||
missing_packages = []
|
||||
|
||||
for package in self.critical_packages:
|
||||
try:
|
||||
__import__(package.replace('-', '_'))
|
||||
print(f" ✅ {package}")
|
||||
if verbose:
|
||||
print(f" ✅ {package}")
|
||||
except ImportError:
|
||||
print(f" ❌ {package} - MISSING")
|
||||
if verbose:
|
||||
print(f" ❌ {package} - MISSING")
|
||||
missing_packages.append(package)
|
||||
|
||||
if missing_packages:
|
||||
print(f"❌ Missing critical packages: {', '.join(missing_packages)}")
|
||||
if verbose:
|
||||
print(f"❌ Missing critical packages: {', '.join(missing_packages)}")
|
||||
return False, missing_packages
|
||||
|
||||
print("✅ All critical dependencies available!")
|
||||
if verbose:
|
||||
print("✅ All critical dependencies available!")
|
||||
return True, []
|
||||
|
||||
def check_optional_dependencies(self) -> Tuple[bool, List[str]]:
|
||||
"""Check if optional dependencies are available."""
|
||||
print("🔍 Checking optional dependencies...")
|
||||
import os
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
if verbose:
|
||||
print("🔍 Checking optional dependencies...")
|
||||
|
||||
missing_packages = []
|
||||
|
||||
for package in self.optional_packages:
|
||||
try:
|
||||
__import__(package.replace('-', '_'))
|
||||
print(f" ✅ {package}")
|
||||
if verbose:
|
||||
print(f" ✅ {package}")
|
||||
except ImportError:
|
||||
print(f" ⚠️ {package} - MISSING (optional)")
|
||||
if verbose:
|
||||
print(f" ⚠️ {package} - MISSING (optional)")
|
||||
missing_packages.append(package)
|
||||
|
||||
if missing_packages:
|
||||
if missing_packages and verbose:
|
||||
print(f"⚠️ Missing optional packages: {', '.join(missing_packages)}")
|
||||
print(" Some features may not be available")
|
||||
|
||||
|
||||
@@ -28,21 +28,29 @@ class EnvironmentSetup:
|
||||
|
||||
def setup_directories(self) -> bool:
|
||||
"""Create necessary directories for ALwrity."""
|
||||
print("📁 Setting up directories...")
|
||||
import os
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
if verbose:
|
||||
print("📁 Setting up directories...")
|
||||
|
||||
if not self.required_directories:
|
||||
print(" ⚠️ Skipping directory creation in production mode")
|
||||
if verbose:
|
||||
print(" ⚠️ Skipping directory creation in production mode")
|
||||
return True
|
||||
|
||||
for directory in self.required_directories:
|
||||
try:
|
||||
Path(directory).mkdir(parents=True, exist_ok=True)
|
||||
print(f" ✅ Created: {directory}")
|
||||
if verbose:
|
||||
print(f" ✅ Created: {directory}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Failed to create {directory}: {e}")
|
||||
if verbose:
|
||||
print(f" ❌ Failed to create {directory}: {e}")
|
||||
return False
|
||||
|
||||
print("✅ All directories created successfully")
|
||||
if verbose:
|
||||
print("✅ All directories created successfully")
|
||||
return True
|
||||
|
||||
def setup_environment_variables(self) -> bool:
|
||||
|
||||
@@ -18,22 +18,31 @@ class RouterManager:
|
||||
|
||||
def include_router_safely(self, router, router_name: str = None) -> bool:
|
||||
"""Include a router safely with error handling."""
|
||||
import os
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
try:
|
||||
self.app.include_router(router)
|
||||
router_name = router_name or getattr(router, 'prefix', 'unknown')
|
||||
self.included_routers.append(router_name)
|
||||
logger.info(f"✅ Router included successfully: {router_name}")
|
||||
if verbose:
|
||||
logger.info(f"✅ Router included successfully: {router_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
router_name = router_name or 'unknown'
|
||||
self.failed_routers.append({"name": router_name, "error": str(e)})
|
||||
logger.warning(f"❌ Router inclusion failed: {router_name} - {e}")
|
||||
if verbose:
|
||||
logger.warning(f"❌ Router inclusion failed: {router_name} - {e}")
|
||||
return False
|
||||
|
||||
def include_core_routers(self) -> bool:
|
||||
"""Include core application routers."""
|
||||
import os
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
try:
|
||||
logger.info("Including core routers...")
|
||||
if verbose:
|
||||
logger.info("Including core routers...")
|
||||
|
||||
# Component logic router
|
||||
from api.component_logic import router as component_logic_router
|
||||
|
||||
@@ -70,129 +70,103 @@ class OnboardingSummaryService:
|
||||
try:
|
||||
db = next(get_db())
|
||||
api_keys = self.db_service.get_api_keys(self.user_id, db)
|
||||
logger.info(f"Retrieved {len(api_keys)} API keys from database for user {self.user_id}")
|
||||
return api_keys
|
||||
db.close()
|
||||
|
||||
if not api_keys:
|
||||
return {
|
||||
"openai": {"configured": False, "value": None},
|
||||
"anthropic": {"configured": False, "value": None},
|
||||
"google": {"configured": False, "value": None}
|
||||
}
|
||||
|
||||
return {
|
||||
"openai": {
|
||||
"configured": bool(api_keys.get('openai_api_key')),
|
||||
"value": api_keys.get('openai_api_key')[:8] + "..." if api_keys.get('openai_api_key') else None
|
||||
},
|
||||
"anthropic": {
|
||||
"configured": bool(api_keys.get('anthropic_api_key')),
|
||||
"value": api_keys.get('anthropic_api_key')[:8] + "..." if api_keys.get('anthropic_api_key') else None
|
||||
},
|
||||
"google": {
|
||||
"configured": bool(api_keys.get('google_api_key')),
|
||||
"value": api_keys.get('google_api_key')[:8] + "..." if api_keys.get('google_api_key') else None
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting API keys from database: {e}")
|
||||
return {}
|
||||
logger.error(f"Error getting API keys: {str(e)}")
|
||||
return {
|
||||
"openai": {"configured": False, "value": None},
|
||||
"anthropic": {"configured": False, "value": None},
|
||||
"google": {"configured": False, "value": None}
|
||||
}
|
||||
|
||||
def _get_website_analysis(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get website analysis data from database (Step 2)."""
|
||||
"""Get website analysis data from database."""
|
||||
try:
|
||||
db = next(get_db())
|
||||
website_data = self.db_service.get_website_analysis(self.user_id, db)
|
||||
if website_data:
|
||||
logger.info(f"Retrieved website analysis from database for user {self.user_id}")
|
||||
else:
|
||||
logger.warning(f"No website analysis found in database for user {self.user_id}")
|
||||
db.close()
|
||||
return website_data
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting website analysis from database: {e}")
|
||||
logger.error(f"Error getting website analysis: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_research_preferences(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get research preferences data from database (Step 3)."""
|
||||
"""Get research preferences from database."""
|
||||
try:
|
||||
db = next(get_db())
|
||||
research_data = self.db_service.get_research_preferences(self.user_id, db)
|
||||
if research_data:
|
||||
logger.info(f"Retrieved research preferences from database for user {self.user_id}")
|
||||
else:
|
||||
logger.warning(f"No research preferences found in database for user {self.user_id}")
|
||||
return research_data
|
||||
preferences = self.db_service.get_research_preferences(self.user_id, db)
|
||||
db.close()
|
||||
return preferences
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting research preferences from database: {e}")
|
||||
logger.error(f"Error getting research preferences: {str(e)}")
|
||||
return None
|
||||
|
||||
def _get_personalization_settings(self, research_preferences: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
"""Get personalization settings from Step 4 (Persona) database."""
|
||||
try:
|
||||
# Try to get from Step 4 (Persona) in database
|
||||
db = next(get_db())
|
||||
persona_data = self.db_service.get_persona_data(self.user_id, db)
|
||||
def _get_personalization_settings(self, research_preferences: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Get personalization settings based on research preferences."""
|
||||
if not research_preferences:
|
||||
return {
|
||||
"writing_style": "professional",
|
||||
"target_audience": "general",
|
||||
"content_focus": "informative"
|
||||
}
|
||||
|
||||
if persona_data:
|
||||
logger.info(f"Retrieved persona data from database for user {self.user_id}")
|
||||
# Extract personalization settings from persona data
|
||||
if 'corePersona' in persona_data:
|
||||
core_persona = persona_data.get('corePersona', {})
|
||||
return {
|
||||
'writing_style': core_persona.get('linguistic_fingerprint', {}).get('tone', 'Professional'),
|
||||
'tone': core_persona.get('tonal_range', {}).get('primary_tone', 'Formal'),
|
||||
'brand_voice': core_persona.get('identity', {}).get('voice', 'Trustworthy and Expert')
|
||||
}
|
||||
return {
|
||||
"writing_style": research_preferences.get('writing_style', 'professional'),
|
||||
"target_audience": research_preferences.get('target_audience', 'general'),
|
||||
"content_focus": research_preferences.get('content_focus', 'informative')
|
||||
}
|
||||
|
||||
# Fallback to research preferences if persona data not available
|
||||
if research_preferences:
|
||||
logger.info(f"Using research preferences as fallback for personalization")
|
||||
return {
|
||||
'writing_style': research_preferences.get('writing_style', {}).get('tone', 'Professional'),
|
||||
'tone': research_preferences.get('writing_style', {}).get('voice', 'Formal'),
|
||||
'brand_voice': research_preferences.get('writing_style', {}).get('complexity', 'Trustworthy and Expert')
|
||||
}
|
||||
def _check_persona_readiness(self, website_analysis: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
"""Check if persona generation is ready based on available data."""
|
||||
if not website_analysis:
|
||||
return {
|
||||
"ready": False,
|
||||
"reason": "Website analysis not completed",
|
||||
"missing_data": ["website_url", "style_analysis"]
|
||||
}
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting personalization settings from database: {e}")
|
||||
return None
|
||||
required_fields = ['website_url', 'writing_style', 'target_audience']
|
||||
missing_fields = [field for field in required_fields if not website_analysis.get(field)]
|
||||
|
||||
def _check_persona_readiness(self, website_analysis: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
"""Check if persona can be generated."""
|
||||
try:
|
||||
persona_service = PersonaAnalysisService()
|
||||
|
||||
# Check if persona can be generated
|
||||
onboarding_data = persona_service._collect_onboarding_data(self.user_id)
|
||||
if onboarding_data:
|
||||
data_sufficiency = persona_service._calculate_data_sufficiency(onboarding_data)
|
||||
return {
|
||||
"ready": data_sufficiency >= 50.0,
|
||||
"data_sufficiency": data_sufficiency,
|
||||
"can_generate": website_analysis is not None
|
||||
}
|
||||
return {"ready": False, "data_sufficiency": 0.0, "can_generate": False}
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not check persona readiness: {str(e)}")
|
||||
return {"ready": False, "error": str(e)}
|
||||
return {
|
||||
"ready": len(missing_fields) == 0,
|
||||
"reason": "All required data available" if len(missing_fields) == 0 else f"Missing: {', '.join(missing_fields)}",
|
||||
"missing_data": missing_fields
|
||||
}
|
||||
|
||||
def _determine_capabilities(self, api_keys: Dict[str, Any], website_analysis: Optional[Dict[str, Any]],
|
||||
research_preferences: Optional[Dict[str, Any]],
|
||||
personalization_settings: Optional[Dict[str, Any]],
|
||||
persona_readiness: Optional[Dict[str, Any]]) -> Dict[str, bool]:
|
||||
"""Determine user capabilities based on onboarding data."""
|
||||
return {
|
||||
"ai_content": len(api_keys) > 0,
|
||||
"style_analysis": website_analysis is not None,
|
||||
"research_tools": research_preferences is not None,
|
||||
"personalization": personalization_settings is not None,
|
||||
"persona_generation": persona_readiness.get("ready", False) if persona_readiness else False,
|
||||
"integrations": False # TODO: Implement
|
||||
personalization_settings: Dict[str, Any],
|
||||
persona_readiness: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Determine available capabilities based on configured data."""
|
||||
capabilities = {
|
||||
"ai_content_generation": any(key.get("configured") for key in api_keys.values()),
|
||||
"website_analysis": website_analysis is not None,
|
||||
"research_capabilities": research_preferences is not None,
|
||||
"persona_generation": persona_readiness.get("ready", False),
|
||||
"content_optimization": website_analysis is not None and research_preferences is not None
|
||||
}
|
||||
|
||||
async def get_website_analysis_data(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get website analysis data for FinalStep."""
|
||||
try:
|
||||
analysis = self._get_website_analysis()
|
||||
|
||||
if analysis:
|
||||
return {
|
||||
"website_url": analysis.get('website_url'),
|
||||
"style_analysis": analysis.get('style_analysis'),
|
||||
"style_patterns": analysis.get('style_patterns'),
|
||||
"style_guidelines": analysis.get('style_guidelines'),
|
||||
"status": analysis.get('status'),
|
||||
"completed_at": analysis.get('created_at')
|
||||
}
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting website analysis data: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
async def get_research_preferences_data(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get research preferences data for FinalStep."""
|
||||
try:
|
||||
return self._get_research_preferences()
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting research preferences data: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Internal server error")
|
||||
return capabilities
|
||||
@@ -19,6 +19,10 @@ from alwrity_utils import HealthChecker, RateLimiter, FrontendServing, RouterMan
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
# Set up clean logging for end users
|
||||
from logging_config import setup_clean_logging
|
||||
setup_clean_logging()
|
||||
|
||||
# Import middleware
|
||||
from middleware.auth_middleware import get_current_user
|
||||
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
# Clerk Authentication
|
||||
CLERK_SECRET_KEY=your_clerk_secret_key_here
|
||||
CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key_here
|
||||
# ALwrity Backend Configuration
|
||||
|
||||
# Google Search Console
|
||||
GSC_REDIRECT_URI=your-domain-name/gsc/callback
|
||||
# API Keys (Configure these in the onboarding process)
|
||||
# OPENAI_API_KEY=your_openai_api_key_here
|
||||
GEMINI_API_KEY=your_gemini_api_key_here
|
||||
# ANTHROPIC_API_KEY=your_anthropic_api_key_here
|
||||
# MISTRAL_API_KEY=your_mistral_api_key_here
|
||||
|
||||
# Wix Integration (Headless OAuth - Client ID only, no Client Secret required)
|
||||
WIX_CLIENT_ID=
|
||||
WIX_REDIRECT_URI=your-domain-name/wix/callback
|
||||
# Research API Keys (Optional)
|
||||
# TAVILY_API_KEY=your_tavily_api_key_here
|
||||
# SERPER_API_KEY=your_serper_api_key_here
|
||||
EXA_API_KEY=your_exa_api_key_here
|
||||
|
||||
# WordPress.com OAuth2 Integration
|
||||
# IMPORTANT: You need to register a WordPress.com application to get valid credentials
|
||||
# 1. Go to https://developer.wordpress.com/apps/
|
||||
# 2. Create a new application
|
||||
# 3. Set the redirect URI to: https://your-domain.com/wp/callback
|
||||
# 4. Copy the Client ID and Client Secret below
|
||||
# For development, these are placeholder values that may not work
|
||||
WORDPRESS_CLIENT_ID=your_wordpress_com_client_id_here
|
||||
WORDPRESS_CLIENT_SECRET=your_wordpress_com_client_secret_here
|
||||
WORDPRESS_REDIRECT_URI=
|
||||
# Authentication
|
||||
# CLERK_SECRET_KEY=your_clerk_secret_key_here
|
||||
|
||||
# Development Settings
|
||||
DISABLE_AUTH=false
|
||||
# OAuth Redirect URIs
|
||||
GSC_REDIRECT_URI=https://your-frontend.vercel.app/gsc/callback
|
||||
WORDPRESS_REDIRECT_URI=https://your-frontend.vercel.app/wp/callback
|
||||
WIX_REDIRECT_URI=https://your-frontend.vercel.app/wix/callback
|
||||
|
||||
# local development
|
||||
DEPLOY_ENV=local
|
||||
|
||||
# Server Configuration
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
DEBUG=true
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
110
backend/logging_config.py
Normal file
110
backend/logging_config.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Logging configuration for ALwrity backend.
|
||||
Provides clean logging setup for end users vs developers.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def setup_clean_logging():
|
||||
"""Set up clean logging for end users."""
|
||||
verbose_mode = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
if not verbose_mode:
|
||||
# Suppress verbose logging for end users - be more aggressive
|
||||
logging.getLogger('sqlalchemy.engine').setLevel(logging.CRITICAL)
|
||||
logging.getLogger('sqlalchemy.pool').setLevel(logging.CRITICAL)
|
||||
logging.getLogger('sqlalchemy.dialects').setLevel(logging.CRITICAL)
|
||||
logging.getLogger('sqlalchemy.orm').setLevel(logging.CRITICAL)
|
||||
logging.getLogger('sqlalchemy').setLevel(logging.CRITICAL)
|
||||
logging.getLogger('sqlalchemy.engine.Engine').setLevel(logging.CRITICAL)
|
||||
|
||||
# Suppress service initialization logs
|
||||
logging.getLogger('services').setLevel(logging.WARNING)
|
||||
logging.getLogger('api').setLevel(logging.WARNING)
|
||||
logging.getLogger('models').setLevel(logging.WARNING)
|
||||
|
||||
# Suppress specific noisy loggers
|
||||
noisy_loggers = [
|
||||
'linkedin_persona_service',
|
||||
'facebook_persona_service',
|
||||
'core_persona_service',
|
||||
'persona_analysis_service',
|
||||
'ai_service_manager',
|
||||
'ai_engine_service',
|
||||
'website_analyzer',
|
||||
'competitor_analyzer',
|
||||
'keyword_researcher',
|
||||
'content_gap_analyzer',
|
||||
'onboarding_data_service',
|
||||
'comprehensive_user_data',
|
||||
'strategy_data',
|
||||
'gap_analysis_data',
|
||||
'phase1_steps',
|
||||
'daily_schedule_generator',
|
||||
'gsc_service',
|
||||
'wordpress_oauth',
|
||||
'data_filter',
|
||||
'source_mapper',
|
||||
'grounding_engine',
|
||||
'blog_content_seo_analyzer',
|
||||
'linkedin_service',
|
||||
'citation_manager',
|
||||
'content_analyzer',
|
||||
'linkedin_prompt_generator',
|
||||
'linkedin_image_storage',
|
||||
'hallucination_detector',
|
||||
'writing_assistant',
|
||||
'onboarding_data_service',
|
||||
'enhanced_linguistic_analyzer',
|
||||
'persona_quality_improver',
|
||||
'logging_middleware',
|
||||
'exa_service',
|
||||
'step3_research_service',
|
||||
'sitemap_service',
|
||||
'linkedin_image_generator',
|
||||
'linkedin_prompt_generator',
|
||||
'linkedin_image_storage',
|
||||
'router_manager',
|
||||
'frontend_serving',
|
||||
'database',
|
||||
'user_business_info',
|
||||
'auth_middleware',
|
||||
'pricing_service',
|
||||
'create_billing_tables'
|
||||
]
|
||||
|
||||
for logger_name in noisy_loggers:
|
||||
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
||||
|
||||
# Configure loguru to be less verbose (only show warnings and errors)
|
||||
logger.remove() # Remove default handler
|
||||
|
||||
def warning_only_filter(record):
|
||||
return record["level"].name in ["WARNING", "ERROR", "CRITICAL"]
|
||||
|
||||
logger.add(
|
||||
sys.stdout.write,
|
||||
level="WARNING",
|
||||
format="{time:HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}\n",
|
||||
filter=warning_only_filter
|
||||
)
|
||||
else:
|
||||
# In verbose mode, show all log levels with detailed formatting
|
||||
logger.remove() # Remove default handler
|
||||
logger.add(
|
||||
sys.stdout.write,
|
||||
level="DEBUG",
|
||||
format="{time:HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}\n"
|
||||
)
|
||||
|
||||
return verbose_mode
|
||||
|
||||
|
||||
def get_uvicorn_log_level():
|
||||
"""Get appropriate uvicorn log level based on verbose mode."""
|
||||
verbose_mode = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
return "debug" if verbose_mode else "warning"
|
||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
logger.info("🔄 Loading UserBusinessInfo model...")
|
||||
logger.debug("🔄 Loading UserBusinessInfo model...")
|
||||
|
||||
class UserBusinessInfo(Base):
|
||||
__tablename__ = 'user_business_info'
|
||||
@@ -35,4 +35,4 @@ class UserBusinessInfo(Base):
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
logger.info("✅ UserBusinessInfo model loaded successfully!")
|
||||
logger.debug("✅ UserBusinessInfo model loaded successfully!")
|
||||
|
||||
@@ -26,12 +26,12 @@ def create_billing_tables():
|
||||
|
||||
try:
|
||||
# Create engine
|
||||
engine = create_engine(DATABASE_URL, echo=True)
|
||||
engine = create_engine(DATABASE_URL, echo=False)
|
||||
|
||||
# Create all tables
|
||||
logger.info("Creating billing and subscription system tables...")
|
||||
logger.debug("Creating billing and subscription system tables...")
|
||||
SubscriptionBase.metadata.create_all(bind=engine)
|
||||
logger.info("✅ Billing and subscription tables created successfully")
|
||||
logger.debug("✅ Billing and subscription tables created successfully")
|
||||
|
||||
# Create session for data initialization
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
@@ -41,13 +41,13 @@ def create_billing_tables():
|
||||
# Initialize pricing and plans
|
||||
pricing_service = PricingService(db)
|
||||
|
||||
logger.info("Initializing default API pricing...")
|
||||
logger.debug("Initializing default API pricing...")
|
||||
pricing_service.initialize_default_pricing()
|
||||
logger.info("✅ Default API pricing initialized")
|
||||
logger.debug("✅ Default API pricing initialized")
|
||||
|
||||
logger.info("Initializing default subscription plans...")
|
||||
logger.debug("Initializing default subscription plans...")
|
||||
pricing_service.initialize_default_plans()
|
||||
logger.info("✅ Default subscription plans initialized")
|
||||
logger.debug("✅ Default subscription plans initialized")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error initializing default data: {e}")
|
||||
@@ -57,7 +57,7 @@ def create_billing_tables():
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
logger.info("🎉 Billing system setup completed successfully!")
|
||||
logger.info("✅ Billing system setup completed successfully!")
|
||||
|
||||
# Display summary
|
||||
display_setup_summary(engine)
|
||||
@@ -94,7 +94,7 @@ def display_setup_summary(engine):
|
||||
|
||||
logger.info(f"\n📊 Created Tables ({len(tables)}):")
|
||||
for table in tables:
|
||||
logger.info(f" • {table[0]}")
|
||||
logger.debug(f" • {table[0]}")
|
||||
|
||||
# Check subscription plans
|
||||
try:
|
||||
@@ -114,7 +114,7 @@ def display_setup_summary(engine):
|
||||
|
||||
for plan in plans:
|
||||
name, tier, monthly, yearly = plan
|
||||
logger.info(f" • {name} ({tier}): ${monthly}/month, ${yearly}/year")
|
||||
logger.debug(f" • {name} ({tier}): ${monthly}/month, ${yearly}/year")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not check subscription plans: {e}")
|
||||
|
||||
@@ -139,7 +139,7 @@ def display_setup_summary(engine):
|
||||
logger.info("\n LLM Pricing (per token) - Top 10:")
|
||||
for entry in pricing_entries:
|
||||
provider, model, input_cost, output_cost = entry
|
||||
logger.info(f" • {provider}/{model}: ${input_cost:.8f} in, ${output_cost:.8f} out")
|
||||
logger.debug(f" • {provider}/{model}: ${input_cost:.8f} in, ${output_cost:.8f} out")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not check API pricing: {e}")
|
||||
|
||||
@@ -183,7 +183,7 @@ def check_existing_tables(engine):
|
||||
|
||||
if existing_tables:
|
||||
logger.warning(f"Found existing billing tables: {[t[0] for t in existing_tables]}")
|
||||
logger.info("Tables already exist. Skipping creation to preserve data.")
|
||||
logger.debug("Tables already exist. Skipping creation to preserve data.")
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -193,7 +193,7 @@ def check_existing_tables(engine):
|
||||
return True # Proceed anyway
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("🚀 Starting billing system database migration...")
|
||||
logger.debug("🚀 Starting billing system database migration...")
|
||||
|
||||
try:
|
||||
# Create engine to check existing tables
|
||||
@@ -201,7 +201,7 @@ if __name__ == "__main__":
|
||||
|
||||
# Check existing tables
|
||||
if not check_existing_tables(engine):
|
||||
logger.info("✅ Billing tables already exist, skipping creation")
|
||||
logger.debug("✅ Billing tables already exist, skipping creation")
|
||||
sys.exit(0)
|
||||
|
||||
# Create tables and initialize data
|
||||
@@ -210,7 +210,7 @@ if __name__ == "__main__":
|
||||
logger.info("✅ Billing system migration completed successfully!")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Migration cancelled by user")
|
||||
logger.warning("Migration cancelled by user")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Migration failed: {e}")
|
||||
|
||||
@@ -47,15 +47,26 @@ class AIServiceMetrics:
|
||||
class AIServiceManager:
|
||||
"""Centralized AI service management for content planning system."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize AI service manager."""
|
||||
self.logger = logger
|
||||
self.metrics: List[AIServiceMetrics] = []
|
||||
self.prompts = self._load_centralized_prompts()
|
||||
self.schemas = self._load_centralized_schemas()
|
||||
self.config = self._load_ai_configuration()
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
logger.info("AIServiceManager initialized")
|
||||
def __new__(cls):
|
||||
"""Implement singleton pattern to prevent multiple initializations."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(AIServiceManager, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize AI service manager (only once)."""
|
||||
if not self._initialized:
|
||||
self.logger = logger
|
||||
self.metrics: List[AIServiceMetrics] = []
|
||||
self.prompts = self._load_centralized_prompts()
|
||||
self.schemas = self._load_centralized_schemas()
|
||||
self.config = self._load_ai_configuration()
|
||||
|
||||
logger.debug("AIServiceManager initialized")
|
||||
self._initialized = True
|
||||
|
||||
def _load_ai_configuration(self) -> Dict[str, Any]:
|
||||
"""Load AI configuration settings."""
|
||||
|
||||
@@ -24,10 +24,21 @@ from services.database import get_db_session
|
||||
class AIEngineService:
|
||||
"""AI engine for content planning insights and analysis."""
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
def __new__(cls):
|
||||
"""Implement singleton pattern to prevent multiple initializations."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(AIEngineService, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the AI engine service."""
|
||||
self.ai_service_manager = AIServiceManager()
|
||||
logger.info("AIEngineService initialized")
|
||||
"""Initialize the AI engine service (only once)."""
|
||||
if not self._initialized:
|
||||
self.ai_service_manager = AIServiceManager()
|
||||
logger.debug("AIEngineService initialized")
|
||||
self._initialized = True
|
||||
|
||||
async def analyze_content_gaps(self, analysis_summary: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -18,13 +18,24 @@ from services.persona.facebook.facebook_persona_service import FacebookPersonaSe
|
||||
class CorePersonaService:
|
||||
"""Core service for generating writing personas using Gemini AI."""
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
def __new__(cls):
|
||||
"""Implement singleton pattern to prevent multiple initializations."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(CorePersonaService, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the core persona service."""
|
||||
self.data_collector = OnboardingDataCollector()
|
||||
self.prompt_builder = PersonaPromptBuilder()
|
||||
self.linkedin_service = LinkedInPersonaService()
|
||||
self.facebook_service = FacebookPersonaService()
|
||||
logger.info("CorePersonaService initialized")
|
||||
"""Initialize the core persona service (only once)."""
|
||||
if not self._initialized:
|
||||
self.data_collector = OnboardingDataCollector()
|
||||
self.prompt_builder = PersonaPromptBuilder()
|
||||
self.linkedin_service = LinkedInPersonaService()
|
||||
self.facebook_service = FacebookPersonaService()
|
||||
logger.debug("CorePersonaService initialized")
|
||||
self._initialized = True
|
||||
|
||||
def generate_core_persona(self, onboarding_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Generate core writing persona using Gemini structured response."""
|
||||
|
||||
@@ -26,7 +26,7 @@ class EnhancedLinguisticAnalyzer:
|
||||
import spacy
|
||||
self.nlp = spacy.load("en_core_web_sm")
|
||||
self.spacy_available = True
|
||||
logger.info("SUCCESS: spaCy model loaded successfully - Enhanced linguistic analysis available")
|
||||
logger.debug("SUCCESS: spaCy model loaded successfully - Enhanced linguistic analysis available")
|
||||
except ImportError as e:
|
||||
logger.error(f"ERROR: spaCy is REQUIRED for persona generation. Install with: pip install spacy && python -m spacy download en_core_web_sm")
|
||||
raise ImportError("spaCy is required for enhanced persona generation. Install with: pip install spacy && python -m spacy download en_core_web_sm") from e
|
||||
|
||||
@@ -20,14 +20,25 @@ from services.llm_providers.gemini_provider import gemini_structured_json_respon
|
||||
class FacebookPersonaService:
|
||||
"""Facebook-specific persona generation and optimization service."""
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
def __new__(cls):
|
||||
"""Implement singleton pattern to prevent multiple initializations."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(FacebookPersonaService, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the Facebook persona service."""
|
||||
self.schemas = FacebookPersonaSchema
|
||||
self.constraints = FacebookPersonaConstraints()
|
||||
self.validation = FacebookPersonaValidation()
|
||||
self.optimization = FacebookPersonaOptimization()
|
||||
self.prompts = FacebookPersonaPrompts()
|
||||
logger.info("FacebookPersonaService initialized")
|
||||
"""Initialize the Facebook persona service (only once)."""
|
||||
if not self._initialized:
|
||||
self.schemas = FacebookPersonaSchema
|
||||
self.constraints = FacebookPersonaConstraints()
|
||||
self.validation = FacebookPersonaValidation()
|
||||
self.optimization = FacebookPersonaOptimization()
|
||||
self.prompts = FacebookPersonaPrompts()
|
||||
logger.debug("FacebookPersonaService initialized")
|
||||
self._initialized = True
|
||||
|
||||
def generate_facebook_persona(self, core_persona: Dict[str, Any], onboarding_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -14,11 +14,22 @@ from .linkedin_persona_schemas import LinkedInPersonaSchemas
|
||||
class LinkedInPersonaService:
|
||||
"""Service for generating LinkedIn-specific persona adaptations."""
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
def __new__(cls):
|
||||
"""Implement singleton pattern to prevent multiple initializations."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(LinkedInPersonaService, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the LinkedIn persona service."""
|
||||
self.prompts = LinkedInPersonaPrompts()
|
||||
self.schemas = LinkedInPersonaSchemas()
|
||||
logger.info("LinkedInPersonaService initialized")
|
||||
"""Initialize the LinkedIn persona service (only once)."""
|
||||
if not self._initialized:
|
||||
self.prompts = LinkedInPersonaPrompts()
|
||||
self.schemas = LinkedInPersonaSchemas()
|
||||
logger.debug("LinkedInPersonaService initialized")
|
||||
self._initialized = True
|
||||
|
||||
def generate_linkedin_persona(self, core_persona: Dict[str, Any], onboarding_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -24,7 +24,7 @@ class PersonaQualityImprover:
|
||||
def __init__(self):
|
||||
"""Initialize the quality improver."""
|
||||
self.linguistic_analyzer = EnhancedLinguisticAnalyzer()
|
||||
logger.info("PersonaQualityImprover initialized")
|
||||
logger.debug("PersonaQualityImprover initialized")
|
||||
|
||||
def assess_persona_quality_comprehensive(
|
||||
self,
|
||||
|
||||
@@ -19,13 +19,24 @@ from services.persona.facebook.facebook_persona_service import FacebookPersonaSe
|
||||
class PersonaAnalysisService:
|
||||
"""Service for analyzing onboarding data and generating writing personas using Gemini AI."""
|
||||
|
||||
_instance = None
|
||||
_initialized = False
|
||||
|
||||
def __new__(cls):
|
||||
"""Implement singleton pattern to prevent multiple initializations."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super(PersonaAnalysisService, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the persona analysis service."""
|
||||
self.core_persona_service = CorePersonaService()
|
||||
self.data_collector = OnboardingDataCollector()
|
||||
self.linkedin_service = LinkedInPersonaService()
|
||||
self.facebook_service = FacebookPersonaService()
|
||||
logger.info("PersonaAnalysisService initialized")
|
||||
"""Initialize the persona analysis service (only once)."""
|
||||
if not self._initialized:
|
||||
self.core_persona_service = CorePersonaService()
|
||||
self.data_collector = OnboardingDataCollector()
|
||||
self.linkedin_service = LinkedInPersonaService()
|
||||
self.facebook_service = FacebookPersonaService()
|
||||
logger.debug("PersonaAnalysisService initialized")
|
||||
self._initialized = True
|
||||
|
||||
def generate_persona_from_onboarding(self, user_id: int, onboarding_session_id: int = None) -> Dict[str, Any]:
|
||||
"""
|
||||
|
||||
@@ -215,7 +215,7 @@ class PricingService:
|
||||
self.db.add(pricing)
|
||||
|
||||
self.db.commit()
|
||||
logger.info("Default API pricing initialized")
|
||||
logger.debug("Default API pricing initialized")
|
||||
|
||||
def initialize_default_plans(self):
|
||||
"""Initialize default subscription plans."""
|
||||
@@ -318,7 +318,7 @@ class PricingService:
|
||||
self.db.add(plan)
|
||||
|
||||
self.db.commit()
|
||||
logger.info("Default subscription plans initialized")
|
||||
logger.debug("Default subscription plans initialized")
|
||||
|
||||
def calculate_api_cost(self, provider: APIProvider, model_name: str,
|
||||
tokens_input: int = 0, tokens_output: int = 0,
|
||||
|
||||
@@ -17,28 +17,37 @@ def bootstrap_linguistic_models():
|
||||
This prevents import-time failures when EnhancedLinguisticAnalyzer is loaded.
|
||||
"""
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
print("🔍 Bootstrapping linguistic models...")
|
||||
verbose = os.getenv("ALWRITY_VERBOSE", "false").lower() == "true"
|
||||
|
||||
if verbose:
|
||||
print("🔍 Bootstrapping linguistic models...")
|
||||
|
||||
# Check and download spaCy model
|
||||
try:
|
||||
import spacy
|
||||
try:
|
||||
nlp = spacy.load("en_core_web_sm")
|
||||
print(" ✅ spaCy model 'en_core_web_sm' available")
|
||||
if verbose:
|
||||
print(" ✅ spaCy model 'en_core_web_sm' available")
|
||||
except OSError:
|
||||
print(" ⚠️ spaCy model 'en_core_web_sm' not found, downloading...")
|
||||
if verbose:
|
||||
print(" ⚠️ spaCy model 'en_core_web_sm' not found, downloading...")
|
||||
try:
|
||||
subprocess.check_call([
|
||||
sys.executable, "-m", "spacy", "download", "en_core_web_sm"
|
||||
])
|
||||
print(" ✅ spaCy model downloaded successfully")
|
||||
if verbose:
|
||||
print(" ✅ spaCy model downloaded successfully")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f" ❌ Failed to download spaCy model: {e}")
|
||||
print(" Please run: python -m spacy download en_core_web_sm")
|
||||
if verbose:
|
||||
print(f" ❌ Failed to download spaCy model: {e}")
|
||||
print(" Please run: python -m spacy download en_core_web_sm")
|
||||
return False
|
||||
except ImportError:
|
||||
print(" ⚠️ spaCy not installed - skipping")
|
||||
if verbose:
|
||||
print(" ⚠️ spaCy not installed - skipping")
|
||||
|
||||
# Check and download NLTK data
|
||||
try:
|
||||
@@ -52,25 +61,32 @@ def bootstrap_linguistic_models():
|
||||
for data_package, path in essential_data:
|
||||
try:
|
||||
nltk.data.find(path)
|
||||
print(f" ✅ NLTK {data_package} available")
|
||||
if verbose:
|
||||
print(f" ✅ NLTK {data_package} available")
|
||||
except LookupError:
|
||||
print(f" ⚠️ NLTK {data_package} not found, downloading...")
|
||||
if verbose:
|
||||
print(f" ⚠️ NLTK {data_package} not found, downloading...")
|
||||
try:
|
||||
nltk.download(data_package, quiet=True)
|
||||
print(f" ✅ NLTK {data_package} downloaded")
|
||||
if verbose:
|
||||
print(f" ✅ NLTK {data_package} downloaded")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Failed to download {data_package}: {e}")
|
||||
if verbose:
|
||||
print(f" ⚠️ Failed to download {data_package}: {e}")
|
||||
# Try fallback
|
||||
if data_package == 'punkt_tab':
|
||||
try:
|
||||
nltk.download('punkt', quiet=True)
|
||||
print(f" ✅ NLTK punkt (fallback) downloaded")
|
||||
if verbose:
|
||||
print(f" ✅ NLTK punkt (fallback) downloaded")
|
||||
except:
|
||||
pass
|
||||
except ImportError:
|
||||
print(" ⚠️ NLTK not installed - skipping")
|
||||
if verbose:
|
||||
print(" ⚠️ NLTK not installed - skipping")
|
||||
|
||||
print("✅ Linguistic model bootstrap complete")
|
||||
if verbose:
|
||||
print("✅ Linguistic model bootstrap complete")
|
||||
return True
|
||||
|
||||
|
||||
@@ -127,11 +143,10 @@ def start_backend(enable_reload=False, production_mode=False):
|
||||
import uvicorn
|
||||
|
||||
# Explicitly initialize database before starting server
|
||||
print("[DB] Initializing database...")
|
||||
init_database()
|
||||
print("[OK] Database initialized successfully")
|
||||
|
||||
print("\n🌐 Backend is starting...")
|
||||
print("\n🌐 ALwrity Backend Server")
|
||||
print("=" * 50)
|
||||
print(" 📖 API Documentation: http://localhost:8000/api/docs")
|
||||
print(" 🔍 Health Check: http://localhost:8000/health")
|
||||
print(" 📊 ReDoc: http://localhost:8000/api/redoc")
|
||||
@@ -142,12 +157,13 @@ def start_backend(enable_reload=False, production_mode=False):
|
||||
print(" 📊 Usage Tracking: http://localhost:8000/api/subscription/usage/demo")
|
||||
|
||||
print("\n[STOP] Press Ctrl+C to stop the server")
|
||||
print("=" * 60)
|
||||
print("\n💡 Usage:")
|
||||
print(" Production mode: python start_alwrity_backend.py --production")
|
||||
print(" Development mode: python start_alwrity_backend.py --dev")
|
||||
print(" With auto-reload: python start_alwrity_backend.py --reload")
|
||||
print("=" * 60)
|
||||
print("=" * 50)
|
||||
|
||||
# Set up clean logging for end users
|
||||
from logging_config import setup_clean_logging, get_uvicorn_log_level
|
||||
|
||||
verbose_mode = setup_clean_logging()
|
||||
uvicorn_log_level = get_uvicorn_log_level()
|
||||
|
||||
uvicorn.run(
|
||||
"app:app",
|
||||
@@ -186,7 +202,7 @@ def start_backend(enable_reload=False, production_mode=False):
|
||||
"api/**/*.py",
|
||||
"services/**/*.py"
|
||||
],
|
||||
log_level="info"
|
||||
log_level=uvicorn_log_level
|
||||
)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
@@ -205,16 +221,23 @@ def main():
|
||||
parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
|
||||
parser.add_argument("--dev", action="store_true", help="Enable development mode (auto-reload)")
|
||||
parser.add_argument("--production", action="store_true", help="Enable production mode (optimized for deployment)")
|
||||
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging for debugging")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Determine mode
|
||||
production_mode = args.production
|
||||
enable_reload = (args.reload or args.dev) and not production_mode
|
||||
verbose_mode = args.verbose
|
||||
|
||||
print("ALwrity Backend Server")
|
||||
# Set global verbose flag for utilities
|
||||
os.environ["ALWRITY_VERBOSE"] = "true" if verbose_mode else "false"
|
||||
|
||||
print("🚀 ALwrity Backend Server")
|
||||
print("=" * 40)
|
||||
print(f"Mode: {'PRODUCTION' if production_mode else 'DEVELOPMENT'}")
|
||||
print(f"Auto-reload: {'ENABLED' if enable_reload else 'DISABLED'}")
|
||||
if verbose_mode:
|
||||
print("Verbose logging: ENABLED")
|
||||
print("=" * 40)
|
||||
|
||||
# Check if we're in the right directory
|
||||
@@ -230,39 +253,59 @@ def main():
|
||||
database_setup = DatabaseSetup(production_mode=production_mode)
|
||||
production_optimizer = ProductionOptimizer()
|
||||
|
||||
# Setup progress tracking
|
||||
setup_steps = [
|
||||
"Checking dependencies",
|
||||
"Setting up environment",
|
||||
"Configuring database",
|
||||
"Starting server"
|
||||
]
|
||||
|
||||
print("🔧 Initializing ALwrity...")
|
||||
|
||||
# Apply production optimizations if needed
|
||||
if production_mode:
|
||||
if not production_optimizer.apply_production_optimizations():
|
||||
print("[ERROR] Production optimization failed")
|
||||
print("❌ Production optimization failed")
|
||||
return False
|
||||
|
||||
# Check and install dependencies
|
||||
# Step 1: Dependencies
|
||||
print(f" 📦 {setup_steps[0]}...", end=" ", flush=True)
|
||||
critical_ok, missing_critical = dependency_manager.check_critical_dependencies()
|
||||
if not critical_ok:
|
||||
print("[ERROR] Critical dependencies missing, installing...")
|
||||
print("installing...", end=" ", flush=True)
|
||||
if not dependency_manager.install_requirements():
|
||||
print("[ERROR] Failed to install dependencies")
|
||||
print("❌ Failed")
|
||||
return False
|
||||
print("✅ Done")
|
||||
else:
|
||||
print("✅ Done")
|
||||
|
||||
# Check optional dependencies (non-critical)
|
||||
dependency_manager.check_optional_dependencies()
|
||||
# Check optional dependencies (non-critical) - only in verbose mode
|
||||
if verbose_mode:
|
||||
dependency_manager.check_optional_dependencies()
|
||||
|
||||
# Setup environment
|
||||
# Step 2: Environment
|
||||
print(f" 🔧 {setup_steps[1]}...", end=" ", flush=True)
|
||||
if not environment_setup.setup_directories():
|
||||
print("[ERROR] Directory setup failed")
|
||||
print("❌ Directory setup failed")
|
||||
return False
|
||||
|
||||
if not environment_setup.setup_environment_variables():
|
||||
print("[ERROR] Environment variable setup failed")
|
||||
print("❌ Environment setup failed")
|
||||
return False
|
||||
|
||||
# Create .env file only in development
|
||||
if not production_mode:
|
||||
environment_setup.create_env_file()
|
||||
print("✅ Done")
|
||||
|
||||
# Setup database
|
||||
# Step 3: Database
|
||||
print(f" 📊 {setup_steps[2]}...", end=" ", flush=True)
|
||||
if not database_setup.setup_essential_tables():
|
||||
print("[WARNING] Database setup had issues, continuing...")
|
||||
print("⚠️ Issues detected, continuing...")
|
||||
else:
|
||||
print("✅ Done")
|
||||
|
||||
# Setup advanced features in development, verify in all modes
|
||||
if not production_mode:
|
||||
@@ -274,7 +317,8 @@ def main():
|
||||
# Note: Linguistic models (spaCy/NLTK) are bootstrapped before imports
|
||||
# See bootstrap_linguistic_models() at the top of this file
|
||||
|
||||
# Start backend
|
||||
# Step 4: Start backend
|
||||
print(f" 🚀 {setup_steps[3]}...")
|
||||
return start_backend(enable_reload=enable_reload, production_mode=production_mode)
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Production Environment Variables for Vercel
|
||||
# Copy this file to .env.production and update with your actual values
|
||||
|
||||
# Backend API URL (from your deployment platform)
|
||||
# Examples:
|
||||
# REACT_APP_API_URL=https://alwrity.onrender.com
|
||||
# REACT_APP_API_URL=https://your-app.railway.app
|
||||
# REACT_APP_API_URL=https://your-app.herokuapp.com
|
||||
REACT_APP_API_URL=https://alwrity.onrender.com
|
||||
|
||||
# Alternative backend URL (fallback)
|
||||
# REACT_APP_BACKEND_URL=https://alwrity.onrender.com
|
||||
|
||||
# Environment
|
||||
REACT_APP_ENVIRONMENT=production
|
||||
|
||||
# Optional: Custom domain for OAuth redirects
|
||||
# REACT_APP_DOMAIN=your-custom-domain.com
|
||||
@@ -57,5 +57,6 @@
|
||||
"devDependencies": {
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"proxy": "http://localhost:8000"
|
||||
"proxy": "http://localhost:8000",
|
||||
"homepage": "/"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Box, CircularProgress, Typography } from '@mui/material';
|
||||
import { CopilotKit } from "@copilotkit/react-core";
|
||||
import { ClerkProvider, useAuth } from '@clerk/clerk-react';
|
||||
@@ -26,6 +26,7 @@ import { SubscriptionProvider } from './contexts/SubscriptionContext';
|
||||
import { apiClient, setAuthTokenGetter } from './api/client';
|
||||
import { useOnboarding } from './contexts/OnboardingContext';
|
||||
import { useState, useEffect } from 'react';
|
||||
import ConnectionErrorPage from './components/shared/ConnectionErrorPage';
|
||||
|
||||
// interface OnboardingStatus {
|
||||
// onboarding_required: boolean;
|
||||
@@ -37,9 +38,6 @@ import { useState, useEffect } from 'react';
|
||||
|
||||
// Conditional CopilotKit wrapper that only shows sidebar on content-planning route
|
||||
const ConditionalCopilotKit: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
// const isContentPlanningRoute = location.pathname === '/content-planning';
|
||||
|
||||
// Do not render CopilotSidebar here. Let specific pages/components control it.
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -54,6 +52,13 @@ const InitialRouteHandler: React.FC = () => {
|
||||
plan: string;
|
||||
isNewUser: boolean;
|
||||
} | null>(null);
|
||||
const [connectionError, setConnectionError] = useState<{
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}>({
|
||||
hasError: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const checkSubscription = async () => {
|
||||
@@ -70,9 +75,26 @@ const InitialRouteHandler: React.FC = () => {
|
||||
plan: subscriptionData?.plan || 'none',
|
||||
isNewUser
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
// Clear any connection errors
|
||||
setConnectionError({
|
||||
hasError: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Error checking subscription:', err);
|
||||
// On error, treat as new user
|
||||
|
||||
// Check if it's a connection error - handle it locally
|
||||
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
|
||||
setConnectionError({
|
||||
hasError: true,
|
||||
error: err,
|
||||
});
|
||||
return; // Don't set subscription status for connection errors
|
||||
}
|
||||
|
||||
// For other errors, treat as new user
|
||||
setSubscriptionStatus({
|
||||
active: false,
|
||||
plan: 'none',
|
||||
@@ -86,6 +108,65 @@ const InitialRouteHandler: React.FC = () => {
|
||||
checkSubscription();
|
||||
}, []);
|
||||
|
||||
// Handle connection error - show connection error page
|
||||
if (connectionError.hasError) {
|
||||
const handleRetry = () => {
|
||||
setConnectionError({
|
||||
hasError: false,
|
||||
error: null,
|
||||
});
|
||||
setCheckingSubscription(true);
|
||||
// Re-trigger the subscription check
|
||||
const checkSubscription = async () => {
|
||||
try {
|
||||
const userId = localStorage.getItem('user_id') || 'anonymous';
|
||||
const response = await apiClient.get(`/api/subscription/status/${userId}`);
|
||||
const subscriptionData = response.data.data;
|
||||
|
||||
const isNewUser = !subscriptionData || subscriptionData.plan === 'none';
|
||||
|
||||
setSubscriptionStatus({
|
||||
active: subscriptionData?.active || false,
|
||||
plan: subscriptionData?.plan || 'none',
|
||||
isNewUser
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('Error checking subscription on retry:', err);
|
||||
|
||||
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
|
||||
setConnectionError({
|
||||
hasError: true,
|
||||
error: err,
|
||||
});
|
||||
} else {
|
||||
setSubscriptionStatus({
|
||||
active: false,
|
||||
plan: 'none',
|
||||
isNewUser: true
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setCheckingSubscription(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkSubscription();
|
||||
};
|
||||
|
||||
const handleGoHome = () => {
|
||||
window.location.href = '/';
|
||||
};
|
||||
|
||||
return (
|
||||
<ConnectionErrorPage
|
||||
onRetry={handleRetry}
|
||||
onGoHome={handleGoHome}
|
||||
message={connectionError.error?.message || "Backend service is not available. Please check if the server is running."}
|
||||
title="Connection Error"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Loading state - checking both subscription and onboarding
|
||||
if (loading || checkingSubscription) {
|
||||
return (
|
||||
@@ -200,7 +281,6 @@ const TokenInstaller: React.FC = () => {
|
||||
const App: React.FC = () => {
|
||||
// React Hooks MUST be at the top before any conditionals
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Get CopilotKit key from localStorage or .env
|
||||
const [copilotApiKey, setCopilotApiKey] = useState(() => {
|
||||
@@ -208,18 +288,10 @@ const App: React.FC = () => {
|
||||
return savedKey || process.env.REACT_APP_COPILOTKIT_API_KEY || '';
|
||||
});
|
||||
|
||||
// Initialize app - loading state will be managed by InitialRouteHandler
|
||||
useEffect(() => {
|
||||
const checkBackendHealth = async () => {
|
||||
try {
|
||||
await apiClient.get('/health');
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
setError('Backend service is not available. Please check if the server is running.');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkBackendHealth();
|
||||
// Remove manual health check - connection errors are handled by ErrorBoundary
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
// Listen for CopilotKit key updates
|
||||
@@ -257,29 +329,6 @@ const App: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
minHeight="100vh"
|
||||
gap={2}
|
||||
p={3}
|
||||
>
|
||||
<Typography variant="h5" color="error" gutterBottom>
|
||||
Connection Error
|
||||
</Typography>
|
||||
<Typography variant="body1" color="textSecondary" textAlign="center">
|
||||
{error}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" textAlign="center">
|
||||
Please ensure the backend server is running and try refreshing the page.
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Get environment variables with fallbacks
|
||||
const clerkPublishableKey = process.env.REACT_APP_CLERK_PUBLISHABLE_KEY || '';
|
||||
|
||||
@@ -72,6 +72,21 @@ apiClient.interceptors.request.use(
|
||||
}
|
||||
);
|
||||
|
||||
// Custom error types for better error handling
|
||||
export class ConnectionError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ConnectionError';
|
||||
}
|
||||
}
|
||||
|
||||
export class NetworkError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'NetworkError';
|
||||
}
|
||||
}
|
||||
|
||||
// Add response interceptor with automatic token refresh on 401
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
@@ -80,6 +95,25 @@ apiClient.interceptors.response.use(
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// Handle network errors and timeouts (backend not available)
|
||||
if (!error.response) {
|
||||
// Network error, timeout, or backend not reachable
|
||||
const connectionError = new NetworkError(
|
||||
'Unable to connect to the backend server. Please check if the server is running.'
|
||||
);
|
||||
console.error('Network/Connection Error:', error.message || error);
|
||||
return Promise.reject(connectionError);
|
||||
}
|
||||
|
||||
// Handle server errors (5xx)
|
||||
if (error.response.status >= 500) {
|
||||
const connectionError = new ConnectionError(
|
||||
'Backend server is experiencing issues. Please try again later.'
|
||||
);
|
||||
console.error('Server Error:', error.response.status, error.response.data);
|
||||
return Promise.reject(connectionError);
|
||||
}
|
||||
|
||||
// If 401 and we haven't retried yet, try to refresh token and retry
|
||||
if (error?.response?.status === 401 && !originalRequest._retry && authTokenGetter) {
|
||||
originalRequest._retry = true;
|
||||
|
||||
498
frontend/src/components/shared/BackendConnectionError.tsx
Normal file
498
frontend/src/components/shared/BackendConnectionError.tsx
Normal file
@@ -0,0 +1,498 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
Stack,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
LinearProgress,
|
||||
Fade,
|
||||
Chip,
|
||||
Card,
|
||||
CardContent,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
import {
|
||||
WifiOff as ConnectionIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Home as HomeIcon,
|
||||
Settings as SettingsIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
CloudQueue as CloudIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface BackendConnectionErrorProps {
|
||||
error?: string;
|
||||
onRetry?: () => void;
|
||||
onGoHome?: () => void;
|
||||
}
|
||||
|
||||
interface ConnectionAttempt {
|
||||
timestamp: number;
|
||||
success: boolean;
|
||||
responseTime?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced Backend Connection Error Component
|
||||
*
|
||||
* Shows a loading state for 2 minutes while attempting to reconnect,
|
||||
* then gracefully shows error if backend remains unresponsive.
|
||||
*/
|
||||
const BackendConnectionError: React.FC<BackendConnectionErrorProps> = ({
|
||||
error = 'Backend service is not available. Please check if the server is running.',
|
||||
onRetry,
|
||||
onGoHome
|
||||
}) => {
|
||||
const [isRetrying, setIsRetrying] = useState(true);
|
||||
const [timeElapsed, setTimeElapsed] = useState(0);
|
||||
const [connectionAttempts, setConnectionAttempts] = useState<ConnectionAttempt[]>([]);
|
||||
const [showError, setShowError] = useState(false);
|
||||
|
||||
const MAX_RETRY_TIME = 120; // 2 minutes in seconds
|
||||
const RETRY_INTERVAL = 5000; // 5 seconds
|
||||
|
||||
// Timer for elapsed time
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setTimeElapsed(prev => {
|
||||
const newTime = prev + 1;
|
||||
if (newTime >= MAX_RETRY_TIME) {
|
||||
setIsRetrying(false);
|
||||
setShowError(true);
|
||||
return MAX_RETRY_TIME;
|
||||
}
|
||||
return newTime;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
// Retry attempts
|
||||
useEffect(() => {
|
||||
if (!isRetrying) return;
|
||||
|
||||
const retryTimer = setInterval(async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Try to connect to a simple health check endpoint with short timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 2000); // 2 second timeout
|
||||
|
||||
const response = await fetch('/health', {
|
||||
method: 'GET',
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
setConnectionAttempts(prev => [...prev, {
|
||||
timestamp: Date.now(),
|
||||
success: response.ok,
|
||||
responseTime
|
||||
}]);
|
||||
|
||||
if (response.ok) {
|
||||
// Backend is back online, trigger retry callback
|
||||
setIsRetrying(false);
|
||||
setShowError(false);
|
||||
if (onRetry) {
|
||||
onRetry();
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
setConnectionAttempts(prev => [...prev, {
|
||||
timestamp: Date.now(),
|
||||
success: false,
|
||||
responseTime: Date.now() - startTime
|
||||
}]);
|
||||
}
|
||||
}, RETRY_INTERVAL);
|
||||
|
||||
return () => clearInterval(retryTimer);
|
||||
}, [isRetrying, onRetry]);
|
||||
|
||||
const progress = (timeElapsed / MAX_RETRY_TIME) * 100;
|
||||
const minutes = Math.floor(timeElapsed / 60);
|
||||
const seconds = timeElapsed % 60;
|
||||
const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
const handleManualRetry = () => {
|
||||
setIsRetrying(true);
|
||||
setShowError(false);
|
||||
setTimeElapsed(0);
|
||||
setConnectionAttempts([]);
|
||||
};
|
||||
|
||||
const handleGoHome = () => {
|
||||
if (onGoHome) {
|
||||
onGoHome();
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
};
|
||||
|
||||
if (showError) {
|
||||
// Show final error state after 2 minutes
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
p: { xs: 2, md: 4 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<Paper
|
||||
elevation={24}
|
||||
sx={{
|
||||
p: { xs: 3, md: 5 },
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255, 255, 255, 0.98)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4} alignItems="center">
|
||||
{/* Error Icon */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(45deg, #f44336 30%, #e91e63 90%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 8px 32px rgba(244, 67, 54, 0.3)',
|
||||
}}
|
||||
>
|
||||
<ConnectionIcon sx={{ fontSize: 48, color: 'white' }} />
|
||||
</Box>
|
||||
|
||||
{/* Error Title */}
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: '#1a1a1a',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Connection Error
|
||||
</Typography>
|
||||
|
||||
{/* Error Message */}
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', maxWidth: 600 }}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
|
||||
{/* Troubleshooting Tips */}
|
||||
<Card sx={{ width: '100%', maxWidth: 600 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<SettingsIcon />
|
||||
Troubleshooting Steps
|
||||
</Typography>
|
||||
<List dense>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Check if the backend server is running" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Verify the backend is accessible on the correct port" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Check firewall and network settings" />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Review backend server logs for errors" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={2}
|
||||
sx={{ width: '100%', maxWidth: 400 }}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={handleManualRetry}
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
background: 'linear-gradient(45deg, #667eea 30%, #764ba2 90%)',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(45deg, #5568d3 30%, #6a3f8f 90%)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
startIcon={<HomeIcon />}
|
||||
onClick={handleGoHome}
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
borderColor: '#667eea',
|
||||
color: '#667eea',
|
||||
'&:hover': {
|
||||
borderColor: '#5568d3',
|
||||
background: 'rgba(102, 126, 234, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Go Home
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{/* Connection Attempts Summary */}
|
||||
{connectionAttempts.length > 0 && (
|
||||
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Connection attempts: {connectionAttempts.length}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Last attempt: {connectionAttempts[connectionAttempts.length - 1]?.success ? 'Successful' : 'Failed'}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Show loading state for first 2 minutes
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
p: { xs: 2, md: 4 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<Paper
|
||||
elevation={24}
|
||||
sx={{
|
||||
p: { xs: 3, md: 5 },
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255, 255, 255, 0.98)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={4} alignItems="center">
|
||||
{/* Loading Animation */}
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<CircularProgress
|
||||
size={80}
|
||||
thickness={4}
|
||||
sx={{
|
||||
color: '#667eea',
|
||||
animation: 'pulse 2s ease-in-out infinite',
|
||||
'@keyframes pulse': {
|
||||
'0%': { opacity: 1 },
|
||||
'50%': { opacity: 0.5 },
|
||||
'100%': { opacity: 1 },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<CloudIcon sx={{ fontSize: 32, color: '#667eea' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Loading Title */}
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: '#1a1a1a',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Connecting to Backend
|
||||
</Typography>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.2)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: '#667eea',
|
||||
borderRadius: 4,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Time and Progress Info */}
|
||||
<Stack spacing={1} alignItems="center">
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Attempting to reconnect... {timeString}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${Math.round(progress)}% complete`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* Motivational Messages */}
|
||||
<Fade in timeout={1000}>
|
||||
<Alert
|
||||
severity="info"
|
||||
icon={<ScheduleIcon />}
|
||||
sx={{
|
||||
maxWidth: 500,
|
||||
textAlign: 'center',
|
||||
'& .MuiAlert-message': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
{timeElapsed < 60
|
||||
? "We're working to restore your connection..."
|
||||
: "Still trying to connect. This may take a moment..."
|
||||
}
|
||||
</Typography>
|
||||
</Alert>
|
||||
</Fade>
|
||||
|
||||
{/* Connection Attempts */}
|
||||
{connectionAttempts.length > 0 && (
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
Connection attempts: {connectionAttempts.length}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{connectionAttempts.filter(attempt => attempt.success).length} successful,{' '}
|
||||
{connectionAttempts.filter(attempt => !attempt.success).length} failed
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={2}
|
||||
sx={{ width: '100%', maxWidth: 400 }}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={handleManualRetry}
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
borderColor: '#667eea',
|
||||
color: '#667eea',
|
||||
'&:hover': {
|
||||
borderColor: '#5568d3',
|
||||
background: 'rgba(102, 126, 234, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Retry Now
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="text"
|
||||
size="large"
|
||||
startIcon={<HomeIcon />}
|
||||
onClick={handleGoHome}
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
color: '#666',
|
||||
'&:hover': {
|
||||
background: 'rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Go Home
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{/* Help Text */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', maxWidth: 400 }}
|
||||
>
|
||||
If this issue persists, please check your internet connection and ensure the backend server is running.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackendConnectionError;
|
||||
317
frontend/src/components/shared/ConnectionErrorPage.tsx
Normal file
317
frontend/src/components/shared/ConnectionErrorPage.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
Stack,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Fade,
|
||||
Slide,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
WifiOff as WifiOffIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Home as HomeIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
ErrorOutline as ErrorOutlineIcon,
|
||||
CloudQueue as CloudIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { keyframes } from '@mui/system';
|
||||
|
||||
interface ConnectionErrorPageProps {
|
||||
onRetry?: () => void;
|
||||
onGoHome?: () => void;
|
||||
showRetry?: boolean;
|
||||
message?: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const pulse = keyframes`
|
||||
0% { opacity: 0.6; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.05); }
|
||||
100% { opacity: 0.6; transform: scale(1); }
|
||||
`;
|
||||
|
||||
const ConnectionErrorPage: React.FC<ConnectionErrorPageProps> = ({
|
||||
onRetry,
|
||||
onGoHome,
|
||||
showRetry = true,
|
||||
message = "Backend service is not available. Please check if the server is running.",
|
||||
title = "Connection Error"
|
||||
}) => {
|
||||
const [countdown, setCountdown] = useState(120); // 2 minutes
|
||||
const [isRetrying, setIsRetrying] = useState(false);
|
||||
const [showProgress, setShowProgress] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
setShowProgress(false);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const handleRetry = async () => {
|
||||
if (!onRetry || isRetrying) return;
|
||||
|
||||
setIsRetrying(true);
|
||||
try {
|
||||
await onRetry();
|
||||
} finally {
|
||||
setIsRetrying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Fade in timeout={800}>
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
p: { xs: 2, md: 4 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<Slide in timeout={600} direction="up">
|
||||
<Paper
|
||||
elevation={24}
|
||||
sx={{
|
||||
p: { xs: 3, md: 5 },
|
||||
borderRadius: 4,
|
||||
background: 'rgba(255, 255, 255, 0.98)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{/* Animated background elements */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 4,
|
||||
background: countdown > 0
|
||||
? 'linear-gradient(90deg, #667eea, #764ba2)'
|
||||
: 'linear-gradient(90deg, #f44336, #e91e63)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack spacing={4} alignItems="center">
|
||||
{/* Animated Icon */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: '50%',
|
||||
background: countdown > 0
|
||||
? 'linear-gradient(45deg, #ff9800 30%, #f44336 90%)'
|
||||
: 'linear-gradient(45deg, #f44336 30%, #e91e63 90%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: '0 8px 32px rgba(244, 67, 54, 0.3)',
|
||||
animation: countdown > 0 ? `${pulse} 2s ease-in-out infinite` : 'none',
|
||||
}}
|
||||
>
|
||||
{countdown > 0 ? (
|
||||
<CloudIcon sx={{ fontSize: 60, color: 'white' }} />
|
||||
) : (
|
||||
<WifiOffIcon sx={{ fontSize: 60, color: 'white' }} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Title and Status */}
|
||||
<Box textAlign="center">
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: countdown > 0 ? '#1a1a1a' : '#d32f2f',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
{countdown > 0 ? (
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="primary"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<CheckCircleIcon color="success" />
|
||||
Attempting to reconnect...
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="error"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<ErrorOutlineIcon />
|
||||
Connection failed
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Countdown Timer */}
|
||||
{showProgress && countdown > 0 && (
|
||||
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Retrying connection...
|
||||
</Typography>
|
||||
<Typography variant="body2" color="primary" fontWeight={600}>
|
||||
{formatTime(countdown)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={((120 - countdown) / 120) * 100}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
'& .MuiLinearProgress-bar': {
|
||||
borderRadius: 4,
|
||||
background: 'linear-gradient(45deg, #667eea, #764ba2)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Main Message */}
|
||||
<Alert
|
||||
severity={countdown > 0 ? "info" : "error"}
|
||||
icon={countdown > 0 ? <ScheduleIcon /> : <ErrorOutlineIcon />}
|
||||
sx={{
|
||||
width: '100%',
|
||||
maxWidth: 600,
|
||||
textAlign: 'left',
|
||||
'& .MuiAlert-message': {
|
||||
width: '100%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
{message}
|
||||
</Typography>
|
||||
|
||||
{countdown > 0 ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Please ensure the backend server is running and try refreshing the page.
|
||||
We'll keep trying to connect automatically.
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
The backend server appears to be unavailable. Please check if it's running
|
||||
and try again, or contact support if the issue persists.
|
||||
</Typography>
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
spacing={2}
|
||||
sx={{ width: '100%', maxWidth: 400 }}
|
||||
>
|
||||
{showRetry && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={handleRetry}
|
||||
disabled={isRetrying}
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
background: 'linear-gradient(45deg, #667eea 30%, #764ba2 90%)',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(45deg, #5568d3 30%, #6a3f8f 90%)',
|
||||
},
|
||||
'&:disabled': {
|
||||
background: 'rgba(102, 126, 234, 0.5)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isRetrying ? 'Retrying...' : 'Retry Connection'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="large"
|
||||
startIcon={<HomeIcon />}
|
||||
onClick={onGoHome}
|
||||
sx={{
|
||||
flex: 1,
|
||||
py: 1.5,
|
||||
borderColor: '#667eea',
|
||||
color: '#667eea',
|
||||
'&:hover': {
|
||||
borderColor: '#5568d3',
|
||||
background: 'rgba(102, 126, 234, 0.05)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Go Home
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
{/* Help Text */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', maxWidth: 500 }}
|
||||
>
|
||||
{countdown > 0
|
||||
? `Automatic retry in ${formatTime(countdown)}. Check your terminal for server status.`
|
||||
: "Error ID: connection_" + Date.now().toString(36) + " • Timestamp: " + new Date().toLocaleString()
|
||||
}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Slide>
|
||||
</Container>
|
||||
</Box>
|
||||
</Fade>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionErrorPage;
|
||||
@@ -136,13 +136,16 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
const { error } = this.state;
|
||||
|
||||
|
||||
// Custom fallback UI provided
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
// Default fallback UI
|
||||
const { error, errorInfo, showDetails } = this.state;
|
||||
const { errorInfo, showDetails } = this.state;
|
||||
const { context, showDetails: showDetailsDefault } = this.props;
|
||||
|
||||
return (
|
||||
|
||||
@@ -121,6 +121,13 @@ export const OnboardingProvider: React.FC<OnboardingProviderProps> = ({ children
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error('OnboardingContext: Error fetching data:', err);
|
||||
|
||||
// Check if it's a connection error that should be handled at the app level
|
||||
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
|
||||
// Re-throw connection errors to be handled by the app-level error boundary
|
||||
throw err;
|
||||
}
|
||||
|
||||
setError(err instanceof Error ? err.message : 'Failed to load onboarding data');
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,13 @@ export const SubscriptionProvider: React.FC<SubscriptionProviderProps> = ({ chil
|
||||
setSubscription(subscriptionData);
|
||||
} catch (err) {
|
||||
console.error('Error checking subscription:', err);
|
||||
|
||||
// Check if it's a connection error that should be handled at the app level
|
||||
if (err instanceof Error && (err.name === 'NetworkError' || err.name === 'ConnectionError')) {
|
||||
// Re-throw connection errors to be handled by the app-level error boundary
|
||||
throw err;
|
||||
}
|
||||
|
||||
setError(err instanceof Error ? err.message : 'Failed to check subscription');
|
||||
|
||||
// Default to free tier on error
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Modal Integration Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.test-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.test-button {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.test-button:hover {
|
||||
background-color: #1565c0;
|
||||
}
|
||||
.log-container {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.log-entry {
|
||||
margin: 5px 0;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.log-info { background-color: #d1ecf1; }
|
||||
.log-success { background-color: #d4edda; }
|
||||
.log-error { background-color: #f8d7da; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-container">
|
||||
<h1>Calendar Generation Modal Integration Test</h1>
|
||||
<p>This test verifies that the calendar generation modal can be triggered and displays properly.</p>
|
||||
|
||||
<h2>Test Steps:</h2>
|
||||
<ol>
|
||||
<li>Click the "Test Modal Integration" button below</li>
|
||||
<li>Check if the modal opens and displays progress</li>
|
||||
<li>Monitor the logs for any errors</li>
|
||||
</ol>
|
||||
|
||||
<button class="test-button" onclick="testModalIntegration()">
|
||||
Test Modal Integration
|
||||
</button>
|
||||
|
||||
<button class="test-button" onclick="testBackendConnection()">
|
||||
Test Backend Connection
|
||||
</button>
|
||||
|
||||
<div class="log-container" id="logContainer">
|
||||
<div class="log-entry log-info">Ready to test modal integration...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function addLog(message, type = 'info') {
|
||||
const logContainer = document.getElementById('logContainer');
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = `log-entry log-${type}`;
|
||||
logEntry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
||||
logContainer.appendChild(logEntry);
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
|
||||
async function testBackendConnection() {
|
||||
addLog('Testing backend connection...', 'info');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/content-planning/calendar-generation/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: 1,
|
||||
calendar_type: 'monthly',
|
||||
industry: 'technology',
|
||||
business_size: 'sme',
|
||||
force_refresh: false
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
addLog(`Backend connection successful! Session ID: ${data.session_id}`, 'success');
|
||||
} else {
|
||||
addLog(`Backend connection failed: ${response.status} ${response.statusText}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
addLog(`Backend connection error: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function testModalIntegration() {
|
||||
addLog('Testing modal integration...', 'info');
|
||||
|
||||
// Simulate the calendar generation flow
|
||||
const mockCalendarConfig = {
|
||||
calendarType: 'monthly',
|
||||
startDate: '2024-01-01',
|
||||
calendarDuration: 30,
|
||||
postingFrequency: 3,
|
||||
priorityPlatforms: ['LinkedIn', 'Twitter'],
|
||||
timeZone: 'UTC',
|
||||
includeWeekends: true,
|
||||
autoSchedule: true,
|
||||
generateTopics: true
|
||||
};
|
||||
|
||||
addLog(`Calendar config prepared: ${JSON.stringify(mockCalendarConfig)}`, 'info');
|
||||
|
||||
// Simulate the API call that should trigger the modal
|
||||
testBackendConnection().then(() => {
|
||||
addLog('Modal should now be visible if integration is working correctly', 'success');
|
||||
addLog('Check the main application for the modal display', 'info');
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-test on page load
|
||||
window.addEventListener('load', () => {
|
||||
addLog('Page loaded, ready for testing', 'info');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user