Files
ALwrity/frontend/src/components/SEODashboard/SEOAnalysisController.tsx

597 lines
20 KiB
TypeScript

/**
* SEO Analysis Controller Component
* Main component that orchestrates enterprise audit and GSC analysis
* with LLM insights generation and traffic improvement strategies
*/
import React, { useState, useEffect } from 'react';
import {
Box,
Container,
Typography,
Button,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
CircularProgress,
Alert,
Stepper,
Step,
StepLabel,
Card,
CardContent,
Grid,
Tab,
Tabs,
Paper,
Chip,
Stack,
LinearProgress,
} from '@mui/material';
import {
PlayArrow as PlayArrowIcon,
Refresh as RefreshIcon,
Settings as SettingsIcon,
Assessment as AssessmentIcon,
AutoAwesome as AutoAwesomeIcon,
TrendingUp as TrendingUpIcon,
Download as DownloadIcon,
} from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
import { enterpriseSeoAPI, EnterpriseAuditResult, GSCAnalysisResult } from '../../api/enterpriseSeoApi';
import { llmInsightsGenerator } from '../../api/llmInsightsGenerator';
import { EnterpriseAuditResults } from './components/EnterpriseAuditResults';
import { GSCAnalysisResults } from './components/GSCAnalysisResults';
import { ActionableInsightsDisplay } from './components/ActionableInsightsDisplay';
import { AIVisibilitySection } from './components/AIVisibilitySection';
interface AnalysisStep {
label: string;
description: string;
}
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index } = props;
return (
<div hidden={value !== index} style={{ width: '100%' }}>
{value === index && <Box sx={{ py: 2 }}>{children}</Box>}
</div>
);
}
const analysisSteps: AnalysisStep[] = [
{ label: 'Website Input', description: 'Enter your website URL' },
{ label: 'Enterprise Audit', description: 'Comprehensive SEO audit' },
{ label: 'GSC Analysis', description: 'Search performance analysis' },
{ label: 'Insights', description: 'AI-powered recommendations' },
{ label: 'Review', description: 'Review results and strategy' },
];
export const SEOAnalysisController: React.FC = () => {
// UI State
const [activeStep, setActiveStep] = useState(0);
const [tabValue, setTabValue] = useState(0);
const [websiteUrl, setWebsiteUrl] = useState('');
const [competitors, setCompetitors] = useState<string[]>([]);
const [targetKeywords, setTargetKeywords] = useState<string[]>([]);
// Analysis State
const [auditResult, setAuditResult] = useState<EnterpriseAuditResult | null>(null);
const [gscResult, setGscResult] = useState<GSCAnalysisResult | null>(null);
const [insights, setInsights] = useState<any[]>([]);
// Loading & Error State
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [progress, setProgress] = useState(0);
// Dialog State
const [openOptionsDialog, setOpenOptionsDialog] = useState(false);
const [options, setOptions] = useState({
includeContentAnalysis: true,
includeCompetitiveAnalysis: true,
generateExecutiveReport: true,
dateRangeDays: 90,
});
// Validation
const isUrlValid = websiteUrl && websiteUrl.startsWith('http');
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
/**
* Execute enterprise audit
*/
const handleStartAudit = async () => {
if (!isUrlValid) {
setError('Please enter a valid website URL starting with http:// or https://');
return;
}
setLoading(true);
setError(null);
setProgress(20);
setActiveStep(1);
try {
// Execute enterprise audit
console.log('Starting enterprise audit for', websiteUrl);
const auditResponse = await enterpriseSeoAPI.executeEnterpriseAudit(websiteUrl, {
competitors: competitors.filter(c => c.trim()),
targetKeywords: targetKeywords.filter(k => k.trim()),
includeContentAnalysis: options.includeContentAnalysis,
includeCompetitiveAnalysis: options.includeCompetitiveAnalysis,
generateExecutiveReport: options.generateExecutiveReport,
});
if (!auditResponse.success) {
throw new Error(auditResponse.message || 'Audit failed');
}
setAuditResult(auditResponse.data);
setProgress(50);
setActiveStep(2);
// Execute GSC analysis
console.log('Starting GSC analysis for', websiteUrl);
const gscResponse = await enterpriseSeoAPI.analyzeGSCSearchPerformance(websiteUrl, {
dateRangeDays: options.dateRangeDays,
includeOpportunities: true,
includeCompetitive: true,
});
if (!gscResponse.success) {
throw new Error(gscResponse.message || 'GSC analysis failed');
}
setGscResult(gscResponse.data);
setProgress(75);
setActiveStep(3);
// Skip insights generation for now - user can generate manually
setProgress(100);
setActiveStep(4);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'An error occurred';
console.error('Analysis error:', err);
setError(errorMsg);
setActiveStep(activeStep);
} finally {
setLoading(false);
}
};
/**
* Generate AI-powered insights
*/
const handleGenerateInsights = async () => {
if (!auditResult && !gscResult) {
setError('No analysis results available');
return;
}
setLoading(true);
setError(null);
try {
let insightResults = [];
if (auditResult) {
const auditInsights = await llmInsightsGenerator.generateEnterpriseAuditInsights(
auditResult,
{ currentMonthlyTraffic: 1000 } // TODO: Get from user
);
insightResults.push(...auditInsights.insights);
}
if (gscResult) {
const gscInsights = await llmInsightsGenerator.generateGSCAnalysisInsights(
gscResult,
{ currentMonthlyTraffic: 1000 } // TODO: Get from user
);
insightResults.push(...gscInsights.insights);
}
setInsights(insightResults);
setActiveStep(4);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to generate insights';
console.error('Insights generation error:', err);
setError(errorMsg);
} finally {
setLoading(false);
}
};
/**
* Download report
*/
const handleDownloadReport = () => {
const reportData = {
website: websiteUrl,
timestamp: new Date().toISOString(),
audit: auditResult,
gscAnalysis: gscResult,
insights: insights,
};
const dataStr = JSON.stringify(reportData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `seo-analysis-${new Date().getTime()}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
/**
* Reset analysis
*/
const handleReset = () => {
setWebsiteUrl('');
setCompetitors([]);
setTargetKeywords([]);
setAuditResult(null);
setGscResult(null);
setInsights([]);
setError(null);
setProgress(0);
setActiveStep(0);
setTabValue(0);
};
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
{/* Header */}
<Box sx={{ mb: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
<AssessmentIcon sx={{ fontSize: 32 }} color="primary" />
<Typography variant="h4" sx={{ fontWeight: 600 }}>
Enterprise SEO Analysis
</Typography>
</Box>
<Typography variant="body2" color="textSecondary">
Comprehensive audit with AI-powered insights to improve organic traffic and rankings
</Typography>
</Box>
{/* Progress Indicator */}
{loading && (
<Card sx={{ mb: 3, bgcolor: 'info.lighter' }}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 1 }}>
<CircularProgress size={24} />
<Typography variant="body2" sx={{ fontWeight: 600 }}>
{activeStep === 1 && 'Running enterprise audit...'}
{activeStep === 2 && 'Analyzing search performance...'}
{activeStep === 3 && 'Generating insights...'}
</Typography>
</Box>
<LinearProgress variant="determinate" value={progress} />
</CardContent>
</Card>
)}
{/* Error Display */}
<AnimatePresence>
{error && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
<Alert
severity="error"
onClose={() => setError(null)}
sx={{ mb: 3 }}
action={
<Button color="inherit" size="small" onClick={() => setError(null)}>
DISMISS
</Button>
}
>
{error}
</Alert>
</motion.div>
)}
</AnimatePresence>
{/* Stepper */}
<Paper sx={{ mb: 4, p: 2 }}>
<Stepper activeStep={activeStep} alternativeLabel>
{analysisSteps.map((step, index) => (
<Step key={index}>
<StepLabel>{step.label}</StepLabel>
</Step>
))}
</Stepper>
</Paper>
{/* Main Content */}
<Grid container spacing={3}>
{/* Left Panel: Input & Controls */}
<Grid item xs={12} md={3}>
<Card>
<CardContent>
<Typography variant="h6" sx={{ mb: 2, fontWeight: 600 }}>
Analysis Configuration
</Typography>
{/* URL Input */}
<TextField
fullWidth
label="Website URL"
placeholder="https://example.com"
value={websiteUrl}
onChange={(e) => setWebsiteUrl(e.target.value)}
size="small"
sx={{ mb: 2 }}
disabled={loading}
helperText="Include http:// or https://"
/>
{/* Competitors Input */}
<TextField
fullWidth
label="Competitor URLs (comma-separated)"
placeholder="https://competitor1.com, https://competitor2.com"
multiline
rows={2}
value={competitors.join(', ')}
onChange={(e) => setCompetitors(e.target.value.split(',').map(c => c.trim()))}
size="small"
sx={{ mb: 2 }}
disabled={loading}
/>
{/* Keywords Input */}
<TextField
fullWidth
label="Target Keywords (comma-separated)"
placeholder="keyword1, keyword2, keyword3"
multiline
rows={2}
value={targetKeywords.join(', ')}
onChange={(e) => setTargetKeywords(e.target.value.split(',').map(k => k.trim()))}
size="small"
sx={{ mb: 3 }}
disabled={loading}
/>
{/* Control Buttons */}
<Stack spacing={1}>
<Button
fullWidth
variant="contained"
startIcon={<PlayArrowIcon />}
onClick={handleStartAudit}
disabled={!isUrlValid || loading}
>
{loading ? 'Running...' : 'Start Analysis'}
</Button>
<Button
fullWidth
variant="outlined"
startIcon={<SettingsIcon />}
onClick={() => setOpenOptionsDialog(true)}
disabled={loading}
>
Analysis Options
</Button>
{(auditResult || gscResult) && (
<>
<Button
fullWidth
variant="outlined"
startIcon={<AutoAwesomeIcon />}
onClick={handleGenerateInsights}
disabled={loading}
>
Generate Insights
</Button>
<Button
fullWidth
variant="outlined"
startIcon={<DownloadIcon />}
onClick={handleDownloadReport}
disabled={loading}
>
Download Report
</Button>
<Button
fullWidth
variant="outlined"
color="secondary"
startIcon={<RefreshIcon />}
onClick={handleReset}
disabled={loading}
>
New Analysis
</Button>
</>
)}
</Stack>
{/* Quick Stats */}
{(auditResult || gscResult) && (
<Box sx={{ mt: 3, pt: 2, borderTop: '1px solid', borderColor: 'divider' }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 600 }}>
Quick Stats
</Typography>
<Stack spacing={1}>
{auditResult && (
<Chip
icon={<AssessmentIcon />}
label={`Audit Score: ${auditResult.executive_summary.overall_score}`}
variant="outlined"
size="small"
/>
)}
{gscResult && (
<Chip
icon={<TrendingUpIcon />}
label={`Clicks: ${gscResult.performance_overview.clicks.toLocaleString()}`}
variant="outlined"
size="small"
/>
)}
{insights.length > 0 && (
<Chip
icon={<AutoAwesomeIcon />}
label={`${insights.length} Insights Generated`}
variant="outlined"
size="small"
color="success"
/>
)}
</Stack>
</Box>
)}
</CardContent>
</Card>
</Grid>
{/* Right Panel: Results */}
<Grid item xs={12} md={9}>
{!auditResult && !gscResult ? (
<Card sx={{ textAlign: 'center', py: 8 }}>
<CardContent>
<AssessmentIcon sx={{ fontSize: 64, color: 'action.disabled', mb: 2 }} />
<Typography variant="h6" color="textSecondary">
No analysis yet
</Typography>
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>
Enter a website URL and click "Start Analysis" to begin
</Typography>
</CardContent>
</Card>
) : (
<Box>
{/* Tabs */}
<Paper sx={{ mb: 2 }}>
<Tabs value={tabValue} onChange={handleTabChange}>
{auditResult && <Tab label="Enterprise Audit" />}
{gscResult && <Tab label="GSC Analysis" />}
{insights.length > 0 && <Tab label="AI Insights" />}
<Tab label="AI Overview Insights" />
</Tabs>
</Paper>
{/* Tab Content */}
<TabPanel value={tabValue} index={0}>
{auditResult && (
<EnterpriseAuditResults
auditResult={auditResult}
insights={insights}
onGenerateInsights={handleGenerateInsights}
onDownloadReport={handleDownloadReport}
/>
)}
</TabPanel>
{auditResult && gscResult && (
<TabPanel value={tabValue} index={1}>
{gscResult && (
<GSCAnalysisResults
analysisResult={gscResult}
insights={insights}
onGenerateInsights={handleGenerateInsights}
onDownloadReport={handleDownloadReport}
/>
)}
</TabPanel>
)}
{!auditResult && gscResult && (
<TabPanel value={tabValue} index={0}>
{gscResult && (
<GSCAnalysisResults
analysisResult={gscResult}
insights={insights}
onGenerateInsights={handleGenerateInsights}
onDownloadReport={handleDownloadReport}
/>
)}
</TabPanel>
)}
{/* AI Overview Insights — always last tab */}
{(() => {
const aioIndex = (auditResult ? 1 : 0) + (gscResult ? 1 : 0) + (insights.length > 0 ? 1 : 0);
return (
<TabPanel value={tabValue} index={aioIndex}>
<AIVisibilitySection
gscConnected={!!gscResult}
siteUrl={websiteUrl}
onConnectGSC={() => setActiveStep(0)}
/>
</TabPanel>
);
})()}
</Box>
)}
</Grid>
</Grid>
</motion.div>
{/* Options Dialog */}
<Dialog open={openOptionsDialog} onClose={() => setOpenOptionsDialog(false)} maxWidth="sm" fullWidth>
<DialogTitle>Analysis Options</DialogTitle>
<DialogContent sx={{ py: 2 }}>
<Stack spacing={2} sx={{ mt: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="body2">Include Content Analysis</Typography>
<input
type="checkbox"
checked={options.includeContentAnalysis}
onChange={(e) => setOptions({ ...options, includeContentAnalysis: e.target.checked })}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="body2">Include Competitive Analysis</Typography>
<input
type="checkbox"
checked={options.includeCompetitiveAnalysis}
onChange={(e) => setOptions({ ...options, includeCompetitiveAnalysis: e.target.checked })}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="body2">Generate Executive Report</Typography>
<input
type="checkbox"
checked={options.generateExecutiveReport}
onChange={(e) => setOptions({ ...options, generateExecutiveReport: e.target.checked })}
/>
</Box>
<TextField
label="GSC Analysis Period (days)"
type="number"
value={options.dateRangeDays}
onChange={(e) => setOptions({ ...options, dateRangeDays: parseInt(e.target.value) })}
inputProps={{ min: 7, max: 365 }}
/>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenOptionsDialog(false)}>Close</Button>
</DialogActions>
</Dialog>
</Container>
);
};
export default SEOAnalysisController;