import { useState, useEffect, Suspense } from 'react'; import { Button } from '@/components/ui/button'; import { SegmentedControl } from '@/components/ui/segmented-control'; import { ScreenLoader, LoadingSpinner } from './screen-loader'; import { LazyBotTrafficChart } from '@/lib/load-bot-traffic-chart'; import { trackTimeRangeChanged, trackViewModeChanged } from '../lib/mixpanel'; import { __, sprintf } from '../lib/i18n'; interface BotVisit { bot_name: string; date: string; visits: number; } interface BotSummary { bot_name: string; total_visits: number; logo: string; } interface BotStatRow { bot_name: string; impressions: number; clicks: number; ctr: number; trend: number; } // Time range options type TimeRange = '7d' | '30d' | '90d'; const TIME_RANGE_OPTIONS: { value: TimeRange; label: string }[] = [ { value: '7d', label: __('Last 7 days', 'llmagnet-llm-txt-generator') }, { value: '30d', label: __('Last 30 days', 'llmagnet-llm-txt-generator') }, { value: '90d', label: __('Last 90 days', 'llmagnet-llm-txt-generator') }, ]; interface AnalyticsProps { apiRoot: string; apiNamespace: string; } export function Analytics({ apiRoot, apiNamespace }: AnalyticsProps) { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [botVisits, setBotVisits] = useState([]); const [botSummaries, setBotSummaries] = useState([]); const [botStatsTable, setBotStatsTable] = useState([]); const [viewMode, setViewMode] = useState<'table' | 'cards'>('table'); // Default to table const [timeRange, setTimeRange] = useState('30d'); // Bot logo mapping const botLogos: Record = { 'ChatGPT': `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/chatgpt.svg`, 'Claude': `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/claude.svg`, 'Gemini': `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/gemini.svg`, 'Bing': `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/bing.svg`, 'Perplexity': `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/perplexity.svg`, 'Llama': `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/llama.svg`, 'Mistral': `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/mistral.svg`, 'Grok': `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/grok.svg`, 'DeepSeek': `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/deepseek.svg`, 'Other LLM': `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/other-llm.svg`, }; // Default logo for unknown bots const defaultLogo = `${window.llmagnetAnalyticsData.pluginUrl}assets/logos/other-llm.svg`; useEffect(() => { fetchData(); }, []); useEffect(() => { if (!isLoading && botVisits.length > 0) { prepareSummaryData(); fetchBotStatsTable(); } }, [isLoading, botVisits]); // Placeholder data when API returns empty (must run before any conditional return — Rules of Hooks) useEffect(() => { if (botVisits.length === 0 && !isLoading && !error) { const placeholderData = [ { bot_name: 'ChatGPT', date: '2025-09-01', visits: 0 }, { bot_name: 'Claude', date: '2025-09-01', visits: 0 }, { bot_name: 'Perplexity', date: '2025-09-01', visits: 0 }, { bot_name: 'Gemini', date: '2025-09-01', visits: 0 }, { bot_name: 'Grok', date: '2025-09-01', visits: 0 }, { bot_name: 'DeepSeek', date: '2025-09-01', visits: 0 }, ]; setBotVisits(placeholderData); const placeholderSummaries = [ { bot_name: 'ChatGPT', total_visits: 0, logo: botLogos['ChatGPT'] || defaultLogo }, { bot_name: 'Claude', total_visits: 0, logo: botLogos['Claude'] || defaultLogo }, { bot_name: 'Perplexity', total_visits: 0, logo: botLogos['Perplexity'] || defaultLogo }, { bot_name: 'Gemini', total_visits: 0, logo: botLogos['Gemini'] || defaultLogo }, { bot_name: 'Grok', total_visits: 0, logo: botLogos['Grok'] || defaultLogo }, { bot_name: 'DeepSeek', total_visits: 0, logo: botLogos['DeepSeek'] || defaultLogo }, ]; setBotSummaries(placeholderSummaries); } }, [isLoading, error, botVisits.length]); const fetchData = async () => { try { setIsLoading(true); const response = await fetch(`${apiRoot}${apiNamespace}/stats`, { headers: { 'X-WP-Nonce': window.llmagnetAnalyticsData.nonce, }, }); if (!response.ok) { throw new Error(`Error fetching data: ${response.status}`); } const data = await response.json(); setBotVisits(data); setError(null); } catch (err) { console.error('Error fetching analytics data:', err); setError(__('Failed to load analytics data. Please try again later.', 'llmagnet-llm-txt-generator')); } finally { setIsLoading(false); } }; const prepareSummaryData = () => { // Group by bot name and sum visits const botMap = new Map(); botVisits.forEach(visit => { const currentTotal = botMap.get(visit.bot_name) || 0; botMap.set(visit.bot_name, currentTotal + parseInt(visit.visits.toString())); }); // Convert to array of summaries const summaries: BotSummary[] = Array.from(botMap.entries()).map(([bot_name, total_visits]) => ({ bot_name, total_visits, logo: botLogos[bot_name] || defaultLogo, })); // Sort by total visits descending summaries.sort((a, b) => b.total_visits - a.total_visits); setBotSummaries(summaries); }; const fetchBotStatsTable = async () => { try { const response = await fetch(`${apiRoot}${apiNamespace}/bot-stats-table`, { headers: { 'X-WP-Nonce': window.llmagnetAnalyticsData.nonce, }, }); if (!response.ok) { throw new Error(`Error fetching bot stats: ${response.status}`); } const data = await response.json(); setBotStatsTable(data); } catch (err) { console.error('Error fetching bot stats table:', err); } }; if (isLoading) { return ; } if (error) { return (

