learning_ai_common_plat/packages/ai-ui/src/CostMeter.tsx
saravanakumardb1 57a09c31dd feat(ai-ui): @bytelyst/ai-ui@0.5.0 — Wave 13.C trust surfaces (CostMeter / ConfidenceTag / RefusalCard)
Three new primitives — every product chat / agent surface should
adopt these to make the model honest about what it is doing.

──────────────────────────────────────────────────────────────────
<CostMeter>  ·  Wave 13.C.1
──────────────────────────────────────────────────────────────────
  packages/ai-ui/src/CostMeter.tsx (new)

  - Live token + (optional) USD readout
  - 4 tiers: neutral (no budget) · ok (<70 %) · warn (>=70 %) ·
    danger (>=95 %) — token-tinted via color-mix() so all four
    palettes degrade gracefully on browsers without var() support
  - NaN-safe — non-finite / negative inputs floor to 0
  - role=status + aria-live=polite + aria-label assembles a
    screen-reader-friendly sentence
  - Mini-bar visual indicator at the end of the pill when budget
    is provided
  - Pure passive surface — never warns / prompts / blocks

──────────────────────────────────────────────────────────────────
<ConfidenceTag>  ·  Wave 13.C.2
──────────────────────────────────────────────────────────────────
  packages/ai-ui/src/ConfidenceTag.tsx (new)

  - Accepts `number | 'high' | 'medium' | 'low' | 'unknown'`
  - Default thresholds 0.8 / 0.5 — both overridable
  - Out-of-range numerics map to `unknown` (no false confidence)
  - Optional `showScore` renders a tabular-nums percent suffix
  - 4 token-tinted palettes (success / warning / danger /
    neutral) — pair naturally with <CitationChip> for the full
    'show your work' story

──────────────────────────────────────────────────────────────────
<RefusalCard>  ·  Wave 13.C.3
──────────────────────────────────────────────────────────────────
  packages/ai-ui/src/RefusalCard.tsx (new)

  - 6 reason archetypes — safety / policy / capability /
    authorization / rate-limit / unknown — each with a typed
    heading + glyph
  - Calm warning palette (never red) — refusals are not errors
  - Up to 3 actionable next steps (further entries silently
    clipped) — one is markable `primary` to render as filled CTA
  - Optional `footer` slot for policy doc links
  - role=note + composite aria-label covering heading +
    explanation

──────────────────────────────────────────────────────────────────
Quality gates
──────────────────────────────────────────────────────────────────
  ✓ pnpm -F @bytelyst/ai-ui test  →  67/67 passing (was 53/53)
    +14 new trust-surface tests in src/__tests__/trust.test.tsx
  ✓ Exports wired in src/index.ts under '0.5 surfaces' section
  ✓ package.json 0.4.0 → 0.5.0

──────────────────────────────────────────────────────────────────
Roadmap tracker — 5 boxes flipped (§11)
──────────────────────────────────────────────────────────────────
  13.C.1  CostMeter shipped
  13.C.2  ConfidenceTag shipped
  13.C.3  RefusalCard shipped
  13.C.7  trust-surfaces showcase (lands in paired showcase commit)
  MAG.3   the trust-surfaces customer-magnet 

Wave 13 Futurism: 5/39 → 9/39 (23%)
Magnet demos:     1/8  → 2/8  (25%)
TOTAL:            19/202 → 24/202 (12%)

