Skip to content

feat(apollo-vertex): add ConfidenceSignal component - #1001

Open
ChloeDalyUiPath wants to merge 1 commit into
UiPath:mainfrom
ChloeDalyUiPath:feat/confidence-signal
Open

feat(apollo-vertex): add ConfidenceSignal component#1001
ChloeDalyUiPath wants to merge 1 commit into
UiPath:mainfrom
ChloeDalyUiPath:feat/confidence-signal

Conversation

@ChloeDalyUiPath

@ChloeDalyUiPath ChloeDalyUiPath commented Aug 3, 2026

Copy link
Copy Markdown

Adds ConfidenceSignal, a signal-bar AI confidence chip (high/medium/low/unknown) with three
density variants (min/med/max), a tooltip on every chip, and a popover for factor breakdowns
and next-step CTAs, plus an opt-in one-time "acquire" animation. Registered in registry.json with
docs at /components/confidence-signal.

Visuals, popover structure/behaviour, and animation timing were matched against the team demo at
ai-confidence-demo-kappa.vercel.app: rounded signal-bar pills, faded same-hue tracks for unfilled
bars, per-level default explanations, and per-factor status tints.

Note on the nextStep requirement

An earlier internal demo site documented the action CTA as required for low/unknown. Per the
live team decision (Peter + Haidy), this PR requires it for medium/low instead (optional for
high/unknown), enforced at compile time via a discriminated union on ConfidenceSignalProps.
Both interpretations were considered; this follows the call, not the demo site.

Accessibility and conventions

  • Every chip carries a tooltip, so variant="min" (icon only) is never unlabelled.
  • Interactive detail lives in a popover, not the tooltip, so nextStep stays reachable by keyboard
    and on touch. The tooltip is suppressed while the popover is open.
  • CTAs accept href (rendered as a link, so navigable/middle-clickable) and/or onClick.
  • The acquire animation is skipped under prefers-reduced-motion: reduce.
  • Level labels and default explanations go through react-i18next under the confidence_signal_*
    prefix in locales/en.json, per AGENTS.md. Only en.json was touched.
  • Split into focused files (-levels, -bars, -chip, -factors, -cta) with target fields on
    every registry.json file entry, per the multi-file registry guidance in AGENTS.md.

Scope

Component only, per the agreed two-PR split. A follow-up PR adds usage guidance to
app/guidelines/ai-toolkit and the components overview entry.

Checks

pnpm install, pnpm registry:build, pnpm format, pnpm lint, pnpm lint:deps, and
pnpm typecheck all pass (lint:deps reports only the 10 pre-existing repo-wide warnings, 0
errors). Server-rendered output verified at /components/confidence-signal: all variants and levels
render, i18n labels resolve, and the SVG markup matches the demo. No runtime errors in the dev
server log.

Opened from a fork — I currently have read-only access to this repo.

Copilot AI review requested due to automatic review settings August 3, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new ConfidenceSignal UI component to the Apollo Vertex registry and documentation. The component is intended to communicate AI confidence levels (high/medium/low/unknown) via a signal-bar chip, optionally showing a hover/click popover with explanations, factor breakdowns, and CTAs.

Changes:

  • Introduces ConfidenceSignal + SignalBars, including an optional “acquire” animation and hover-open / 150ms-close popover behavior.
  • Registers the component in apps/apollo-vertex/registry.json for the Vertex registry build pipeline.
  • Adds a new docs page at /components/confidence-signal and adds it to the components nav.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
apps/apollo-vertex/registry/confidence-signal/confidence-signal.tsx Implements the ConfidenceSignal chip, popover content, and signal-bar SVG/animation.
apps/apollo-vertex/registry.json Registers the new registry:ui entry for confidence-signal.
apps/apollo-vertex/app/components/confidence-signal/page.mdx Adds component documentation and usage examples.
apps/apollo-vertex/app/components/_meta.ts Adds “Confidence Signal” to the components navigation.
Suppressed comments (5)

apps/apollo-vertex/registry/confidence-signal/confidence-signal.tsx:328

  • Same issue for nextStep: href is part of the prop type but isn’t used, so link-style next steps can’t be implemented without custom wrappers.
              onClick={nextStep.onClick}
            >
              {nextStep.label}
              <ArrowUpRight className="size-3" />
            </Button>

apps/apollo-vertex/app/components/confidence-signal/page.mdx:33

  • Same as above: nextStep is shown without href/onClick, producing a no-op CTA in the docs example.
<div className="p-4 border rounded-lg mt-4 flex flex-wrap gap-4 items-center">
  <ConfidenceSignal level="high" variant="med" />
  <ConfidenceSignal level="medium" variant="med" nextStep={{ label: 'Review' }} />
  <ConfidenceSignal level="low" variant="med" nextStep={{ label: 'Review' }} />
  <ConfidenceSignal level="unknown" variant="med" />
