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.
This commit is contained in:
saravanakumardb1 2026-05-27 16:53:03 -07:00
parent 57a09c31dd
commit ec9e11b243
5 changed files with 758 additions and 0 deletions

View File

@ -0,0 +1,259 @@
import {
Children,
cloneElement,
isValidElement,
useEffect,
useId,
useRef,
useState,
type CSSProperties,
type MouseEvent,
type ReactElement,
type ReactNode,
} from 'react';
export interface DebugOverlayPayload {
/** Stable id surfaced in the inspector — falls back to React key. */
id?: string;
/** Free-form title for the modal header. */
title?: string;
/** Arbitrary plain-data payload — rendered as syntax-light JSON. */
data: unknown;
}
export interface DebugOverlayProps {
/** What the user normally sees — wrap an AI bubble, tool card, etc. */
children: ReactNode;
/** The structured payload to reveal on Shift-click. */
payload: DebugOverlayPayload;
/**
* Modifier required to open the inspector.
* - `shift` (default) accessible, no platform-conflict
* - `alt` for cases where Shift is already used
* - `meta` on mac, Win key elsewhere
*/
modifier?: 'shift' | 'alt' | 'meta';
/** Bypass entirely (production mode). */
disabled?: boolean;
/** Optional className applied to the wrapper. */
className?: string;
style?: CSSProperties;
}
/**
* `<DebugOverlay>` Shift-click any wrapped surface to inspect its
* underlying data. Wave 13.C.5 (MAG.8).
*
* Wraps a single child element and patches its onClick. The modifier
* is configurable; production sites can pass `disabled` to render the
* child unchanged.
*
* Render JSON via a built-in `<pre>`; the host can wrap a custom
* formatter by stringifying upstream.
*/
export function DebugOverlay({
children,
payload,
modifier = 'shift',
disabled = false,
className,
style,
}: DebugOverlayProps) {
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLElement | null>(null);
// Style cursor hint on the wrapper when active.
const wrapperStyle: CSSProperties = {
display: 'inline-block',
cursor: disabled ? undefined : 'help',
...style,
};
// Patch the single child to intercept clicks.
const onWrapperClick = (e: MouseEvent) => {
if (disabled) return;
const matchMod =
modifier === 'shift'
? e.shiftKey
: modifier === 'alt'
? e.altKey
: e.metaKey || e.ctrlKey;
if (!matchMod) return;
e.preventDefault();
e.stopPropagation();
triggerRef.current = (e.currentTarget as HTMLElement) ?? null;
setOpen(true);
};
return (
<>
<span
data-testid="bl-debug-overlay-wrapper"
data-modifier={modifier}
data-disabled={disabled ? 'true' : 'false'}
className={className}
style={wrapperStyle}
onClick={onWrapperClick}
>
{patchSingleChild(children)}
</span>
{open && (
<DebugOverlayModal
payload={payload}
onClose={() => {
setOpen(false);
triggerRef.current?.focus?.();
}}
/>
)}
</>
);
}
/** Pass-through; we only intercept on the outer span. */
function patchSingleChild(children: ReactNode): ReactNode {
const arr = Children.toArray(children);
if (arr.length !== 1 || !isValidElement(arr[0])) return children;
// Keep the child untouched — the wrapper span handles the click.
return cloneElement(arr[0] as ReactElement);
}
function DebugOverlayModal({
payload,
onClose,
}: {
payload: DebugOverlayPayload;
onClose: () => void;
}) {
const titleId = useId();
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
closeRef.current?.focus();
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
onClose();
}
};
window.addEventListener('keydown', onKey);
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
window.removeEventListener('keydown', onKey);
document.body.style.overflow = prev;
};
}, [onClose]);
let pretty = '';
try {
pretty = JSON.stringify(payload.data, null, 2);
} catch {
pretty = '[unserialisable payload]';
}
return (
<div
data-testid="bl-debug-overlay-modal-root"
style={{
position: 'fixed',
inset: 0,
zIndex: 70,
display: 'grid',
placeItems: 'center',
padding: 24,
}}
>
<button
type="button"
aria-label="Close inspector"
onClick={onClose}
data-testid="bl-debug-overlay-backdrop"
style={{
position: 'absolute',
inset: 0,
background: 'rgba(0,0,0,0.55)',
backdropFilter: 'blur(4px)',
border: 0,
cursor: 'pointer',
}}
/>
<section
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
data-testid="bl-debug-overlay-modal"
style={{
position: 'relative',
width: 'min(720px, 96vw)',
maxHeight: '88vh',
display: 'flex',
flexDirection: 'column',
background: 'var(--bl-surface-card, #fff)',
color: 'var(--bl-text-primary, #111)',
borderRadius: 16,
border: '1px solid var(--bl-border, rgba(0,0,0,0.12))',
boxShadow: '0 24px 64px rgba(0,0,0,0.32)',
overflow: 'hidden',
}}
>
<header
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
borderBottom: '1px solid var(--bl-border-subtle, rgba(0,0,0,0.06))',
}}
>
<h3
id={titleId}
style={{ margin: 0, fontSize: 14, fontWeight: 600 }}
>
{payload.title ?? 'Inspector'}
{payload.id && (
<span style={{ opacity: 0.55, fontWeight: 400 }}>
{' · '}
<code style={{ fontSize: 12 }}>{payload.id}</code>
</span>
)}
</h3>
<button
ref={closeRef}
type="button"
aria-label="Close"
onClick={onClose}
style={{
border: 0,
background: 'transparent',
cursor: 'pointer',
fontSize: 18,
lineHeight: 1,
padding: 4,
color: 'var(--bl-text-secondary, #555)',
}}
>
×
</button>
</header>
<pre
data-testid="bl-debug-overlay-payload"
style={{
margin: 0,
padding: 16,
overflow: 'auto',
background: 'var(--bl-surface-muted, #f6f6f6)',
color: 'var(--bl-text-primary, #111)',
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
fontSize: 12,
lineHeight: 1.5,
whiteSpace: 'pre',
}}
>
{pretty}
</pre>
</section>
</div>
);
}

