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)}`; /** * `` — 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 = { 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 (
{safeTokens.toLocaleString()} {budgetTokens && ( {' / '} {budgetTokens.toLocaleString()} )}{' '} tok {usd && ( {usd} )} {pct !== null && (
); }