</div>

apps/apollo-vertex/app/components/confidence-signal/page.mdx:42

  • Same as above: nextStep is shown without href/onClick, producing a no-op CTA in the docs example.
<div className="p-4 border rounded-lg mt-4 flex flex-wrap gap-4 items-center">
  <ConfidenceSignal level="high" variant="max" />
  <ConfidenceSignal level="medium" variant="max" nextStep={{ label: 'Review' }} />
  <ConfidenceSignal level="low" variant="max" nextStep={{ label: 'Review' }} />
  <ConfidenceSignal level="unknown" variant="max" />
</div>

apps/apollo-vertex/app/components/confidence-signal/page.mdx:74

  • The popover example provides nextStep without href/onClick, which renders a no-op CTA in the docs.
      { label: 'Document match', value: '2 / 5', status: 'error' },
      { label: 'Historical accuracy', value: '61%' },
    ]}
    nextStep={{ label: 'Review manually' }}
  />

apps/apollo-vertex/app/components/confidence-signal/page.mdx:84

  • The acquire-animation example passes nextStep without href/onClick, producing a no-op CTA in the docs.
<div className="p-4 border rounded-lg mt-4 flex flex-wrap gap-4 items-center">
  <ConfidenceSignal level="high" variant="max" animateIn />
  <ConfidenceSignal level="medium" variant="max" animateIn nextStep={{ label: 'Review' }} />
</div>

Comment on lines +53 to +59
const ACQUIRE_KEYFRAMES = `
@keyframes confidence-signal-acquire {
0% { transform: scaleY(1); }
30% { transform: scaleY(0.05); }
100% { transform: scaleY(1); }
}
`;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The SVG now carries a local @media (prefers-reduced-motion: reduce) rule that overrides the inline animation, so the bars render at their final state with no motion.

Comment on lines +128 to +133
const LEVEL_TEXT_CLASS: Record<ConfidenceLevel, string> = {
high: "text-success",
medium: "text-amber-700",
low: "text-destructive",
unknown: "text-foreground",
};

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Medium now uses text-warning-foreground dark:text-warning, matching badge.tsx/alert.tsx. The split is needed because --warning is a light amber that fails contrast on a light background while --warning-foreground is near-black in both themes. Note the bar fills themselves stay literal hues on purpose: the signal metaphor relies on a fixed green/amber/red ramp reading identically in both themes, the way a battery or wifi icon does.

Comment on lines +306 to +316
{explainCta && (
<Button
variant="outline"
size="sm"
className="w-full"
onClick={explainCta.onClick}
>
{explainCta.label}
<ArrowUpRight className="size-3" />
</Button>
)}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. ConfidenceSignalCta now renders an anchor via Button asChild when href is present, so the target is navigable, middle-clickable, and copyable. onClick still fires. Also tightened the type so at least one of href/onClick is now required.

Comment on lines +19 to +24
<div className="p-4 border rounded-lg mt-4 flex flex-wrap gap-4 items-center">
<ConfidenceSignal level="high" variant="min" />
<ConfidenceSignal level="medium" variant="min" nextStep={{ label: 'Review' }} />
<ConfidenceSignal level="low" variant="min" nextStep={{ label: 'Review' }} />
<ConfidenceSignal level="unknown" variant="min" />
</div>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Every example CTA now passes a real href. This is also enforced at the type level now, so a CTA with neither href nor onClick fails to compile rather than silently rendering a no-op.

{ label: 'Source quality', value: 'High', status: 'success' },
{ label: 'Data recency', value: '< 30 days', status: 'success' },
]}
explainCta={{ label: 'View audit trail' }}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, same as above. explainCta in the popover examples now points at a real target.

@ChloeDalyUiPath
ChloeDalyUiPath marked this pull request as ready for review August 5, 2026 13:31
@ChloeDalyUiPath
ChloeDalyUiPath requested a review from a team as a code owner August 5, 2026 13:31
Comment on lines +202 to +217
const closeTimer = React.useRef<ReturnType<typeof setTimeout> | null>(null);

React.useEffect(
() => () => {
if (closeTimer.current) clearTimeout(closeTimer.current);
},
[],
);

