import React, { useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { FaWandMagicSparkles } from 'react-icons/fa6'; import { LuFileText } from 'react-icons/lu'; import EmptyState from '../components/agent-analytics/EmptyState'; import ErrorBanner from '../components/agent-analytics/ErrorBanner'; import { Spinner } from '../components/ui/Spinner'; import GenerateIdeasModal from '../components/visibility/GenerateIdeasModal'; import { contentTypeLabel, PAGE_CONTENT_TYPES, } from '../components/visibility/helpers'; import PublishedArticlesList from '../components/visibility/PublishedArticlesList'; import RecommendedArticlesList from '../components/visibility/RecommendedArticlesList'; import { useAppStateContext } from '../context/user.data.context'; import type { ArticleCounts, ArticleSummary, PageContentType, SuggestionEntry, } from '../service/visibility/visibility.interface'; import { listArticles, listBrands, listSuggestions, } from '../service/visibility/visibility.service'; /** How often the list polls while a generation is in flight. */ const POLL_INTERVAL_MS = 4000; /** Filter value: a specific page type or "all". */ type TypeFilter = PageContentType | 'all'; /** Tabs for the content list. */ const CONTENT_TABS: ReadonlyArray<{ id: 'recommended' | 'published'; label: string; }> = [ { id: 'recommended', label: 'Recommended' }, { id: 'published', label: 'Published' }, ]; /** * Contents page. The dedicated home for generated blog / service / content * pages: a page-type filter, Recommended vs Published tabs, and the article * cards. "Run template" (and any generation) lands the merchant here. * * Client-only, like the AI Visibility report - all data lives on the agent * backend and is fetched with the merchant's recomaze JWT. * * @returns {JSX.Element} The Contents page. */ const AiVisibilityContents = (): JSX.Element => { const { token, clientId } = useAppStateContext(); const hasClientCreds = Boolean(token && clientId); const [brandId, setBrandId] = useState(''); const [language, setLanguage] = useState('en'); const [articles, setArticles] = useState([]); const [suggestions, setSuggestions] = useState([]); const [counts, setCounts] = useState({ recommended: 0, published: 0, }); const [typeFilter, setTypeFilter] = useState('all'); const [tab, setTab] = useState<'recommended' | 'published'>('recommended'); const [ideasOpen, setIdeasOpen] = useState(false); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const hasBrand = brandId.length > 0; const loadAll = useCallback(async (): Promise => { if (!hasClientCreds) return; setLoading(true); setError(null); try { const brandList = await listBrands(clientId as string, token as string); const brand = brandList.brands[0] ?? null; if (!brand) { setBrandId(''); setLoading(false); return; } setBrandId(brand.brand_id); setLanguage(brand.language || 'en'); const [articleList, suggestionList] = await Promise.all([ listArticles(clientId as string, token as string, { brand_id: brand.brand_id, }), listSuggestions(clientId as string, token as string, brand.brand_id), ]); setArticles(articleList.articles); setCounts(articleList.counts); setSuggestions(suggestionList.suggestions); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load contents.'); } finally { setLoading(false); } }, [hasClientCreds, clientId, token]); useEffect(() => { void loadAll(); }, [loadAll]); const refresh = useCallback(async (): Promise => { if (!brandId || !hasClientCreds) return; try { const [articleList, suggestionList] = await Promise.all([ // Bypass the 5-min server cache so a finished article stops reporting // "generating" on the next poll. listArticles(clientId as string, token as string, { brand_id: brandId, force: true, }), listSuggestions(clientId as string, token as string, brandId, true), ]); setArticles(articleList.articles); setCounts(articleList.counts); setSuggestions(suggestionList.suggestions); } catch { /* soft-fail: the next poll or navigation picks it up */ } }, [brandId, hasClientCreds, clientId, token]); // Poll while anything is still generating so cards advance to "ready" on // their own - covers generations kicked off from a template run or elsewhere. const refreshRef = useRef(refresh); refreshRef.current = refresh; useEffect(() => { const generating = articles.some( article => article.status === 'generating' || article.status === 'pending' ) || suggestions.some( suggestion => suggestion.lifecycle_state === 'generating' ); if (!generating) return; const timer = setInterval(() => { void refreshRef.current(); }, POLL_INTERVAL_MS); return () => clearInterval(timer); }, [articles, suggestions]); const sortedArticles = useMemo( () => [...articles].sort((first, second) => { const firstTime = first.created_at ? Date.parse(first.created_at) : 0; const secondTime = second.created_at ? Date.parse(second.created_at) : 0; return secondTime - firstTime; }), [articles] ); const typeFilteredArticles = useMemo( () => typeFilter === 'all' ? sortedArticles : sortedArticles.filter( article => (article.content_type ?? 'blog') === typeFilter ), [sortedArticles, typeFilter] ); const handleArticleReady = (): void => { void refresh(); }; const handleArticlePublishedChanged = (): void => { void refresh(); }; const handleSuggestionRejected = (rejectedId: string): void => { setSuggestions(prev => prev.filter(suggestion => suggestion.suggestion_id !== rejectedId) ); }; const handleArticleDeleted = (deletedId: string): void => { setArticles(prev => prev.filter(article => article.article_id !== deletedId) ); }; const handleIdeasGenerated = (ideas: SuggestionEntry[]): void => { setSuggestions(prev => [...ideas, ...prev]); }; if (!hasClientCreds) { return (
); } return (

Generated content

Every blog, service, and content page you generate lands here. New AI-generated drafts appear under Recommended; publish them to move them to Published.

{hasBrand && ( )}
{error && } {loading ? (
) : !hasBrand ? ( ) : ( <>
{(['all', ...PAGE_CONTENT_TYPES] as TypeFilter[]).map(value => ( ))}
{CONTENT_TABS.map(contentTab => { const isSelected = tab === contentTab.id; const count = counts[contentTab.id]; return ( ); })}
{tab === 'recommended' && ( )} {tab === 'published' && ( )}
)}
); }; export default AiVisibilityContents;