import React, { useEffect, useState } from 'react'; import { FiChevronDown, FiChevronUp, FiLink } from 'react-icons/fi'; import InfoTooltip from '../agent-analytics/InfoTooltip'; import type { CitationAuthorityTier, CitedSourceEntry, CitedSourcesResponse, } from '../../service/visibility/visibility.interface'; import { getCitedSources } from '../../service/visibility/visibility.service'; import { toneToBadgeClass } from './helpers'; interface CitedSourcesPanelProps { clientId: string; token: string; brandId: string; } /** Sources listed before the "Show all" toggle reveals the rest. */ const SOURCES_COLLAPSED = 8; const TIER_TONE: Record = { top: 'success', mid: 'info', low: undefined, unknown: undefined, }; const TIER_LABEL: Record = { top: 'Top authority', mid: 'Mid authority', low: 'Low authority', unknown: 'Unrated', }; /** One cited-source row: domain, authority tier, citation count, brand flags. */ function SourceRow({ source }: { source: CitedSourceEntry }): JSX.Element { return (
{source.domain} {source.is_owned && ( Your site )} {source.cites_brand && !source.is_owned && ( Cites you )}
{TIER_LABEL[source.authority_tier]} {`${source.total_citations_30d}×`}
); } /** * Cited-sources panel (Beta): the domains AI search cites in the brand's * category plus the competitors it's co-cited alongside. Collapsed to the top * sources with a "Show all" toggle. Fetches client-side. */ const CitedSourcesPanel = ({ clientId, token, brandId, }: CitedSourcesPanelProps): JSX.Element | null => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [expanded, setExpanded] = useState(false); useEffect(() => { let cancelled = false; setLoading(true); setError(null); getCitedSources(clientId, token, brandId) .then(result => { if (!cancelled) setData(result); }) .catch(err => { if (!cancelled) setError( err instanceof Error ? err.message : 'Failed to load cited sources.' ); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [clientId, token, brandId]); // Nothing scanned yet: stay silent rather than render an empty card. if (!loading && !error && (!data || data.sources.length === 0)) { return null; } const loaded = data?.sources ?? []; const shown = expanded ? loaded : loaded.slice(0, SOURCES_COLLAPSED); const canExpand = loaded.length > SOURCES_COLLAPSED; // Backend caps the payload at the top 50; surface the long tail as a count. const beyondLoaded = (data?.total_sources ?? 0) - loaded.length; return (

Cited sources

Beta

Where AI answers in your category get their facts. Earn a mention on these to get cited too.

{loading && (
)} {error &&

{error}

} {!loading && !error && data && data.sources.length > 0 && ( <>
{data.total_sources} {' '} sources {data.brand_cited_sources} {' '} cite you {`${data.top_tier_share}%`} {' '} top authority
{shown.map(source => ( ))}
{(canExpand || beyondLoaded > 0) && (
{canExpand ? ( ) : ( )} {beyondLoaded > 0 && ( {`+${beyondLoaded} more in the long tail`} )}
)} {data.co_citation.length > 0 && (

You're cited alongside

{data.co_citation.map(neighbor => ( {neighbor.competitor_domain} {neighbor.shared_sources} ))}
)} )}
); }; export default CitedSourcesPanel;