Vendored snapshot + showcase /ai-ui/* + /futurism/trust-surfaces
routes land in the paired showcase commit.

Pending in 13.C: ProvenanceDrawer (.4) · DebugOverlay (.5) ·
PrivacyBadge (.6).
2026-05-27 16:45:07 -07:00

163 lines
4.5 KiB
TypeScript

import type { CSSProperties } from 'react';
export interface CostMeterProps {
/** Tokens already consumed in this session (or invocation). */
tokensUsed: number;
/** Soft budget — UI tints amber at 70 %, red at 95 %. */
budgetTokens?: number;
/**
* Per-1k-token cost in USD. If provided, a dollar estimate renders
* alongside the token count.
*/
costPer1kUsd?: number;
/** Hide the inline `$0.0123` cost line. */
hideUsd?: boolean;
/** Override the accessible label. */
ariaLabel?: string;
className?: string;
style?: CSSProperties;
}
const fmtUsd = (n: number) =>
n >= 1
? `$${n.toFixed(2)}`
: n >= 0.01
? `$${n.toFixed(3)}`
: `$${n.toFixed(4)}`;
/**
* `<CostMeter>` — honest live token + dollar readout for any AI flow.
*
* Trust surface (Wave 13.C.1). Designed for the chat sidebar / debug
* tray pattern: tiny pill that turns amber → red as the user
* approaches the configured budget. No surprises at the end of the
* billing cycle.
*
* The component is intentionally **passive** — it never warns, prompts,
* or blocks. Surfacing the number is enough; let the host decide UX.
*/
export function CostMeter({
tokensUsed,
budgetTokens,
costPer1kUsd,
hideUsd,
ariaLabel,
className,
style,
}: CostMeterProps) {
const safeTokens = Math.max(0, Math.floor(tokensUsed) || 0);
const pct =
budgetTokens && budgetTokens > 0
? Math.min(1, safeTokens / budgetTokens)
: null;
const tier =
pct === null
? 'neutral'
: pct >= 0.95
? 'danger'
: pct >= 0.7
? 'warn'
: 'ok';
const tint: Record<typeof tier, { bg: string; fg: string; ring: string }> = {
neutral: {
bg: 'var(--bl-surface-muted, rgba(0,0,0,0.05))',
fg: 'var(--bl-text-secondary, #555)',
ring: 'var(--bl-border, rgba(0,0,0,0.12))',
},
ok: {
bg: 'color-mix(in srgb, var(--bl-success, #10b981) 14%, transparent)',
fg: 'var(--bl-success, #047857)',
ring: 'color-mix(in srgb, var(--bl-success, #10b981) 35%, transparent)',
},
warn: {
bg: 'color-mix(in srgb, var(--bl-warning, #f59e0b) 16%, transparent)',
fg: 'var(--bl-warning, #b45309)',
ring: 'color-mix(in srgb, var(--bl-warning, #f59e0b) 35%, transparent)',
},
danger: {
bg: 'color-mix(in srgb, var(--bl-danger, #ef4444) 16%, transparent)',
fg: 'var(--bl-danger, #b91c1c)',
ring: 'color-mix(in srgb, var(--bl-danger, #ef4444) 40%, transparent)',
},
};
const usd =
costPer1kUsd !== undefined && !hideUsd
? fmtUsd((safeTokens / 1000) * costPer1kUsd)
: null;
const label =
ariaLabel ??
(budgetTokens
? `${safeTokens} of ${budgetTokens} tokens used${usd ? `, ${usd}` : ''}`
: `${safeTokens} tokens used${usd ? `, ${usd}` : ''}`);
return (
<div
role="status"
aria-live="polite"
aria-label={label}
data-testid="bl-cost-meter"
data-tier={tier}
className={className}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: 8,
padding: '4px 10px',
borderRadius: 999,
backgroundColor: tint[tier].bg,
border: `1px solid ${tint[tier].ring}`,
color: tint[tier].fg,
fontSize: 12,
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
...style,
}}
>
<span aria-hidden="true"></span>
<span style={{ fontWeight: 600 }}>
{safeTokens.toLocaleString()}
{budgetTokens && (
<span style={{ opacity: 0.6 }}>
{' / '}
{budgetTokens.toLocaleString()}
</span>
)}{' '}
<span style={{ fontWeight: 400 }}>tok</span>
</span>
{usd && (
<span style={{ opacity: 0.85 }} data-testid="bl-cost-meter-usd">
{usd}
</span>
)}
{pct !== null && (
<span
aria-hidden="true"
style={{
position: 'relative',
width: 32,
height: 4,
borderRadius: 999,
backgroundColor: tint[tier].ring,
overflow: 'hidden',
}}
>
<span
style={{
position: 'absolute',
inset: 0,
transform: `scaleX(${pct})`,
transformOrigin: '0 0',
backgroundColor: tint[tier].fg,
transition: 'transform 240ms ease',
}}
/>
</span>
)}
</div>
);
}