Recovered state: integrated TrendSurferAgent, restored frontend/backend files, and cleaned up recovery scripts

This commit is contained in:
ajaysi
2026-02-08 13:56:57 +05:30
parent 1db10ccd0f
commit e404a86502
333 changed files with 42223 additions and 10875 deletions

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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}

View File

@@ -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;

View File

@@ -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;