import { lazy, Suspense, useEffect, useState } from 'react';
import { __, sprintf } from '../lib/i18n';
/**
* WP dashboard (index.php) widget body (adoption plan Feature 3).
*
* Layout: score gauge row → 30d AI-visits trend → CTA footer.
*
* recharts is the heavy dependency here, so the trend chart
* (BotTrafficChart) is lazy-loaded via dynamic import: the score row
* paints first and the chart chunk loads afterwards, keeping the WP
* dashboard's first paint light.
*/
const BotTrafficChart = lazy(() =>
import('./bot-traffic-chart').then((m) => ({ default: m.BotTrafficChart }))
);
export interface WidgetData {
overviewUrl: string;
contentSettingsUrl: string;
pluginUrl: string;
restNamespace: string;
llmsTxtExists: boolean;
lastGenerated: string | null;
}
interface TimelineSummary {
delta: number | null;
trend: 'up' | 'down' | 'flat' | 'na' | string;
}
function gaugeColor(score: number): string {
if (score >= 90) return '#22c55e';
if (score >= 50) return '#f97316';
return '#ef4444';
}
/** Small SVG donut gauge, color-banded like ScoreBadge (red/orange/green). */
function ScoreGauge({ score }: { score: number | null }) {
const size = 64;
const stroke = 7;
const r = (size - stroke) / 2;
const circumference = 2 * Math.PI * r;
const clamped = score === null ? 0 : Math.max(0, Math.min(100, Math.round(score)));
const color = score === null ? '#d1d5db' : gaugeColor(clamped);
return (
{score === null ? '—' : clamped}
);
}
function ChartSkeleton() {
return
;
}
export function WpDashboardWidget({ data }: { data: WidgetData }) {
const [score, setScore] = useState(null);
const [scoreLoaded, setScoreLoaded] = useState(false);
const [timeline, setTimeline] = useState(null);
const apiSettings = (window as any).wpApiSettings as { root?: string; nonce?: string } | undefined;
const apiRoot: string = apiSettings?.root ?? '/wp-json/';
useEffect(() => {
const headers = { 'X-WP-Nonce': apiSettings?.nonce ?? '' };
fetch(`${apiRoot}${data.restNamespace}/visibility-score`, {
headers,
credentials: 'same-origin',
})
.then((r) => (r.ok ? r.json() : null))
.then((payload) => {
if (payload && typeof payload.score === 'number') {
setScore(payload.score);
}
})
.catch(() => undefined)
.finally(() => setScoreLoaded(true));
fetch(`${apiRoot}${data.restNamespace}/visibility/timeline?range=30d`, {
headers,
credentials: 'same-origin',
})
.then((r) => (r.ok ? r.json() : null))
.then((payload) => {
if (payload) {
setTimeline({ delta: payload.delta ?? null, trend: payload.trend ?? 'na' });
}
})
.catch(() => undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const delta = timeline?.delta;
const showDelta = typeof delta === 'number' && timeline?.trend !== 'na';
return (
{/* Score row */}
{scoreLoaded ? (
) : (
)}
{__('AI Visibility Score', 'llmagnet-llm-txt-generator')}
{score === null && scoreLoaded ? (
__('Not calculated yet', 'llmagnet-llm-txt-generator')
) : (
<>
{__('Last 30 days', 'llmagnet-llm-txt-generator')}
{showDelta && (
= 0 ? 'text-green-600' : 'text-red-600')
}
>
{delta! >= 0 ? '▲' : '▼'} {Math.abs(delta!)}
)}
>
)}
{/* 30d AI-visits trend — recharts chunk loads lazily after first paint */}
{/* BotTrafficChart hard-codes its plot height for the full-page
analytics view; constrain it to widget size without touching
the shared component. */}
}>
null}
/>
{/* CTA footer */}
);
}