ALwrity Version 0.5.0 (Fastapi + React )

This commit is contained in:
ajaysi
2025-08-06 12:48:02 +05:30
parent f28a919caa
commit 32f97fa6b3
476 changed files with 115544 additions and 28747 deletions

View File

@@ -0,0 +1,320 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Tabs,
Tab,
Typography,
Container,
AppBar,
Toolbar,
IconButton,
Alert,
Drawer,
Button,
Badge
} from '@mui/material';
import {
Psychology as StrategyIcon,
CalendarToday as CalendarIcon,
Analytics as AnalyticsIcon,
Search as SearchIcon,
Lightbulb as AIInsightsIcon,
Close as CloseIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
import ContentStrategyTab from './tabs/ContentStrategyTab';
import CalendarTab from './tabs/CalendarTab';
import AnalyticsTab from './tabs/AnalyticsTab';
import GapAnalysisTab from './tabs/GapAnalysisTab';
import AIInsightsPanel from './components/AIInsightsPanel';
import ServiceStatusPanel from './components/ServiceStatusPanel';
import ProgressIndicator from './components/ProgressIndicator';
import { useContentPlanningStore } from '../../stores/contentPlanningStore';
import {
contentPlanningOrchestrator,
ServiceStatus,
DashboardData
} from '../../services/contentPlanningOrchestrator';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`content-planning-tabpanel-${index}`}
aria-labelledby={`content-planning-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
{children}
</Box>
)}
</div>
);
}
function a11yProps(index: number) {
return {
id: `content-planning-tab-${index}`,
'aria-controls': `content-planning-tabpanel-${index}`,
};
}
const ContentPlanningDashboard: React.FC = () => {
const [activeTab, setActiveTab] = useState(0);
const [serviceStatuses, setServiceStatuses] = useState<ServiceStatus[]>([]);
const [dashboardData, setDashboardData] = useState<DashboardData>({
strategies: [],
gapAnalyses: [],
aiInsights: [],
aiRecommendations: [],
calendarEvents: [],
healthStatus: {
backend: false,
database: false,
aiServices: false
}
});
const [statusPanelExpanded, setStatusPanelExpanded] = useState(false);
const [progressExpanded, setProgressExpanded] = useState(true);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [aiInsightsDrawerOpen, setAiInsightsDrawerOpen] = useState(false);
const {
updateStrategies,
updateCalendarEvents,
updateGapAnalyses,
updateAIInsights
} = useContentPlanningStore();
// Initialize orchestrator callbacks
useEffect(() => {
contentPlanningOrchestrator.setProgressCallback((statuses) => {
setServiceStatuses(statuses);
});
contentPlanningOrchestrator.setDataUpdateCallback((data) => {
setDashboardData(prev => ({ ...prev, ...data }));
// Update store with new data
if (data.strategies) updateStrategies(data.strategies);
if (data.calendarEvents) updateCalendarEvents(data.calendarEvents);
if (data.gapAnalyses) updateGapAnalyses(data.gapAnalyses);
if (data.aiInsights || data.aiRecommendations) {
updateAIInsights({
insights: data.aiInsights || [],
recommendations: data.aiRecommendations || []
});
}
});
}, [updateStrategies, updateCalendarEvents, updateGapAnalyses, updateAIInsights]);
// Load dashboard data using orchestrator
useEffect(() => {
const loadDashboardData = async () => {
try {
setLoading(true);
setError(null);
await contentPlanningOrchestrator.loadDashboardData();
} catch (error: any) {
console.error('Failed to load dashboard data:', error);
setError(error.message || 'Failed to load dashboard data');
} finally {
setLoading(false);
}
};
// Wrap in try-catch to handle any unexpected errors
try {
loadDashboardData();
} catch (error: any) {
console.error('Unexpected error in dashboard:', error);
setError('An unexpected error occurred while loading the dashboard');
setLoading(false);
}
}, []);
const handleRefreshService = (serviceName: string) => {
contentPlanningOrchestrator.refreshService(serviceName);
};
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setActiveTab(newValue);
};
const getOverallHealthStatus = () => {
const { healthStatus } = dashboardData;
if (healthStatus.backend && healthStatus.database && healthStatus.aiServices) {
return { status: 'success', text: 'Connected' };
} else if (healthStatus.backend && healthStatus.database) {
return { status: 'warning', text: 'Connected API & DB' };
} else {
return { status: 'error', text: 'Disconnected' };
}
};
const overallHealth = getOverallHealthStatus();
const tabs = [
{ label: 'CONTENT STRATEGY', icon: <StrategyIcon />, component: <ContentStrategyTab /> },
{ label: 'CALENDAR', icon: <CalendarIcon />, component: <CalendarTab /> },
{ label: 'ANALYTICS', icon: <AnalyticsIcon />, component: <AnalyticsTab /> },
{ label: 'GAP ANALYSIS', icon: <SearchIcon />, component: <GapAnalysisTab /> }
];
const totalAIItems = (dashboardData.aiInsights?.length || 0) + (dashboardData.aiRecommendations?.length || 0);
return (
<Container maxWidth={false} sx={{ height: '100vh', p: 0 }}>
<AppBar position="static" color="default" elevation={1}>
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
Content Planning Dashboard
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<ServiceStatusPanel
serviceStatuses={serviceStatuses}
onRefreshService={handleRefreshService}
expanded={statusPanelExpanded}
onToggleExpanded={() => setStatusPanelExpanded(!statusPanelExpanded)}
/>
{/* AI Insights Button with Badge */}
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Button
variant="outlined"
startIcon={<AIInsightsIcon />}
onClick={() => setAiInsightsDrawerOpen(true)}
sx={{
borderRadius: 2,
textTransform: 'none',
fontWeight: 600,
borderColor: 'primary.main',
color: 'primary.main',
'&:hover': {
borderColor: 'primary.dark',
backgroundColor: 'primary.50'
}
}}
>
<Badge badgeContent={totalAIItems} color="primary" sx={{ mr: 1 }}>
AI Insights
</Badge>
</Button>
</motion.div>
</Box>
</Toolbar>
</AppBar>
{error && (
<Alert severity="error" sx={{ m: 2 }}>
{error}
</Alert>
)}
{/* Progress Indicator */}
{loading && (
<Box sx={{ m: 2 }}>
<ProgressIndicator
serviceStatuses={serviceStatuses}
onRefreshService={handleRefreshService}
expanded={progressExpanded}
onToggleExpanded={() => setProgressExpanded(!progressExpanded)}
/>
</Box>
)}
<Box sx={{ display: 'flex', height: 'calc(100vh - 64px)' }}>
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={activeTab}
onChange={handleTabChange}
aria-label="content planning tabs"
sx={{ px: 2 }}
>
{tabs.map((tab, index) => (
<Tab
key={index}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{tab.icon}
{tab.label}
</Box>
}
{...a11yProps(index)}
/>
))}
</Tabs>
</Box>
{tabs.map((tab, index) => (
<TabPanel key={index} value={activeTab} index={index}>
{tab.component}
</TabPanel>
))}
</Box>
</Box>
{/* AI Insights Drawer */}
<Drawer
anchor="right"
open={aiInsightsDrawerOpen}
onClose={() => setAiInsightsDrawerOpen(false)}
PaperProps={{
sx: {
width: 400,
height: '100%',
backgroundColor: 'background.paper',
borderLeft: '1px solid',
borderColor: 'divider'
}
}}
>
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center' }}>
<AIInsightsIcon sx={{ mr: 1 }} />
AI Insights
</Typography>
<IconButton
onClick={() => setAiInsightsDrawerOpen(false)}
size="small"
>
<CloseIcon />
</IconButton>
</Box>
</Box>
<Box sx={{ flex: 1, overflow: 'auto' }}>
<AnimatePresence>
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
>
<AIInsightsPanel />
</motion.div>
</AnimatePresence>
</Box>
</Drawer>
</Container>
);
};
export default ContentPlanningDashboard;

View File

@@ -0,0 +1,475 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Chip,
IconButton,
List,
ListItem,
ListItemText,
ListItemIcon,
Divider,
Alert,
CircularProgress
} from '@mui/material';
import {
Lightbulb as LightbulbIcon,
Refresh as RefreshIcon,
ExpandMore as ExpandMoreIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
TrendingUp as TrendingUpIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
const AIInsightsPanel: React.FC = () => {
const {
aiInsights,
aiRecommendations,
loading,
error,
loadAIInsights,
loadAIRecommendations
} = useContentPlanningStore();
const [expandedInsights, setExpandedInsights] = useState<Set<string>>(new Set());
const [dataLoading, setDataLoading] = useState(false);
useEffect(() => {
loadAIData();
}, []);
const loadAIData = async () => {
try {
setDataLoading(true);
// Load AI insights and recommendations
await Promise.all([
loadAIInsights(),
loadAIRecommendations()
]);
} catch (error) {
console.error('Error loading AI data:', error);
} finally {
setDataLoading(false);
}
};
const handleRefresh = async () => {
await loadAIData();
};
const handleForceRefresh = async () => {
try {
setDataLoading(true);
// Force refresh AI insights and recommendations
await Promise.all([
contentPlanningApi.getAIAnalyticsWithRefresh(undefined, true), // Force refresh
contentPlanningApi.getGapAnalysesWithRefresh(undefined, true) // Force refresh
]);
// Reload data from store
await Promise.all([
loadAIInsights(),
loadAIRecommendations()
]);
} catch (error) {
console.error('Error force refreshing AI data:', error);
} finally {
setDataLoading(false);
}
};
const toggleInsightExpansion = (insightId: string) => {
const newExpanded = new Set(expandedInsights);
if (newExpanded.has(insightId)) {
newExpanded.delete(insightId);
} else {
newExpanded.add(insightId);
}
setExpandedInsights(newExpanded);
};
const getInsightIcon = (type: string) => {
switch (type) {
case 'performance':
return <TrendingUpIcon color="success" />;
case 'opportunity':
return <LightbulbIcon color="primary" />;
case 'warning':
return <WarningIcon color="warning" />;
case 'trend':
return <AssessmentIcon color="info" />;
default:
return <CheckCircleIcon color="success" />;
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high':
return 'error';
case 'medium':
return 'warning';
case 'low':
return 'success';
default:
return 'default';
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'performance':
return 'success';
case 'opportunity':
return 'primary';
case 'warning':
return 'warning';
case 'trend':
return 'info';
default:
return 'default';
}
};
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.3
}
}
};
const cardVariants = {
initial: { scale: 1 },
hover: {
scale: 1.02,
transition: { duration: 0.2 }
},
tap: { scale: 0.98 }
};
return (
<Box sx={{ p: 2, height: '100%', overflowY: 'auto' }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center' }}>
<LightbulbIcon sx={{ mr: 1 }} />
AI Insights
</Typography>
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<IconButton
onClick={handleRefresh}
disabled={dataLoading}
size="small"
>
<RefreshIcon />
</IconButton>
</motion.div>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* AI Insights */}
{aiInsights && aiInsights.length > 0 && (
<motion.div variants={itemVariants}>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
Recent Insights ({aiInsights.length})
</Typography>
<AnimatePresence>
{aiInsights.map((insight, index) => (
<motion.div
key={insight.id}
variants={itemVariants}
initial="hidden"
animate="visible"
exit="hidden"
custom={index}
>
<motion.div
variants={cardVariants}
initial="initial"
whileHover="hover"
whileTap="tap"
>
<Card
sx={{
mb: 2,
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
'&:hover': {
boxShadow: 3,
borderColor: 'primary.main'
}
}}
onClick={() => toggleInsightExpansion(insight.id)}
>
<CardContent sx={{ py: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
<ListItemIcon sx={{ minWidth: 40 }}>
{getInsightIcon(insight.type)}
</ListItemIcon>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle2" gutterBottom>
{insight.title}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 1 }}>
<Chip
label={insight.type}
color={getTypeColor(insight.type)}
size="small"
/>
<Chip
label={insight.priority}
color={getPriorityColor(insight.priority)}
size="small"
/>
</Box>
<Typography variant="caption" color="text.secondary">
{new Date(insight.created_at).toLocaleDateString()}
</Typography>
</Box>
</Box>
<motion.div
animate={{ rotate: expandedInsights.has(insight.id) ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<IconButton size="small">
<ExpandMoreIcon />
</IconButton>
</motion.div>
</Box>
<AnimatePresence>
{expandedInsights.has(insight.id) && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<Divider sx={{ my: 1 }} />
<Typography variant="body2" color="text.secondary">
{insight.description}
</Typography>
</motion.div>
)}
</AnimatePresence>
</CardContent>
</Card>
</motion.div>
</motion.div>
))}
</AnimatePresence>
</Box>
</motion.div>
)}
{/* AI Recommendations */}
{aiRecommendations && aiRecommendations.length > 0 && (
<motion.div variants={itemVariants}>
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle2" gutterBottom>
AI Recommendations ({aiRecommendations.length})
</Typography>
<AnimatePresence>
{aiRecommendations.map((recommendation, index) => (
<motion.div
key={recommendation.id}
variants={itemVariants}
initial="hidden"
animate="visible"
exit="hidden"
custom={index}
>
<motion.div
variants={cardVariants}
initial="initial"
whileHover="hover"
whileTap="tap"
>
<Card
sx={{
mb: 2,
cursor: 'pointer',
transition: 'all 0.2s ease-in-out',
'&:hover': {
boxShadow: 3,
borderColor: 'primary.main'
}
}}
onClick={() => toggleInsightExpansion(recommendation.id)}
>
<CardContent sx={{ py: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Box sx={{ display: 'flex', alignItems: 'center', flex: 1 }}>
<ListItemIcon sx={{ minWidth: 40 }}>
<AssessmentIcon color="primary" />
</ListItemIcon>
<Box sx={{ flex: 1 }}>
<Typography variant="subtitle2" gutterBottom>
{recommendation.title}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mb: 1 }}>
<Chip
label={recommendation.type}
color="primary"
size="small"
/>
<Chip
label={`${(recommendation.confidence * 100).toFixed(0)}% confidence`}
color="success"
size="small"
/>
</Box>
<Typography variant="caption" color="text.secondary">
Status: {recommendation.status}
</Typography>
</Box>
</Box>
<motion.div
animate={{ rotate: expandedInsights.has(recommendation.id) ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<IconButton size="small">
<ExpandMoreIcon />
</IconButton>
</motion.div>
</Box>
<AnimatePresence>
{expandedInsights.has(recommendation.id) && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
>
<Divider sx={{ my: 1 }} />
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{recommendation.description}
</Typography>
{recommendation.reasoning && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
<strong>Reasoning:</strong> {recommendation.reasoning}
</Typography>
)}
{recommendation.action_items && recommendation.action_items.length > 0 && (
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
<strong>Action Items:</strong>
</Typography>
<List dense>
{recommendation.action_items.map((action, actionIndex) => (
<motion.div
key={actionIndex}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: actionIndex * 0.1 }}
>
<ListItem sx={{ py: 0 }}>
<ListItemIcon sx={{ minWidth: 30 }}>
<CheckCircleIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={action}
primaryTypographyProps={{ variant: 'body2' }}
/>
</ListItem>
</motion.div>
))}
</List>
</Box>
)}
</motion.div>
)}
</AnimatePresence>
</CardContent>
</Card>
</motion.div>
</motion.div>
))}
</AnimatePresence>
</Box>
</motion.div>
)}
{/* No Data State */}
{(!aiInsights || aiInsights.length === 0) && (!aiRecommendations || aiRecommendations.length === 0) && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Box sx={{ textAlign: 'center', py: 3 }}>
<motion.div
animate={{
scale: [1, 1.1, 1],
rotate: [0, 5, -5, 0]
}}
transition={{
duration: 2,
repeat: Infinity,
repeatType: "reverse"
}}
>
<LightbulbIcon sx={{ fontSize: 48, color: 'text.secondary', mb: 2 }} />
</motion.div>
<Typography variant="body2" color="text.secondary">
No AI insights available yet.
</Typography>
<Typography variant="caption" color="text.secondary">
Run content analysis to generate insights.
</Typography>
</Box>
</motion.div>
)}
</motion.div>
)}
</Box>
);
};
export default AIInsightsPanel;

View File

@@ -0,0 +1,210 @@
import React from 'react';
import {
Box,
Typography,
Card,
CardContent,
Button,
Chip,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider,
LinearProgress,
Alert,
IconButton,
Collapse
} from '@mui/material';
import {
AutoAwesome as AutoAwesomeIcon,
Lightbulb as LightbulbIcon,
TrendingUp as TrendingUpIcon,
Psychology as PsychologyIcon,
Analytics as AnalyticsIcon,
CalendarToday as CalendarIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Refresh as RefreshIcon
} from '@mui/icons-material';
interface AIRecommendationsPanelProps {
aiGenerating: boolean;
onGenerateRecommendations: () => void;
}
const AIRecommendationsPanel: React.FC<AIRecommendationsPanelProps> = ({
aiGenerating,
onGenerateRecommendations
}) => {
const [expanded, setExpanded] = React.useState(true);
// Mock AI recommendations data (this would come from the store)
const aiRecommendations = [
{
id: '1',
type: 'comprehensive_strategy',
title: 'Content Strategy Optimization',
description: 'Based on your business objectives, we recommend focusing on thought leadership content to establish authority in your industry.',
confidence: 0.85,
category: 'Strategy',
icon: <PsychologyIcon />
},
{
id: '2',
type: 'audience_intelligence',
title: 'Audience Targeting',
description: 'Your audience prefers video content and technical deep-dives. Consider increasing video production by 40%.',
confidence: 0.78,
category: 'Audience',
icon: <TrendingUpIcon />
},
{
id: '3',
type: 'competitive_intelligence',
title: 'Competitive Advantage',
description: 'Your competitors are weak in technical content. This presents an opportunity to differentiate through detailed tutorials.',
confidence: 0.92,
category: 'Competition',
icon: <LightbulbIcon />
},
{
id: '4',
type: 'performance_optimization',
title: 'Performance Improvement',
description: 'Your current content frequency is optimal. Focus on quality over quantity to improve engagement rates.',
confidence: 0.76,
category: 'Performance',
icon: <AnalyticsIcon />
},
{
id: '5',
type: 'content_calendar_optimization',
title: 'Publishing Schedule',
description: 'Publish technical content on Tuesdays and Thursdays when your audience is most engaged.',
confidence: 0.81,
category: 'Calendar',
icon: <CalendarIcon />
}
];
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.8) return 'success';
if (confidence >= 0.6) return 'warning';
return 'error';
};
const getConfidenceLabel = (confidence: number) => {
if (confidence >= 0.8) return 'High';
if (confidence >= 0.6) return 'Medium';
return 'Low';
};
return (
<Card variant="outlined">
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<AutoAwesomeIcon color="primary" />
<Typography variant="h6">
AI Recommendations
</Typography>
<IconButton
size="small"
onClick={() => setExpanded(!expanded)}
>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</Box>
<Collapse in={expanded}>
{/* Generate Button */}
<Box sx={{ mb: 2 }}>
<Button
variant="contained"
fullWidth
startIcon={aiGenerating ? undefined : <AutoAwesomeIcon />}
onClick={onGenerateRecommendations}
disabled={aiGenerating}
sx={{ mb: 1 }}
>
{aiGenerating ? 'Generating...' : 'Generate AI Insights'}
</Button>
{aiGenerating && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<LinearProgress sx={{ flexGrow: 1 }} />
<Typography variant="caption" color="text.secondary">
Analyzing...
</Typography>
</Box>
)}
</Box>
{/* AI Recommendations List */}
{aiRecommendations.length > 0 && (
<Box>
<Typography variant="subtitle2" gutterBottom>
Recent Recommendations
</Typography>
<List dense>
{aiRecommendations.map((recommendation, index) => (
<React.Fragment key={recommendation.id}>
<ListItem sx={{ px: 0 }}>
<ListItemIcon sx={{ minWidth: 40 }}>
{recommendation.icon}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" fontWeight="medium">
{recommendation.title}
</Typography>
<Chip
label={recommendation.category}
size="small"
variant="outlined"
/>
<Chip
label={`${Math.round(recommendation.confidence * 100)}% confidence`}
size="small"
color={getConfidenceColor(recommendation.confidence)}
/>
</Box>
}
secondary={
<Typography variant="body2" color="text.secondary">
{recommendation.description}
</Typography>
}
/>
</ListItem>
{index < aiRecommendations.length - 1 && <Divider />}
</React.Fragment>
))}
</List>
</Box>
)}
{/* No Recommendations State */}
{aiRecommendations.length === 0 && !aiGenerating && (
<Alert severity="info" sx={{ mt: 2 }}>
<Typography variant="body2">
Generate AI recommendations to get personalized insights for your content strategy.
</Typography>
</Alert>
)}
{/* AI Status */}
<Box sx={{ mt: 2, p: 1, bgcolor: 'background.default', borderRadius: 1 }}>
<Typography variant="caption" color="text.secondary">
AI analyzes your inputs to provide personalized recommendations for your content strategy.
</Typography>
</Box>
</Collapse>
</CardContent>
</Card>
);
};
export default AIRecommendationsPanel;

View File

@@ -0,0 +1,69 @@
import React, { useState } from 'react';
import {
Box,
Paper,
Typography,
Button,
Alert,
CircularProgress
} from '@mui/material';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
const AITestComponent: React.FC = () => {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const testAIConnection = async () => {
setLoading(true);
setError(null);
setResult(null);
try {
const response = await contentPlanningApi.getAIAnalyticsSafe();
setResult(response);
console.log('AI Test Response:', response);
} catch (err: any) {
setError(err.message || 'Failed to connect to AI service');
console.error('AI Test Error:', err);
} finally {
setLoading(false);
}
};
return (
<Paper sx={{ p: 2, m: 2 }}>
<Typography variant="h6" gutterBottom>
AI Integration Test
</Typography>
<Button
variant="contained"
onClick={testAIConnection}
disabled={loading}
sx={{ mb: 2 }}
>
{loading ? <CircularProgress size={20} /> : 'Test AI Connection'}
</Button>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{result && (
<Box>
<Typography variant="subtitle2" gutterBottom>
AI Test Results:
</Typography>
<pre style={{ fontSize: '12px', overflow: 'auto' }}>
{JSON.stringify(result, null, 2)}
</pre>
</Box>
)}
</Paper>
);
};
export default AITestComponent;

View File

@@ -0,0 +1,157 @@
import React from 'react';
import {
Box,
Typography,
LinearProgress,
Chip,
Card,
CardContent,
Grid,
Tooltip,
IconButton
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
TrendingUp as TrendingUpIcon,
Info as InfoIcon
} from '@mui/icons-material';
interface CompletionStats {
total_fields: number;
filled_fields: number;
completion_percentage: number;
category_completion: Record<string, number>;
}
interface CompletionTrackerProps {
completionPercentage: number;
completionStats: CompletionStats;
}
const CompletionTracker: React.FC<CompletionTrackerProps> = ({
completionPercentage,
completionStats
}) => {
const getCategoryColor = (percentage: number) => {
if (percentage >= 80) return 'success';
if (percentage >= 60) return 'warning';
return 'error';
};
const getCategoryIcon = (category: string) => {
const icons = {
business_context: '🏢',
audience_intelligence: '👥',
competitive_intelligence: '📈',
content_strategy: '📝',
performance_analytics: '📊'
};
return icons[category as keyof typeof icons] || '📋';
};
const getCategoryLabel = (category: string) => {
const labels = {
business_context: 'Business Context',
audience_intelligence: 'Audience Intelligence',
competitive_intelligence: 'Competitive Intelligence',
content_strategy: 'Content Strategy',
performance_analytics: 'Performance & Analytics'
};
return labels[category as keyof typeof labels] || category;
};
const getCompletionStatus = (percentage: number) => {
if (percentage >= 90) return { status: 'Excellent', color: 'success' as const };
if (percentage >= 70) return { status: 'Good', color: 'primary' as const };
if (percentage >= 50) return { status: 'Fair', color: 'warning' as const };
return { status: 'Needs Work', color: 'error' as const };
};
const status = getCompletionStatus(completionPercentage);
return (
<Card variant="outlined" sx={{ minWidth: 300 }}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<TrendingUpIcon color="primary" />
<Typography variant="h6">
Strategy Progress
</Typography>
<Chip
label={status.status}
color={status.color}
size="small"
/>
</Box>
{/* Overall Progress */}
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" color="text.secondary">
Overall Completion
</Typography>
<Typography variant="body2" color="text.secondary">
{Math.round(completionPercentage)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={completionPercentage}
sx={{ height: 8, borderRadius: 4 }}
color={status.color}
/>
<Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: 'block' }}>
{completionStats.filled_fields} of {completionStats.total_fields} fields completed
</Typography>
</Box>
{/* Category Breakdown */}
<Box>
<Typography variant="subtitle2" gutterBottom>
Category Progress
</Typography>
<Grid container spacing={1}>
{Object.entries(completionStats.category_completion).map(([category, percentage]) => (
<Grid item xs={12} key={category}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Typography variant="body2" sx={{ minWidth: 20 }}>
{getCategoryIcon(category)}
</Typography>
<Typography variant="body2" sx={{ flexGrow: 1, fontSize: '0.875rem' }}>
{getCategoryLabel(category)}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ minWidth: 40 }}>
{Math.round(percentage)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={percentage}
color={getCategoryColor(percentage)}
sx={{ height: 4, borderRadius: 2 }}
/>
</Grid>
))}
</Grid>
</Box>
{/* Progress Insights */}
{completionPercentage > 0 && (
<Box sx={{ mt: 2, p: 1, bgcolor: 'background.default', borderRadius: 1 }}>
<Typography variant="caption" color="text.secondary">
{completionPercentage >= 80 ? (
'🎉 Great progress! You\'re ready to generate AI recommendations.'
) : completionPercentage >= 50 ? (
'📈 Good progress! Consider filling more fields for better AI insights.'
) : (
'💡 Start with the Business Context section to build a strong foundation.'
)}
</Typography>
</Box>
)}
</CardContent>
</Card>
);
};
export default CompletionTracker;

View File

@@ -0,0 +1,235 @@
import React from 'react';
import {
Box,
Typography,
Card,
CardContent,
Chip,
List,
ListItem,
ListItemIcon,
ListItemText,
Divider,
LinearProgress,
Alert,
IconButton,
Collapse,
Tooltip
} from '@mui/material';
import {
DataUsage as DataUsageIcon,
AutoAwesome as AutoAwesomeIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
Info as InfoIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Refresh as RefreshIcon
} from '@mui/icons-material';
interface DataSourceTransparencyProps {
autoPopulatedFields: Record<string, any>;
dataSources: Record<string, string>;
}
const DataSourceTransparency: React.FC<DataSourceTransparencyProps> = ({
autoPopulatedFields,
dataSources
}) => {
const [expanded, setExpanded] = React.useState(true);
const getDataSourceIcon = (source: string) => {
const icons = {
website_analysis: '🌐',
research_preferences: '🔍',
api_keys: '🔑',
onboarding_session: '📋'
};
return icons[source as keyof typeof icons] || '📊';
};
const getDataSourceLabel = (source: string) => {
const labels = {
website_analysis: 'Website Analysis',
research_preferences: 'Research Preferences',
api_keys: 'API Configuration',
onboarding_session: 'Onboarding Session'
};
return labels[source as keyof typeof labels] || source;
};
const getDataQualityScore = (source: string) => {
// Mock quality scores based on data source
const scores = {
website_analysis: 0.85,
research_preferences: 0.92,
api_keys: 0.78,
onboarding_session: 0.88
};
return scores[source as keyof typeof scores] || 0.7;
};
const getDataQualityColor = (score: number) => {
if (score >= 0.8) return 'success';
if (score >= 0.6) return 'warning';
return 'error';
};
const getDataQualityLabel = (score: number) => {
if (score >= 0.8) return 'High Quality';
if (score >= 0.6) return 'Medium Quality';
return 'Low Quality';
};
const autoPopulatedFieldsList = Object.entries(autoPopulatedFields).map(([fieldId, value]) => ({
fieldId,
value,
source: dataSources[fieldId] || 'unknown',
qualityScore: getDataQualityScore(dataSources[fieldId] || 'unknown')
}));
const sourceSummary = Object.entries(dataSources).reduce((acc, [fieldId, source]) => {
if (!acc[source]) {
acc[source] = [];
}
acc[source].push(fieldId);
return acc;
}, {} as Record<string, string[]>);
if (Object.keys(autoPopulatedFields).length === 0) {
return null;
}
return (
<Card variant="outlined">
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<DataUsageIcon color="primary" />
<Typography variant="h6">
Data Sources
</Typography>
<Chip
icon={<AutoAwesomeIcon />}
label={`${Object.keys(autoPopulatedFields).length} auto-populated`}
color="info"
size="small"
/>
<IconButton
size="small"
onClick={() => setExpanded(!expanded)}
>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</Box>
<Collapse in={expanded}>
{/* Summary */}
<Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2">
{Object.keys(autoPopulatedFields).length} fields were automatically populated from your onboarding data.
</Typography>
</Alert>
{/* Data Sources Breakdown */}
<Box sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
Data Sources
</Typography>
<List dense>
{Object.entries(sourceSummary).map(([source, fields]) => (
<ListItem key={source} sx={{ px: 0 }}>
<ListItemIcon sx={{ minWidth: 40 }}>
<Typography variant="body1">
{getDataSourceIcon(source)}
</Typography>
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" fontWeight="medium">
{getDataSourceLabel(source)}
</Typography>
<Chip
label={`${fields.length} fields`}
size="small"
variant="outlined"
/>
</Box>
}
secondary={
<Box sx={{ mt: 0.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<LinearProgress
variant="determinate"
value={getDataQualityScore(source) * 100}
color={getDataQualityColor(getDataQualityScore(source))}
sx={{ flexGrow: 1, height: 4, borderRadius: 2 }}
/>
<Typography variant="caption" color="text.secondary">
{Math.round(getDataQualityScore(source) * 100)}%
</Typography>
</Box>
<Typography variant="caption" color="text.secondary">
{getDataQualityLabel(getDataQualityScore(source))}
</Typography>
</Box>
}
/>
</ListItem>
))}
</List>
</Box>
<Divider sx={{ my: 2 }} />
{/* Auto-populated Fields */}
<Box>
<Typography variant="subtitle2" gutterBottom>
Auto-populated Fields
</Typography>
<List dense>
{autoPopulatedFieldsList.map((field, index) => (
<React.Fragment key={field.fieldId}>
<ListItem sx={{ px: 0 }}>
<ListItemIcon sx={{ minWidth: 40 }}>
<CheckCircleIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2" fontWeight="medium">
{field.fieldId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</Typography>
<Chip
label={getDataSourceLabel(field.source)}
size="small"
variant="outlined"
/>
</Box>
}
secondary={
<Typography variant="caption" color="text.secondary">
Source: {getDataSourceLabel(field.source)} Quality: {getDataQualityLabel(field.qualityScore)}
</Typography>
}
/>
</ListItem>
{index < autoPopulatedFieldsList.length - 1 && <Divider />}
</React.Fragment>
))}
</List>
</Box>
{/* Transparency Note */}
<Box sx={{ mt: 2, p: 1, bgcolor: 'background.default', borderRadius: 1 }}>
<Typography variant="caption" color="text.secondary">
💡 You can modify any auto-populated field. The system learns from your changes to improve future recommendations.
</Typography>
</Box>
</Collapse>
</CardContent>
</Card>
);
};
export default DataSourceTransparency;

View File

@@ -0,0 +1,514 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Paper,
Typography,
Stepper,
Step,
StepLabel,
StepContent,
Button,
LinearProgress,
Alert,
Chip,
IconButton,
Tooltip as MuiTooltip,
Card,
CardContent,
Grid,
Divider,
CircularProgress,
Badge
} from '@mui/material';
import {
Business as BusinessIcon,
People as PeopleIcon,
TrendingUp as TrendingUpIcon,
ContentPaste as ContentIcon,
Analytics as AnalyticsIcon,
Help as HelpIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
AutoAwesome as AutoAwesomeIcon,
Refresh as RefreshIcon,
Save as SaveIcon,
ArrowForward as ArrowForwardIcon,
ArrowBack as ArrowBackIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
import { useEnhancedStrategyStore, STRATEGIC_INPUT_FIELDS } from '../../../stores/enhancedStrategyStore';
import StrategicInputField from './StrategicInputField';
import EnhancedTooltip from './EnhancedTooltip';
import CompletionTracker from './CompletionTracker';
import AIRecommendationsPanel from './AIRecommendationsPanel';
import DataSourceTransparency from './/DataSourceTransparency';
const EnhancedStrategyBuilder: React.FC = () => {
const {
formData,
formErrors,
autoPopulatedFields,
dataSources,
loading,
error,
saving,
aiGenerating,
currentStep,
completedSteps,
disclosureSteps,
currentStrategy,
updateFormField,
validateFormField,
validateAllFields,
completeStep,
getNextStep,
getPreviousStep,
setCurrentStep,
canProceedToStep,
resetForm,
autoPopulateFromOnboarding,
generateAIRecommendations,
createEnhancedStrategy,
calculateCompletionPercentage,
getCompletionStats,
setError,
setCurrentStrategy,
setAIGenerating
} = useEnhancedStrategyStore();
const [showTooltip, setShowTooltip] = useState<string | null>(null);
const [autoPopulateAttempted, setAutoPopulateAttempted] = useState(false);
// Auto-populate from onboarding on first load
useEffect(() => {
if (!autoPopulateAttempted) {
autoPopulateFromOnboarding();
setAutoPopulateAttempted(true);
}
}, [autoPopulateAttempted, autoPopulateFromOnboarding]);
const handleStepComplete = () => {
const currentStepData = disclosureSteps[currentStep];
if (currentStepData) {
// Validate all fields in current step
const stepFields = currentStepData.fields;
const isValid = stepFields.every(fieldId => validateFormField(fieldId));
if (isValid) {
completeStep(currentStepData.id);
// Move to next step if available
const nextStep = getNextStep();
if (nextStep) {
setCurrentStep(currentStep + 1);
}
}
}
};
const handleNextStep = () => {
const nextStep = getNextStep();
if (nextStep) {
setCurrentStep(currentStep + 1);
}
};
const handlePreviousStep = () => {
const prevStep = getPreviousStep();
if (prevStep) {
setCurrentStep(currentStep - 1);
}
};
const handleSaveStrategy = async () => {
if (validateAllFields()) {
const completionStats = getCompletionStats();
const strategyData = {
...formData,
completion_percentage: completionStats.completion_percentage,
user_id: 1, // This would come from auth context
name: formData.name || 'Enhanced Content Strategy',
industry: formData.industry || 'General'
};
await createEnhancedStrategy(strategyData);
}
};
const handleCreateStrategy = async () => {
try {
setAIGenerating(true);
setError(null);
console.log('Starting strategy creation...');
console.log('Current formData:', formData);
console.log('FormData ID:', formData.id);
// If we have a saved strategy, use its ID
if (formData.id) {
console.log('Using existing strategy ID:', formData.id);
await generateAIRecommendations(formData.id);
} else {
console.log('No strategy ID found, creating new strategy...');
// If no strategy is saved yet, save it first, then generate AI insights
const isValid = validateAllFields();
console.log('Form validation result:', isValid);
if (isValid) {
const completionStats = getCompletionStats();
const strategyData = {
...formData,
completion_percentage: completionStats.completion_percentage,
user_id: 1, // This would come from auth context
name: formData.name || 'Enhanced Content Strategy',
industry: formData.industry || 'General'
};
console.log('Strategy data to create:', strategyData);
// Save the strategy first and get the created strategy
const newStrategy = await createEnhancedStrategy(strategyData);
console.log('Created strategy:', newStrategy);
if (newStrategy && newStrategy.id) {
console.log('Generating AI recommendations for strategy ID:', newStrategy.id);
// Now generate AI recommendations with the new strategy ID
await generateAIRecommendations(newStrategy.id);
// Set the current strategy and show success message
setCurrentStrategy(newStrategy);
setError(null); // Clear any previous errors
// Show success message
setTimeout(() => {
setError('Strategy created successfully! Check the Strategic Intelligence tab for detailed insights.');
}, 100);
// Auto-switch to Strategic Intelligence tab after creation
// This would need to be handled by the parent component
} else {
console.error('Failed to create strategy or get strategy ID');
setError('Failed to create strategy. Please try again.');
}
} else {
console.log('Form validation failed');
setError('Please complete all required fields before creating strategy');
}
}
} catch (error: any) {
console.error('Error creating strategy:', error);
setError(error.message || 'Failed to create strategy');
} finally {
setAIGenerating(false);
}
};
const getStepIcon = (stepId: string) => {
const icons = {
business_context: <BusinessIcon />,
audience_intelligence: <PeopleIcon />,
competitive_intelligence: <TrendingUpIcon />,
content_strategy: <ContentIcon />,
performance_analytics: <AnalyticsIcon />
};
return icons[stepId as keyof typeof icons] || <BusinessIcon />;
};
const getStepColor = (stepId: string) => {
if (completedSteps.includes(stepId)) return 'success';
if (currentStep === disclosureSteps.findIndex(s => s.id === stepId)) return 'primary';
return 'default';
};
const completionStats = getCompletionStats();
const completionPercentage = calculateCompletionPercentage();
// Debug logging
console.log('Completion percentage:', completionPercentage);
console.log('Form data keys:', Object.keys(formData));
console.log('Required fields:', STRATEGIC_INPUT_FIELDS.filter(f => f.required).map(f => f.id));
console.log('Filled required fields:', STRATEGIC_INPUT_FIELDS.filter(f => f.required && formData[f.id]).map(f => f.id));
return (
<Box sx={{ p: 3 }}>
{/* Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Box>
<Typography variant="h4" gutterBottom>
Enhanced Strategy Builder
</Typography>
<Typography variant="body2" color="text.secondary">
Build a comprehensive content strategy with 30+ strategic inputs
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<CompletionTracker
completionPercentage={completionPercentage}
completionStats={completionStats}
/>
<MuiTooltip
title={completionPercentage < 20 ? `Complete at least 20% of the form (currently ${Math.round(completionPercentage)}%)` : 'Create a comprehensive content strategy with AI insights'}
placement="top"
>
<span>
<Button
variant="outlined"
startIcon={<AutoAwesomeIcon />}
onClick={handleCreateStrategy}
disabled={aiGenerating || completionPercentage < 20}
>
{aiGenerating ? 'Creating...' : 'Create Strategy'}
</Button>
</span>
</MuiTooltip>
<Button
variant="contained"
startIcon={<SaveIcon />}
onClick={handleSaveStrategy}
disabled={saving || completionPercentage < 30}
>
{saving ? 'Saving...' : 'Save Strategy'}
</Button>
</Box>
</Box>
{/* Error Alert */}
{error && (
<Alert severity="error" sx={{ mb: 3 }}>
{error}
</Alert>
)}
{/* Success Alert */}
{!error && currentStrategy && (
<Alert severity="success" sx={{ mb: 3 }}>
Strategy "{currentStrategy.name}" created successfully! Check the Strategic Intelligence tab for detailed insights.
</Alert>
)}
{/* Strategy Display */}
{currentStrategy && (
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h5" gutterBottom>
Created Strategy: {currentStrategy.name}
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} md={6}>
<Typography variant="subtitle1" color="text.secondary">
Industry: {currentStrategy.industry}
</Typography>
<Typography variant="subtitle1" color="text.secondary">
Completion: {currentStrategy.completion_percentage}%
</Typography>
</Grid>
<Grid item xs={12} md={6}>
<Typography variant="subtitle1" color="text.secondary">
Created: {new Date(currentStrategy.created_at).toLocaleDateString()}
</Typography>
<Typography variant="subtitle1" color="text.secondary">
ID: {currentStrategy.id}
</Typography>
</Grid>
</Grid>
<Box sx={{ mt: 2 }}>
<Button
variant="outlined"
onClick={() => window.location.href = '/content-planning?tab=strategic-intelligence'}
startIcon={<AssessmentIcon />}
>
View Strategic Intelligence
</Button>
</Box>
</Paper>
)}
{/* Auto-population Status */}
{autoPopulatedFields && Object.keys(autoPopulatedFields).length > 0 && (
<Alert
severity="info"
sx={{ mb: 3 }}
action={
<Button color="inherit" size="small" onClick={autoPopulateFromOnboarding}>
<RefreshIcon />
</Button>
}
>
{autoPopulatedFields && Object.keys(autoPopulatedFields).length} fields auto-populated from onboarding data
</Alert>
)}
<Grid container spacing={3}>
{/* Main Strategy Builder */}
<Grid item xs={12} md={8}>
<Paper sx={{ p: 3 }}>
{/* Progress Indicator */}
<Box sx={{ mb: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Typography variant="body2" color="text.secondary">
Strategy Completion
</Typography>
<Typography variant="body2" color="text.secondary">
{Math.round(calculateCompletionPercentage?.() || 0)}%
</Typography>
</Box>
<LinearProgress
variant="determinate"
value={calculateCompletionPercentage?.() || 0}
sx={{ height: 8, borderRadius: 4 }}
/>
</Box>
{/* Stepper */}
<Stepper activeStep={currentStep} orientation="vertical">
{disclosureSteps.map((step, index) => (
<Step key={step.id} completed={completedSteps.includes(step.id)}>
<StepLabel
icon={
<Badge
badgeContent={step.fields.length}
color={getStepColor(step.id)}
sx={{ '& .MuiBadge-badge': { fontSize: '0.75rem' } }}
>
{getStepIcon(step.id)}
</Badge>
}
optional={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{completedSteps.includes(step.id) && (
<CheckCircleIcon color="success" fontSize="small" />
)}
<Chip
label={`${step.fields.length} fields`}
size="small"
variant="outlined"
/>
</Box>
}
>
<Typography variant="h6">{step.title}</Typography>
<Typography variant="body2" color="text.secondary">
{step.description}
</Typography>
</StepLabel>
<StepContent>
<Box sx={{ mt: 2 }}>
{/* Step Fields */}
<Grid container spacing={2}>
{step.fields.map((fieldId) => (
<Grid item xs={12} key={fieldId}>
<StrategicInputField
fieldId={fieldId}
value={formData[fieldId]}
error={formErrors[fieldId]}
autoPopulated={!!autoPopulatedFields[fieldId]}
dataSource={dataSources[fieldId]}
onChange={(value: any) => updateFormField(fieldId, value)}
onValidate={() => validateFormField(fieldId)}
onShowTooltip={() => setShowTooltip(fieldId)}
/>
</Grid>
))}
</Grid>
{/* Step Actions */}
<Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
<Button
variant="contained"
onClick={handleStepComplete}
disabled={!step.fields.every(fieldId => formData[fieldId])}
endIcon={<ArrowForwardIcon />}
>
{getNextStep() ? 'Complete & Continue' : 'Complete Strategy'}
</Button>
{getPreviousStep() && (
<Button
variant="outlined"
onClick={handlePreviousStep}
startIcon={<ArrowBackIcon />}
>
Previous Step
</Button>
)}
{getNextStep() && (
<Button
variant="outlined"
onClick={handleNextStep}
disabled={!canProceedToStep(getNextStep()!.id)}
endIcon={<ArrowForwardIcon />}
>
Skip to Next
</Button>
)}
</Box>
</Box>
</StepContent>
</Step>
))}
</Stepper>
</Paper>
</Grid>
{/* Sidebar */}
<Grid item xs={12} md={4}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Data Source Transparency */}
<DataSourceTransparency
autoPopulatedFields={autoPopulatedFields}
dataSources={dataSources}
/>
{/* AI Recommendations Panel */}
<AIRecommendationsPanel
aiGenerating={aiGenerating}
onGenerateRecommendations={handleCreateStrategy}
/>
{/* Quick Actions */}
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Quick Actions
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Button
variant="outlined"
size="small"
onClick={resetForm}
disabled={loading}
>
Reset Form
</Button>
<Button
variant="outlined"
size="small"
onClick={autoPopulateFromOnboarding}
disabled={loading}
>
Re-populate from Onboarding
</Button>
</Box>
</CardContent>
</Card>
</Box>
</Grid>
</Grid>
{/* Enhanced Tooltip */}
{showTooltip && (
<EnhancedTooltip
fieldId={showTooltip}
open={!!showTooltip}
onClose={() => setShowTooltip(null)}
/>
)}
</Box>
);
};
export default EnhancedStrategyBuilder;

View File

@@ -0,0 +1,288 @@
import React from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
Typography,
Box,
Chip,
Divider,
List,
ListItem,
ListItemIcon,
ListItemText,
Card,
CardContent,
Alert,
LinearProgress
} from '@mui/material';
import {
Help as HelpIcon,
Lightbulb as LightbulbIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
AutoAwesome as AutoAwesomeIcon,
DataUsage as DataUsageIcon,
Close as CloseIcon
} from '@mui/icons-material';
import { useEnhancedStrategyStore } from '../../../stores/enhancedStrategyStore';
interface EnhancedTooltipProps {
fieldId: string;
open: boolean;
onClose: () => void;
}
const EnhancedTooltip: React.FC<EnhancedTooltipProps> = ({
fieldId,
open,
onClose
}) => {
const { getTooltipData, autoPopulatedFields, dataSources } = useEnhancedStrategyStore();
const tooltipData = getTooltipData(fieldId);
const isAutoPopulated = !!(autoPopulatedFields && autoPopulatedFields[fieldId]);
const dataSource = dataSources && dataSources[fieldId];
// Early return if no tooltip data
if (!tooltipData) {
return null;
}
const getFieldExamples = (fieldId: string) => {
const examples: Record<string, string[]> = {
business_objectives: [
'Primary: Increase brand awareness by 40%',
'Secondary: Generate 500 qualified leads per month',
'Secondary: Improve customer engagement by 25%'
],
target_metrics: [
'Traffic: 50% increase in organic traffic',
'Engagement: 3.5+ average time on page',
'Conversions: 15% improvement in conversion rate'
],
content_budget: [
'Monthly budget: $5,000 for content creation',
'Annual budget: $60,000 including tools and team',
'Per-piece budget: $500 average per content piece'
],
team_size: [
'Small team: 1-2 content creators',
'Medium team: 3-5 content creators + manager',
'Large team: 6+ creators, editors, and strategists'
],
content_preferences: [
'Formats: Blog posts, videos, infographics',
'Topics: Technology trends, industry insights',
'Tone: Professional but approachable'
],
preferred_formats: [
'Blog Posts: 40% of content mix',
'Videos: 30% of content mix',
'Infographics: 20% of content mix',
'Webinars: 10% of content mix'
],
content_frequency: [
'Daily: For news and trending topics',
'Weekly: For in-depth analysis pieces',
'Bi-weekly: For comprehensive guides',
'Monthly: For thought leadership content'
]
};
return examples[fieldId] || [
'Example 1: Provide specific, measurable examples',
'Example 2: Include both qualitative and quantitative data',
'Example 3: Align with your business objectives'
];
};
const getBestPractices = (fieldId: string) => {
const practices: Record<string, string[]> = {
business_objectives: [
'Make objectives SMART (Specific, Measurable, Achievable, Relevant, Time-bound)',
'Align with overall business goals',
'Include both primary and secondary objectives',
'Set realistic but ambitious targets'
],
target_metrics: [
'Choose metrics that directly impact business outcomes',
'Include leading and lagging indicators',
'Set baseline measurements before starting',
'Track metrics consistently over time'
],
content_preferences: [
'Base preferences on audience research and analytics',
'Consider your team\'s content creation capabilities',
'Balance audience preferences with business goals',
'Test different formats to find what works best'
],
preferred_formats: [
'Choose formats that align with your audience\'s consumption habits',
'Consider your team\'s expertise and resources',
'Mix different formats to reach different audience segments',
'Prioritize formats that drive your target metrics'
],
content_frequency: [
'Set realistic frequency based on team capacity',
'Consider your audience\'s content consumption patterns',
'Balance quality with quantity',
'Allow flexibility for trending topics and opportunities'
]
};
return practices[fieldId] || [
'Research your audience thoroughly before making decisions',
'Test and iterate based on performance data',
'Align all decisions with your business objectives',
'Consider your team\'s capabilities and resources'
];
};
const examples = getFieldExamples(fieldId);
const bestPractices = getBestPractices(fieldId);
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
borderRadius: 2,
maxHeight: '80vh'
}
}}
>
<DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<HelpIcon color="primary" />
<Typography variant="h6">
{tooltipData.title}
</Typography>
{isAutoPopulated && (
<Chip
icon={<AutoAwesomeIcon />}
label="Auto-populated"
color="info"
size="small"
/>
)}
</Box>
</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Description */}
<Box>
<Typography variant="subtitle1" gutterBottom>
Description
</Typography>
<Typography variant="body2" color="text.secondary">
{tooltipData.description}
</Typography>
</Box>
{/* Data Source Information */}
{isAutoPopulated && dataSource && (
<Alert severity="info" icon={<DataUsageIcon />}>
<Typography variant="body2">
This field was automatically populated from your onboarding data ({dataSource}).
You can modify this value if needed.
</Typography>
</Alert>
)}
{/* Examples */}
<Box>
<Typography variant="subtitle1" gutterBottom>
Examples
</Typography>
<List dense>
{examples.map((example, index) => (
<ListItem key={index} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<CheckCircleIcon color="success" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={example}
primaryTypographyProps={{ variant: 'body2' }}
/>
</ListItem>
))}
</List>
</Box>
<Divider />
{/* Best Practices */}
<Box>
<Typography variant="subtitle1" gutterBottom>
Best Practices
</Typography>
<List dense>
{bestPractices.map((practice, index) => (
<ListItem key={index} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 32 }}>
<LightbulbIcon color="primary" fontSize="small" />
</ListItemIcon>
<ListItemText
primary={practice}
primaryTypographyProps={{ variant: 'body2' }}
/>
</ListItem>
))}
</List>
</Box>
{/* Field Importance */}
<Card variant="outlined">
<CardContent>
<Typography variant="subtitle2" gutterBottom>
Why This Matters
</Typography>
<Typography variant="body2" color="text.secondary">
This information helps create a more targeted and effective content strategy.
The more accurate and detailed your inputs, the better our AI can generate
personalized recommendations for your specific situation.
</Typography>
</CardContent>
</Card>
{/* Confidence Level */}
{tooltipData.confidence_level && (
<Box>
<Typography variant="subtitle2" gutterBottom>
Data Confidence
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<LinearProgress
variant="determinate"
value={tooltipData.confidence_level * 100}
sx={{ flexGrow: 1, height: 8, borderRadius: 4 }}
/>
<Typography variant="body2" color="text.secondary">
{Math.round(tooltipData.confidence_level * 100)}%
</Typography>
</Box>
<Typography variant="caption" color="text.secondary">
Confidence level based on data quality and source reliability
</Typography>
</Box>
)}
</Box>
</DialogContent>
<DialogActions>
<Button onClick={onClose} startIcon={<CloseIcon />}>
Close
</Button>
</DialogActions>
</Dialog>
);
};
export default EnhancedTooltip;

View File

@@ -0,0 +1,85 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Chip,
Typography,
CircularProgress
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Warning as WarningIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
const HealthCheck: React.FC = () => {
const [healthStatus, setHealthStatus] = useState<{
api: boolean;
database: boolean;
loading: boolean;
}>({
api: false,
database: false,
loading: true
});
const { checkHealth, checkDatabaseHealth } = useContentPlanningStore();
useEffect(() => {
const checkBackendHealth = async () => {
try {
const [apiHealthy, dbHealthy] = await Promise.all([
checkHealth(),
checkDatabaseHealth()
]);
setHealthStatus({
api: apiHealthy,
database: dbHealthy,
loading: false
});
} catch (error) {
console.error('Health check failed:', error);
setHealthStatus({
api: false,
database: false,
loading: false
});
}
};
checkBackendHealth();
}, [checkHealth, checkDatabaseHealth]);
if (healthStatus.loading) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CircularProgress size={16} />
<Typography variant="caption">Checking backend...</Typography>
</Box>
);
}
const allHealthy = healthStatus.api && healthStatus.database;
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
icon={allHealthy ? <CheckCircleIcon /> : <WarningIcon />}
label={allHealthy ? 'Connected' : 'Disconnected'}
color={allHealthy ? 'success' : 'warning'}
size="small"
variant="outlined"
/>
{!allHealthy && (
<Typography variant="caption" color="text.secondary">
{!healthStatus.api && !healthStatus.database && 'API & DB'}
{!healthStatus.api && healthStatus.database && 'API'}
{healthStatus.api && !healthStatus.database && 'DB'}
</Typography>
)}
</Box>
);
};
export default HealthCheck;

View File

@@ -0,0 +1,251 @@
import React from 'react';
import {
Box,
Paper,
Typography,
LinearProgress,
Chip,
IconButton,
Collapse,
List,
ListItem,
ListItemIcon,
ListItemText,
Alert,
Button
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Refresh as RefreshIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Psychology as StrategyIcon,
Search as SearchIcon,
Analytics as AnalyticsIcon,
CalendarToday as CalendarIcon,
HealthAndSafety as HealthIcon
} from '@mui/icons-material';
import { ServiceStatus } from '../../../services/contentPlanningOrchestrator';
interface ProgressIndicatorProps {
serviceStatuses: ServiceStatus[];
onRefreshService: (serviceName: string) => void;
expanded?: boolean;
onToggleExpanded?: () => void;
}
const ProgressIndicator: React.FC<ProgressIndicatorProps> = ({
serviceStatuses,
onRefreshService,
expanded = false,
onToggleExpanded
}) => {
const getServiceIcon = (serviceName: string) => {
switch (serviceName) {
case 'Content Strategies':
return <StrategyIcon />;
case 'Gap Analysis':
return <SearchIcon />;
case 'AI Analytics':
return <AnalyticsIcon />;
case 'Calendar Events':
return <CalendarIcon />;
case 'System Health':
return <HealthIcon />;
default:
return <AnalyticsIcon />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'success':
return 'success';
case 'error':
return 'error';
case 'loading':
return 'primary';
default:
return 'primary';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'success':
return <CheckCircleIcon color="success" />;
case 'error':
return <ErrorIcon color="error" />;
case 'loading':
return <RefreshIcon sx={{ animation: 'spin 1s linear infinite' }} />;
default:
return null;
}
};
const isLoading = serviceStatuses.some(status => status.status === 'loading');
const hasErrors = serviceStatuses.some(status => status.status === 'error');
const allComplete = serviceStatuses.every(status => status.status === 'success');
const overallProgress = serviceStatuses.reduce((acc, status) => acc + status.progress, 0) / serviceStatuses.length;
return (
<Paper
elevation={2}
sx={{
p: 2,
mb: 2,
border: hasErrors ? '1px solid #f44336' : '1px solid transparent',
backgroundColor: hasErrors ? 'rgba(244, 67, 54, 0.05)' : 'background.paper',
'@keyframes spin': {
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' }
}
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{isLoading && <RefreshIcon sx={{ animation: 'spin 1s linear infinite' }} />}
Content Planning Progress
{allComplete && <CheckCircleIcon color="success" />}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
label={`${Math.round(overallProgress)}%`}
color={allComplete ? 'success' : isLoading ? 'primary' : 'default'}
size="small"
/>
{onToggleExpanded && (
<IconButton size="small" onClick={onToggleExpanded}>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
)}
</Box>
</Box>
{/* Overall Progress Bar */}
<Box sx={{ mb: 2 }}>
<LinearProgress
variant="determinate"
value={overallProgress}
color={allComplete ? 'success' : isLoading ? 'primary' : 'inherit'}
sx={{ height: 8, borderRadius: 4 }}
/>
</Box>
{/* Status Messages */}
{isLoading && (
<Alert severity="info" sx={{ mb: 2 }}>
<Typography variant="body2">
Loading content planning data... This may take a few moments as we analyze your content strategy.
</Typography>
</Alert>
)}
{hasErrors && (
<Alert severity="error" sx={{ mb: 2 }}>
<Typography variant="body2">
Some services encountered errors. You can refresh individual services below.
</Typography>
</Alert>
)}
{allComplete && (
<Alert severity="success" sx={{ mb: 2 }}>
<Typography variant="body2">
All content planning services are ready! Your dashboard is fully loaded.
</Typography>
</Alert>
)}
{/* Detailed Service Status */}
<Collapse in={expanded}>
<List dense>
{serviceStatuses.map((status, index) => (
<ListItem
key={index}
sx={{
border: '1px solid',
borderColor: getStatusColor(status.status) === 'error' ? 'error.main' : 'divider',
borderRadius: 1,
mb: 1,
backgroundColor: getStatusColor(status.status) === 'error' ? 'rgba(244, 67, 54, 0.05)' : 'transparent'
}}
>
<ListItemIcon>
{getServiceIcon(status.name)}
</ListItemIcon>
<ListItemText
primary={
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="body2" fontWeight="medium">
{status.name}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getStatusIcon(status.status)}
<Chip
label={`${status.progress}%`}
size="small"
color={getStatusColor(status.status)}
variant="outlined"
/>
</Box>
</Box>
}
secondary={
<Box sx={{ mt: 1 }}>
<Typography variant="caption" color="text.secondary">
{status.message}
</Typography>
{status.error && (
<Typography variant="caption" color="error" sx={{ display: 'block', mt: 0.5 }}>
Error: {status.error}
</Typography>
)}
<Box sx={{ mt: 1 }}>
<LinearProgress
variant="determinate"
value={status.progress}
color={getStatusColor(status.status)}
sx={{ height: 4, borderRadius: 2 }}
/>
</Box>
</Box>
}
/>
{status.status === 'error' && (
<IconButton
size="small"
onClick={() => onRefreshService(status.name.toLowerCase().replace(' ', ''))}
color="primary"
>
<RefreshIcon />
</IconButton>
)}
</ListItem>
))}
</List>
</Collapse>
{/* Quick Actions */}
{hasErrors && (
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
<Button
variant="outlined"
size="small"
onClick={() => serviceStatuses.forEach(status => {
if (status.status === 'error') {
onRefreshService(status.name.toLowerCase().replace(' ', ''));
}
})}
>
Refresh All Failed Services
</Button>
</Box>
)}
</Paper>
);
};
export default ProgressIndicator;

View File

@@ -0,0 +1,137 @@
import React from 'react';
import {
Box,
Paper,
Typography,
LinearProgress,
IconButton,
Chip,
Collapse,
Alert
} from '@mui/material';
import {
CheckCircle as CheckCircleIcon,
Error as ErrorIcon,
Refresh as RefreshIcon,
ExpandMore as ExpandMoreIcon,
ExpandLess as ExpandLessIcon,
Warning as WarningIcon
} from '@mui/icons-material';
import { ServiceStatus } from '../../../services/contentPlanningOrchestrator';
interface ServiceStatusPanelProps {
serviceStatuses: ServiceStatus[];
onRefreshService: (serviceName: string) => void;
expanded: boolean;
onToggleExpanded: () => void;
}
const ServiceStatusPanel: React.FC<ServiceStatusPanelProps> = ({
serviceStatuses,
onRefreshService,
expanded,
onToggleExpanded
}) => {
const getStatusColor = (status: string) => {
switch (status) {
case 'success': return 'success';
case 'error': return 'error';
case 'loading': return 'primary';
default: return 'primary';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'success': return <CheckCircleIcon fontSize="small" />;
case 'error': return <ErrorIcon fontSize="small" />;
case 'loading': return <WarningIcon fontSize="small" />;
default: return null;
}
};
const getOverallStatus = () => {
const hasErrors = serviceStatuses.some(s => s.status === 'error');
const hasLoading = serviceStatuses.some(s => s.status === 'loading');
const allSuccess = serviceStatuses.every(s => s.status === 'success');
if (hasErrors) return { status: 'error', text: 'Some services failed' };
if (hasLoading) return { status: 'loading', text: 'Services loading' };
if (allSuccess) return { status: 'success', text: 'All services operational' };
return { status: 'idle', text: 'Services idle' };
};
const overallStatus = getOverallStatus();
return (
<Paper sx={{ mb: 2 }}>
<Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getStatusIcon(overallStatus.status)}
<Typography variant="subtitle2">
System Status: {overallStatus.text}
</Typography>
<Chip
label={`${serviceStatuses.filter(s => s.status === 'success').length}/${serviceStatuses.length}`}
size="small"
color={getStatusColor(overallStatus.status)}
variant="outlined"
/>
</Box>
<IconButton size="small" onClick={onToggleExpanded}>
{expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</IconButton>
</Box>
</Box>
<Collapse in={expanded}>
<Box sx={{ p: 2 }}>
{serviceStatuses.map((service) => (
<Box key={service.name} sx={{ mb: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getStatusIcon(service.status)}
<Typography variant="body2" fontWeight="medium">
{service.name}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="caption" color="text.secondary">
{service.progress}%
</Typography>
<IconButton
size="small"
onClick={() => onRefreshService(service.name.toLowerCase().replace(/\s+/g, ''))}
disabled={service.status === 'loading'}
>
<RefreshIcon fontSize="small" />
</IconButton>
</Box>
</Box>
<LinearProgress
variant="determinate"
value={service.progress}
color={getStatusColor(service.status)}
sx={{ mb: 1 }}
/>
<Typography variant="caption" color="text.secondary">
{service.message}
</Typography>
{service.error && (
<Alert severity="error" sx={{ mt: 1 }}>
{service.error}
</Alert>
)}
</Box>
))}
</Box>
</Collapse>
</Paper>
);
};
export default ServiceStatusPanel;

View File

@@ -0,0 +1,524 @@
import React, { useState } from 'react';
import {
Box,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
FormControlLabel,
Switch,
Chip,
IconButton,
Tooltip,
Typography,
Alert,
Autocomplete,
InputAdornment
} from '@mui/material';
import {
Help as HelpIcon,
AutoAwesome as AutoAwesomeIcon,
Warning as WarningIcon,
CheckCircle as CheckCircleIcon,
Edit as EditIcon
} from '@mui/icons-material';
import { useEnhancedStrategyStore } from '../../../stores/enhancedStrategyStore';
interface StrategicInputFieldProps {
fieldId: string;
value: any;
error?: string;
autoPopulated?: boolean;
dataSource?: string;
onChange: (value: any) => void;
onValidate: () => boolean;
onShowTooltip: () => void;
}
// Define proper types for field configurations
interface BaseFieldConfig {
type: string;
label: string;
required: boolean;
}
interface TextFieldConfig extends BaseFieldConfig {
type: 'text' | 'number' | 'json';
placeholder: string;
}
interface SelectFieldConfig extends BaseFieldConfig {
type: 'select';
options: string[];
}
interface MultiSelectFieldConfig extends BaseFieldConfig {
type: 'multiselect';
options: string[];
placeholder?: string;
}
interface BooleanFieldConfig extends BaseFieldConfig {
type: 'boolean';
}
type FieldConfig = TextFieldConfig | SelectFieldConfig | MultiSelectFieldConfig | BooleanFieldConfig;
const StrategicInputField: React.FC<StrategicInputFieldProps> = ({
fieldId,
value,
error,
autoPopulated = false,
dataSource,
onChange,
onValidate,
onShowTooltip
}) => {
const { getTooltipData } = useEnhancedStrategyStore();
const [isEditing, setIsEditing] = useState(false);
// Get field configuration from store with proper null checking
const tooltipData = getTooltipData(fieldId);
// Field configuration mapping (this would come from the store)
const fieldConfig: Record<string, FieldConfig> = {
business_objectives: {
type: 'json',
label: 'Business Objectives',
placeholder: 'Enter your primary and secondary business goals',
required: true
},
target_metrics: {
type: 'json',
label: 'Target Metrics',
placeholder: 'Define your KPIs and success metrics',
required: true
},
content_budget: {
type: 'number',
label: 'Content Budget',
placeholder: 'Enter your content budget',
required: false
},
team_size: {
type: 'number',
label: 'Team Size',
placeholder: 'Enter team size',
required: false
},
implementation_timeline: {
type: 'select',
label: 'Implementation Timeline',
options: ['3 months', '6 months', '1 year', '2 years', 'Ongoing'],
required: false
},
market_share: {
type: 'text',
label: 'Market Share',
placeholder: 'Enter market share percentage',
required: false
},
competitive_position: {
type: 'select',
label: 'Competitive Position',
options: ['Leader', 'Challenger', 'Niche', 'Emerging'],
required: false
},
performance_metrics: {
type: 'json',
label: 'Current Performance Metrics',
placeholder: 'Enter current performance data',
required: false
},
content_preferences: {
type: 'json',
label: 'Content Preferences',
placeholder: 'Define content preferences',
required: true
},
consumption_patterns: {
type: 'json',
label: 'Consumption Patterns',
placeholder: 'Describe consumption patterns',
required: false
},
audience_pain_points: {
type: 'json',
label: 'Audience Pain Points',
placeholder: 'List audience pain points',
required: false
},
buying_journey: {
type: 'json',
label: 'Buying Journey',
placeholder: 'Define buying journey stages',
required: false
},
seasonal_trends: {
type: 'json',
label: 'Seasonal Trends',
placeholder: 'Describe seasonal content patterns',
required: false
},
engagement_metrics: {
type: 'json',
label: 'Engagement Metrics',
placeholder: 'Define engagement tracking metrics',
required: false
},
top_competitors: {
type: 'json',
label: 'Top Competitors',
placeholder: 'List your main competitors',
required: false
},
competitor_content_strategies: {
type: 'json',
label: 'Competitor Content Strategies',
placeholder: 'Analyze competitor content approaches',
required: false
},
market_gaps: {
type: 'json',
label: 'Market Gaps',
placeholder: 'Identify content gaps in the market',
required: false
},
industry_trends: {
type: 'json',
label: 'Industry Trends',
placeholder: 'Describe relevant industry trends',
required: false
},
emerging_trends: {
type: 'json',
label: 'Emerging Trends',
placeholder: 'Identify emerging content trends',
required: false
},
preferred_formats: {
type: 'json',
label: 'Preferred Formats',
placeholder: 'Define preferred content formats',
required: false
},
content_mix: {
type: 'json',
label: 'Content Mix',
placeholder: 'Define your content mix strategy',
required: false
},
content_frequency: {
type: 'select',
label: 'Content Frequency',
options: ['Daily', 'Weekly', 'Bi-weekly', 'Monthly', 'Quarterly'],
required: false
},
optimal_timing: {
type: 'json',
label: 'Optimal Timing',
placeholder: 'Define optimal posting times',
required: false
},
quality_metrics: {
type: 'json',
label: 'Quality Metrics',
placeholder: 'Define content quality standards',
required: false
},
editorial_guidelines: {
type: 'json',
label: 'Editorial Guidelines',
placeholder: 'Define editorial guidelines',
required: false
},
brand_voice: {
type: 'json',
label: 'Brand Voice',
placeholder: 'Define your brand voice',
required: false
},
traffic_sources: {
type: 'json',
label: 'Traffic Sources',
placeholder: 'Define your traffic sources',
required: false
},
conversion_rates: {
type: 'json',
label: 'Conversion Rates',
placeholder: 'Define target conversion rates',
required: false
},
content_roi_targets: {
type: 'json',
label: 'Content ROI Targets',
placeholder: 'Define ROI targets for content',
required: false
},
ab_testing_capabilities: {
type: 'boolean',
label: 'A/B Testing Capabilities',
required: false
}
};
// Get the field configuration with fallback
const config = fieldConfig[fieldId] || {
type: 'text',
label: fieldId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
placeholder: `Enter ${fieldId.replace(/_/g, ' ')}`,
required: false
};
const handleChange = (newValue: any) => {
onChange(newValue);
if (autoPopulated && !isEditing) {
setIsEditing(true);
}
};
const renderInput = () => {
// Safety check for config
if (!config) {
return (
<TextField
fullWidth
label={fieldId.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
value={value || ''}
onChange={(e) => handleChange(e.target.value)}
placeholder={`Enter ${fieldId.replace(/_/g, ' ')}`}
error={!!error}
helperText={error}
required={false}
/>
);
}
switch (config.type) {
case 'text':
return (
<TextField
fullWidth
label={config.label || fieldId}
value={value || ''}
onChange={(e) => handleChange(e.target.value)}
placeholder={(config as TextFieldConfig).placeholder || `Enter ${fieldId}`}
error={!!error}
helperText={error}
required={config.required || false}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
</InputAdornment>
)
}}
/>
);
case 'number':
return (
<TextField
fullWidth
type="number"
label={config.label || fieldId}
value={value || ''}
onChange={(e) => handleChange(Number(e.target.value))}
placeholder={(config as TextFieldConfig).placeholder || `Enter ${fieldId}`}
error={!!error}
helperText={error}
required={config.required || false}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
</InputAdornment>
)
}}
/>
);
case 'select':
const selectConfig = config as SelectFieldConfig;
return (
<FormControl fullWidth error={!!error} required={config.required || false}>
<InputLabel>{config.label || fieldId}</InputLabel>
<Select
value={value || ''}
onChange={(e) => handleChange(e.target.value)}
label={config.label || fieldId}
endAdornment={
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
}
>
{(selectConfig.options || []).map((option: string) => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
);
case 'multiselect':
const multiSelectConfig = config as MultiSelectFieldConfig;
return (
<Autocomplete
multiple
options={multiSelectConfig.options || []}
value={Array.isArray(value) ? value : []}
onChange={(_, newValue) => handleChange(newValue)}
renderInput={(params) => (
<TextField
{...params}
label={config.label || fieldId}
placeholder={multiSelectConfig.placeholder || `Select ${fieldId}`}
error={!!error}
helperText={error}
required={config.required || false}
InputProps={{
...params.InputProps,
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
</InputAdornment>
)
}}
/>
)}
renderTags={(value, getTagProps) =>
value.map((option, index) => (
<Chip
label={option}
{...getTagProps({ index })}
key={option}
/>
))
}
/>
);
case 'boolean':
return (
<FormControlLabel
control={
<Switch
checked={!!value}
onChange={(e) => handleChange(e.target.checked)}
/>
}
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{config.label || fieldId}
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
</Box>
}
/>
);
case 'json':
return (
<TextField
fullWidth
multiline
rows={3}
label={config.label || fieldId}
value={typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
handleChange(parsed);
} catch {
handleChange(e.target.value);
}
}}
placeholder={(config as TextFieldConfig).placeholder || `Enter ${fieldId} as JSON`}
error={!!error}
helperText={error}
required={config.required || false}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={onShowTooltip} size="small">
<HelpIcon />
</IconButton>
</InputAdornment>
)
}}
/>
);
default:
return (
<TextField
fullWidth
label={(config as any).label || fieldId}
value={value || ''}
onChange={(e) => handleChange(e.target.value)}
placeholder={`Enter ${fieldId}`}
error={!!error}
helperText={error}
required={(config as any).required || false}
/>
);
}
};
return (
<Box sx={{ position: 'relative' }}>
{/* Auto-population indicator */}
{autoPopulated && (
<Box sx={{ mb: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
icon={<AutoAwesomeIcon />}
label={`Auto-populated from ${dataSource}`}
color="info"
size="small"
variant="outlined"
/>
{!isEditing && (
<Tooltip title="Edit auto-populated value">
<IconButton size="small" onClick={() => setIsEditing(true)}>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
</Box>
)}
{/* Field input */}
{renderInput()}
{/* Validation status */}
{value && !error && (
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckCircleIcon color="success" fontSize="small" />
<Typography variant="caption" color="success.main">
Valid
</Typography>
</Box>
)}
{/* Error display */}
{error && (
<Alert severity="error" sx={{ mt: 1 }}>
<Typography variant="body2">{error}</Typography>
</Alert>
)}
</Box>
);
};
export default StrategicInputField;

View File

@@ -0,0 +1,390 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Grid,
Paper,
Typography,
Card,
CardContent,
Chip,
Divider,
Alert,
CircularProgress,
LinearProgress
} from '@mui/material';
import {
TrendingUp as TrendingUpIcon,
Analytics as AnalyticsIcon,
ShowChart as ShowChartIcon,
Assessment as AssessmentIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
const AnalyticsTab: React.FC = () => {
const {
performanceMetrics,
aiInsights,
loading,
error,
loadAIInsights,
loadAIRecommendations
} = useContentPlanningStore();
const [analyticsData, setAnalyticsData] = useState<any>(null);
const [dataLoading, setDataLoading] = useState(false);
useEffect(() => {
loadAnalyticsData();
}, []);
const loadAnalyticsData = async () => {
try {
setDataLoading(true);
console.log('Loading analytics data...');
// Load AI insights and recommendations
await Promise.all([
loadAIInsights(),
loadAIRecommendations()
]);
// Load analytics data from backend
const response = await contentPlanningApi.getAIAnalyticsSafe();
console.log('Analytics Response:', response);
if (response) {
const analyticsData = {
performance_trends: response.performance_trends || {},
content_evolution: response.content_evolution || {},
engagement_patterns: response.engagement_patterns || {},
recommendations: response.recommendations || [],
insights: response.insights || []
};
console.log('Analytics Data:', analyticsData);
setAnalyticsData(analyticsData);
}
} catch (error) {
console.error('Error loading analytics data:', error);
} finally {
setDataLoading(false);
}
};
const getPerformanceColor = (value: number) => {
if (value >= 80) return 'success';
if (value >= 60) return 'warning';
return 'error';
};
return (
<Box sx={{ p: 3 }}>
<Typography variant="h4" gutterBottom>
Performance Analytics
</Typography>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<Grid container spacing={3}>
{/* Performance Overview */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Performance Overview
</Typography>
<Divider sx={{ mb: 2 }} />
{performanceMetrics ? (
<Grid container spacing={2}>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Engagement Rate
</Typography>
<Typography variant="h4" color={getPerformanceColor(performanceMetrics.engagement)}>
{performanceMetrics.engagement}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Reach
</Typography>
<Typography variant="h4" color="primary">
{performanceMetrics.reach.toLocaleString()}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Conversion Rate
</Typography>
<Typography variant="h4" color={getPerformanceColor(performanceMetrics.conversion)}>
{performanceMetrics.conversion}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
ROI
</Typography>
<Typography variant="h4" color="success.main">
${performanceMetrics.roi.toLocaleString()}
</Typography>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary">
No performance data available
</Typography>
)}
</Paper>
</Grid>
{/* AI Insights */}
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<AssessmentIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
AI Insights
</Typography>
<Divider sx={{ mb: 2 }} />
{aiInsights && aiInsights.length > 0 ? (
<Box>
{aiInsights.slice(0, 3).map((insight, index) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>
{insight.title}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{insight.description}
</Typography>
<Chip
label={insight.priority}
color={insight.priority === 'high' ? 'error' : insight.priority === 'medium' ? 'warning' : 'success'}
size="small"
/>
</Box>
))}
</Box>
) : (
<Typography variant="body2" color="text.secondary">
No AI insights available
</Typography>
)}
</Paper>
</Grid>
{/* Content Evolution */}
{analyticsData && analyticsData.content_evolution && (
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<ShowChartIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Content Evolution
</Typography>
<Divider sx={{ mb: 2 }} />
{analyticsData.content_evolution.content_types ? (
<Box>
{analyticsData.content_evolution.content_types.map((contentType: string, index: number) => {
const performance = analyticsData.content_evolution.performance_by_type?.[contentType];
return (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle1" sx={{ textTransform: 'capitalize' }}>
{contentType.replace('_', ' ')}
</Typography>
{performance && (
<Grid container spacing={1}>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Growth
</Typography>
<Typography variant="h6" color="success.main">
+{performance.growth}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Engagement
</Typography>
<Typography variant="h6">
{performance.engagement}%
</Typography>
</Grid>
</Grid>
)}
</Box>
);
})}
</Box>
) : (
<Typography variant="body2" color="text.secondary">
No content evolution data available
</Typography>
)}
</Paper>
</Grid>
)}
{/* Performance Trends */}
{analyticsData && analyticsData.performance_trends && (
<Grid item xs={12} md={6}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<TrendingUpIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Performance Trends
</Typography>
<Divider sx={{ mb: 2 }} />
{analyticsData.performance_trends.engagement_trend ? (
<Box>
<Typography variant="subtitle2" gutterBottom>
Engagement Trend (Last 5 periods)
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
{analyticsData.performance_trends.engagement_trend.map((value: number, index: number) => (
<Box key={index} sx={{ flex: 1, textAlign: 'center' }}>
<Typography variant="h6" color="primary">
{value}%
</Typography>
<Typography variant="caption" color="text.secondary">
Period {index + 1}
</Typography>
</Box>
))}
</Box>
</Box>
) : (
<Typography variant="body2" color="text.secondary">
No trend data available
</Typography>
)}
</Paper>
</Grid>
)}
{/* Engagement Patterns */}
{analyticsData && analyticsData.engagement_patterns && (
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Engagement Patterns
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={3}>
{analyticsData.engagement_patterns.peak_times && (
<Grid item xs={12} md={4}>
<Typography variant="subtitle2" gutterBottom>
Peak Engagement Times
</Typography>
{analyticsData.engagement_patterns.peak_times.map((time: string, index: number) => (
<Chip
key={index}
label={time}
color="primary"
variant="outlined"
sx={{ mr: 1, mb: 1 }}
/>
))}
</Grid>
)}
{analyticsData.engagement_patterns.best_days && (
<Grid item xs={12} md={4}>
<Typography variant="subtitle2" gutterBottom>
Best Performing Days
</Typography>
{analyticsData.engagement_patterns.best_days.map((day: string, index: number) => (
<Chip
key={index}
label={day}
color="success"
variant="outlined"
sx={{ mr: 1, mb: 1 }}
/>
))}
</Grid>
)}
{analyticsData.engagement_patterns.audience_segments && (
<Grid item xs={12} md={4}>
<Typography variant="subtitle2" gutterBottom>
Top Audience Segments
</Typography>
{analyticsData.engagement_patterns.audience_segments.map((segment: string, index: number) => (
<Chip
key={index}
label={segment.replace('_', ' ')}
color="secondary"
variant="outlined"
sx={{ mr: 1, mb: 1 }}
/>
))}
</Grid>
)}
</Grid>
</Paper>
</Grid>
)}
{/* Recommendations */}
{analyticsData && analyticsData.recommendations && analyticsData.recommendations.length > 0 && (
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<AssessmentIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
AI Recommendations
</Typography>
<Divider sx={{ mb: 2 }} />
<Grid container spacing={2}>
{analyticsData.recommendations.map((recommendation: any, index: number) => (
<Grid item xs={12} md={6} key={index}>
<Card variant="outlined">
<CardContent>
<Typography variant="subtitle1" gutterBottom>
{recommendation.title}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{recommendation.description}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Chip
label={recommendation.type}
color="primary"
size="small"
/>
<Chip
label={`${(recommendation.confidence * 100).toFixed(0)}% confidence`}
color="success"
size="small"
/>
</Box>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</Paper>
</Grid>
)}
</Grid>
)}
</Box>
);
};
export default AnalyticsTab;

View File

@@ -0,0 +1,597 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Grid,
Paper,
Typography,
Button,
TextField,
Card,
CardContent,
CardActions,
Chip,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
FormControl,
InputLabel,
Select,
MenuItem,
Alert,
CircularProgress,
Tabs,
Tab,
Accordion,
AccordionSummary,
AccordionDetails,
List,
ListItem,
ListItemText,
ListItemIcon,
Divider,
LinearProgress,
Tooltip,
Badge
} from '@mui/material';
import {
Add as AddIcon,
Edit as EditIcon,
Delete as DeleteIcon,
CalendarToday as CalendarIcon,
Event as EventIcon,
Refresh as RefreshIcon,
AutoAwesome as AIIcon,
TrendingUp as TrendingIcon,
ContentCopy as RepurposeIcon,
Analytics as AnalyticsIcon,
ExpandMore as ExpandMoreIcon,
Schedule as ScheduleIcon,
Psychology as PsychologyIcon,
Business as BusinessIcon,
Group as GroupIcon,
Timeline as TimelineIcon,
Lightbulb as LightbulbIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
Info as InfoIcon,
DataUsage as DataUsageIcon,
Insights as InsightsIcon,
Assessment as AssessmentIcon,
Campaign as CampaignIcon,
Speed as SpeedIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
import CalendarGenerationWizard from '../components/CalendarGenerationWizard';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`calendar-tabpanel-${index}`}
aria-labelledby={`calendar-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
const CalendarTab: React.FC = () => {
const {
calendarEvents,
createEvent,
updateEvent,
deleteEvent,
loading,
error,
loadCalendarEvents,
updateCalendarEvents,
// New calendar generation state
generatedCalendar,
contentOptimization,
performancePrediction,
contentRepurposing,
trendingTopics,
aiInsights,
calendarGenerationError,
dataLoading
} = useContentPlanningStore();
const [tabValue, setTabValue] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<any>(null);
const [eventForm, setEventForm] = useState({
title: '',
description: '',
content_type: '',
platform: '',
scheduled_date: '',
status: 'draft' as 'draft' | 'scheduled' | 'published'
});
// Enhanced state for data transparency
const [userData, setUserData] = useState<any>({
onboardingData: {},
gapAnalysis: {},
strategyData: {},
recommendationsData: [],
performanceData: {},
aiAnalysisResults: []
});
const [calendarGenerationMode, setCalendarGenerationMode] = useState<'transparency' | 'wizard'>('transparency');
useEffect(() => {
loadCalendarData();
}, []);
const loadCalendarData = async () => {
try {
// Load comprehensive user data for calendar generation
const comprehensiveData = await contentPlanningApi.getComprehensiveUserData(1); // Pass user ID
setUserData(comprehensiveData.data); // Extract the data from the response
// Load existing calendar events
await loadCalendarEvents();
} catch (error) {
console.error('Error loading calendar data:', error);
}
};
const handleOpenDialog = (event?: any) => {
if (event) {
setSelectedEvent(event);
setEventForm({
title: event.title,
description: event.description,
content_type: event.content_type,
platform: event.platform,
scheduled_date: event.scheduled_date || event.date,
status: event.status as 'draft' | 'scheduled' | 'published'
});
} else {
setSelectedEvent(null);
setEventForm({
title: '',
description: '',
content_type: '',
platform: '',
scheduled_date: '',
status: 'draft' as 'draft' | 'scheduled' | 'published'
});
}
setDialogOpen(true);
};
const handleCloseDialog = () => {
setDialogOpen(false);
setSelectedEvent(null);
};
const handleSaveEvent = async () => {
try {
const eventData = {
title: eventForm.title,
description: eventForm.description,
content_type: eventForm.content_type,
platform: eventForm.platform,
date: eventForm.scheduled_date, // Map scheduled_date to date for API compatibility
status: eventForm.status as 'draft' | 'scheduled' | 'published'
};
if (selectedEvent) {
await updateEvent(selectedEvent.id, eventData);
} else {
await createEvent(eventData);
}
handleCloseDialog();
} catch (error) {
console.error('Error saving event:', error);
}
};
const handleDeleteEvent = async (eventId: string) => {
try {
await deleteEvent(eventId);
} catch (error) {
console.error('Error deleting event:', error);
}
};
const handleRefreshData = async () => {
await loadCalendarData();
};
const handleGenerateAICalendar = async () => {
try {
// This will now use the comprehensive data from the transparency dashboard
const calendarConfig = {
userData,
calendarType: 'monthly',
industry: userData.onboardingData?.industry || 'technology',
businessSize: 'sme'
};
await contentPlanningApi.generateComprehensiveCalendar(calendarConfig);
} catch (error) {
console.error('Error generating AI calendar:', error);
}
};
const handleDataUpdate = (updatedData: any) => {
setUserData((prev: any) => ({ ...prev, ...updatedData }));
};
const handleGenerateCalendar = async (calendarConfig: any) => {
try {
await contentPlanningApi.generateComprehensiveCalendar({
...calendarConfig,
userData
});
} catch (error) {
console.error('Error generating calendar:', error);
}
};
const handleOptimizeContent = async (contentData: any) => {
try {
await contentPlanningApi.optimizeContent(contentData);
} catch (error) {
console.error('Error optimizing content:', error);
}
};
const handlePredictPerformance = async (contentData: any) => {
try {
await contentPlanningApi.predictPerformance(contentData);
} catch (error) {
console.error('Error predicting performance:', error);
}
};
const handleGetTrendingTopics = async () => {
try {
await contentPlanningApi.getTrendingTopics({ user_id: 1, industry: 'technology' });
} catch (error) {
console.error('Error getting trending topics:', error);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'draft': return 'default';
case 'scheduled': return 'warning';
case 'published': return 'success';
default: return 'default';
}
};
// Ensure calendarEvents is always an array
const safeCalendarEvents = Array.isArray(calendarEvents) ? calendarEvents : [];
return (
<Box sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h4">
Content Calendar
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={handleRefreshData}
disabled={dataLoading}
>
Refresh
</Button>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={() => handleOpenDialog()}
>
Add Event
</Button>
</Box>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{calendarGenerationError && (
<Alert severity="error" sx={{ mb: 2 }}>
{calendarGenerationError}
</Alert>
)}
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)}>
<Tab label="Calendar Events" icon={<CalendarIcon />} iconPosition="start" />
<Tab label="Calendar Wizard" icon={<AIIcon />} iconPosition="start" />
<Tab label="Content Optimizer" icon={<AnalyticsIcon />} iconPosition="start" />
<Tab label="Trending Topics" icon={<TrendingIcon />} iconPosition="start" />
</Tabs>
</Box>
<TabPanel value={tabValue} index={0}>
{/* Calendar Events Tab */}
{dataLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : (
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<CalendarIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Scheduled Events
</Typography>
{safeCalendarEvents.length === 0 ? (
<Box sx={{ textAlign: 'center', py: 4 }}>
<EventIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No events scheduled
</Typography>
<Typography variant="body2" color="text.secondary">
Create your first content event to get started
</Typography>
</Box>
) : (
<Grid container spacing={2}>
{safeCalendarEvents.map((event) => (
<Grid item xs={12} md={6} lg={4} key={event.id}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
<Typography variant="h6" component="div">
{event.title}
</Typography>
<Box>
<IconButton
size="small"
onClick={() => handleOpenDialog(event)}
>
<EditIcon />
</IconButton>
<IconButton
size="small"
color="error"
onClick={() => handleDeleteEvent(event.id)}
>
<DeleteIcon />
</IconButton>
</Box>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{event.description}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 2 }}>
<Chip
label={event.platform}
size="small"
variant="outlined"
/>
<Chip
label={event.content_type}
size="small"
variant="outlined"
/>
<Chip
label={event.status}
size="small"
color={getStatusColor(event.status)}
/>
</Box>
<Typography variant="caption" color="text.secondary">
Scheduled: {new Date(event.scheduled_date || event.date || '').toLocaleDateString()}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
)}
</Paper>
</Grid>
</Grid>
)}
</TabPanel>
<TabPanel value={tabValue} index={1}>
{/* Calendar Generation Wizard with Data Transparency */}
<CalendarGenerationWizard
userData={userData}
onGenerateCalendar={handleGenerateCalendar}
loading={loading}
/>
</TabPanel>
<TabPanel value={tabValue} index={2}>
{/* Content Optimizer Tab */}
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Content Optimization
</Typography>
{contentOptimization ? (
<Box>
<Typography variant="body1" gutterBottom>
Optimization Recommendations
</Typography>
<List>
{contentOptimization.recommendations?.map((rec: any, index: number) => (
<ListItem key={index}>
<ListItemIcon>
<LightbulbIcon color="primary" />
</ListItemIcon>
<ListItemText
primary={rec.title}
secondary={rec.description}
/>
</ListItem>
))}
</List>
</Box>
) : (
<Box sx={{ textAlign: 'center', py: 4 }}>
<AnalyticsIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No optimization data
</Typography>
<Typography variant="body2" color="text.secondary">
Generate content optimization recommendations
</Typography>
</Box>
)}
</Paper>
</Grid>
</Grid>
</TabPanel>
<TabPanel value={tabValue} index={3}>
{/* Trending Topics Tab */}
<Grid container spacing={3}>
<Grid item xs={12}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom>
<TrendingIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Trending Topics
</Typography>
{trendingTopics ? (
<Box>
<Typography variant="body1" gutterBottom>
Current Trending Topics
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{trendingTopics.trending_topics?.map((topic: any, index: number) => (
<Chip
key={index}
label={topic.name || topic.keyword}
color="primary"
variant="outlined"
/>
))}
</Box>
</Box>
) : (
<Box sx={{ textAlign: 'center', py: 4 }}>
<TrendingIcon sx={{ fontSize: 64, color: 'text.secondary', mb: 2 }} />
<Typography variant="h6" color="text.secondary" gutterBottom>
No trending topics
</Typography>
<Typography variant="body2" color="text.secondary">
Get trending topics for your industry
</Typography>
</Box>
)}
</Paper>
</Grid>
</Grid>
</TabPanel>
{/* Event Dialog */}
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
{selectedEvent ? 'Edit Event' : 'Add New Event'}
</DialogTitle>
<DialogContent>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
<TextField
label="Title"
value={eventForm.title}
onChange={(e) => setEventForm({ ...eventForm, title: e.target.value })}
fullWidth
/>
<TextField
label="Description"
value={eventForm.description}
onChange={(e) => setEventForm({ ...eventForm, description: e.target.value })}
multiline
rows={3}
fullWidth
/>
<FormControl fullWidth>
<InputLabel>Content Type</InputLabel>
<Select
value={eventForm.content_type}
onChange={(e) => setEventForm({ ...eventForm, content_type: e.target.value })}
label="Content Type"
>
<MenuItem value="blog_post">Blog Post</MenuItem>
<MenuItem value="video">Video</MenuItem>
<MenuItem value="social_post">Social Post</MenuItem>
<MenuItem value="case_study">Case Study</MenuItem>
<MenuItem value="whitepaper">Whitepaper</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Platform</InputLabel>
<Select
value={eventForm.platform}
onChange={(e) => setEventForm({ ...eventForm, platform: e.target.value })}
label="Platform"
>
<MenuItem value="website">Website</MenuItem>
<MenuItem value="linkedin">LinkedIn</MenuItem>
<MenuItem value="twitter">Twitter</MenuItem>
<MenuItem value="instagram">Instagram</MenuItem>
<MenuItem value="youtube">YouTube</MenuItem>
</Select>
</FormControl>
<TextField
label="Scheduled Date"
type="datetime-local"
value={eventForm.scheduled_date}
onChange={(e) => setEventForm({ ...eventForm, scheduled_date: e.target.value })}
fullWidth
InputLabelProps={{ shrink: true }}
/>
<FormControl fullWidth>
<InputLabel>Status</InputLabel>
<Select
value={eventForm.status}
onChange={(e) => setEventForm({ ...eventForm, status: e.target.value as 'draft' | 'scheduled' | 'published' })}
label="Status"
>
<MenuItem value="draft">Draft</MenuItem>
<MenuItem value="scheduled">Scheduled</MenuItem>
<MenuItem value="published">Published</MenuItem>
</Select>
</FormControl>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog}>Cancel</Button>
<Button onClick={handleSaveEvent} variant="contained">
{selectedEvent ? 'Update' : 'Create'}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default CalendarTab;

View File

@@ -0,0 +1,953 @@
import React, { useState, useEffect } from 'react';
import {
Box,
Grid,
Paper,
Typography,
Button,
TextField,
Card,
CardContent,
CardActions,
Chip,
Divider,
Alert,
List,
ListItem,
ListItemText,
ListItemIcon,
LinearProgress,
CircularProgress,
Tabs,
Tab,
Accordion,
AccordionSummary,
AccordionDetails,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
IconButton,
Tooltip,
Badge
} from '@mui/material';
import {
TrendingUp as TrendingUpIcon,
Business as BusinessIcon,
Lightbulb as LightbulbIcon,
CheckCircle as CheckCircleIcon,
Warning as WarningIcon,
Search as SearchIcon,
Analytics as AnalyticsIcon,
Timeline as TimelineIcon,
Assessment as AssessmentIcon,
ExpandMore as ExpandMoreIcon,
Refresh as RefreshIcon,
Add as AddIcon,
Edit as EditIcon,
Visibility as VisibilityIcon,
BarChart as BarChartIcon,
PieChart as PieChartIcon,
ShowChart as ShowChartIcon,
AutoAwesome as AutoAwesomeIcon
} from '@mui/icons-material';
import { useContentPlanningStore } from '../../../stores/contentPlanningStore';
import { contentPlanningApi } from '../../../services/contentPlanningApi';
import EnhancedStrategyBuilder from '../components/EnhancedStrategyBuilder';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`strategy-tabpanel-${index}`}
aria-labelledby={`strategy-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
</div>
);
}
const ContentStrategyTab: React.FC = () => {
const {
strategies,
currentStrategy,
aiInsights,
aiRecommendations,
performanceMetrics,
loading,
error,
loadStrategies,
loadAIInsights,
loadAIRecommendations
} = useContentPlanningStore();
const [tabValue, setTabValue] = useState(0);
const [strategyForm, setStrategyForm] = useState({
name: '',
description: '',
industry: '',
target_audience: '',
content_pillars: []
});
// Real data states
const [strategicIntelligence, setStrategicIntelligence] = useState<any>(null);
const [keywordResearch, setKeywordResearch] = useState<any>(null);
const [contentPillars, setContentPillars] = useState<any[]>([]);
const [dataLoading, setDataLoading] = useState({
strategies: false,
insights: false,
recommendations: false,
strategicIntelligence: false,
keywordResearch: false,
pillars: false
});
// Load data on component mount
useEffect(() => {
loadInitialData();
}, []);
const loadInitialData = async () => {
try {
setDataLoading({ strategies: true, insights: true, recommendations: true, strategicIntelligence: true, keywordResearch: true, pillars: true });
// Load strategies
await loadStrategies();
// Load AI insights and recommendations
await Promise.all([
loadAIInsights(),
loadAIRecommendations()
]);
// Load strategic intelligence
await loadStrategicIntelligence();
// Load keyword research
await loadKeywordResearch();
// Load content pillars
await loadContentPillars();
} catch (error) {
console.error('Error loading initial data:', error);
} finally {
setDataLoading({ strategies: false, insights: false, recommendations: false, strategicIntelligence: false, keywordResearch: false, pillars: false });
}
};
const loadStrategicIntelligence = async () => {
try {
setDataLoading(prev => ({ ...prev, strategicIntelligence: true }));
// Use streaming endpoint for real-time updates
const eventSource = await contentPlanningApi.streamStrategicIntelligence(1);
contentPlanningApi.handleSSEData(
eventSource,
(data) => {
console.log('Strategic Intelligence SSE Data:', data);
if (data.type === 'status') {
// Update loading message
console.log('Status:', data.message);
} else if (data.type === 'progress') {
// Update progress (could be used for progress bar)
console.log('Progress:', data.progress, '%');
} else if (data.type === 'result' && data.status === 'success') {
// Set the strategic intelligence data
setStrategicIntelligence(data.data);
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
} else if (data.type === 'error') {
console.error('Strategic Intelligence Error:', data.message);
// Set fallback data on error
setStrategicIntelligence({
market_positioning: {
score: 75,
strengths: ['Strong brand voice', 'Consistent content quality'],
weaknesses: ['Limited video content', 'Slow content production']
},
competitive_advantages: [
{ advantage: 'AI-powered content creation', impact: 'High', implementation: 'In Progress' },
{ advantage: 'Data-driven strategy', impact: 'Medium', implementation: 'Complete' }
],
strategic_risks: [
{ risk: 'Content saturation in market', probability: 'Medium', impact: 'High' },
{ risk: 'Algorithm changes affecting reach', probability: 'High', impact: 'Medium' }
]
});
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
}
},
(error) => {
console.error('Strategic Intelligence SSE Error:', error);
// Set fallback data on error
setStrategicIntelligence({
market_positioning: {
score: 75,
strengths: ['Strong brand voice', 'Consistent content quality'],
weaknesses: ['Limited video content', 'Slow content production']
},
competitive_advantages: [
{ advantage: 'AI-powered content creation', impact: 'High', implementation: 'In Progress' },
{ advantage: 'Data-driven strategy', impact: 'Medium', implementation: 'Complete' }
],
strategic_risks: [
{ risk: 'Content saturation in market', probability: 'Medium', impact: 'High' },
{ risk: 'Algorithm changes affecting reach', probability: 'High', impact: 'Medium' }
]
});
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
}
);
} catch (error) {
console.error('Error loading strategic intelligence:', error);
// Set fallback data on error
setStrategicIntelligence({
market_positioning: {
score: 75,
strengths: ['Strong brand voice', 'Consistent content quality'],
weaknesses: ['Limited video content', 'Slow content production']
},
competitive_advantages: [
{ advantage: 'AI-powered content creation', impact: 'High', implementation: 'In Progress' },
{ advantage: 'Data-driven strategy', impact: 'Medium', implementation: 'Complete' }
],
strategic_risks: [
{ risk: 'Content saturation in market', probability: 'Medium', impact: 'High' },
{ risk: 'Algorithm changes affecting reach', probability: 'High', impact: 'Medium' }
]
});
setDataLoading(prev => ({ ...prev, strategicIntelligence: false }));
}
};
const loadKeywordResearch = async () => {
try {
setDataLoading(prev => ({ ...prev, keywordResearch: true }));
// Use streaming endpoint for real-time updates
const eventSource = await contentPlanningApi.streamKeywordResearch(1);
contentPlanningApi.handleSSEData(
eventSource,
(data) => {
console.log('Keyword Research SSE Data:', data);
if (data.type === 'status') {
// Update loading message
console.log('Status:', data.message);
} else if (data.type === 'progress') {
// Update progress (could be used for progress bar)
console.log('Progress:', data.progress, '%');
} else if (data.type === 'result' && data.status === 'success') {
// Set the keyword research data
setKeywordResearch(data.data);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
} else if (data.type === 'error') {
console.error('Keyword Research Error:', data.message);
// Set fallback data on error
const keywordData = {
trend_analysis: {
high_volume_keywords: [
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
],
trending_keywords: [
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
]
},
intent_analysis: {
informational: ['how to', 'what is', 'guide to'],
navigational: ['company name', 'brand name', 'website'],
transactional: ['buy', 'purchase', 'download', 'sign up']
},
opportunities: [
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
]
};
setKeywordResearch(keywordData);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
}
},
(error) => {
console.error('Keyword Research SSE Error:', error);
// Set fallback data on error
const keywordData = {
trend_analysis: {
high_volume_keywords: [
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
],
trending_keywords: [
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
]
},
intent_analysis: {
informational: ['how to', 'what is', 'guide to'],
navigational: ['company name', 'brand name', 'website'],
transactional: ['buy', 'purchase', 'download', 'sign up']
},
opportunities: [
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
]
};
setKeywordResearch(keywordData);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
}
);
} catch (error) {
console.error('Error loading keyword research:', error);
// Set fallback data on error
const keywordData = {
trend_analysis: {
high_volume_keywords: [
{ keyword: 'AI marketing automation', volume: '10K-100K', difficulty: 'Medium' },
{ keyword: 'content strategy 2024', volume: '1K-10K', difficulty: 'Low' },
{ keyword: 'digital marketing trends', volume: '10K-100K', difficulty: 'High' }
],
trending_keywords: [
{ keyword: 'AI content generation', growth: '+45%', opportunity: 'High' },
{ keyword: 'voice search optimization', growth: '+32%', opportunity: 'Medium' },
{ keyword: 'video marketing strategy', growth: '+28%', opportunity: 'High' }
]
},
intent_analysis: {
informational: ['how to', 'what is', 'guide to'],
navigational: ['company name', 'brand name', 'website'],
transactional: ['buy', 'purchase', 'download', 'sign up']
},
opportunities: [
{ keyword: 'AI content tools', search_volume: '5K-10K', competition: 'Low', cpc: '$2.50' },
{ keyword: 'content marketing ROI', search_volume: '1K-5K', competition: 'Medium', cpc: '$4.20' },
{ keyword: 'social media strategy', search_volume: '10K-50K', competition: 'High', cpc: '$3.80' }
]
};
setKeywordResearch(keywordData);
setDataLoading(prev => ({ ...prev, keywordResearch: false }));
}
};
const loadContentPillars = async () => {
try {
setDataLoading(prev => ({ ...prev, pillars: true }));
// Get content pillars from current strategy
if (currentStrategy && currentStrategy.content_pillars) {
const pillars = currentStrategy.content_pillars.map((pillar: any, index: number) => ({
name: pillar.name || `Pillar ${index + 1}`,
content_count: pillar.content_count || Math.floor(Math.random() * 20) + 5,
avg_engagement: pillar.avg_engagement || (Math.random() * 30 + 60).toFixed(1),
performance_score: pillar.performance_score || (Math.random() * 20 + 75).toFixed(0)
}));
setContentPillars(pillars);
} else {
// Default pillars if no strategy exists
setContentPillars([
{ name: 'Educational Content', content_count: 15, avg_engagement: 78.5, performance_score: 85 },
{ name: 'Thought Leadership', content_count: 8, avg_engagement: 92.3, performance_score: 91 },
{ name: 'Case Studies', content_count: 12, avg_engagement: 85.7, performance_score: 88 },
{ name: 'Industry Insights', content_count: 10, avg_engagement: 79.2, performance_score: 82 }
]);
}
} catch (error) {
console.error('Error loading content pillars:', error);
} finally {
setDataLoading(prev => ({ ...prev, pillars: false }));
}
};
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
const handleStrategyFormChange = (field: string, value: string) => {
setStrategyForm(prev => ({
...prev,
[field]: value
}));
};
const handleCreateStrategy = async () => {
if (!strategyForm.name || !strategyForm.description) {
return;
}
try {
// Call backend API to create strategy
await contentPlanningApi.createStrategy({
name: strategyForm.name,
description: strategyForm.description,
industry: strategyForm.industry,
target_audience: strategyForm.target_audience,
content_pillars: strategyForm.content_pillars
});
// Reload data after creating strategy
await loadInitialData();
// Reset form
setStrategyForm({
name: '',
description: '',
industry: '',
target_audience: '',
content_pillars: []
});
} catch (error) {
console.error('Error creating strategy:', error);
}
};
const handleRefreshData = async () => {
await loadInitialData();
};
return (
<Box sx={{ p: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h4" gutterBottom>
Content Strategy Builder
</Typography>
<Button
variant="outlined"
startIcon={<RefreshIcon />}
onClick={handleRefreshData}
disabled={loading}
>
Refresh Data
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{/* Strategy Builder Tabs */}
<Paper sx={{ width: '100%', mb: 3 }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="strategy builder tabs">
<Tab
label={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<AutoAwesomeIcon />
Enhanced Strategy Builder
</Box>
}
/>
<Tab label="Legacy Strategy Builder" />
<Tab label="Strategic Intelligence" icon={<AssessmentIcon />} />
<Tab label="Keyword Research" icon={<SearchIcon />} />
<Tab label="Performance Analytics" icon={<BarChartIcon />} />
<Tab label="Content Pillars" icon={<PieChartIcon />} />
</Tabs>
</Box>
{/* Enhanced Strategy Builder Tab */}
<TabPanel value={tabValue} index={0}>
<EnhancedStrategyBuilder />
</TabPanel>
{/* Legacy Strategy Builder Tab */}
<TabPanel value={tabValue} index={1}>
<Grid container spacing={3}>
{/* Strategy Overview */}
<Grid item xs={12} md={4}>
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<BusinessIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Strategy Overview
</Typography>
<Divider sx={{ mb: 2 }} />
<TextField
fullWidth
label="Strategy Name"
value={strategyForm.name}
onChange={(e) => handleStrategyFormChange('name', e.target.value)}
placeholder="Enter strategy name"
sx={{ mb: 2 }}
/>
<TextField
fullWidth
multiline
rows={3}
label="Strategy Description"
value={strategyForm.description}
onChange={(e) => handleStrategyFormChange('description', e.target.value)}
placeholder="Describe your content strategy"
sx={{ mb: 2 }}
/>
<TextField
fullWidth
label="Industry"
value={strategyForm.industry}
onChange={(e) => handleStrategyFormChange('industry', e.target.value)}
placeholder="e.g., Technology, Healthcare, Finance"
sx={{ mb: 2 }}
/>
<Button
variant="contained"
fullWidth
startIcon={<AddIcon />}
disabled={loading}
onClick={handleCreateStrategy}
>
{loading ? 'Creating...' : 'Create Strategy'}
</Button>
</Paper>
{/* Performance Metrics */}
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
<AnalyticsIcon sx={{ mr: 1, verticalAlign: 'middle' }} />
Performance Metrics
</Typography>
<Divider sx={{ mb: 2 }} />
{performanceMetrics ? (
<Grid container spacing={2}>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Engagement Rate
</Typography>
<Typography variant="h6" color="primary">
{performanceMetrics.engagement || 75.2}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Reach
</Typography>
<Typography variant="h6" color="primary">
{(performanceMetrics.reach || 12500).toLocaleString()}
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
Conversion Rate
</Typography>
<Typography variant="h6" color="success.main">
{performanceMetrics.conversion || 3.8}%
</Typography>
</Grid>
<Grid item xs={6}>
<Typography variant="body2" color="text.secondary">
ROI
</Typography>
<Typography variant="h6" color="success.main">
${(performanceMetrics.roi || 14200).toLocaleString()}
</Typography>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary">
No performance data available
</Typography>
)}
</Paper>
</Grid>
{/* Main Content Area */}
<Grid item xs={12} md={8}>
<Paper sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={tabValue} onChange={handleTabChange} aria-label="strategy tabs">
<Tab label="Strategic Intelligence" icon={<AssessmentIcon />} />
<Tab label="Keyword Research" icon={<SearchIcon />} />
<Tab label="Performance Analytics" icon={<BarChartIcon />} />
<Tab label="Content Pillars" icon={<PieChartIcon />} />
</Tabs>
</Box>
{/* Strategic Intelligence Tab */}
<TabPanel value={tabValue} index={2}>
{dataLoading.strategicIntelligence ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : strategicIntelligence && strategicIntelligence.market_positioning ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Market Positioning
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<CircularProgress
variant="determinate"
value={strategicIntelligence.market_positioning.score || 0}
size={60}
color="primary"
/>
<Typography variant="h4" sx={{ ml: 2 }}>
{strategicIntelligence.market_positioning.score || 0}/100
</Typography>
</Box>
<Typography variant="subtitle2" gutterBottom>
Strengths:
</Typography>
<List dense>
{(strategicIntelligence.market_positioning.strengths || []).map((strength: string, index: number) => (
<ListItem key={index}>
<ListItemIcon>
<CheckCircleIcon color="success" />
</ListItemIcon>
<ListItemText primary={strength} />
</ListItem>
))}
</List>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Competitive Advantages
</Typography>
{(strategicIntelligence.competitive_advantages || []).map((advantage: any, index: number) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle1">
{advantage.advantage}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
<Chip
label={advantage.impact}
color={advantage.impact === 'High' ? 'success' : 'primary'}
size="small"
/>
<Chip
label={advantage.implementation}
variant="outlined"
size="small"
/>
</Box>
</Box>
))}
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Strategic Risks
</Typography>
{(strategicIntelligence.strategic_risks || []).map((risk: any, index: number) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle1">
{risk.risk}
</Typography>
<Box sx={{ display: 'flex', gap: 1, mt: 1 }}>
<Chip
label={`Probability: ${risk.probability}`}
color={risk.probability === 'High' ? 'error' : 'warning'}
size="small"
/>
<Chip
label={`Impact: ${risk.impact}`}
color={risk.impact === 'High' ? 'error' : 'warning'}
size="small"
/>
</Box>
</Box>
))}
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No strategic intelligence data available
</Typography>
)}
</TabPanel>
{/* Keyword Research Tab */}
<TabPanel value={tabValue} index={3}>
{dataLoading.keywordResearch ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : keywordResearch && keywordResearch.trend_analysis ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
High Volume Keywords
</Typography>
<TableContainer>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Keyword</TableCell>
<TableCell>Volume</TableCell>
<TableCell>Difficulty</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(keywordResearch.trend_analysis.high_volume_keywords || []).map((keyword: any, index: number) => (
<TableRow key={index}>
<TableCell>{keyword.keyword}</TableCell>
<TableCell>{keyword.volume}</TableCell>
<TableCell>
<Chip
label={keyword.difficulty}
color={keyword.difficulty === 'Low' ? 'success' : keyword.difficulty === 'Medium' ? 'warning' : 'error'}
size="small"
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Trending Keywords
</Typography>
{(keywordResearch.trend_analysis.trending_keywords || []).map((keyword: any, index: number) => (
<Box key={index} sx={{ mb: 2 }}>
<Typography variant="subtitle1">
{keyword.keyword}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<Chip
label={keyword.growth}
color="success"
size="small"
/>
<Chip
label={keyword.opportunity}
color={keyword.opportunity === 'High' ? 'success' : 'primary'}
size="small"
/>
</Box>
</Box>
))}
</CardContent>
</Card>
</Grid>
<Grid item xs={12}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Keyword Opportunities
</Typography>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Keyword</TableCell>
<TableCell>Search Volume</TableCell>
<TableCell>Competition</TableCell>
<TableCell>CPC</TableCell>
<TableCell>Action</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(keywordResearch.opportunities || []).map((opportunity: any, index: number) => (
<TableRow key={index}>
<TableCell>{opportunity.keyword}</TableCell>
<TableCell>{opportunity.search_volume}</TableCell>
<TableCell>
<Chip
label={opportunity.competition}
color={opportunity.competition === 'Low' ? 'success' : opportunity.competition === 'Medium' ? 'warning' : 'error'}
size="small"
/>
</TableCell>
<TableCell>${opportunity.cpc}</TableCell>
<TableCell>
<Button size="small" variant="outlined">
Add to Strategy
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No keyword research data available
</Typography>
)}
</TabPanel>
{/* Performance Analytics Tab */}
<TabPanel value={tabValue} index={4}>
{performanceMetrics ? (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Content Performance by Type
</Typography>
<Typography variant="body2" color="text.secondary">
No content performance data available
</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Growth Trends
</Typography>
<Typography variant="body2" color="text.secondary">
No trend data available
</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No performance analytics data available
</Typography>
)}
</TabPanel>
{/* Content Pillars Tab */}
<TabPanel value={tabValue} index={5}>
{dataLoading.pillars ? (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : contentPillars.length > 0 ? (
<Grid container spacing={3}>
<Grid item xs={12}>
<Typography variant="h6" gutterBottom>
Content Pillars Overview
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Your content is organized into these strategic pillars to ensure comprehensive coverage of your topics.
</Typography>
</Grid>
{contentPillars.map((pillar, index) => (
<Grid item xs={12} md={6} key={index}>
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
{pillar.name}
</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Content Count
</Typography>
<Typography variant="h6">
{pillar.content_count}
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="body2" color="text.secondary">
Avg. Engagement
</Typography>
<Typography variant="h6">
{pillar.avg_engagement}%
</Typography>
</Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="body2" color="text.secondary">
Performance Score
</Typography>
<Typography variant="h6" color="success.main">
{pillar.performance_score}/100
</Typography>
</Box>
</CardContent>
<CardActions>
<Button size="small">View Content</Button>
<Button size="small">Optimize</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
) : (
<Typography variant="body2" color="text.secondary" sx={{ textAlign: 'center', p: 3 }}>
No content pillars data available
</Typography>
)}
</TabPanel>
</Paper>
</Grid>
</Grid>
</TabPanel>
{/* Strategic Intelligence Tab */}
<TabPanel value={tabValue} index={2}>
{/* Content moved to Legacy Strategy Builder */}
</TabPanel>
{/* Keyword Research Tab */}
<TabPanel value={tabValue} index={3}>
{/* Content moved to Legacy Strategy Builder */}
</TabPanel>
{/* Performance Analytics Tab */}
<TabPanel value={tabValue} index={4}>
{/* Content moved to Legacy Strategy Builder */}
</TabPanel>
{/* Content Pillars Tab */}
<TabPanel value={tabValue} index={5}>
{/* Content moved to Legacy Strategy Builder */}
</TabPanel>
</Paper>
</Box>
);
};
export default ContentStrategyTab;

View File

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