const handleEnter = () => {
if (closeTimer.current) clearTimeout(closeTimer.current);
setOpen(true);
};
const handleLeave = () => {
closeTimer.current = setTimeout(() => setOpen(false), 150);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to re-implement a tooltip. For consistency and simplicity's sake, let's use the existing Tooltip component.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Now uses the existing Tooltip from @/components/ui/tooltip rather than a hand-rolled one. Every chip carries it, which also covers variant="min" where the icon would otherwise be unlabelled.

Comment on lines +284 to +301
<div
key={factor.label}
className="flex items-center justify-between gap-2"
>
<span className="text-xs text-muted-foreground">
{factor.label}
</span>
<span
className={cn(
"text-xs font-medium",
factor.status
? FACTOR_STATUS_CLASS[factor.status]
: "text-foreground",
)}
>
{factor.value}
</span>
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be it's own component so this component becomes more readable.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Split into confidence-signal-bars, -chip, -factors, -cta, and -levels, so the main file is now just composition (~150 lines).

Comment on lines +232 to +249
const chip = (
<button
type="button"
data-slot="confidence-signal"
data-level={level}
className={cn(
"inline-flex items-center gap-1.5 text-xs font-semibold focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50",
LEVEL_TEXT_CLASS[level],
hasPopoverContent && "cursor-pointer",
className,
)}
aria-label={LEVEL_LABEL[level]}
{...props}
>
<SignalBars level={level} animateIn={animateIn} />
{label}
</button>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this it's own component and pass things like label via props. That reduces this component's cognitive complexity quite a bit

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. This is now ConfidenceSignalChip, taking label, accessibleLabel, level, animateIn, and interactive as props.

Comment on lines +135 to +154
const LEVEL_LABEL: Record<ConfidenceLevel, string> = {
high: "High confidence",
medium: "Medium confidence",
low: "Low confidence",
unknown: "Unknown confidence",
};

const LEVEL_SHORT_LABEL: Record<ConfidenceLevel, string> = {
high: "High",
medium: "Medium",
low: "Low",
unknown: "Unknown",
};

const LEVEL_DEFAULT_EXPLANATION: Record<ConfidenceLevel, string> = {
high: "The output is well-supported and reliable.",
medium: "Some uncertainty remains — review before acting.",
low: "Limited evidence — verify this before relying on it.",
unknown: "The system cannot determine an answer reliably.",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be translation keys.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Level labels, short labels, and default explanations are now translation keys under the confidence_signal_* prefix, resolved with useTranslation(). Added to locales/en.json in alphabetical order; no other locale files touched.

Copilot AI review requested due to automatic review settings August 7, 2026 09:07
@ChloeDalyUiPath
ChloeDalyUiPath force-pushed the feat/confidence-signal branch from 2303d26 to bce2dca Compare August 7, 2026 09:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (5)

apps/apollo-vertex/registry/confidence-signal/confidence-signal.tsx:113

  • tooltipOpen is preserved while the popover is open. If the tooltip was open before opening the popover, closing the popover (e.g. by clicking outside) will immediately reopen the tooltip even when the pointer/focus is no longer on the trigger. Resetting tooltipOpen as part of the popover onOpenChange avoids this stale-state reopen/flash.
    <Popover open={detailsOpen} onOpenChange={setDetailsOpen}>

apps/apollo-vertex/registry/confidence-signal/confidence-signal-factors.tsx:32

  • key={factor.label} is not guaranteed to be unique (labels can repeat), which can cause React key collisions and unstable row reconciliation. Use a stable unique key (e.g. include the index, or introduce an id on ConfidenceFactor).
      {factors.map((factor) => (
        <ConfidenceSignalFactorRow key={factor.label} factor={factor} />
      ))}

apps/apollo-vertex/registry/confidence-signal/confidence-signal-levels.ts:17

  • ConfidenceCta currently allows providing neither href nor onClick, which renders a CTA that looks interactive but does nothing (and the rest of this component set assumes at least one action). Consider enforcing “at least one of href/onClick” at the type level to prevent invalid CTAs.
  label: string;
  /** Renders the CTA as a link. Takes precedence over `onClick` alone. */
  href?: string;
  onClick?: () => void;
}

apps/apollo-vertex/registry/confidence-signal/confidence-signal-cta.tsx:28

  • if (cta.href) treats an empty-string href as “no href”, which would render a <button> instead of an <a> and drop link affordances. Checking href !== undefined matches the intended optionality and is robust against empty strings.
  if (cta.href) {

apps/apollo-vertex/registry/confidence-signal/confidence-signal-chip.tsx:46

  • aria-label and type="button" are currently set before {...props}, so callers can accidentally override them via props spread (which contradicts the “always announced” accessibleLabel contract and can reintroduce default submit-button behavior in forms). Spread props first, then set type/aria-label so the component guarantees these attributes.
    <button
      type="button"
      className={cn(
        "inline-flex items-center gap-1.5 text-xs font-semibold focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50",
        LEVEL_CONFIG[level].textClass,
        interactive && "cursor-pointer",
        className,
      )}
      aria-label={accessibleLabel}
      {...props}
      // After the spread: as a Tooltip/Popover trigger this chip is cloned with
      // the trigger's own `data-slot`, which would otherwise mask its identity.
      data-slot="confidence-signal"
      data-level={level}
    >

Adds a signal-bar AI confidence chip (high/medium/low/unknown) with
min/med/max density variants, a tooltip on every chip, and a popover for
factor breakdowns and next-step CTAs, plus an opt-in one-time acquire
animation that respects prefers-reduced-motion.

The action CTA (nextStep) is required for medium/low confidence per team
decision, and optional for high/unknown, enforced via a discriminated
union type. Level labels and default explanations resolve through
react-i18next under the confidence_signal_* prefix.

Registered in registry.json with docs at /components/confidence-signal.
Copilot AI review requested due to automatic review settings August 7, 2026 12:04
@ChloeDalyUiPath
ChloeDalyUiPath force-pushed the feat/confidence-signal branch from bce2dca to b6e36a1 Compare August 7, 2026 12:04
@ChloeDalyUiPath

Copy link
Copy Markdown
Author

Pushed an update addressing all review feedback. Rebased into the single commit rather than stacking fixups, per the repo's git workflow.

@frankkluijtmans — your four points:

Request Change
Use the existing Tooltip component Now imports @/components/ui/tooltip; the hand-rolled hover logic is gone
Split for readability Now 5 files: -bars, -chip, -factors, -cta, -levels. Main file is composition only
Extract into its own component with props ConfidenceSignalChip, taking label/accessibleLabel/level/animateIn/interactive
These should be translation keys confidence_signal_* keys via useTranslation(), added to locales/en.json alphabetically. No other locales touched

Copilot's earlier pass: prefers-reduced-motion now honoured, text-amber-700 replaced with text-warning-foreground dark:text-warning, href actually renders an anchor, and the docs examples no longer show no-op CTAs.

Copilot's latest pass (3 suppressed comments), also fixed:

  • Tooltip could flash back open after dismissing the popover, because tooltipOpen kept its pre-popover state. The popover's onOpenChange now clears it on close.
  • key={factor.label} could collide when two factors share a label. Now keyed on label:value; rows matching on both are indistinguishable to the reader anyway.
  • ConfidenceCta permitted neither href nor onClick. Now a union requiring at least one, so a dead CTA fails to compile. Verified with a negative test: { label } alone errors, { label, href } and { label, onClick } both pass.

Checks: format, lint, lint:deps (0 errors; the 10 warnings are pre-existing repo-wide), typecheck (fresh, uncached), and registry:build all pass. Page renders clean at /components/confidence-signal with no runtime errors, i18n resolving, and no leaked keys.

Two notes for reviewers:

  1. The label / Label PR size checks fail with Resource not accessible by integration. That's the fork-PR limitation — labeling actions get a read-only token and can't attach labels. Not caused by anything in this diff.
  2. Interactive hover/click behaviour was verified in an earlier round; this round's verification was server-rendered output plus type-level tests, as my browser tooling dropped mid-session. The tooltip-flash fix in particular is worth a quick manual poke before merge.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (3)

apps/apollo-vertex/registry/confidence-signal/confidence-signal.tsx:123

  • The popover currently relies on nesting PopoverTrigger inside TooltipTrigger (via the tooltip variable). After making the tooltip trigger directly on the chip, the popover needs its own trigger wrapper. Wrapping the tooltip+chip in a PopoverTrigger asChild on a simple DOM element avoids nested Radix triggers while still allowing clicks/Enter on the inner button to bubble and open the popover.
    <Popover open={detailsOpen} onOpenChange={handleDetailsOpenChange}>
      {tooltip}
      <PopoverContent align="start" className="flex w-64 flex-col gap-3">

apps/apollo-vertex/registry/confidence-signal/confidence-signal-chip.tsx:26

  • ConfidenceSignalChip is used as the child of Radix TooltipTrigger asChild / PopoverTrigger asChild. For Radix asChild to work correctly (positioning, focus management), the child must accept a ref. This component is a plain function component, so it doesn’t forward refs and can cause runtime warnings or broken tooltip/popover behavior. Convert it to React.forwardRef<HTMLButtonElement, ConfidenceSignalChipProps> and pass the ref to the <button>.
function ConfidenceSignalChip({
  level,
  label,
  accessibleLabel,
  animateIn = false,

apps/apollo-vertex/registry/confidence-signal/confidence-signal.tsx:103

  • TooltipTrigger asChild is currently given a PopoverTrigger element when hasDetails is true. Radix asChild requires the child to be a DOM element or a forwardRef component; PopoverTrigger here is a wrapper component (not forwardRef), so the tooltip trigger ref/handlers won’t attach reliably. Consider making the tooltip always trigger directly on the chip, and move the popover trigger wrapper to the popover render path instead.

This issue also appears on line 121 of the same file.

    <Tooltip open={tooltipOpen && !detailsOpen} onOpenChange={setTooltipOpen}>
      <TooltipTrigger asChild>
        {hasDetails ? <PopoverTrigger asChild>{chip}</PopoverTrigger> : chip}
      </TooltipTrigger>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants