Recovered state: integrated TrendSurferAgent, restored frontend/backend files, and cleaned up recovery scripts
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
@@ -15,63 +15,269 @@ import {
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Alert
|
||||
Alert,
|
||||
Tooltip,
|
||||
CircularProgress,
|
||||
LinearProgress
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
Analytics as AnalyticsIcon,
|
||||
CheckCircle as CheckIcon,
|
||||
Insights as InsightsIcon
|
||||
Check as CheckIcon,
|
||||
Insights as InsightsIcon,
|
||||
CheckCircleOutline as CheckCircleIcon,
|
||||
AutoAwesome as AIIcon
|
||||
} from '@mui/icons-material';
|
||||
import { apiClient, longRunningApiClient } from '../../../api/client';
|
||||
import { SitemapBenchmarkResults } from './SitemapBenchmarkResults';
|
||||
import { StrategicInsightsResults } from './StrategicInsightsResults';
|
||||
|
||||
export const ComingSoonSection: React.FC = () => {
|
||||
export const ComingSoonSection: React.FC<{ missingData?: boolean }> = ({ missingData = false }) => {
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [selectedFeature, setSelectedFeature] = useState<string | null>(null);
|
||||
const [scheduledStatus, setScheduledStatus] = useState<any>(null);
|
||||
const [sitemapBenchmarkRunning, setSitemapBenchmarkRunning] = useState(false);
|
||||
const [sitemapBenchmarkError, setSitemapBenchmarkError] = useState<string | null>(null);
|
||||
const [sitemapBenchmarkData, setSitemapBenchmarkData] = useState<any>(null);
|
||||
const [loadingBenchmarkData, setLoadingBenchmarkData] = useState(false);
|
||||
const [isLongRunning, setIsLongRunning] = useState(false);
|
||||
const [strategicInsightsRunning, setStrategicInsightsRunning] = useState(false);
|
||||
const [strategicInsightsError, setStrategicInsightsError] = useState<string | null>(null);
|
||||
const [strategicInsightsData, setStrategicInsightsData] = useState<any>(null);
|
||||
const [loadingStrategicHistory, setLoadingStrategicHistory] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadStatus = async () => {
|
||||
try {
|
||||
const res = await apiClient.get('/api/onboarding/step3/scheduled-tasks-status');
|
||||
setScheduledStatus(res.data);
|
||||
|
||||
// If report is available, fetch the full data
|
||||
if (res.data?.competitive_sitemap_benchmarking?.report?.available) {
|
||||
fetchBenchmarkData();
|
||||
}
|
||||
} catch {
|
||||
setScheduledStatus(null);
|
||||
}
|
||||
};
|
||||
|
||||
const loadHistory = async () => {
|
||||
setLoadingStrategicHistory(true);
|
||||
try {
|
||||
const res = await apiClient.get('/api/seo-dashboard/strategic-insights/history');
|
||||
if (res.data?.history?.length > 0) {
|
||||
setStrategicInsightsData(res.data.history[0]); // Show latest
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch strategic insights history", e);
|
||||
} finally {
|
||||
setLoadingStrategicHistory(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadStatus();
|
||||
loadHistory();
|
||||
}, []);
|
||||
|
||||
const fetchBenchmarkData = async () => {
|
||||
setLoadingBenchmarkData(true);
|
||||
try {
|
||||
const res = await apiClient.get('/api/onboarding/step3/sitemap-benchmark-report');
|
||||
setSitemapBenchmarkData(res.data);
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch benchmark report", e);
|
||||
} finally {
|
||||
setLoadingBenchmarkData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deepStatus = scheduledStatus?.deep_competitor_analysis;
|
||||
const deepBulb = deepStatus?.bulb || 'unknown';
|
||||
const deepReason = deepStatus?.reason;
|
||||
const deepTask = deepStatus?.task;
|
||||
|
||||
const sitemapStatus = scheduledStatus?.competitive_sitemap_benchmarking;
|
||||
const sitemapBulb = sitemapStatus?.bulb || 'unknown';
|
||||
const sitemapReason = sitemapStatus?.reason;
|
||||
const sitemapReport = sitemapStatus?.report;
|
||||
|
||||
const getBulbColor = (bulb: string) => {
|
||||
if (bulb === 'green') return '#22c55e';
|
||||
if (bulb === 'red') return '#ef4444';
|
||||
return '#94a3b8';
|
||||
};
|
||||
|
||||
const getFeatureStatusLabel = (featureId: string, fallback: string) => {
|
||||
if (featureId === 'sitemap-benchmarking') {
|
||||
if (sitemapReport?.available) return 'Report Ready (No AI)';
|
||||
return 'Available (No AI)';
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const runSitemapBenchmark = async () => {
|
||||
setSitemapBenchmarkError(null);
|
||||
setSitemapBenchmarkRunning(true);
|
||||
setIsLongRunning(false);
|
||||
try {
|
||||
await longRunningApiClient.post('/api/seo/competitive-sitemap-benchmarking/run', { max_competitors: 5 });
|
||||
|
||||
// Poll for completion with adaptive backoff
|
||||
let attempts = 0;
|
||||
const maxAttempts = 60; // Adjusted for ~10-12 mins (matching backend timeout)
|
||||
let currentInterval = 2000;
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
attempts++;
|
||||
|
||||
// Mark as long running after ~2 minutes (approx 30 attempts)
|
||||
if (attempts > 30) {
|
||||
setIsLongRunning(true);
|
||||
}
|
||||
|
||||
const res = await apiClient.get('/api/onboarding/step3/scheduled-tasks-status');
|
||||
setScheduledStatus(res.data);
|
||||
|
||||
// Check status flag
|
||||
const reportAvailable = res.data?.competitive_sitemap_benchmarking?.report?.available;
|
||||
const reportStatus = res.data?.competitive_sitemap_benchmarking?.report?.status;
|
||||
const reportError = res.data?.competitive_sitemap_benchmarking?.report?.error;
|
||||
let reportFetched = false;
|
||||
|
||||
// Check for failure
|
||||
if (reportStatus === 'failed' || reportError) {
|
||||
setSitemapBenchmarkRunning(false);
|
||||
setSitemapBenchmarkError(reportError || "Benchmark failed during execution.");
|
||||
return; // Stop polling
|
||||
}
|
||||
|
||||
// If available, try to fetch data
|
||||
if (reportAvailable && !sitemapBenchmarkData) {
|
||||
try {
|
||||
const reportRes = await apiClient.get('/api/onboarding/step3/sitemap-benchmark-report');
|
||||
if (reportRes?.data) {
|
||||
setSitemapBenchmarkData(reportRes.data);
|
||||
reportFetched = true;
|
||||
}
|
||||
} catch {
|
||||
// Report might be saving or transient error
|
||||
}
|
||||
}
|
||||
|
||||
if (reportAvailable || reportFetched) {
|
||||
if (!reportFetched && !sitemapBenchmarkData) {
|
||||
await fetchBenchmarkData();
|
||||
}
|
||||
setOpenModal(false); // Close modal on success
|
||||
setSitemapBenchmarkRunning(false);
|
||||
setIsLongRunning(false);
|
||||
|
||||
// Focus on results
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById('sitemap-benchmark-results');
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, 500);
|
||||
return; // Stop polling
|
||||
} else if (attempts >= maxAttempts) {
|
||||
setSitemapBenchmarkRunning(false);
|
||||
setIsLongRunning(false);
|
||||
setSitemapBenchmarkError("Benchmark timed out (10 mins limit). It may still be running in the background.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Adaptive backoff: Slow down polling over time
|
||||
if (attempts > 5) currentInterval = 4000; // After ~10s, slow to 4s
|
||||
if (attempts > 15) currentInterval = 8000; // After ~50s, slow to 8s
|
||||
if (attempts > 25) currentInterval = 15000; // After ~2m, slow to 15s
|
||||
|
||||
setTimeout(poll, currentInterval);
|
||||
|
||||
} catch (e) {
|
||||
console.error("Polling error", e);
|
||||
// Continue polling on error, but maybe wait longer
|
||||
setTimeout(poll, currentInterval + 1000);
|
||||
}
|
||||
};
|
||||
|
||||
// Start polling
|
||||
setTimeout(poll, currentInterval);
|
||||
|
||||
} catch (e: any) {
|
||||
setSitemapBenchmarkError(e?.response?.data?.detail || e?.message || 'Failed to run benchmark');
|
||||
setSitemapBenchmarkRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const runStrategicInsights = async () => {
|
||||
setStrategicInsightsError(null);
|
||||
setStrategicInsightsRunning(true);
|
||||
try {
|
||||
const res = await apiClient.post('/api/seo-dashboard/strategic-insights/run');
|
||||
if (res.data?.success) {
|
||||
setStrategicInsightsData(res.data.report);
|
||||
setOpenModal(false);
|
||||
// Focus on results
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById('strategic-insights-results');
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
} catch (e: any) {
|
||||
setStrategicInsightsError(e?.response?.data?.detail || e?.message || 'Failed to run strategic insights');
|
||||
} finally {
|
||||
setStrategicInsightsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const features = [
|
||||
{
|
||||
id: 'deep-competitor-analysis',
|
||||
title: 'Deep Competitor Analysis',
|
||||
description: 'Comprehensive analysis of competitor websites and content strategies',
|
||||
description: 'We dig deep into your competitors\' strategies so you don\'t have to.',
|
||||
icon: <SearchIcon />,
|
||||
status: 'Coming Soon',
|
||||
status: 'Auto-scheduled',
|
||||
color: '#3b82f6',
|
||||
details: [
|
||||
'Analyze 15-25 relevant competitors automatically discovered',
|
||||
'Crawl competitor homepages for content strategy analysis',
|
||||
'Extract competitive advantages and market positioning',
|
||||
'Identify content gaps and opportunities',
|
||||
'Generate strategic recommendations based on competitive intelligence'
|
||||
'Uncover their top-performing content and keywords',
|
||||
'Identify their unique selling propositions (USPs)',
|
||||
'Spot gaps in their content strategy you can exploit',
|
||||
'Analyze their publishing frequency and patterns',
|
||||
'Get a clear roadmap to outperform them'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'sitemap-benchmarking',
|
||||
title: 'Competitive Sitemap Benchmarking',
|
||||
description: 'Compare your site structure against competitors',
|
||||
description: 'See exactly how your website stacks up against the market leaders.',
|
||||
icon: <AnalyticsIcon />,
|
||||
status: 'In Development',
|
||||
status: 'Available (No AI)',
|
||||
color: '#10b981',
|
||||
details: [
|
||||
'Analyze competitor sitemaps for structure insights',
|
||||
'Benchmark content volume against market leaders',
|
||||
'Compare publishing frequency and patterns',
|
||||
'Identify missing content categories',
|
||||
'Get SEO structure optimization recommendations'
|
||||
'Visualize your content volume vs. competitors',
|
||||
'Compare site structure and ease of navigation',
|
||||
'Check if you are publishing enough content',
|
||||
'Find missing categories your competitors have',
|
||||
'Get instant, data-backed improvement ideas'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'ai-competitive-insights',
|
||||
title: 'AI-Powered Competitive Insights',
|
||||
description: 'Advanced AI analysis of competitive landscape',
|
||||
description: 'Turn raw data into a winning game plan with AI.',
|
||||
icon: <InsightsIcon />,
|
||||
status: 'Planned',
|
||||
color: '#8b5cf6',
|
||||
details: [
|
||||
'AI-generated competitive intelligence reports',
|
||||
'Market positioning analysis with business impact',
|
||||
'Content strategy recommendations based on competitor data',
|
||||
'Competitive advantage identification',
|
||||
'Strategic roadmap for competitive differentiation'
|
||||
'Receive a personalized "Winning Moves" report',
|
||||
'Understand the business impact of your strategy',
|
||||
'Get specific content ideas to steal market share',
|
||||
'Identify your true competitive advantages',
|
||||
'Build a roadmap for long-term growth'
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -87,10 +293,10 @@ export const ComingSoonSection: React.FC = () => {
|
||||
<>
|
||||
<Box sx={{ mt: 4, mb: 2 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 700, color: '#1e293b', mb: 1.5 }}>
|
||||
🔍 Coming Soon
|
||||
🔍 Scheduled Tasks
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#64748b', mb: 4, fontSize: '1.1rem' }}>
|
||||
Advanced competitor analysis features to give you the competitive edge
|
||||
Long-running analyses that run automatically after onboarding
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
@@ -134,18 +340,48 @@ export const ComingSoonSection: React.FC = () => {
|
||||
{feature.icon}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, color: '#1e293b', mb: 1 }}>
|
||||
{feature.title}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, color: '#1e293b' }}>
|
||||
{feature.title}
|
||||
</Typography>
|
||||
{feature.id === 'deep-competitor-analysis' && (
|
||||
<Tooltip title={deepReason || 'Scheduled automatically after onboarding completion'}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: '50%',
|
||||
bgcolor: getBulbColor(deepBulb),
|
||||
boxShadow: `0 0 0 4px ${getBulbColor(deepBulb)}20`
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{feature.id === 'sitemap-benchmarking' && (
|
||||
<Tooltip title={sitemapReport?.available ? `Last run: ${sitemapReport?.last_run || 'available'}` : (sitemapReason || 'Run anytime (No AI)')}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: '50%',
|
||||
bgcolor: getBulbColor(sitemapBulb),
|
||||
boxShadow: `0 0 0 4px ${getBulbColor(sitemapBulb)}20`
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
<Chip
|
||||
label={feature.status}
|
||||
label={getFeatureStatusLabel(feature.id, feature.status)}
|
||||
size="small"
|
||||
icon={feature.status === 'Auto-scheduled' ? <CheckCircleIcon sx={{ '&&': { color: feature.color, fontSize: '1rem' } }} /> : undefined}
|
||||
sx={{
|
||||
backgroundColor: `${feature.color}20`,
|
||||
color: feature.color,
|
||||
fontWeight: 600,
|
||||
backgroundColor: feature.status === 'Auto-scheduled' ? '#ecfdf5' : `${feature.color}20`,
|
||||
color: feature.status === 'Auto-scheduled' ? '#059669' : feature.color,
|
||||
fontWeight: 700,
|
||||
fontSize: '0.75rem',
|
||||
height: 24,
|
||||
border: feature.status === 'Auto-scheduled' ? '1px solid #a7f3d0' : 'none',
|
||||
'& .MuiChip-label': {
|
||||
px: 1.5
|
||||
}
|
||||
@@ -204,10 +440,36 @@ export const ComingSoonSection: React.FC = () => {
|
||||
</Alert>
|
||||
</Box>
|
||||
|
||||
{sitemapReport?.available && sitemapBenchmarkData && (
|
||||
<Box id="sitemap-benchmark-results" sx={{ mt: 4, animation: 'fadeIn 0.5s ease-out' }}>
|
||||
<SitemapBenchmarkResults
|
||||
data={{
|
||||
user: sitemapBenchmarkData.user,
|
||||
competitors: sitemapBenchmarkData.competitors,
|
||||
timestamp: sitemapBenchmarkData.timestamp,
|
||||
benchmark: sitemapBenchmarkData.benchmark || {}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{strategicInsightsData && (
|
||||
<Box id="strategic-insights-results" sx={{ mt: 4 }}>
|
||||
<StrategicInsightsResults report={strategicInsightsData} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Feature Details Modal */}
|
||||
<Dialog
|
||||
open={openModal}
|
||||
onClose={() => setOpenModal(false)}
|
||||
onClose={(event, reason) => {
|
||||
if (reason !== 'backdropClick' && reason !== 'escapeKeyDown') {
|
||||
setOpenModal(false);
|
||||
} else if (!sitemapBenchmarkRunning) {
|
||||
setOpenModal(false);
|
||||
}
|
||||
}}
|
||||
disableEscapeKeyDown={sitemapBenchmarkRunning}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
@@ -292,69 +554,173 @@ export const ComingSoonSection: React.FC = () => {
|
||||
How It Works:
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#64748b', fontSize: '1.05rem', lineHeight: 1.7 }}>
|
||||
Our AI automatically discovers 15-25 relevant competitors using advanced search algorithms.
|
||||
Then we crawl each competitor's homepage to analyze their content strategy, identify their
|
||||
competitive advantages, and find content gaps that present opportunities for your business.
|
||||
Once you finish onboarding, Alwrity automatically starts analyzing the competitors we found.
|
||||
We compare your website's performance against theirs to find hidden opportunities.
|
||||
You'll see the results in your SEO Dashboard, including a breakdown of what makes them successful and how you can do better.
|
||||
</Typography>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: '#334155', fontWeight: 600 }}>
|
||||
Status:
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#64748b', mt: 0.5 }}>
|
||||
{deepBulb === 'red'
|
||||
? (deepReason || "Not eligible yet. No competitors found.")
|
||||
: "Eligible. This will run automatically after onboarding."}
|
||||
</Typography>
|
||||
{deepTask?.exists && (
|
||||
<Typography variant="body2" sx={{ color: '#64748b', mt: 0.5 }}>
|
||||
{deepTask.last_status
|
||||
? `Last run: ${deepTask.last_status}${deepTask.last_run ? ` at ${deepTask.last_run}` : ''}`
|
||||
: (deepTask.next_execution ? `Next scheduled: ${deepTask.next_execution}` : `Task status: ${deepTask.status || 'unknown'}`)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{selectedFeatureData.id === 'sitemap-benchmarking' && (
|
||||
<Box sx={{ mt: 3, p: 3, backgroundColor: '#f0f9ff', borderRadius: 3, border: '1px solid #e2e8f0' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 2, color: '#1e293b', fontSize: '1.1rem' }}>
|
||||
Competitive Intelligence:
|
||||
Why This Matters:
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#64748b', fontSize: '1.05rem', lineHeight: 1.7 }}>
|
||||
We analyze competitor sitemaps to understand their content structure, publishing patterns,
|
||||
and SEO optimization. This gives you data-driven insights into how your site compares to
|
||||
market leaders and what improvements will have the biggest competitive impact.
|
||||
We scan competitor websites to understand how they organize their content and how often they publish.
|
||||
This shows you exactly where you need to improve to match or beat the market leaders.
|
||||
</Typography>
|
||||
|
||||
{sitemapBenchmarkRunning && (
|
||||
<Box sx={{ mt: 3, mb: 2 }}>
|
||||
<LinearProgress />
|
||||
<Typography variant="caption" sx={{ mt: 1, display: 'block', textAlign: 'center', color: '#64748b' }}>
|
||||
{isLongRunning
|
||||
? "This is taking longer than usual. Large websites can take a few minutes..."
|
||||
: "Analyzing competitor websites... please wait."}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{loadingBenchmarkData ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : sitemapReport?.available ? (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Alert severity="success">
|
||||
Benchmark Report is ready! Close this window to view the detailed analysis below.
|
||||
</Alert>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body2" sx={{ color: '#334155', fontWeight: 700 }}>
|
||||
Status:
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: sitemapReport?.status === 'failed' ? '#ef4444' : '#64748b', mt: 0.5 }}>
|
||||
{sitemapReport?.available
|
||||
? 'Report is ready.'
|
||||
: sitemapReport?.status === 'failed'
|
||||
? `Failed: ${sitemapReport?.error || 'Unknown error'}`
|
||||
: sitemapReport?.status === 'processing'
|
||||
? 'Analysis in progress...'
|
||||
: 'No report yet. You can run it now (No AI).'}
|
||||
</Typography>
|
||||
{sitemapReport?.last_run && (
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 0.75, color: '#64748b' }}>
|
||||
Last run: {sitemapReport.last_run}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{sitemapBenchmarkError && (
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 0.75, color: '#ef4444' }}>
|
||||
{sitemapBenchmarkError}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{selectedFeatureData.id === 'ai-competitive-insights' && (
|
||||
<Box sx={{ mt: 3, p: 3, backgroundColor: '#f0f9ff', borderRadius: 3, border: '1px solid #e2e8f0' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 2, color: '#1e293b', fontSize: '1.1rem' }}>
|
||||
Strategic Value:
|
||||
The "Winning Moves" Advantage:
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#64748b', fontSize: '1.05rem', lineHeight: 1.7 }}>
|
||||
Our AI analyzes the competitive landscape to provide strategic recommendations with
|
||||
business impact estimates. You'll get specific content priorities, competitive positioning
|
||||
advice, and a roadmap for differentiating your brand in the market.
|
||||
We turn millions of data points into a clear "Winning Moves" report.
|
||||
See exactly which content will drive the most traffic and revenue,
|
||||
and get a step-by-step plan to steal market share from your competitors.
|
||||
</Typography>
|
||||
|
||||
{strategicInsightsRunning && (
|
||||
<Box sx={{ mt: 3, mb: 2 }}>
|
||||
<LinearProgress />
|
||||
<Typography variant="caption" sx={{ mt: 1, display: 'block', textAlign: 'center', color: '#64748b' }}>
|
||||
Our AI is analyzing market shifts and competitor moves... this takes about 30-45 seconds.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{strategicInsightsError && (
|
||||
<Typography variant="caption" sx={{ display: 'block', mt: 1.5, color: '#ef4444', textAlign: 'center' }}>
|
||||
{strategicInsightsError}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ p: 3, pt: 1, backgroundColor: '#f8fafc', borderTop: '1px solid #e2e8f0' }}>
|
||||
<Button
|
||||
onClick={() => setOpenModal(false)}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
borderColor: '#d1d5db',
|
||||
color: '#6b7280',
|
||||
'&:hover': {
|
||||
borderColor: '#9ca3af',
|
||||
backgroundColor: '#f9fafb'
|
||||
}
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<DialogActions sx={{ p: 3, pt: 1, backgroundColor: '#f8fafc', borderTop: '1px solid #e2e8f0', justifyContent: 'space-between' }}>
|
||||
{selectedFeatureData?.id === 'sitemap-benchmarking' && (
|
||||
<Button
|
||||
onClick={runSitemapBenchmark}
|
||||
variant="contained"
|
||||
disabled={sitemapBenchmarkRunning}
|
||||
startIcon={sitemapBenchmarkRunning ? <CircularProgress size={20} color="inherit" /> : null}
|
||||
sx={{
|
||||
backgroundColor: '#10b981',
|
||||
'&:hover': { backgroundColor: '#059669' },
|
||||
textTransform: 'none',
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{sitemapBenchmarkRunning ? 'Running Benchmark...' : 'Run Benchmark Now (No AI)'}
|
||||
</Button>
|
||||
)}
|
||||
{selectedFeatureData?.id === 'ai-competitive-insights' && (
|
||||
<Button
|
||||
onClick={runStrategicInsights}
|
||||
variant="contained"
|
||||
disabled={strategicInsightsRunning || missingData}
|
||||
startIcon={strategicInsightsRunning ? <CircularProgress size={20} color="inherit" /> : <AIIcon />}
|
||||
sx={{
|
||||
backgroundColor: '#8b5cf6',
|
||||
'&:hover': { backgroundColor: '#7c3aed' },
|
||||
textTransform: 'none',
|
||||
fontWeight: 700
|
||||
}}
|
||||
>
|
||||
{strategicInsightsRunning ? 'Generating Winning Moves...' : (missingData ? 'Complete Step 2 First' : 'Run AI Analysis Now')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => setOpenModal(false)}
|
||||
variant="contained"
|
||||
sx={{
|
||||
backgroundColor: selectedFeatureData?.color || '#3b82f6',
|
||||
backgroundColor: '#3b82f6',
|
||||
px: 4,
|
||||
py: 1,
|
||||
borderRadius: 2,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
boxShadow: '0 4px 6px -1px rgba(59, 130, 246, 0.5)',
|
||||
'&:hover': {
|
||||
backgroundColor: selectedFeatureData?.color || '#3b82f6',
|
||||
opacity: 0.9
|
||||
backgroundColor: '#2563eb',
|
||||
boxShadow: '0 10px 15px -3px rgba(59, 130, 246, 0.5)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
Notify Me When Ready
|
||||
Got it, thanks!
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Grid,
|
||||
Chip,
|
||||
Card,
|
||||
CardContent,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
Alert,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Speed as SpeedIcon,
|
||||
Description as DescriptionIcon,
|
||||
AccountTree as TreeIcon,
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Lightbulb as LightbulbIcon,
|
||||
CheckCircle as CheckIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
export interface BenchmarkMetrics {
|
||||
total_urls: number;
|
||||
publishing_velocity: number;
|
||||
average_path_depth: number;
|
||||
max_path_depth: number;
|
||||
top_url_patterns: Record<string, number>;
|
||||
file_types: Record<string, number>;
|
||||
date_range?: {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Opportunity {
|
||||
type: string;
|
||||
title: string;
|
||||
items?: Array<{
|
||||
section: string;
|
||||
competitor_presence: number;
|
||||
competitor_url_count: number;
|
||||
}>;
|
||||
metrics?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface BenchmarkData {
|
||||
user?: {
|
||||
summary: BenchmarkMetrics;
|
||||
error?: string;
|
||||
};
|
||||
competitors?: {
|
||||
summaries: Record<string, BenchmarkMetrics>;
|
||||
errors?: Record<string, string>;
|
||||
};
|
||||
// Support for potential flat structure (legacy or different service versions)
|
||||
user_summary?: BenchmarkMetrics;
|
||||
competitor_summaries?: Record<string, BenchmarkMetrics>;
|
||||
|
||||
timestamp?: string;
|
||||
benchmark?: {
|
||||
website_url?: string;
|
||||
competitors_analyzed?: number;
|
||||
user_sections_count?: number;
|
||||
competitor_section_leaders?: Array<{
|
||||
competitor_url: string;
|
||||
total_urls: number;
|
||||
sections_count: number;
|
||||
publishing_velocity: number;
|
||||
average_path_depth?: number;
|
||||
lastmod_coverage?: number;
|
||||
}>;
|
||||
gaps?: {
|
||||
missing_sections?: Array<{
|
||||
section: string;
|
||||
competitor_presence: number;
|
||||
competitor_url_count: number;
|
||||
}>;
|
||||
};
|
||||
opportunities?: Array<Opportunity>;
|
||||
gaps_vs_competitors?: {
|
||||
missing_sections?: Array<{
|
||||
section: string;
|
||||
competitor_url_count: number;
|
||||
competitor_presence?: number;
|
||||
}>;
|
||||
};
|
||||
keyword_hints?: Array<{
|
||||
keyword: string;
|
||||
seen_in_url_patterns: boolean;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
data: BenchmarkData;
|
||||
}
|
||||
|
||||
export const SitemapBenchmarkResults: React.FC<Props> = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
const { benchmark } = data;
|
||||
|
||||
// Handle data mapping from potentially nested structure
|
||||
const user_summary = data.user_summary || data.user?.summary || {} as BenchmarkMetrics;
|
||||
const competitor_summaries = data.competitor_summaries || data.competitors?.summaries || {};
|
||||
const competitor_errors = data.competitors?.errors || {};
|
||||
const user_error = data.user?.error;
|
||||
|
||||
// Calculate competitor averages
|
||||
const competitorUrls = Object.keys(competitor_summaries || {});
|
||||
const avgCompetitorUrls = competitorUrls.length > 0
|
||||
? Math.round(competitorUrls.reduce((acc, url) => acc + (competitor_summaries[url]?.total_urls || 0), 0) / competitorUrls.length)
|
||||
: 0;
|
||||
|
||||
const avgCompetitorVelocity = competitorUrls.length > 0
|
||||
? parseFloat((competitorUrls.reduce((acc, url) => acc + (competitor_summaries[url]?.publishing_velocity || 0), 0) / competitorUrls.length).toFixed(2))
|
||||
: 0;
|
||||
|
||||
const avgCompetitorDepth = competitorUrls.length > 0
|
||||
? parseFloat((competitorUrls.reduce((acc, url) => acc + (competitor_summaries[url]?.average_path_depth || 0), 0) / competitorUrls.length).toFixed(2))
|
||||
: 0;
|
||||
|
||||
const MetricCard = ({ title, userValue, competitorValue, icon, unit = '', description }: any) => {
|
||||
const isBelowAvg = userValue < competitorValue;
|
||||
|
||||
return (
|
||||
<Card
|
||||
elevation={0}
|
||||
sx={{
|
||||
height: '100%',
|
||||
border: `1px solid #e2e8f0`,
|
||||
borderRadius: 3,
|
||||
bgcolor: '#f8fafc',
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.primary.main,
|
||||
bgcolor: '#ffffff',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.05)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: 2.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ p: 1, borderRadius: 2, bgcolor: theme.palette.primary.main + '10', color: theme.palette.primary.main, mr: 1.5, display: 'flex' }}>
|
||||
{icon}
|
||||
</Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: '#475569' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Tooltip title={description} arrow placement="top">
|
||||
<Box sx={{ cursor: 'help', color: '#94a3b8', display: 'flex' }}>
|
||||
<LightbulbIcon sx={{ fontSize: 18 }} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mb: 2.5 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 800, color: '#1e293b', mb: 0.5 }}>
|
||||
{userValue}{unit}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
||||
Your Site
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
pt: 2,
|
||||
borderTop: `1px dashed #e2e8f0`
|
||||
}}>
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#334155' }}>
|
||||
{competitorValue}{unit}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8' }}>
|
||||
Competitor Avg
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip
|
||||
label={userValue >= competitorValue ? 'Above Avg' : 'Below Avg'}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 24,
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 700,
|
||||
bgcolor: userValue >= competitorValue ? '#ecfdf5' : '#fff7ed',
|
||||
color: userValue >= competitorValue ? '#059669' : '#c2410c',
|
||||
border: `1px solid ${userValue >= competitorValue ? '#a7f3d0' : '#ffedd5'}`,
|
||||
borderRadius: 1.5
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const metricDescriptions = {
|
||||
volume: "Total number of indexed pages discovered in your sitemap compared to the average of your top competitors.",
|
||||
velocity: "Average number of new pages published per week, indicating how active the content strategy is.",
|
||||
depth: "Average number of clicks required to reach content from the homepage based on URL structure."
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800, color: '#1e293b', mb: 1, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<TrendingUpIcon sx={{ color: theme.palette.primary.main, fontSize: 28 }} />
|
||||
Benchmark Analysis
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ color: '#64748b' }}>
|
||||
Comparison based on <strong>{competitorUrls.length}</strong> competitor sitemaps.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Main Metrics Row with Errors */}
|
||||
<Grid container spacing={3} sx={{ mb: 5 }}>
|
||||
{/* Errors Area A (if exists) */}
|
||||
{(user_error || Object.keys(competitor_errors).length > 0) ? (
|
||||
<Grid item xs={12}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} lg={4}>
|
||||
<Box sx={{ height: '100%' }}>
|
||||
{user_error && (
|
||||
<Alert severity="error" sx={{ mb: 2, borderRadius: 3, border: '1px solid #fee2e2' }}>
|
||||
<Typography variant="subtitle2" fontWeight="bold">User Sitemap Error:</Typography>
|
||||
{user_error}
|
||||
</Alert>
|
||||
)}
|
||||
{Object.keys(competitor_errors).length > 0 && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
sx={{
|
||||
height: '100%',
|
||||
borderRadius: 3,
|
||||
border: '1px solid #ffedd5',
|
||||
bgcolor: '#fffaf5',
|
||||
'& .MuiAlert-message': { width: '100%' }
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, mb: 1, color: '#9a3412' }}>
|
||||
{Object.keys(competitor_errors).length} competitors could not be analyzed:
|
||||
</Typography>
|
||||
<Box sx={{ maxHeight: 150, overflowY: 'auto', pr: 1 }}>
|
||||
<List dense disablePadding>
|
||||
{Object.entries(competitor_errors).map(([url, error]) => (
|
||||
<ListItem key={url} disablePadding sx={{ py: 0.5 }}>
|
||||
<ListItemText
|
||||
primary={url.replace('https://', '').replace('www.', '')}
|
||||
secondary={String(error)}
|
||||
primaryTypographyProps={{ variant: 'caption', fontWeight: 700, color: '#c2410c' }}
|
||||
secondaryTypographyProps={{ variant: 'caption', color: '#9a3412' }}
|
||||
sx={{ m: 0 }}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} lg={8}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<MetricCard
|
||||
title="Content Volume"
|
||||
userValue={user_summary.total_urls || 0}
|
||||
competitorValue={avgCompetitorUrls}
|
||||
icon={<DescriptionIcon />}
|
||||
description={metricDescriptions.volume}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<MetricCard
|
||||
title="Publishing Velocity"
|
||||
userValue={user_summary.publishing_velocity ? parseFloat(user_summary.publishing_velocity.toFixed(2)) : 0}
|
||||
competitorValue={avgCompetitorVelocity}
|
||||
icon={<SpeedIcon />}
|
||||
unit=" /wk"
|
||||
description={metricDescriptions.velocity}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={4}>
|
||||
<MetricCard
|
||||
title="Structure Depth"
|
||||
userValue={user_summary.average_path_depth ? parseFloat(user_summary.average_path_depth.toFixed(2)) : 0}
|
||||
competitorValue={avgCompetitorDepth}
|
||||
icon={<TreeIcon />}
|
||||
unit=" clicks"
|
||||
description={metricDescriptions.depth}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<>
|
||||
<Grid item xs={12} md={4}>
|
||||
<MetricCard
|
||||
title="Content Volume"
|
||||
userValue={user_summary.total_urls || 0}
|
||||
competitorValue={avgCompetitorUrls}
|
||||
icon={<DescriptionIcon />}
|
||||
description={metricDescriptions.volume}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<MetricCard
|
||||
title="Publishing Velocity"
|
||||
userValue={user_summary.publishing_velocity ? parseFloat(user_summary.publishing_velocity.toFixed(2)) : 0}
|
||||
competitorValue={avgCompetitorVelocity}
|
||||
icon={<SpeedIcon />}
|
||||
unit=" /wk"
|
||||
description={metricDescriptions.velocity}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={4}>
|
||||
<MetricCard
|
||||
title="Structure Depth"
|
||||
userValue={user_summary.average_path_depth ? parseFloat(user_summary.average_path_depth.toFixed(2)) : 0}
|
||||
competitorValue={avgCompetitorDepth}
|
||||
icon={<TreeIcon />}
|
||||
unit=" clicks"
|
||||
description={metricDescriptions.depth}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Industry Benchmarks Section */}
|
||||
<Box sx={{ mb: 6 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 800, color: '#1e293b', mb: 3, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<LightbulbIcon sx={{ color: '#f59e0b' }} />
|
||||
Industry Benchmarks
|
||||
</Typography>
|
||||
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 4,
|
||||
borderRadius: 4,
|
||||
border: '1px solid #e2e8f0',
|
||||
bgcolor: '#ffffff'
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, mb: 3, color: '#334155' }}>
|
||||
Common competitor sections you may be missing
|
||||
</Typography>
|
||||
|
||||
{(benchmark?.gaps?.missing_sections || benchmark?.gaps_vs_competitors?.missing_sections) &&
|
||||
(benchmark?.gaps?.missing_sections || benchmark?.gaps_vs_competitors?.missing_sections || []).length > 0 ? (
|
||||
<Grid container spacing={2}>
|
||||
{(benchmark?.gaps?.missing_sections || benchmark?.gaps_vs_competitors?.missing_sections || []).map((gap: any, index: number) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 2,
|
||||
bgcolor: '#f1f5f9',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
border: '1px solid #e2e8f0'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<CheckIcon sx={{ color: '#94a3b8', fontSize: 18 }} />
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#475569', textTransform: 'capitalize' }}>
|
||||
/{gap.section}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Tooltip title={`${gap.competitor_count || Math.round((gap.competitor_presence || 0) * competitorUrls.length)} out of ${competitorUrls.length} competitors have this section.`}>
|
||||
<Chip
|
||||
label={`${Math.round((gap.competitor_presence || 0) * 100)}% Presence`}
|
||||
size="small"
|
||||
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 800, bgcolor: '#e2e8f0', color: '#64748b' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
) : (
|
||||
<Box sx={{ p: 4, textAlign: 'center', bgcolor: '#f8fafc', borderRadius: 3, border: '1px dashed #e2e8f0' }}>
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8', fontWeight: 500 }}>
|
||||
No significant content gaps identified compared to your competitors.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Box>
|
||||
|
||||
{/* Actionable Insights */}
|
||||
{benchmark?.opportunities && benchmark.opportunities.length > 0 && (
|
||||
<Box sx={{ mb: 6 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 800, color: '#1e293b', mb: 3, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<LightbulbIcon sx={{ color: '#f59e0b' }} />
|
||||
Strategic Insights
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{benchmark.opportunities.map((opp: any, index: number) => (
|
||||
<Grid item xs={12} md={6} key={index}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 3,
|
||||
height: '100%',
|
||||
bgcolor: '#ffffff',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: 3,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 1.5 }}>
|
||||
<Box sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: '50%',
|
||||
bgcolor: theme.palette.primary.main + '20',
|
||||
color: theme.palette.primary.main,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
mt: 0.2
|
||||
}}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 900 }}>!</Typography>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ fontWeight: 700, color: '#334155', lineHeight: 1.4 }}>
|
||||
{opp.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
{opp.metrics && (
|
||||
<Box sx={{ ml: 5, mt: 'auto', pt: 2, borderTop: '1px solid #f1f5f9', display: 'flex', gap: 3, flexWrap: 'wrap' }}>
|
||||
{Object.entries(opp.metrics).map(([key, value]) => (
|
||||
<Box key={key} sx={{ mb: 1, '&:last-child': { mb: 0 } }}>
|
||||
<Typography variant="caption" sx={{ color: '#64748b', textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||
{(() => {
|
||||
if (key.startsWith('user_')) {
|
||||
return 'Your ' + key.replace('user_', '').replace(/_/g, ' ');
|
||||
}
|
||||
if (key.includes('competitor_median_')) {
|
||||
return 'Competitor Avg ' + key.replace('competitor_median_', '').replace(/_/g, ' ');
|
||||
}
|
||||
return key.replace(/_/g, ' ');
|
||||
})()}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700, color: '#475569' }}>
|
||||
{typeof value === 'number' ? value.toFixed(2) : String(value)}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Competitor Leaders Table */}
|
||||
{benchmark?.competitor_section_leaders && benchmark.competitor_section_leaders.length > 0 && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="subtitle1" fontWeight={700} gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2, color: '#1e293b' }}>
|
||||
<DescriptionIcon color="info" fontSize="small" />
|
||||
Top Competitor Stats
|
||||
</Typography>
|
||||
<TableContainer component={Paper} elevation={0} sx={{ border: '1px solid #e2e8f0', borderRadius: 2 }}>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow sx={{ bgcolor: '#f8fafc' }}>
|
||||
<TableCell sx={{ fontWeight: 700, color: '#1e293b' }}>Competitor</TableCell>
|
||||
<Tooltip title="Total number of pages found in the sitemap" arrow>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#1e293b', cursor: 'help' }}>Total URLs</TableCell>
|
||||
</Tooltip>
|
||||
<Tooltip title="Number of distinct URL path sections (e.g., /blog/, /products/)" arrow>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#1e293b', cursor: 'help' }}>Sections</TableCell>
|
||||
</Tooltip>
|
||||
<Tooltip title="Average new pages published per week" arrow>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#1e293b', cursor: 'help' }}>Velocity/wk</TableCell>
|
||||
</Tooltip>
|
||||
<Tooltip title="Average URL path depth (clicks from root)" arrow>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#1e293b', cursor: 'help' }}>Avg Depth</TableCell>
|
||||
</Tooltip>
|
||||
<Tooltip title="Percentage of URLs with valid last-modified dates" arrow>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#1e293b', cursor: 'help' }}>Date Coverage</TableCell>
|
||||
</Tooltip>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{benchmark.competitor_section_leaders.map((comp, idx) => (
|
||||
<TableRow key={idx} sx={{ bgcolor: '#ffffff', '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component="th" scope="row" sx={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: 500, color: '#334155' }}>
|
||||
<Tooltip title={comp.competitor_url}>
|
||||
<span>{new URL(comp.competitor_url).hostname}</span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: '#475569' }}>{comp.total_urls}</TableCell>
|
||||
<TableCell align="right" sx={{ color: '#475569' }}>{comp.sections_count}</TableCell>
|
||||
<TableCell align="right" sx={{ color: '#475569' }}>{comp.publishing_velocity?.toFixed(1) || '-'}</TableCell>
|
||||
<TableCell align="right" sx={{ color: '#475569' }}>{comp.average_path_depth?.toFixed(1) || '-'}</TableCell>
|
||||
<TableCell align="right" sx={{ color: '#475569' }}>
|
||||
{comp.lastmod_coverage ? `${(comp.lastmod_coverage * 100).toFixed(0)}%` : '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Competitor Content Strategy Patterns (formerly Content Gaps) */}
|
||||
{((benchmark?.gaps_vs_competitors?.missing_sections && benchmark.gaps_vs_competitors.missing_sections.length > 0) ||
|
||||
(benchmark?.gaps?.missing_sections && benchmark.gaps.missing_sections.length > 0)) && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="subtitle1" fontWeight={700} gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1, color: '#0f172a' }}>
|
||||
<TrendingUpIcon color="primary" fontSize="small" />
|
||||
Competitor Content Strategy Patterns
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3, maxWidth: '800px' }}>
|
||||
The following content categories appear frequently across your competitors' websites but are missing from yours.
|
||||
Consider creating content in these areas to capture similar traffic and improve your competitive positioning.
|
||||
</Typography>
|
||||
<Grid container spacing={2}>
|
||||
{(benchmark?.gaps?.missing_sections || benchmark?.gaps_vs_competitors?.missing_sections || [])
|
||||
.filter((gap: any) => gap.section && gap.section.length > 3) // Filter out short sections like language codes (/es, /fr)
|
||||
.map((gap: any, index: number) => {
|
||||
// Fix for "200% Presence" bug: normalize values
|
||||
let presence = gap.competitor_presence || 0;
|
||||
// If presence > 1, it's likely a raw count, so normalize it
|
||||
if (presence > 1) {
|
||||
presence = presence / (competitorUrls.length || 1);
|
||||
}
|
||||
const percentage = Math.min(Math.round(presence * 100), 100);
|
||||
const count = gap.competitor_count || Math.round(presence * competitorUrls.length);
|
||||
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={4} key={index}>
|
||||
<Paper variant="outlined" sx={{ p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between', bgcolor: '#ffffff', border: '1px solid #e2e8f0' }}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" fontWeight="bold" sx={{ color: '#1e293b' }}>
|
||||
{gap.section}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#64748b' }}>
|
||||
Used by {count} of {competitorUrls.length} competitors
|
||||
</Typography>
|
||||
</Box>
|
||||
<Tooltip title={`${percentage}% of your competitors have this section`}>
|
||||
<Chip
|
||||
label={`${percentage}% Match`}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
sx={{ fontWeight: 700, bgcolor: '#eff6ff', border: '1px solid #bfdbfe', color: '#1d4ed8' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Paper>
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{benchmark?.keyword_hints && benchmark.keyword_hints.length > 0 && (
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="subtitle1" fontWeight={700} gutterBottom sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<LightbulbIcon color="warning" fontSize="small" />
|
||||
Keyword Opportunities (from URL patterns)
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{benchmark.keyword_hints.map((hint, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={hint.keyword}
|
||||
color={hint.seen_in_url_patterns ? "success" : "default"}
|
||||
variant={hint.seen_in_url_patterns ? "filled" : "outlined"}
|
||||
icon={hint.seen_in_url_patterns ? <CheckIcon fontSize="small" /> : undefined}
|
||||
sx={{ borderColor: theme.palette.divider }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<TableContainer component={Paper} elevation={0} sx={{ border: '1px solid #e2e8f0', borderRadius: 2 }}>
|
||||
<Table size="small">
|
||||
<TableHead sx={{ bgcolor: '#f8fafc' }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 700, color: '#1e293b' }}>Website</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#1e293b' }}>Total Pages</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#1e293b' }}>Velocity (posts/week)</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#1e293b' }}>Avg Depth</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#1e293b' }}>Top Category</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{/* User Row */}
|
||||
<TableRow sx={{ bgcolor: theme.palette.primary.main + '08', '& td, & th': { borderBottom: '1px solid #e2e8f0' } }}>
|
||||
<TableCell component="th" scope="row">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="body2" fontWeight={700} color="primary.main">Your Website</Typography>
|
||||
<Chip label="You" size="small" color="primary" sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700 }} />
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#334155' }}>{user_summary.total_urls}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#334155' }}>{user_summary.publishing_velocity?.toFixed(2) || '0.00'}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#334155' }}>{user_summary.average_path_depth?.toFixed(2) || '0.00'}</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 700, color: '#334155' }}>
|
||||
{Object.keys(user_summary.top_url_patterns || {})[0] || '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{/* Competitor Rows */}
|
||||
{competitorUrls.map((url, idx) => {
|
||||
const data = competitor_summaries[url];
|
||||
const domain = url.replace(/^https?:\/\/(www\.)?/, '').split('/')[0];
|
||||
return (
|
||||
<TableRow key={url} sx={{ bgcolor: '#ffffff', '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
<TableCell component="th" scope="row" sx={{ color: '#475569', fontWeight: 500 }}>
|
||||
{domain}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ color: '#64748b' }}>{data?.total_urls || 0}</TableCell>
|
||||
<TableCell align="right" sx={{ color: '#64748b' }}>{data?.publishing_velocity?.toFixed(2) || '0.00'}</TableCell>
|
||||
<TableCell align="right" sx={{ color: '#64748b' }}>{data?.average_path_depth?.toFixed(2) || '0.00'}</TableCell>
|
||||
<TableCell align="right" sx={{ color: '#64748b' }}>
|
||||
{Object.keys(data?.top_url_patterns || {})[0] || '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
);
|
||||
|
||||
};
|
||||
@@ -0,0 +1,265 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Grid,
|
||||
Chip,
|
||||
Card,
|
||||
CardContent,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
Button,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Avatar
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp as TrendingUpIcon,
|
||||
Lightbulb as LightbulbIcon,
|
||||
Warning as WarningIcon,
|
||||
Speed as SpeedIcon,
|
||||
CheckCircle as CheckIcon,
|
||||
ArrowForward as ArrowIcon,
|
||||
Star as StarIcon,
|
||||
Bolt as BoltIcon,
|
||||
AutoAwesome as AIIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
export interface StrategicInsight {
|
||||
type: string;
|
||||
insight: string;
|
||||
reasoning?: string;
|
||||
priority: string;
|
||||
estimated_impact: string;
|
||||
implementation_time?: string;
|
||||
}
|
||||
|
||||
export interface ContentRecommendation {
|
||||
recommendation: string;
|
||||
priority: string;
|
||||
estimated_traffic: string;
|
||||
roi_estimate: string;
|
||||
}
|
||||
|
||||
export interface StrategicInsightsReport {
|
||||
week_commencing: string;
|
||||
generated_at: string;
|
||||
metrics: {
|
||||
market_velocity: number;
|
||||
user_velocity: number;
|
||||
};
|
||||
insights: {
|
||||
the_big_move: StrategicInsight;
|
||||
low_hanging_fruit: ContentRecommendation[];
|
||||
threat_alerts: StrategicInsight[];
|
||||
};
|
||||
raw_data?: any;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
report: StrategicInsightsReport;
|
||||
hideCreateContent?: boolean;
|
||||
}
|
||||
|
||||
export const StrategicInsightsResults: React.FC<Props> = ({ report, hideCreateContent = false }) => {
|
||||
const theme = useTheme();
|
||||
const { insights, metrics, week_commencing } = report;
|
||||
|
||||
const handleCreateContent = (topic: string) => {
|
||||
// Logic to redirect to Blog Writer with pre-filled prompt
|
||||
const prompt = encodeURIComponent(`Write a high-quality blog post about "${topic}" that outperforms my competitors. Focus on unique value propositions and clear CTAs.`);
|
||||
window.location.href = `/blog-writer?prompt=${prompt}`;
|
||||
};
|
||||
|
||||
const PriorityChip = ({ priority }: { priority: string }) => {
|
||||
const isHigh = priority?.toLowerCase() === 'high';
|
||||
return (
|
||||
<Chip
|
||||
label={priority}
|
||||
size="small"
|
||||
sx={{
|
||||
height: 20,
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 800,
|
||||
bgcolor: isHigh ? '#fee2e2' : '#f1f5f9',
|
||||
color: isHigh ? '#b91c1c' : '#475569',
|
||||
border: `1px solid ${isHigh ? '#fecaca' : '#e2e8f0'}`,
|
||||
ml: 1
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 4, animation: 'fadeIn 0.5s ease-out' }}>
|
||||
<Box sx={{ mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 800, color: '#1e293b', mb: 1, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<AIIcon sx={{ color: '#8b5cf6', fontSize: 28 }} />
|
||||
Weekly Strategic Intelligence
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#64748b' }}>
|
||||
AI-generated insights for the week commencing <strong>{new Date(week_commencing).toLocaleDateString()}</strong>.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Tooltip title="Market velocity indicates how many new pages your competitors are publishing per week.">
|
||||
<Paper variant="outlined" sx={{ px: 2, py: 1, bgcolor: '#f8fafc', borderRadius: 2, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<SpeedIcon sx={{ color: '#64748b', fontSize: 20 }} />
|
||||
<Box>
|
||||
<Typography variant="caption" display="block" sx={{ color: '#94a3b8', fontWeight: 700, lineHeight: 1 }}>MARKET VELOCITY</Typography>
|
||||
<Typography variant="subtitle2" sx={{ color: '#1e293b', fontWeight: 800 }}>{metrics.market_velocity} posts/wk</Typography>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* The Big Move - Hero Insight */}
|
||||
<Grid item xs={12}>
|
||||
<Card
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderRadius: 4,
|
||||
background: 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)',
|
||||
color: 'white',
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'absolute', right: -20, top: -20, opacity: 0.1 }}>
|
||||
<BoltIcon sx={{ fontSize: 200 }} />
|
||||
</Box>
|
||||
<CardContent sx={{ p: 4 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 3 }}>
|
||||
<Avatar sx={{ bgcolor: 'rgba(255,255,255,0.2)', width: 56, height: 56 }}>
|
||||
<StarIcon sx={{ fontSize: 32 }} />
|
||||
</Avatar>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="overline" sx={{ fontWeight: 800, letterSpacing: 2, opacity: 0.9 }}>
|
||||
THE BIG MOVE
|
||||
</Typography>
|
||||
<Typography variant="h4" sx={{ fontWeight: 800, mb: 2 }}>
|
||||
{insights.the_big_move?.insight || "Analyzing market shifts..."}
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ opacity: 0.9, mb: 3, maxWidth: '800px', lineHeight: 1.7 }}>
|
||||
{insights.the_big_move?.reasoning || "We've detected a significant strategic shift in your competitive landscape. Addressing this now will give you a first-mover advantage."}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Chip label={`Impact: ${insights.the_big_move?.estimated_impact || 'High'}`} sx={{ bgcolor: 'rgba(255,255,255,0.2)', color: 'white', fontWeight: 700 }} />
|
||||
<Chip label={`Priority: ${insights.the_big_move?.priority || 'Critical'}`} sx={{ bgcolor: 'rgba(255,255,255,0.2)', color: 'white', fontWeight: 700 }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* Low Hanging Fruit - Actionable Recommendations */}
|
||||
<Grid item xs={12} lg={7}>
|
||||
<Paper elevation={0} sx={{ p: 3, borderRadius: 4, border: '1px solid #e2e8f0', bgcolor: '#ffffff', height: '100%' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 800, color: '#1e293b', mb: 3, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<LightbulbIcon sx={{ color: '#f59e0b' }} />
|
||||
Low-Hanging Fruit
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#64748b', mb: 3 }}>
|
||||
Topics your competitors are starting to cover that you can easily outperform with better content.
|
||||
</Typography>
|
||||
|
||||
<List disablePadding>
|
||||
{insights.low_hanging_fruit?.slice(0, 4).map((rec, idx) => (
|
||||
<React.Fragment key={idx}>
|
||||
<ListItem
|
||||
sx={{
|
||||
px: 0,
|
||||
py: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'flex-start', mb: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: '#334155', flex: 1 }}>
|
||||
{rec.recommendation}
|
||||
</Typography>
|
||||
<PriorityChip priority={rec.priority} />
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, width: '100%' }}>
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<TrendingUpIcon sx={{ fontSize: 14 }} /> Traffic: {rec.estimated_traffic}
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<BoltIcon sx={{ fontSize: 14 }} /> ROI: {rec.roi_estimate}
|
||||
</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
{!hideCreateContent && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
endIcon={<ArrowIcon />}
|
||||
onClick={() => handleCreateContent(rec.recommendation)}
|
||||
sx={{ fontWeight: 700, textTransform: 'none' }}
|
||||
>
|
||||
Create Content
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</ListItem>
|
||||
{idx < 3 && <Divider />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Threat Alerts */}
|
||||
<Grid item xs={12} md={5}>
|
||||
<Paper elevation={0} sx={{ p: 3, borderRadius: 4, border: '1px solid #e2e8f0', bgcolor: '#fffcfc', height: '100%' }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 800, color: '#1e293b', mb: 3, display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<WarningIcon sx={{ color: '#ef4444' }} />
|
||||
Threat Alerts
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{insights.threat_alerts?.slice(0, 3).map((threat, idx) => (
|
||||
<Box
|
||||
key={idx}
|
||||
sx={{
|
||||
p: 2,
|
||||
borderRadius: 3,
|
||||
bgcolor: '#ffffff',
|
||||
border: '1px solid #fee2e2',
|
||||
borderLeft: '4px solid #ef4444'
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800, color: '#991b1b', mb: 0.5 }}>
|
||||
{threat.type || 'Competitive Threat'}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: '#475569', mb: 1.5, lineHeight: 1.5 }}>
|
||||
{threat.insight}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 1 }}>
|
||||
<Chip label={threat.estimated_impact} size="small" sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700, bgcolor: '#fef2f2', color: '#ef4444' }} />
|
||||
<Button size="small" variant="outlined" color="error" sx={{ fontSize: '0.7rem', fontWeight: 700, textTransform: 'none' }}>
|
||||
Mitigation Strategy
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
{!insights.threat_alerts?.length && (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<CheckIcon sx={{ color: '#10b981', fontSize: 40, mb: 1, opacity: 0.5 }} />
|
||||
<Typography variant="body2" sx={{ color: '#94a3b8' }}>No immediate threats detected this week.</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user