ALwrity Version 0.5.0 (Fastapi + React )
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user