/** * SEO Analysis Modal Component * * Displays comprehensive SEO analysis results with visual charts and actionable recommendations. * Integrates with CopilotKit for real-time progress updates and user interactions. */ import React, { useState, useEffect, useCallback } from 'react'; import { Dialog, DialogContent, Button, Chip, LinearProgress, Card, CardContent, CardHeader, Typography, Box, Tabs, Tab, Alert, Grid, Paper, IconButton, Tooltip, Avatar, CircularProgress } from '@mui/material'; import { hashContent, getSeoCacheKey } from '../../utils/contentHash'; import { apiClient, triggerSubscriptionError } from '../../api/client'; import { CheckCircle, Cancel, Warning, TrendingUp, Search, Refresh, Close } from '@mui/icons-material'; import { KeywordAnalysis, ReadabilityAnalysis, StructureAnalysis, Recommendations } from './SEO'; import OverallScoreCard from './SEO/OverallScoreCard'; interface SEOAnalysisResult { overall_score: number; category_scores: { structure: number; keywords: number; readability: number; quality: number; headings: number; ai_insights: number; }; analysis_summary: { overall_grade: string; status: string; strongest_category: string; weakest_category: string; key_strengths: string[]; key_weaknesses: string[]; ai_summary: string; }; actionable_recommendations: Array<{ category: string; priority: 'High' | 'Medium' | 'Low'; recommendation: string; impact: string; }>; visualization_data: { score_radar: { categories: string[]; scores: number[]; max_score: number; }; keyword_analysis: { densities: Record; missing_keywords: string[]; over_optimization: string[]; }; readability_metrics: Record; content_stats: { word_count: number; sections: number; paragraphs: number; }; }; detailed_analysis?: { content_structure?: { total_sections: number; total_paragraphs: number; total_sentences: number; has_introduction: boolean; has_conclusion: boolean; has_call_to_action: boolean; structure_score: number; recommendations: string[]; }; keyword_analysis?: { primary_keywords: string[]; long_tail_keywords: string[]; semantic_keywords: string[]; keyword_density: Record; keyword_distribution: Record; missing_keywords: string[]; over_optimization: string[]; recommendations: string[]; }; readability_analysis?: { metrics: Record; avg_sentence_length: number; avg_paragraph_length: number; readability_score: number; target_audience: string; recommendations: string[]; }; content_quality?: { word_count: number; unique_words: number; vocabulary_diversity: number; transition_words_used: number; content_depth_score: number; flow_score: number; recommendations: string[]; }; heading_structure?: { h1_count: number; h2_count: number; h3_count: number; h1_headings: string[]; h2_headings: string[]; h3_headings: string[]; heading_hierarchy_score: number; recommendations: string[]; }; }; generated_at: string; } interface SEOAnalysisModalProps { isOpen: boolean; onClose: () => void; blogContent: string; blogTitle?: string; researchData: any; onApplyRecommendations?: (recommendations: SEOAnalysisResult['actionable_recommendations']) => Promise; onAnalysisComplete?: (analysis: SEOAnalysisResult) => void; } export const SEOAnalysisModal: React.FC = ({ isOpen, onClose, blogContent, blogTitle, researchData, onApplyRecommendations, onAnalysisComplete }) => { const [isAnalyzing, setIsAnalyzing] = useState(false); const [analysisResult, setAnalysisResult] = useState(null); const [progress, setProgress] = useState(0); const [progressMessage, setProgressMessage] = useState(''); const [error, setError] = useState(null); const [tabValue, setTabValue] = useState('recommendations'); const [contentHash, setContentHash] = useState(''); const [isApplying, setIsApplying] = useState(false); const [applyError, setApplyError] = useState(null); const [fromCache, setFromCache] = useState(false); // Debug logging only in development and when modal state changes meaningfully useEffect(() => { if (process.env.NODE_ENV === 'development' && isOpen) { console.log('SEOAnalysisModal render:', { isOpen, blogContent: blogContent?.length, researchData: !!researchData }); } }, [isOpen, blogContent?.length, researchData]); const runSEOAnalysis = useCallback(async (forceRefresh = false) => { // Prevent multiple simultaneous calls if (isAnalyzing && !forceRefresh) { console.log('⏸️ SEO analysis already in progress, skipping duplicate call'); return; } try { setIsAnalyzing(true); setError(null); setProgress(0); setProgressMessage('Checking cache for previous SEO analysis...'); // Cache check - always check cache first unless force refresh is requested // Compute hash if not already available let hash = contentHash; if (!hash) { hash = await hashContent(`${blogTitle || ''}\n${blogContent}`); // Update state for future use setContentHash(hash); } const cacheKey = getSeoCacheKey(hash, blogTitle); console.log('πŸ” Checking SEO cache', { cacheKey, hasHash: !!hash, forceRefresh, hashLength: hash?.length, titleLength: blogTitle?.length, contentLength: blogContent?.length }); if (!forceRefresh) { const cached = typeof window !== 'undefined' ? window.localStorage.getItem(cacheKey) : null; if (cached) { try { const parsed = JSON.parse(cached) as SEOAnalysisResult; // Validate cached data has required fields if (parsed && typeof parsed.overall_score === 'number' && parsed.category_scores) { console.log('βœ… Using cached SEO analysis', { cacheKey, overall_score: parsed.overall_score }); setFromCache(true); setAnalysisResult(parsed); setIsAnalyzing(false); setProgress(100); setProgressMessage('SEO analysis loaded from cache'); // Notify parent that analysis is complete (from cache) if (onAnalysisComplete) { onAnalysisComplete(parsed); } return; } else { console.warn('⚠️ Cached SEO analysis data is invalid, will fetch fresh analysis'); } } catch (parseError) { console.warn('⚠️ Failed to parse cached SEO analysis, will fetch fresh analysis', parseError); // Remove invalid cache entry if (typeof window !== 'undefined') { window.localStorage.removeItem(cacheKey); } } } else { console.log('ℹ️ No cached SEO analysis found, will fetch from API', { cacheKey }); } } else { console.log('πŸ”„ Force refresh requested, skipping cache check'); } // Backend call β€” run concurrently with progress simulation // Use longer timeout (120s) since SEO analysis can take 40-60s const responsePromise = apiClient.post('/api/blog-writer/seo/analyze', { blog_content: blogContent, blog_title: blogTitle, research_data: researchData }, { timeout: 120000 }); // Simulated progress runs alongside the API call to keep the user informed. // Each stage.at is cumulative ms from start. Cancelled when the API returns. let progressCancelled = false; const progressStages = [ { at: 2000, progress: 10, message: 'Extracting keywords from research data...' }, { at: 8000, progress: 25, message: 'Analyzing content structure and readability...' }, { at: 20000, progress: 40, message: 'Evaluating heading hierarchy and flow...' }, { at: 35000, progress: 55, message: 'Checking keyword density and optimization...' }, { at: 50000, progress: 70, message: 'Generating AI-powered SEO insights...' }, { at: 65000, progress: 85, message: 'Compiling analysis results and recommendations...' }, ]; (async () => { const startTime = Date.now(); for (const stage of progressStages) { if (progressCancelled) return; const elapsed = Date.now() - startTime; const wait = Math.max(0, stage.at - elapsed); if (wait > 0) await new Promise(resolve => setTimeout(resolve, wait)); if (progressCancelled) return; setProgress(stage.progress); setProgressMessage(stage.message); } })(); const response = await responsePromise; progressCancelled = true; const result = response.data; console.log('πŸ” Backend SEO Analysis Response:', result); if (!result.success) throw new Error(result.recommendations?.[0] || 'SEO analysis failed'); if (!result.overall_score && result.overall_score !== 0) throw new Error('Invalid SEO score received from API'); const convertedResult: SEOAnalysisResult = { overall_score: result.overall_score, category_scores: { structure: result.category_scores?.structure || 0, keywords: result.category_scores?.keywords || 0, readability: result.category_scores?.readability || 0, quality: result.category_scores?.quality || 0, headings: result.category_scores?.headings || 0, ai_insights: result.category_scores?.ai_insights || 0 }, analysis_summary: result.analysis_summary || { overall_grade: result.overall_score >= 80 ? 'A' : result.overall_score >= 60 ? 'B' : 'C', status: result.overall_score >= 80 ? 'Excellent' : result.overall_score >= 60 ? 'Good' : 'Needs Improvement', strongest_category: 'structure', weakest_category: 'keywords', key_strengths: ['Good content structure', 'Appropriate length'], key_weaknesses: ['Keyword optimization needs work'], ai_summary: 'Content provides good value with room for SEO improvements.' }, actionable_recommendations: (result.actionable_recommendations || []).map((rec: any) => ({ category: rec.category || 'General', priority: rec.priority || 'Medium' as const, recommendation: rec.recommendation || rec, impact: rec.impact || 'Improves SEO performance' })), visualization_data: { score_radar: { categories: ['structure', 'keywords', 'readability', 'quality', 'headings', 'ai_insights'], scores: [ result.category_scores?.structure || 0, result.category_scores?.keywords || 0, result.category_scores?.readability || 0, result.category_scores?.quality || 0, result.category_scores?.headings || 0, result.category_scores?.ai_insights || 0 ], max_score: 100 }, keyword_analysis: { densities: result.visualization_data?.keyword_analysis?.densities || {}, missing_keywords: result.visualization_data?.keyword_analysis?.missing_keywords || [], over_optimization: result.visualization_data?.keyword_analysis?.over_optimization || [] }, readability_metrics: result.visualization_data?.readability_metrics || {}, content_stats: { word_count: result.visualization_data?.content_stats?.word_count || 0, sections: result.visualization_data?.content_stats?.sections || 0, paragraphs: result.visualization_data?.content_stats?.paragraphs || 0 } }, detailed_analysis: result.detailed_analysis || undefined, generated_at: new Date().toISOString() }; setFromCache(false); setAnalysisResult(convertedResult); // Save to cache - use the same cacheKey that was used for checking try { // Use the same hash and cacheKey from the cache check section // This ensures consistency between cache check and save if (typeof window !== 'undefined' && cacheKey) { window.localStorage.setItem(cacheKey, JSON.stringify(convertedResult)); console.log('πŸ’Ύ SEO analysis cached', { cacheKey, overall_score: convertedResult.overall_score }); } } catch (cacheError) { console.warn('⚠️ Failed to cache SEO analysis', cacheError); } setIsAnalyzing(false); // Notify parent that analysis is complete (fresh analysis) if (onAnalysisComplete) { onAnalysisComplete(convertedResult); } } catch (err: any) { console.error('SEO analysis failed:', err); // Check if this is a subscription error (429/402) and trigger global subscription modal const status = err?.response?.status; if (status === 429 || status === 402) { console.log('SEOAnalysisModal: Detected subscription error, triggering global handler', { status, data: err?.response?.data }); const handled = await triggerSubscriptionError(err); if (handled) { console.log('SEOAnalysisModal: Global subscription error handler triggered successfully'); // Don't set local error - let the global modal handle it setIsAnalyzing(false); return; } else { console.warn('SEOAnalysisModal: Global subscription error handler did not handle the error'); } } // For non-subscription errors, show local error message setError(err instanceof Error ? err.message : 'Analysis failed'); setIsAnalyzing(false); } }, [blogContent, blogTitle, researchData, contentHash, onAnalysisComplete]); // Precompute hash when modal opens and trigger cache check // Use a ref to prevent multiple simultaneous calls const hasRunAnalysisRef = React.useRef(false); useEffect(() => { if (isOpen && !hasRunAnalysisRef.current) { hasRunAnalysisRef.current = true; (async () => { const h = await hashContent(`${blogTitle || ''}\n${blogContent}`); setContentHash(h); // After hash is computed, check cache if we don't have analysis result yet if (!analysisResult) { // Small delay to ensure hash is set in state setTimeout(() => { runSEOAnalysis(); }, 100); } })(); } else if (!isOpen) { // Reset hash and flag when modal closes setContentHash(''); hasRunAnalysisRef.current = false; } }, [isOpen, blogContent, blogTitle, analysisResult, runSEOAnalysis]); // Fallback: if modal opens and hash is already computed, check cache immediately useEffect(() => { if (isOpen && !analysisResult && contentHash && !hasRunAnalysisRef.current) { hasRunAnalysisRef.current = true; runSEOAnalysis(); } }, [isOpen, analysisResult, contentHash, runSEOAnalysis]); const getScoreColor = (score: number) => { if (score >= 80) return 'success.main'; if (score >= 60) return 'warning.main'; return 'error.main'; }; // Tooltip content for each metric const getMetricTooltip = (category: string) => { const tooltips = { structure: { title: "Content Structure Analysis", description: "Evaluates how well your content is organized and structured for both readers and search engines.", methodology: "Analyzes heading hierarchy (H1, H2, H3), paragraph length, section organization, and logical flow.", score_meaning: "Higher scores indicate better content organization, clear headings, and logical structure.", examples: "Good: Clear H1 title, logical H2 sections, short paragraphs. Poor: No headings, long walls of text." }, keywords: { title: "Keyword Optimization Analysis", description: "Measures how effectively your target keywords are used throughout the content.", methodology: "Analyzes keyword density, distribution, placement in headings, and semantic keyword usage.", score_meaning: "Higher scores indicate optimal keyword usage without over-optimization.", examples: "Good: 1-3% keyword density, keywords in headings. Poor: Keyword stuffing or missing target keywords." }, readability: { title: "Readability Assessment", description: "Evaluates how easy your content is to read and understand for your target audience.", methodology: "Uses Flesch Reading Ease, sentence length, word complexity, and paragraph structure.", score_meaning: "Higher scores indicate content that's easier to read and understand.", examples: "Good: Short sentences, simple words, clear paragraphs. Poor: Long complex sentences, jargon." }, quality: { title: "Content Quality Evaluation", description: "Assesses the depth, value, and comprehensiveness of your content.", methodology: "Analyzes word count, content depth, information density, and topic coverage.", score_meaning: "Higher scores indicate more comprehensive and valuable content.", examples: "Good: Detailed explanations, examples, comprehensive coverage. Poor: Thin content, lack of detail." }, headings: { title: "Heading Structure Analysis", description: "Evaluates the effectiveness of your heading hierarchy and organization.", methodology: "Analyzes heading distribution, hierarchy levels, keyword usage in headings, and logical flow.", score_meaning: "Higher scores indicate better heading structure and organization.", examples: "Good: Clear H1, logical H2/H3 progression. Poor: Missing headings, poor hierarchy." }, ai_insights: { title: "AI-Powered Content Insights", description: "Advanced analysis of content engagement potential and user value.", methodology: "Uses AI to analyze content quality, engagement factors, and user value proposition.", score_meaning: "Higher scores indicate content that's more likely to engage and provide value to readers.", examples: "Good: Clear value proposition, engaging content, actionable insights. Poor: Generic content, low engagement potential." } }; return tooltips[category as keyof typeof tooltips] || tooltips.structure; }; return ( SEO Analysis Results {fromCache && analysisResult?.generated_at && ( )} Comprehensive analysis of your blog content's SEO optimization {isAnalyzing && ( {progressMessage} )} {error && ( {error} )} {analysisResult && ( {/* Overall Score Section */} {/* Detailed Analysis Tabs */} setTabValue(newValue)} variant="fullWidth" sx={{ '& .MuiTab-root': { color: 'text.secondary', fontWeight: 500, '&.Mui-selected': { color: 'primary.main', fontWeight: 600 } }, '& .MuiTabs-indicator': { background: 'linear-gradient(90deg, #4caf50, #8bc34a)', height: 3 } }} > {tabValue === 'recommendations' && ( )} {tabValue === 'keywords' && ( )} {tabValue === 'readability' && ( )} {tabValue === 'structure' && ( analysisResult ? ( ) : ( Loading structure analysis... ) )} {tabValue === 'insights' && ( AI-Powered Insights Content Summary {analysisResult.analysis_summary.ai_summary} Key Strengths {analysisResult.analysis_summary.key_strengths.map((strength, index) => ( {strength} ))} Areas for Improvement {analysisResult.analysis_summary.key_weaknesses.map((weakness, index) => ( {weakness} ))} )} {/* Action Buttons */} {applyError && ( {applyError} )} )} ); };