{error}

); } // Always show the UI, even if there's no data if (botVisits.length === 0 && !isLoading) { // If we get here, the placeholder data wasn't set, so show a message console.log('No bot visits and not loading - showing placeholder message'); return (

{__('No Bot Visits Yet', 'llmagnet-llm-txt-generator')}

{__('No LLM bot visits have been recorded yet.', 'llmagnet-llm-txt-generator')}

{__('Bot visits will be automatically logged when LLM crawlers access your site.', 'llmagnet-llm-txt-generator')}

); } return (
{/* Header with time range selector */}

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

{__('Track LLM bot visits and engagement with your content.', 'llmagnet-llm-txt-generator')}

{/* Time Range Selector */}
({ value: o.value, label: o.label }))} value={timeRange} onChange={(v) => { setTimeRange(v); trackTimeRangeChanged(v, 'analytics'); }} size="sm" />
{/* Bot Traffic Chart */}
}> ( <>

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

{TIME_RANGE_OPTIONS.find(o => o.value === timeRange)?.label}
{botFilterIcons}
)} />

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

{/* View Mode Toggle */}
{ setViewMode(v); trackViewModeChanged(v); }} size="sm" />
{/* Table View */} {viewMode === 'table' && (
{botStatsTable.length > 0 ? ( botStatsTable.map((row) => ( )) ) : ( )}
{__('LLM', 'llmagnet-llm-txt-generator')} {__('Impressions', 'llmagnet-llm-txt-generator')} {__('Clicks', 'llmagnet-llm-txt-generator')} {__('CTR', 'llmagnet-llm-txt-generator')} {__('Trend', 'llmagnet-llm-txt-generator')}
{row.bot_name} { (e.target as HTMLImageElement).src = defaultLogo; }} /> {row.bot_name} {row.impressions.toLocaleString()} {row.clicks.toLocaleString()} {row.ctr.toFixed(2)}% 0 ? 'bg-green-100 text-green-700' : row.trend < 0 ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700' }`}> {row.trend > 0 ? '📈' : row.trend < 0 ? '📉' : '→'} {Math.abs(row.trend).toFixed(1)}%
{__('No bot data available yet', 'llmagnet-llm-txt-generator')}
)} {/* Card View */} {viewMode === 'cards' && (
{botSummaries.map((bot) => (

{bot.bot_name}

{sprintf(__('Total visits: %s', 'llmagnet-llm-txt-generator'), bot.total_visits)}

{`${bot.bot_name} { (e.target as HTMLImageElement).src = defaultLogo; }} />
))}
)} ); }