import React, { useEffect, useMemo, useState } from 'react'; import InfoTooltip from '../agent-analytics/InfoTooltip'; import type { ArticlePromptImpactResponse, PromptImpactEntry, } from '../../service/visibility/visibility.interface'; import { getArticlePromptImpact } from '../../service/visibility/visibility.service'; import { PROMPT_TREND_STATUS_META, PromptSparkline, TrendStatusIcon, VISIBILITY_SCORE_HELP, trendChange, } from './PromptTrend'; import { toneToBadgeClass } from './helpers'; interface PromptImpactPanelProps { /** Recomaze client id. */ clientId: string; /** Recomaze JWT. */ token: string; /** Article whose prompt impact to load. */ articleId: string; } /** Aggregate read off the article's tracked prompts for the hero + verdict. */ interface ImpactSummary { trackedCount: number; avgScore: number | null; avgBaseline: number | null; avgDelta: number | null; improving: number; declining: number; collecting: number; } /** * Roll the per-prompt entries into the headline numbers and the verdict copy. * * @param {PromptImpactEntry[]} prompts - Per-prompt impact entries. * @returns {ImpactSummary} Aggregate counts and averages. */ function summarize(prompts: PromptImpactEntry[]): ImpactSummary { const tracked = prompts.filter( entry => entry.tracked && entry.current_score !== null ); const scored = tracked.map(entry => entry.current_score as number); const baselines = tracked .map(entry => entry.baseline_score) .filter((score): score is number => score !== null); const withDelta = tracked .map(entry => entry.delta_score) .filter((delta): delta is number => delta !== null); const avg = (values: number[]): number | null => values.length === 0 ? null : Math.round( values.reduce((sum, value) => sum + value, 0) / values.length ); return { trackedCount: tracked.length, avgScore: avg(scored), avgBaseline: avg(baselines), avgDelta: avg(withDelta), improving: tracked.filter(entry => entry.status === 'improving').length, declining: tracked.filter(entry => entry.status === 'declining').length, collecting: prompts.filter(entry => entry.status === 'collecting').length, }; } /** * Pick a friendly one-line verdict from the aggregate. * * @param {ImpactSummary} summary - Aggregate counts and averages. * @returns {string} A human verdict sentence. */ function verdictFor(summary: ImpactSummary): string { if (summary.trackedCount === 0 || summary.avgDelta === null) { return 'Too early to tell. Impact shows up here after the next weekly scan.'; } if (summary.improving > summary.declining && summary.avgDelta > 0) { return 'Paying off. Visibility is climbing on the prompts this article targets.'; } if (summary.declining > summary.improving && summary.avgDelta < 0) { return 'Visibility dipped since publish. The content may need a refresh.'; } return 'Mixed so far. Some prompts moved up, others slipped.'; } /** Inline right-arrow glyph for the before -> after readout. */ const ArrowRightIcon = ({ size = 12 }: { size?: number }): JSX.Element => ( ); /** * Render one targeted prompt's impact row with a before -> after readout. * * @param {object} props - Component props. * @param {PromptImpactEntry} props.entry - The per-prompt impact entry. * @param {string | null} props.publishedWeek - Week to dot on the sparkline. * @returns {JSX.Element} The row element. */ const PromptImpactRow = ({ entry, publishedWeek, }: { entry: PromptImpactEntry; publishedWeek: string | null; }): JSX.Element => { if (!entry.tracked) { return (
{entry.prompt_text}
Not in your tracked set yet. Add it on the prompts page to start measuring this one.
{entry.prompt_text}
{hasBeforeAfter ? ( {entry.baseline_score}{error}
} {!loading && !error && data && summary && ( <>{verdictFor(summary)}
{`Across ${summary.trackedCount} tracked prompt${ summary.trackedCount === 1 ? '' : 's' } this article targets, vs the week before publish. Correlation, not proof.`}