import { useCallback, useEffect, useRef, useState } from 'react'; import apiFetch from '@/wp-externals/api-fetch'; import { useSelect } from '@/wp-externals/data'; import type { ReadinessRecommendation } from '@/components/readiness-recommendations-list'; /** * Per-post visibility score + diagnostics for the editor surfaces * (adoption plan F2.2, Phase E Lane E1). * * Data sources: * - admins (`canManage`): the existing per-post details routes * GET /llm-analytics/v1/page-visibility-score?post_id=N (posts/pages) or * GET /llm-analytics/v1/product-visibility-score?product_id=N (products) — * full breakdown incl. content-quality components + recommendations. * - editors / shop managers (no manage_options): graceful fallback to the * batch GET /scores endpoint (edit_post-gated per the Phase 0.2 capability * decision) — persisted headline score only, `restricted` flag set. * * Refetches when a real (non-autosave) save completes (E1-2). */ export interface VisibilityDetails { score: number; bot_visibility?: { score: number; components?: Record }; content_quality?: { score: number; components?: Record }; recommendations?: Array; } export interface VisibilityData { loading: boolean; score: number | null; details: VisibilityDetails | null; /** True when only the persisted score is visible (no manage_options). */ restricted: boolean; error: string | null; refetch: () => void; } const API_NS = 'llm-analytics/v1'; export function useVisibilityData( postId: number | undefined, postType: string | undefined ): VisibilityData { const canManage = Boolean(window.llmagnetEditorData?.canManage); const [state, setState] = useState>({ loading: true, score: null, details: null, restricted: false, error: null, }); const fetchPersistedScore = useCallback(async (id: number): Promise => { const res = await apiFetch({ path: `/${API_NS}/scores?post_ids=${id}&context=editor` }); const row = res?.scores?.[String(id)] ?? null; return row && typeof row.score === 'number' ? row.score : null; }, []); const refetch = useCallback(() => { if (!postId || typeof apiFetch !== 'function') { return; } let cancelled = false; setState((s) => ({ ...s, loading: true, error: null })); (async () => { try { if (canManage) { const path = postType === 'product' ? `/${API_NS}/product-visibility-score?product_id=${postId}&range_days=30` : `/${API_NS}/page-visibility-score?post_id=${postId}&range_days=30`; const details = await apiFetch({ path }); if (cancelled) return; if (details && typeof details.score === 'number') { setState({ loading: false, score: details.score, details: details as VisibilityDetails, restricted: false, error: null, }); } else { // {success:false}-style payload — treat as "no score yet". setState({ loading: false, score: null, details: null, restricted: false, error: null }); } return; } const score = await fetchPersistedScore(postId); if (cancelled) return; setState({ loading: false, score, details: null, restricted: true, error: null }); } catch (err: any) { if (cancelled) return; const status = err?.data?.status; if (status === 401 || status === 403) { // Capability drift between payload and live check — fall back to // the edit_post-gated persisted score. try { const score = await fetchPersistedScore(postId); if (cancelled) return; setState({ loading: false, score, details: null, restricted: true, error: null }); return; } catch { // fall through to the generic error state } } setState({ loading: false, score: null, details: null, restricted: false, error: typeof err?.message === 'string' ? err.message : 'Request failed', }); } })(); return () => { cancelled = true; }; }, [postId, postType, canManage, fetchPersistedScore]); useEffect(() => { const cancel = refetch(); return typeof cancel === 'function' ? cancel : undefined; }, [refetch]); // Refetch after a real save completes (isSavingPost true→false, E1-2). // The PHP side clears the per-post score transients on save_post, so this // returns a freshly computed score rather than the 1h-cached one. const isSaving: boolean = useSelect( (select: any) => { const editor = select('core/editor'); return editor ? editor.isSavingPost() && !editor.isAutosavingPost() : false; }, [] ); const wasSaving = useRef(false); useEffect(() => { if (wasSaving.current && !isSaving) { refetch(); } wasSaving.current = isSaving; }, [isSaving, refetch]); return { ...state, refetch }; }