import { useState, useEffect } from 'react'; import { __ } from '../lib/i18n'; import { formatCompactNumber } from '../lib/utils'; import chatgptIcon from '../assets/images/chatgpt-icon.svg'; import claudeIcon from '../assets/images/claude-icon.svg'; import googleIcon from '../assets/images/google-icon.svg'; import grokIcon from '../assets/images/grok-icon.svg'; interface BotMention { bot_name: string; mentions: number; previousMentions: number; change: number; // percentage change percentage?: number; } interface MentionsOverviewProps { apiRoot: string; apiNamespace: string; } // Bot logos mapping using local SVG assets const botLogos: Record = { 'ChatGPT': chatgptIcon, 'Gemini': googleIcon, 'Grok': grokIcon, 'Claude': claudeIcon, }; // Component to render bot logo const BotLogo = ({ botName }: { botName: string }) => { const logoUrl = botLogos[botName] || botLogos['ChatGPT']; const [useSvg, setUseSvg] = useState(false); const renderSvgLogo = () => { switch (botName) { case 'ChatGPT': return ( ); case 'Gemini': return ( ); case 'Grok': return ( ); case 'Claude': return ( ); default: return null; } }; if (useSvg) { return (
{renderSvgLogo()}
); } return ( {botName} setUseSvg(true)} /> ); }; export function MentionsOverview({ apiRoot, apiNamespace }: MentionsOverviewProps) { const [mentions, setMentions] = useState([]); const [isLoading, setIsLoading] = useState(true); const [fetchError, setFetchError] = useState(null); useEffect(() => { const fetchMentions = async () => { try { setIsLoading(true); setFetchError(null); // Check for test mode flag in localStorage const isTestMode = typeof window !== 'undefined' && localStorage.getItem('llmagnet_test') === 'true'; if (isTestMode) { // Generate dummy data for testing const dummyData: BotMention[] = [ { bot_name: 'ChatGPT', mentions: 4482, previousMentions: 4075, change: 10, // 10% increase percentage: 38, // 38% of total }, { bot_name: 'Gemini', mentions: 3242, previousMentions: 2947, change: 10, // 10% increase percentage: 27, // 27% of total }, { bot_name: 'Grok', mentions: 2587, previousMentions: 2852, change: -9, // 9% decrease percentage: -22, // 22% of total }, { bot_name: 'Claude', mentions: 1832, previousMentions: 1665, change: 0, // 10% increase percentage: 0, // 15% of total }, ]; // Sort by mentions descending dummyData.sort((a, b) => b.mentions - a.mentions); setMentions(dummyData); setIsLoading(false); return; } // Calculate date range for last 30 days (current period) const endDate = new Date(); const startDate = new Date(); startDate.setDate(endDate.getDate() - 30); // Calculate date range for previous 30 days (comparison period) const previousEndDate = new Date(startDate); previousEndDate.setDate(previousEndDate.getDate() - 1); // Day before current period starts const previousStartDate = new Date(previousEndDate); previousStartDate.setDate(previousStartDate.getDate() - 30); // Format dates in local time (not UTC) to avoid timezone issues const formatLocalDate = (date: Date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; const startDateStr = formatLocalDate(startDate); const endDateStr = formatLocalDate(endDate); const previousStartDateStr = formatLocalDate(previousStartDate); const previousEndDateStr = formatLocalDate(previousEndDate); // Fetch bot visits for current period (last 30 days) // Use the same endpoint format as dashboard-analytics const currentEndpoint = `${apiRoot}${apiNamespace}/stats?start_date=${encodeURIComponent(startDateStr)}&end_date=${encodeURIComponent(endDateStr)}`; const currentResponse = await fetch(currentEndpoint, { headers: { 'X-WP-Nonce': (window as any).wpApiSettings?.nonce || '', }, credentials: 'same-origin', }); // Fetch bot visits for previous period (30 days before current) const previousEndpoint = `${apiRoot}${apiNamespace}/stats?start_date=${encodeURIComponent(previousStartDateStr)}&end_date=${encodeURIComponent(previousEndDateStr)}`; const previousResponse = await fetch(previousEndpoint, { headers: { 'X-WP-Nonce': (window as any).wpApiSettings?.nonce || '', }, credentials: 'same-origin', }); // Process current period data let currentData: Array<{ bot_name: string; visits: number }> = []; if (currentResponse.ok) { const data = await currentResponse.json(); if (data && Array.isArray(data)) { currentData = data; } } else { console.error('Failed to fetch current period data:', currentResponse.status); } // Process previous period data let previousData: Array<{ bot_name: string; visits: number }> = []; if (previousResponse.ok) { const data = await previousResponse.json(); if (data && Array.isArray(data)) { previousData = data; } } else { console.error('Failed to fetch previous period data:', previousResponse.status); } // If both requests failed, set error but still show cards if (!currentResponse.ok && !previousResponse.ok) { setFetchError(__('Error fetching data', 'llmagnet-llm-txt-generator')); } // Process the data to get total mentions per bot for current period const botCurrentMap: Record = {}; if (currentData && Array.isArray(currentData)) { currentData.forEach((visit: { bot_name: string; visits: number | string; total_visits?: number | string }) => { if (visit.bot_name) { // Ensure we're adding numbers, not concatenating strings const visitCount = Number(visit.visits) || Number(visit.total_visits) || 0; botCurrentMap[visit.bot_name] = (botCurrentMap[visit.bot_name] || 0) + visitCount; } }); } // Process the data to get total mentions per bot for previous period const botPreviousMap: Record = {}; if (previousData && Array.isArray(previousData)) { previousData.forEach((visit: { bot_name: string; visits: number | string; total_visits?: number | string }) => { if (visit.bot_name) { // Ensure we're adding numbers, not concatenating strings const visitCount = Number(visit.visits) || Number(visit.total_visits) || 0; botPreviousMap[visit.bot_name] = (botPreviousMap[visit.bot_name] || 0) + visitCount; } }); } // Convert to array and filter for the 4 main bots const mainBots = ['ChatGPT', 'Gemini', 'Grok', 'Claude']; const totalMentions = Object.values(botCurrentMap).reduce((sum, count) => sum + count, 0); const mentionsData: BotMention[] = mainBots.map(botName => { const currentCount = botCurrentMap[botName] || 0; const previousCount = botPreviousMap[botName] || 0; // Calculate percentage change let change = 0; if (previousCount > 0) { change = Math.round(((currentCount - previousCount) / previousCount) * 100); } else if (currentCount > 0) { change = 100; // New data, 100% increase } return { bot_name: botName, mentions: currentCount, previousMentions: previousCount, change: change, percentage: totalMentions > 0 ? Math.round((currentCount / totalMentions) * 100) : 0, }; }); mentionsData.sort((a, b) => b.mentions - a.mentions); setMentions(mentionsData); } catch (err) { console.error('Error fetching mentions:', err); setFetchError(__('Error fetching data', 'llmagnet-llm-txt-generator')); // Set default empty data - always show cards setMentions( ['ChatGPT', 'Gemini', 'Grok', 'Claude'].map((botName) => ({ bot_name: botName, mentions: 0, previousMentions: 0, change: 0, percentage: 0, })) ); } finally { setIsLoading(false); } }; fetchMentions(); }, [apiRoot, apiNamespace]); if (isLoading) { return (

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

{[1, 2, 3, 4].map((i) => (
))}
); } return (

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

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

{fetchError && ( {fetchError} )}
{mentions.map((mention) => (
{/* Top section: Logo and Bot Name */}
{/* Logo */} {/* Bot Name */} {mention.bot_name}
{/* Bottom section: Mention Count and Percentage Badge */}
{/* Mention Count */}
= 1000 ? mention.mentions.toLocaleString() : undefined} > {formatCompactNumber(mention.mentions)}
{/* Percentage Indicator - Always show, even if 0 */}
0 ? 'rounded-[10790.8px]' : mention.percentage !== undefined && mention.percentage < 0 ? 'rounded-[10790.8px]' : '' }`} style={{ minWidth: '45.4px', backgroundColor: mention.percentage !== undefined && mention.percentage > 0 ? '#D1FAE5' : mention.percentage !== undefined && mention.percentage < 0 ? '#FEE2E2' : 'transparent' }}>
0 ? '#2E8B57' : mention.percentage !== undefined && mention.percentage < 0 ? '#DC2626' : '#6B7280' }}> {mention.percentage !== undefined ? Math.abs(mention.percentage) : 0}%
))}
); }