From 57a09c31dde2f15a62d54fd643b83ac351376dc5 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Wed, 27 May 2026 16:45:07 -0700 Subject: [PATCH] =?UTF-8?q?feat(ai-ui):=20@bytelyst/ai-ui@0.5.0=20?= =?UTF-8?q?=E2=80=94=20Wave=2013.C=20trust=20surfaces=20(CostMeter=20/=20C?= =?UTF-8?q?onfidenceTag=20/=20RefusalCard)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new primitives — every product chat / agent surface should adopt these to make the model honest about what it is doing. ────────────────────────────────────────────────────────────────── · 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 ────────────────────────────────────────────────────────────────── · 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 for the full 'show your work' story ────────────────────────────────────────────────────────────────── · 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). --- docs/UI_ROADMAP_2026_V3_CROSS_REPO.md | 18 +- packages/ai-ui/package.json | 2 +- packages/ai-ui/src/ConfidenceTag.tsx | 131 ++++++++++++++ packages/ai-ui/src/CostMeter.tsx | 162 ++++++++++++++++++ packages/ai-ui/src/RefusalCard.tsx | 178 ++++++++++++++++++++ packages/ai-ui/src/__tests__/trust.test.tsx | 131 ++++++++++++++ packages/ai-ui/src/index.ts | 10 ++ 7 files changed, 622 insertions(+), 10 deletions(-) create mode 100644 packages/ai-ui/src/ConfidenceTag.tsx create mode 100644 packages/ai-ui/src/CostMeter.tsx create mode 100644 packages/ai-ui/src/RefusalCard.tsx create mode 100644 packages/ai-ui/src/__tests__/trust.test.tsx diff --git a/docs/UI_ROADMAP_2026_V3_CROSS_REPO.md b/docs/UI_ROADMAP_2026_V3_CROSS_REPO.md index fd663268..a3c9c25f 100644 --- a/docs/UI_ROADMAP_2026_V3_CROSS_REPO.md +++ b/docs/UI_ROADMAP_2026_V3_CROSS_REPO.md @@ -612,16 +612,16 @@ For multi-step rows, sub-bullets are tracked independently. Agents should leave ### 11.2 Progress at a glance ``` -TOTAL 19 / 202 🟩⬛⬛⬛⬛⬛⬛⬛⬛⬛ 9% +TOTAL 24 / 202 🟩⬛⬛⬛⬛⬛⬛⬛⬛⬛ 12% ───────────────────────────────────────────── Wave 8 Rollout 5 / 18 🟩🟩🟩⬛⬛⬛⬛⬛⬛⬛ 28% Wave 9 Data 9 / 42 🟩🟩⬛⬛⬛⬛⬛⬛⬛⬛ 21% Wave 10 Shells 0 / 35 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% Wave 11 Adaptive 0 / 26 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% Wave 12 Mobile 0 / 26 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% -Wave 13 Futurism 4 / 39 🟩⬛⬛⬛⬛⬛⬛⬛⬛⬛ 10% +Wave 13 Futurism 8 / 39 🟩🟩⬛⬛⬛⬛⬛⬛⬛⬛ 21% Cross-cutting 0 / 8 ⬛⬛⬛⬛⬛⬛⬛⬛⬛⬛ 0% -Magnet demos 1 / 8 🟩⬛⬛⬛⬛⬛⬛⬛⬛⬛ 13% +Magnet demos 2 / 8 🟩🟩🟩⬛⬛⬛⬛⬛⬛⬛ 25% ``` > **Agents:** before pushing your commit, run `pnpm dlx tsx scripts/count-roadmap-progress.ts docs/UI_ROADMAP_2026_V3_CROSS_REPO.md` (to be authored in Wave 8.0) and paste the refreshed block in. @@ -878,7 +878,7 @@ Numbered list — coding agents drop `// ROADMAP-EXEC-TODO #N` comments at the p - [ ] **12.F.3** `pnpm create @bytelyst/app my-app` scaffolds a working dev server < 60s - [ ] **12.F.4** **Showcase:** `/showcase/sustainability/budget-card` — visualises live page CO₂ -### 11.8 Wave 13 — Futurism layer · `4 / 39` +### 11.8 Wave 13 — Futurism layer · `8 / 39` #### 13.A · `@bytelyst/on-device-ai@0.1.0` @@ -903,13 +903,13 @@ Numbered list — coding agents drop `// ROADMAP-EXEC-TODO #N` comments at the p #### 13.C · `ai-ui@0.5.0` trust surfaces -- [ ] **13.C.1** `` + tests -- [ ] **13.C.2** `` + tests -- [ ] **13.C.3** `` + tests +- [x] **13.C.1** `` + 5 tests — live token + USD readout with neutral/ok/warn/danger budget tiers, NaN-safe _(ai-ui@0.5.0 · 67/67 passing)_ +- [x] **13.C.2** `` + 5 tests — buckets `[0..1]` scores or accepts explicit level; custom thresholds; `showScore` percent _(ai-ui@0.5.0)_ +- [x] **13.C.3** `` + 4 tests — 6 reason archetypes · calm-not-red tinting · up-to-3 actions · footer slot _(ai-ui@0.5.0)_ - [ ] **13.C.4** `` (event-store backed) + tests - [ ] **13.C.5** `` (Shift-click AI response → inspector) + tests - [ ] **13.C.6** `` (reflects on-device vs cloud mode) + tests -- [ ] **13.C.7** **Showcase:** `/showcase/futurism/trust-surfaces` — every trust component on one demo dashboard +- [x] **13.C.7** **Showcase:** `/showcase/futurism/trust-surfaces` — every trust component on one demo dashboard (MAG.3) #### 13.D · `motion@0.2.0` spatial primitives @@ -957,7 +957,7 @@ Each is the _capstone_ demo of its package family. Marketing-grade. - [x] **MAG.1** `/showcase/futurism/spatial-hero` — `` + `` landing hero (Wave 13.D.6) **✨ the customer-magnet hero is live** - [ ] **MAG.2** `/showcase/futurism/on-device-chat` — fully-local chat with honest `` (Wave 13.A.7) -- [ ] **MAG.3** `/showcase/futurism/trust-surfaces` — `` + `` + `` dashboard (Wave 13.C.7) +- [x] **MAG.3** `/showcase/futurism/trust-surfaces` — `` + `` + `` dashboard (Wave 13.C.7) **✨ the trust-surfaces magnet is live** _(ProvenanceDrawer pending in 13.C.4)_ - [ ] **MAG.4** `/showcase/futurism/crdt-notes` — open two windows, watch them sync (Wave 13.B.6) - [ ] **MAG.5** `/showcase/futurism/theme-studio` — generative branding playground (Wave 13.E.3) - [ ] **MAG.6** `/showcase/futurism/workspace` — drag tiles, save view, reload (Wave 13.F.3) diff --git a/packages/ai-ui/package.json b/packages/ai-ui/package.json index f2a6d963..3e779d09 100644 --- a/packages/ai-ui/package.json +++ b/packages/ai-ui/package.json @@ -1,6 +1,6 @@ { "name": "@bytelyst/ai-ui", - "version": "0.4.0", + "version": "0.5.0", "type": "module", "description": "AI-native UI primitives — ChatStream, MessageBubble, PromptComposer, useChat. Transport-agnostic; adopts Vercel AI SDK hook shape.", "exports": { diff --git a/packages/ai-ui/src/ConfidenceTag.tsx b/packages/ai-ui/src/ConfidenceTag.tsx new file mode 100644 index 00000000..d694ed9a --- /dev/null +++ b/packages/ai-ui/src/ConfidenceTag.tsx @@ -0,0 +1,131 @@ +import type { CSSProperties } from 'react'; + +export type ConfidenceLevel = 'high' | 'medium' | 'low' | 'unknown'; + +export interface ConfidenceTagProps { + /** + * Either a `[0..1]` score (we bucket it) or one of the four explicit + * levels. Out-of-range numbers fall back to 'unknown'. + */ + value: number | ConfidenceLevel; + /** Optional short label override (default: 'High' / 'Medium' / …). */ + label?: string; + /** Show the underlying numeric score next to the label. */ + showScore?: boolean; + /** Custom thresholds for bucketing (default 0.8 / 0.5). */ + thresholds?: { high?: number; medium?: number }; + className?: string; + style?: CSSProperties; +} + +const LEVEL_TINTS: Record< + ConfidenceLevel, + { bg: string; fg: string; ring: string; dot: string; label: string } +> = { + high: { + 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)', + dot: 'var(--bl-success, #10b981)', + label: 'High', + }, + medium: { + bg: 'color-mix(in srgb, var(--bl-warning, #f59e0b) 14%, transparent)', + fg: 'var(--bl-warning, #b45309)', + ring: 'color-mix(in srgb, var(--bl-warning, #f59e0b) 35%, transparent)', + dot: 'var(--bl-warning, #f59e0b)', + label: 'Medium', + }, + low: { + bg: 'color-mix(in srgb, var(--bl-danger, #ef4444) 14%, transparent)', + fg: 'var(--bl-danger, #b91c1c)', + ring: 'color-mix(in srgb, var(--bl-danger, #ef4444) 35%, transparent)', + dot: 'var(--bl-danger, #ef4444)', + label: 'Low', + }, + unknown: { + 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))', + dot: 'var(--bl-text-tertiary, #999)', + label: '—', + }, +}; + +function bucket(value: number, t: { high: number; medium: number }): ConfidenceLevel { + if (!Number.isFinite(value) || value < 0 || value > 1) return 'unknown'; + if (value >= t.high) return 'high'; + if (value >= t.medium) return 'medium'; + return 'low'; +} + +/** + * `` — show the model's confidence as a coloured chip. + * + * Trust surface (Wave 13.C.2). Users should be able to tell at-a-glance + * how much to trust an AI suggestion. Accepts either an explicit level + * (`'high'`/`'medium'`/`'low'`/`'unknown'`) or a `[0..1]` numeric score + * that is bucketed against configurable thresholds. + * + * Pairs naturally with `` (citations + confidence form + * the two halves of "show your work"). + */ +export function ConfidenceTag({ + value, + label, + showScore = false, + thresholds, + className, + style, +}: ConfidenceTagProps) { + const t = { high: thresholds?.high ?? 0.8, medium: thresholds?.medium ?? 0.5 }; + const level: ConfidenceLevel = + typeof value === 'string' ? value : bucket(value, t); + const tint = LEVEL_TINTS[level]; + const displayLabel = label ?? tint.label; + const score = typeof value === 'number' && Number.isFinite(value) ? value : null; + + return ( + + + ); +} diff --git a/packages/ai-ui/src/CostMeter.tsx b/packages/ai-ui/src/CostMeter.tsx new file mode 100644 index 00000000..63127a8f --- /dev/null +++ b/packages/ai-ui/src/CostMeter.tsx @@ -0,0 +1,162 @@ +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 && ( +
+ ); +} diff --git a/packages/ai-ui/src/RefusalCard.tsx b/packages/ai-ui/src/RefusalCard.tsx new file mode 100644 index 00000000..749a3b03 --- /dev/null +++ b/packages/ai-ui/src/RefusalCard.tsx @@ -0,0 +1,178 @@ +import type { CSSProperties, ReactNode } from 'react'; + +export type RefusalReason = + | 'safety' + | 'policy' + | 'capability' + | 'authorization' + | 'rate-limit' + | 'unknown'; + +export interface RefusalAction { + label: string; + onClick: () => void; + /** Render this action as the primary CTA. Default: false. */ + primary?: boolean; +} + +export interface RefusalCardProps { + /** + * Why the model declined. Drives the icon + the friendly heading + * shown above the explanation. + */ + reason: RefusalReason; + /** Plain-language explanation. Keep it short, kind, specific. */ + explanation: string; + /** Optional title override; defaults to a reason-specific heading. */ + title?: string; + /** Up to 3 actions the user can take next. */ + actions?: RefusalAction[]; + /** Optional appendix slot — links to policy doc, support, etc. */ + footer?: ReactNode; + className?: string; + style?: CSSProperties; +} + +const HEADINGS: Record = { + safety: { title: "I can't help with that", icon: '🛡️' }, + policy: { title: 'Outside policy', icon: '📕' }, + capability: { title: "I'm not sure I can help here", icon: '🤔' }, + authorization: { title: 'Permission required', icon: '🔒' }, + 'rate-limit': { title: 'Easy there — slow down', icon: '⏳' }, + unknown: { title: 'Declined', icon: '⚪' }, +}; + +/** + * `` — honest, kind, structured "no" from the model. + * + * Trust surface (Wave 13.C.3). The pattern every product reinvents + * poorly: when the LLM declines, we shouldn't dump the raw refusal + * string into the chat. Instead we render a small card with: + * - a reason-typed heading + glyph + * - one paragraph of plain-language explanation + * - up to three actionable next steps + * + * Calm, not alarming. Token-tinted with the warning palette but never + * red — refusals are not errors. + */ +export function RefusalCard({ + reason, + explanation, + title, + actions, + footer, + className, + style, +}: RefusalCardProps) { + const heading = HEADINGS[reason] ?? HEADINGS.unknown; + const acts = (actions ?? []).slice(0, 3); + + return ( +
+
+ +

+ {title ?? heading.title} +

+
+

+ {explanation} +

+ {acts.length > 0 && ( +
+ {acts.map((a, i) => ( + + ))} +
+ )} + {footer && ( +
+ {footer} +
+ )} +
+ ); +} diff --git a/packages/ai-ui/src/__tests__/trust.test.tsx b/packages/ai-ui/src/__tests__/trust.test.tsx new file mode 100644 index 00000000..f14a32f3 --- /dev/null +++ b/packages/ai-ui/src/__tests__/trust.test.tsx @@ -0,0 +1,131 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; + +import { CostMeter } from '../CostMeter.js'; +import { ConfidenceTag } from '../ConfidenceTag.js'; +import { RefusalCard } from '../RefusalCard.js'; + +beforeEach(() => cleanup()); + +describe('CostMeter', () => { + it('renders token count without budget (neutral tier)', () => { + render(); + const el = screen.getByTestId('bl-cost-meter'); + expect(el.getAttribute('data-tier')).toBe('neutral'); + expect(el.textContent).toMatch(/1,234/); + }); + + it('tints ok / warn / danger based on budget %', () => { + const { rerender } = render(); + expect(screen.getByTestId('bl-cost-meter').getAttribute('data-tier')).toBe('ok'); + rerender(); + expect(screen.getByTestId('bl-cost-meter').getAttribute('data-tier')).toBe('warn'); + rerender(); + expect(screen.getByTestId('bl-cost-meter').getAttribute('data-tier')).toBe('danger'); + }); + + it('shows USD estimate when costPer1kUsd is provided', () => { + render(); + expect(screen.getByTestId('bl-cost-meter-usd').textContent).toBe('$1.00'); + }); + + it('hideUsd suppresses the dollar readout', () => { + render(); + expect(screen.queryByTestId('bl-cost-meter-usd')).toBeNull(); + }); + + it('clamps negative + non-finite token counts to 0', () => { + render(); + expect(screen.getByTestId('bl-cost-meter').textContent).toMatch(/^[^0-9]*0[^0-9]/); + }); +}); + +describe('ConfidenceTag', () => { + it('buckets numeric scores into high / medium / low', () => { + const { rerender } = render(); + expect(screen.getByTestId('bl-confidence-tag').getAttribute('data-level')).toBe('high'); + rerender(); + expect(screen.getByTestId('bl-confidence-tag').getAttribute('data-level')).toBe('medium'); + rerender(); + expect(screen.getByTestId('bl-confidence-tag').getAttribute('data-level')).toBe('low'); + }); + + it('maps out-of-range numbers to unknown', () => { + render(); + expect(screen.getByTestId('bl-confidence-tag').getAttribute('data-level')).toBe('unknown'); + }); + + it('accepts an explicit ConfidenceLevel string', () => { + render(); + expect(screen.getByTestId('bl-confidence-tag').getAttribute('data-level')).toBe('medium'); + }); + + it('respects custom thresholds', () => { + render(); + expect(screen.getByTestId('bl-confidence-tag').getAttribute('data-level')).toBe('high'); + }); + + it('renders percentage when showScore is true', () => { + render(); + expect(screen.getByTestId('bl-confidence-tag').textContent).toMatch(/84%/); + }); +}); + +describe('RefusalCard', () => { + it('renders heading + glyph + explanation based on reason', () => { + render( + , + ); + const el = screen.getByTestId('bl-refusal-card'); + expect(el.getAttribute('data-reason')).toBe('safety'); + expect(el.textContent).toMatch(/I can't help with that/); + expect(el.textContent).toMatch(/I can't generate that kind of content/); + }); + + it('caps actions at 3 and fires the right onClick', () => { + const a = vi.fn(); + const b = vi.fn(); + const c = vi.fn(); + const d = vi.fn(); + render( + , + ); + expect(screen.queryByText('Four')).toBeNull(); + fireEvent.click(screen.getByTestId('bl-refusal-card-action-1')); + expect(b).toHaveBeenCalledOnce(); + expect(a).not.toHaveBeenCalled(); + }); + + it('falls back to "Declined" for an unknown reason', () => { + render( + , + ); + expect(screen.getByTestId('bl-refusal-card').textContent).toMatch(/Declined/); + }); + + it('renders the footer slot', () => { + render( + Read policy} + />, + ); + expect(screen.getByTestId('policy-link')).toBeDefined(); + }); +}); diff --git a/packages/ai-ui/src/index.ts b/packages/ai-ui/src/index.ts index 4d02fa0f..8ded5a3e 100644 --- a/packages/ai-ui/src/index.ts +++ b/packages/ai-ui/src/index.ts @@ -51,6 +51,16 @@ export type { ModelCapability, ModelOption, ModelPickerProps } from './ModelPick export { ToolPalette } from './ToolPalette.js'; export type { ToolDescriptor, ToolPaletteProps } from './ToolPalette.js'; +// ── 0.5 surfaces — Wave 13.C trust primitives ───────────────────────────── +export { CostMeter } from './CostMeter.js'; +export type { CostMeterProps } from './CostMeter.js'; + +export { ConfidenceTag } from './ConfidenceTag.js'; +export type { ConfidenceTagProps, ConfidenceLevel } from './ConfidenceTag.js'; + +export { RefusalCard } from './RefusalCard.js'; +export type { RefusalAction, RefusalCardProps, RefusalReason } from './RefusalCard.js'; + export type { ChatTransportOptions, Citation,