View File

@ -0,0 +1,126 @@
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>
);
}

View File

@ -0,0 +1,256 @@
import { useEffect, useId, useRef, type CSSProperties, type ReactNode } from 'react';
export interface ProvenanceEvent {
/** Stable id — used as React key. */
id: string;
/** ISO timestamp string. Renders as locale time + relative offset. */
at: string;
/** Free-form event kind label (`prompt`, `tool-call`, `model`, …). */
kind: string;
/** Plain-language one-line description. */
summary: string;
/** Optional inspector slot (JSON, code block, etc.). */
details?: ReactNode;
}
export interface ProvenanceDrawerProps {
/** Whether the drawer is open. */
open: boolean;
/** Called when the user hits Esc or clicks the overlay. */
onClose: () => void;
/** Ordered list of events — newest LAST. */
events: ProvenanceEvent[];
/** Optional title (default 'Provenance'). */
title?: string;
/** Drawer width in px. Default 420. */
width?: number;
className?: string;
style?: CSSProperties;
}
/**
* `<ProvenanceDrawer>` slide-in right drawer that lists every step the
* model + tools took to produce the current response.
*
* Trust surface (Wave 13.C.4). Backs onto whatever event log the host
* exposes (event-store, in-memory ring, etc.); the component is pure
* presentation passes the array straight through.
*
* Accessibility: role="dialog" + aria-modal, focus-traps with the
* initial focus on the close button, returns focus to the trigger on
* close (the host must manage the trigger ref).
*/
export function ProvenanceDrawer({
open,
onClose,
events,
title = 'Provenance',
width = 420,
className,
style,
}: ProvenanceDrawerProps) {
const titleId = useId();
const closeBtnRef = useRef<HTMLButtonElement>(null);
// Escape to close + initial focus on the close button.
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
onClose();
}
};
window.addEventListener('keydown', onKey);
closeBtnRef.current?.focus();
return () => window.removeEventListener('keydown', onKey);
}, [open, onClose]);
// Body scroll lock while open.
useEffect(() => {
if (!open) return;
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = prev;
};
}, [open]);
if (!open) return null;
return (
<div
data-testid="bl-provenance-drawer-root"
style={{
position: 'fixed',
inset: 0,
zIndex: 60,
...style,
}}
className={className}
>
{/* Backdrop */}
<button
type="button"
aria-label="Close provenance"
onClick={onClose}
data-testid="bl-provenance-drawer-backdrop"
style={{
position: 'absolute',
inset: 0,
background: 'rgba(0,0,0,0.42)',
backdropFilter: 'blur(2px)',
border: 0,
cursor: 'pointer',
}}
/>
<aside
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
data-testid="bl-provenance-drawer"
style={{
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
width: `min(${width}px, 96vw)`,
backgroundColor: 'var(--bl-surface-card, #fff)',
color: 'var(--bl-text-primary, #111)',
borderLeft: '1px solid var(--bl-border, rgba(0,0,0,0.12))',
boxShadow: '-12px 0 40px rgba(0,0,0,0.18)',
display: 'flex',
flexDirection: 'column',
}}
>
<header
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
borderBottom: '1px solid var(--bl-border-subtle, rgba(0,0,0,0.06))',
}}
>
<h3 id={titleId} style={{ margin: 0, fontSize: 14, fontWeight: 600 }}>
{title}{' '}
<span style={{ opacity: 0.55, fontWeight: 400 }}>
· {events.length} {events.length === 1 ? 'event' : 'events'}
</span>
</h3>
<button
ref={closeBtnRef}
type="button"
onClick={onClose}
aria-label="Close"
style={{
border: 0,
background: 'transparent',
cursor: 'pointer',
fontSize: 18,
lineHeight: 1,
padding: 4,
color: 'var(--bl-text-secondary, #555)',
}}
>
×
</button>
</header>
<div
style={{
flex: 1,
overflowY: 'auto',
padding: 12,
display: 'flex',
flexDirection: 'column',
gap: 10,
}}
>
{events.length === 0 ? (
<p
style={{
margin: 0,
fontSize: 13,
color: 'var(--bl-text-tertiary, #888)',
}}
>
No events recorded for this response.
</p>
) : (
events.map((ev) => <ProvenanceRow key={ev.id} ev={ev} />)
)}
</div>
</aside>
</div>
);
}
function ProvenanceRow({ ev }: { ev: ProvenanceEvent }) {
let when = ev.at;
try {
const d = new Date(ev.at);
if (!Number.isNaN(d.getTime())) {
when = d.toLocaleTimeString();
}
} catch {
/* fall through */
}
return (
<article
data-testid="bl-provenance-row"
data-kind={ev.kind}
style={{
border: '1px solid var(--bl-border-subtle, rgba(0,0,0,0.06))',
borderRadius: 10,
padding: 10,
backgroundColor: 'var(--bl-surface, #fafafa)',
}}
>
<header
style={{
display: 'flex',
alignItems: 'baseline',
justifyContent: 'space-between',
gap: 8,
marginBottom: 4,
}}
>
<span
style={{
fontSize: 10,
fontFamily: 'ui-monospace, SFMono-Regular, monospace',
textTransform: 'uppercase',
letterSpacing: 0.4,
color: 'var(--bl-text-tertiary, #888)',
}}
>
{ev.kind}
</span>
<time
style={{
fontSize: 11,
color: 'var(--bl-text-tertiary, #888)',
fontVariantNumeric: 'tabular-nums',
}}
dateTime={ev.at}
>
{when}
</time>
</header>
<p style={{ margin: 0, fontSize: 13, lineHeight: 1.45 }}>{ev.summary}</p>
{ev.details && (
<details
style={{
marginTop: 6,
fontSize: 12,
color: 'var(--bl-text-secondary, #555)',
}}
>
<summary style={{ cursor: 'pointer' }}>Details</summary>
<div style={{ marginTop: 6 }}>{ev.details}</div>
</details>
)}
</article>
);
}

