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;