fix: credit tracking, voice clone TTL, avatar upload ui, asset serving fallback, OAuth encryption, free plan video renders, backlink outreach sprint
This commit is contained in:
580
frontend/src/components/SEODashboard/SEOAnalysisController.tsx
Normal file
580
frontend/src/components/SEODashboard/SEOAnalysisController.tsx
Normal file
@@ -0,0 +1,580 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
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" />}
|
||||
</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>
|
||||
)}
|
||||
</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;
|
||||
Reference in New Issue
Block a user