ALwrity version 0.5.5
This commit is contained in:
@@ -662,6 +662,16 @@ async def get_latest_generated_strategy(
|
|||||||
logger.info(f"🔍 Querying database for strategies with user_id: {user_id}")
|
logger.info(f"🔍 Querying database for strategies with user_id: {user_id}")
|
||||||
|
|
||||||
# Query for the most recent strategy with comprehensive AI analysis
|
# Query for the most recent strategy with comprehensive AI analysis
|
||||||
|
# First, let's see all strategies for this user
|
||||||
|
all_strategies = db.query(EnhancedContentStrategy).filter(
|
||||||
|
EnhancedContentStrategy.user_id == user_id
|
||||||
|
).order_by(desc(EnhancedContentStrategy.created_at)).all()
|
||||||
|
|
||||||
|
logger.info(f"🔍 Found {len(all_strategies)} total strategies for user {user_id}")
|
||||||
|
for i, strategy in enumerate(all_strategies):
|
||||||
|
logger.info(f" Strategy {i+1}: ID={strategy.id}, name={strategy.name}, created_at={strategy.created_at}, has_comprehensive_ai_analysis={strategy.comprehensive_ai_analysis is not None}")
|
||||||
|
|
||||||
|
# Now query for the most recent strategy with comprehensive AI analysis
|
||||||
latest_db_strategy = db.query(EnhancedContentStrategy).filter(
|
latest_db_strategy = db.query(EnhancedContentStrategy).filter(
|
||||||
EnhancedContentStrategy.user_id == user_id,
|
EnhancedContentStrategy.user_id == user_id,
|
||||||
EnhancedContentStrategy.comprehensive_ai_analysis.isnot(None)
|
EnhancedContentStrategy.comprehensive_ai_analysis.isnot(None)
|
||||||
@@ -682,9 +692,31 @@ async def get_latest_generated_strategy(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info(f"⚠️ No strategy found in database for user: {user_id}")
|
logger.info(f"⚠️ No strategy with comprehensive_ai_analysis found in database for user: {user_id}")
|
||||||
if latest_db_strategy:
|
|
||||||
logger.info(f"🔍 Strategy found but no comprehensive_ai_analysis: {latest_db_strategy.id}")
|
# Fallback: Try to get the most recent strategy regardless of comprehensive_ai_analysis
|
||||||
|
fallback_strategy = db.query(EnhancedContentStrategy).filter(
|
||||||
|
EnhancedContentStrategy.user_id == user_id
|
||||||
|
).order_by(desc(EnhancedContentStrategy.created_at)).first()
|
||||||
|
|
||||||
|
if fallback_strategy:
|
||||||
|
logger.info(f"🔍 Found fallback strategy: ID={fallback_strategy.id}, name={fallback_strategy.name}")
|
||||||
|
logger.info(f"🔍 Fallback strategy has ai_recommendations: {fallback_strategy.ai_recommendations is not None}")
|
||||||
|
|
||||||
|
# Try to use ai_recommendations as the strategy data
|
||||||
|
if fallback_strategy.ai_recommendations:
|
||||||
|
logger.info(f"✅ Using ai_recommendations as strategy data for fallback strategy {fallback_strategy.id}")
|
||||||
|
return ResponseBuilder.create_success_response(
|
||||||
|
message="Latest generated strategy retrieved successfully from database (fallback)",
|
||||||
|
data={
|
||||||
|
"user_id": user_id,
|
||||||
|
"strategy": fallback_strategy.ai_recommendations,
|
||||||
|
"completed_at": fallback_strategy.created_at.isoformat(),
|
||||||
|
"strategy_id": fallback_strategy.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info(f"⚠️ Fallback strategy has no ai_recommendations either")
|
||||||
else:
|
else:
|
||||||
logger.info(f"🔍 No strategy record found at all for user: {user_id}")
|
logger.info(f"🔍 No strategy record found at all for user: {user_id}")
|
||||||
except Exception as db_error:
|
except Exception as db_error:
|
||||||
|
|||||||
@@ -1141,7 +1141,7 @@ async def stream_autofill_refresh(
|
|||||||
async def refresh_autofill(
|
async def refresh_autofill(
|
||||||
user_id: Optional[int] = Query(None, description="User ID to build auto-fill for"),
|
user_id: Optional[int] = Query(None, description="User ID to build auto-fill for"),
|
||||||
use_ai: bool = Query(True, description="Use AI augmentation during refresh"),
|
use_ai: bool = Query(True, description="Use AI augmentation during refresh"),
|
||||||
ai_only: bool = Query(False, description="AI-first refresh: return AI overrides when available"),
|
ai_only: bool = Query(True, description="🚨 CRITICAL: Force AI-only generation to ensure real AI values"),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Non-stream endpoint to return a fresh auto-fill payload (no DB writes)."""
|
"""Non-stream endpoint to return a fresh auto-fill payload (no DB writes)."""
|
||||||
@@ -1149,7 +1149,8 @@ async def refresh_autofill(
|
|||||||
actual_user_id = user_id or 1
|
actual_user_id = user_id or 1
|
||||||
started = datetime.utcnow()
|
started = datetime.utcnow()
|
||||||
refresh_service = AutoFillRefreshService(db)
|
refresh_service = AutoFillRefreshService(db)
|
||||||
payload = await refresh_service.build_fresh_payload_with_transparency(actual_user_id, use_ai=use_ai, ai_only=ai_only)
|
# 🚨 CRITICAL: Force AI-only generation for refresh to ensure real AI values
|
||||||
|
payload = await refresh_service.build_fresh_payload_with_transparency(actual_user_id, use_ai=True, ai_only=True)
|
||||||
total_ms = int((datetime.utcnow() - started).total_seconds() * 1000)
|
total_ms = int((datetime.utcnow() - started).total_seconds() * 1000)
|
||||||
meta = payload.get('meta') or {}
|
meta = payload.get('meta') or {}
|
||||||
meta.update({'http_total_ms': total_ms, 'http_started_at': started.isoformat()})
|
meta.update({'http_total_ms': total_ms, 'http_started_at': started.isoformat()})
|
||||||
|
|||||||
@@ -68,23 +68,36 @@ class AIStrategyGenerator:
|
|||||||
try:
|
try:
|
||||||
self.logger.info(f"🚀 Generating comprehensive AI strategy for user: {user_id}")
|
self.logger.info(f"🚀 Generating comprehensive AI strategy for user: {user_id}")
|
||||||
|
|
||||||
|
# Track which components failed during generation
|
||||||
|
failed_components = []
|
||||||
|
|
||||||
# Step 1: Generate base strategy fields (using existing autofill system)
|
# Step 1: Generate base strategy fields (using existing autofill system)
|
||||||
base_strategy = await self._generate_base_strategy_fields(user_id, context)
|
base_strategy = await self._generate_base_strategy_fields(user_id, context)
|
||||||
|
|
||||||
# Step 2: Generate strategic insights and recommendations
|
# Step 2: Generate strategic insights and recommendations
|
||||||
strategic_insights = await self._generate_strategic_insights(base_strategy, context)
|
strategic_insights = await self._generate_strategic_insights(base_strategy, context)
|
||||||
|
if strategic_insights.get("ai_generation_failed"):
|
||||||
|
failed_components.append("strategic_insights")
|
||||||
|
|
||||||
# Step 3: Generate competitive analysis
|
# Step 3: Generate competitive analysis
|
||||||
competitive_analysis = await self._generate_competitive_analysis(base_strategy, context)
|
competitive_analysis = await self._generate_competitive_analysis(base_strategy, context)
|
||||||
|
if competitive_analysis.get("ai_generation_failed"):
|
||||||
|
failed_components.append("competitive_analysis")
|
||||||
|
|
||||||
# Step 4: Generate performance predictions
|
# Step 4: Generate performance predictions
|
||||||
performance_predictions = await self._generate_performance_predictions(base_strategy, context)
|
performance_predictions = await self._generate_performance_predictions(base_strategy, context)
|
||||||
|
if performance_predictions.get("ai_generation_failed"):
|
||||||
|
failed_components.append("performance_predictions")
|
||||||
|
|
||||||
# Step 5: Generate implementation roadmap
|
# Step 5: Generate implementation roadmap
|
||||||
implementation_roadmap = await self._generate_implementation_roadmap(base_strategy, context)
|
implementation_roadmap = await self._generate_implementation_roadmap(base_strategy, context)
|
||||||
|
if implementation_roadmap.get("ai_generation_failed"):
|
||||||
|
failed_components.append("implementation_roadmap")
|
||||||
|
|
||||||
# Step 6: Generate risk assessment
|
# Step 6: Generate risk assessment
|
||||||
risk_assessment = await self._generate_risk_assessment(base_strategy, context)
|
risk_assessment = await self._generate_risk_assessment(base_strategy, context)
|
||||||
|
if risk_assessment.get("ai_generation_failed"):
|
||||||
|
failed_components.append("risk_assessment")
|
||||||
|
|
||||||
# Step 7: Compile comprehensive strategy (NO CONTENT CALENDAR)
|
# Step 7: Compile comprehensive strategy (NO CONTENT CALENDAR)
|
||||||
comprehensive_strategy = {
|
comprehensive_strategy = {
|
||||||
@@ -97,7 +110,9 @@ class AIStrategyGenerator:
|
|||||||
"personalization_level": "high",
|
"personalization_level": "high",
|
||||||
"ai_generated": True,
|
"ai_generated": True,
|
||||||
"comprehensive": True,
|
"comprehensive": True,
|
||||||
"content_calendar_ready": False # Indicates calendar needs to be generated separately
|
"content_calendar_ready": False, # Indicates calendar needs to be generated separately
|
||||||
|
"failed_components": failed_components,
|
||||||
|
"generation_status": "partial" if failed_components else "complete"
|
||||||
},
|
},
|
||||||
"base_strategy": base_strategy,
|
"base_strategy": base_strategy,
|
||||||
"strategic_insights": strategic_insights,
|
"strategic_insights": strategic_insights,
|
||||||
@@ -114,7 +129,11 @@ class AIStrategyGenerator:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.logger.info(f"✅ Comprehensive AI strategy generated successfully for user: {user_id}")
|
if failed_components:
|
||||||
|
self.logger.warning(f"⚠️ Strategy generated with partial AI components. Failed: {failed_components}")
|
||||||
|
self.logger.info(f"✅ Partial AI strategy generated successfully for user: {user_id}")
|
||||||
|
else:
|
||||||
|
self.logger.info(f"✅ Comprehensive AI strategy generated successfully for user: {user_id}")
|
||||||
return comprehensive_strategy
|
return comprehensive_strategy
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -223,8 +242,15 @@ class AIStrategyGenerator:
|
|||||||
return transformed_response
|
return transformed_response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error generating strategic insights: {str(e)}")
|
logger.warning(f"⚠️ AI service overload or error during strategic insights: {str(e)}")
|
||||||
raise RuntimeError(f"Failed to generate strategic insights: {str(e)}")
|
logger.info("🔄 Continuing strategy generation without strategic insights...")
|
||||||
|
|
||||||
|
# Return empty strategic insights to allow strategy generation to continue
|
||||||
|
return {
|
||||||
|
"insights": [],
|
||||||
|
"ai_generation_failed": True,
|
||||||
|
"failure_reason": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
async def _generate_competitive_analysis(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
async def _generate_competitive_analysis(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||||
"""Generate competitive analysis using AI."""
|
"""Generate competitive analysis using AI."""
|
||||||
@@ -300,8 +326,18 @@ class AIStrategyGenerator:
|
|||||||
return transformed_response
|
return transformed_response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error generating competitive analysis: {str(e)}")
|
logger.warning(f"⚠️ AI service overload or error during competitive analysis: {str(e)}")
|
||||||
raise RuntimeError(f"Failed to generate competitive analysis: {str(e)}")
|
logger.info("🔄 Continuing strategy generation without competitive analysis...")
|
||||||
|
|
||||||
|
# Return empty competitive analysis to allow strategy generation to continue
|
||||||
|
return {
|
||||||
|
"competitors": [],
|
||||||
|
"market_gaps": [],
|
||||||
|
"opportunities": [],
|
||||||
|
"recommendations": [],
|
||||||
|
"ai_generation_failed": True,
|
||||||
|
"failure_reason": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
async def _generate_content_calendar(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
async def _generate_content_calendar(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||||
"""Generate content calendar using AI."""
|
"""Generate content calendar using AI."""
|
||||||
@@ -502,8 +538,18 @@ class AIStrategyGenerator:
|
|||||||
return transformed_response
|
return transformed_response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error generating performance predictions: {str(e)}")
|
logger.warning(f"⚠️ AI service overload or error during performance predictions: {str(e)}")
|
||||||
raise RuntimeError(f"Failed to generate performance predictions: {str(e)}")
|
logger.info("🔄 Continuing strategy generation without performance predictions...")
|
||||||
|
|
||||||
|
# Return empty performance predictions to allow strategy generation to continue
|
||||||
|
return {
|
||||||
|
"traffic_predictions": {},
|
||||||
|
"engagement_predictions": {},
|
||||||
|
"conversion_predictions": {},
|
||||||
|
"roi_predictions": {},
|
||||||
|
"ai_generation_failed": True,
|
||||||
|
"failure_reason": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
async def _generate_implementation_roadmap(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
async def _generate_implementation_roadmap(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||||
"""Generate implementation roadmap using AI."""
|
"""Generate implementation roadmap using AI."""
|
||||||
@@ -600,8 +646,19 @@ class AIStrategyGenerator:
|
|||||||
return transformed_response
|
return transformed_response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error generating implementation roadmap: {str(e)}")
|
logger.warning(f"⚠️ AI service overload or error during implementation roadmap: {str(e)}")
|
||||||
raise RuntimeError(f"Failed to generate implementation roadmap: {str(e)}")
|
logger.info("🔄 Continuing strategy generation without implementation roadmap...")
|
||||||
|
|
||||||
|
# Return empty implementation roadmap to allow strategy generation to continue
|
||||||
|
return {
|
||||||
|
"phases": [],
|
||||||
|
"timeline": {},
|
||||||
|
"resource_allocation": {},
|
||||||
|
"success_metrics": [],
|
||||||
|
"total_duration": "TBD",
|
||||||
|
"ai_generation_failed": True,
|
||||||
|
"failure_reason": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
async def _generate_risk_assessment(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
async def _generate_risk_assessment(self, base_strategy: Dict[str, Any], context: Dict[str, Any], ai_manager: Optional[Any] = None) -> Dict[str, Any]:
|
||||||
"""Generate risk assessment using AI."""
|
"""Generate risk assessment using AI."""
|
||||||
@@ -738,8 +795,29 @@ class AIStrategyGenerator:
|
|||||||
return transformed_response
|
return transformed_response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"❌ Error generating risk assessment: {str(e)}")
|
logger.warning(f"⚠️ AI service overload or error during risk assessment: {str(e)}")
|
||||||
raise RuntimeError(f"Failed to generate risk assessment: {str(e)}")
|
logger.info("🔄 Continuing strategy generation without risk assessment...")
|
||||||
|
|
||||||
|
# Return empty risk assessment to allow strategy generation to continue
|
||||||
|
return {
|
||||||
|
"risks": [],
|
||||||
|
"overall_risk_level": "Medium",
|
||||||
|
"risk_categories": {
|
||||||
|
"technical_risks": [],
|
||||||
|
"market_risks": [],
|
||||||
|
"operational_risks": [],
|
||||||
|
"financial_risks": []
|
||||||
|
},
|
||||||
|
"mitigation_strategies": [],
|
||||||
|
"monitoring_framework": {
|
||||||
|
"key_indicators": [],
|
||||||
|
"monitoring_frequency": "Monthly",
|
||||||
|
"escalation_procedures": [],
|
||||||
|
"review_schedule": "Quarterly"
|
||||||
|
},
|
||||||
|
"ai_generation_failed": True,
|
||||||
|
"failure_reason": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
def _build_strategic_insights_prompt(self, base_strategy: Dict[str, Any], context: Dict[str, Any]) -> str:
|
def _build_strategic_insights_prompt(self, base_strategy: Dict[str, Any], context: Dict[str, Any]) -> str:
|
||||||
"""Build prompt for strategic insights generation."""
|
"""Build prompt for strategic insights generation."""
|
||||||
|
|||||||
@@ -105,12 +105,17 @@ async def rate_limit_middleware(request: Request, call_next):
|
|||||||
client_ip = request.client.host if request.client else "unknown"
|
client_ip = request.client.host if request.client else "unknown"
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
|
|
||||||
# Exempt streaming endpoints from rate limiting
|
# Exempt streaming endpoints and frequently called endpoints from rate limiting
|
||||||
path = request.url.path
|
path = request.url.path
|
||||||
if any(streaming_path in path for streaming_path in [
|
if any(streaming_path in path for streaming_path in [
|
||||||
"/stream/strategies",
|
"/stream/strategies",
|
||||||
"/stream/strategic-intelligence",
|
"/stream/strategic-intelligence",
|
||||||
"/stream/keyword-research"
|
"/stream/keyword-research",
|
||||||
|
"/latest-strategy", # Exempt latest strategy endpoint from rate limiting
|
||||||
|
"/ai-analytics", # Exempt AI analytics endpoint from rate limiting
|
||||||
|
"/gap-analysis", # Exempt gap analysis endpoint from rate limiting
|
||||||
|
"/calendar-events", # Exempt calendar events endpoint from rate limiting
|
||||||
|
"/health" # Exempt health check endpoints from rate limiting
|
||||||
]):
|
]):
|
||||||
# Allow streaming endpoints without rate limiting
|
# Allow streaming endpoints without rate limiting
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
@@ -125,7 +130,12 @@ async def rate_limit_middleware(request: Request, call_next):
|
|||||||
logger.warning(f"Rate limit exceeded for {client_ip}")
|
logger.warning(f"Rate limit exceeded for {client_ip}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=429,
|
status_code=429,
|
||||||
content={"detail": "Too many requests", "retry_after": RATE_LIMIT_WINDOW}
|
content={"detail": "Too many requests", "retry_after": RATE_LIMIT_WINDOW},
|
||||||
|
headers={
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "*",
|
||||||
|
"Access-Control-Allow-Headers": "*"
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add current request
|
# Add current request
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"files": {
|
|
||||||
"main.css": "/static/css/main.c9966057.css",
|
|
||||||
"main.js": "/static/js/main.bdc25f29.js",
|
|
||||||
"index.html": "/index.html",
|
|
||||||
"main.c9966057.css.map": "/static/css/main.c9966057.css.map",
|
|
||||||
"main.bdc25f29.js.map": "/static/js/main.bdc25f29.js.map"
|
|
||||||
},
|
|
||||||
"entrypoints": [
|
|
||||||
"static/css/main.c9966057.css",
|
|
||||||
"static/js/main.bdc25f29.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Alwrity - AI Content Creation Platform"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Alwrity - AI Content Creation Platform</title><script defer="defer" src="/static/js/main.bdc25f29.js"></script><link href="/static/css/main.c9966057.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
@import url(https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap);html{scroll-behavior:smooth}:focus{outline:2px solid #667eea;outline-offset:2px}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.7}}@keyframes shimmer{0%{background-position:-200px 0}to{background-position:calc(200px + 100%) 0}}@keyframes float{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes glow{0%,to{box-shadow:0 0 5px #667eea80}50%{box-shadow:0 0 20px #667eeacc}}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;font-weight:400;line-height:1.6}button{transition:all .3s cubic-bezier(.4,0,.2,1)}button:active{transform:scale(.98)}.MuiCard-root{transition:all .3s cubic-bezier(.4,0,.2,1)}.MuiCard-root:hover{transform:translateY(-2px)}.glass-effect{-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);background:#ffffff1a;border:1px solid #fff3;border-radius:12px}.glass-effect-hover:hover{background:#ffffff26;border:1px solid #ffffff4d;box-shadow:0 8px 32px #0000001a;transform:translateY(-4px)}.MuiTextField-root .MuiOutlinedInput-root{transition:all .3s ease}.MuiTextField-root .MuiOutlinedInput-root:hover{transform:translateY(-1px)}.MuiLinearProgress-root{border-radius:4px;overflow:hidden}.MuiLinearProgress-bar{transition:transform .3s ease}.MuiChip-root{font-weight:600;transition:all .3s ease}.MuiChip-root:hover{transform:translateY(-1px)}.MuiAlert-root{border-radius:8px;font-weight:500}.MuiTooltip-tooltip{border-radius:6px;font-weight:500}.MuiStepLabel-root{transition:all .3s ease}.MuiStepLabel-root:hover{transform:scale(1.05)}.MuiIconButton-root{transition:all .3s cubic-bezier(.4,0,.2,1)}.MuiIconButton-root:hover{transform:scale(1.1)}.MuiPaper-root{transition:all .3s cubic-bezier(.4,0,.2,1)}a{text-decoration:none;transition:all .3s ease}a:hover{text-decoration:underline}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-track{background:#f1f1f1;border-radius:4px}::-webkit-scrollbar-thumb{background:#c1c1c1;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#a8a8a8}.loading-shimmer{animation:shimmer 1.5s infinite;background:linear-gradient(90deg,#f0f0f0 25%,#e0e0e0 50%,#f0f0f0 75%);background-size:200px 100%}.focus-ring{position:relative}.focus-ring:after{border:2px solid #0000;border-radius:inherit;bottom:-2px;content:"";left:-2px;position:absolute;right:-2px;top:-2px;transition:border-color .3s ease}.focus-ring:focus:after{border-color:#667eea}.gradient-bg{background:linear-gradient(135deg,#667eea,#764ba2)}.gradient-bg-secondary{background:linear-gradient(135deg,#8b5cf6,#7c3aed)}.gradient-bg-premium{background:linear-gradient(135deg,#667eea,#764ba2 50%,#f093fb)}.shadow-sm{box-shadow:0 1px 2px 0 #0000000d}.shadow-md{box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.shadow-lg{box-shadow:0 10px 15px -3px #0000001a,0 4px 6px -2px #0000000d}.shadow-xl{box-shadow:0 20px 25px -5px #0000001a,0 10px 10px -5px #0000000a}.shadow-2xl{box-shadow:0 25px 50px -12px #00000040}.glass-shadow{box-shadow:0 8px 32px #0000001a}.glass-shadow-hover:hover{box-shadow:0 16px 48px #00000026}.animate-float{animation:float 3s ease-in-out infinite}.animate-glow{animation:glow 2s ease-in-out infinite}.animate-fade-in{animation:fadeIn .6s ease-out}.animate-slide-up{animation:slideUp .6s ease-out}.hover-lift:hover{transform:translateY(-4px)}.hover-lift:hover,.hover-scale:hover{transition:transform .3s cubic-bezier(.4,0,.2,1)}.hover-scale:hover{transform:scale(1.05)}.hover-glow:hover{box-shadow:0 0 20px #667eea4d;transition:box-shadow .3s ease}.text-gradient{-webkit-text-fill-color:#0000;background:linear-gradient(135deg,#667eea,#764ba2);-webkit-background-clip:text;background-clip:text}.text-shadow{text-shadow:0 2px 4px #0000004d}@media (max-width:768px){.mobile-hidden{display:none}.mobile-full{width:100%!important}.mobile-text-center{text-align:center}}@media (max-width:480px){.mobile-padding{padding:16px}}@media print{.no-print{display:none!important}}@media (prefers-color-scheme:dark){.auto-dark{background:#1a1a1a;color:#fff}}@media (prefers-contrast:high){.high-contrast{border:2px solid}}@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}
|
|
||||||
/*# sourceMappingURL=main.c9966057.css.map*/
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -100,6 +100,7 @@ const ContentStrategyBuilder: React.FC = () => {
|
|||||||
dataSources,
|
dataSources,
|
||||||
inputDataPoints,
|
inputDataPoints,
|
||||||
personalizationData,
|
personalizationData,
|
||||||
|
confidenceScores,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
saving,
|
saving,
|
||||||
@@ -271,6 +272,48 @@ const ContentStrategyBuilder: React.FC = () => {
|
|||||||
completionStats
|
completionStats
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add ref for scroll to review section
|
||||||
|
const reviewSectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Handle scroll to review section
|
||||||
|
const handleScrollToReview = () => {
|
||||||
|
if (reviewSectionRef.current) {
|
||||||
|
reviewSectionRef.current.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle continue with present values
|
||||||
|
const handleContinueWithPresent = () => {
|
||||||
|
console.log('🎯 Continuing with present autofilled values');
|
||||||
|
// This will show the next button and allow user to proceed
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine if we have autofill data
|
||||||
|
const hasAutofillData = Object.keys(autoPopulatedFields).length > 0;
|
||||||
|
|
||||||
|
// Get last autofill time from session storage or use current time
|
||||||
|
const lastAutofillTime = sessionStorage.getItem('lastAutofillTime') || new Date().toISOString();
|
||||||
|
|
||||||
|
// Get data source from store
|
||||||
|
const dataSource = Object.keys(dataSources).length > 0 ? 'Onboarding Database' : undefined;
|
||||||
|
|
||||||
|
// Log autofill data status for debugging
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('📋 StrategyBuilder: Autofill data status:', {
|
||||||
|
hasAutofillData,
|
||||||
|
autoPopulatedFieldsCount: Object.keys(autoPopulatedFields).length,
|
||||||
|
dataSourcesCount: Object.keys(dataSources).length,
|
||||||
|
inputDataPointsCount: Object.keys(inputDataPoints).length,
|
||||||
|
personalizationDataCount: Object.keys(personalizationData).length,
|
||||||
|
confidenceScoresCount: Object.keys(confidenceScores).length,
|
||||||
|
lastAutofillTime,
|
||||||
|
dataSource
|
||||||
|
});
|
||||||
|
}, [hasAutofillData, autoPopulatedFields, dataSources, inputDataPoints, personalizationData, confidenceScores, lastAutofillTime, dataSource]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Enhanced handleCreateStrategy to show enterprise modal
|
// Enhanced handleCreateStrategy to show enterprise modal
|
||||||
@@ -332,39 +375,16 @@ const ContentStrategyBuilder: React.FC = () => {
|
|||||||
|
|
||||||
if (Object.keys(completionStats.category_completion).length > 0) {
|
if (Object.keys(completionStats.category_completion).length > 0) {
|
||||||
const firstCategory = Object.keys(completionStats.category_completion)[0];
|
const firstCategory = Object.keys(completionStats.category_completion)[0];
|
||||||
console.log('🎯 Setting default category:', firstCategory);
|
|
||||||
setActiveCategory(firstCategory);
|
setActiveCategory(firstCategory);
|
||||||
hasSetDefaultCategory.current = true;
|
hasSetDefaultCategory.current = true;
|
||||||
console.log('✅ hasSetDefaultCategory set to true');
|
|
||||||
}
|
}
|
||||||
}, [completionStats.category_completion]); // Removed activeCategory dependency
|
}, [completionStats.category_completion]); // Removed activeCategory dependency
|
||||||
|
|
||||||
// Debug activeCategory changes
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('🔄 activeCategory changed to:', activeCategory);
|
|
||||||
console.trace('📍 Stack trace for activeCategory change');
|
|
||||||
}, [activeCategory]);
|
|
||||||
|
|
||||||
// Monitor modal state for debugging
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('🎯 Modal state changed - transparencyModalOpen:', transparencyModalOpen);
|
|
||||||
}, [transparencyModalOpen]);
|
|
||||||
|
|
||||||
// Monitor enterprise modal state for debugging
|
// Monitor enterprise modal state for debugging
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🎯 Enterprise modal state changed - showEnterpriseModal:', showEnterpriseModal);
|
|
||||||
|
|
||||||
// If modal was unexpectedly closed, log it
|
// If modal was unexpectedly closed, log it
|
||||||
if (!showEnterpriseModal && aiGenerating) {
|
if (!showEnterpriseModal && aiGenerating) {
|
||||||
console.warn('🎯 WARNING: Enterprise modal closed while AI is generating');
|
console.warn('Enterprise modal closed while AI is generating');
|
||||||
}
|
|
||||||
|
|
||||||
// Only warn about unexpected closure if it's not due to hot reload
|
|
||||||
if (!showEnterpriseModal && !aiGenerating) {
|
|
||||||
const savedModalState = sessionStorage.getItem('showEnterpriseModal');
|
|
||||||
if (savedModalState !== 'true') {
|
|
||||||
console.warn('🎯 WARNING: Enterprise modal closed unexpectedly (not due to hot reload)');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [showEnterpriseModal, aiGenerating]);
|
}, [showEnterpriseModal, aiGenerating]);
|
||||||
|
|
||||||
@@ -382,14 +402,27 @@ const ContentStrategyBuilder: React.FC = () => {
|
|||||||
|
|
||||||
// Wrapper for the hook function to maintain the same interface
|
// Wrapper for the hook function to maintain the same interface
|
||||||
const handleConfirmCategoryReviewWrapper = () => {
|
const handleConfirmCategoryReviewWrapper = () => {
|
||||||
console.log('🔧 Wrapper called with activeCategory:', activeCategory);
|
|
||||||
handleConfirmCategoryReview(activeCategory);
|
handleConfirmCategoryReview(activeCategory);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
{/* Header with Title (Region B) - Enhanced with Futuristic Styling */}
|
{/* Header with Title (Region B) - Enhanced with Futuristic Styling */}
|
||||||
<HeaderSection autoPopulatedFields={autoPopulatedFields} />
|
<HeaderSection
|
||||||
|
autoPopulatedFields={autoPopulatedFields}
|
||||||
|
dataSources={dataSources}
|
||||||
|
inputDataPoints={inputDataPoints}
|
||||||
|
personalizationData={personalizationData}
|
||||||
|
confidenceScores={confidenceScores}
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
onRefreshAutofill={handleAIRefresh}
|
||||||
|
onContinueWithPresent={handleContinueWithPresent}
|
||||||
|
onScrollToReview={handleScrollToReview}
|
||||||
|
hasAutofillData={hasAutofillData}
|
||||||
|
lastAutofillTime={lastAutofillTime}
|
||||||
|
dataSource={dataSource}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Error Alert */}
|
{/* Error Alert */}
|
||||||
<ErrorAlert
|
<ErrorAlert
|
||||||
@@ -444,81 +477,49 @@ const ContentStrategyBuilder: React.FC = () => {
|
|||||||
onShowEducationalInfo={handleShowEducationalInfo}
|
onShowEducationalInfo={handleShowEducationalInfo}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
|
||||||
<Typography variant="subtitle2" gutterBottom>
|
|
||||||
Quick Actions
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<AutoAwesomeIcon />}
|
|
||||||
onClick={() => setShowAIRecommendations(true)}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
View AI Insights
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<InfoIcon />}
|
|
||||||
onClick={() => setShowDataSourceTransparency(true)}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
View Data Sources
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="outlined"
|
|
||||||
startIcon={<RefreshIcon />}
|
|
||||||
onClick={() => autoPopulateFromOnboarding(true)}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
Refresh Data
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<Grid item xs={12} md={8}>
|
<Grid item xs={12} md={8}>
|
||||||
<Paper sx={{ p: 3, minHeight: '600px', background: 'linear-gradient(180deg, #faf7ff, #f1f0ff)' }}>
|
<Paper sx={{ p: 3, minHeight: '600px', background: 'linear-gradient(180deg, #faf7ff, #f1f0ff)' }}>
|
||||||
<CategoryDetailView
|
<div ref={reviewSectionRef}>
|
||||||
activeCategory={activeCategory}
|
<CategoryDetailView
|
||||||
formData={formData}
|
activeCategory={activeCategory}
|
||||||
formErrors={formErrors}
|
formData={formData}
|
||||||
autoPopulatedFields={autoPopulatedFields}
|
formErrors={formErrors}
|
||||||
dataSources={dataSources}
|
autoPopulatedFields={autoPopulatedFields}
|
||||||
inputDataPoints={inputDataPoints}
|
dataSources={dataSources}
|
||||||
personalizationData={personalizationData}
|
inputDataPoints={inputDataPoints}
|
||||||
completionStats={completionStats}
|
personalizationData={personalizationData}
|
||||||
reviewedCategories={reviewedCategories}
|
completionStats={completionStats}
|
||||||
isMarkingReviewed={isMarkingReviewed}
|
reviewedCategories={reviewedCategories}
|
||||||
showEducationalInfo={showEducationalInfo}
|
isMarkingReviewed={isMarkingReviewed}
|
||||||
STRATEGIC_INPUT_FIELDS={STRATEGIC_INPUT_FIELDS}
|
showEducationalInfo={showEducationalInfo}
|
||||||
onUpdateFormField={updateFormField}
|
STRATEGIC_INPUT_FIELDS={STRATEGIC_INPUT_FIELDS}
|
||||||
onValidateFormField={validateFormField}
|
onUpdateFormField={updateFormField}
|
||||||
onShowTooltip={setShowTooltip}
|
onValidateFormField={validateFormField}
|
||||||
onViewDataSource={(fieldId) => {
|
onShowTooltip={setShowTooltip}
|
||||||
// If a specific field is provided, show field-specific data source info
|
onViewDataSource={(fieldId) => {
|
||||||
if (fieldId) {
|
// If a specific field is provided, show field-specific data source info
|
||||||
console.log('🎯 Viewing data source for field:', fieldId);
|
if (fieldId) {
|
||||||
// For now, just open the general data source transparency modal
|
console.log('🎯 Viewing data source for field:', fieldId);
|
||||||
// In the future, this could open a field-specific modal
|
// For now, just open the general data source transparency modal
|
||||||
setShowDataSourceTransparency(true);
|
// In the future, this could open a field-specific modal
|
||||||
} else {
|
setShowDataSourceTransparency(true);
|
||||||
setShowDataSourceTransparency(true);
|
} else {
|
||||||
}
|
setShowDataSourceTransparency(true);
|
||||||
}}
|
}
|
||||||
onConfirmCategoryReview={handleConfirmCategoryReviewWrapper}
|
}}
|
||||||
onSetActiveCategory={setActiveCategory}
|
onConfirmCategoryReview={handleConfirmCategoryReviewWrapper}
|
||||||
onSetShowEducationalInfo={setShowEducationalInfo}
|
onSetActiveCategory={setActiveCategory}
|
||||||
getCategoryIcon={getCategoryIcon}
|
onSetShowEducationalInfo={setShowEducationalInfo}
|
||||||
getCategoryColor={getCategoryColor}
|
getCategoryIcon={getCategoryIcon}
|
||||||
getEducationalContent={getEducationalContent}
|
getCategoryColor={getCategoryColor}
|
||||||
/>
|
getEducationalContent={getEducationalContent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -560,11 +561,17 @@ const ContentStrategyBuilder: React.FC = () => {
|
|||||||
educationalContent={storeEducationalContent}
|
educationalContent={storeEducationalContent}
|
||||||
generationProgress={storeGenerationProgress}
|
generationProgress={storeGenerationProgress}
|
||||||
onReviewStrategy={() => {
|
onReviewStrategy={() => {
|
||||||
console.log('🎯 User clicked "Next: Review Strategy and Create Calendar"');
|
|
||||||
setShowEducationalModal(false);
|
setShowEducationalModal(false);
|
||||||
|
|
||||||
|
// Set flag to indicate coming from strategy builder
|
||||||
|
sessionStorage.setItem('fromStrategyBuilder', 'true');
|
||||||
|
|
||||||
// Navigate to content planning dashboard with Content Strategy tab active
|
// Navigate to content planning dashboard with Content Strategy tab active
|
||||||
navigate('/content-planning', {
|
navigate('/content-planning', {
|
||||||
state: { activeTab: 0 } // 0 = Content Strategy tab
|
state: {
|
||||||
|
activeTab: 0, // 0 = Content Strategy tab
|
||||||
|
fromStrategyBuilder: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -601,10 +608,6 @@ const ContentStrategyBuilder: React.FC = () => {
|
|||||||
open={transparencyModalOpen}
|
open={transparencyModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setTransparencyModalOpen(false);
|
setTransparencyModalOpen(false);
|
||||||
// Ensure form data is refreshed after modal closes
|
|
||||||
console.log('🎯 Modal closed - ensuring form data is updated');
|
|
||||||
console.log('🎯 Current autoPopulatedFields:', Object.keys(autoPopulatedFields || {}));
|
|
||||||
console.log('🎯 Current formData keys:', Object.keys(formData || {}));
|
|
||||||
}}
|
}}
|
||||||
autoPopulatedFields={autoPopulatedFields}
|
autoPopulatedFields={autoPopulatedFields}
|
||||||
dataSources={dataSources}
|
dataSources={dataSources}
|
||||||
@@ -621,8 +624,6 @@ const ContentStrategyBuilder: React.FC = () => {
|
|||||||
<EnterpriseDatapointsModal
|
<EnterpriseDatapointsModal
|
||||||
open={showEnterpriseModal}
|
open={showEnterpriseModal}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
console.log('🎯 Enterprise modal onClose called');
|
|
||||||
console.log('🎯 Current aiGenerating state:', aiGenerating);
|
|
||||||
setShowEnterpriseModal(false);
|
setShowEnterpriseModal(false);
|
||||||
sessionStorage.removeItem('showEnterpriseModal'); // Clear sessionStorage
|
sessionStorage.removeItem('showEnterpriseModal'); // Clear sessionStorage
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -426,26 +426,15 @@ const StrategicInputField: React.FC<StrategicInputFieldProps> = ({
|
|||||||
return option === value;
|
return option === value;
|
||||||
}}
|
}}
|
||||||
value={(() => {
|
value={(() => {
|
||||||
// Debug logging for Autocomplete value parsing
|
|
||||||
console.log('🎯 Autocomplete value parsing:', {
|
|
||||||
fieldId,
|
|
||||||
originalValue: value,
|
|
||||||
valueType: typeof value,
|
|
||||||
isArray: Array.isArray(value),
|
|
||||||
availableOptions: multiSelectConfig.options
|
|
||||||
});
|
|
||||||
|
|
||||||
let parsedValues: string[] = [];
|
let parsedValues: string[] = [];
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
parsedValues = value;
|
parsedValues = value;
|
||||||
console.log('🎯 Using array value:', parsedValues);
|
|
||||||
} else if (typeof value === 'object' && value !== null) {
|
} else if (typeof value === 'object' && value !== null) {
|
||||||
// Handle object values (convert to array of keys or values)
|
// Handle object values (convert to array of keys or values)
|
||||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
// Convert object to array of keys or values based on context
|
// Convert object to array of keys or values based on context
|
||||||
const objectKeys = Object.keys(value);
|
const objectKeys = Object.keys(value);
|
||||||
const objectValues = Object.values(value);
|
|
||||||
|
|
||||||
// For traffic_sources, we might want to use the keys or convert percentages to options
|
// For traffic_sources, we might want to use the keys or convert percentages to options
|
||||||
if (fieldId === 'traffic_sources') {
|
if (fieldId === 'traffic_sources') {
|
||||||
@@ -466,12 +455,9 @@ const StrategicInputField: React.FC<StrategicInputFieldProps> = ({
|
|||||||
parsedValues = objectKeys
|
parsedValues = objectKeys
|
||||||
.map(key => trafficMapping[key.toLowerCase()])
|
.map(key => trafficMapping[key.toLowerCase()])
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
console.log('🎯 Converted object to traffic sources:', parsedValues);
|
|
||||||
} else {
|
} else {
|
||||||
// For other fields, use object keys
|
// For other fields, use object keys
|
||||||
parsedValues = objectKeys;
|
parsedValues = objectKeys;
|
||||||
console.log('🎯 Using object keys:', parsedValues);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof value === 'string') {
|
} else if (typeof value === 'string') {
|
||||||
@@ -480,10 +466,8 @@ const StrategicInputField: React.FC<StrategicInputFieldProps> = ({
|
|||||||
const parsed = JSON.parse(value);
|
const parsed = JSON.parse(value);
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
parsedValues = parsed;
|
parsedValues = parsed;
|
||||||
console.log('🎯 Parsed as JSON array:', parsedValues);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('🎯 JSON parse failed, trying alternative parsing');
|
|
||||||
// If not valid JSON, try to extract array-like content
|
// If not valid JSON, try to extract array-like content
|
||||||
if (value.startsWith('[') && value.endsWith(']')) {
|
if (value.startsWith('[') && value.endsWith(']')) {
|
||||||
// Remove outer brackets and try to parse as comma-separated
|
// Remove outer brackets and try to parse as comma-separated
|
||||||
@@ -493,37 +477,48 @@ const StrategicInputField: React.FC<StrategicInputFieldProps> = ({
|
|||||||
// Remove quotes and trim
|
// Remove quotes and trim
|
||||||
return item.trim().replace(/^["']|["']$/g, '');
|
return item.trim().replace(/^["']|["']$/g, '');
|
||||||
}).filter(item => item);
|
}).filter(item => item);
|
||||||
console.log('🎯 Parsed as array-like string:', parsedValues);
|
|
||||||
} else if (value.includes(',')) {
|
} else if (value.includes(',')) {
|
||||||
// If not array-like, try simple comma splitting
|
// If not array-like, try simple comma splitting
|
||||||
parsedValues = value.split(',').map(item => item.trim()).filter(item => item);
|
parsedValues = value.split(',').map(item => item.trim()).filter(item => item);
|
||||||
console.log('🎯 Parsed as comma-separated string:', parsedValues);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter values to only include valid options
|
// Filter values to only include valid options and improve matching
|
||||||
const validOptions = multiSelectConfig.options || [];
|
const validOptions = multiSelectConfig.options || [];
|
||||||
const filteredValues = parsedValues.filter(val => {
|
const filteredValues = parsedValues.filter(val => {
|
||||||
// Check for exact match
|
// Check for exact match
|
||||||
if (validOptions.includes(val)) {
|
if (validOptions.includes(val)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Check for partial match (case-insensitive)
|
// Check for partial match (case-insensitive) with better logic
|
||||||
const partialMatch = validOptions.find(option =>
|
const partialMatch = validOptions.find(option => {
|
||||||
option.toLowerCase().includes(val.toLowerCase()) ||
|
const optionLower = option.toLowerCase();
|
||||||
val.toLowerCase().includes(option.toLowerCase())
|
const valLower = val.toLowerCase();
|
||||||
);
|
|
||||||
if (partialMatch) {
|
// Check if option contains the value or vice versa
|
||||||
console.log('🎯 Found partial match:', val, '->', partialMatch);
|
return optionLower.includes(valLower) || valLower.includes(optionLower);
|
||||||
return true;
|
});
|
||||||
}
|
|
||||||
console.log('🎯 No match found for:', val);
|
return !!partialMatch;
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🎯 Final filtered values:', filteredValues);
|
// Map partial matches to exact options
|
||||||
return filteredValues;
|
const mappedValues = filteredValues.map(val => {
|
||||||
|
const exactMatch = validOptions.find(option => option === val);
|
||||||
|
if (exactMatch) return exactMatch;
|
||||||
|
|
||||||
|
// Find the best partial match
|
||||||
|
const partialMatch = validOptions.find(option => {
|
||||||
|
const optionLower = option.toLowerCase();
|
||||||
|
const valLower = val.toLowerCase();
|
||||||
|
return optionLower.includes(valLower) || valLower.includes(optionLower);
|
||||||
|
});
|
||||||
|
|
||||||
|
return partialMatch || val;
|
||||||
|
});
|
||||||
|
|
||||||
|
return mappedValues;
|
||||||
})()}
|
})()}
|
||||||
onChange={(_, newValue) => handleChange(newValue)}
|
onChange={(_, newValue) => handleChange(newValue)}
|
||||||
renderInput={(params) => (
|
renderInput={(params) => (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Save as SaveIcon
|
Save as SaveIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { ActionButtonsProps, ActionButtonsBusinessLogicProps } from '../types/contentStrategy.types';
|
import { ActionButtonsProps, ActionButtonsBusinessLogicProps } from '../types/contentStrategy.types';
|
||||||
|
import { useContentPlanningStore } from '../../../../../stores/contentPlanningStore';
|
||||||
|
|
||||||
// Business Logic Hook
|
// Business Logic Hook
|
||||||
export const useActionButtonsBusinessLogic = ({
|
export const useActionButtonsBusinessLogic = ({
|
||||||
@@ -29,11 +30,18 @@ export const useActionButtonsBusinessLogic = ({
|
|||||||
contentPlanningApi
|
contentPlanningApi
|
||||||
}: ActionButtonsBusinessLogicProps) => {
|
}: ActionButtonsBusinessLogicProps) => {
|
||||||
|
|
||||||
|
// Get the content planning store to cache the latest generated strategy
|
||||||
|
const { setLatestGeneratedStrategy } = useContentPlanningStore();
|
||||||
|
|
||||||
const handleCreateStrategy = async () => {
|
const handleCreateStrategy = async () => {
|
||||||
try {
|
try {
|
||||||
setAIGenerating(true);
|
setAIGenerating(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// Clear any previous cached strategy when starting new generation
|
||||||
|
setLatestGeneratedStrategy(null);
|
||||||
|
console.log('🧹 Cleared previous cached strategy for new generation');
|
||||||
|
|
||||||
console.log('Starting strategy creation...');
|
console.log('Starting strategy creation...');
|
||||||
console.log('Current formData:', formData);
|
console.log('Current formData:', formData);
|
||||||
console.log('FormData ID:', formData.id);
|
console.log('FormData ID:', formData.id);
|
||||||
@@ -152,6 +160,20 @@ export const useActionButtonsBusinessLogic = ({
|
|||||||
(strategy: any) => {
|
(strategy: any) => {
|
||||||
console.log('✅ Strategy generation completed successfully!');
|
console.log('✅ Strategy generation completed successfully!');
|
||||||
setCurrentStrategy(strategy);
|
setCurrentStrategy(strategy);
|
||||||
|
|
||||||
|
// Cache the latest generated strategy in the content planning store
|
||||||
|
console.log('💾 Attempting to cache strategy:', {
|
||||||
|
strategyId: strategy?.id || strategy?.strategy_id,
|
||||||
|
strategyName: strategy?.name || strategy?.strategy_name,
|
||||||
|
hasStrategicInsights: !!strategy?.strategic_insights,
|
||||||
|
hasCompetitiveAnalysis: !!strategy?.competitive_analysis,
|
||||||
|
hasPerformancePredictions: !!strategy?.performance_predictions,
|
||||||
|
hasImplementationRoadmap: !!strategy?.implementation_roadmap,
|
||||||
|
hasRiskAssessment: !!strategy?.risk_assessment
|
||||||
|
});
|
||||||
|
setLatestGeneratedStrategy(strategy);
|
||||||
|
console.log('💾 Cached latest generated strategy in store');
|
||||||
|
|
||||||
// Set progress to 100% when completion is detected
|
// Set progress to 100% when completion is detected
|
||||||
setGenerationProgress(100);
|
setGenerationProgress(100);
|
||||||
console.log('🎯 Setting progress to 100% in onComplete callback');
|
console.log('🎯 Setting progress to 100% in onComplete callback');
|
||||||
@@ -196,6 +218,11 @@ export const useActionButtonsBusinessLogic = ({
|
|||||||
|
|
||||||
const newStrategy = await createEnhancedStrategy(strategyData);
|
const newStrategy = await createEnhancedStrategy(strategyData);
|
||||||
setCurrentStrategy(newStrategy);
|
setCurrentStrategy(newStrategy);
|
||||||
|
|
||||||
|
// Update the cache with the saved strategy
|
||||||
|
setLatestGeneratedStrategy(newStrategy);
|
||||||
|
console.log('💾 Updated cache with saved strategy');
|
||||||
|
|
||||||
setError('Strategy saved successfully!');
|
setError('Strategy saved successfully!');
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(`Error saving strategy: ${err.message || 'Unknown error'}`);
|
setError(`Error saving strategy: ${err.message || 'Unknown error'}`);
|
||||||
|
|||||||
@@ -0,0 +1,457 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Accordion,
|
||||||
|
AccordionSummary,
|
||||||
|
AccordionDetails,
|
||||||
|
Chip,
|
||||||
|
Grid,
|
||||||
|
Paper,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
ListItemIcon,
|
||||||
|
Alert,
|
||||||
|
LinearProgress,
|
||||||
|
Tooltip,
|
||||||
|
IconButton
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
ExpandMore as ExpandMoreIcon,
|
||||||
|
Info as InfoIcon,
|
||||||
|
DataUsage as DataUsageIcon,
|
||||||
|
Psychology as PsychologyIcon,
|
||||||
|
Person as PersonIcon,
|
||||||
|
Analytics as AnalyticsIcon,
|
||||||
|
CheckCircle as CheckCircleIcon,
|
||||||
|
Warning as WarningIcon,
|
||||||
|
Close as CloseIcon,
|
||||||
|
Refresh as RefreshIcon,
|
||||||
|
TrendingUp as TrendingUpIcon,
|
||||||
|
Security as SecurityIcon,
|
||||||
|
Visibility as VisibilityIcon,
|
||||||
|
AutoAwesome as AutoAwesomeIcon
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
interface AutofillDataTransparencyProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
autoPopulatedFields: Record<string, any>;
|
||||||
|
dataSources: Record<string, string>;
|
||||||
|
inputDataPoints: Record<string, any>;
|
||||||
|
personalizationData: Record<string, any>;
|
||||||
|
confidenceScores: Record<string, number>;
|
||||||
|
lastAutofillTime?: string;
|
||||||
|
dataSource?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutofillDataTransparency: React.FC<AutofillDataTransparencyProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
autoPopulatedFields,
|
||||||
|
dataSources,
|
||||||
|
inputDataPoints,
|
||||||
|
personalizationData,
|
||||||
|
confidenceScores,
|
||||||
|
lastAutofillTime,
|
||||||
|
dataSource
|
||||||
|
}) => {
|
||||||
|
const [activeAccordion, setActiveAccordion] = useState<string | false>('data-sources');
|
||||||
|
|
||||||
|
const handleAccordionChange = (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
|
||||||
|
setActiveAccordion(isExpanded ? panel : false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate data freshness
|
||||||
|
const getDataFreshness = () => {
|
||||||
|
if (!lastAutofillTime) return 'Unknown';
|
||||||
|
const lastUpdate = new Date(lastAutofillTime);
|
||||||
|
const now = new Date();
|
||||||
|
const diffInHours = Math.floor((now.getTime() - lastUpdate.getTime()) / (1000 * 60 * 60));
|
||||||
|
|
||||||
|
if (diffInHours < 1) return 'Just now';
|
||||||
|
if (diffInHours < 24) return `${diffInHours} hours ago`;
|
||||||
|
const diffInDays = Math.floor(diffInHours / 24);
|
||||||
|
return `${diffInDays} days ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get data quality score
|
||||||
|
const getDataQualityScore = () => {
|
||||||
|
const scores = Object.values(confidenceScores);
|
||||||
|
if (scores.length === 0) return 0;
|
||||||
|
return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get field count by category
|
||||||
|
const getFieldCountByCategory = () => {
|
||||||
|
const categories: Record<string, number> = {};
|
||||||
|
Object.keys(autoPopulatedFields).forEach(fieldId => {
|
||||||
|
const category = fieldId.split('_')[0] || 'other';
|
||||||
|
categories[category] = (categories[category] || 0) + 1;
|
||||||
|
});
|
||||||
|
return categories;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataQualityScore = getDataQualityScore();
|
||||||
|
const dataFreshness = getDataFreshness();
|
||||||
|
const fieldCountByCategory = getFieldCountByCategory();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
maxWidth="lg"
|
||||||
|
fullWidth
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
borderRadius: 3,
|
||||||
|
background: 'linear-gradient(135deg, #f8f9ff 0%, #f1f4ff 100%)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<VisibilityIcon color="primary" sx={{ fontSize: 28 }} />
|
||||||
|
<Typography variant="h5" fontWeight="bold" color="primary">
|
||||||
|
Autofill Data Transparency
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton onClick={onClose} size="large">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
|
Complete transparency about how your strategy inputs were auto-populated
|
||||||
|
</Typography>
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogContent sx={{ p: 3 }}>
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<Grid container spacing={3} sx={{ mb: 3 }}>
|
||||||
|
<Grid item xs={12} md={3}>
|
||||||
|
<Paper sx={{ p: 2, textAlign: 'center', background: 'linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%)' }}>
|
||||||
|
<DataUsageIcon color="primary" sx={{ fontSize: 40, mb: 1 }} />
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
{Object.keys(autoPopulatedFields).length}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Fields Auto-populated
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={3}>
|
||||||
|
<Paper sx={{ p: 2, textAlign: 'center', background: 'linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%)' }}>
|
||||||
|
<TrendingUpIcon color="secondary" sx={{ fontSize: 40, mb: 1 }} />
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
{dataQualityScore}%
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Data Quality Score
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={3}>
|
||||||
|
<Paper sx={{ p: 2, textAlign: 'center', background: 'linear-gradient(135deg, #e8f5e8 0%, #c8e6c9 100%)' }}>
|
||||||
|
<RefreshIcon color="success" sx={{ fontSize: 40, mb: 1 }} />
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
{dataFreshness}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Last Updated
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={3}>
|
||||||
|
<Paper sx={{ p: 2, textAlign: 'center', background: 'linear-gradient(135deg, #fff3e0 0%, #ffcc02 100%)' }}>
|
||||||
|
<SecurityIcon color="warning" sx={{ fontSize: 40, mb: 1 }} />
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
{Object.keys(dataSources).length}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Data Sources Used
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Data Quality Indicator */}
|
||||||
|
<Alert
|
||||||
|
severity={dataQualityScore >= 80 ? 'success' : dataQualityScore >= 60 ? 'warning' : 'error'}
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
icon={dataQualityScore >= 80 ? <CheckCircleIcon /> : <WarningIcon />}
|
||||||
|
>
|
||||||
|
<Typography variant="body1" fontWeight="bold">
|
||||||
|
Data Quality Assessment: {dataQualityScore >= 80 ? 'Excellent' : dataQualityScore >= 60 ? 'Good' : 'Needs Review'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
Based on confidence scores from {Object.keys(confidenceScores).length} fields
|
||||||
|
</Typography>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={dataQualityScore}
|
||||||
|
sx={{ mt: 1, height: 8, borderRadius: 4 }}
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* Detailed Information Accordions */}
|
||||||
|
<Accordion
|
||||||
|
expanded={activeAccordion === 'data-sources'}
|
||||||
|
onChange={handleAccordionChange('data-sources')}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<DataUsageIcon color="primary" />
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
Data Sources & Integration
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Primary Data Sources
|
||||||
|
</Typography>
|
||||||
|
<List dense>
|
||||||
|
{Object.entries(dataSources).map(([fieldId, source]) => (
|
||||||
|
<ListItem key={fieldId}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<InfoIcon color="primary" />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={fieldId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||||
|
secondary={source}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Integration Details
|
||||||
|
</Typography>
|
||||||
|
<Paper sx={{ p: 2, background: '#f8f9fa' }}>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Data Source:</strong> {dataSource || 'Onboarding Integration'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Integration Method:</strong> AI-Powered Analysis
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Data Processing:</strong> Real-time with validation
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>Privacy Compliance:</strong> GDPR & CCPA Compliant
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion
|
||||||
|
expanded={activeAccordion === 'user-info'}
|
||||||
|
onChange={handleAccordionChange('user-info')}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<PersonIcon color="primary" />
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
User Information & Personalization
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{Object.entries(personalizationData).map(([fieldId, data]) => (
|
||||||
|
<Grid item xs={12} md={6} key={fieldId}>
|
||||||
|
<Paper sx={{ p: 2, border: '1px solid #e0e0e0' }}>
|
||||||
|
<Typography variant="h6" gutterBottom color="primary">
|
||||||
|
{fieldId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||||
|
</Typography>
|
||||||
|
{typeof data === 'object' && data !== null ? (
|
||||||
|
<Box>
|
||||||
|
{data.explanation && (
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
<strong>Explanation:</strong> {data.explanation}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{data.personalization_factors && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" fontWeight="bold" gutterBottom>
|
||||||
|
Personalization Factors:
|
||||||
|
</Typography>
|
||||||
|
{Object.entries(data.personalization_factors).map(([key, value]) => (
|
||||||
|
<Chip
|
||||||
|
key={key}
|
||||||
|
label={`${key}: ${value}`}
|
||||||
|
size="small"
|
||||||
|
sx={{ mr: 1, mb: 1 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2">{String(data)}</Typography>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion
|
||||||
|
expanded={activeAccordion === 'ai-analysis'}
|
||||||
|
onChange={handleAccordionChange('ai-analysis')}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<PsychologyIcon color="primary" />
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
AI Analysis Results
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
Confidence Scores by Field
|
||||||
|
</Typography>
|
||||||
|
<List dense>
|
||||||
|
{Object.entries(confidenceScores).map(([fieldId, score]) => (
|
||||||
|
<ListItem key={fieldId}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Box sx={{
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: score >= 80 ? 'success.main' : score >= 60 ? 'warning.main' : 'error.main'
|
||||||
|
}} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={fieldId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||||
|
secondary={`${score}% confidence`}
|
||||||
|
/>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={score}
|
||||||
|
sx={{ width: 100, height: 6, borderRadius: 3 }}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
AI Processing Details
|
||||||
|
</Typography>
|
||||||
|
<Paper sx={{ p: 2, background: '#f8f9fa' }}>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>AI Model:</strong> Claude 3.5 Sonnet
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Analysis Type:</strong> Contextual Understanding
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Processing Time:</strong> Real-time
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
<strong>Validation:</strong> Multi-step verification
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>Quality Checks:</strong> Generic placeholder detection
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion
|
||||||
|
expanded={activeAccordion === 'field-details'}
|
||||||
|
onChange={handleAccordionChange('field-details')}
|
||||||
|
>
|
||||||
|
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<AnalyticsIcon color="primary" />
|
||||||
|
<Typography variant="h6" fontWeight="bold">
|
||||||
|
Field-by-Field Breakdown
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{Object.entries(autoPopulatedFields).map(([fieldId, value]) => (
|
||||||
|
<Grid item xs={12} md={6} key={fieldId}>
|
||||||
|
<Paper sx={{ p: 2, border: '1px solid #e0e0e0' }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||||
|
<Typography variant="h6" color="primary">
|
||||||
|
{fieldId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
label={dataSources[fieldId] || 'AI Generated'}
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
<strong>Value:</strong> {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)}
|
||||||
|
</Typography>
|
||||||
|
{confidenceScores[fieldId] && (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
<strong>Confidence:</strong> {confidenceScores[fieldId]}%
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{inputDataPoints[fieldId] && (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
<strong>Data Points:</strong> {Object.keys(inputDataPoints[fieldId]).length} sources
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||||
|
<Button onClick={onClose} variant="outlined">
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<AutoAwesomeIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
// This could trigger a refresh of the autofill data
|
||||||
|
console.log('Refreshing autofill data...');
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh Data
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutofillDataTransparency;
|
||||||
@@ -1,19 +1,118 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Paper,
|
Paper,
|
||||||
Box,
|
Box,
|
||||||
Typography
|
Typography,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
Alert,
|
||||||
|
Collapse,
|
||||||
|
Tooltip,
|
||||||
|
Grid,
|
||||||
|
LinearProgress
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
CheckCircle as CheckCircleIcon
|
CheckCircle as CheckCircleIcon,
|
||||||
|
Refresh as RefreshIcon,
|
||||||
|
PlayArrow as PlayArrowIcon,
|
||||||
|
Schedule as ScheduleIcon,
|
||||||
|
Info as InfoIcon,
|
||||||
|
ArrowDownward as ArrowDownwardIcon,
|
||||||
|
Visibility as VisibilityIcon,
|
||||||
|
DataUsage as DataUsageIcon,
|
||||||
|
TrendingUp as TrendingUpIcon,
|
||||||
|
Security as SecurityIcon,
|
||||||
|
AutoAwesome as AutoAwesomeIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import AutofillDataTransparency from './AutofillDataTransparency';
|
||||||
|
|
||||||
interface HeaderSectionProps {
|
interface HeaderSectionProps {
|
||||||
autoPopulatedFields: any;
|
autoPopulatedFields: any;
|
||||||
|
dataSources: any;
|
||||||
|
inputDataPoints: any;
|
||||||
|
personalizationData: any;
|
||||||
|
confidenceScores: any;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
onRefreshAutofill: () => void;
|
||||||
|
onContinueWithPresent: () => void;
|
||||||
|
onScrollToReview: () => void;
|
||||||
|
hasAutofillData: boolean;
|
||||||
|
lastAutofillTime?: string;
|
||||||
|
dataSource?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderSection: React.FC<HeaderSectionProps> = ({ autoPopulatedFields }) => {
|
const HeaderSection: React.FC<HeaderSectionProps> = ({
|
||||||
|
autoPopulatedFields,
|
||||||
|
dataSources,
|
||||||
|
inputDataPoints,
|
||||||
|
personalizationData,
|
||||||
|
confidenceScores,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
onRefreshAutofill,
|
||||||
|
onContinueWithPresent,
|
||||||
|
onScrollToReview,
|
||||||
|
hasAutofillData,
|
||||||
|
lastAutofillTime,
|
||||||
|
dataSource
|
||||||
|
}) => {
|
||||||
|
const [showTransparencyModal, setShowTransparencyModal] = useState(false);
|
||||||
|
const [showDataInfo, setShowDataInfo] = useState(false);
|
||||||
|
const [showNextButton, setShowNextButton] = useState(false);
|
||||||
|
|
||||||
|
// Show next button when autofill is complete
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasAutofillData && Object.keys(autoPopulatedFields).length > 0) {
|
||||||
|
setShowNextButton(true);
|
||||||
|
}
|
||||||
|
}, [hasAutofillData, autoPopulatedFields]);
|
||||||
|
|
||||||
|
// Determine cache status and show appropriate buttons
|
||||||
|
const getCacheStatus = () => {
|
||||||
|
if (hasAutofillData && Object.keys(autoPopulatedFields).length > 0) {
|
||||||
|
return 'cached';
|
||||||
|
} else if (Object.keys(inputDataPoints).length > 0) {
|
||||||
|
return 'partial';
|
||||||
|
} else {
|
||||||
|
return 'empty';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cacheStatus = getCacheStatus();
|
||||||
|
|
||||||
|
const formatTimeAgo = (timestamp: string) => {
|
||||||
|
const now = new Date();
|
||||||
|
const time = new Date(timestamp);
|
||||||
|
const diffInMinutes = Math.floor((now.getTime() - time.getTime()) / (1000 * 60));
|
||||||
|
|
||||||
|
if (diffInMinutes < 1) return 'Just now';
|
||||||
|
if (diffInMinutes < 60) return `${diffInMinutes} minutes ago`;
|
||||||
|
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)} hours ago`;
|
||||||
|
return `${Math.floor(diffInMinutes / 1440)} days ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate data quality score
|
||||||
|
const getDataQualityScore = () => {
|
||||||
|
const scores = Object.values(confidenceScores).filter((score): score is number => typeof score === 'number');
|
||||||
|
if (scores.length === 0) return 0;
|
||||||
|
return Math.round(scores.reduce((a, b) => a + b, 0) / scores.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get field count by category
|
||||||
|
const getFieldCountByCategory = () => {
|
||||||
|
const categories: Record<string, number> = {};
|
||||||
|
Object.keys(autoPopulatedFields).forEach(fieldId => {
|
||||||
|
const category = fieldId.split('_')[0] || 'other';
|
||||||
|
categories[category] = (categories[category] || 0) + 1;
|
||||||
|
});
|
||||||
|
return categories;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataQualityScore = getDataQualityScore();
|
||||||
|
const fieldCountByCategory = getFieldCountByCategory();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
@@ -22,9 +121,9 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({ autoPopulatedFields }) =>
|
|||||||
>
|
>
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
p: 2.5, // More compact padding
|
p: 2.5,
|
||||||
mb: 3,
|
mb: 3,
|
||||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)', // Enhanced gradient
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%)',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@@ -44,39 +143,379 @@ const HeaderSection: React.FC<HeaderSectionProps> = ({ autoPopulatedFields }) =>
|
|||||||
border: '1px solid rgba(255,255,255,0.2)',
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', position: 'relative', zIndex: 1 }}>
|
<Box sx={{ position: 'relative', zIndex: 1 }}>
|
||||||
<Box>
|
{/* Main Header */}
|
||||||
<Typography
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||||
variant="h4"
|
<Box sx={{ flex: 1 }}>
|
||||||
gutterBottom
|
<Typography
|
||||||
sx={{
|
variant="h4"
|
||||||
fontWeight: 'bold',
|
gutterBottom
|
||||||
background: 'linear-gradient(45deg, #fff, #f0f0f0)',
|
sx={{
|
||||||
backgroundClip: 'text',
|
fontWeight: 'bold',
|
||||||
WebkitBackgroundClip: 'text',
|
background: 'linear-gradient(45deg, #fff, #f0f0f0)',
|
||||||
WebkitTextFillColor: 'transparent',
|
backgroundClip: 'text',
|
||||||
textShadow: '0 0 20px rgba(255,255,255,0.5)',
|
WebkitBackgroundClip: 'text',
|
||||||
mb: 1
|
WebkitTextFillColor: 'transparent',
|
||||||
}}
|
textShadow: '0 0 20px rgba(255,255,255,0.5)',
|
||||||
>
|
mb: 1
|
||||||
AI Content Strategy Co-pilot
|
}}
|
||||||
</Typography>
|
>
|
||||||
<Typography variant="body1" sx={{ opacity: 0.9, fontSize: '0.9rem' }}>
|
AI Content Strategy Co-pilot
|
||||||
Build a comprehensive content strategy with 30+ strategic inputs
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body1" sx={{ opacity: 0.9, fontSize: '0.9rem' }}>
|
||||||
|
Build a comprehensive content strategy with 30+ strategic inputs
|
||||||
{/* Auto-population Status - Moved to header (Region 4) */}
|
</Typography>
|
||||||
{autoPopulatedFields && Object.keys(autoPopulatedFields).length > 0 && (
|
</Box>
|
||||||
<Box sx={{ mt: 1.5, display: 'flex', alignItems: 'center', gap: 1 }}>
|
</Box>
|
||||||
<CheckCircleIcon sx={{ color: 'rgba(255,255,255,0.8)', fontSize: 18 }} />
|
|
||||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', fontSize: '0.85rem' }}>
|
{/* Enhanced Data Status Grid */}
|
||||||
{Object.keys(autoPopulatedFields).length} fields auto-populated from onboarding data
|
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||||
|
{/* Auto-populated Fields Count */}
|
||||||
|
<Grid item xs={6} sm={3}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||||
|
backdropFilter: 'blur(10px)'
|
||||||
|
}}>
|
||||||
|
<DataUsageIcon sx={{ fontSize: 20, color: 'rgba(255, 255, 255, 0.8)' }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.1rem' }}>
|
||||||
|
{Object.keys(autoPopulatedFields).length}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ opacity: 0.8, fontSize: '0.7rem' }}>
|
||||||
|
Fields Auto-populated
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Data Quality Score */}
|
||||||
|
<Grid item xs={6} sm={3}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||||
|
backdropFilter: 'blur(10px)'
|
||||||
|
}}>
|
||||||
|
<TrendingUpIcon sx={{ fontSize: 20, color: 'rgba(255, 255, 255, 0.8)' }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.1rem' }}>
|
||||||
|
{dataQualityScore}%
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ opacity: 0.8, fontSize: '0.7rem' }}>
|
||||||
|
Data Quality
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Last Updated */}
|
||||||
|
<Grid item xs={6} sm={3}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||||
|
backdropFilter: 'blur(10px)'
|
||||||
|
}}>
|
||||||
|
<ScheduleIcon sx={{ fontSize: 20, color: 'rgba(255, 255, 255, 0.8)' }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.1rem' }}>
|
||||||
|
{lastAutofillTime ? formatTimeAgo(lastAutofillTime).split(' ')[0] : 'N/A'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ opacity: 0.8, fontSize: '0.7rem' }}>
|
||||||
|
Last Updated
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Data Sources */}
|
||||||
|
<Grid item xs={6} sm={3}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 2,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||||
|
backdropFilter: 'blur(10px)'
|
||||||
|
}}>
|
||||||
|
<SecurityIcon sx={{ fontSize: 20, color: 'rgba(255, 255, 255, 0.8)' }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 'bold', fontSize: '1.1rem' }}>
|
||||||
|
{Object.keys(dataSources).length}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ opacity: 0.8, fontSize: '0.7rem' }}>
|
||||||
|
Data Sources
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Data Quality Progress Bar */}
|
||||||
|
{dataQualityScore > 0 && (
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
||||||
|
<Typography variant="body2" sx={{ opacity: 0.9, fontSize: '0.8rem' }}>
|
||||||
|
Data Quality Score
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ opacity: 0.9, fontSize: '0.8rem', fontWeight: 'bold' }}>
|
||||||
|
{dataQualityScore}%
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
<LinearProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={dataQualityScore}
|
||||||
|
sx={{
|
||||||
|
height: 6,
|
||||||
|
borderRadius: 3,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
'& .MuiLinearProgress-bar': {
|
||||||
|
background: dataQualityScore >= 80
|
||||||
|
? 'linear-gradient(90deg, #4caf50, #66bb6a)'
|
||||||
|
: dataQualityScore >= 60
|
||||||
|
? 'linear-gradient(90deg, #ff9800, #ffb74d)'
|
||||||
|
: 'linear-gradient(90deg, #f44336, #ef5350)',
|
||||||
|
borderRadius: 3
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Enhanced Status Chips */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 2, flexWrap: 'wrap' }}>
|
||||||
|
{cacheStatus === 'cached' && (
|
||||||
|
<Chip
|
||||||
|
icon={<CheckCircleIcon />}
|
||||||
|
label={`${Object.keys(autoPopulatedFields).length} fields auto-populated`}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'rgba(76, 175, 80, 0.2)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(76, 175, 80, 0.3)',
|
||||||
|
'& .MuiChip-icon': { color: 'rgba(76, 175, 80, 0.8)' },
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '0.8rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{dataSource && (
|
||||||
|
<Tooltip title="Click to view data source information">
|
||||||
|
<Chip
|
||||||
|
icon={<InfoIcon />}
|
||||||
|
label={`Source: ${dataSource}`}
|
||||||
|
onClick={() => setShowDataInfo(!showDataInfo)}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.2)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Category Distribution Chips */}
|
||||||
|
{Object.keys(fieldCountByCategory).length > 0 && (
|
||||||
|
<Chip
|
||||||
|
icon={<AutoAwesomeIcon />}
|
||||||
|
label={`${Object.keys(fieldCountByCategory).length} categories`}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'rgba(156, 39, 176, 0.2)',
|
||||||
|
color: 'white',
|
||||||
|
border: '1px solid rgba(156, 39, 176, 0.3)',
|
||||||
|
'& .MuiChip-icon': { color: 'rgba(156, 39, 176, 0.8)' },
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '0.8rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Data Source Information */}
|
||||||
|
<Collapse in={showDataInfo}>
|
||||||
|
<Alert
|
||||||
|
severity="info"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.2)',
|
||||||
|
color: 'white',
|
||||||
|
'& .MuiAlert-icon': { color: 'rgba(255, 255, 255, 0.8)' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
<strong>Data Source:</strong> {dataSource || 'Onboarding Database'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
<strong>Input Data Points:</strong> {Object.keys(inputDataPoints).length} available
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>Adaptive Monitoring:</strong> ALwrity continuously monitors databases for new data points to ensure you have the latest information.
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
{/* Conditional Action Buttons */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||||
|
{cacheStatus === 'cached' ? (
|
||||||
|
// Case 1: Data exists in cache - show refresh vs continue options
|
||||||
|
<>
|
||||||
|
<Tooltip title="Refresh with latest database data and AI analysis">
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
onClick={onRefreshAutofill}
|
||||||
|
disabled={loading}
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Refreshing...' : 'Refresh & Autofill Inputs'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title="Continue with current autofilled values">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<PlayArrowIcon />}
|
||||||
|
onClick={onContinueWithPresent}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
color: 'white',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.3)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Continue with Present Values
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
) : cacheStatus === 'partial' ? (
|
||||||
|
// Case 2: Partial data - show refresh option
|
||||||
|
<Tooltip title="Refresh with latest database data and AI analysis">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
onClick={onRefreshAutofill}
|
||||||
|
disabled={loading}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'rgba(255, 193, 7, 0.8)',
|
||||||
|
color: 'white',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(255, 193, 7, 0.9)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Refreshing...' : 'Refresh & Autofill Strategy Inputs'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
// Case 3: No data - show initial autofill
|
||||||
|
<Tooltip title="Fetch latest data from database and autofill strategy inputs">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
onClick={onRefreshAutofill}
|
||||||
|
disabled={loading}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: 'rgba(76, 175, 80, 0.8)',
|
||||||
|
color: 'white',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(76, 175, 80, 0.9)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Autofilling...' : 'Refresh & Autofill Strategy Inputs'}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Next Step Button - shown after autofill completion */}
|
||||||
|
{showNextButton && (
|
||||||
|
<Tooltip title="Scroll to review section and mark inputs as reviewed">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<ArrowDownwardIcon />}
|
||||||
|
onClick={onScrollToReview}
|
||||||
|
sx={{
|
||||||
|
background: 'linear-gradient(135deg, #4caf50 0%, #66bb6a 50%, #81c784 100%)',
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 600,
|
||||||
|
'&:hover': {
|
||||||
|
background: 'linear-gradient(135deg, #66bb6a 0%, #81c784 50%, #a5d6a7 100%)',
|
||||||
|
transform: 'translateY(-1px)'
|
||||||
|
},
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next: Review Strategy Inputs & Create Strategy
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Know More Details Button - shown when autofill data exists */}
|
||||||
|
{hasAutofillData && Object.keys(autoPopulatedFields).length > 0 && (
|
||||||
|
<Tooltip title="View detailed information about autofill data sources and AI analysis">
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
startIcon={<VisibilityIcon />}
|
||||||
|
onClick={() => setShowTransparencyModal(true)}
|
||||||
|
sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.8)',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'white',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Know More Details
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
|
{/* Autofill Data Transparency Modal */}
|
||||||
|
<AutofillDataTransparency
|
||||||
|
open={showTransparencyModal}
|
||||||
|
onClose={() => setShowTransparencyModal(false)}
|
||||||
|
autoPopulatedFields={autoPopulatedFields}
|
||||||
|
dataSources={dataSources}
|
||||||
|
inputDataPoints={inputDataPoints}
|
||||||
|
personalizationData={personalizationData}
|
||||||
|
confidenceScores={confidenceScores}
|
||||||
|
lastAutofillTime={lastAutofillTime}
|
||||||
|
dataSource={dataSource}
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,14 +35,12 @@ export const useAIRefresh = ({
|
|||||||
// This approach uses direct HTTP calls with visual feedback
|
// This approach uses direct HTTP calls with visual feedback
|
||||||
|
|
||||||
// Open transparency modal and initialize transparency state
|
// Open transparency modal and initialize transparency state
|
||||||
console.log('🎯 Opening transparency modal...');
|
|
||||||
setTransparencyModalOpen(true);
|
setTransparencyModalOpen(true);
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
setStoreGenerationProgress(0);
|
setStoreGenerationProgress(0);
|
||||||
setCurrentPhase('autofill_initialization');
|
setCurrentPhase('autofill_initialization');
|
||||||
clearTransparencyMessages();
|
clearTransparencyMessages();
|
||||||
addTransparencyMessage('Starting strategy inputs generation process...');
|
addTransparencyMessage('Starting strategy inputs generation process...');
|
||||||
console.log('🎯 Modal state set, transparency initialized');
|
|
||||||
|
|
||||||
setAIGenerating(true);
|
setAIGenerating(true);
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
@@ -68,12 +66,6 @@ export const useAIRefresh = ({
|
|||||||
const transparencyInterval = setInterval(() => {
|
const transparencyInterval = setInterval(() => {
|
||||||
if (messageIndex < transparencyMessages.length) {
|
if (messageIndex < transparencyMessages.length) {
|
||||||
const message = transparencyMessages[messageIndex];
|
const message = transparencyMessages[messageIndex];
|
||||||
console.log('🎯 Raw Polling Message:', {
|
|
||||||
type: message.type,
|
|
||||||
message: message.message,
|
|
||||||
progress: message.progress,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
setCurrentPhase(message.type);
|
setCurrentPhase(message.type);
|
||||||
addTransparencyMessage(message.message);
|
addTransparencyMessage(message.message);
|
||||||
setStoreGenerationProgress(message.progress);
|
setStoreGenerationProgress(message.progress);
|
||||||
@@ -85,80 +77,20 @@ export const useAIRefresh = ({
|
|||||||
}, 2000); // Send a message every 2 seconds for better UX
|
}, 2000); // Send a message every 2 seconds for better UX
|
||||||
|
|
||||||
// Call the non-streaming refresh endpoint (Polling-based approach)
|
// Call the non-streaming refresh endpoint (Polling-based approach)
|
||||||
console.log('🎯 Calling AI refresh endpoint (Polling-based)...');
|
|
||||||
const response = await contentPlanningApi.refreshAutofill(1, true, true);
|
const response = await contentPlanningApi.refreshAutofill(1, true, true);
|
||||||
console.log('🎯 Raw Polling Response:', {
|
|
||||||
success: !!response,
|
|
||||||
hasData: !!response?.fields,
|
|
||||||
responseStructure: {
|
|
||||||
hasFieldsProperty: !!response?.fields,
|
|
||||||
hasSourcesProperty: !!response?.sources,
|
|
||||||
hasMetaProperty: !!response?.meta
|
|
||||||
},
|
|
||||||
fieldsCount: Object.keys(response?.fields || {}).length,
|
|
||||||
sourcesCount: Object.keys(response?.sources || {}).length,
|
|
||||||
meta: response?.meta || {},
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear the transparency interval since we got the response
|
// Clear the transparency interval since we got the response
|
||||||
clearInterval(transparencyInterval);
|
clearInterval(transparencyInterval);
|
||||||
|
|
||||||
// Process the response
|
// Process the response
|
||||||
// The API method already returns the extracted data, not the full response
|
|
||||||
if (response) {
|
if (response) {
|
||||||
// Debug the actual response structure
|
|
||||||
console.log('🎯 Raw response structure:', {
|
|
||||||
responseType: typeof response,
|
|
||||||
responseKeys: Object.keys(response),
|
|
||||||
hasFieldsProperty: response?.hasOwnProperty('fields'),
|
|
||||||
hasSourcesProperty: response?.hasOwnProperty('sources'),
|
|
||||||
hasMetaProperty: response?.hasOwnProperty('meta')
|
|
||||||
});
|
|
||||||
|
|
||||||
// Debug the actual response data
|
|
||||||
console.log('🎯 Raw response:', response);
|
|
||||||
console.log('🎯 Raw response.fields:', response?.fields);
|
|
||||||
console.log('🎯 Raw response.sources:', response?.sources);
|
|
||||||
console.log('🎯 Raw response.meta:', response?.meta);
|
|
||||||
|
|
||||||
// The API method already returns the extracted payload from ResponseBuilder
|
|
||||||
// So response is already the payload with fields, sources, meta, etc.
|
|
||||||
const payload = response;
|
const payload = response;
|
||||||
|
|
||||||
// Debug the payload structure
|
|
||||||
console.log('🎯 Payload structure:', {
|
|
||||||
payloadType: typeof payload,
|
|
||||||
payloadKeys: Object.keys(payload),
|
|
||||||
hasFieldsProperty: payload?.hasOwnProperty('fields'),
|
|
||||||
hasSourcesProperty: payload?.hasOwnProperty('sources'),
|
|
||||||
hasMetaProperty: payload?.hasOwnProperty('meta'),
|
|
||||||
fieldsKeys: payload?.fields ? Object.keys(payload.fields) : 'no fields'
|
|
||||||
});
|
|
||||||
|
|
||||||
const fields = payload.fields || {};
|
const fields = payload.fields || {};
|
||||||
const sources = payload.sources || {};
|
const sources = payload.sources || {};
|
||||||
const inputDataPoints = payload.input_data_points || {};
|
const inputDataPoints = payload.input_data_points || {};
|
||||||
const meta = payload.meta || {};
|
const meta = payload.meta || {};
|
||||||
|
|
||||||
// Debug the extracted data
|
console.log('🎯 AI Refresh - Generated fields:', Object.keys(fields).length);
|
||||||
console.log('🎯 Extracted fields:', fields);
|
|
||||||
console.log('🎯 Extracted sources:', sources);
|
|
||||||
console.log('🎯 Extracted inputDataPoints:', inputDataPoints);
|
|
||||||
console.log('🎯 Extracted meta:', meta);
|
|
||||||
console.log('🎯 Fields count:', Object.keys(fields).length);
|
|
||||||
console.log('🎯 Sources count:', Object.keys(sources).length);
|
|
||||||
console.log('🎯 InputDataPoints count:', Object.keys(inputDataPoints).length);
|
|
||||||
|
|
||||||
console.log('🎯 AI Refresh Result - Payload:', payload);
|
|
||||||
console.log('🎯 AI Refresh Result - Fields:', fields);
|
|
||||||
console.log('🎯 AI Refresh Result - Meta:', meta);
|
|
||||||
console.log('🎯 Fields structure check:', {
|
|
||||||
fieldsType: typeof fields,
|
|
||||||
fieldsKeys: Object.keys(fields),
|
|
||||||
sampleField: fields[Object.keys(fields)[0]],
|
|
||||||
hasValueProperty: fields[Object.keys(fields)[0]]?.hasOwnProperty('value')
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🚨 CRITICAL: Check if AI generation failed
|
// 🚨 CRITICAL: Check if AI generation failed
|
||||||
if (meta.error || !meta.ai_used) {
|
if (meta.error || !meta.ai_used) {
|
||||||
@@ -187,11 +119,11 @@ export const useAIRefresh = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ AI generation successful - generated ${fieldsCount} fields, AI overrides: ${meta.ai_overrides_count || 0}`);
|
console.log(`✅ AI generation successful - ${fieldsCount} fields generated`);
|
||||||
|
|
||||||
// 🚨 CRITICAL: Validate data source (only check for explicit failure states)
|
// 🚨 CRITICAL: Validate data source (only check for explicit failure states)
|
||||||
if (meta.data_source === 'ai_generation_failed' || meta.data_source === 'ai_generation_error') {
|
if (meta.data_source === 'ai_generation_failed' || meta.data_source === 'ai_generation_error') {
|
||||||
console.error('❌ Invalid data source:', meta.data_source);
|
console.error('❌ AI generation failed:', meta.data_source);
|
||||||
setError(`AI generation failed: ${meta.error || 'Invalid data source. Please try again.'}`);
|
setError(`AI generation failed: ${meta.error || 'Invalid data source. Please try again.'}`);
|
||||||
setTransparencyModalOpen(false);
|
setTransparencyModalOpen(false);
|
||||||
setAIGenerating(false);
|
setAIGenerating(false);
|
||||||
@@ -202,51 +134,34 @@ export const useAIRefresh = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ AI generation successful - processing real AI data');
|
|
||||||
|
|
||||||
const fieldValues: Record<string, any> = {};
|
const fieldValues: Record<string, any> = {};
|
||||||
const confidenceScores: Record<string, number> = {};
|
const confidenceScores: Record<string, number> = {};
|
||||||
|
|
||||||
Object.keys(fields).forEach((fieldId) => {
|
Object.keys(fields).forEach((fieldId) => {
|
||||||
const fieldData = fields[fieldId];
|
const fieldData = fields[fieldId];
|
||||||
console.log(`🎯 Processing field ${fieldId}:`, fieldData);
|
|
||||||
|
|
||||||
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
|
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
|
||||||
fieldValues[fieldId] = fieldData.value;
|
fieldValues[fieldId] = fieldData.value;
|
||||||
console.log(`✅ Field ${fieldId} value extracted:`, fieldData.value);
|
|
||||||
|
|
||||||
// Extract confidence score if available
|
// Extract confidence score if available
|
||||||
if (fieldData.confidence) {
|
if (fieldData.confidence) {
|
||||||
confidenceScores[fieldId] = fieldData.confidence;
|
confidenceScores[fieldId] = fieldData.confidence;
|
||||||
console.log(`🎯 Field ${fieldId} confidence: ${fieldData.confidence}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract personalization data if available
|
|
||||||
if (fieldData.personalization_data) {
|
|
||||||
console.log(`🎯 Field ${fieldId} personalization:`, fieldData.personalization_data);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`⚠️ Field ${fieldId} has invalid structure:`, fieldData);
|
console.warn(`⚠️ Field ${fieldId} has invalid structure`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🎯 Processed field values:', Object.keys(fieldValues));
|
// Update the store with the new data - COMPLETELY REPLACE old data
|
||||||
console.log('🎯 Confidence scores:', confidenceScores);
|
|
||||||
console.log('🎯 Field values details:', fieldValues);
|
|
||||||
|
|
||||||
// Update the store with the new data
|
|
||||||
useStrategyBuilderStore.setState((state) => {
|
useStrategyBuilderStore.setState((state) => {
|
||||||
const newState = {
|
const newState = {
|
||||||
autoPopulatedFields: { ...state.autoPopulatedFields, ...fieldValues },
|
autoPopulatedFields: fieldValues, // 🚨 CRITICAL: Replace, don't merge
|
||||||
dataSources: { ...state.dataSources, ...sources },
|
dataSources: sources, // 🚨 CRITICAL: Replace, don't merge
|
||||||
inputDataPoints: { ...state.inputDataPoints, ...inputDataPoints },
|
inputDataPoints: inputDataPoints, // 🚨 CRITICAL: Replace, don't merge
|
||||||
confidenceScores: { ...state.confidenceScores, ...confidenceScores },
|
confidenceScores: confidenceScores, // 🚨 CRITICAL: Replace, don't merge
|
||||||
formData: { ...state.formData, ...fieldValues }
|
formData: { ...state.formData, ...fieldValues } // Keep existing manual edits
|
||||||
};
|
};
|
||||||
console.log('🎯 Updated store state:', newState);
|
console.log('✅ Store updated with fresh AI data:', Object.keys(fieldValues).length, 'fields');
|
||||||
console.log('🎯 Field values being added:', fieldValues);
|
|
||||||
console.log('🎯 Confidence scores being added:', confidenceScores);
|
|
||||||
console.log('🎯 Store autoPopulatedFields count:', Object.keys(newState.autoPopulatedFields).length);
|
|
||||||
return newState;
|
return newState;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,17 +178,14 @@ export const useAIRefresh = ({
|
|||||||
setRefreshProgress(100);
|
setRefreshProgress(100);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
// Update session storage with fresh autofill timestamp
|
||||||
|
sessionStorage.setItem('lastAutofillTime', new Date().toISOString());
|
||||||
|
|
||||||
// Reset refresh state
|
// Reset refresh state
|
||||||
setAIGenerating(false);
|
setAIGenerating(false);
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
console.log('🎯 Polling-based AI refresh completed successfully!', {
|
console.log('✅ AI refresh completed:', Object.keys(fieldValues).length, 'fields generated');
|
||||||
fieldsGenerated: Object.keys(fieldValues).length,
|
|
||||||
confidenceScoresCount: Object.keys(confidenceScores).length,
|
|
||||||
dataSourcesCount: Object.keys(sources).length,
|
|
||||||
approach: 'Polling (No SSE)',
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid response from AI refresh endpoint');
|
throw new Error('Invalid response from AI refresh endpoint');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,24 @@ export const useAutoPopulation = ({
|
|||||||
// Auto-populate from onboarding on first load
|
// Auto-populate from onboarding on first load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoPopulateAttempted && !isAutoPopulating) {
|
if (!autoPopulateAttempted && !isAutoPopulating) {
|
||||||
|
console.log('🚀 useAutoPopulation: Triggering initial auto-population');
|
||||||
|
console.log('📊 useAutoPopulation: Current completion stats:', {
|
||||||
|
totalFields: completionStats?.total_fields || 0,
|
||||||
|
filledFields: completionStats?.filled_fields || 0,
|
||||||
|
completionPercentage: completionStats?.completion_percentage || 0
|
||||||
|
});
|
||||||
|
|
||||||
setIsAutoPopulating(true);
|
setIsAutoPopulating(true);
|
||||||
autoPopulateFromOnboarding();
|
autoPopulateFromOnboarding();
|
||||||
setAutoPopulateAttempted(true);
|
setAutoPopulateAttempted(true);
|
||||||
setIsAutoPopulating(false);
|
setIsAutoPopulating(false);
|
||||||
|
|
||||||
|
console.log('✅ useAutoPopulation: Auto-population triggered successfully');
|
||||||
|
} else {
|
||||||
|
console.log('⏸️ useAutoPopulation: Auto-population skipped', {
|
||||||
|
autoPopulateAttempted,
|
||||||
|
isAutoPopulating
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [autoPopulateAttempted, isAutoPopulating]); // Removed autoPopulateFromOnboarding from dependencies
|
}, [autoPopulateAttempted, isAutoPopulating]); // Removed autoPopulateFromOnboarding from dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -49,30 +49,18 @@ export const useCategoryReview = ({ completionStats, setError, setActiveCategory
|
|||||||
// Use the updated reviewedCategories state that includes the current category
|
// Use the updated reviewedCategories state that includes the current category
|
||||||
const updatedReviewedCategories = new Set([...Array.from(reviewedCategories), activeCategory]);
|
const updatedReviewedCategories = new Set([...Array.from(reviewedCategories), activeCategory]);
|
||||||
|
|
||||||
console.log('🔍 Navigation Debug:', {
|
|
||||||
activeCategory,
|
|
||||||
currentIndex,
|
|
||||||
allCategories,
|
|
||||||
reviewedCategories: Array.from(reviewedCategories),
|
|
||||||
updatedReviewedCategories: Array.from(updatedReviewedCategories)
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextUnreviewedCategory = allCategories.find((categoryId, index) => {
|
const nextUnreviewedCategory = allCategories.find((categoryId, index) => {
|
||||||
if (index <= currentIndex) return false;
|
if (index <= currentIndex) return false;
|
||||||
return !updatedReviewedCategories.has(categoryId);
|
return !updatedReviewedCategories.has(categoryId);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('🎯 Next Category Found:', nextUnreviewedCategory);
|
|
||||||
|
|
||||||
if (nextUnreviewedCategory) {
|
if (nextUnreviewedCategory) {
|
||||||
// Actually navigate to the next category
|
// Actually navigate to the next category
|
||||||
console.log('🚀 Navigating to:', nextUnreviewedCategory);
|
|
||||||
setActiveCategory(nextUnreviewedCategory);
|
setActiveCategory(nextUnreviewedCategory);
|
||||||
setCategoryCompletionMessage(`🎯 Moving to next category: ${nextUnreviewedCategory.split('_').map(word =>
|
setCategoryCompletionMessage(`🎯 Moving to next category: ${nextUnreviewedCategory.split('_').map(word =>
|
||||||
word.charAt(0).toUpperCase() + word.slice(1)
|
word.charAt(0).toUpperCase() + word.slice(1)
|
||||||
).join(' ')}`);
|
).join(' ')}`);
|
||||||
} else {
|
} else {
|
||||||
console.log('🎉 All categories reviewed!');
|
|
||||||
setCategoryCompletionMessage('🎉 All categories reviewed and confirmed! You can now create your strategy.');
|
setCategoryCompletionMessage('🎉 All categories reviewed and confirmed! You can now create your strategy.');
|
||||||
}
|
}
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|||||||
@@ -22,51 +22,42 @@ export const useModalManagement = ({
|
|||||||
|
|
||||||
// Monitor aiGenerating state for debugging
|
// Monitor aiGenerating state for debugging
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🎯 useModalManagement: aiGenerating state changed:', aiGenerating);
|
// Removed verbose logging for cleaner console
|
||||||
}, [aiGenerating]);
|
}, [aiGenerating]);
|
||||||
|
|
||||||
// Handle proceed with current strategy (30 fields)
|
// Handle proceed with current strategy (30 fields)
|
||||||
const handleProceedWithCurrentStrategy = async () => {
|
const handleProceedWithCurrentStrategy = async () => {
|
||||||
console.log('🎯 User clicked "Proceed with Current Strategy"');
|
|
||||||
setShowEnterpriseModal(false);
|
setShowEnterpriseModal(false);
|
||||||
sessionStorage.removeItem('showEnterpriseModal'); // Clear sessionStorage
|
sessionStorage.removeItem('showEnterpriseModal'); // Clear sessionStorage
|
||||||
|
|
||||||
// Add a small delay to ensure modal closes properly before showing educational modal
|
// Add a small delay to ensure modal closes properly before showing educational modal
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
console.log('🎯 Calling original handleCreateStrategy after enterprise modal closes');
|
|
||||||
try {
|
try {
|
||||||
// Ensure we're not already generating
|
// Ensure we're not already generating
|
||||||
if (!aiGenerating && originalHandleCreateStrategyRef.current) {
|
if (!aiGenerating && originalHandleCreateStrategyRef.current) {
|
||||||
console.log('🎯 Starting strategy generation...');
|
|
||||||
await originalHandleCreateStrategyRef.current();
|
await originalHandleCreateStrategyRef.current();
|
||||||
} else {
|
|
||||||
console.log('🎯 Already generating, skipping duplicate call');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('🎯 Error in handleProceedWithCurrentStrategy:', error);
|
console.error('Error in handleProceedWithCurrentStrategy:', error);
|
||||||
}
|
}
|
||||||
}, 300); // Increased delay to ensure modal closes completely
|
}, 300); // Increased delay to ensure modal closes completely
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle add enterprise datapoints (coming soon)
|
// Handle add enterprise datapoints (coming soon)
|
||||||
const handleAddEnterpriseDatapoints = async () => {
|
const handleAddEnterpriseDatapoints = async () => {
|
||||||
console.log('🎯 User clicked "Add Enterprise Datapoints"');
|
|
||||||
setShowEnterpriseModal(false);
|
setShowEnterpriseModal(false);
|
||||||
sessionStorage.removeItem('showEnterpriseModal'); // Clear sessionStorage
|
sessionStorage.removeItem('showEnterpriseModal'); // Clear sessionStorage
|
||||||
|
|
||||||
// For now, just proceed with current strategy
|
// For now, just proceed with current strategy
|
||||||
// In Phase 2, this will enable enterprise datapoints
|
// In Phase 2, this will enable enterprise datapoints
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
console.log('🎯 Calling original handleCreateStrategy for enterprise datapoints');
|
|
||||||
try {
|
try {
|
||||||
// Ensure we're not already generating
|
// Ensure we're not already generating
|
||||||
if (!aiGenerating && originalHandleCreateStrategyRef.current) {
|
if (!aiGenerating && originalHandleCreateStrategyRef.current) {
|
||||||
await originalHandleCreateStrategyRef.current();
|
await originalHandleCreateStrategyRef.current();
|
||||||
} else {
|
|
||||||
console.log('🎯 Already generating, skipping duplicate call');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('🎯 Error in handleAddEnterpriseDatapoints:', error);
|
console.error('Error in handleAddEnterpriseDatapoints:', error);
|
||||||
}
|
}
|
||||||
}, 200); // Increased delay to ensure modal closes completely
|
}, 200); // Increased delay to ensure modal closes completely
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -91,19 +91,9 @@ const StrategyAutofillTransparencyModal: React.FC<StrategyAutofillTransparencyMo
|
|||||||
const [lastMessageCount, setLastMessageCount] = useState(0);
|
const [lastMessageCount, setLastMessageCount] = useState(0);
|
||||||
const [showNewMessageIndicator, setShowNewMessageIndicator] = useState(false);
|
const [showNewMessageIndicator, setShowNewMessageIndicator] = useState(false);
|
||||||
|
|
||||||
// Debug logging for props
|
// Debug logging for props - removed for cleaner console
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🎯 StrategyAutofillTransparencyModal Props:', {
|
// Props monitoring removed for cleaner console
|
||||||
open,
|
|
||||||
autoPopulatedFields: Object.keys(autoPopulatedFields || {}).length,
|
|
||||||
dataSources: Object.keys(dataSources || {}).length,
|
|
||||||
inputDataPoints: Object.keys(inputDataPoints || {}).length,
|
|
||||||
isGenerating,
|
|
||||||
generationProgress,
|
|
||||||
currentPhase,
|
|
||||||
transparencyMessages: transparencyMessages?.length,
|
|
||||||
error
|
|
||||||
});
|
|
||||||
}, [open, autoPopulatedFields, dataSources, inputDataPoints, isGenerating, generationProgress, currentPhase, transparencyMessages, error]);
|
}, [open, autoPopulatedFields, dataSources, inputDataPoints, isGenerating, generationProgress, currentPhase, transparencyMessages, error]);
|
||||||
|
|
||||||
// Auto-scroll to bottom when new messages arrive
|
// Auto-scroll to bottom when new messages arrive
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, memo } from 'react';
|
||||||
import { Box, CircularProgress, Alert, Typography } from '@mui/material';
|
import { Box, CircularProgress, Alert, Typography } from '@mui/material';
|
||||||
import StrategyHeader from './components/StrategyHeader';
|
import StrategyHeader from './components/StrategyHeader';
|
||||||
import StrategicInsightsCard from './components/StrategicInsightsCard';
|
import StrategicInsightsCard from './components/StrategicInsightsCard';
|
||||||
@@ -7,22 +7,33 @@ import PerformancePredictionsCard from './components/PerformancePredictionsCard'
|
|||||||
import ImplementationRoadmapCard from './components/ImplementationRoadmapCard';
|
import ImplementationRoadmapCard from './components/ImplementationRoadmapCard';
|
||||||
import RiskAssessmentCard from './components/RiskAssessmentCard';
|
import RiskAssessmentCard from './components/RiskAssessmentCard';
|
||||||
import ReviewProgressHeader from './components/ReviewProgressHeader';
|
import ReviewProgressHeader from './components/ReviewProgressHeader';
|
||||||
|
import StrategyErrorBoundary from './components/StrategyErrorBoundary';
|
||||||
import { StrategyData } from './types/strategy.types';
|
import { StrategyData } from './types/strategy.types';
|
||||||
import { useStrategyReviewStore } from '../../../../stores/strategyReviewStore';
|
import { useStrategyReviewStore } from '../../../../stores/strategyReviewStore';
|
||||||
|
import { hasValidData } from './utils/defensiveRendering';
|
||||||
|
import { createSafeStrategyData, validateStrategyData } from './utils/strategyDataValidator';
|
||||||
|
|
||||||
interface StrategyIntelligenceTabProps {
|
interface StrategyIntelligenceTabProps {
|
||||||
strategyData?: StrategyData | null;
|
strategyData?: StrategyData | null;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
error?: string | null;
|
error?: string | null;
|
||||||
|
strategyStatus?: 'active' | 'inactive' | 'pending' | 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
const StrategyIntelligenceTab: React.FC<StrategyIntelligenceTabProps> = ({
|
const StrategyIntelligenceTab: React.FC<StrategyIntelligenceTabProps> = ({
|
||||||
strategyData,
|
strategyData,
|
||||||
loading = false,
|
loading = false,
|
||||||
error = null
|
error = null,
|
||||||
|
strategyStatus = 'none'
|
||||||
}) => {
|
}) => {
|
||||||
// Get review process state from store
|
// Get review process state from store with selective subscription
|
||||||
const { reviewProcessStarted, startReviewProcess, components, initializeComponents } = useStrategyReviewStore();
|
const reviewProcessStarted = useStrategyReviewStore(state => state.reviewProcessStarted);
|
||||||
|
const startReviewProcess = useStrategyReviewStore(state => state.startReviewProcess);
|
||||||
|
const components = useStrategyReviewStore(state => state.components);
|
||||||
|
const initializeComponents = useStrategyReviewStore(state => state.initializeComponents);
|
||||||
|
const isAllReviewed = useStrategyReviewStore(state => state.isAllReviewed);
|
||||||
|
const isActivated = useStrategyReviewStore(state => state.isActivated);
|
||||||
|
const resetAllReviews = useStrategyReviewStore(state => state.resetAllReviews);
|
||||||
|
|
||||||
// Initialize components if they don't exist
|
// Initialize components if they don't exist
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,7 +70,63 @@ const StrategyIntelligenceTab: React.FC<StrategyIntelligenceTabProps> = ({
|
|||||||
}
|
}
|
||||||
}, [components.length, initializeComponents]);
|
}, [components.length, initializeComponents]);
|
||||||
|
|
||||||
|
// Create safe strategy data for rendering
|
||||||
|
const safeStrategyData = createSafeStrategyData(strategyData);
|
||||||
|
|
||||||
|
// Auto-start review process for pending strategies
|
||||||
|
useEffect(() => {
|
||||||
|
if (strategyStatus === 'pending' && !reviewProcessStarted && safeStrategyData) {
|
||||||
|
console.log('🔄 Auto-starting review process for pending strategy');
|
||||||
|
console.log('📋 Review Process: Starting review workflow for newly generated strategy');
|
||||||
|
console.log('🎯 Strategy Review Workflow: User should now see the review interface');
|
||||||
|
startReviewProcess();
|
||||||
|
}
|
||||||
|
}, [strategyStatus, safeStrategyData, startReviewProcess, reviewProcessStarted]);
|
||||||
|
|
||||||
|
// Log review process state changes (only when important changes occur, with debounce)
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (reviewProcessStarted || strategyStatus === 'pending') {
|
||||||
|
console.log('🔄 Review Process State:', {
|
||||||
|
strategyStatus,
|
||||||
|
reviewProcessStarted,
|
||||||
|
hasStrategyData: !!safeStrategyData,
|
||||||
|
componentsCount: components.length,
|
||||||
|
isAllReviewed: isAllReviewed(),
|
||||||
|
isActivated: isActivated()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 150); // 150ms debounce
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [strategyStatus, reviewProcessStarted, safeStrategyData, components.length, isAllReviewed, isActivated]);
|
||||||
|
|
||||||
|
// Log when review interface becomes visible
|
||||||
|
useEffect(() => {
|
||||||
|
if (reviewProcessStarted && safeStrategyData) {
|
||||||
|
console.log('🎯 RENDERING: Review interface is now visible to user');
|
||||||
|
console.log('📋 Review Interface: User can now review and confirm the strategy');
|
||||||
|
console.log('📊 Current Strategy Data:', {
|
||||||
|
hasStrategicInsights: !!safeStrategyData.strategic_insights,
|
||||||
|
hasCompetitiveAnalysis: !!safeStrategyData.competitive_analysis,
|
||||||
|
hasPerformancePredictions: !!safeStrategyData.performance_predictions,
|
||||||
|
hasImplementationRoadmap: !!safeStrategyData.implementation_roadmap,
|
||||||
|
hasRiskAssessment: !!safeStrategyData.risk_assessment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [reviewProcessStarted, safeStrategyData]);
|
||||||
|
|
||||||
|
// Log when StrategyHeader is rendered with new data
|
||||||
|
useEffect(() => {
|
||||||
|
if (safeStrategyData) {
|
||||||
|
console.log('🎯 RENDERING: StrategyHeader with status:', strategyStatus, 'and confirmed:', strategyStatus === 'active');
|
||||||
|
}
|
||||||
|
}, [strategyStatus, safeStrategyData]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleStartReviewProcess = () => {
|
const handleStartReviewProcess = () => {
|
||||||
|
console.log('🔄 Manual review process started by user');
|
||||||
startReviewProcess();
|
startReviewProcess();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,7 +146,23 @@ const StrategyIntelligenceTab: React.FC<StrategyIntelligenceTabProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!strategyData) {
|
// Validate strategy data before rendering
|
||||||
|
const hasValidStrategyData = (data: StrategyData | null): boolean => {
|
||||||
|
if (!data) return false;
|
||||||
|
|
||||||
|
// Check if the data has meaningful content
|
||||||
|
const hasStrategicInsights = hasValidData(data.strategic_insights);
|
||||||
|
const hasCompetitiveAnalysis = hasValidData(data.competitive_analysis);
|
||||||
|
const hasPerformancePredictions = hasValidData(data.performance_predictions);
|
||||||
|
const hasImplementationRoadmap = hasValidData(data.implementation_roadmap);
|
||||||
|
const hasRiskAssessment = hasValidData(data.risk_assessment);
|
||||||
|
|
||||||
|
// Return true if at least one component has valid data
|
||||||
|
return hasStrategicInsights || hasCompetitiveAnalysis || hasPerformancePredictions ||
|
||||||
|
hasImplementationRoadmap || hasRiskAssessment;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!safeStrategyData || !hasValidStrategyData(safeStrategyData)) {
|
||||||
return (
|
return (
|
||||||
<Box sx={{ textAlign: 'center', p: 4 }}>
|
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||||
<Typography variant="h6" color="text.secondary" gutterBottom>
|
<Typography variant="h6" color="text.secondary" gutterBottom>
|
||||||
@@ -88,6 +171,11 @@ const StrategyIntelligenceTab: React.FC<StrategyIntelligenceTabProps> = ({
|
|||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
Generate a comprehensive strategy first to view strategic intelligence.
|
Generate a comprehensive strategy first to view strategic intelligence.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{strategyData && (
|
||||||
|
<Typography variant="caption" sx={{ display: 'block', mt: 2, color: 'text.secondary' }}>
|
||||||
|
Strategy data exists but contains no valid components.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -96,13 +184,14 @@ const StrategyIntelligenceTab: React.FC<StrategyIntelligenceTabProps> = ({
|
|||||||
<Box sx={{ p: 3 }}>
|
<Box sx={{ p: 3 }}>
|
||||||
{/* Header Section */}
|
{/* Header Section */}
|
||||||
<StrategyHeader
|
<StrategyHeader
|
||||||
strategyData={strategyData}
|
strategyData={safeStrategyData}
|
||||||
strategyConfirmed={false}
|
strategyConfirmed={strategyStatus === 'active'}
|
||||||
|
strategyStatus={strategyStatus}
|
||||||
onStartReview={handleStartReviewProcess}
|
onStartReview={handleStartReviewProcess}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Review Progress Header - Only shown when review process is started */}
|
{/* Review Progress Header - Only shown when review process is started */}
|
||||||
{reviewProcessStarted && <ReviewProgressHeader strategyData={strategyData} />}
|
{reviewProcessStarted && <ReviewProgressHeader strategyData={safeStrategyData} />}
|
||||||
|
|
||||||
{/* Strategy Intelligence Cards */}
|
{/* Strategy Intelligence Cards */}
|
||||||
<Box
|
<Box
|
||||||
@@ -131,14 +220,24 @@ const StrategyIntelligenceTab: React.FC<StrategyIntelligenceTabProps> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StrategicInsightsCard strategyData={strategyData} />
|
<StrategyErrorBoundary>
|
||||||
<CompetitiveAnalysisCard strategyData={strategyData} />
|
<StrategicInsightsCard strategyData={safeStrategyData} />
|
||||||
<PerformancePredictionsCard strategyData={strategyData} />
|
</StrategyErrorBoundary>
|
||||||
<ImplementationRoadmapCard strategyData={strategyData} />
|
<StrategyErrorBoundary>
|
||||||
<RiskAssessmentCard strategyData={strategyData} />
|
<CompetitiveAnalysisCard strategyData={safeStrategyData} />
|
||||||
|
</StrategyErrorBoundary>
|
||||||
|
<StrategyErrorBoundary>
|
||||||
|
<PerformancePredictionsCard strategyData={safeStrategyData} />
|
||||||
|
</StrategyErrorBoundary>
|
||||||
|
<StrategyErrorBoundary>
|
||||||
|
<ImplementationRoadmapCard strategyData={safeStrategyData} />
|
||||||
|
</StrategyErrorBoundary>
|
||||||
|
<StrategyErrorBoundary>
|
||||||
|
<RiskAssessmentCard strategyData={safeStrategyData} />
|
||||||
|
</StrategyErrorBoundary>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StrategyIntelligenceTab;
|
export default memo(StrategyIntelligenceTab);
|
||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
getListItemStyles
|
getListItemStyles
|
||||||
} from '../styles';
|
} from '../styles';
|
||||||
import ProgressiveCard from './ProgressiveCard';
|
import ProgressiveCard from './ProgressiveCard';
|
||||||
|
import { safeRenderText, safeRenderArray, hasValidData, getFallbackValue } from '../utils/defensiveRendering';
|
||||||
|
|
||||||
interface CompetitiveAnalysisCardProps {
|
interface CompetitiveAnalysisCardProps {
|
||||||
strategyData: StrategyData | null;
|
strategyData: StrategyData | null;
|
||||||
@@ -68,6 +69,8 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
return description.split(' ').slice(0, 2).join(' ');
|
return description.split(' ').slice(0, 2).join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (!strategyData?.competitive_analysis) {
|
if (!strategyData?.competitive_analysis) {
|
||||||
return (
|
return (
|
||||||
<ProgressiveCard
|
<ProgressiveCard
|
||||||
@@ -290,7 +293,7 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={gap}
|
primary={safeRenderText(gap)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -324,7 +327,7 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={opportunity}
|
primary={safeRenderText(opportunity)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -358,7 +361,7 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={recommendation}
|
primary={safeRenderText(recommendation)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -399,14 +402,14 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={area}
|
primary={safeRenderText(area)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
sx: { lineHeight: 1.4, color: ANALYSIS_CARD_STYLES.colors.text.primary }
|
sx: { lineHeight: 1.4, color: ANALYSIS_CARD_STYLES.colors.text.primary }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
@@ -433,7 +436,7 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={advantage}
|
primary={safeRenderText(advantage)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -466,7 +469,7 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={advantage}
|
primary={safeRenderText(advantage)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -510,7 +513,7 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={strength}
|
primary={safeRenderText(strength)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -543,7 +546,7 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={weakness}
|
primary={safeRenderText(weakness)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -576,7 +579,7 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={opportunity}
|
primary={safeRenderText(opportunity)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -609,7 +612,7 @@ const CompetitiveAnalysisCard: React.FC<CompetitiveAnalysisCardProps> = ({ strat
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={threat}
|
primary={safeRenderText(threat)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
Close as CloseIcon
|
Close as CloseIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { safeRenderText, safeRenderArray, hasValidData, getFallbackValue } from '../utils/defensiveRendering';
|
||||||
|
|
||||||
// Import our advanced chart components
|
// Import our advanced chart components
|
||||||
import {
|
import {
|
||||||
@@ -472,7 +473,7 @@ const EnhancedPerformanceVisualization: React.FC<EnhancedPerformanceVisualizatio
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LightbulbIcon color="warning" />
|
<LightbulbIcon color="warning" />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={recommendation} />
|
<ListItemText primary={safeRenderText(recommendation)} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
getListItemStyles
|
getListItemStyles
|
||||||
} from '../styles';
|
} from '../styles';
|
||||||
import ProgressiveCard from './ProgressiveCard';
|
import ProgressiveCard from './ProgressiveCard';
|
||||||
|
import { safeRenderText, safeRenderArray, hasValidData, getFallbackValue } from '../utils/defensiveRendering';
|
||||||
|
|
||||||
interface ImplementationRoadmapCardProps {
|
interface ImplementationRoadmapCardProps {
|
||||||
strategyData: StrategyData | null;
|
strategyData: StrategyData | null;
|
||||||
@@ -50,6 +51,15 @@ const ImplementationRoadmapCard: React.FC<ImplementationRoadmapCardProps> = ({ s
|
|||||||
return 'Budget allocation not specified';
|
return 'Budget allocation not specified';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to safely render text content
|
||||||
|
const safeRenderText = (content: any): string => {
|
||||||
|
if (typeof content === 'string') return content;
|
||||||
|
if (typeof content === 'object' && content !== null) {
|
||||||
|
return JSON.stringify(content);
|
||||||
|
}
|
||||||
|
return 'Data not available';
|
||||||
|
};
|
||||||
|
|
||||||
if (!strategyData?.implementation_roadmap) {
|
if (!strategyData?.implementation_roadmap) {
|
||||||
return (
|
return (
|
||||||
<ProgressiveCard
|
<ProgressiveCard
|
||||||
@@ -212,7 +222,7 @@ const ImplementationRoadmapCard: React.FC<ImplementationRoadmapCardProps> = ({ s
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={milestone}
|
primary={safeRenderText(milestone)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -295,14 +305,14 @@ const ImplementationRoadmapCard: React.FC<ImplementationRoadmapCardProps> = ({ s
|
|||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={task}
|
primary={safeRenderText(task)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
sx: { lineHeight: 1.4, color: ANALYSIS_CARD_STYLES.colors.text.primary }
|
sx: { lineHeight: 1.4, color: ANALYSIS_CARD_STYLES.colors.text.primary }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
@@ -322,7 +332,7 @@ const ImplementationRoadmapCard: React.FC<ImplementationRoadmapCardProps> = ({ s
|
|||||||
<CheckCircleIcon sx={{ color: ANALYSIS_CARD_STYLES.colors.success, fontSize: 16 }} />
|
<CheckCircleIcon sx={{ color: ANALYSIS_CARD_STYLES.colors.success, fontSize: 16 }} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={milestone}
|
primary={safeRenderText(milestone)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -354,7 +364,7 @@ const ImplementationRoadmapCard: React.FC<ImplementationRoadmapCardProps> = ({ s
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={resource}
|
primary={safeRenderText(resource)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -387,7 +397,7 @@ const ImplementationRoadmapCard: React.FC<ImplementationRoadmapCardProps> = ({ s
|
|||||||
<CheckCircleIcon sx={{ color: ANALYSIS_CARD_STYLES.colors.success, fontSize: 16 }} />
|
<CheckCircleIcon sx={{ color: ANALYSIS_CARD_STYLES.colors.success, fontSize: 16 }} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={milestone}
|
primary={safeRenderText(milestone)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -421,7 +431,7 @@ const ImplementationRoadmapCard: React.FC<ImplementationRoadmapCardProps> = ({ s
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={requirement}
|
primary={safeRenderText(requirement)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -455,7 +465,7 @@ const ImplementationRoadmapCard: React.FC<ImplementationRoadmapCardProps> = ({ s
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={path}
|
primary={safeRenderText(path)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -531,7 +541,7 @@ const ImplementationRoadmapCard: React.FC<ImplementationRoadmapCardProps> = ({ s
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={metric}
|
primary={safeRenderText(metric)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
Refresh as RefreshIcon
|
Refresh as RefreshIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { safeRenderText, safeRenderArray, hasValidData, getFallbackValue } from '../utils/defensiveRendering';
|
||||||
|
|
||||||
interface MonitoringTask {
|
interface MonitoringTask {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -433,7 +434,7 @@ const MetricTransparencyCard: React.FC<MetricTransparencyCardProps> = ({
|
|||||||
<CheckCircleIcon sx={{ fontSize: 16, color: '#4caf50' }} />
|
<CheckCircleIcon sx={{ fontSize: 16, color: '#4caf50' }} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={rec}
|
primary={safeRenderText(rec)}
|
||||||
sx={{
|
sx={{
|
||||||
'& .MuiListItemText-primary': {
|
'& .MuiListItemText-primary': {
|
||||||
fontSize: '0.85rem',
|
fontSize: '0.85rem',
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
getListItemStyles
|
getListItemStyles
|
||||||
} from '../styles';
|
} from '../styles';
|
||||||
import ProgressiveCard from './ProgressiveCard';
|
import ProgressiveCard from './ProgressiveCard';
|
||||||
|
import { safeRenderText, safeRenderArray, hasValidData, getFallbackValue } from '../utils/defensiveRendering';
|
||||||
|
|
||||||
interface PerformancePredictionsCardProps {
|
interface PerformancePredictionsCardProps {
|
||||||
strategyData: StrategyData | null;
|
strategyData: StrategyData | null;
|
||||||
@@ -34,6 +35,15 @@ const PerformancePredictionsCard: React.FC<PerformancePredictionsCardProps> = ({
|
|||||||
const sectionStyles = getSectionStyles();
|
const sectionStyles = getSectionStyles();
|
||||||
const listItemStyles = getListItemStyles();
|
const listItemStyles = getListItemStyles();
|
||||||
|
|
||||||
|
// Helper function to safely render text content
|
||||||
|
const safeRenderText = (content: any): string => {
|
||||||
|
if (typeof content === 'string') return content;
|
||||||
|
if (typeof content === 'object' && content !== null) {
|
||||||
|
return JSON.stringify(content);
|
||||||
|
}
|
||||||
|
return 'Data not available';
|
||||||
|
};
|
||||||
|
|
||||||
if (!strategyData?.performance_predictions) {
|
if (!strategyData?.performance_predictions) {
|
||||||
return (
|
return (
|
||||||
<ProgressiveCard
|
<ProgressiveCard
|
||||||
|
|||||||
@@ -68,13 +68,7 @@ const ProgressiveCard: React.FC<ProgressiveCardProps> = ({
|
|||||||
const componentReviewedAt = component?.reviewedAt;
|
const componentReviewedAt = component?.reviewedAt;
|
||||||
|
|
||||||
// Debug logging for component status
|
// Debug logging for component status
|
||||||
if (componentId) {
|
// Removed verbose logging for cleaner console
|
||||||
console.log(`🔧 ProgressiveCard [${componentId}]:`, {
|
|
||||||
componentStatus,
|
|
||||||
componentReviewedAt,
|
|
||||||
allComponents: components.map(c => ({ id: c.id, status: c.status }))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle hover interactions
|
// Handle hover interactions
|
||||||
const handleMouseEnter = () => {
|
const handleMouseEnter = () => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { ANALYSIS_CARD_STYLES } from '../styles';
|
import { ANALYSIS_CARD_STYLES } from '../styles';
|
||||||
|
import { safeRenderText, safeRenderArray, hasValidData, getFallbackValue } from '../utils/defensiveRendering';
|
||||||
|
|
||||||
interface ReviewConfirmationDialogProps {
|
interface ReviewConfirmationDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -204,7 +205,7 @@ const ReviewConfirmationDialog: React.FC<ReviewConfirmationDialogProps> = ({
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={item}
|
primary={safeRenderText(item)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body1',
|
variant: 'body1',
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, memo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -32,16 +32,15 @@ interface ReviewProgressHeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ReviewProgressHeader: React.FC<ReviewProgressHeaderProps> = ({ strategyData }) => {
|
const ReviewProgressHeader: React.FC<ReviewProgressHeaderProps> = ({ strategyData }) => {
|
||||||
const {
|
// Use selective store subscriptions to prevent unnecessary re-renders
|
||||||
components,
|
const components = useStrategyReviewStore(state => state.components);
|
||||||
reviewProgress,
|
const reviewProgress = useStrategyReviewStore(state => state.reviewProgress);
|
||||||
isAllReviewed,
|
const isAllReviewed = useStrategyReviewStore(state => state.isAllReviewed);
|
||||||
isActivated,
|
const isActivated = useStrategyReviewStore(state => state.isActivated);
|
||||||
resetAllReviews,
|
const resetAllReviews = useStrategyReviewStore(state => state.resetAllReviews);
|
||||||
getUnreviewedComponents,
|
const getUnreviewedComponents = useStrategyReviewStore(state => state.getUnreviewedComponents);
|
||||||
getReviewedComponents,
|
const getReviewedComponents = useStrategyReviewStore(state => state.getReviewedComponents);
|
||||||
activateStrategy
|
const activateStrategy = useStrategyReviewStore(state => state.activateStrategy);
|
||||||
} = useStrategyReviewStore();
|
|
||||||
|
|
||||||
// Initialize navigation orchestrator
|
// Initialize navigation orchestrator
|
||||||
const navigationOrchestrator = useNavigationOrchestrator();
|
const navigationOrchestrator = useNavigationOrchestrator();
|
||||||
@@ -56,17 +55,7 @@ const ReviewProgressHeader: React.FC<ReviewProgressHeaderProps> = ({ strategyDat
|
|||||||
const reviewedCount = getReviewedComponents().length;
|
const reviewedCount = getReviewedComponents().length;
|
||||||
const totalCount = components.length;
|
const totalCount = components.length;
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging - removed for cleaner console
|
||||||
console.log('🔍 ReviewProgressHeader Debug:', {
|
|
||||||
components,
|
|
||||||
reviewProgress,
|
|
||||||
unreviewedCount,
|
|
||||||
reviewedCount,
|
|
||||||
totalCount,
|
|
||||||
isAllReviewed: isAllReviewed(),
|
|
||||||
isActivated: isActivated(),
|
|
||||||
strategyData
|
|
||||||
});
|
|
||||||
|
|
||||||
const getProgressColor = () => {
|
const getProgressColor = () => {
|
||||||
if (isActivated()) return ANALYSIS_CARD_STYLES.colors.success;
|
if (isActivated()) return ANALYSIS_CARD_STYLES.colors.success;
|
||||||
@@ -98,7 +87,6 @@ const ReviewProgressHeader: React.FC<ReviewProgressHeaderProps> = ({ strategyDat
|
|||||||
|
|
||||||
const handleConfirmStrategy = async () => {
|
const handleConfirmStrategy = async () => {
|
||||||
// This will be called by the enhanced button when activation is confirmed
|
// This will be called by the enhanced button when activation is confirmed
|
||||||
console.log('🎯 Strategy activation confirmed');
|
|
||||||
|
|
||||||
// Activate the strategy in the store
|
// Activate the strategy in the store
|
||||||
activateStrategy();
|
activateStrategy();
|
||||||
@@ -107,7 +95,6 @@ const ReviewProgressHeader: React.FC<ReviewProgressHeaderProps> = ({ strategyDat
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGenerateContentCalendar = () => {
|
const handleGenerateContentCalendar = () => {
|
||||||
console.log('🎯 Generate content calendar clicked');
|
|
||||||
|
|
||||||
// Prepare strategy context for navigation
|
// Prepare strategy context for navigation
|
||||||
const strategyContext = {
|
const strategyContext = {
|
||||||
@@ -430,4 +417,4 @@ const ReviewProgressHeader: React.FC<ReviewProgressHeaderProps> = ({ strategyDat
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ReviewProgressHeader;
|
export default memo(ReviewProgressHeader);
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Chip,
|
||||||
|
Tooltip,
|
||||||
|
CircularProgress,
|
||||||
|
Badge,
|
||||||
|
Button
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
CheckCircle as CheckCircleIcon,
|
||||||
|
Schedule as ScheduleIcon,
|
||||||
|
AutoAwesome as AutoAwesomeIcon
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { StrategyData } from '../types/strategy.types';
|
||||||
|
import { useStrategyReviewStore, StrategyComponent } from '../../../../../stores/strategyReviewStore';
|
||||||
|
import { ANALYSIS_CARD_STYLES } from '../styles';
|
||||||
|
import EnhancedStrategyActivationButton from './EnhancedStrategyActivationButton';
|
||||||
|
import { useNavigationOrchestrator } from '../../../../../services/navigationOrchestrator';
|
||||||
|
|
||||||
|
interface ReviewProgressSectionProps {
|
||||||
|
strategyData: StrategyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReviewProgressSection: React.FC<ReviewProgressSectionProps> = ({ strategyData }) => {
|
||||||
|
const {
|
||||||
|
components,
|
||||||
|
reviewProgress,
|
||||||
|
isAllReviewed,
|
||||||
|
isActivated,
|
||||||
|
resetAllReviews,
|
||||||
|
getUnreviewedComponents,
|
||||||
|
getReviewedComponents,
|
||||||
|
activateStrategy
|
||||||
|
} = useStrategyReviewStore();
|
||||||
|
|
||||||
|
// Initialize navigation orchestrator
|
||||||
|
const navigationOrchestrator = useNavigationOrchestrator();
|
||||||
|
|
||||||
|
// Extract domain name from strategy data
|
||||||
|
const getDomainName = () => {
|
||||||
|
// Since StrategyMetadata doesn't have domain, we'll use a fallback
|
||||||
|
return "alwrity.com"; // fallback
|
||||||
|
};
|
||||||
|
|
||||||
|
const unreviewedCount = getUnreviewedComponents().length;
|
||||||
|
const reviewedCount = getReviewedComponents().length;
|
||||||
|
const totalCount = components.length;
|
||||||
|
|
||||||
|
// Debug logging
|
||||||
|
console.log('🔍 ReviewProgressSection Debug:', {
|
||||||
|
components,
|
||||||
|
reviewProgress,
|
||||||
|
unreviewedCount,
|
||||||
|
reviewedCount,
|
||||||
|
totalCount,
|
||||||
|
isAllReviewed: isAllReviewed(),
|
||||||
|
isActivated: isActivated(),
|
||||||
|
strategyData
|
||||||
|
});
|
||||||
|
|
||||||
|
const getProgressColor = () => {
|
||||||
|
if (isActivated()) return ANALYSIS_CARD_STYLES.colors.success;
|
||||||
|
if (reviewProgress === 100) return ANALYSIS_CARD_STYLES.colors.success;
|
||||||
|
if (reviewProgress >= 60) return ANALYSIS_CARD_STYLES.colors.primary;
|
||||||
|
if (reviewProgress >= 30) return ANALYSIS_CARD_STYLES.colors.warning;
|
||||||
|
return ANALYSIS_CARD_STYLES.colors.error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProgressText = () => {
|
||||||
|
if (isActivated()) return 'Strategy Active & Monitored!';
|
||||||
|
if (reviewProgress === 100) return 'All components reviewed!';
|
||||||
|
if (reviewProgress >= 60) return 'Great progress!';
|
||||||
|
if (reviewProgress >= 30) return 'Making good progress';
|
||||||
|
return 'Getting started';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare strategy data for the enhanced button
|
||||||
|
const buttonStrategyData = strategyData ? {
|
||||||
|
id: strategyData.strategy_metadata?.user_id || strategyData.metadata?.user_id || 1,
|
||||||
|
business_name: strategyData.strategy_metadata?.strategy_name || strategyData.metadata?.strategy_name || "ALwrity",
|
||||||
|
domain: getDomainName(),
|
||||||
|
// Add other strategy data as needed
|
||||||
|
} : {
|
||||||
|
id: 1,
|
||||||
|
business_name: "ALwrity",
|
||||||
|
domain: getDomainName(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmStrategy = async () => {
|
||||||
|
// This will be called by the enhanced button when activation is confirmed
|
||||||
|
console.log('🎯 Strategy activation confirmed');
|
||||||
|
|
||||||
|
// Activate the strategy in the store
|
||||||
|
activateStrategy();
|
||||||
|
|
||||||
|
// You can add additional logic here if needed
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateContentCalendar = () => {
|
||||||
|
console.log('🎯 Generate content calendar clicked');
|
||||||
|
|
||||||
|
// Prepare strategy context for navigation
|
||||||
|
const strategyContext = {
|
||||||
|
strategyId: (strategyData?.strategy_metadata?.user_id || strategyData?.metadata?.user_id || '1').toString(),
|
||||||
|
strategyData: strategyData,
|
||||||
|
activationStatus: 'active' as const,
|
||||||
|
activationTimestamp: new Date().toISOString(),
|
||||||
|
userPreferences: {},
|
||||||
|
strategicIntelligence: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigate to calendar wizard using navigation orchestrator
|
||||||
|
navigationOrchestrator.navigateToCalendarWizard(
|
||||||
|
strategyContext.strategyId,
|
||||||
|
strategyContext
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
|
>
|
||||||
|
{/* Header Section */}
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" sx={{
|
||||||
|
fontWeight: 700,
|
||||||
|
background: 'linear-gradient(45deg, #667eea, #764ba2)',
|
||||||
|
backgroundClip: 'text',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent',
|
||||||
|
mb: 0.5
|
||||||
|
}}>
|
||||||
|
Strategy Review Progress
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.8)', fontWeight: 500 }}>
|
||||||
|
Review all strategy components to activate your content strategy
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Progress Circle */}
|
||||||
|
<Box sx={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
<Box sx={{ position: 'relative' }}>
|
||||||
|
<CircularProgress
|
||||||
|
variant="determinate"
|
||||||
|
value={reviewProgress}
|
||||||
|
size={60}
|
||||||
|
thickness={4}
|
||||||
|
sx={{
|
||||||
|
color: getProgressColor(),
|
||||||
|
'& .MuiCircularProgress-circle': {
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
filter: 'drop-shadow(0 0 8px rgba(102, 126, 234, 0.5))'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" sx={{
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'white',
|
||||||
|
lineHeight: 1
|
||||||
|
}}>
|
||||||
|
{reviewProgress}%
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Progress Text */}
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
color: getProgressColor(),
|
||||||
|
fontSize: '0.8rem'
|
||||||
|
}}>
|
||||||
|
{getProgressText()}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
fontSize: '0.7rem'
|
||||||
|
}}>
|
||||||
|
{reviewedCount} of {totalCount} reviewed
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Component Status */}
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Typography variant="body2" sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
mb: 1,
|
||||||
|
color: 'rgba(255, 255, 255, 0.9)'
|
||||||
|
}}>
|
||||||
|
Component Status:
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||||
|
{components.map((component: StrategyComponent) => (
|
||||||
|
<Tooltip
|
||||||
|
key={component.id}
|
||||||
|
title={`${component.title}: ${component.status === 'reviewed' ? 'Reviewed' : 'Pending Review'}`}
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
badgeContent={
|
||||||
|
component.status === 'reviewed' ? (
|
||||||
|
<CheckCircleIcon sx={{ fontSize: 10, color: 'white' }} />
|
||||||
|
) : (
|
||||||
|
<ScheduleIcon sx={{ fontSize: 10, color: 'white' }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
color={component.status === 'reviewed' ? 'success' : 'warning'}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
label={component.title}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
background: component.status === 'reviewed'
|
||||||
|
? 'rgba(76, 175, 80, 0.3)'
|
||||||
|
: 'rgba(255, 152, 0, 0.3)',
|
||||||
|
color: component.status === 'reviewed' ? '#4caf50' : '#ff9800',
|
||||||
|
border: `1px solid ${component.status === 'reviewed' ? 'rgba(76, 175, 80, 0.5)' : 'rgba(255, 152, 0, 0.5)'}`,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.65rem',
|
||||||
|
height: 22,
|
||||||
|
'&:hover': {
|
||||||
|
background: component.status === 'reviewed'
|
||||||
|
? 'rgba(76, 175, 80, 0.4)'
|
||||||
|
: 'rgba(255, 152, 0, 0.4)',
|
||||||
|
transform: 'translateY(-1px)'
|
||||||
|
},
|
||||||
|
transition: 'all 0.2s ease'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Completion Status */}
|
||||||
|
{isAllReviewed() && (
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||||
|
<Chip
|
||||||
|
icon={<CheckCircleIcon />}
|
||||||
|
label={isActivated() ? "Strategy Active & Monitored" : "Ready for Calendar Creation"}
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
background: ANALYSIS_CARD_STYLES.colors.success,
|
||||||
|
color: 'white',
|
||||||
|
fontWeight: 500,
|
||||||
|
animation: 'pulse 2s infinite',
|
||||||
|
fontSize: '0.65rem',
|
||||||
|
height: 22,
|
||||||
|
'@keyframes pulse': {
|
||||||
|
'0%, 100%': { opacity: 1 },
|
||||||
|
'50%': { opacity: 0.7 }
|
||||||
|
},
|
||||||
|
'& .MuiChip-icon': {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 14
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Completion Message */}
|
||||||
|
{isAllReviewed() && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.3, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<Box sx={{
|
||||||
|
mt: 1.5,
|
||||||
|
p: 1.5,
|
||||||
|
borderRadius: 1,
|
||||||
|
background: isActivated() ? 'rgba(76, 175, 80, 0.15)' : 'rgba(76, 175, 80, 0.1)',
|
||||||
|
border: '1px solid rgba(76, 175, 80, 0.2)',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
<Typography variant="body2" sx={{
|
||||||
|
color: ANALYSIS_CARD_STYLES.colors.success,
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.8rem'
|
||||||
|
}}>
|
||||||
|
{isActivated()
|
||||||
|
? '🎉 Your content strategy is now active and being monitored! AI-powered insights and performance tracking are now live.'
|
||||||
|
: '🎉 All strategy components have been reviewed! You can now proceed to create your content calendar.'
|
||||||
|
}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Enhanced Strategy Activation Button - Only shown when all components are reviewed and not yet activated */}
|
||||||
|
{isAllReviewed() && !isActivated() && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<Box sx={{ mt: 3, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<EnhancedStrategyActivationButton
|
||||||
|
strategyData={buttonStrategyData}
|
||||||
|
strategyConfirmed={false}
|
||||||
|
onConfirmStrategy={handleConfirmStrategy}
|
||||||
|
onGenerateContentCalendar={handleGenerateContentCalendar}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Strategy Activated Success Message */}
|
||||||
|
{isActivated() && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.3 }}
|
||||||
|
>
|
||||||
|
<Box sx={{
|
||||||
|
mt: 3,
|
||||||
|
p: 3,
|
||||||
|
borderRadius: 2,
|
||||||
|
background: 'linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(76, 175, 80, 0.05) 100%)',
|
||||||
|
border: '2px solid rgba(76, 175, 80, 0.3)',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}>
|
||||||
|
<Typography variant="h6" sx={{
|
||||||
|
color: ANALYSIS_CARD_STYLES.colors.success,
|
||||||
|
fontWeight: 700,
|
||||||
|
mb: 1
|
||||||
|
}}>
|
||||||
|
🚀 Strategy Successfully Activated!
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{
|
||||||
|
color: 'text.secondary',
|
||||||
|
mb: 2
|
||||||
|
}}>
|
||||||
|
Your content strategy is now live and being monitored with AI-powered analytics.
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
onClick={handleGenerateContentCalendar}
|
||||||
|
startIcon={<AutoAwesomeIcon />}
|
||||||
|
sx={{
|
||||||
|
background: 'linear-gradient(135deg, #4caf50 0%, #45a049 100%)',
|
||||||
|
borderRadius: 3,
|
||||||
|
px: 4,
|
||||||
|
py: 1.5,
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: 'none',
|
||||||
|
boxShadow: '0 8px 32px rgba(76, 175, 80, 0.3)',
|
||||||
|
'&:hover': {
|
||||||
|
background: 'linear-gradient(135deg, #45a049 0%, #3d8b40 100%)',
|
||||||
|
boxShadow: '0 12px 40px rgba(76, 175, 80, 0.4)',
|
||||||
|
transform: 'translateY(-2px)'
|
||||||
|
},
|
||||||
|
transition: 'all 0.3s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Generate Content Calendar
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReviewProgressSection;
|
||||||
@@ -37,7 +37,7 @@ const ReviewStatusIndicator: React.FC<ReviewStatusIndicatorProps> = ({
|
|||||||
isReviewing = false
|
isReviewing = false
|
||||||
}) => {
|
}) => {
|
||||||
// Debug logging for status
|
// Debug logging for status
|
||||||
console.log('🔧 ReviewStatusIndicator received status:', status);
|
// Removed verbose logging for cleaner console
|
||||||
const getStatusConfig = () => {
|
const getStatusConfig = () => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'activated':
|
case 'activated':
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
getListItemStyles
|
getListItemStyles
|
||||||
} from '../styles';
|
} from '../styles';
|
||||||
import ProgressiveCard from './ProgressiveCard';
|
import ProgressiveCard from './ProgressiveCard';
|
||||||
|
import { safeRenderText, safeRenderArray, hasValidData, getFallbackValue } from '../utils/defensiveRendering';
|
||||||
|
|
||||||
interface RiskAssessmentCardProps {
|
interface RiskAssessmentCardProps {
|
||||||
strategyData: StrategyData | null;
|
strategyData: StrategyData | null;
|
||||||
@@ -51,6 +52,15 @@ const RiskAssessmentCard: React.FC<RiskAssessmentCardProps> = ({ strategyData })
|
|||||||
return ANALYSIS_CARD_STYLES.colors.info;
|
return ANALYSIS_CARD_STYLES.colors.info;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to safely render text content
|
||||||
|
const safeRenderText = (content: any): string => {
|
||||||
|
if (typeof content === 'string') return content;
|
||||||
|
if (typeof content === 'object' && content !== null) {
|
||||||
|
return JSON.stringify(content);
|
||||||
|
}
|
||||||
|
return 'Data not available';
|
||||||
|
};
|
||||||
|
|
||||||
if (!strategyData?.risk_assessment) {
|
if (!strategyData?.risk_assessment) {
|
||||||
return (
|
return (
|
||||||
<ProgressiveCard
|
<ProgressiveCard
|
||||||
@@ -283,14 +293,14 @@ const RiskAssessmentCard: React.FC<RiskAssessmentCardProps> = ({ strategyData })
|
|||||||
<ListItemIcon sx={listItemStyles.listItemIcon}>
|
<ListItemIcon sx={listItemStyles.listItemIcon}>
|
||||||
<CheckCircleIcon sx={{ color: ANALYSIS_CARD_STYLES.colors.success, fontSize: 16 }} />
|
<CheckCircleIcon sx={{ color: ANALYSIS_CARD_STYLES.colors.success, fontSize: 16 }} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={typeof strategy === 'string' ? strategy : strategy.mitigation || strategy.risk || 'Mitigation strategy'}
|
primary={safeRenderText(typeof strategy === 'string' ? strategy : strategy.mitigation || strategy.risk || 'Mitigation strategy')}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
sx: { lineHeight: 1.4, color: ANALYSIS_CARD_STYLES.colors.text.primary }
|
sx: { lineHeight: 1.4, color: ANALYSIS_CARD_STYLES.colors.text.primary }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
@@ -345,14 +355,14 @@ const RiskAssessmentCard: React.FC<RiskAssessmentCardProps> = ({ strategyData })
|
|||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={typeof risk === 'string' ? risk : risk.risk || 'Risk'}
|
primary={safeRenderText(typeof risk === 'string' ? risk : risk.risk || 'Risk')}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
sx: { lineHeight: 1.4, color: ANALYSIS_CARD_STYLES.colors.text.primary }
|
sx: { lineHeight: 1.4, color: ANALYSIS_CARD_STYLES.colors.text.primary }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
getListItemStyles
|
getListItemStyles
|
||||||
} from '../styles';
|
} from '../styles';
|
||||||
import ProgressiveCard from './ProgressiveCard';
|
import ProgressiveCard from './ProgressiveCard';
|
||||||
|
import { safeRenderText, safeRenderArray, hasValidData, getFallbackValue } from '../utils/defensiveRendering';
|
||||||
|
|
||||||
interface StrategicInsightsCardProps {
|
interface StrategicInsightsCardProps {
|
||||||
strategyData: StrategyData | null;
|
strategyData: StrategyData | null;
|
||||||
@@ -80,6 +81,8 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Summary content - always visible
|
// Summary content - always visible
|
||||||
const summaryContent = (
|
const summaryContent = (
|
||||||
<Box>
|
<Box>
|
||||||
@@ -236,7 +239,7 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={strength}
|
primary={safeRenderText(strength)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -269,7 +272,7 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={opportunity}
|
primary={safeRenderText(opportunity)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -327,7 +330,7 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={driver}
|
primary={safeRenderText(driver)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -378,7 +381,7 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={strength}
|
primary={safeRenderText(strength)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -411,7 +414,7 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={opportunity}
|
primary={safeRenderText(opportunity)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -476,8 +479,8 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
opacity: 0.7
|
opacity: 0.7
|
||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={insight.insight}
|
primary={safeRenderText(insight.insight)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
@@ -487,7 +490,7 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
<Box sx={{ mt: 0.5 }}>
|
<Box sx={{ mt: 0.5 }}>
|
||||||
<Box sx={{ display: 'flex', gap: 1, mb: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 1, mb: 0.5 }}>
|
||||||
<Chip
|
<Chip
|
||||||
label={`P: ${insight.priority || 'Medium'}`}
|
label={`P: ${safeRenderText(insight.priority) || 'Medium'}`}
|
||||||
size="small"
|
size="small"
|
||||||
sx={getEnhancedChipStyles(
|
sx={getEnhancedChipStyles(
|
||||||
insight.priority === 'High' ? ANALYSIS_CARD_STYLES.colors.error :
|
insight.priority === 'High' ? ANALYSIS_CARD_STYLES.colors.error :
|
||||||
@@ -496,7 +499,7 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
).chip}
|
).chip}
|
||||||
/>
|
/>
|
||||||
<Chip
|
<Chip
|
||||||
label={`I: ${insight.estimated_impact || 'Medium'}`}
|
label={`I: ${safeRenderText(insight.estimated_impact) || 'Medium'}`}
|
||||||
size="small"
|
size="small"
|
||||||
sx={getEnhancedChipStyles(
|
sx={getEnhancedChipStyles(
|
||||||
insight.estimated_impact === 'High' ? ANALYSIS_CARD_STYLES.colors.error :
|
insight.estimated_impact === 'High' ? ANALYSIS_CARD_STYLES.colors.error :
|
||||||
@@ -505,7 +508,7 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
).chip}
|
).chip}
|
||||||
/>
|
/>
|
||||||
<Chip
|
<Chip
|
||||||
label={`C: ${insight.confidence_level || 'Medium'}`}
|
label={`C: ${safeRenderText(insight.confidence_level) || 'Medium'}`}
|
||||||
size="small"
|
size="small"
|
||||||
sx={getEnhancedChipStyles(
|
sx={getEnhancedChipStyles(
|
||||||
insight.confidence_level === 'High' ? ANALYSIS_CARD_STYLES.colors.success :
|
insight.confidence_level === 'High' ? ANALYSIS_CARD_STYLES.colors.success :
|
||||||
@@ -514,19 +517,19 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
).chip}
|
).chip}
|
||||||
/>
|
/>
|
||||||
<Chip
|
<Chip
|
||||||
label={`T: ${insight.implementation_time || '3 months'}`}
|
label={`T: ${safeRenderText(insight.implementation_time) || '3 months'}`}
|
||||||
size="small"
|
size="small"
|
||||||
sx={getEnhancedChipStyles(ANALYSIS_CARD_STYLES.colors.info).chip}
|
sx={getEnhancedChipStyles(ANALYSIS_CARD_STYLES.colors.info).chip}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{insight.reasoning && (
|
{hasValidData(insight.reasoning) && (
|
||||||
<Typography variant="caption" sx={{
|
<Typography variant="caption" sx={{
|
||||||
display: 'block',
|
display: 'block',
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
color: ANALYSIS_CARD_STYLES.colors.text.secondary,
|
color: ANALYSIS_CARD_STYLES.colors.text.secondary,
|
||||||
fontStyle: 'italic'
|
fontStyle: 'italic'
|
||||||
}}>
|
}}>
|
||||||
<strong>Reasoning:</strong> {insight.reasoning}
|
<strong>Reasoning:</strong> {safeRenderText(insight.reasoning)}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -562,7 +565,7 @@ const StrategicInsightsCard: React.FC<StrategicInsightsCardProps> = ({ strategyD
|
|||||||
}} />
|
}} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={opportunity}
|
primary={safeRenderText(opportunity)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
variant: 'body2',
|
variant: 'body2',
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
|
import { Box, Typography, Alert, Button } from '@mui/material';
|
||||||
|
import { Refresh as RefreshIcon } from '@mui/icons-material';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
fallback?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StrategyErrorBoundary extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error: Error): State {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
console.error('Strategy component error:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRetry = () => {
|
||||||
|
this.setState({ hasError: false, error: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
if (this.props.fallback) {
|
||||||
|
return this.props.fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 2, textAlign: 'center' }}>
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
<Typography variant="body2" gutterBottom>
|
||||||
|
Error rendering strategy component
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ display: 'block', mt: 1 }}>
|
||||||
|
{this.state.error?.message || 'Unknown error occurred'}
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<RefreshIcon />}
|
||||||
|
onClick={this.handleRetry}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StrategyErrorBoundary;
|
||||||
@@ -42,10 +42,12 @@ import { getStrategyName, getStrategyGenerationDate } from '../utils/strategyTra
|
|||||||
interface StrategyHeaderProps {
|
interface StrategyHeaderProps {
|
||||||
strategyData: StrategyData | null;
|
strategyData: StrategyData | null;
|
||||||
strategyConfirmed: boolean;
|
strategyConfirmed: boolean;
|
||||||
|
strategyStatus?: 'active' | 'inactive' | 'pending' | 'none';
|
||||||
onStartReview?: () => void;
|
onStartReview?: () => void;
|
||||||
|
disableCardWrapper?: boolean; // New prop to control Card wrapper rendering
|
||||||
}
|
}
|
||||||
|
|
||||||
const StrategyHeader: React.FC<StrategyHeaderProps> = ({ strategyData, strategyConfirmed, onStartReview }) => {
|
const StrategyHeader: React.FC<StrategyHeaderProps> = ({ strategyData, strategyConfirmed, strategyStatus = 'none', onStartReview, disableCardWrapper = false }) => {
|
||||||
const [showNextStepText, setShowNextStepText] = useState(false);
|
const [showNextStepText, setShowNextStepText] = useState(false);
|
||||||
|
|
||||||
if (!strategyData) return null;
|
if (!strategyData) return null;
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// Utility functions for defensive rendering in strategy components
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely renders text content, handling objects, arrays, and other non-string types
|
||||||
|
*/
|
||||||
|
export const safeRenderText = (content: any): string => {
|
||||||
|
if (typeof content === 'string') return content;
|
||||||
|
if (typeof content === 'number') return content.toString();
|
||||||
|
if (typeof content === 'boolean') return content.toString();
|
||||||
|
if (Array.isArray(content)) return content.join(', ');
|
||||||
|
if (typeof content === 'object' && content !== null) {
|
||||||
|
// If it's an empty object, return a fallback message
|
||||||
|
if (Object.keys(content).length === 0) {
|
||||||
|
return 'Data not available';
|
||||||
|
}
|
||||||
|
// If it has a meaningful structure, try to extract useful information
|
||||||
|
if (content.title) return content.title;
|
||||||
|
if (content.name) return content.name;
|
||||||
|
if (content.description) return content.description;
|
||||||
|
if (content.text) return content.text;
|
||||||
|
// As a last resort, stringify the object
|
||||||
|
return JSON.stringify(content);
|
||||||
|
}
|
||||||
|
return 'Data not available';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely renders array content, ensuring it's always an array
|
||||||
|
*/
|
||||||
|
export const safeRenderArray = (content: any): any[] => {
|
||||||
|
if (Array.isArray(content)) return content;
|
||||||
|
if (typeof content === 'string') return [content];
|
||||||
|
if (typeof content === 'object' && content !== null) {
|
||||||
|
// If it's an empty object, return empty array
|
||||||
|
if (Object.keys(content).length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// Try to extract array-like data
|
||||||
|
if (content.items && Array.isArray(content.items)) return content.items;
|
||||||
|
if (content.data && Array.isArray(content.data)) return content.data;
|
||||||
|
if (content.list && Array.isArray(content.list)) return content.list;
|
||||||
|
// As a last resort, return the object as a single item
|
||||||
|
return [content];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely renders object content, ensuring it's always a valid object
|
||||||
|
*/
|
||||||
|
export const safeRenderObject = (content: any): Record<string, any> => {
|
||||||
|
if (typeof content === 'object' && content !== null) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates if a strategy data field contains meaningful data
|
||||||
|
*/
|
||||||
|
export const hasValidData = (content: any): boolean => {
|
||||||
|
if (content === null || content === undefined) return false;
|
||||||
|
if (typeof content === 'string') return content.trim().length > 0;
|
||||||
|
if (Array.isArray(content)) return content.length > 0;
|
||||||
|
if (typeof content === 'object') {
|
||||||
|
// Check if it's an empty object
|
||||||
|
if (Object.keys(content).length === 0) return false;
|
||||||
|
// Check if it has meaningful properties
|
||||||
|
return Object.values(content).some(value => hasValidData(value));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a fallback value for empty or invalid data
|
||||||
|
*/
|
||||||
|
export const getFallbackValue = (fieldName: string): string => {
|
||||||
|
const fallbacks: Record<string, string> = {
|
||||||
|
insight: 'Insight data not available',
|
||||||
|
recommendation: 'Recommendation data not available',
|
||||||
|
prediction: 'Prediction data not available',
|
||||||
|
analysis: 'Analysis data not available',
|
||||||
|
strategy: 'Strategy data not available',
|
||||||
|
risk: 'Risk data not available',
|
||||||
|
opportunity: 'Opportunity data not available',
|
||||||
|
strength: 'Strength data not available',
|
||||||
|
weakness: 'Weakness data not available',
|
||||||
|
threat: 'Threat data not available',
|
||||||
|
milestone: 'Milestone data not available',
|
||||||
|
task: 'Task data not available',
|
||||||
|
resource: 'Resource data not available',
|
||||||
|
requirement: 'Requirement data not available',
|
||||||
|
metric: 'Metric data not available',
|
||||||
|
gap: 'Gap data not available',
|
||||||
|
advantage: 'Advantage data not available',
|
||||||
|
area: 'Area data not available',
|
||||||
|
path: 'Path data not available'
|
||||||
|
};
|
||||||
|
|
||||||
|
return fallbacks[fieldName.toLowerCase()] || 'Data not available';
|
||||||
|
};
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { StrategyData } from '../types/strategy.types';
|
||||||
|
import { hasValidData } from './defensiveRendering';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates strategy data to ensure it's safe for rendering
|
||||||
|
*/
|
||||||
|
export const validateStrategyData = (data: StrategyData | null | undefined): {
|
||||||
|
isValid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
warnings: string[];
|
||||||
|
} => {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
errors: ['Strategy data is null or undefined'],
|
||||||
|
warnings: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for empty objects that could cause rendering issues
|
||||||
|
const checkForEmptyObjects = (obj: any, path: string): void => {
|
||||||
|
if (typeof obj === 'object' && obj !== null) {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
obj.forEach((item, index) => {
|
||||||
|
checkForEmptyObjects(item, `${path}[${index}]`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const keys = Object.keys(obj);
|
||||||
|
if (keys.length === 0) {
|
||||||
|
warnings.push(`Empty object found at ${path}`);
|
||||||
|
} else {
|
||||||
|
keys.forEach(key => {
|
||||||
|
checkForEmptyObjects(obj[key], `${path}.${key}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check each major component
|
||||||
|
const components = [
|
||||||
|
{ name: 'strategic_insights', data: data.strategic_insights },
|
||||||
|
{ name: 'competitive_analysis', data: data.competitive_analysis },
|
||||||
|
{ name: 'performance_predictions', data: data.performance_predictions },
|
||||||
|
{ name: 'implementation_roadmap', data: data.implementation_roadmap },
|
||||||
|
{ name: 'risk_assessment', data: data.risk_assessment }
|
||||||
|
];
|
||||||
|
|
||||||
|
components.forEach(({ name, data: componentData }) => {
|
||||||
|
if (componentData) {
|
||||||
|
checkForEmptyObjects(componentData, name);
|
||||||
|
|
||||||
|
// Check if the component has meaningful data
|
||||||
|
if (!hasValidData(componentData)) {
|
||||||
|
warnings.push(`${name} component exists but contains no meaningful data`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warnings.push(`${name} component is missing`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check metadata
|
||||||
|
if (data.strategy_metadata) {
|
||||||
|
checkForEmptyObjects(data.strategy_metadata, 'strategy_metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.metadata) {
|
||||||
|
checkForEmptyObjects(data.metadata, 'metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check summary
|
||||||
|
if (data.summary) {
|
||||||
|
checkForEmptyObjects(data.summary, 'summary');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: errors.length === 0,
|
||||||
|
errors,
|
||||||
|
warnings
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes strategy data to prevent rendering errors
|
||||||
|
*/
|
||||||
|
export const sanitizeStrategyData = (data: StrategyData | null | undefined): StrategyData | null => {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const sanitizeValue = (value: any): any => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(sanitizeValue).filter(v => v !== null);
|
||||||
|
} else {
|
||||||
|
const keys = Object.keys(value);
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return null; // Remove empty objects
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized: any = {};
|
||||||
|
keys.forEach(key => {
|
||||||
|
const sanitizedValue = sanitizeValue(value[key]);
|
||||||
|
if (sanitizedValue !== null) {
|
||||||
|
sanitized[key] = sanitizedValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.keys(sanitized).length > 0 ? sanitized : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a deep copy and sanitize
|
||||||
|
const sanitized = JSON.parse(JSON.stringify(data));
|
||||||
|
|
||||||
|
// Sanitize each component
|
||||||
|
if (sanitized.strategic_insights) {
|
||||||
|
sanitized.strategic_insights = sanitizeValue(sanitized.strategic_insights);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sanitized.competitive_analysis) {
|
||||||
|
sanitized.competitive_analysis = sanitizeValue(sanitized.competitive_analysis);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sanitized.performance_predictions) {
|
||||||
|
sanitized.performance_predictions = sanitizeValue(sanitized.performance_predictions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sanitized.implementation_roadmap) {
|
||||||
|
sanitized.implementation_roadmap = sanitizeValue(sanitized.implementation_roadmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sanitized.risk_assessment) {
|
||||||
|
sanitized.risk_assessment = sanitizeValue(sanitized.risk_assessment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a safe strategy data object for rendering
|
||||||
|
*/
|
||||||
|
export const createSafeStrategyData = (data: StrategyData | null | undefined): StrategyData | null => {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
// First validate the data
|
||||||
|
const validation = validateStrategyData(data);
|
||||||
|
|
||||||
|
if (validation.errors.length > 0) {
|
||||||
|
console.warn('Strategy data validation errors:', validation.errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validation.warnings.length > 0) {
|
||||||
|
console.warn('Strategy data validation warnings:', validation.warnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then sanitize the data
|
||||||
|
return sanitizeStrategyData(data);
|
||||||
|
};
|
||||||
@@ -54,7 +54,7 @@ interface StrategyOnboardingDialogProps {
|
|||||||
onEditStrategy: () => void;
|
onEditStrategy: () => void;
|
||||||
onCreateNewStrategy: () => void;
|
onCreateNewStrategy: () => void;
|
||||||
currentStrategy: any;
|
currentStrategy: any;
|
||||||
strategyStatus: 'active' | 'inactive' | 'none';
|
strategyStatus: 'active' | 'inactive' | 'pending' | 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
const StrategyOnboardingDialog: React.FC<StrategyOnboardingDialogProps> = ({
|
const StrategyOnboardingDialog: React.FC<StrategyOnboardingDialogProps> = ({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Paper,
|
Paper,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
AutoAwesome as AutoAwesomeIcon,
|
AutoAwesome as AutoAwesomeIcon,
|
||||||
Edit as EditIcon
|
Edit as EditIcon
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
|
||||||
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
import { contentPlanningApi } from '../../../services/contentPlanningApi';
|
||||||
import StrategyIntelligenceTab from '../components/StrategyIntelligence/StrategyIntelligenceTab';
|
import StrategyIntelligenceTab from '../components/StrategyIntelligence/StrategyIntelligenceTab';
|
||||||
@@ -19,17 +20,19 @@ import StrategyOnboardingDialog from '../components/StrategyOnboardingDialog';
|
|||||||
import { StrategyData } from '../components/StrategyIntelligence/types/strategy.types';
|
import { StrategyData } from '../components/StrategyIntelligence/types/strategy.types';
|
||||||
|
|
||||||
const ContentStrategyTab: React.FC = () => {
|
const ContentStrategyTab: React.FC = () => {
|
||||||
const {
|
const location = useLocation();
|
||||||
strategies,
|
// Use selective store subscriptions to prevent unnecessary re-renders
|
||||||
currentStrategy,
|
const strategies = useContentPlanningStore(state => state.strategies);
|
||||||
aiInsights,
|
const currentStrategy = useContentPlanningStore(state => state.currentStrategy);
|
||||||
aiRecommendations,
|
const latestGeneratedStrategy = useContentPlanningStore(state => state.latestGeneratedStrategy);
|
||||||
loading,
|
const aiInsights = useContentPlanningStore(state => state.aiInsights);
|
||||||
error,
|
const aiRecommendations = useContentPlanningStore(state => state.aiRecommendations);
|
||||||
loadStrategies,
|
const loading = useContentPlanningStore(state => state.loading);
|
||||||
loadAIInsights,
|
const error = useContentPlanningStore(state => state.error);
|
||||||
loadAIRecommendations
|
const loadStrategies = useContentPlanningStore(state => state.loadStrategies);
|
||||||
} = useContentPlanningStore();
|
const loadAIInsights = useContentPlanningStore(state => state.loadAIInsights);
|
||||||
|
const loadAIRecommendations = useContentPlanningStore(state => state.loadAIRecommendations);
|
||||||
|
const setLatestGeneratedStrategy = useContentPlanningStore(state => state.setLatestGeneratedStrategy);
|
||||||
|
|
||||||
const [strategyForm, setStrategyForm] = useState({
|
const [strategyForm, setStrategyForm] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -52,23 +55,61 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Strategy status and onboarding
|
// Strategy status and onboarding
|
||||||
const [strategyStatus, setStrategyStatus] = useState<'active' | 'inactive' | 'none'>('none');
|
const [strategyStatus, setStrategyStatus] = useState<'active' | 'inactive' | 'pending' | 'none'>('none');
|
||||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||||
const [hasCheckedStrategy, setHasCheckedStrategy] = useState(false);
|
const [hasCheckedStrategy, setHasCheckedStrategy] = useState(false);
|
||||||
|
|
||||||
|
// Navigation state detection
|
||||||
|
const [isFromStrategyBuilder, setIsFromStrategyBuilder] = useState(false);
|
||||||
|
|
||||||
// Load data on component mount
|
// Load data on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadInitialData();
|
loadInitialData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check if coming from strategy builder
|
||||||
|
useEffect(() => {
|
||||||
|
const locationState = location.state as any;
|
||||||
|
const isFromBuilder = locationState?.fromStrategyBuilder ||
|
||||||
|
locationState?.activeTab === 0 || // Content Strategy tab
|
||||||
|
sessionStorage.getItem('fromStrategyBuilder') === 'true';
|
||||||
|
|
||||||
|
console.log('🔍 ContentStrategyTab: Navigation state check:', {
|
||||||
|
locationState,
|
||||||
|
isFromBuilder,
|
||||||
|
sessionStorage: sessionStorage.getItem('fromStrategyBuilder')
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsFromStrategyBuilder(isFromBuilder);
|
||||||
|
|
||||||
|
// Clear the session storage flag after reading it
|
||||||
|
if (sessionStorage.getItem('fromStrategyBuilder') === 'true') {
|
||||||
|
sessionStorage.removeItem('fromStrategyBuilder');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the cache when navigating away from strategy builder
|
||||||
|
if (!isFromBuilder && latestGeneratedStrategy) {
|
||||||
|
console.log('🧹 Clearing latest generated strategy cache (navigating away from strategy builder)');
|
||||||
|
// Note: We don't clear the cache here as it might be needed for the current session
|
||||||
|
}
|
||||||
|
}, [location.state]);
|
||||||
|
|
||||||
|
// Track strategy status changes for debugging (with debounce)
|
||||||
|
useEffect(() => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
console.log('🔄 Strategy Status Changed:', {
|
||||||
|
status: strategyStatus,
|
||||||
|
hasStrategyData: !!strategyData,
|
||||||
|
strategyDataKeys: strategyData ? Object.keys(strategyData) : [],
|
||||||
|
isFromStrategyBuilder
|
||||||
|
});
|
||||||
|
}, 100); // 100ms debounce
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [strategyStatus, strategyData, isFromStrategyBuilder]);
|
||||||
|
|
||||||
// Check strategy status when strategies are loaded
|
// Check strategy status when strategies are loaded
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🔄 useEffect triggered - strategies changed:', strategies);
|
|
||||||
console.log('🔄 Strategies type:', typeof strategies);
|
|
||||||
console.log('🔄 Is Array:', Array.isArray(strategies));
|
|
||||||
console.log('🔄 Strategies length:', strategies?.length);
|
|
||||||
console.log('🔄 Has checked strategy:', hasCheckedStrategy);
|
|
||||||
|
|
||||||
// Handle different response formats
|
// Handle different response formats
|
||||||
let strategiesArray: any[] = [];
|
let strategiesArray: any[] = [];
|
||||||
|
|
||||||
@@ -80,64 +121,237 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
strategiesArray = (strategies as any).strategies;
|
strategiesArray = (strategies as any).strategies;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔄 StrategiesArray length:', strategiesArray.length);
|
|
||||||
|
|
||||||
if (strategiesArray.length > 0) {
|
if (strategiesArray.length > 0) {
|
||||||
console.log('✅ Strategies found, checking status...');
|
|
||||||
checkStrategyStatus();
|
checkStrategyStatus();
|
||||||
loadStrategyData();
|
|
||||||
|
// Add debounce to prevent rapid successive calls
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
loadStrategyData();
|
||||||
|
}, 500); // 500ms debounce
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
} else if (strategiesArray.length === 0 && hasCheckedStrategy) {
|
} else if (strategiesArray.length === 0 && hasCheckedStrategy) {
|
||||||
// Only set to 'none' if we've already checked and confirmed no strategies
|
// Only set to 'none' if we've already checked and confirmed no strategies
|
||||||
console.log('❌ No strategies found, setting status to none...');
|
|
||||||
setStrategyStatus('none');
|
setStrategyStatus('none');
|
||||||
setShowOnboarding(true);
|
setShowOnboarding(true);
|
||||||
}
|
}
|
||||||
// If strategiesArray.length === 0 and !hasCheckedStrategy, do nothing (wait for data to load)
|
// If strategiesArray.length === 0 and !hasCheckedStrategy, do nothing (wait for data to load)
|
||||||
}, [strategies, loadStrategies]);
|
}, [strategies, loadStrategies, isFromStrategyBuilder]);
|
||||||
|
|
||||||
const loadStrategyData = async () => {
|
const loadStrategyData = async () => {
|
||||||
|
// Prevent multiple simultaneous requests
|
||||||
|
if (strategyDataLoading) {
|
||||||
|
console.log('🔄 Strategy data loading already in progress, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setStrategyDataLoading(true);
|
setStrategyDataLoading(true);
|
||||||
setStrategyDataError(null);
|
setStrategyDataError(null);
|
||||||
|
|
||||||
const userId = 1; // Default user ID
|
const userId = 1; // Default user ID
|
||||||
|
|
||||||
// Try to get the latest generated strategy
|
// PRIORITY 0: Check cache first for latest generated strategy
|
||||||
|
console.log('🔍 PRIORITY 0: Checking cache for latest generated strategy...');
|
||||||
|
console.log('🔍 Cache state:', {
|
||||||
|
hasCache: !!latestGeneratedStrategy,
|
||||||
|
cacheData: latestGeneratedStrategy ? {
|
||||||
|
hasStrategicInsights: !!latestGeneratedStrategy.strategic_insights,
|
||||||
|
hasCompetitiveAnalysis: !!latestGeneratedStrategy.competitive_analysis,
|
||||||
|
hasPerformancePredictions: !!latestGeneratedStrategy.performance_predictions,
|
||||||
|
hasImplementationRoadmap: !!latestGeneratedStrategy.implementation_roadmap,
|
||||||
|
hasRiskAssessment: !!latestGeneratedStrategy.risk_assessment
|
||||||
|
} : null
|
||||||
|
});
|
||||||
|
if (latestGeneratedStrategy && latestGeneratedStrategy.strategic_insights) {
|
||||||
|
console.log('🚀 PRIORITY 0: Found latest generated strategy in cache!');
|
||||||
|
console.log('📊 Cached strategy data structure:', {
|
||||||
|
hasStrategicInsights: !!latestGeneratedStrategy.strategic_insights,
|
||||||
|
hasCompetitiveAnalysis: !!latestGeneratedStrategy.competitive_analysis,
|
||||||
|
hasPerformancePredictions: !!latestGeneratedStrategy.performance_predictions,
|
||||||
|
hasImplementationRoadmap: !!latestGeneratedStrategy.implementation_roadmap,
|
||||||
|
hasRiskAssessment: !!latestGeneratedStrategy.risk_assessment,
|
||||||
|
strategyId: latestGeneratedStrategy.id,
|
||||||
|
strategyName: latestGeneratedStrategy.name
|
||||||
|
});
|
||||||
|
setStrategyData(latestGeneratedStrategy);
|
||||||
|
|
||||||
|
// Set strategy status to pending for newly generated strategy (needs review)
|
||||||
|
setStrategyStatus('pending');
|
||||||
|
setShowOnboarding(false);
|
||||||
|
console.log('📋 Set strategy status to pending for cached strategy (needs review)');
|
||||||
|
console.log('🔄 Strategy Review Workflow: New strategy ready for review - User should see review process');
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.log('❌ PRIORITY 0: No strategy found in cache, proceeding to next priority...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORITY 1: If coming from strategy builder, prioritize the latest generated strategy
|
||||||
|
if (isFromStrategyBuilder) {
|
||||||
|
// Try with exponential backoff to handle rate limits
|
||||||
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||||
|
try {
|
||||||
|
console.log(`🔍 PRIORITY 1 (Attempt ${attempt}/2): Trying to get latest generated strategy from polling system...`);
|
||||||
|
|
||||||
|
// Add exponential backoff delay for subsequent attempts
|
||||||
|
if (attempt > 1) {
|
||||||
|
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s delays
|
||||||
|
console.log(`⏳ Waiting ${delay}ms before retry...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestStrategyResponse = await contentPlanningApi.getLatestGeneratedStrategyWithRetry(userId);
|
||||||
|
|
||||||
|
console.log(`🔍 Latest strategy response (attempt ${attempt}):`, latestStrategyResponse);
|
||||||
|
|
||||||
|
if (latestStrategyResponse && latestStrategyResponse.strategic_insights) {
|
||||||
|
console.log('✅ Found latest generated strategy in polling system');
|
||||||
|
console.log('📊 Latest strategy data structure:', {
|
||||||
|
hasStrategicInsights: !!latestStrategyResponse.strategic_insights,
|
||||||
|
hasCompetitiveAnalysis: !!latestStrategyResponse.competitive_analysis,
|
||||||
|
hasPerformancePredictions: !!latestStrategyResponse.performance_predictions,
|
||||||
|
hasImplementationRoadmap: !!latestStrategyResponse.implementation_roadmap,
|
||||||
|
hasRiskAssessment: !!latestStrategyResponse.risk_assessment,
|
||||||
|
strategyId: latestStrategyResponse.id,
|
||||||
|
strategyName: latestStrategyResponse.name
|
||||||
|
});
|
||||||
|
setStrategyData(latestStrategyResponse);
|
||||||
|
|
||||||
|
// Cache the strategy since it wasn't cached during generation
|
||||||
|
console.log('💾 Caching strategy from polling system since it was not cached during generation');
|
||||||
|
setLatestGeneratedStrategy(latestStrategyResponse);
|
||||||
|
|
||||||
|
// Set strategy status to pending for newly generated strategy (needs review)
|
||||||
|
setStrategyStatus('pending');
|
||||||
|
setShowOnboarding(false);
|
||||||
|
console.log('📋 Set strategy status to pending for polling system strategy (needs review)');
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.log(`⚠️ Latest strategy response missing strategic_insights (attempt ${attempt}), trying database...`);
|
||||||
|
console.log('🔍 Latest strategy response structure:', latestStrategyResponse);
|
||||||
|
}
|
||||||
|
} catch (pollingError: any) {
|
||||||
|
console.log(`⚠️ Error getting latest strategy from polling system (attempt ${attempt}):`, pollingError);
|
||||||
|
|
||||||
|
// Check if it's a rate limit error
|
||||||
|
if (pollingError?.response?.status === 429) {
|
||||||
|
console.log('🚫 Rate limit hit, will retry with longer delay...');
|
||||||
|
if (attempt === 2) {
|
||||||
|
console.log('❌ Rate limit persists, skipping polling system and trying database...');
|
||||||
|
break; // Exit the loop and try database instead
|
||||||
|
}
|
||||||
|
} else if (attempt === 2) {
|
||||||
|
console.log('❌ All attempts failed, continuing to database fallback...');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORITY 2: Try to get the latest generated strategy from polling system
|
||||||
try {
|
try {
|
||||||
const latestStrategyResponse = await contentPlanningApi.getLatestGeneratedStrategy(userId);
|
const latestStrategyResponse = await contentPlanningApi.getLatestGeneratedStrategy(userId);
|
||||||
|
|
||||||
console.log('🔍 Latest strategy response from API:', latestStrategyResponse);
|
|
||||||
|
|
||||||
if (latestStrategyResponse && latestStrategyResponse.strategic_insights) {
|
if (latestStrategyResponse && latestStrategyResponse.strategic_insights) {
|
||||||
console.log('✅ Found latest generated strategy:', latestStrategyResponse);
|
|
||||||
setStrategyData(latestStrategyResponse);
|
setStrategyData(latestStrategyResponse);
|
||||||
|
|
||||||
|
// Cache the strategy since it wasn't cached during generation
|
||||||
|
console.log('💾 Caching strategy from general polling since it was not cached during generation');
|
||||||
|
setLatestGeneratedStrategy(latestStrategyResponse);
|
||||||
|
|
||||||
|
// Set strategy status to pending for newly generated strategy (needs review)
|
||||||
|
setStrategyStatus('pending');
|
||||||
|
setShowOnboarding(false);
|
||||||
|
console.log('📋 Set strategy status to pending for general polling strategy (needs review)');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (pollingError) {
|
} catch (pollingError) {
|
||||||
console.log('No latest strategy found in polling system, checking database...', pollingError);
|
// Continue to next priority
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no strategy found in polling system, try to get from database
|
// PRIORITY 3: If no strategy found in polling system, try to get from database
|
||||||
|
console.log('🎯 PRIORITY 3: Checking database for strategies...');
|
||||||
try {
|
try {
|
||||||
const strategiesResponse = await contentPlanningApi.getEnhancedStrategies(userId);
|
const strategiesResponse = await contentPlanningApi.getEnhancedStrategies(userId);
|
||||||
|
|
||||||
const strategies = strategiesResponse?.data?.strategies || strategiesResponse?.strategies || [];
|
const strategies = strategiesResponse?.data?.strategies || strategiesResponse?.strategies || [];
|
||||||
|
|
||||||
if (strategies && strategies.length > 0) {
|
if (strategies && strategies.length > 0) {
|
||||||
const latestStrategy = strategies[0];
|
console.log('🔍 Found strategies in database:', strategies.length);
|
||||||
|
|
||||||
|
// Sort strategies by creation date (newest first) to ensure we get the latest
|
||||||
|
const sortedStrategies = strategies.sort((a: any, b: any) => {
|
||||||
|
const dateA = new Date(a.created_at || a.createdAt || 0);
|
||||||
|
const dateB = new Date(b.created_at || b.createdAt || 0);
|
||||||
|
return dateB.getTime() - dateA.getTime(); // Descending order (newest first)
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🔍 Sorted strategies by creation date:', sortedStrategies.map((s: any) => ({
|
||||||
|
id: s.id,
|
||||||
|
name: s.name,
|
||||||
|
created_at: s.created_at || s.createdAt,
|
||||||
|
has_comprehensive_analysis: !!s.comprehensive_ai_analysis,
|
||||||
|
has_ai_recommendations: !!s.ai_recommendations
|
||||||
|
})));
|
||||||
|
|
||||||
|
// If coming from strategy builder, prioritize strategies with comprehensive_ai_analysis
|
||||||
|
if (isFromStrategyBuilder) {
|
||||||
|
const latestComprehensiveStrategy = sortedStrategies.find((s: any) => s.comprehensive_ai_analysis);
|
||||||
|
if (latestComprehensiveStrategy) {
|
||||||
|
console.log('✅ Found latest comprehensive strategy in database:', latestComprehensiveStrategy.id);
|
||||||
|
setStrategyData(latestComprehensiveStrategy.comprehensive_ai_analysis);
|
||||||
|
|
||||||
|
// Set strategy status to active for existing database strategy
|
||||||
|
setStrategyStatus('active');
|
||||||
|
setShowOnboarding(false);
|
||||||
|
console.log('✅ Set strategy status to active for database comprehensive strategy');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to ai_recommendations if comprehensive_ai_analysis not available
|
||||||
|
const latestWithRecommendations = sortedStrategies.find((s: any) => s.ai_recommendations);
|
||||||
|
if (latestWithRecommendations) {
|
||||||
|
console.log('✅ Found latest strategy with ai_recommendations in database:', latestWithRecommendations.id);
|
||||||
|
setStrategyData(latestWithRecommendations.ai_recommendations);
|
||||||
|
|
||||||
|
// Set strategy status to active for existing database strategy
|
||||||
|
setStrategyStatus('active');
|
||||||
|
setShowOnboarding(false);
|
||||||
|
console.log('✅ Set strategy status to active for database recommendations strategy');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most recent strategy (first after sorting)
|
||||||
|
const latestStrategy = sortedStrategies[0];
|
||||||
|
|
||||||
if (latestStrategy.comprehensive_ai_analysis) {
|
if (latestStrategy.comprehensive_ai_analysis) {
|
||||||
console.log('✅ Found comprehensive strategy in database:', latestStrategy);
|
|
||||||
setStrategyData(latestStrategy.comprehensive_ai_analysis);
|
setStrategyData(latestStrategy.comprehensive_ai_analysis);
|
||||||
|
|
||||||
|
// Set strategy status to active for existing database strategy
|
||||||
|
setStrategyStatus('active');
|
||||||
|
setShowOnboarding(false);
|
||||||
|
console.log('✅ Set strategy status to active for database latest strategy');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (dbError) {
|
} catch (dbError: any) {
|
||||||
console.log('No comprehensive strategies found in database:', dbError);
|
console.error('Database error:', dbError);
|
||||||
|
|
||||||
|
// Check if it's a rate limit error
|
||||||
|
if (dbError?.response?.status === 429) {
|
||||||
|
console.log('🚫 Database request also hit rate limit, showing error to user...');
|
||||||
|
setStrategyDataError('Server is temporarily overloaded. Please try again in a few moments.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no strategy data is available
|
// If no strategy data is available
|
||||||
console.log('❌ No comprehensive strategy data found');
|
|
||||||
setStrategyData(null);
|
setStrategyData(null);
|
||||||
setStrategyDataError('No comprehensive strategy data available. Please generate a strategy first.');
|
setStrategyDataError('No comprehensive strategy data available. Please generate a strategy first.');
|
||||||
|
|
||||||
@@ -150,13 +364,20 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add a timeout to prevent infinite loading
|
||||||
|
useEffect(() => {
|
||||||
|
if (strategyDataLoading) {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
console.log('⏰ Strategy data loading timeout, resetting state...');
|
||||||
|
setStrategyDataLoading(false);
|
||||||
|
setStrategyDataError('Loading timeout. Please refresh the page.');
|
||||||
|
}, 30000); // 30 second timeout
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}, [strategyDataLoading]);
|
||||||
|
|
||||||
const checkStrategyStatus = () => {
|
const checkStrategyStatus = () => {
|
||||||
console.log('🔍 Checking strategy status...');
|
|
||||||
console.log('🔍 Strategies from store:', strategies);
|
|
||||||
console.log('🔍 Strategies type:', typeof strategies);
|
|
||||||
console.log('🔍 Is Array:', Array.isArray(strategies));
|
|
||||||
console.log('🔍 Strategies length:', strategies?.length);
|
|
||||||
|
|
||||||
// Handle different response formats
|
// Handle different response formats
|
||||||
let strategiesArray: any[] = [];
|
let strategiesArray: any[] = [];
|
||||||
|
|
||||||
@@ -168,21 +389,15 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
strategiesArray = (strategies as any).strategies;
|
strategiesArray = (strategies as any).strategies;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🔍 StrategiesArray length:', strategiesArray.length);
|
|
||||||
|
|
||||||
if (strategiesArray.length > 0) {
|
if (strategiesArray.length > 0) {
|
||||||
// Find the most recent strategy
|
// Find the most recent strategy
|
||||||
const latestStrategy = strategiesArray[0]; // Assuming strategies are sorted by date
|
const latestStrategy = strategiesArray[0]; // Assuming strategies are sorted by date
|
||||||
|
|
||||||
console.log('✅ Found strategies in database:', strategiesArray.length);
|
|
||||||
console.log('📊 Latest strategy:', latestStrategy);
|
|
||||||
|
|
||||||
// For now, we'll assume strategies are active if they exist
|
// For now, we'll assume strategies are active if they exist
|
||||||
// In a real implementation, you would check a status field from the database
|
// In a real implementation, you would check a status field from the database
|
||||||
setStrategyStatus('active');
|
setStrategyStatus('active');
|
||||||
setShowOnboarding(false);
|
setShowOnboarding(false);
|
||||||
} else {
|
} else {
|
||||||
console.log('❌ No strategies found in database');
|
|
||||||
setStrategyStatus('none');
|
setStrategyStatus('none');
|
||||||
setShowOnboarding(true);
|
setShowOnboarding(true);
|
||||||
}
|
}
|
||||||
@@ -193,14 +408,30 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
setDataLoading({ strategies: true, insights: true, recommendations: true, strategicIntelligence: true });
|
setDataLoading({ strategies: true, insights: true, recommendations: true, strategicIntelligence: true });
|
||||||
|
|
||||||
// Load strategies
|
// Load strategies first (most important)
|
||||||
|
console.log('🔄 Loading strategies...');
|
||||||
await loadStrategies();
|
await loadStrategies();
|
||||||
|
|
||||||
// Load AI insights and recommendations
|
// Add delay between requests to avoid rate limits
|
||||||
await Promise.all([
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
loadAIInsights(),
|
|
||||||
loadAIRecommendations()
|
// Load AI insights and recommendations sequentially instead of in parallel
|
||||||
]);
|
console.log('🔄 Loading AI insights...');
|
||||||
|
try {
|
||||||
|
await loadAIInsights();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Failed to load AI insights:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add delay between requests
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
console.log('🔄 Loading AI recommendations...');
|
||||||
|
try {
|
||||||
|
await loadAIRecommendations();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Failed to load AI recommendations:', error);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading initial data:', error);
|
console.error('Error loading initial data:', error);
|
||||||
@@ -245,6 +476,28 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Pending Strategy Status Banner */}
|
||||||
|
{strategyStatus === 'pending' && (
|
||||||
|
<Alert
|
||||||
|
severity="info"
|
||||||
|
sx={{ mb: 3 }}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setShowOnboarding(true)}
|
||||||
|
startIcon={<PlayArrowIcon />}
|
||||||
|
>
|
||||||
|
Review & Activate
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Typography variant="body1">
|
||||||
|
<strong>Strategy Ready for Review:</strong> Your AI-generated content strategy is ready! Please review all components and confirm to activate your strategy.
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Strategy Status Banner */}
|
{/* Strategy Status Banner */}
|
||||||
{strategyStatus === 'inactive' && (
|
{strategyStatus === 'inactive' && (
|
||||||
<Alert
|
<Alert
|
||||||
@@ -310,14 +563,40 @@ const ContentStrategyTab: React.FC = () => {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Strategic Intelligence - Only show if there's an active strategy */}
|
{/* Strategic Intelligence - Show for both active and pending strategies */}
|
||||||
{strategyStatus === 'active' && (
|
{(strategyStatus === 'active' || strategyStatus === 'pending') && (
|
||||||
<Paper sx={{ width: '100%', mb: 3 }}>
|
<Paper sx={{ width: '100%', mb: 3 }}>
|
||||||
<StrategyIntelligenceTab
|
{strategyDataLoading ? (
|
||||||
strategyData={strategyData}
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', p: 4 }}>
|
||||||
loading={strategyDataLoading}
|
<CircularProgress />
|
||||||
error={strategyDataError}
|
<Typography variant="body2" sx={{ ml: 2, color: 'text.secondary' }}>
|
||||||
/>
|
Loading strategy data...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : strategyDataError ? (
|
||||||
|
<Alert severity="error" sx={{ m: 2 }}>
|
||||||
|
{strategyDataError}
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<StrategyIntelligenceTab
|
||||||
|
strategyData={strategyData}
|
||||||
|
loading={strategyDataLoading}
|
||||||
|
error={strategyDataError}
|
||||||
|
strategyStatus={strategyStatus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Loading indicator for initial data */}
|
||||||
|
{Object.values(dataLoading).some(loading => loading) && (
|
||||||
|
<Paper sx={{ width: '100%', mb: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', p: 4 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography variant="body2" sx={{ ml: 2, color: 'text.secondary' }}>
|
||||||
|
Loading dashboard data...
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const CreateTab: React.FC = () => {
|
|||||||
// Get strategy context from the provider
|
// Get strategy context from the provider
|
||||||
const { state: { strategyContext }, isFromStrategyActivation } = useStrategyCalendarContext();
|
const { state: { strategyContext }, isFromStrategyActivation } = useStrategyCalendarContext();
|
||||||
|
|
||||||
console.log('🔍 CreateTab: Rendering', { userData, strategyContext, locationState: location.state });
|
// Removed verbose logging for cleaner console
|
||||||
|
|
||||||
// Memoize the strategy activation status to avoid infinite re-renders
|
// Memoize the strategy activation status to avoid infinite re-renders
|
||||||
const fromStrategyActivation = useMemo(() => {
|
const fromStrategyActivation = useMemo(() => {
|
||||||
@@ -72,7 +72,7 @@ const CreateTab: React.FC = () => {
|
|||||||
|
|
||||||
// Auto-switch to Calendar Wizard tab when coming from strategy activation
|
// Auto-switch to Calendar Wizard tab when coming from strategy activation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('🔍 CreateTab: Checking strategy activation status:', { fromStrategyActivation });
|
// Removed verbose logging for cleaner console
|
||||||
|
|
||||||
// Check multiple sources for strategy activation status
|
// Check multiple sources for strategy activation status
|
||||||
const isFromStrategy = fromStrategyActivation ||
|
const isFromStrategy = fromStrategyActivation ||
|
||||||
|
|||||||
@@ -282,11 +282,6 @@ export const StrategyCalendarProvider: React.FC<StrategyCalendarProviderProps> =
|
|||||||
// Check if we have a preserved strategy context from navigation
|
// Check if we have a preserved strategy context from navigation
|
||||||
const result = state.strategyContext?.activationStatus === 'active' &&
|
const result = state.strategyContext?.activationStatus === 'active' &&
|
||||||
state.strategyContext?.activationTimestamp !== null;
|
state.strategyContext?.activationTimestamp !== null;
|
||||||
console.log('🔍 StrategyCalendarContext: isFromStrategyActivation check:', {
|
|
||||||
activationStatus: state.strategyContext?.activationStatus,
|
|
||||||
activationTimestamp: state.strategyContext?.activationTimestamp,
|
|
||||||
result
|
|
||||||
});
|
|
||||||
return result;
|
return result;
|
||||||
}, [state.strategyContext?.activationStatus, state.strategyContext?.activationTimestamp]);
|
}, [state.strategyContext?.activationStatus, state.strategyContext?.activationTimestamp]);
|
||||||
|
|
||||||
|
|||||||
@@ -458,6 +458,27 @@ class ContentPlanningAPI {
|
|||||||
return this.handleRequest(() => this.getAIAnalytics(userId), true);
|
return this.handleRequest(() => this.getAIAnalytics(userId), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced version with rate limit handling for AI analytics
|
||||||
|
async getAIAnalyticsWithRetry(userId?: number, maxRetries: number = 2): Promise<any> {
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get(`${this.baseURL}/ai-analytics/`, {
|
||||||
|
params: { user_id: userId || 1 }
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.status === 429 && attempt < maxRetries) {
|
||||||
|
// Rate limit hit, wait and retry
|
||||||
|
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
||||||
|
console.log(`🚫 Rate limit hit for AI analytics, waiting ${delay}ms before retry ${attempt + 1}/${maxRetries}`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error; // Re-throw if it's not a rate limit or we've exhausted retries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// AI Analytics with force refresh option
|
// AI Analytics with force refresh option
|
||||||
async getAIAnalyticsWithRefresh(userId?: number, forceRefresh = false): Promise<any> {
|
async getAIAnalyticsWithRefresh(userId?: number, forceRefresh = false): Promise<any> {
|
||||||
try {
|
try {
|
||||||
@@ -517,7 +538,7 @@ class ContentPlanningAPI {
|
|||||||
|
|
||||||
async getComprehensiveUserData(userId?: number): Promise<any> {
|
async getComprehensiveUserData(userId?: number): Promise<any> {
|
||||||
return this.handleRequest(async () => {
|
return this.handleRequest(async () => {
|
||||||
const response = await apiClient.get(`${this.baseURL}/comprehensive-user-data`, {
|
const response = await apiClient.get(`${this.baseURL}/calendar-generation/comprehensive-user-data`, {
|
||||||
params: { user_id: userId }
|
params: { user_id: userId }
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -593,30 +614,17 @@ class ContentPlanningAPI {
|
|||||||
|
|
||||||
// Non-streaming autofill refresh method
|
// Non-streaming autofill refresh method
|
||||||
async refreshAutofill(userId?: number, useAI: boolean = true, aiOnly: boolean = false): Promise<any> {
|
async refreshAutofill(userId?: number, useAI: boolean = true, aiOnly: boolean = false): Promise<any> {
|
||||||
const params: any = { use_ai: useAI, ai_only: aiOnly };
|
const params: any = {
|
||||||
|
use_ai: useAI,
|
||||||
|
ai_only: aiOnly,
|
||||||
|
_t: Date.now() // 🚨 CRITICAL: Cache-busting timestamp to ensure fresh AI generation
|
||||||
|
};
|
||||||
if (userId) params.user_id = userId;
|
if (userId) params.user_id = userId;
|
||||||
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/autofill/refresh`, null, { params });
|
const response = await apiClient.post(`${this.baseURL}/enhanced-strategies/autofill/refresh`, null, { params });
|
||||||
|
|
||||||
// Debug the API response
|
|
||||||
console.log('🎯 API refreshAutofill response:', {
|
|
||||||
responseType: typeof response,
|
|
||||||
responseKeys: Object.keys(response),
|
|
||||||
dataType: typeof response.data,
|
|
||||||
dataKeys: response.data ? Object.keys(response.data) : 'no data',
|
|
||||||
hasDataProperty: response.data?.hasOwnProperty('data'),
|
|
||||||
hasFieldsProperty: response.data?.hasOwnProperty('fields'),
|
|
||||||
dataDataKeys: response.data?.data ? Object.keys(response.data.data) : 'no data.data'
|
|
||||||
});
|
|
||||||
|
|
||||||
// The backend returns ResponseBuilder format: { status, message, data, status_code, timestamp }
|
// The backend returns ResponseBuilder format: { status, message, data, status_code, timestamp }
|
||||||
// We need to return the actual payload from response.data.data
|
// We need to return the actual payload from response.data.data
|
||||||
const result = response.data?.data || response.data;
|
const result = response.data?.data || response.data;
|
||||||
console.log('🎯 API refreshAutofill returning:', {
|
|
||||||
resultType: typeof result,
|
|
||||||
resultKeys: Object.keys(result),
|
|
||||||
hasFields: result?.hasOwnProperty('fields'),
|
|
||||||
fieldsCount: result?.fields ? Object.keys(result.fields).length : 0
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -714,21 +722,33 @@ class ContentPlanningAPI {
|
|||||||
return this.handleRequest(async () => {
|
return this.handleRequest(async () => {
|
||||||
const params = userId ? { user_id: userId } : {};
|
const params = userId ? { user_id: userId } : {};
|
||||||
const response = await apiClient.get(`${this.baseURL}/content-strategy/ai-generation/latest-strategy`, { params });
|
const response = await apiClient.get(`${this.baseURL}/content-strategy/ai-generation/latest-strategy`, { params });
|
||||||
console.log('🔍 getLatestGeneratedStrategy response:', response.data);
|
|
||||||
console.log('🔍 Response structure:', {
|
|
||||||
hasData: !!response.data,
|
|
||||||
dataKeys: Object.keys(response.data || {}),
|
|
||||||
hasStrategy: !!response.data?.data?.strategy,
|
|
||||||
strategyKeys: response.data?.data?.strategy ? Object.keys(response.data.data.strategy) : []
|
|
||||||
});
|
|
||||||
// Return the strategy data from the nested response structure
|
// Return the strategy data from the nested response structure
|
||||||
const result = response.data?.data?.strategy;
|
const result = response.data?.data?.strategy;
|
||||||
console.log('🔍 Returning result:', result);
|
|
||||||
console.log('🔍 Result keys:', Object.keys(result || {}));
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced version with rate limit handling
|
||||||
|
async getLatestGeneratedStrategyWithRetry(userId?: number, maxRetries: number = 2): Promise<any> {
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const params = userId ? { user_id: userId } : {};
|
||||||
|
const response = await apiClient.get(`${this.baseURL}/content-strategy/ai-generation/latest-strategy`, { params });
|
||||||
|
const result = response.data?.data?.strategy;
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.status === 429 && attempt < maxRetries) {
|
||||||
|
// Rate limit hit, wait and retry
|
||||||
|
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
|
||||||
|
console.log(`🚫 Rate limit hit, waiting ${delay}ms before retry ${attempt + 1}/${maxRetries}`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error; // Re-throw if it's not a rate limit or we've exhausted retries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async startStrategyGenerationPolling(userId: number, strategyName: string): Promise<any> {
|
async startStrategyGenerationPolling(userId: number, strategyName: string): Promise<any> {
|
||||||
return this.handleRequest(async () => {
|
return this.handleRequest(async () => {
|
||||||
const response = await apiClient.post(`${this.baseURL}/content-strategy/ai-generation/generate-comprehensive-strategy-polling`, {
|
const response = await apiClient.post(`${this.baseURL}/content-strategy/ai-generation/generate-comprehensive-strategy-polling`, {
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ interface ContentPlanningStore {
|
|||||||
// State
|
// State
|
||||||
strategies: ContentStrategy[];
|
strategies: ContentStrategy[];
|
||||||
currentStrategy: ContentStrategy | null;
|
currentStrategy: ContentStrategy | null;
|
||||||
|
latestGeneratedStrategy: any | null; // Cache for the latest generated strategy
|
||||||
calendarEvents: CalendarEvent[];
|
calendarEvents: CalendarEvent[];
|
||||||
gapAnalyses: ContentGapAnalysis[];
|
gapAnalyses: ContentGapAnalysis[];
|
||||||
aiRecommendations: AIRecommendation[];
|
aiRecommendations: AIRecommendation[];
|
||||||
@@ -182,6 +183,7 @@ interface ContentPlanningStore {
|
|||||||
updateStrategy: (id: string, updates: Partial<ContentStrategy>) => Promise<void>;
|
updateStrategy: (id: string, updates: Partial<ContentStrategy>) => Promise<void>;
|
||||||
deleteStrategy: (id: string) => Promise<void>;
|
deleteStrategy: (id: string) => Promise<void>;
|
||||||
setCurrentStrategy: (strategy: ContentStrategy | null) => void;
|
setCurrentStrategy: (strategy: ContentStrategy | null) => void;
|
||||||
|
setLatestGeneratedStrategy: (strategy: any | null) => void;
|
||||||
|
|
||||||
// Calendar actions
|
// Calendar actions
|
||||||
createEvent: (event: Omit<CalendarEvent, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
|
createEvent: (event: Omit<CalendarEvent, 'id' | 'created_at' | 'updated_at'>) => Promise<void>;
|
||||||
@@ -269,6 +271,7 @@ export const useContentPlanningStore = create<ContentPlanningStore>((set, get) =
|
|||||||
// Initial state
|
// Initial state
|
||||||
strategies: [],
|
strategies: [],
|
||||||
currentStrategy: null,
|
currentStrategy: null,
|
||||||
|
latestGeneratedStrategy: null,
|
||||||
calendarEvents: [],
|
calendarEvents: [],
|
||||||
gapAnalyses: [],
|
gapAnalyses: [],
|
||||||
aiRecommendations: [],
|
aiRecommendations: [],
|
||||||
@@ -345,6 +348,18 @@ export const useContentPlanningStore = create<ContentPlanningStore>((set, get) =
|
|||||||
},
|
},
|
||||||
|
|
||||||
setCurrentStrategy: (strategy) => set({ currentStrategy: strategy }),
|
setCurrentStrategy: (strategy) => set({ currentStrategy: strategy }),
|
||||||
|
setLatestGeneratedStrategy: (strategy) => {
|
||||||
|
console.log('🔧 Store: Setting latest generated strategy:', {
|
||||||
|
strategyId: strategy?.id || strategy?.strategy_id,
|
||||||
|
strategyName: strategy?.name || strategy?.strategy_name,
|
||||||
|
hasStrategicInsights: !!strategy?.strategic_insights,
|
||||||
|
hasCompetitiveAnalysis: !!strategy?.competitive_analysis,
|
||||||
|
hasPerformancePredictions: !!strategy?.performance_predictions,
|
||||||
|
hasImplementationRoadmap: !!strategy?.implementation_roadmap,
|
||||||
|
hasRiskAssessment: !!strategy?.risk_assessment
|
||||||
|
});
|
||||||
|
set({ latestGeneratedStrategy: strategy });
|
||||||
|
},
|
||||||
|
|
||||||
// Calendar actions
|
// Calendar actions
|
||||||
createEvent: async (event) => {
|
createEvent: async (event) => {
|
||||||
@@ -476,24 +491,17 @@ export const useContentPlanningStore = create<ContentPlanningStore>((set, get) =
|
|||||||
loadStrategies: async () => {
|
loadStrategies: async () => {
|
||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
try {
|
try {
|
||||||
console.log('🔍 Loading strategies from API...');
|
|
||||||
const strategies = await contentPlanningApi.getStrategiesSafe();
|
const strategies = await contentPlanningApi.getStrategiesSafe();
|
||||||
console.log('🔍 API response for strategies:', strategies);
|
|
||||||
console.log('🔍 Strategies type:', typeof strategies);
|
|
||||||
console.log('🔍 Is Array:', Array.isArray(strategies));
|
|
||||||
|
|
||||||
if (Array.isArray(strategies)) {
|
if (Array.isArray(strategies)) {
|
||||||
console.log('✅ Strategies loaded successfully (direct array):', strategies.length);
|
|
||||||
set({ strategies, loading: false });
|
set({ strategies, loading: false });
|
||||||
} else if (strategies && strategies.strategies && Array.isArray(strategies.strategies)) {
|
} else if (strategies && strategies.strategies && Array.isArray(strategies.strategies)) {
|
||||||
console.log('✅ Strategies found in response.strategies:', strategies.strategies.length);
|
|
||||||
set({ strategies: strategies.strategies, loading: false });
|
set({ strategies: strategies.strategies, loading: false });
|
||||||
} else {
|
} else {
|
||||||
console.log('❌ No strategies found in response');
|
|
||||||
set({ strategies: [], loading: false });
|
set({ strategies: [], loading: false });
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('❌ Error loading strategies:', error);
|
console.error('Error loading strategies:', error);
|
||||||
set({ error: error.message || 'Failed to load strategies', loading: false });
|
set({ error: error.message || 'Failed to load strategies', loading: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -521,7 +529,7 @@ export const useContentPlanningStore = create<ContentPlanningStore>((set, get) =
|
|||||||
loadAIInsights: async () => {
|
loadAIInsights: async () => {
|
||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const response = await contentPlanningApi.getAIAnalyticsSafe();
|
const response = await contentPlanningApi.getAIAnalyticsWithRetry();
|
||||||
|
|
||||||
// Validate response structure
|
// Validate response structure
|
||||||
if (!response || typeof response !== 'object') {
|
if (!response || typeof response !== 'object') {
|
||||||
@@ -559,6 +567,21 @@ export const useContentPlanningStore = create<ContentPlanningStore>((set, get) =
|
|||||||
set({ aiInsights: transformedInsights, loading: false });
|
set({ aiInsights: transformedInsights, loading: false });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error loading AI insights:', error);
|
console.error('Error loading AI insights:', error);
|
||||||
|
|
||||||
|
// Check if it's a rate limit error
|
||||||
|
if (error.message?.includes('rate limit') || error.message?.includes('429')) {
|
||||||
|
console.warn('⚠️ Rate limit hit for AI insights, using empty data');
|
||||||
|
set({ aiInsights: [], loading: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a network error
|
||||||
|
if (error.message?.includes('network') || error.message?.includes('connection')) {
|
||||||
|
console.warn('⚠️ Network error for AI insights, using empty data');
|
||||||
|
set({ aiInsights: [], loading: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
set({ error: error.message || 'Failed to load AI insights', loading: false, aiInsights: [] });
|
set({ error: error.message || 'Failed to load AI insights', loading: false, aiInsights: [] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -566,7 +589,7 @@ export const useContentPlanningStore = create<ContentPlanningStore>((set, get) =
|
|||||||
loadAIRecommendations: async () => {
|
loadAIRecommendations: async () => {
|
||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const response = await contentPlanningApi.getAIAnalyticsSafe();
|
const response = await contentPlanningApi.getAIAnalyticsWithRetry();
|
||||||
|
|
||||||
// Validate response structure
|
// Validate response structure
|
||||||
if (!response || typeof response !== 'object') {
|
if (!response || typeof response !== 'object') {
|
||||||
@@ -593,6 +616,21 @@ export const useContentPlanningStore = create<ContentPlanningStore>((set, get) =
|
|||||||
set({ aiRecommendations: transformedRecommendations, loading: false });
|
set({ aiRecommendations: transformedRecommendations, loading: false });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error loading AI recommendations:', error);
|
console.error('Error loading AI recommendations:', error);
|
||||||
|
|
||||||
|
// Check if it's a rate limit error
|
||||||
|
if (error.message?.includes('rate limit') || error.message?.includes('429')) {
|
||||||
|
console.warn('⚠️ Rate limit hit for AI recommendations, using empty data');
|
||||||
|
set({ aiRecommendations: [], loading: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a network error
|
||||||
|
if (error.message?.includes('network') || error.message?.includes('connection')) {
|
||||||
|
console.warn('⚠️ Network error for AI recommendations, using empty data');
|
||||||
|
set({ aiRecommendations: [], loading: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
set({ error: error.message || 'Failed to load AI recommendations', loading: false, aiRecommendations: [] });
|
set({ error: error.message || 'Failed to load AI recommendations', loading: false, aiRecommendations: [] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,78 @@ import { contentPlanningApi } from '../services/contentPlanningApi';
|
|||||||
// Global flag to prevent multiple simultaneous auto-population calls
|
// Global flag to prevent multiple simultaneous auto-population calls
|
||||||
let isAutoPopulating = false;
|
let isAutoPopulating = false;
|
||||||
|
|
||||||
|
// Helper function to detect generic placeholder values
|
||||||
|
const isGenericPlaceholder = (fieldId: string, value: any): boolean => {
|
||||||
|
if (!value) return false;
|
||||||
|
|
||||||
|
const stringValue = String(value).toLowerCase();
|
||||||
|
|
||||||
|
// Common generic placeholder patterns
|
||||||
|
const genericPatterns = [
|
||||||
|
'example.com',
|
||||||
|
'your website',
|
||||||
|
'your business',
|
||||||
|
'your industry',
|
||||||
|
'your target audience',
|
||||||
|
'your content',
|
||||||
|
'your goals',
|
||||||
|
'your objectives',
|
||||||
|
'your metrics',
|
||||||
|
'your budget',
|
||||||
|
'your team',
|
||||||
|
'your timeline',
|
||||||
|
'your competitors',
|
||||||
|
'your market',
|
||||||
|
'your brand',
|
||||||
|
'your voice',
|
||||||
|
'your tone',
|
||||||
|
'your strategy',
|
||||||
|
'your plan',
|
||||||
|
'your approach',
|
||||||
|
'your focus',
|
||||||
|
'your niche',
|
||||||
|
'your sector',
|
||||||
|
'your domain',
|
||||||
|
'your company',
|
||||||
|
'your organization',
|
||||||
|
'your brand',
|
||||||
|
'your product',
|
||||||
|
'your service',
|
||||||
|
'your offering',
|
||||||
|
'your solution',
|
||||||
|
'your value proposition',
|
||||||
|
'your unique selling point',
|
||||||
|
'your competitive advantage',
|
||||||
|
'your market position',
|
||||||
|
'your customer base',
|
||||||
|
'your user base',
|
||||||
|
'your audience',
|
||||||
|
'your customers',
|
||||||
|
'your clients',
|
||||||
|
'your users',
|
||||||
|
'your visitors',
|
||||||
|
'your readers',
|
||||||
|
'your followers',
|
||||||
|
'your subscribers',
|
||||||
|
'your leads',
|
||||||
|
'your prospects',
|
||||||
|
'your potential customers',
|
||||||
|
'your target market',
|
||||||
|
'your target segment',
|
||||||
|
'your target demographic',
|
||||||
|
'your target group',
|
||||||
|
'your target population',
|
||||||
|
'your target users',
|
||||||
|
'your target readers',
|
||||||
|
'your target audience',
|
||||||
|
'your target customers',
|
||||||
|
'your target clients'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if the value contains any generic placeholder patterns
|
||||||
|
return genericPatterns.some(pattern => stringValue.includes(pattern));
|
||||||
|
};
|
||||||
|
|
||||||
// Enhanced Strategy Types
|
// Enhanced Strategy Types
|
||||||
export interface EnhancedStrategy {
|
export interface EnhancedStrategy {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -638,7 +710,6 @@ export const useStrategyBuilderStore = create<StrategyBuilderStore>((set, get) =
|
|||||||
if (forceRefresh) {
|
if (forceRefresh) {
|
||||||
try {
|
try {
|
||||||
await contentPlanningApi.clearEnhancedCache(1);
|
await contentPlanningApi.clearEnhancedCache(1);
|
||||||
console.log('♻️ Cleared enhanced strategy cache for fresh onboarding data');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Cache clear failed (non-blocking):', e);
|
console.warn('Cache clear failed (non-blocking):', e);
|
||||||
}
|
}
|
||||||
@@ -646,16 +717,55 @@ export const useStrategyBuilderStore = create<StrategyBuilderStore>((set, get) =
|
|||||||
|
|
||||||
// Fetch onboarding data to auto-populate fields
|
// Fetch onboarding data to auto-populate fields
|
||||||
const response = await contentPlanningApi.getOnboardingData();
|
const response = await contentPlanningApi.getOnboardingData();
|
||||||
console.log('📡 Backend response:', response);
|
|
||||||
|
// Enhanced logging for autofill data
|
||||||
|
console.log('📊 Autofill Response Structure:', {
|
||||||
|
hasResponse: !!response,
|
||||||
|
responseKeys: response ? Object.keys(response) : [],
|
||||||
|
fieldsCount: response?.fields ? Object.keys(response.fields).length : 0,
|
||||||
|
sourcesCount: response?.sources ? Object.keys(response.sources).length : 0,
|
||||||
|
inputDataPointsCount: response?.input_data_points ? Object.keys(response.input_data_points).length : 0,
|
||||||
|
hasMeta: !!response?.meta
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate response structure
|
||||||
|
if (!response) {
|
||||||
|
throw new Error('Invalid response structure from backend');
|
||||||
|
}
|
||||||
|
|
||||||
// Extract field values and sources from the new backend format
|
// Extract field values and sources from the new backend format
|
||||||
const fields = response.data?.fields || {};
|
const fields = response.fields || {};
|
||||||
const sources = response.data?.sources || {};
|
const sources = response.sources || {};
|
||||||
const inputDataPoints = response.data?.input_data_points || {};
|
const inputDataPoints = response.input_data_points || {};
|
||||||
|
|
||||||
console.log('📋 Extracted fields:', fields);
|
// Log detailed field information
|
||||||
console.log('🔗 Data sources:', sources);
|
console.log('🎯 Autofill Field Details:', {
|
||||||
console.log('📝 Input data points:', inputDataPoints);
|
totalFields: Object.keys(fields).length,
|
||||||
|
fieldIds: Object.keys(fields),
|
||||||
|
sampleFieldData: Object.keys(fields).slice(0, 3).map(id => ({
|
||||||
|
id,
|
||||||
|
hasValue: !!fields[id]?.value,
|
||||||
|
hasPersonalization: !!fields[id]?.personalization_data,
|
||||||
|
hasConfidence: !!fields[id]?.confidence_score,
|
||||||
|
valueType: typeof fields[id]?.value
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate AI generation success
|
||||||
|
const meta = response.meta || {};
|
||||||
|
console.log('🤖 AI Generation Meta:', {
|
||||||
|
aiUsed: meta.ai_used,
|
||||||
|
aiOverridesCount: meta.ai_overrides_count,
|
||||||
|
error: meta.error,
|
||||||
|
processingTime: meta.processing_time
|
||||||
|
});
|
||||||
|
|
||||||
|
if (meta.ai_used === false || meta.ai_overrides_count === 0) {
|
||||||
|
console.log('❌ AI generation failed - no real AI values produced');
|
||||||
|
throw new Error(meta.error || 'AI generation failed to produce strategy fields. Please try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ AI generation successful:', Object.keys(fields).length, 'fields');
|
||||||
|
|
||||||
// Transform the fields object to extract values for formData
|
// Transform the fields object to extract values for formData
|
||||||
const fieldValues: Record<string, any> = {};
|
const fieldValues: Record<string, any> = {};
|
||||||
@@ -663,61 +773,77 @@ export const useStrategyBuilderStore = create<StrategyBuilderStore>((set, get) =
|
|||||||
const personalizationData: Record<string, any> = {};
|
const personalizationData: Record<string, any> = {};
|
||||||
const confidenceScores: Record<string, number> = {};
|
const confidenceScores: Record<string, number> = {};
|
||||||
|
|
||||||
// Check if fields is empty and provide fallback
|
// Check if fields is empty - no fallback to mock data
|
||||||
if (Object.keys(fields).length === 0) {
|
if (Object.keys(fields).length === 0) {
|
||||||
console.log('⚠️ No fields found in onboarding data, using default values');
|
console.log('❌ No fields found in onboarding data - AI generation may have failed');
|
||||||
|
console.log('🚫 No fallback to mock data - user must retry or provide manual input');
|
||||||
|
|
||||||
|
// Set error state instead of mock data
|
||||||
|
set({
|
||||||
|
loading: false,
|
||||||
|
error: 'AI generation failed to produce strategy fields. Please try again or provide manual input.',
|
||||||
|
autoPopulatedFields: {},
|
||||||
|
personalizationData: {},
|
||||||
|
dataSources: {},
|
||||||
|
inputDataPoints: {}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process actual fields from backend
|
||||||
|
let processedFields = 0;
|
||||||
|
let skippedFields = 0;
|
||||||
|
let fieldsWithPersonalization = 0;
|
||||||
|
let fieldsWithConfidence = 0;
|
||||||
|
|
||||||
// Set default values for strategy builder
|
|
||||||
const defaultFields: Record<string, any> = {
|
|
||||||
industry: 'Technology',
|
|
||||||
business_objectives: 'Increase brand awareness and drive sales',
|
|
||||||
target_metrics: { traffic: 10000, conversion_rate: 2.5 },
|
|
||||||
content_budget: 5000,
|
|
||||||
team_size: 3,
|
|
||||||
content_preferences: ['Blog posts', 'Social media', 'Email marketing'],
|
|
||||||
preferred_formats: ['Blog posts', 'Whitepapers', 'Videos'],
|
|
||||||
content_mix: { blog_posts: 40, whitepapers: 20, videos: 15, social_media: 25 }
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(defaultFields).forEach(fieldId => {
|
|
||||||
fieldValues[fieldId] = defaultFields[fieldId];
|
|
||||||
autoPopulatedFields[fieldId] = defaultFields[fieldId];
|
|
||||||
confidenceScores[fieldId] = 0.7; // Medium confidence for defaults
|
|
||||||
console.log(`✅ Set default value for ${fieldId}:`, defaultFields[fieldId]);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Process actual fields from backend
|
|
||||||
Object.keys(fields).forEach(fieldId => {
|
Object.keys(fields).forEach(fieldId => {
|
||||||
const fieldData = fields[fieldId];
|
const fieldData = fields[fieldId];
|
||||||
console.log(`🔍 Processing field ${fieldId}:`, fieldData);
|
|
||||||
|
|
||||||
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
|
if (fieldData && typeof fieldData === 'object' && 'value' in fieldData) {
|
||||||
fieldValues[fieldId] = fieldData.value;
|
// Validate that the value is not a generic placeholder
|
||||||
autoPopulatedFields[fieldId] = fieldData.value;
|
const value = fieldData.value;
|
||||||
|
const isGenericPlaceholderValue = isGenericPlaceholder(fieldId, value);
|
||||||
|
|
||||||
|
if (isGenericPlaceholderValue) {
|
||||||
|
console.log(`⚠️ Skipping ${fieldId} - contains generic placeholder value: "${value}"`);
|
||||||
|
skippedFields++;
|
||||||
|
return; // Skip this field
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldValues[fieldId] = value;
|
||||||
|
autoPopulatedFields[fieldId] = value;
|
||||||
|
processedFields++;
|
||||||
|
|
||||||
// Extract personalization data if available
|
// Extract personalization data if available
|
||||||
if (fieldData.personalization_data) {
|
if (fieldData.personalization_data) {
|
||||||
personalizationData[fieldId] = fieldData.personalization_data;
|
personalizationData[fieldId] = fieldData.personalization_data;
|
||||||
console.log(`🎯 Personalization data for ${fieldId}:`, fieldData.personalization_data);
|
fieldsWithPersonalization++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract confidence score if available
|
// Extract confidence score if available
|
||||||
if (fieldData.confidence_score) {
|
if (fieldData.confidence_score) {
|
||||||
confidenceScores[fieldId] = fieldData.confidence_score;
|
confidenceScores[fieldId] = fieldData.confidence_score;
|
||||||
console.log(`💯 Confidence score for ${fieldId}:`, fieldData.confidence_score);
|
fieldsWithConfidence++;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ Auto-populated ${fieldId}:`, fieldData.value);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(`❌ Skipping ${fieldId} - invalid data structure`);
|
console.log(`❌ Skipping ${fieldId} - invalid data structure:`, fieldData);
|
||||||
|
skippedFields++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
// Log field processing summary
|
||||||
|
console.log('📈 Field Processing Summary:', {
|
||||||
|
totalFields: Object.keys(fields).length,
|
||||||
|
processedFields,
|
||||||
|
skippedFields,
|
||||||
|
fieldsWithPersonalization,
|
||||||
|
fieldsWithConfidence,
|
||||||
|
averageConfidence: fieldsWithConfidence > 0
|
||||||
|
? (Object.values(confidenceScores).filter((score): score is number => typeof score === 'number').reduce((a, b) => a + b, 0) / fieldsWithConfidence).toFixed(1)
|
||||||
|
: 'N/A'
|
||||||
|
});
|
||||||
|
|
||||||
console.log('📝 Final field values:', fieldValues);
|
console.log('✅ Auto-population completed:', Object.keys(fieldValues).length, 'fields');
|
||||||
console.log('🔄 Final auto-populated fields:', autoPopulatedFields);
|
|
||||||
console.log('🎯 Personalization data:', personalizationData);
|
|
||||||
console.log('💯 Confidence scores:', confidenceScores);
|
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
autoPopulatedFields,
|
autoPopulatedFields,
|
||||||
@@ -728,85 +854,100 @@ export const useStrategyBuilderStore = create<StrategyBuilderStore>((set, get) =
|
|||||||
formData: { ...state.formData, ...fieldValues }
|
formData: { ...state.formData, ...fieldValues }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log('✅ Auto-population completed successfully');
|
// Log final state summary
|
||||||
} catch (error: any) {
|
console.log('🎉 Auto-population State Summary:', {
|
||||||
console.error('❌ Auto-population error:', error);
|
autoPopulatedFieldsCount: Object.keys(autoPopulatedFields).length,
|
||||||
const errorMessage = error.message || 'Failed to auto-populate from onboarding';
|
dataSourcesCount: Object.keys(sources).length,
|
||||||
set({
|
inputDataPointsCount: Object.keys(inputDataPoints).length,
|
||||||
error: errorMessage,
|
personalizationDataCount: Object.keys(personalizationData).length,
|
||||||
loading: false
|
confidenceScoresCount: Object.keys(confidenceScores).length,
|
||||||
|
formDataUpdated: Object.keys(fieldValues).length,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
// If it's a rate limit error, set a flag to prevent further attempts
|
console.log('✅ Auto-population completed successfully');
|
||||||
if (errorMessage.includes('Too many requests') || errorMessage.includes('No response from server')) {
|
|
||||||
set({ autoPopulationBlocked: true });
|
// Store the autofill completion time
|
||||||
}
|
sessionStorage.setItem('lastAutofillTime', new Date().toISOString());
|
||||||
} finally {
|
|
||||||
set({ loading: false });
|
} catch (error: any) {
|
||||||
isAutoPopulating = false; // Reset global flag
|
console.error('❌ Auto-population error:', error);
|
||||||
}
|
const errorMessage = error.message || 'Failed to auto-populate from onboarding';
|
||||||
},
|
set({
|
||||||
|
error: errorMessage,
|
||||||
updateAutoPopulatedField: (fieldId, value, source) => {
|
loading: false
|
||||||
set((state) => ({
|
});
|
||||||
autoPopulatedFields: { ...state.autoPopulatedFields, [fieldId]: value },
|
|
||||||
dataSources: { ...state.dataSources, [fieldId]: source }
|
// If it's a rate limit error, set a flag to prevent further attempts
|
||||||
}));
|
if (errorMessage.includes('Too many requests') || errorMessage.includes('No response from server')) {
|
||||||
},
|
set({ autoPopulationBlocked: true });
|
||||||
|
}
|
||||||
overrideAutoPopulatedField: (fieldId, value) => {
|
} finally {
|
||||||
set((state) => ({
|
set({ loading: false });
|
||||||
formData: { ...state.formData, [fieldId]: value },
|
isAutoPopulating = false; // Reset global flag
|
||||||
autoPopulatedFields: { ...state.autoPopulatedFields, [fieldId]: value }
|
}
|
||||||
}));
|
},
|
||||||
},
|
|
||||||
|
updateAutoPopulatedField: (fieldId: string, value: any, source: string) => {
|
||||||
// UI Actions
|
set((state) => ({
|
||||||
setLoading: (loading) => set({ loading }),
|
autoPopulatedFields: { ...state.autoPopulatedFields, [fieldId]: value },
|
||||||
setError: (error) => set({ error }),
|
dataSources: { ...state.dataSources, [fieldId]: source }
|
||||||
setSaving: (saving) => set({ saving }),
|
}));
|
||||||
|
},
|
||||||
// Completion Tracking
|
|
||||||
calculateCompletionPercentage: () => {
|
overrideAutoPopulatedField: (fieldId: string, value: any) => {
|
||||||
const formData = get().formData;
|
set((state) => ({
|
||||||
const requiredFields = STRATEGIC_INPUT_FIELDS.filter(field => field.required);
|
formData: { ...state.formData, [fieldId]: value },
|
||||||
const filledRequiredFields = requiredFields.filter(field => {
|
autoPopulatedFields: { ...state.autoPopulatedFields, [fieldId]: value }
|
||||||
const value = formData[field.id];
|
}));
|
||||||
return value && (Array.isArray(value) ? value.length > 0 : true);
|
},
|
||||||
});
|
|
||||||
|
// UI Actions
|
||||||
return requiredFields.length > 0 ? (filledRequiredFields.length / requiredFields.length) * 100 : 0;
|
setLoading: (loading: boolean) => set({ loading }),
|
||||||
},
|
setError: (error: string | null) => set({ error }),
|
||||||
|
setSaving: (saving: boolean) => set({ saving }),
|
||||||
getCompletionStats: () => {
|
|
||||||
const formData = get().formData;
|
// Completion Tracking
|
||||||
const totalFields = STRATEGIC_INPUT_FIELDS.length;
|
calculateCompletionPercentage: () => {
|
||||||
const filledFields = STRATEGIC_INPUT_FIELDS.filter(field => {
|
const formData = get().formData;
|
||||||
const value = formData[field.id];
|
const requiredFields = STRATEGIC_INPUT_FIELDS.filter(field => field.required);
|
||||||
return value && (Array.isArray(value) ? value.length > 0 : true);
|
const filledRequiredFields = requiredFields.filter(field => {
|
||||||
}).length;
|
const value = formData[field.id];
|
||||||
|
return value && (Array.isArray(value) ? value.length > 0 : true);
|
||||||
const completionPercentage = totalFields > 0 ? (filledFields / totalFields) * 100 : 0;
|
});
|
||||||
|
|
||||||
// Calculate completion by category
|
return requiredFields.length > 0 ? (filledRequiredFields.length / requiredFields.length) * 100 : 0;
|
||||||
const categoryCompletion: Record<string, number> = {};
|
},
|
||||||
const categories = Array.from(new Set(STRATEGIC_INPUT_FIELDS.map(field => field.category)));
|
|
||||||
|
getCompletionStats: () => {
|
||||||
categories.forEach(category => {
|
const formData = get().formData;
|
||||||
const categoryFields = STRATEGIC_INPUT_FIELDS.filter(field => field.category === category);
|
const totalFields = STRATEGIC_INPUT_FIELDS.length;
|
||||||
const filledCategoryFields = categoryFields.filter(field => {
|
const filledFields = STRATEGIC_INPUT_FIELDS.filter(field => {
|
||||||
const value = formData[field.id];
|
const value = formData[field.id];
|
||||||
return value && (Array.isArray(value) ? value.length > 0 : true);
|
return value && (Array.isArray(value) ? value.length > 0 : true);
|
||||||
}).length;
|
}).length;
|
||||||
|
|
||||||
categoryCompletion[category] = categoryFields.length > 0 ? (filledCategoryFields / categoryFields.length) * 100 : 0;
|
const completionPercentage = totalFields > 0 ? (filledFields / totalFields) * 100 : 0;
|
||||||
});
|
|
||||||
|
// Calculate completion by category
|
||||||
return {
|
const categoryCompletion: Record<string, number> = {};
|
||||||
total_fields: totalFields,
|
const categories = Array.from(new Set(STRATEGIC_INPUT_FIELDS.map(field => field.category)));
|
||||||
filled_fields: filledFields,
|
|
||||||
completion_percentage: completionPercentage,
|
categories.forEach(category => {
|
||||||
category_completion: categoryCompletion
|
const categoryFields = STRATEGIC_INPUT_FIELDS.filter(field => field.category === category);
|
||||||
};
|
const filledCategoryFields = categoryFields.filter(field => {
|
||||||
}
|
const value = formData[field.id];
|
||||||
}));
|
return value && (Array.isArray(value) ? value.length > 0 : true);
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
categoryCompletion[category] = categoryFields.length > 0 ? (filledCategoryFields / categoryFields.length) * 100 : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
total_fields: totalFields,
|
||||||
|
filled_fields: filledFields,
|
||||||
|
completion_percentage: completionPercentage,
|
||||||
|
category_completion: categoryCompletion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|||||||
@@ -171,7 +171,24 @@ export const useStrategyReviewStore = create<ReviewState>()(
|
|||||||
|
|
||||||
// Start review process
|
// Start review process
|
||||||
startReviewProcess: () => {
|
startReviewProcess: () => {
|
||||||
set({ reviewProcessStarted: true });
|
console.log('🔧 Starting review process - resetting all reviews first');
|
||||||
|
// Reset all reviews when starting a new review process
|
||||||
|
const { components } = get();
|
||||||
|
const resetComponents = components.map(comp => ({
|
||||||
|
...comp,
|
||||||
|
status: 'not_reviewed' as ReviewStatus,
|
||||||
|
reviewedAt: undefined,
|
||||||
|
reviewedBy: undefined,
|
||||||
|
notes: undefined
|
||||||
|
}));
|
||||||
|
|
||||||
|
set({
|
||||||
|
reviewProcessStarted: true,
|
||||||
|
components: resetComponents,
|
||||||
|
reviewProgress: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('🔧 Review process started with reset components');
|
||||||
},
|
},
|
||||||
|
|
||||||
// Update review progress
|
// Update review progress
|
||||||
@@ -181,7 +198,6 @@ export const useStrategyReviewStore = create<ReviewState>()(
|
|||||||
const totalCount = components.length;
|
const totalCount = components.length;
|
||||||
const progress = totalCount > 0 ? (reviewedCount / totalCount) * 100 : 0;
|
const progress = totalCount > 0 ? (reviewedCount / totalCount) * 100 : 0;
|
||||||
|
|
||||||
console.log('🔧 Updating progress:', { reviewedCount, totalCount, progress, components: components.map(c => ({ id: c.id, status: c.status })) });
|
|
||||||
set({ reviewProgress: progress });
|
set({ reviewProgress: progress });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user