import { useState, Suspense } from 'react'; import { useScrollLock } from '../hooks/use-scroll-lock'; import { TopPagesProducts } from './top-pages-products'; import { MentionsOverview } from './mentions-overview'; import { LoadingSpinner } from './screen-loader'; import { FirstWeekEmptyState } from './first-week-empty-state'; import { LazyBotTrafficChart } from '@/lib/load-bot-traffic-chart'; import { ChevronRight, ChevronDown, Radar, Circle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { VisibilityTimeline } from './visibility-timeline'; import { trackVisibilityScoreRefreshed, setUserProperty } from '../lib/mixpanel'; import type { AnalyticsData } from '../lib/mixpanel'; import { __, sprintf, _n } from '../lib/i18n'; import { OverviewEngagementCards } from './overview-engagement-cards'; import { VisibilityScoreRadar } from './visibility-score-radar'; import { VisibilityScoreBreakdownModal } from './visibility-score-breakdown-modal'; type RecommendationPriority = 'critical' | 'warning' | 'tip'; interface RecommendationItem { id: string; title: string; description: string; priority: RecommendationPriority; actionLabel?: string; actionType?: 'button' | 'link'; actionHandler?: () => void; actionUrl?: string; } interface VisibilityScoreBreakdown { score: number; weight: number; weighted_contribution: number; details: string; } interface VisibilityScoreData { score: number; frequency_score: number; visit_type_score: number; visit_count_score: number; page_status_score: number; url_type_score: number; total_visits: number; unique_pages: number; range_days: number; breakdown: { frequency: VisibilityScoreBreakdown; visit_type: VisibilityScoreBreakdown; visit_count: VisibilityScoreBreakdown; page_status: VisibilityScoreBreakdown; url_type: VisibilityScoreBreakdown; }; } interface DashboardData { rootPath: string; lastGenerated: string | null; llmsTxtExists: boolean; llmsTxtUrl: string; llmsTxtSize?: string; postsCount: number; markdownCount: number; itemsInLlms: number; totalAllPostTypes: number; botVisits30Days: number; botClicks30Days: number; visibilityScore?: VisibilityScoreData; imageCount: number; imagesWithoutAlt: Array<{ id: number; url: string; type: string; post_id: number; post_title: string; preview_url: string; }>; postTypeBreakdown?: Array<{ name: string; label: string; total: number; selected: number; in_llms?: number; }>; settings: { post_types: string[]; full_content: boolean; days_to_include: number; delete_on_uninstall: boolean; include_images?: boolean; llm_response_images?: Array<{ id: number; position: 'before' | 'after'; }>; // Keep old fields for backward compatibility llm_response_image_id?: number; llm_response_image_position?: 'before' | 'after'; }; postTypes: Array<{ name: string; label: string; }>; imageData?: { id: number; url: string; preview_url: string; width?: number; height?: number; } | null; woocommerce?: { active: boolean; version: string | null; product_count: number; currency: string; currency_symbol: string; }; llmsFullInfo?: { exists: boolean; size: number; size_formatted: string; post_count: number; warning: number | null; }; pluginVersion: string; wordpressVersion: string; robotsTxtStatus?: { has_physical_file: boolean; has_llms_reference: boolean; injection_method: 'filter' | 'file' | 'none'; robots_txt_url: string; }; onboarding?: { steps: { first_llm_visit: boolean; llms_txt: boolean; email_reports: boolean; robots_txt: boolean; }; completed: boolean; dismissed: boolean; step_links: { first_llm_visit: string; llms_txt: string; email_reports: string; robots_txt: string; }; step_prompts: { first_llm_visit_primary: string; first_llm_visit_secondary: string; }; site_url: string; }; overviewStarRating?: number; analyticsData?: AnalyticsData; } interface OverviewProps { data: DashboardData; onGenerateNow: () => Promise<{ success: boolean; message: string; timestamp?: string }>; onboardingDismissed?: boolean; onboardingSteps?: { first_llm_visit: boolean; llms_txt: boolean; email_reports: boolean; robots_txt: boolean; }; onReopenOnboarding?: () => void; } export function Overview({ data, onGenerateNow, onboardingDismissed, onReopenOnboarding }: OverviewProps) { // Generate functionality - kept for potential future use in the redesigned page const [generateMessage, setGenerateMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const [isGeneratingNow, setIsGeneratingNow] = useState(false); const [currentLlmsTxtExists, setCurrentLlmsTxtExists] = useState(data.llmsTxtExists); // Get visibility score from backend data or fallback to simple calculation const getVisibilityScore = (): number => { // Return test score if test mode is enabled if (localStorage.getItem('llmagnet_test') === 'true') { return 80; } // Use the refreshed score if available if (currentVisibilityScore && typeof currentVisibilityScore.score === 'number') { return currentVisibilityScore.score; } // Fallback to old simple calculation if new data not available if (data.totalAllPostTypes === 0) return 0; return Math.round((data.itemsInLlms / Math.max(data.totalAllPostTypes, 1)) * 100); }; // State for showing score breakdown tooltip const [showScoreBreakdown, setShowScoreBreakdown] = useState(false); const [scoreChartView, setScoreChartView] = useState<'circle' | 'radar'>('circle'); useScrollLock(showScoreBreakdown); // State for visibility score refresh const [isRefreshingScore, setIsRefreshingScore] = useState(false); const [currentVisibilityScore, setCurrentVisibilityScore] = useState(data.visibilityScore || null); // Function to refresh visibility score const refreshVisibilityScore = async () => { trackVisibilityScoreRefreshed(); setIsRefreshingScore(true); try { const apiRoot = (window as any).wpApiSettings?.root || ''; const nonce = (window as any).wpApiSettings?.nonce || ''; const response = await fetch(`${apiRoot}llm-analytics/v1/visibility-score/calculate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce, }, body: JSON.stringify({ range_days: 30 }), }); if (response.ok) { const result = await response.json(); if (result.score_data) { setCurrentVisibilityScore(result.score_data); setUserProperty({ visibility_score: result.score_data.total_score ?? result.score_data.score }); } } } catch (error) { console.error('Failed to refresh visibility score:', error); } finally { setIsRefreshingScore(false); } }; const refreshLlmsTxtExists = async (): Promise => { try { const response = await fetch(`/llms.txt?t=${Date.now()}`, { cache: 'no-store' }); if (!response.ok) { setCurrentLlmsTxtExists(false); return false; } const content = await response.text(); const exists = content.trim().length > 0; setCurrentLlmsTxtExists(exists); return exists; } catch (error) { console.error('Failed to verify llms.txt after generation:', error); setCurrentLlmsTxtExists(false); return false; } }; const handleGenerateNow = async () => { try { const result = await onGenerateNow(); setGenerateMessage({ type: result.success ? 'success' : 'error', text: result.message }); // Clear message after 5 seconds setTimeout(() => setGenerateMessage(null), 5000); } catch (error) { setGenerateMessage({ type: 'error', text: __('An error occurred while generating files.', 'llmagnet-llm-txt-generator') }); } }; const handleGenerateRecommendation = async () => { setIsGeneratingNow(true); try { await handleGenerateNow(); await new Promise((resolve) => setTimeout(resolve, 600)); await refreshLlmsTxtExists(); } finally { setIsGeneratingNow(false); } }; // State for global success message const [globalSuccessMessage, setGlobalSuccessMessage] = useState(''); // Robots.txt status const [robotsStatus, setRobotsStatus] = useState(data.robotsTxtStatus); const [isFixingRobots, setIsFixingRobots] = useState(false); const handleFixRobotsTxt = async () => { setIsFixingRobots(true); try { const apiRoot = (window as any).wpApiSettings?.root || ''; const nonce = (window as any).wpApiSettings?.nonce || ''; const response = await fetch(`${apiRoot}llm-analytics/v1/robots-inject`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce }, }); if (response.ok) { const result = await response.json(); if (result.status) { setRobotsStatus(result.status); } if (result.success) { setGlobalSuccessMessage(__('robots.txt now includes the llms.txt reference.', 'llmagnet-llm-txt-generator')); } else if (result.status?.has_physical_file) { setGlobalSuccessMessage(__('A physical robots.txt is on this site. Copy the llms.txt block into that file.', 'llmagnet-llm-txt-generator')); } setTimeout(() => setGlobalSuccessMessage(''), 5000); } } catch (err) { console.error('Failed to fix robots.txt:', err); } finally { setIsFixingRobots(false); } }; const visibilityScore = getVisibilityScore(); const scoreBreakdown = currentVisibilityScore?.breakdown || data.visibilityScore?.breakdown; const contentSettingsUrl = '/wp-admin/admin.php?page=llmagnet-ai-seo-content-settings'; const agentReadyUrl = '/wp-admin/admin.php?page=llmagnet-ai-seo-agent-ready'; const recommendations: RecommendationItem[] = [ ...(!currentLlmsTxtExists ? [ { id: 'R01', title: __('Generate llms.txt', 'llmagnet-llm-txt-generator'), description: __('Your llms.txt file is not available yet, so AI crawlers may miss your content.', 'llmagnet-llm-txt-generator'), priority: 'critical' as const, actionLabel: __('Generate Now', 'llmagnet-llm-txt-generator'), actionType: 'button' as const, actionHandler: handleGenerateRecommendation, }, ] : []), ...(robotsStatus && !robotsStatus.has_llms_reference ? [ { id: 'R04', title: __('Add llms.txt to robots.txt', 'llmagnet-llm-txt-generator'), description: __('Some crawlers use robots.txt to discover llms.txt. Add the reference to improve discovery.', 'llmagnet-llm-txt-generator'), priority: 'warning' as const, actionLabel: __('Fix Now', 'llmagnet-llm-txt-generator'), actionType: 'button' as const, actionHandler: handleFixRobotsTxt, }, ] : []), ...(data.botVisits30Days <= 0 ? [ { id: 'R03', title: __('No AI bot traffic in 30 days', 'llmagnet-llm-txt-generator'), description: __('We have not detected AI bot visits yet. Publish more crawlable content and regenerate llms.txt.', 'llmagnet-llm-txt-generator'), priority: 'warning' as const, actionLabel: __('Open Content Settings', 'llmagnet-llm-txt-generator'), actionType: 'link' as const, actionUrl: contentSettingsUrl, }, ] : []), ...(!data.settings.full_content ? [ { id: 'R05', title: __('Full content export is off', 'llmagnet-llm-txt-generator'), description: __('Turn on Full Content to provide richer context to AI crawlers in your exported content.', 'llmagnet-llm-txt-generator'), priority: 'warning' as const, actionLabel: __('Enable in Settings', 'llmagnet-llm-txt-generator'), actionType: 'link' as const, actionUrl: contentSettingsUrl, }, ] : []), ] .sort((a, b) => { const priorityOrder: Record = { critical: 0, warning: 1, tip: 2, }; return priorityOrder[a.priority] - priorityOrder[b.priority]; }) .slice(0, 3); const getVisibilityStatusInfo = (score: number) => { if (score >= 90) { return { label: __('Excellent', 'llmagnet-llm-txt-generator'), message: __("Great job! Your site is well optimized for AI Search Engines.", 'llmagnet-llm-txt-generator'), indicatorColor: 'bg-green-500', }; } if (score >= 75) { return { label: __('Good', 'llmagnet-llm-txt-generator'), message: __("You're doing well, but there's room for improvement.", 'llmagnet-llm-txt-generator'), indicatorColor: 'bg-lime-500', }; } if (score >= 50) { return { label: __('Needs Improvement', 'llmagnet-llm-txt-generator'), message: __("You're missing opportunities to reach users through AI Search Engines.", 'llmagnet-llm-txt-generator'), indicatorColor: 'bg-[#D93025]', }; } return { label: __('Needs Attention', 'llmagnet-llm-txt-generator'), message: __("You're missing opportunities to reach users through AI Search Engines.", 'llmagnet-llm-txt-generator'), indicatorColor: 'bg-red-500', }; }; const visibilityStatus = getVisibilityStatusInfo(visibilityScore); /** Priority is conveyed by a small square indicator (same motif as the visibility status card). */ const getRecommendationStyles = (priority: RecommendationPriority) => { if (priority === 'critical') { return { indicator: 'bg-rose-500' }; } if (priority === 'warning') { return { indicator: 'bg-amber-500' }; } return { indicator: 'bg-[#7c3aed]' }; }; const recommendationPriorityLabel: Record = { critical: __('High priority', 'llmagnet-llm-txt-generator'), warning: __('Recommended', 'llmagnet-llm-txt-generator'), tip: __('Suggestion', 'llmagnet-llm-txt-generator'), }; /** Single open accordion panel; all collapsed on load for a compact column. */ const [openRecommendationId, setOpenRecommendationId] = useState(null); const toggleRecommendation = (id: string) => { setOpenRecommendationId((prev) => (prev === id ? null : id)); }; return (
{/* Global success message */} {globalSuccessMessage && (
{globalSuccessMessage}
)} {/* Setup Guide Banner - shown when onboarding was dismissed but not completed */} {onboardingDismissed && data.onboarding && (() => { const stepLabels = [ 'llms.txt', 'robots.txt', __('LLM visit', 'llmagnet-llm-txt-generator'), ]; const stepKeys = ['llms_txt', 'robots_txt', 'first_llm_visit'] as const; const doneCount = stepKeys.filter((key) => data.onboarding!.steps[key]).length; return (
{/* Icon */}
{/* Text + steps */}
{doneCount === 0 ? __("You haven't started setup yet", 'llmagnet-llm-txt-generator') : sprintf(__('Setup %1$d/3 complete', 'llmagnet-llm-txt-generator'), doneCount)} {/* Step dots */}
{stepKeys.map((key) => (
))}

{__('Finish setup to get discovered by AI assistants', 'llmagnet-llm-txt-generator')}

{/* CTA */}
); })()} {/* Generate Message Section */} {generateMessage && (
{generateMessage.text}
)} {/* Mentions Overview */} {/* Row 1: Visibility Score + Visibility Score Over Time */}
{/* Visibility Score - 1/3 */}
{scoreBreakdown && ( )}

{__('Visibility Score', 'llmagnet-llm-txt-generator')}

{scoreChartView === 'radar' && scoreBreakdown ? (
) : ( <>
= 90 ? '#22c55e' : visibilityScore >= 75 ? '#84cc16' : visibilityScore >= 50 ? '#eab308' : '#ef4444' } strokeWidth="10" strokeDasharray={isRefreshingScore ? '40 534' : `${(visibilityScore / 100) * 534} 534`} strokeLinecap="round" style={{ transform: 'rotate(-90deg)', transformOrigin: '50% 50%', transition: 'stroke-dasharray 0.5s ease-out', animation: isRefreshingScore ? 'spin 1s linear infinite' : 'none', }} /> {isRefreshingScore ? '...' : visibilityScore} {__('Of 100', 'llmagnet-llm-txt-generator')}

{visibilityStatus.message}

)}
{/* Visibility Score Over Time - 2/3 */}
{/* Row 2: Top Pages/Products + Recommendations placeholder */}
{/* Top Pages/Products - 2/3 */}
{/* Recommendations - 1/3 */}

{__('Recommendations actions', 'llmagnet-llm-txt-generator')}

{recommendations.length > 0 ? (
{recommendations.map((item) => { const styles = getRecommendationStyles(item.priority); const isExpanded = openRecommendationId === item.id; const panelId = `rec-panel-${item.id}`; const headerId = `rec-header-${item.id}`; return (

{item.description}

{item.actionLabel && item.actionType === 'button' && item.actionHandler && ( )} {item.actionLabel && item.actionType === 'link' && item.actionUrl && ( )}
); })}
) : (

{__('All set. No actionable recommendations right now.', 'llmagnet-llm-txt-generator')}

)}
{/* Bot Traffic */}
{/* Bot Traffic Chart - 2/3 */}
}> ( <>

{__('Bot Traffic', 'llmagnet-llm-txt-generator')}

{__('View all', 'llmagnet-llm-txt-generator')}
{botFilterIcons}

{__('Traffic in the last 30 days', 'llmagnet-llm-txt-generator')}

)} />
{/* Analytics Summary
{/* Bot Analytics
*/} {/* Alt Text Modal */} {showScoreBreakdown && scoreBreakdown && ( setShowScoreBreakdown(false)} /> )}
); }