/** * Top-level event categories. Each category has retention + routing * behavior. Downstream sinks can route per category. */ type EventCategory = 'intent' | 'interaction' | 'lifecycle' | 'audit' | 'experiment' | 'custom'; interface BaseEvent { /** Stable id assigned at emit time. */ id: string; /** Schema version this event was emitted against. */ schemaVersion: string; /** Category routes downstream sink behavior. */ category: EventCategory; /** Type discriminator within the category. */ kind: string; /** Unix epoch ms at emit time on the client. */ timestamp: number; /** Multi-tenant scope, omitted in single-tenant / extension modes. */ orgId?: string; visitorId?: string; sessionId?: string; /** Per-agent-run linkage, when applicable. */ runId?: string; /** Page context at emit time. */ page?: { path: string; locale?: string; title?: string; }; /** Browser / device snapshot — populated by the SDK once per session. */ device?: { ua?: string; deviceType?: 'desktop' | 'mobile' | 'tablet' | 'tv' | 'other'; locale?: string; }; /** Category-specific shape lives in `props`. */ props?: Record; } /** * Transport layer — batching, retry, ordering — for the unified ingest * stream. * * Pluggable sink: anything implementing `EventSink` works. The * transport handles the bits that every sink would otherwise reinvent: * * - Batching: events queue in memory, flush every N ms or M events * - Retry with exponential backoff, capped at 5 attempts * - Sequence ordering: events carry a monotonic seq per session * - Page-unload flush via Beacon API * - Optional gzip body when supported */ interface EventSink { send(batch: BaseEvent[]): Promise; /** Flush any sink-internal buffers. Optional. */ flush?(): Promise; /** Tear down. Optional. */ dispose?(): Promise; } /** * Local event store — persistent, queryable, capped. * * Hosts who install dddk usually want intent data SOMEWHERE: their * own backend, dddk-console, a BI warehouse. EventStore is the * "self-hosted by default" answer — the SDK keeps a rolling local * copy of every event in the end-user's browser (IndexedDB), capped * to a sensible default that the host can lift, query, and export to * CSV / NDJSON / SQL whenever they want. * * Storage is per-end-user (IndexedDB is origin-scoped) — not a * central DB. There is no per-store quota *we* enforce beyond the * browser's; the cap below is a politeness setting so the SDK * doesn't silently consume gigabytes of a visitor's quota. * * Cap policy: * - Default 50,000 events OR 30 days, whichever fires first. * - `onFull: 'ring'` (default) deletes the oldest events to make * room. The data is lost — by design, ring buffers don't keep * history. * - `onFull: 'drop-new'` rejects incoming events instead. Useful * when the host wants to keep early-session events (e.g. for * RL training) and tolerate gaps later. * - `onFull: { notifyHost }` hands the decision to the host. The * callback can drain to backend, clear locally, raise the cap, * or whatever — return value is ignored, just side-effects. * * Set `cap: { maxEvents: Infinity, maxDays: Infinity }` to disable * the cap entirely. */ interface Cap { /** Max events to retain. `Infinity` disables the count cap. */ maxEvents: number; /** Max retention in days. `Infinity` disables the age cap. */ maxDays: number; } interface CapInfo { /** Cap values currently in effect. */ cap: Cap; /** Current store size at the moment the cap triggered. */ current: { events: number; oldestTs: number | null; }; /** The event we were about to write when the cap hit. */ pendingEvent: BaseEvent; } /** * Host callback for the `notify-host` policy. The store does NOT auto- * evict before calling — the host gets to decide. After the callback * resolves, the store does NOT retry the failing write; if the host * wants the event kept, they must explicitly do so (e.g. by calling * `store.clear()` first to make room, then re-emit). This is the * "loud" mode — chosen by hosts that hate silent data loss. */ type NotifyHostHandler = (info: CapInfo) => void | Promise; type OnFullPolicy = 'ring' | 'drop-new' | { notifyHost: NotifyHostHandler; }; interface EventStoreOpts { /** IndexedDB database name. Default `'dddk-events'`. Hosts wiring * multiple isolated stores (e.g. one per tenant inside the same * browser) should pass distinct names. */ dbName?: string; /** Retention cap. Both bounds are optional and default to 50k / 30d. * `Infinity` on either disables that bound. */ cap?: Partial; /** Behavior when the cap is exceeded. Default `'ring'`. */ onFull?: OnFullPolicy; } interface EventQuery { /** Filter by category. Single value or array (OR). */ category?: EventCategory | EventCategory[]; /** Filter by kind. Single value or array (OR). */ kind?: string | string[]; /** Timestamp lower bound (inclusive, ms-epoch). */ from?: number; /** Timestamp upper bound (inclusive, ms-epoch). */ to?: number; /** Per-id filters. */ sessionId?: string; visitorId?: string; runId?: string; /** Pagination. */ limit?: number; offset?: number; /** Sort by timestamp. Default `'desc'` (newest first). */ order?: 'asc' | 'desc'; } /** * One opened EventStore wraps an IndexedDB connection. Hosts open it * once and reuse the instance for the page's lifetime. Closing is * optional — IndexedDB closes on tab unload automatically — but * `close()` exists for hot-reload / test cleanup. */ declare class EventStore { private db; private cap; private onFull; private writeInFlight; private constructor(); /** * Open (creating if needed) the underlying IndexedDB database. * Throws if IndexedDB is unavailable (SSR / Node / locked-down * iframes). Hosts evaluating dddk in those environments should * skip EventStore wiring. */ static open(opts?: EventStoreOpts): Promise; /** * Persist one event. Cap policy fires here. Resolves to: * `{ stored: true }` — written * `{ stored: false, reason: 'full' }` — cap rejected (drop-new) * `{ stored: false, reason: 'host-rejected' }` — host's notify callback ran, * store didn't auto-evict */ write(event: BaseEvent): Promise<{ stored: boolean; reason?: 'full' | 'host-rejected'; }>; private doWrite; private handleFull; /** * Query events. Filters that have a matching IDB index are applied * server-side; the rest run as in-memory filters on the retrieved * subset. For 50k-cap stores even a full scan is sub-50ms. */ query(q?: EventQuery): Promise; /** Total count of matching events. Filter shape mirrors `query`. */ count(q?: Pick): Promise; /** Aggregate stats. Cheap — uses the timestamp index. */ size(): Promise<{ events: number; oldestTs: number | null; newestTs: number | null; }>; /** Delete every event. Cap settings stay. */ clear(): Promise; /** * Delete a subset. Returns the count of deleted events. Useful for * "after I've shipped these to the backend, drop them locally". */ drop(opts: { olderThanMs?: number; keepLast?: number; matching?: Pick; }): Promise; /** * Return an `EventSink` view of this store so it can plug straight * into `Transport`: * * ```ts * const transport = new Transport({ * sinks: [new HttpSink(...), eventStore.sink()], * }); * ``` * * Errors at write time are swallowed (logged) — the transport's * other sinks shouldn't fail just because the local store hit a * quota or was rejected by `onFull: 'drop-new'`. */ sink(): EventSink; /** Close the IDB connection. Optional; called automatically on tab unload. */ close(): void; private tx; private put; private countAll; private oldestTimestamp; private newestTimestamp; private evictOlderThan; private evictOldestN; } /** * Pre-built aggregation queries over an `EventStore`. * * Each function takes a store + time range and returns a chart-ready * shape. Hosts who want a different aggregation just call `store.query` * themselves and build their own chart from the raw events. * * Day buckets: UTC midnight. We deliberately don't try to honor the * host's locale here — the dashboard renders both buckets and labels * in UTC. Doing local-time bucketing means re-computing on every TZ * change which is more code than it saves. */ interface TimeRange { from: number; to: number; } interface DayBucket { /** Unix-epoch ms of the UTC midnight that opens the bucket. */ x: number; /** Aggregated value. */ y: number; } /** All events in the range, bucketed per UTC day. */ declare function eventsPerDay(store: EventStore, range: TimeRange): Promise; interface CountSlice { label: string; value: number; } /** * Top N most-activated palette items in the range. Reads `palette_activated` * events and groups by `props.itemId` (falling back to `props.id` / * `props.command` so we don't drop events from older emit shapes). */ declare function topPaletteItems(store: EventStore, range: TimeRange, n?: number): Promise; interface CompletionRate { started: number; completed: number; stopped: number; /** `completed / (completed + stopped)` — runs the agent actually finished. */ rate: number; } declare function agentCompletionRate(store: EventStore, range: TimeRange): Promise; interface FeedbackDistribution { satisfied: number; unsatisfied: number; /** `satisfied === null` — user pressed Esc, didn't commit either way. */ skipped: number; } declare function feedbackDistribution(store: EventStore, range: TimeRange): Promise; declare function voiceUsagePerDay(store: EventStore, range: TimeRange): Promise; declare function avgLatencyPerDay(store: EventStore, range: TimeRange): Promise; /** * Vanilla-SVG chart helpers — line / bar / donut / number tile. * * Each function returns a fresh `SVGElement` (or `HTMLElement` for the * number tile). The dashboard composes these into a grid; nothing * here knows about layout. * * No charting library — the SDK can't afford the ~70KB Chart.js * adds. SVG primitives plus 5 lines of math per chart kind covers * the six tiles we ship. Hosts who want richer charts read events * out of `EventStore.query` and bring their own library. * * Theming reads from CSS variables — `--dddk-accent` / `--dddk-bg` * etc. — so the dashboard inherits the host's theme tokens with * zero config. */ /** Compact number format: 1.2K / 3.4M / etc. Falls back to integer. */ declare function fmtCompact(n: number): string; /** Percent format, 1 decimal place. */ declare function fmtPercent(ratio: number): string; interface ChartOptions { width?: number; height?: number; /** Padding around the plot area — leaves room for axis labels. */ padding?: { top: number; right: number; bottom: number; left: number; }; /** Accent color override. Defaults to var(--dddk-accent). */ color?: string; /** Optional title above the chart. */ title?: string; /** Optional subtitle below the title (subtler weight). */ subtitle?: string; } declare function lineChart(data: Array<{ x: number; y: number; }>, opts?: ChartOptions): SVGSVGElement; declare function barChart(data: Array<{ label: string; value: number; }>, opts?: ChartOptions): SVGSVGElement; declare function donut(value: number, max: number, opts?: ChartOptions & { centerLabel?: string; }): SVGSVGElement; interface NumberTileOptions { label: string; value: string | number; hint?: string; /** Trend indicator: '+5%', '-2%', etc. Optional. */ delta?: string; deltaDirection?: 'up' | 'down' | 'flat'; } /** * A simple stat tile — big number, small label. Returns an HTMLDivElement * so the dashboard grid can lay it out with the SVG charts. */ declare function numberTile(opts: NumberTileOptions): HTMLDivElement; /** * Bundled mini-dashboard — mount-anywhere stat panel over an * `EventStore`. * * Host opens an EventStore, calls `renderDashboard(container, store)`, * and gets six charts: event volume, top palette items, agent * completion rate, feedback distribution, voice usage, average LLM * latency. The dashboard is vanilla SVG (no charting library) and * inherits the host's `--dddk-*` CSS tokens so the colors match * whatever theme is wired upstream. * * NOT a SaaS console replacement. Cross-tenant / RL trajectory * export / long-term retention stay in dddk-console (paid). This is * the "30-day single-host visibility" tier that gives an OSS user * actionable numbers without setting up backend infrastructure. */ type ChartId = 'volume' | 'palette' | 'agent-completion' | 'feedback' | 'voice' | 'latency'; declare const ALL_CHARTS: ReadonlyArray; interface DashboardOptions { /** Window of events to aggregate. Default: last 30 days. */ range?: TimeRange; /** * Which charts to render, in this order. Default: all six in the * declared order. Pass a subset to drop tiles. */ charts?: ReadonlyArray; /** Locale for built-in labels. `'en'` | `'zh-TW'`. Default `'en'`. */ locale?: 'en' | 'zh-TW'; /** * Auto-refresh interval. `0` disables polling — call `handle.refresh()` * manually from `IntentEvent` subscriptions if you want push-style * updates. Default `0`. */ refreshIntervalMs?: number; } interface DashboardHandle { /** Re-run every query + redraw every chart. */ refresh(): Promise; /** Change the time range; redraws as a side effect. */ setRange(range: TimeRange): Promise; /** Stop auto-refresh and detach DOM. */ destroy(): void; } /** * Render the dashboard into `container`. Clears the container first. * The returned handle controls refresh / range / teardown. * * Idiomatic usage: * const store = await EventStore.open(); * const handle = await renderDashboard(document.getElementById('panel')!, store); * // when host tears down: * handle.destroy(); */ declare function renderDashboard(container: HTMLElement, store: EventStore, options?: DashboardOptions): Promise; export { ALL_CHARTS, type ChartId, type CompletionRate, type CountSlice, type DashboardHandle, type DashboardOptions, type DayBucket, type FeedbackDistribution, type TimeRange, agentCompletionRate, avgLatencyPerDay, barChart, donut, eventsPerDay, feedbackDistribution, fmtCompact, fmtPercent, lineChart, numberTile, renderDashboard, topPaletteItems, voiceUsagePerDay };