import { Link } from 'react-router-dom'; import React, { useEffect, useMemo, useState } from 'react'; import { FaArrowRight, FaRegFileAlt, FaRegLightbulb, FaRegNewspaper, FaSpinner, } from 'react-icons/fa'; import { HiSparkles } from 'react-icons/hi'; import { formatNumber } from '../agent-analytics/helpers'; import type { ArticleListResponse, SuggestionEntry, } from '../../service/visibility/visibility.interface'; import { generateArticle, listArticles, listBrands, listSuggestions, } from '../../service/visibility/visibility.service'; const ARTICLES_TO = '/ai-visibility'; const MAX_IDEAS = 5; interface ArticlesState { data: ArticleListResponse; ideas: SuggestionEntry[]; brandId: string | null; } const EMPTY_STATE: ArticlesState = { data: { articles: [], counts: { recommended: 0, published: 0 } }, ideas: [], brandId: null, }; /** * Dashboard "Article ideas" widget - top ungenerated ideas with one-click * generate. Generated articles live behind "View all"; only ideas show here. * * @param {object} props - Recomaze auth. * @return {React.ReactElement} The articles section. */ export function LatestArticles({ clientId, token, }: { clientId: string; token: string; }) { const [state, setState] = useState(EMPTY_STATE); const [loading, setLoading] = useState(true); useEffect(() => { if (!clientId || !token) return; let cancelled = false; setLoading(true); /** * Fetch the five most recently created articles (any status); the backend * returns them sorted by created_at desc. Benign empty fallback on error. */ const loadArticles = async (): Promise => { try { return await listArticles(clientId, token, { limit: 5 }); } catch { return EMPTY_STATE.data; } }; /** Fetch the first brand's top ideas; benign empty fallback on error. */ const loadIdeas = async (): Promise<{ brandId: string | null; ideas: SuggestionEntry[]; }> => { try { const brandsRes = await listBrands(clientId, token); const brandId = brandsRes.brands?.[0]?.brand_id ?? null; if (!brandId) return { brandId: null, ideas: [] }; const suggestionsRes = await listSuggestions(clientId, token, brandId); const ideas = (suggestionsRes.suggestions ?? []) .filter( suggestion => suggestion.lifecycle_state === 'suggested' || suggestion.lifecycle_state === 'accepted' ) .slice(0, MAX_IDEAS); return { brandId, ideas }; } catch { return { brandId: null, ideas: [] }; } }; void (async () => { try { const [data, ideaResult] = await Promise.all([ loadArticles(), loadIdeas(), ]); if (!cancelled) { setState({ data, ideas: ideaResult.ideas, brandId: ideaResult.brandId, }); } } catch { // Benign: the store may have no visibility data yet. } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [clientId, token]); const { data, ideas, brandId } = state; const { articles, counts } = data; const inProgressSuggestionIds = useMemo( () => new Set( articles .filter( article => article.status === 'pending' || article.status === 'generating' ) .map(article => article.triggered_by_suggestion_id) .filter(Boolean) ), [articles] ); const visibleIdeas = brandId ? ideas.filter(idea => !inProgressSuggestionIds.has(idea.suggestion_id)) : []; return (

Content ideas

Generate AEO content for your tracked prompts in one click

View all
{loading ? ( ) : visibleIdeas.length === 0 ? ( ) : (
{visibleIdeas.map(idea => ( ))}
)}
); } function CountBadge({ label, value, dot, }: { label: string; value: number; dot: string; }) { return ( {label} {value} ); } function IdeaRow({ idea, brandId, clientId, token, }: { idea: SuggestionEntry; brandId: string; clientId: string; token: string; }) { const [dispatching, setDispatching] = useState(false); const [dispatched, setDispatched] = useState(false); const [error, setError] = useState(null); const handleGenerate = async () => { if (dispatching || dispatched) return; setDispatching(true); setError(null); try { await generateArticle(clientId, token, { brand_id: brandId, topic: idea.topic || idea.title, target_prompts: idea.target_prompts ?? [], keywords: idea.keywords ?? [], language: idea.language || undefined, suggestion_id: idea.suggestion_id, triggered_by: 'suggestion', }); setDispatched(true); } catch (generateError) { setError( generateError instanceof Error ? generateError.message : 'Failed to start generation. Please try again.' ); } finally { setDispatching(false); } }; return (

{idea.title}

{idea.language && ( {idea.language} )}
Idea {idea.priority_score > 0 && ( Score {Math.round(idea.priority_score)} )} {idea.estimated_word_count > 0 && ( ~{formatNumber(idea.estimated_word_count)} words )}
{(dispatching || dispatched) && !error && (

Keep this page open. We're pulling matching products from your catalog and drafting the content. This usually takes a minute or two.

)} {error &&

{error}

}
); } function SkeletonRows() { return (
{Array.from({ length: 3 }).map((_, index) => (
))}
); } function EmptyState() { return (

No content ideas right now

New ideas appear here after each visibility scan; your generated content is under View all.

); }