import React, { useEffect, useState } from 'react'; import type { ArticlePromptImpactResponse } from '../../service/visibility/visibility.interface'; import { getArticlePromptImpact } from '../../service/visibility/visibility.service'; import { trendChange } from './PromptTrend'; interface ArticleImpactBadgeProps { /** Recomaze client id. */ clientId: string; /** Recomaze JWT. */ token: string; /** Article whose prompt impact to summarise. */ articleId: string; } /** Inline target glyph for the "Targets:" prefix. */ const TargetIcon = ({ size = 12 }: { size?: number }): JSX.Element => ( ); /** Inline arrow glyph; ``up`` flips the vertical direction. */ const ArrowIcon = ({ up, size = 12, }: { up: boolean; size?: number; }): JSX.Element => ( ); /** * Card-level "from which prompt, and did it grow?" chip. Fetches the article's * per-prompt impact and shows the prompt it targets plus the average * visibility and the delta since publish, so a merchant scanning the list sees * the source and the result without opening it. Renders nothing when the * article ties to no prompt or the fetch fails - the card stays clean. * * @param {ArticleImpactBadgeProps} props - Component props. * @returns {JSX.Element | null} The chip, or ``null`` when there's nothing. */ export const ArticleImpactBadge = ({ clientId, token, articleId, }: ArticleImpactBadgeProps): JSX.Element | null => { const [data, setData] = useState(null); useEffect(() => { let cancelled = false; getArticlePromptImpact(clientId, token, articleId) .then(result => { if (!cancelled) setData(result); }) .catch(() => { // Silent: the card just renders without the chip. }); return () => { cancelled = true; }; }, [clientId, token, articleId]); if (!data || data.prompts.length === 0) return null; const entries = data.prompts; const label = entries.length === 1 ? entries[0].prompt_text : `${entries.length} prompts`; const scored = entries .filter(entry => entry.tracked && entry.current_score !== null) .map(entry => entry.current_score as number); const baselines = entries .map(entry => entry.baseline_score) .filter((score): score is number => score !== null); const deltas = entries .map(entry => entry.delta_score) .filter((delta): delta is number => delta !== null); const avg = (values: number[]): number => Math.round(values.reduce((sum, value) => sum + value, 0) / values.length); const avgScore = scored.length ? avg(scored) : null; const avgBaseline = baselines.length ? avg(baselines) : null; const avgDelta = deltas.length ? avg(deltas) : null; const change = trendChange(avgBaseline, avgScore, avgDelta); const tone = change.kind === 'delta' && change.value > 0 ? 'text-green-600' : change.kind === 'delta' && change.value < 0 ? 'text-red-600' : change.kind === 'new' ? 'text-green-600' : 'text-gray-500'; return (
Targets: {label} {avgScore !== null ? ( AI visibility {avgScore} {change.kind === 'new' && ( New since publish )} {change.kind === 'delta' && ( = 0} /> {`${change.value > 0 ? '+' : ''}${change.value} since publish`} )} ) : ( Tracking ยท stats after the next scan )}
); }; export default ArticleImpactBadge;