/** * The shared visual language for "what did AI decide on this prompt": * per-engine verdict chips and the "Won by" rival chips. Used by the * wins/losses cards on Overview and by every card on the Tracked Prompts * tab, so a verdict reads identically wherever the merchant meets it. */ import React from 'react'; import type { WinLossEngineOutcome, WinLossRival, } from '../../service/visibility/visibility.interface'; import { sentimentToTone } from './helpers'; import Pill from './Pill'; /** Props for {@link EngineOutcomeChips}. */ interface EngineOutcomeChipsProps { /** The week's per-engine verdicts for one prompt. */ outcomes: WinLossEngineOutcome[]; } /** * The prompt's verdict, as ONE chip. Engines are deliberately not named: a * row that spelled out every engine it asked turned into a wall of vendor * names, and two chips contradicting each other ("mentioned" beside "not * mentioned") read as a bug rather than as coverage. The merchant gets the * one answer they act on: were you in the answer, and how high. * * @param {EngineOutcomeChipsProps} props - The week's verdicts, one entry per * engine that answered. * @returns {JSX.Element | null} The chip row, or ``null`` without verdicts. */ export const EngineOutcomeChips = ({ outcomes, }: EngineOutcomeChipsProps): JSX.Element | null => { if (outcomes.length === 0) return null; const mentioning = outcomes.filter(outcome => outcome.mentioned); const mentioned = mentioning.length > 0; // Best rank the brand held anywhere it was named. const bestPosition = mentioning.reduce( (best, outcome) => outcome.position !== null && (best === null || outcome.position < best) ? outcome.position : best, null ); const sentiment = mentioning.find(outcome => outcome.sentiment)?.sentiment ?? null; return ( AI {' · '} {mentioned ? ( bestPosition !== null ? ( {`#${bestPosition}`} ) : ( 'mentioned' ) ) : ( 'not mentioned' )} {sentiment && ( {sentiment} )} ); }; /** Props for {@link WonByChips}. */ interface WonByChipsProps { /** Ranked rivals the AI named on this prompt. */ rivals: WinLossRival[]; /** * Lead-in word before the chips. "Won by" only fits a prompt the brand * LOST; where the brand is in the answer the rivals merely share it. An * empty string renders no lead-in at all, for callers that print the label * on its own line above the chips. */ label?: string; } /** * Rival chips: the competitors the AI named on this prompt, with their * leaderboard share of voice when they made it. * * @param {WonByChipsProps} props - Ranked rivals plus the lead-in word. * @returns {JSX.Element} The chip row. */ export const WonByChips = ({ rivals, label = 'Won by', }: WonByChipsProps): JSX.Element => ( {/* An empty label means the caller put it on its own line above. */} {label || null} {rivals.map(rival => ( {rival.name || rival.domain} {rival.share_of_voice !== null ? ` · ${rival.share_of_voice}% SOV` : ''} ))} );