learning_ai_common_plat/packages/ai-ui/src/PrivacyBadge.tsx
saravanakumardb1 ec9e11b243 feat(ai-ui): complete Wave 13.C — PrivacyBadge + ProvenanceDrawer + DebugOverlay
The remaining three trust surfaces ship in this commit, completing
Wave 13.C and unlocking MAG.8 (debug-overlay).

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

  - 4 modes: on-device · cloud · hybrid · unknown
  - Token-tinted (success/info/accent/neutral) — instant trust
    signal without forcing the user into settings
  - Optional `detail` line (model id, device name, routing policy)
  - `iconOnly` variant for sidebar / tray placements
  - role=status + composite aria-label

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

  - Slide-in right drawer listing every step the model + tools took
  - Pure presentation — host passes the event array straight through
  - role=dialog + aria-modal + aria-labelledby
  - Initial focus on close button; Esc + backdrop both close
  - Body scroll lock while open (restores prior overflow value)
  - Empty-state copy when events=[]
  - Per-row <details> slot for inspecting full payload

──────────────────────────────────────────────────────────────────
<DebugOverlay>  ·  Wave 13.C.5  (MAG.8 magnet)
──────────────────────────────────────────────────────────────────
  packages/ai-ui/src/DebugOverlay.tsx (new)

  - Wraps any child surface — Shift-click reveals a modal inspector
    with the raw JSON payload
  - Modifier configurable: shift (default) / alt / meta
  - Production toggle: `disabled` short-circuits the wrapper (no
    cursor-hint, no click interception)
  - role=dialog + aria-modal + Esc to close + focus restore on close
  - Stringifies payload safely (catches non-serialisable values)
  - Cursor: help on the wrapper to hint discoverability

──────────────────────────────────────────────────────────────────
Quality gates
──────────────────────────────────────────────────────────────────
  ✓ pnpm -F @bytelyst/ai-ui test  →  79/79 passing (was 67/67)
    +12 new cases:
      - PrivacyBadge (3)
      - ProvenanceDrawer (5: closed-state · dialog semantics ·
        backdrop · Escape · empty-state)
      - DebugOverlay (4: plain-click ignored · Shift opens · disabled
        bypass · alt modifier)
  ✓ Exports wired in src/index.ts (PrivacyBadge, ProvenanceDrawer,
    DebugOverlay + all types)

──────────────────────────────────────────────────────────────────
Roadmap tracker (lands in subsequent commit)
──────────────────────────────────────────────────────────────────
  13.C.4  ProvenanceDrawer shipped
  13.C.5  DebugOverlay shipped
  13.C.6  PrivacyBadge shipped
  MAG.8   the debug-overlay magnet (showcase lands in paired commit)

Wave 13.C is now complete (7/7). Wave 13 Futurism: 9/39 → 12/39.
2026-05-27 16:53:03 -07:00

127 lines
3.6 KiB
TypeScript

import type { CSSProperties } from 'react';
export type PrivacyMode = 'on-device' | 'cloud' | 'hybrid' | 'unknown';
export interface PrivacyBadgeProps {
/** Where does the inference actually run? */
mode: PrivacyMode;
/** Short label override (default: 'On device' / 'Cloud' / …). */
label?: string;
/**
* Optional one-line attribution — e.g. provider/model id, or the
* device name. Rendered as a smaller secondary string.
*/
detail?: string;
/** Render with only the dot — no text. Default false. */
iconOnly?: boolean;
className?: string;
style?: CSSProperties;
}
const TINTS: Record<
PrivacyMode,
{ bg: string; fg: string; ring: string; dot: string; label: string; icon: string }
> = {
'on-device': {
bg: 'color-mix(in srgb, var(--bl-success, #10b981) 12%, transparent)',
fg: 'var(--bl-success, #047857)',
ring: 'color-mix(in srgb, var(--bl-success, #10b981) 35%, transparent)',
dot: 'var(--bl-success, #10b981)',
label: 'On device',
icon: '📴',
},
cloud: {
bg: 'color-mix(in srgb, var(--bl-info, #0ea5e9) 12%, transparent)',
fg: 'var(--bl-info, #075985)',
ring: 'color-mix(in srgb, var(--bl-info, #0ea5e9) 35%, transparent)',
dot: 'var(--bl-info, #0ea5e9)',
label: 'Cloud',
icon: '☁️',
},
hybrid: {
bg: 'color-mix(in srgb, var(--bl-accent, #6366f1) 12%, transparent)',
fg: 'var(--bl-accent, #4338ca)',
ring: 'color-mix(in srgb, var(--bl-accent, #6366f1) 35%, transparent)',
dot: 'var(--bl-accent, #6366f1)',
label: 'Hybrid',
icon: '🔀',
},
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: 'Unknown',
icon: '❓',
},
};
/**
* `<PrivacyBadge>` — honest indicator of where AI inference happens.
*
* Trust surface (Wave 13.C.6). Every chat / agent UI should make
* privacy state visible to the user — never hidden in settings.
*
* - `on-device` — fully local (e.g. WebLLM, ollama, Apple Intelligence)
* - `cloud` — server-side (OpenAI, Anthropic, your own infra)
* - `hybrid` — routing decides per-request (router/llm-router)
* - `unknown` — host can't determine; render rather than hide
*/
export function PrivacyBadge({
mode,
label,
detail,
iconOnly = false,
className,
style,
}: PrivacyBadgeProps) {
const tint = TINTS[mode] ?? TINTS.unknown;
const text = label ?? tint.label;
return (
<span
role="status"
data-testid="bl-privacy-badge"
data-mode={mode}
aria-label={`Privacy mode: ${text}${detail ? ` (${detail})` : ''}`}
className={className}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: iconOnly ? 0 : 6,
padding: iconOnly ? '4px' : '2px 8px',
borderRadius: 999,
backgroundColor: tint.bg,
border: `1px solid ${tint.ring}`,
color: tint.fg,
fontSize: 11,
fontWeight: 600,
letterSpacing: 0.2,
textTransform: 'uppercase',
...style,
}}
>
<span
aria-hidden="true"
style={{
display: 'inline-block',
width: 6,
height: 6,
borderRadius: 999,
backgroundColor: tint.dot,
}}
/>
{!iconOnly && (
<>
<span>{text}</span>
{detail && (
<span style={{ opacity: 0.7, fontWeight: 400, textTransform: 'none' }}>
· {detail}
</span>
)}
</>
)}
</span>
);
}