View File

@ -129,3 +129,105 @@ describe('RefusalCard', () => {
expect(screen.getByTestId('policy-link')).toBeDefined();
});
});
// ── Wave 13.C.4-.6 ──────────────────────────────────────────────────────
import { PrivacyBadge } from '../PrivacyBadge.js';
import { ProvenanceDrawer, type ProvenanceEvent } from '../ProvenanceDrawer.js';
import { DebugOverlay } from '../DebugOverlay.js';
describe('PrivacyBadge', () => {
it('records mode + label for each of the 4 modes', () => {
const modes = ['on-device', 'cloud', 'hybrid', 'unknown'] as const;
modes.forEach((m) => {
cleanup();
render(<PrivacyBadge mode={m} />);
const el = screen.getByTestId('bl-privacy-badge');
expect(el.getAttribute('data-mode')).toBe(m);
});
});
it('renders detail line when provided', () => {
render(<PrivacyBadge mode="cloud" detail="gpt-4o-mini" />);
expect(screen.getByTestId('bl-privacy-badge').textContent).toMatch(/gpt-4o-mini/);
});
it('iconOnly variant suppresses text content', () => {
render(<PrivacyBadge mode="on-device" iconOnly />);
const el = screen.getByTestId('bl-privacy-badge');
expect(el.textContent ?? '').toBe('');
});
});
const EVENTS: ProvenanceEvent[] = [
{ id: 'e1', at: '2026-05-27T19:30:00Z', kind: 'prompt', summary: 'User asked about X' },
{ id: 'e2', at: '2026-05-27T19:30:01Z', kind: 'model', summary: 'gpt-4o picked the answer', details: <code>tokens=412</code> },
];
describe('ProvenanceDrawer', () => {
it('renders null when closed', () => {
render(<ProvenanceDrawer open={false} onClose={() => {}} events={EVENTS} />);
expect(screen.queryByTestId('bl-provenance-drawer')).toBeNull();
});
it('renders dialog + events when open', () => {
render(<ProvenanceDrawer open onClose={() => {}} events={EVENTS} />);
expect(screen.getByTestId('bl-provenance-drawer').getAttribute('aria-modal')).toBe('true');
expect(screen.getAllByTestId('bl-provenance-row')).toHaveLength(2);
});
it('calls onClose when backdrop is clicked', () => {
const onClose = vi.fn();
render(<ProvenanceDrawer open onClose={onClose} events={EVENTS} />);
fireEvent.click(screen.getByTestId('bl-provenance-drawer-backdrop'));
expect(onClose).toHaveBeenCalledOnce();
});
it('calls onClose on Escape', () => {
const onClose = vi.fn();
render(<ProvenanceDrawer open onClose={onClose} events={EVENTS} />);
fireEvent.keyDown(window, { key: 'Escape' });
expect(onClose).toHaveBeenCalled();
});
it('shows the empty-state copy when events=[]', () => {
render(<ProvenanceDrawer open onClose={() => {}} events={[]} />);
expect(screen.getByTestId('bl-provenance-drawer').textContent).toMatch(/No events/);
});
});
describe('DebugOverlay', () => {
it('does NOT open on plain click', () => {
render(
<DebugOverlay payload={{ title: 'Bubble', data: { a: 1 } }}>
<span data-testid="child">hello</span>
</DebugOverlay>,
);
fireEvent.click(screen.getByTestId('child'));
expect(screen.queryByTestId('bl-debug-overlay-modal')).toBeNull();
});
it('opens on Shift-click and shows JSON payload', () => {
render(
<DebugOverlay payload={{ id: 'm1', title: 'Bubble', data: { hello: 'world' } }}>
<span data-testid="child">hello</span>
</DebugOverlay>,
);
fireEvent.click(screen.getByTestId('bl-debug-overlay-wrapper'), { shiftKey: true });
const pre = screen.getByTestId('bl-debug-overlay-payload');
expect(pre.textContent).toMatch(/"hello": "world"/);
});
it('respects disabled — no modal opens even with Shift', () => {
render(
<DebugOverlay disabled payload={{ data: {} }}>
<span data-testid="child">hello</span>
</DebugOverlay>,
);
fireEvent.click(screen.getByTestId('bl-debug-overlay-wrapper'), { shiftKey: true });
expect(screen.queryByTestId('bl-debug-overlay-modal')).toBeNull();
});
it('alt modifier honoured when configured', () => {
render(
<DebugOverlay modifier="alt" payload={{ data: 1 }}>
<span data-testid="child">x</span>
</DebugOverlay>,
);
fireEvent.click(screen.getByTestId('bl-debug-overlay-wrapper'), { shiftKey: true });
expect(screen.queryByTestId('bl-debug-overlay-modal')).toBeNull();
fireEvent.click(screen.getByTestId('bl-debug-overlay-wrapper'), { altKey: true });
expect(screen.getByTestId('bl-debug-overlay-modal')).toBeDefined();
});
});

View File

@ -61,6 +61,21 @@ export type { ConfidenceTagProps, ConfidenceLevel } from './ConfidenceTag.js';
export { RefusalCard } from './RefusalCard.js';
export type { RefusalAction, RefusalCardProps, RefusalReason } from './RefusalCard.js';
export { PrivacyBadge } from './PrivacyBadge.js';
export type { PrivacyBadgeProps, PrivacyMode } from './PrivacyBadge.js';
export { ProvenanceDrawer } from './ProvenanceDrawer.js';
export type {
ProvenanceDrawerProps,
ProvenanceEvent,
} from './ProvenanceDrawer.js';
export { DebugOverlay } from './DebugOverlay.js';
export type {
DebugOverlayPayload,
DebugOverlayProps,
} from './DebugOverlay.js';
export type {
ChatTransportOptions,
Citation,