Recovered state: integrated TrendSurferAgent, restored frontend/backend files, and cleaned up recovery scripts
This commit is contained in:
@@ -13,13 +13,16 @@ import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
Divider,
|
||||
Avatar
|
||||
Avatar,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
CircularProgress
|
||||
} from '@mui/material';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useAuth, useUser, SignInButton, SignOutButton } from '@clerk/clerk-react';
|
||||
import { apiClient } from '../../api/client';
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Person as PersonIcon,
|
||||
ExitToApp as ExitIcon,
|
||||
@@ -27,7 +30,9 @@ import {
|
||||
MoreVert as MoreVertIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
Info as InfoIcon
|
||||
Info as InfoIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
AutoAwesome as AIIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Shared components
|
||||
@@ -37,9 +42,6 @@ import { SEOCopilotKitProvider, SEOCopilotSuggestions } from './index';
|
||||
// Removed SEOCopilotTest
|
||||
import useSEOCopilotStore from '../../stores/seoCopilotStore';
|
||||
|
||||
// GSC Components
|
||||
import GSCLoginButton from './components/GSCLoginButton';
|
||||
|
||||
// Zustand store
|
||||
import { useSEODashboardStore } from '../../stores/seoDashboardStore';
|
||||
|
||||
@@ -55,6 +57,14 @@ import { useBingOAuth } from '../../hooks/useBingOAuth';
|
||||
import { useGSCConnection } from '../OnboardingWizard/common/useGSCConnection';
|
||||
|
||||
// SEO Dashboard component
|
||||
import { SitemapBenchmarkResults } from '../OnboardingWizard/CompetitorAnalysisStep/SitemapBenchmarkResults';
|
||||
import { StrategicInsightsResults } from '../OnboardingWizard/CompetitorAnalysisStep/StrategicInsightsResults';
|
||||
import { AdvertoolsInsights } from './components/AdvertoolsInsights';
|
||||
|
||||
// Phase 2B: Semantic Dashboard components
|
||||
import SemanticHealthCard from './components/SemanticHealthCard';
|
||||
import SemanticInsights from './components/SemanticInsights';
|
||||
|
||||
const SEODashboard: React.FC = () => {
|
||||
// Clerk authentication hooks
|
||||
const { isSignedIn, isLoaded } = useAuth();
|
||||
@@ -102,6 +112,12 @@ const SEODashboard: React.FC = () => {
|
||||
|
||||
// Competitor analysis data from onboarding step 3
|
||||
const [competitorAnalysisData, setCompetitorAnalysisData] = useState<any>(null);
|
||||
const [deepCompetitorAnalysisData, setDeepCompetitorAnalysisData] = useState<any>(null);
|
||||
const [strategicInsightsHistory, setStrategicInsightsHistory] = useState<any[]>([]);
|
||||
const [strategicInsightsLoading, setStrategicInsightsLoading] = useState(false);
|
||||
const [competitiveSitemapBenchmarkingReport, setCompetitiveSitemapBenchmarkingReport] = useState<any>(null);
|
||||
const [competitiveSitemapBenchmarkingLoading, setCompetitiveSitemapBenchmarkingLoading] = useState(false);
|
||||
const [competitiveSitemapBenchmarkingError, setCompetitiveSitemapBenchmarkingError] = useState<string | null>(null);
|
||||
|
||||
// PlatformAnalytics refresh handle
|
||||
const platformRefreshRef = useRef<(() => Promise<void>) | null>(null);
|
||||
@@ -120,8 +136,23 @@ const SEODashboard: React.FC = () => {
|
||||
// Load competitor analysis data on component mount
|
||||
useEffect(() => {
|
||||
loadCompetitorAnalysisData();
|
||||
fetchStrategicInsightsHistory();
|
||||
}, []);
|
||||
|
||||
const fetchStrategicInsightsHistory = async () => {
|
||||
setStrategicInsightsLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get('/api/seo-dashboard/strategic-insights/history');
|
||||
if (res.data?.history?.length > 0) {
|
||||
setStrategicInsightsHistory(res.data.history);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch strategic insights history", e);
|
||||
} finally {
|
||||
setStrategicInsightsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Reconnect handlers using existing OAuth hooks
|
||||
const handleGSCReconnect = async () => {
|
||||
try {
|
||||
@@ -220,6 +251,35 @@ const SEODashboard: React.FC = () => {
|
||||
console.log('SEO overview response:', response.status, response.statusText);
|
||||
console.log('Real SEO data received:', response.data);
|
||||
setData(response.data);
|
||||
|
||||
try {
|
||||
const deepResponse = await apiClient.get('/api/seo-dashboard/deep-competitor-analysis', {
|
||||
params: { site_url: websiteUrl }
|
||||
});
|
||||
setDeepCompetitorAnalysisData(deepResponse.data);
|
||||
} catch (e) {
|
||||
console.warn('Deep competitor analysis not available yet:', e);
|
||||
setDeepCompetitorAnalysisData(null);
|
||||
}
|
||||
|
||||
try {
|
||||
const sitemapBenchResponse = await apiClient.get('/api/seo/competitive-sitemap-benchmarking');
|
||||
const report = sitemapBenchResponse?.data?.data?.report ?? null;
|
||||
setCompetitiveSitemapBenchmarkingReport(report);
|
||||
} catch (e) {
|
||||
console.warn('Competitive sitemap benchmarking not available yet:', e);
|
||||
setCompetitiveSitemapBenchmarkingReport(null);
|
||||
}
|
||||
|
||||
try {
|
||||
setStrategicInsightsLoading(true);
|
||||
const strategicHistoryRes = await apiClient.get('/api/seo-dashboard/strategic-insights/history');
|
||||
setStrategicInsightsHistory(strategicHistoryRes.data?.history || []);
|
||||
} catch (e) {
|
||||
console.warn('Strategic insights history not available yet:', e);
|
||||
} finally {
|
||||
setStrategicInsightsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching SEO dashboard data:', error);
|
||||
// Fallback to mock data on error
|
||||
@@ -269,6 +329,8 @@ const SEODashboard: React.FC = () => {
|
||||
website_url: websiteUrl || undefined // Convert null to undefined for TypeScript
|
||||
};
|
||||
setData(mockData);
|
||||
setDeepCompetitorAnalysisData(null);
|
||||
setCompetitiveSitemapBenchmarkingReport(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -318,7 +380,6 @@ const SEODashboard: React.FC = () => {
|
||||
setLoading(true);
|
||||
await refreshSEOAnalysis();
|
||||
await fetchPlatformStatus();
|
||||
setLastRefresh(new Date());
|
||||
} catch (error) {
|
||||
console.error('Error refreshing data:', error);
|
||||
} finally {
|
||||
@@ -326,6 +387,20 @@ const SEODashboard: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const runStrategicInsights = async () => {
|
||||
setStrategicInsightsLoading(true);
|
||||
try {
|
||||
const res = await apiClient.post('/api/seo-dashboard/strategic-insights/run');
|
||||
if (res.data?.success) {
|
||||
setStrategicInsightsHistory(prev => [res.data.report, ...prev]);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Failed to run strategic insights:', e);
|
||||
} finally {
|
||||
setStrategicInsightsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Background jobs visibility (user-triggered)
|
||||
const [showBackgroundJobs, setShowBackgroundJobs] = useState(false);
|
||||
|
||||
@@ -368,6 +443,21 @@ const SEODashboard: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const runCompetitiveSitemapBenchmarking = async () => {
|
||||
setCompetitiveSitemapBenchmarkingError(null);
|
||||
setCompetitiveSitemapBenchmarkingLoading(true);
|
||||
try {
|
||||
await apiClient.post('/api/seo/competitive-sitemap-benchmarking/run', { max_competitors: null });
|
||||
const sitemapBenchResponse = await apiClient.get('/api/seo/competitive-sitemap-benchmarking');
|
||||
const report = sitemapBenchResponse?.data?.data?.report ?? null;
|
||||
setCompetitiveSitemapBenchmarkingReport(report);
|
||||
} catch (e: any) {
|
||||
setCompetitiveSitemapBenchmarkingError(e?.response?.data?.detail || e?.message || 'Failed to run benchmark');
|
||||
} finally {
|
||||
setCompetitiveSitemapBenchmarkingLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton variant="rectangular" height={200} />;
|
||||
@@ -764,6 +854,123 @@ const SEODashboard: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Full Site Technical SEO Audit (from onboarding background job) */}
|
||||
{data.technical_seo_audit && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🧩 Technical SEO Audit
|
||||
</Typography>
|
||||
<Tooltip title="Full-site audit runs automatically after onboarding. Low-scoring pages are marked as Fix Scheduled.">
|
||||
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
{data.technical_seo_audit.status === 'scheduled' && (
|
||||
<Chip
|
||||
icon={<ScheduleIcon />}
|
||||
label={`Scheduled${data.technical_seo_audit.next_execution ? ` • ${new Date(data.technical_seo_audit.next_execution).toLocaleString()}` : ''}`}
|
||||
sx={{ bgcolor: 'rgba(255, 193, 7, 0.15)', color: '#FFC107' }}
|
||||
/>
|
||||
)}
|
||||
{data.technical_seo_audit.status === 'ready' && (
|
||||
<Chip
|
||||
icon={<CheckCircleIcon />}
|
||||
label="Results Available"
|
||||
sx={{ bgcolor: 'rgba(76, 175, 80, 0.15)', color: '#4CAF50' }}
|
||||
/>
|
||||
)}
|
||||
{data.technical_seo_audit.status === 'error' && (
|
||||
<Chip
|
||||
label="Audit Error"
|
||||
sx={{ bgcolor: 'rgba(244, 67, 54, 0.15)', color: '#F44336' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{data.technical_seo_audit.status === 'scheduled' && (
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
Full-site audit runs automatically after onboarding. This may take a few minutes depending on how many pages we discover.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Pages Audited
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: 'white', fontWeight: 700 }}>
|
||||
{data.technical_seo_audit.pages_audited}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Average Score
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#2196F3', fontWeight: 700 }}>
|
||||
{data.technical_seo_audit.avg_score}/100
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Fix Scheduled
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#FF9800', fontWeight: 700 }}>
|
||||
{data.technical_seo_audit.fix_scheduled_pages}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{data.technical_seo_audit.worst_pages?.length > 0 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ color: 'white', fontWeight: 600, mb: 1 }}>
|
||||
Lowest Scoring Pages
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{data.technical_seo_audit.worst_pages.slice(0, 5).map((p) => (
|
||||
<Box
|
||||
key={p.page_url}
|
||||
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2 }}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: 'rgba(255, 255, 255, 0.85)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
|
||||
title={p.page_url}
|
||||
>
|
||||
{p.page_url}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Chip
|
||||
size="small"
|
||||
label={`${p.overall_score}/100`}
|
||||
sx={{ bgcolor: 'rgba(33, 150, 243, 0.15)', color: '#90CAF9' }}
|
||||
/>
|
||||
<Chip
|
||||
size="small"
|
||||
label={p.status === 'fix_scheduled' ? 'Fix Scheduled' : p.status}
|
||||
sx={{ bgcolor: 'rgba(255, 152, 0, 0.15)', color: '#FFB74D' }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</GlassCard>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Data-Driven Content Intelligence (Advertools) */}
|
||||
{data.advertools_insights && (
|
||||
<AdvertoolsInsights data={data.advertools_insights} />
|
||||
)}
|
||||
|
||||
{/* Competitive Analysis from Onboarding Step 3 */}
|
||||
{competitorAnalysisData && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
@@ -872,6 +1079,359 @@ const SEODashboard: React.FC = () => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Strategic Insights (Winning Moves) */}
|
||||
{strategicInsightsHistory.length > 0 && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🏆 Strategic Insights (Winning Moves)
|
||||
</Typography>
|
||||
<Tooltip title="AI-generated weekly strategic briefs to outperform competitors.">
|
||||
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Chip
|
||||
label={`Latest: ${new Date(strategicInsightsHistory[0].generated_at).toLocaleDateString()}`}
|
||||
size="small"
|
||||
sx={{ bgcolor: 'rgba(139, 92, 246, 0.15)', color: '#a78bfa' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<StrategicInsightsResults
|
||||
report={strategicInsightsHistory[0]}
|
||||
hideCreateContent={false}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Phase 2B: Semantic Intelligence Dashboard */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🧠 Semantic Intelligence
|
||||
</Typography>
|
||||
<Tooltip title="Real-time semantic analysis powered by AI. Updates every 24 hours.">
|
||||
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* Semantic Health Overview */}
|
||||
<Grid container spacing={2} sx={{ mb: 3 }}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<SemanticHealthCard compact />
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
{/* Placeholder for additional semantic metrics */}
|
||||
<SemanticInsights maxInsights={2} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Full Semantic Dashboard */}
|
||||
<SemanticInsights />
|
||||
</Box>
|
||||
|
||||
{/* Deep Competitor Analysis (auto-scheduled) */}
|
||||
{deepCompetitorAnalysisData && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🔍 Deep Competitor Analysis
|
||||
</Typography>
|
||||
<Tooltip title="Auto-scheduled after onboarding completion. Uses Step 2 website insights and Step 3 competitors.">
|
||||
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={4}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Status
|
||||
</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={(deepCompetitorAnalysisData.status || 'unknown').toString()}
|
||||
sx={{ bgcolor: 'rgba(34, 197, 94, 0.15)', color: '#86efac', fontWeight: 700 }}
|
||||
/>
|
||||
{deepCompetitorAnalysisData.last_status && (
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 1, color: 'rgba(255, 255, 255, 0.6)' }}>
|
||||
Last run: {deepCompetitorAnalysisData.last_status}
|
||||
</Typography>
|
||||
)}
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Competitors
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ color: '#4CAF50', fontWeight: 700 }}>
|
||||
{deepCompetitorAnalysisData.competitors_count ?? (deepCompetitorAnalysisData.report?.competitors?.length || 0)}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
analyzed
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={4}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 1 }}>
|
||||
Schedule
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<ScheduleIcon sx={{ color: 'rgba(255,255,255,0.7)', fontSize: 18 }} />
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.85)' }}>
|
||||
{deepCompetitorAnalysisData.next_execution
|
||||
? deepCompetitorAnalysisData.next_execution
|
||||
: (deepCompetitorAnalysisData.last_run ? 'Completed' : 'Pending')}
|
||||
</Typography>
|
||||
</Box>
|
||||
{deepCompetitorAnalysisData.last_run && (
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 1, color: 'rgba(255, 255, 255, 0.6)' }}>
|
||||
Last run: {deepCompetitorAnalysisData.last_run}
|
||||
</Typography>
|
||||
)}
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{!deepCompetitorAnalysisData.report && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Alert severity="info">
|
||||
Deep competitor analysis is scheduled or running. Once complete, the full per-competitor extraction, AI analysis, and aggregated insights will appear here.
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{deepCompetitorAnalysisData.report?.aggregation && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 2 }}>
|
||||
Aggregated Insights
|
||||
</Typography>
|
||||
<GlassCard sx={{ p: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ color: 'rgba(255,255,255,0.9)', fontWeight: 700, mb: 1 }}>
|
||||
Common Themes
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)', mb: 2 }}>
|
||||
{(deepCompetitorAnalysisData.report.aggregation.common_patterns?.common_themes || []).slice(0, 8).join(' • ') || '—'}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="subtitle2" sx={{ color: 'rgba(255,255,255,0.9)', fontWeight: 700, mb: 1 }}>
|
||||
Top Opportunities
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)', mb: 2 }}>
|
||||
{(deepCompetitorAnalysisData.report.aggregation.content_gaps_and_opportunities || [])
|
||||
.slice(0, 5)
|
||||
.map((g: any) => g.gap)
|
||||
.filter(Boolean)
|
||||
.join(' • ') || '—'}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="subtitle2" sx={{ color: 'rgba(255,255,255,0.9)', fontWeight: 700, mb: 1 }}>
|
||||
Recommended Actions
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)' }}>
|
||||
{(deepCompetitorAnalysisData.report.aggregation.strategic_recommendations || [])
|
||||
.slice(0, 5)
|
||||
.map((r: any) => r.action)
|
||||
.filter(Boolean)
|
||||
.join(' • ') || '—'}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{deepCompetitorAnalysisData.report?.competitors?.length > 0 && (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600, mb: 2 }}>
|
||||
Per-Competitor Details
|
||||
</Typography>
|
||||
{deepCompetitorAnalysisData.report.competitors.slice(0, 25).map((c: any, idx: number) => {
|
||||
const input = c?.input || {};
|
||||
const extraction = c?.extraction || {};
|
||||
const ai = c?.ai_analysis || {};
|
||||
const title = input.name || input.domain || `Competitor ${idx + 1}`;
|
||||
const domain = input.domain || input.url || '';
|
||||
return (
|
||||
<Accordion key={`${domain}-${idx}`} sx={{ bgcolor: 'rgba(255,255,255,0.06)', mb: 1, borderRadius: 2 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon sx={{ color: 'rgba(255,255,255,0.8)' }} />}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="subtitle2" sx={{ color: 'white', fontWeight: 700 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
|
||||
{domain}
|
||||
</Typography>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ color: 'white', fontWeight: 700, mb: 1 }}>
|
||||
Extraction
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)', mb: 1 }}>
|
||||
{extraction.page_meta?.title || '—'}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
|
||||
{(extraction.page_meta?.meta_description || '').slice(0, 220) || '—'}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)', mt: 2 }}>
|
||||
CTA signals: {(extraction.signals?.cta_signals?.keyword_hits || []).slice(0, 8).join(', ') || '—'}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)', mt: 1 }}>
|
||||
Proof signals: {(extraction.signals?.proof_signals?.keyword_hits || []).slice(0, 6).join(', ') || '—'}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<GlassCard sx={{ p: 2 }}>
|
||||
<Typography variant="subtitle2" sx={{ color: 'white', fontWeight: 700, mb: 1 }}>
|
||||
AI Analysis
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.85)', mb: 1 }}>
|
||||
Value prop: {ai.positioning?.value_prop || '—'}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.85)', mb: 1 }}>
|
||||
Primary offer: {ai.positioning?.primary_offer || '—'}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.85)', mb: 1 }}>
|
||||
Themes: {(ai.content_strategy?.themes || []).slice(0, 6).join(' • ') || '—'}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.85)' }}>
|
||||
Opportunities vs you: {(ai.comparison_to_user_baseline?.opportunities || []).slice(0, 4).join(' • ') || '—'}
|
||||
</Typography>
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Weekly Strategic Brief */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🧠 Weekly Strategy Brief
|
||||
</Typography>
|
||||
<Tooltip title="AI-powered strategic insights based on competitor content velocity and market shifts.">
|
||||
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={strategicInsightsLoading ? <CircularProgress size={20} color="inherit" /> : <AIIcon />}
|
||||
onClick={runStrategicInsights}
|
||||
disabled={strategicInsightsLoading}
|
||||
sx={{
|
||||
bgcolor: '#8b5cf6',
|
||||
'&:hover': { bgcolor: '#7c3aed' },
|
||||
textTransform: 'none',
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{strategicInsightsLoading ? 'Analyzing...' : 'Run Analysis Now'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{strategicInsightsHistory.length > 0 ? (
|
||||
<GlassCard sx={{ p: 0, overflow: 'hidden', border: 'none', bgcolor: 'transparent' }}>
|
||||
<StrategicInsightsResults report={strategicInsightsHistory[0]} />
|
||||
</GlassCard>
|
||||
) : (
|
||||
<GlassCard sx={{ p: 4, textAlign: 'center' }}>
|
||||
<Typography variant="body1" sx={{ color: 'rgba(255,255,255,0.7)', mb: 2 }}>
|
||||
No strategic insights generated yet. Run your first analysis to see "The Big Move" and market opportunities.
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={runStrategicInsights}
|
||||
sx={{ color: 'white', borderColor: 'rgba(255,255,255,0.3)' }}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
</GlassCard>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{(competitiveSitemapBenchmarkingReport || competitorAnalysisData) && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 2, mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🗺️ Competitive Sitemap Benchmarking (No AI)
|
||||
</Typography>
|
||||
<Tooltip title="Uses public sitemaps and deterministic rules (no LLM calls) to compare structure, coverage, and publishing signals.">
|
||||
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={runCompetitiveSitemapBenchmarking}
|
||||
disabled={competitiveSitemapBenchmarkingLoading}
|
||||
sx={{
|
||||
bgcolor: '#10b981',
|
||||
'&:hover': { bgcolor: '#059669' },
|
||||
textTransform: 'none',
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{competitiveSitemapBenchmarkingLoading ? 'Running…' : 'Run Benchmark'}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{competitiveSitemapBenchmarkingError && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Alert severity="error">{competitiveSitemapBenchmarkingError}</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!competitiveSitemapBenchmarkingReport && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Alert severity="info">
|
||||
No benchmarking report yet. Run it to compare your sitemap structure against competitors and discover missing sections.
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{competitiveSitemapBenchmarkingReport && competitiveSitemapBenchmarkingReport.benchmark && (
|
||||
<SitemapBenchmarkResults
|
||||
data={{
|
||||
user_summary: competitiveSitemapBenchmarkingReport.benchmark.user?.summary || {},
|
||||
competitor_summaries: competitiveSitemapBenchmarkingReport.benchmark.competitors?.summaries || {},
|
||||
timestamp: competitiveSitemapBenchmarkingReport.timestamp,
|
||||
benchmark: competitiveSitemapBenchmarkingReport.benchmark
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Strategic Insights Section */}
|
||||
{strategicInsightsHistory.length > 0 && (
|
||||
<Box sx={{ mb: 4 }} id="strategic-insights-results">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🧠 AI-Powered Strategic Insights
|
||||
</Typography>
|
||||
<Tooltip title="Weekly strategic briefs generated by AI analysis of competitor content moves and market shifts.">
|
||||
<InfoIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<StrategicInsightsResults report={strategicInsightsHistory[0]} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* SEO Analyzer Panel */}
|
||||
<SEOAnalyzerPanel
|
||||
analysisData={analysisData}
|
||||
@@ -892,4 +1452,4 @@ const SEODashboard: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SEODashboard;
|
||||
export default SEODashboard;
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
Typography,
|
||||
Chip,
|
||||
Tooltip,
|
||||
Divider,
|
||||
LinearProgress,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Topic as TopicIcon,
|
||||
HealthAndSafety as HealthIcon,
|
||||
Update as UpdateIcon,
|
||||
Timeline as VelocityIcon,
|
||||
Warning as WarningIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { GlassCard } from '../../shared/styled';
|
||||
|
||||
interface AdvertoolsInsightsProps {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export const AdvertoolsInsights: React.FC<AdvertoolsInsightsProps> = ({ data }) => {
|
||||
if (!data || (!data.augmented_themes?.length && !data.site_health?.total_urls)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { augmented_themes, site_health, last_audit, last_health_check, tasks, avg_word_count } = data;
|
||||
|
||||
const getStatusDisplay = (taskType: string) => {
|
||||
const status = tasks?.[taskType];
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return { label: 'Running...', color: 'secondary', icon: <UpdateIcon sx={{ fontSize: 14 }} /> };
|
||||
case 'failed':
|
||||
return { label: 'Failed', color: 'error', icon: <WarningIcon sx={{ fontSize: 14 }} /> };
|
||||
case 'pending':
|
||||
return { label: 'Scheduled', color: 'default', icon: <UpdateIcon sx={{ fontSize: 14 }} /> };
|
||||
default:
|
||||
return { label: 'Active', color: 'success', icon: null };
|
||||
}
|
||||
};
|
||||
|
||||
const auditStatus = getStatusDisplay('content_audit');
|
||||
const healthStatus = getStatusDisplay('site_health');
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
🚀 Data-Driven Content Intelligence (Advertools)
|
||||
</Typography>
|
||||
<Tooltip title="Deep insights extracted from your actual site content and structure.">
|
||||
<UpdateIcon sx={{ color: 'rgba(255, 255, 255, 0.5)', fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* Content Themes & Persona Augmentation */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<GlassCard sx={{ p: 3, height: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TopicIcon sx={{ color: '#8b5cf6' }} />
|
||||
<Typography variant="subtitle1" sx={{ color: 'white', fontWeight: 700 }}>
|
||||
Augmented Content Themes
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip
|
||||
label={auditStatus.label}
|
||||
size="small"
|
||||
color={auditStatus.color as any}
|
||||
variant="outlined"
|
||||
icon={auditStatus.icon as any}
|
||||
sx={{ height: 20, fontSize: '0.65rem' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)', mb: 2 }}>
|
||||
Actual themes discovered from your content crawl. These are used to refine your brand persona.
|
||||
</Typography>
|
||||
|
||||
{augmented_themes && augmented_themes.length > 0 ? (
|
||||
<>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 2 }}>
|
||||
{augmented_themes.slice(0, 15).map((theme: any, idx: number) => (
|
||||
<Tooltip key={idx} title={`Frequency: ${theme.abs_freq}`}>
|
||||
<Chip
|
||||
label={theme.word}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(139, 92, 246, 0.1)',
|
||||
color: '#a78bfa',
|
||||
border: '1px solid rgba(139, 92, 246, 0.2)',
|
||||
'&:hover': { bgcolor: 'rgba(139, 92, 246, 0.2)' }
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</Box>
|
||||
<Grid container spacing={1}>
|
||||
{avg_word_count && (
|
||||
<Grid item xs={6}>
|
||||
<Box sx={{ p: 1.5, bgcolor: 'rgba(255,255,255,0.03)', borderRadius: 2 }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)', display: 'block' }}>
|
||||
Avg. Content Length
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
{avg_word_count} words
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
{site_health?.top_pillars && (
|
||||
<Grid item xs={6}>
|
||||
<Box sx={{ p: 1.5, bgcolor: 'rgba(255,255,255,0.03)', borderRadius: 2 }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)', display: 'block' }}>
|
||||
Primary Structure
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" sx={{ color: 'white', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
/{Object.keys(site_health.top_pillars)[0] || 'root'}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</>
|
||||
) : (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
{tasks?.content_audit === 'running' ? 'Crawl in progress...' : (tasks?.content_audit === 'failed' ? 'Audit failed. Check sitemap.' : 'No themes discovered yet.')}
|
||||
</Typography>
|
||||
{tasks?.content_audit === 'running' && <LinearProgress sx={{ mt: 1, borderRadius: 1 }} color="secondary" />}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{last_audit && (
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 2, color: 'rgba(255, 255, 255, 0.4)' }}>
|
||||
Last updated: {new Date(last_audit).toLocaleDateString()}
|
||||
</Typography>
|
||||
)}
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
|
||||
{/* Site Health & Freshness */}
|
||||
<Grid item xs={12} md={6}>
|
||||
<GlassCard sx={{ p: 3, height: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<HealthIcon sx={{ color: '#10b981' }} />
|
||||
<Typography variant="subtitle1" sx={{ color: 'white', fontWeight: 700 }}>
|
||||
Site Health & Freshness
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip
|
||||
label={healthStatus.label}
|
||||
size="small"
|
||||
color={healthStatus.color as any}
|
||||
variant="outlined"
|
||||
icon={healthStatus.icon as any}
|
||||
sx={{ height: 20, fontSize: '0.65rem' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{site_health && site_health.total_urls ? (
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Box sx={{ p: 1.5, bgcolor: 'rgba(255,255,255,0.03)', borderRadius: 2 }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)', display: 'block' }}>
|
||||
Total Pages
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ color: 'white' }}>
|
||||
{site_health.total_urls}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Box sx={{ p: 1.5, bgcolor: 'rgba(255,255,255,0.03)', borderRadius: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<VelocityIcon sx={{ fontSize: 14, color: '#3b82f6' }} />
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||
Publishing Velocity
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="h6" sx={{ color: 'white' }}>
|
||||
{site_health.publishing_velocity} <Typography component="span" variant="caption">/ week</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Box sx={{ p: 1.5, bgcolor: 'rgba(255,255,255,0.03)', borderRadius: 2, border: site_health.stale_content_percentage > 30 ? '1px solid rgba(239, 68, 68, 0.2)' : 'none' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<WarningIcon sx={{ fontSize: 14, color: site_health.stale_content_percentage > 30 ? '#ef4444' : '#f59e0b' }} />
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||
Stale Content (6+ months)
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="h6" sx={{ color: site_health.stale_content_percentage > 30 ? '#f87171' : 'white' }}>
|
||||
{site_health.stale_content_count} pages ({site_health.stale_content_percentage}%)
|
||||
</Typography>
|
||||
</Box>
|
||||
{site_health.stale_content_percentage > 30 && (
|
||||
<Chip label="High Risk" size="small" color="error" variant="outlined" sx={{ height: 20, fontSize: '0.65rem' }} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255, 255, 255, 0.5)' }}>
|
||||
{tasks?.site_health === 'running' ? 'Analyzing sitemap...' : (tasks?.site_health === 'failed' ? 'Sitemap analysis failed.' : 'Sitemap analysis pending.')}
|
||||
</Typography>
|
||||
{tasks?.site_health === 'running' && <LinearProgress sx={{ mt: 1, borderRadius: 1 }} color="primary" />}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{last_health_check && (
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 2, color: 'rgba(255, 255, 255, 0.4)' }}>
|
||||
Last checked: {new Date(last_health_check).toLocaleDateString()}
|
||||
</Typography>
|
||||
)}
|
||||
</GlassCard>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,258 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Checkbox,
|
||||
Button,
|
||||
Paper,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Alert
|
||||
} from '@mui/material';
|
||||
import {
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Error as ErrorIcon,
|
||||
Warning as WarningIcon,
|
||||
Refresh as RefreshIcon
|
||||
} from '@mui/icons-material';
|
||||
import axios from 'axios';
|
||||
import { GlassCard } from '../../shared/styled';
|
||||
|
||||
interface PageAudit {
|
||||
id: number;
|
||||
page_url: string;
|
||||
overall_score: number;
|
||||
status: string;
|
||||
issues: any[];
|
||||
recommendations: any[];
|
||||
last_analyzed_at: string;
|
||||
}
|
||||
|
||||
const PageAuditList: React.FC = () => {
|
||||
const [pages, setPages] = useState<PageAudit[]>([]);
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [aiLoading, setAiLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchPages();
|
||||
}, []);
|
||||
|
||||
const fetchPages = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await axios.get('/api/seo-dashboard/pages');
|
||||
setPages(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching pages:', error);
|
||||
setError('Failed to load analyzed pages.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAll = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.checked) {
|
||||
setSelected(pages.map((n) => n.page_url));
|
||||
} else {
|
||||
setSelected([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (name: string) => {
|
||||
const selectedIndex = selected.indexOf(name);
|
||||
let newSelected: string[] = [];
|
||||
|
||||
if (selectedIndex === -1) {
|
||||
newSelected = newSelected.concat(selected, name);
|
||||
} else if (selectedIndex === 0) {
|
||||
newSelected = newSelected.concat(selected.slice(1));
|
||||
} else if (selectedIndex === selected.length - 1) {
|
||||
newSelected = newSelected.concat(selected.slice(0, -1));
|
||||
} else if (selectedIndex > 0) {
|
||||
newSelected = newSelected.concat(
|
||||
selected.slice(0, selectedIndex),
|
||||
selected.slice(selectedIndex + 1),
|
||||
);
|
||||
}
|
||||
setSelected(newSelected);
|
||||
};
|
||||
|
||||
const handleRunAI = async () => {
|
||||
if (selected.length === 0) return;
|
||||
|
||||
setAiLoading(true);
|
||||
try {
|
||||
await axios.post('/api/seo-dashboard/analyze-urls-ai', { urls: selected });
|
||||
await fetchPages(); // Refresh to show updates
|
||||
setSelected([]);
|
||||
} catch (error) {
|
||||
console.error('Error running AI analysis:', error);
|
||||
setError('Failed to run AI analysis.');
|
||||
} finally {
|
||||
setAiLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 90) return 'success';
|
||||
if (score >= 70) return 'warning';
|
||||
return 'error';
|
||||
};
|
||||
|
||||
const hasAiInsights = (page: PageAudit) => {
|
||||
return page.recommendations && page.recommendations.some((r: any) => r.source === 'ai_on_demand');
|
||||
};
|
||||
|
||||
return (
|
||||
<GlassCard sx={{ p: 3, mt: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
📄 Full Site Analysis
|
||||
</Typography>
|
||||
<Box>
|
||||
<Button
|
||||
startIcon={<RefreshIcon />}
|
||||
onClick={fetchPages}
|
||||
disabled={loading || aiLoading}
|
||||
sx={{ mr: 1, color: 'rgba(255,255,255,0.7)' }}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AutoAwesomeIcon />}
|
||||
onClick={handleRunAI}
|
||||
disabled={selected.length === 0 || aiLoading}
|
||||
sx={{
|
||||
background: 'linear-gradient(45deg, #9C27B0, #E040FB)',
|
||||
'&:disabled': {
|
||||
background: 'rgba(255, 255, 255, 0.12)',
|
||||
color: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{aiLoading ? 'Analyzing...' : `Get AI Insights (${selected.length})`}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
|
||||
{loading && <LinearProgress sx={{ mb: 2 }} />}
|
||||
|
||||
<TableContainer component={Paper} sx={{ bgcolor: 'transparent', boxShadow: 'none' }}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="page audit table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
color="primary"
|
||||
indeterminate={selected.length > 0 && selected.length < pages.length}
|
||||
checked={pages.length > 0 && selected.length === pages.length}
|
||||
onChange={handleSelectAll}
|
||||
inputProps={{ 'aria-label': 'select all pages' }}
|
||||
sx={{ color: 'rgba(255,255,255,0.5)' }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{ color: 'rgba(255,255,255,0.7)' }}>Page URL</TableCell>
|
||||
<TableCell align="right" sx={{ color: 'rgba(255,255,255,0.7)' }}>Score</TableCell>
|
||||
<TableCell align="right" sx={{ color: 'rgba(255,255,255,0.7)' }}>Status</TableCell>
|
||||
<TableCell align="center" sx={{ color: 'rgba(255,255,255,0.7)' }}>AI Insights</TableCell>
|
||||
<TableCell align="right" sx={{ color: 'rgba(255,255,255,0.7)' }}>Last Analyzed</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{pages.length === 0 && !loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} align="center" sx={{ color: 'rgba(255,255,255,0.5)', py: 3 }}>
|
||||
No pages analyzed yet. The background scan will populate this list shortly.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
pages.map((row) => {
|
||||
const isItemSelected = selected.indexOf(row.page_url) !== -1;
|
||||
const labelId = `enhanced-table-checkbox-${row.id}`;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
hover
|
||||
onClick={() => handleClick(row.page_url)}
|
||||
role="checkbox"
|
||||
aria-checked={isItemSelected}
|
||||
tabIndex={-1}
|
||||
key={row.id}
|
||||
selected={isItemSelected}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'&.Mui-selected': { bgcolor: 'rgba(33, 150, 243, 0.08) !important' },
|
||||
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.05) !important' }
|
||||
}}
|
||||
>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
color="primary"
|
||||
checked={isItemSelected}
|
||||
inputProps={{ 'aria-labelledby': labelId }}
|
||||
sx={{ color: 'rgba(255,255,255,0.5)' }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell component="th" id={labelId} scope="row" sx={{ color: 'white', maxWidth: 300, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
<Tooltip title={row.page_url}>
|
||||
<span>{row.page_url}</span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Chip
|
||||
label={row.overall_score || 'N/A'}
|
||||
color={getScoreColor(row.overall_score)}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ fontWeight: 'bold' }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: 'rgba(255,255,255,0.7)', textTransform: 'capitalize' }}>
|
||||
{row.status?.replace('_', ' ')}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{hasAiInsights(row) ? (
|
||||
<Chip
|
||||
icon={<AutoAwesomeIcon sx={{ fontSize: '14px !important' }} />}
|
||||
label="Ready"
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(156, 39, 176, 0.2)',
|
||||
color: '#E040FB',
|
||||
borderColor: '#E040FB',
|
||||
border: '1px solid'
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.3)' }}>-</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||
{new Date(row.last_analyzed_at).toLocaleDateString()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</GlassCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageAuditList;
|
||||
@@ -37,6 +37,7 @@ import IssueDetailsDialog from './IssueDetailsDialog';
|
||||
import AnalysisDetailsDialog from './AnalysisDetailsDialog';
|
||||
import SEOAnalysisLoading from './SEOAnalysisLoading';
|
||||
import SEOAnalysisError from './SEOAnalysisError';
|
||||
import PageAuditList from './PageAuditList';
|
||||
|
||||
const SEOAnalyzerPanel: React.FC<SEOAnalyzerPanelProps> = ({
|
||||
analysisData,
|
||||
@@ -247,6 +248,9 @@ const SEOAnalyzerPanel: React.FC<SEOAnalyzerPanelProps> = ({
|
||||
</AnimatePresence>
|
||||
</GlassCard>
|
||||
|
||||
{/* Full Site Page List */}
|
||||
<PageAuditList />
|
||||
|
||||
{/* Dialogs */}
|
||||
<IssueDetailsDialog
|
||||
open={showIssueDialog}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Skeleton,
|
||||
Alert,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Speed as SpeedIcon,
|
||||
Refresh as RefreshIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Warning as WarningIcon,
|
||||
Error as ErrorIcon,
|
||||
Info as InfoIcon
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GlassCard, ShimmerHeader } from '../../shared/styled';
|
||||
import { semanticDashboardAPI } from '../../../api/semanticDashboard';
|
||||
import { SemanticHealthMetric } from '../../../api/semanticDashboard';
|
||||
|
||||
interface SemanticHealthCardProps {
|
||||
className?: string;
|
||||
onRefresh?: () => void;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const SemanticHealthCard: React.FC<SemanticHealthCardProps> = ({
|
||||
className,
|
||||
onRefresh,
|
||||
compact = false
|
||||
}) => {
|
||||
const [semanticHealth, setSemanticHealth] = useState<SemanticHealthMetric | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
|
||||
|
||||
// Fetch semantic health data
|
||||
const fetchSemanticHealth = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const health = await semanticDashboardAPI.getSemanticHealth();
|
||||
setSemanticHealth(health);
|
||||
setLastUpdated(new Date().toISOString());
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch semantic health';
|
||||
setError(errorMessage);
|
||||
console.error('Error fetching semantic health:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch data on component mount
|
||||
useEffect(() => {
|
||||
fetchSemanticHealth();
|
||||
}, []);
|
||||
|
||||
// Auto-refresh every 24 hours (86400000ms)
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
fetchSemanticHealth();
|
||||
}, 86400000); // 24 hours
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
if (onRefresh) {
|
||||
onRefresh();
|
||||
}
|
||||
await fetchSemanticHealth();
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy': return '#4CAF50';
|
||||
case 'warning': return '#FF9800';
|
||||
case 'critical': return '#F44336';
|
||||
default: return '#9E9E9E';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
return <CheckCircleIcon sx={{ color: '#4CAF50' }} />;
|
||||
case 'warning':
|
||||
return <WarningIcon sx={{ color: '#FF9800' }} />;
|
||||
case 'critical':
|
||||
return <ErrorIcon sx={{ color: '#F44336' }} />;
|
||||
default:
|
||||
return <InfoIcon sx={{ color: '#9E9E9E' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatLastUpdated = (timestamp: string | null) => {
|
||||
if (!timestamp) return 'Never';
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - date.getTime();
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
|
||||
if (diffHours < 1) return 'Just now';
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
return `${Math.floor(diffHours / 24)}d ago`;
|
||||
};
|
||||
|
||||
if (error && !compact) {
|
||||
return (
|
||||
<GlassCard className={className}>
|
||||
<CardContent>
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
<Box display="flex" gap={1}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
startIcon={<RefreshIcon />}
|
||||
size="small"
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</GlassCard>
|
||||
);
|
||||
}
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<Card
|
||||
sx={{
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 2
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
<CardContent sx={{ p: 2 }}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<SpeedIcon sx={{ color: '#64B5F6', fontSize: 20 }} />
|
||||
<Typography variant="subtitle2" sx={{ color: 'white' }}>
|
||||
Semantic Health
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{semanticHealth && getStatusIcon(semanticHealth.status)}
|
||||
{semanticHealth && (
|
||||
<Typography variant="h6" sx={{ color: 'white' }}>
|
||||
{Math.round(semanticHealth.value * 100)}%
|
||||
</Typography>
|
||||
)}
|
||||
<Tooltip title="Refresh">
|
||||
<IconButton
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
sx={{ color: 'rgba(255,255,255,0.7)', p: 0.5 }}
|
||||
size="small"
|
||||
>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && !semanticHealth && (
|
||||
<LinearProgress sx={{ mt: 1, height: 2 }} />
|
||||
)}
|
||||
|
||||
{semanticHealth && (
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={semanticHealth.value * 100}
|
||||
sx={{
|
||||
mt: 1,
|
||||
height: 2,
|
||||
borderRadius: 1,
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: getStatusColor(semanticHealth.status),
|
||||
borderRadius: 1
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GlassCard className={className}>
|
||||
<ShimmerHeader />
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={2}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<SpeedIcon sx={{ color: '#64B5F6' }} />
|
||||
<Typography variant="h6" sx={{ color: 'white', fontWeight: 600 }}>
|
||||
Semantic Health
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
{formatLastUpdated(lastUpdated)}
|
||||
</Typography>
|
||||
<Tooltip title="Refresh semantic analysis">
|
||||
<IconButton
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
sx={{ color: 'rgba(255,255,255,0.7)' }}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{loading && !semanticHealth ? (
|
||||
<Box>
|
||||
<Skeleton variant="rectangular" height={60} sx={{ mb: 2, borderRadius: 2 }} />
|
||||
<Skeleton variant="text" width="80%" />
|
||||
<Skeleton variant="text" width="60%" />
|
||||
</Box>
|
||||
) : semanticHealth ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
{getStatusIcon(semanticHealth.status)}
|
||||
<Box ml={2} flex={1}>
|
||||
<Typography variant="h6" sx={{ color: 'white' }}>
|
||||
{semanticHealth.metric_name.replace(/_/g, ' ').toUpperCase()}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
{semanticHealth.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box textAlign="right">
|
||||
<Typography variant="h4" sx={{ color: 'white', fontWeight: 700 }}>
|
||||
{Math.round(semanticHealth.value * 100)}%
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={semanticHealth.value * 100}
|
||||
sx={{
|
||||
width: 100,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: 'rgba(255,255,255,0.2)',
|
||||
'& .MuiLinearProgress-bar': {
|
||||
backgroundColor: getStatusColor(semanticHealth.status),
|
||||
borderRadius: 3
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{semanticHealth.recommendations.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ color: 'rgba(255,255,255,0.8)', mb: 1 }}>
|
||||
Recommendations:
|
||||
</Typography>
|
||||
{semanticHealth.recommendations.map((rec, index) => (
|
||||
<Typography key={index} variant="body2" sx={{ color: 'rgba(255,255,255,0.7)', mb: 0.5 }}>
|
||||
• {rec}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</motion.div>
|
||||
) : (
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.7)' }}>
|
||||
No semantic health data available
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</GlassCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default SemanticHealthCard;
|
||||
@@ -0,0 +1,464 @@
|
||||
/**
|
||||
* Semantic Insights Components for ALwrity Onboarding Step 3
|
||||
* React components for displaying AI-powered semantic analysis results.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Chip,
|
||||
Divider,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Psychology as PsychologyIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Lightbulb as LightbulbIcon,
|
||||
Warning as WarningIcon,
|
||||
Assessment as AssessmentIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Info as InfoIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
PriorityHigh as PriorityHighIcon,
|
||||
Stars as StarsIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// TypeScript interfaces for semantic insights
|
||||
export interface ContentPillar {
|
||||
pillar_id: string;
|
||||
theme: string;
|
||||
size: number;
|
||||
relevance_score: number;
|
||||
key_topics: string[];
|
||||
competitor_coverage: number;
|
||||
user_coverage: number;
|
||||
}
|
||||
|
||||
export interface SemanticGap {
|
||||
topic: string;
|
||||
reason: string;
|
||||
competitor_count: number;
|
||||
opportunity_score: number;
|
||||
suggested_content_ideas: string[];
|
||||
}
|
||||
|
||||
export interface ThemeAnalysis {
|
||||
theme: string;
|
||||
relevance_score: number;
|
||||
user_content_relevance: number;
|
||||
competitor_content_relevance: number;
|
||||
content_opportunities: string[];
|
||||
}
|
||||
|
||||
export interface StrategicRecommendation {
|
||||
type: 'content_pillars' | 'content_gaps' | 'content_themes' | 'strategic_overview';
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
title: string;
|
||||
description: string;
|
||||
action_items: string[];
|
||||
estimated_impact: 'high' | 'medium' | 'low';
|
||||
implementation_difficulty: 'easy' | 'moderate' | 'challenging';
|
||||
}
|
||||
|
||||
export interface SemanticInsights {
|
||||
content_pillars: ContentPillar[];
|
||||
semantic_gaps: SemanticGap[];
|
||||
themes_analysis: ThemeAnalysis[];
|
||||
strategic_recommendations: StrategicRecommendation[];
|
||||
confidence_scores: {
|
||||
pillar_discovery: boolean;
|
||||
gap_analysis: boolean;
|
||||
theme_analysis: boolean;
|
||||
};
|
||||
analysis_timestamp: string;
|
||||
total_competitors_analyzed: number;
|
||||
total_pages_analyzed: number;
|
||||
}
|
||||
|
||||
interface SemanticInsightsDisplayProps {
|
||||
insights: SemanticInsights;
|
||||
isLoading?: boolean;
|
||||
onRefresh?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SemanticInsightsDisplay: React.FC<SemanticInsightsDisplayProps> = ({
|
||||
insights,
|
||||
isLoading = false,
|
||||
onRefresh,
|
||||
className
|
||||
}) => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box className={className}>
|
||||
<Paper elevation={2} sx={{ p: 3, borderRadius: 3 }}>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<PsychologyIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6" component="h3">
|
||||
AI-Powered Semantic Analysis
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box display="flex" justifyContent="center" alignItems="center" py={4}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Analyzing semantic patterns and competitive landscape...
|
||||
</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!insights || insights.content_pillars.length === 0) {
|
||||
return (
|
||||
<Box className={className}>
|
||||
<Paper elevation={2} sx={{ p: 3, borderRadius: 3 }}>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<PsychologyIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6" component="h3">
|
||||
AI-Powered Semantic Analysis
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Semantic insights will appear here after competitor analysis is complete.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={className}>
|
||||
<Paper elevation={2} sx={{ p: 3, borderRadius: 3 }}>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between" mb={3}>
|
||||
<Box display="flex" alignItems="center">
|
||||
<PsychologyIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6" component="h3">
|
||||
AI-Powered Semantic Insights
|
||||
</Typography>
|
||||
</Box>
|
||||
{onRefresh && (
|
||||
<Tooltip title="Refresh semantic analysis">
|
||||
<IconButton onClick={onRefresh} size="small">
|
||||
<AssessmentIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Content Pillars Section */}
|
||||
<ContentPillarsSection pillars={insights.content_pillars} />
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* Semantic Gaps Section */}
|
||||
<SemanticGapsSection gaps={insights.semantic_gaps} />
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* Strategic Recommendations */}
|
||||
<StrategicRecommendationsSection recommendations={insights.strategic_recommendations} />
|
||||
|
||||
{/* Analysis Summary */}
|
||||
<Box mt={3} pt={2} borderTop="1px solid" borderColor="divider">
|
||||
<Typography variant="caption" color="text.secondary" display="block" gutterBottom>
|
||||
Analysis Summary
|
||||
</Typography>
|
||||
<Box display="flex" gap={2} flexWrap="wrap">
|
||||
<Chip
|
||||
icon={<StarsIcon />}
|
||||
label={`${insights.total_competitors_analyzed} competitors analyzed`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
icon={<AssessmentIcon />}
|
||||
label={`${insights.total_pages_analyzed} pages processed`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
icon={<CheckCircleIcon />}
|
||||
label={`${insights.content_pillars.length} content pillars identified`}
|
||||
size="small"
|
||||
color="success"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
icon={<WarningIcon />}
|
||||
label={`${insights.semantic_gaps.length} content gaps found`}
|
||||
size="small"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ContentPillarsSection: React.FC<{ pillars: ContentPillar[] }> = ({ pillars }) => {
|
||||
if (!pillars || pillars.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<TrendingUpIcon color="success" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6" component="h4">
|
||||
Content Pillars Discovered
|
||||
</Typography>
|
||||
<Tooltip title="These are your core content themes based on semantic analysis of your website">
|
||||
<IconButton size="small" sx={{ ml: 1 }}>
|
||||
<InfoIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{pillars.map((pillar, index) => (
|
||||
<Grid item xs={12} md={6} key={pillar.pillar_id}>
|
||||
<Card variant="outlined" sx={{ height: '100%' }}>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" mb={1}>
|
||||
<Typography variant="subtitle1" component="h5" sx={{ flexGrow: 1 }}>
|
||||
{pillar.theme}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${pillar.size} items`}
|
||||
size="small"
|
||||
color="success"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box mb={1}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Relevance Score: {Math.round(pillar.relevance_score * 100)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{pillar.key_topics && pillar.key_topics.length > 0 && (
|
||||
<Box mb={1}>
|
||||
<Typography variant="caption" color="text.secondary" display="block" gutterBottom>
|
||||
Key Topics:
|
||||
</Typography>
|
||||
<Box display="flex" flexWrap="wrap" gap={0.5}>
|
||||
{pillar.key_topics.slice(0, 3).map((topic, idx) => (
|
||||
<Chip key={idx} label={topic} size="small" variant="outlined" />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mt={2}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Your Coverage: {Math.round(pillar.user_coverage * 100)}%
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Competitor Coverage: {Math.round(pillar.competitor_coverage * 100)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const SemanticGapsSection: React.FC<{ gaps: SemanticGap[] }> = ({ gaps }) => {
|
||||
if (!gaps || gaps.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<WarningIcon color="warning" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6" component="h4">
|
||||
Content Gaps Identified
|
||||
</Typography>
|
||||
<Tooltip title="Topics your competitors cover that you haven't addressed yet">
|
||||
<IconButton size="small" sx={{ ml: 1 }}>
|
||||
<InfoIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{gaps.map((gap, index) => (
|
||||
<Accordion key={index} sx={{ mb: 1 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Box display="flex" alignItems="center" width="100%">
|
||||
<PriorityHighIcon color="warning" sx={{ mr: 1 }} />
|
||||
<Typography variant="subtitle1" sx={{ flexGrow: 1 }}>
|
||||
{gap.topic}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={`${gap.competitor_count} competitors`}
|
||||
size="small"
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{gap.reason}
|
||||
</Typography>
|
||||
|
||||
{gap.suggested_content_ideas && gap.suggested_content_ideas.length > 0 && (
|
||||
<Box mt={2}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Suggested Content Ideas:
|
||||
</Typography>
|
||||
<List dense>
|
||||
{gap.suggested_content_ideas.map((idea, idx) => (
|
||||
<ListItem key={idx}>
|
||||
<ListItemIcon>
|
||||
<LightbulbIcon fontSize="small" color="primary" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={idea} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box mt={2}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Opportunity Score: {Math.round(gap.opportunity_score * 100)}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const StrategicRecommendationsSection: React.FC<{ recommendations: StrategicRecommendation[] }> = ({ recommendations }) => {
|
||||
if (!recommendations || recommendations.length === 0) return null;
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'error';
|
||||
case 'medium': return 'warning';
|
||||
case 'low': return 'info';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
const getImpactIcon = (impact: string) => {
|
||||
switch (impact) {
|
||||
case 'high': return <TrendingUpIcon color="error" />;
|
||||
case 'medium': return <TrendingUpIcon color="warning" />;
|
||||
case 'low': return <TrendingUpIcon color="info" />;
|
||||
default: return <TrendingUpIcon />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" alignItems="center" mb={2}>
|
||||
<AssessmentIcon color="primary" sx={{ mr: 1 }} />
|
||||
<Typography variant="h6" component="h4">
|
||||
Strategic Recommendations
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{recommendations.map((rec, index) => (
|
||||
<Card key={index} variant="outlined" sx={{ mb: 2 }}>
|
||||
<CardContent>
|
||||
<Box display="flex" alignItems="center" mb={1}>
|
||||
{getImpactIcon(rec.estimated_impact)}
|
||||
<Typography variant="subtitle1" sx={{ ml: 1, flexGrow: 1 }}>
|
||||
{rec.title}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={rec.priority.toUpperCase()}
|
||||
size="small"
|
||||
color={getPriorityColor(rec.priority)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||
{rec.description}
|
||||
</Typography>
|
||||
|
||||
{rec.action_items && rec.action_items.length > 0 && (
|
||||
<Box mt={2}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Action Items:
|
||||
</Typography>
|
||||
<List dense>
|
||||
{rec.action_items.map((item, idx) => (
|
||||
<ListItem key={idx}>
|
||||
<ListItemIcon>
|
||||
<CheckCircleIcon fontSize="small" color="success" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={item} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box display="flex" gap={1} mt={2}>
|
||||
<Chip
|
||||
label={`Impact: ${rec.estimated_impact}`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Chip
|
||||
label={`Difficulty: ${rec.implementation_difficulty}`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
interface SemanticInsightsProps {
|
||||
maxInsights?: number;
|
||||
}
|
||||
|
||||
const SemanticInsights: React.FC<SemanticInsightsProps> = ({ maxInsights }) => {
|
||||
// Mock data or state management here
|
||||
// For now, returning null or a placeholder if no data, or using the Display component with empty data
|
||||
|
||||
// TODO: Connect to real API and map data
|
||||
const mockInsights: SemanticInsights = {
|
||||
content_pillars: [],
|
||||
semantic_gaps: [],
|
||||
themes_analysis: [],
|
||||
strategic_recommendations: [],
|
||||
confidence_scores: {
|
||||
pillar_discovery: false,
|
||||
gap_analysis: false,
|
||||
theme_analysis: false
|
||||
},
|
||||
analysis_timestamp: new Date().toISOString(),
|
||||
total_competitors_analyzed: 0,
|
||||
total_pages_analyzed: 0
|
||||
};
|
||||
|
||||
return <SemanticInsightsDisplay insights={mockInsights} isLoading={false} />;
|
||||
};
|
||||
|
||||
export default SemanticInsights;
|
||||
Reference in New Issue
Block a user